1212import shutil
1313import os
1414import re
15+ from dataclasses import dataclass , field
1516from pathlib import Path
1617from typing import Optional , Dict , Tuple
1718import json
1819
1920
21+ @dataclass
22+ class LaTeXCompileResult :
23+ pdf_path : Optional [Path ]
24+ summary : str = ""
25+ log_text : str = ""
26+ errors : list [str ] = field (default_factory = list )
27+ warnings : list [str ] = field (default_factory = list )
28+ return_code : Optional [int ] = None
29+ engine : str = ""
30+ generated_pdf : bool = False
31+ timed_out : bool = False
32+ log_path : Optional [Path ] = None
33+
34+
2035def _hidden_subprocess_kwargs () -> dict :
2136 if os .name != "nt" :
2237 return {}
@@ -142,38 +157,134 @@ def _extract_latex_error_message(log_text: str, tex_file: Path) -> str:
142157 return ""
143158
144159
145- def compile_tex_document (
160+ def _extract_latex_errors (log_text : str , tex_file : Path ) -> list [str ]:
161+ lines = [line .rstrip () for line in str (log_text or "" ).splitlines ()]
162+ tex_name = tex_file .name
163+ tex_path = str (tex_file )
164+ errors = []
165+
166+ for index , line in enumerate (lines ):
167+ stripped = line .strip ()
168+ if not stripped :
169+ continue
170+ if stripped .startswith ("! " ):
171+ detail = [stripped ]
172+ for extra in lines [index + 1 :index + 4 ]:
173+ extra_stripped = extra .strip ()
174+ if extra_stripped :
175+ detail .append (extra_stripped )
176+ if extra_stripped .startswith ("l." ) or extra_stripped .startswith (tex_name ) or extra_stripped .startswith (tex_path ):
177+ break
178+ errors .append (" | " .join (detail ))
179+ continue
180+ if tex_name in stripped or tex_path in stripped :
181+ if ": error:" in stripped .lower () or re .search (r":\d+:" , stripped ):
182+ detail = [stripped ]
183+ for extra in lines [index + 1 :index + 3 ]:
184+ extra_stripped = extra .strip ()
185+ if extra_stripped and not extra_stripped .startswith ("This is " ):
186+ detail .append (extra_stripped )
187+ errors .append (" | " .join (detail ))
188+
189+ seen = set ()
190+ compact = []
191+ for item in errors :
192+ cleaned = re .sub (r"\s+" , " " , item ).strip ()[:400 ]
193+ if cleaned and cleaned not in seen :
194+ seen .add (cleaned )
195+ compact .append (cleaned )
196+ return compact
197+
198+
199+ def _extract_latex_warnings (log_text : str ) -> list [str ]:
200+ warnings = []
201+ for raw_line in str (log_text or "" ).splitlines ():
202+ line = raw_line .strip ()
203+ if not line :
204+ continue
205+ lower = line .lower ()
206+ if "warning" not in lower :
207+ continue
208+ if lower .startswith ("this is " ):
209+ continue
210+ cleaned = re .sub (r"\s+" , " " , line ).strip ()[:300 ]
211+ if cleaned :
212+ warnings .append (cleaned )
213+
214+ seen = set ()
215+ unique = []
216+ for item in warnings :
217+ if item not in seen :
218+ seen .add (item )
219+ unique .append (item )
220+ return unique
221+
222+
223+ def _read_compile_log_file (log_file : Path ) -> str :
224+ try :
225+ if log_file .exists ():
226+ return log_file .read_text (encoding = "utf-8" , errors = "replace" )
227+ except Exception :
228+ pass
229+ return ""
230+
231+
232+ def _merge_compile_logs (stdout_text : str , stderr_text : str , file_log_text : str ) -> str :
233+ parts = []
234+ if stdout_text :
235+ parts .append (stdout_text .strip ())
236+ if stderr_text :
237+ parts .append (stderr_text .strip ())
238+ if file_log_text :
239+ file_log = file_log_text .strip ()
240+ if file_log and file_log not in "\n \n " .join (parts ):
241+ parts .append (file_log )
242+ return "\n \n " .join (part for part in parts if part )
243+
244+
245+ def compile_tex_document_detailed (
146246 tex_content : str ,
147247 output_dir : Path ,
148248 jobname : str = "document_preview" ,
149249 timeout : int = 25 ,
150- ) -> Tuple [ Optional [ Path ], str ] :
250+ ) -> LaTeXCompileResult :
151251 mode = get_document_render_mode ()
152252 if not is_supported_document_render_mode (mode ):
153- return None , "请先在设置中选择 LaTeX + pdflatex 或 LaTeX + xelatex。"
253+ return LaTeXCompileResult (
254+ pdf_path = None ,
255+ summary = "请先在设置中选择 LaTeX + pdflatex 或 LaTeX + xelatex。" ,
256+ )
154257
155258 latex_path = _latex_settings .get_latex_path () if _latex_settings else None
156259 latex_cmd = _resolve_latex_command_for_mode (mode , latex_path )
260+ engine_name = "xelatex" if mode == "latex_xelatex" else "pdflatex"
157261 if not latex_cmd :
158- engine_name = "xelatex" if mode == "latex_xelatex" else "pdflatex"
159- return None , f"未找到可用的 { engine_name } ,请先在设置中完成 LaTeX 路径配置。"
262+ return LaTeXCompileResult (
263+ pdf_path = None ,
264+ summary = f"未找到可用的 { engine_name } ,请先在设置中完成 LaTeX 路径配置。" ,
265+ engine = engine_name ,
266+ )
160267
161268 text = str (tex_content or "" ).strip ()
162269 if not text :
163- return None , "当前没有可编译的 TeX 文档内容。"
270+ return LaTeXCompileResult (
271+ pdf_path = None ,
272+ summary = "当前没有可编译的 TeX 文档内容。" ,
273+ engine = engine_name ,
274+ )
164275
165276 output_path = Path (output_dir )
166277 output_path .mkdir (parents = True , exist_ok = True )
167278 tex_file = output_path / f"{ jobname } .tex"
168279 pdf_file = output_path / f"{ jobname } .pdf"
280+ log_file = output_path / f"{ jobname } .log"
169281 tex_file .write_text (text , encoding = "utf-8" )
170282
171283 try :
172284 result = subprocess .run (
173285 [
174286 latex_cmd ,
175287 "-interaction=nonstopmode" ,
176- "-halt-on-error" ,
177288 "-file-line-error" ,
178289 "-synctex=1" ,
179290 "-output-directory" ,
@@ -188,19 +299,73 @@ def compile_tex_document(
188299 cwd = str (output_path ),
189300 ** _hidden_subprocess_kwargs (),
190301 )
191- except subprocess .TimeoutExpired :
192- return None , "TeX 文档编译超时,请检查内容或 LaTeX 环境。"
302+ file_log_text = _read_compile_log_file (log_file )
303+ log_text = _merge_compile_logs (result .stdout , result .stderr , file_log_text )
304+ errors = _extract_latex_errors (log_text , tex_file )
305+ warnings = _extract_latex_warnings (log_text )
306+ generated_pdf = pdf_file .exists ()
307+ if generated_pdf and errors :
308+ summary = "编译存在错误,已尽量生成 PDF。请查看下方编译日志。"
309+ elif generated_pdf and warnings :
310+ summary = "编译完成,但存在警告;请查看下方编译日志。"
311+ elif generated_pdf :
312+ summary = ""
313+ else :
314+ cleaned = _extract_latex_error_message (log_text , tex_file )
315+ if cleaned :
316+ cleaned = re .sub (r"\s+" , " " , cleaned ).strip ()[:320 ]
317+ summary = cleaned or "TeX 文档编译失败,请检查源码和 LaTeX 环境。"
318+ return LaTeXCompileResult (
319+ pdf_path = pdf_file if generated_pdf else None ,
320+ summary = summary ,
321+ log_text = log_text ,
322+ errors = errors ,
323+ warnings = warnings ,
324+ return_code = int (result .returncode ),
325+ engine = engine_name ,
326+ generated_pdf = generated_pdf ,
327+ log_path = log_file if log_file .exists () else None ,
328+ )
329+ except subprocess .TimeoutExpired as exc :
330+ file_log_text = _read_compile_log_file (log_file )
331+ log_text = _merge_compile_logs (
332+ getattr (exc , "stdout" , "" ) or "" ,
333+ getattr (exc , "stderr" , "" ) or "" ,
334+ file_log_text ,
335+ )
336+ return LaTeXCompileResult (
337+ pdf_path = pdf_file if pdf_file .exists () else None ,
338+ summary = "TeX 文档编译超时,请检查内容或 LaTeX 环境。" ,
339+ log_text = log_text ,
340+ errors = _extract_latex_errors (log_text , tex_file ),
341+ warnings = _extract_latex_warnings (log_text ),
342+ engine = engine_name ,
343+ generated_pdf = pdf_file .exists (),
344+ timed_out = True ,
345+ log_path = log_file if log_file .exists () else None ,
346+ )
193347 except Exception as exc :
194- return None , f"TeX 文档编译失败: { exc } "
348+ return LaTeXCompileResult (
349+ pdf_path = None ,
350+ summary = f"TeX 文档编译失败: { exc } " ,
351+ engine = engine_name ,
352+ log_path = log_file if log_file .exists () else None ,
353+ )
195354
196- if result .returncode != 0 or not pdf_file .exists ():
197- log_text = "\n " .join (filter (None , [result .stdout , result .stderr ]))
198- cleaned = _extract_latex_error_message (log_text , tex_file )
199- if cleaned :
200- cleaned = re .sub (r"\s+" , " " , cleaned ).strip ()[:320 ]
201- return None , cleaned or "TeX 文档编译失败,请检查源码和 LaTeX 环境。"
202355
203- return pdf_file , ""
356+ def compile_tex_document (
357+ tex_content : str ,
358+ output_dir : Path ,
359+ jobname : str = "document_preview" ,
360+ timeout : int = 25 ,
361+ ) -> Tuple [Optional [Path ], str ]:
362+ result = compile_tex_document_detailed (
363+ tex_content = tex_content ,
364+ output_dir = output_dir ,
365+ jobname = jobname ,
366+ timeout = timeout ,
367+ )
368+ return result .pdf_path if result .generated_pdf else None , result .summary
204369
205370
206371def synctex_edit_from_pdf (
0 commit comments