Skip to content

Commit fa88c1e

Browse files
Merge pull request #79 from SakuraMathcraft/tauri-client
Tauri client
2 parents 6aa695b + 15a0b47 commit fa88c1e

11 files changed

Lines changed: 1385 additions & 78 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ tauri_stderr.log
3737

3838
# Release notes (local docs, do not version)
3939
RELEASE_NOTES*.md
40+
docs/
4041

4142
# Android signing
4243
*.jks

readme.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
![Forks](https://img.shields.io/github/forks/SakuraMathcraft/LaTeXSnipper?style=flat-square&label=Forks&color=1f6feb)
1010
![Issues](https://img.shields.io/github/issues/SakuraMathcraft/LaTeXSnipper?style=flat-square&label=Issues&color=d1481e)
1111
![License](https://img.shields.io/badge/license-MIT-blue?style=flat-square)
12-
![Version](https://img.shields.io/badge/version-v2.3-brightgreen?style=flat-square)
12+
![Version](https://img.shields.io/badge/version-v2.3.1-brightgreen?style=flat-square)
1313
![Platform](https://img.shields.io/badge/platform-Windows-orange?style=flat-square)
1414
![Python](https://img.shields.io/badge/python-3.11-blue?style=flat-square)
1515

@@ -56,7 +56,7 @@ The `v2.0` Math Workbench supports a complete workflow:
5656

5757
<img width="1919" height="1014" alt="v2 3深色" src="https://github.com/user-attachments/assets/c6dffd39-26d9-4e54-aba9-a4b010d3603e" />
5858

59-
The `v2.3` Auto Typesetting Document Window supports source-level editing with synchronized preview:
59+
The `v2.3.1` Auto Typesetting Document Window supports source-level editing with synchronized preview:
6060

6161
1. Open "Auto Typesetting" from the handwriting window
6262
2. Edit full source in the left `TeX Document` pane
@@ -124,7 +124,7 @@ For heavy expressions, the engine uses automatic fallback:
124124
### Option 1: Download the executable
125125

126126
1. Visit the [Releases page](https://github.com/SakuraMathcraft/LaTeXSnipper/releases)
127-
2. Download the latest `LaTeXSnipper_setup_v2.3.exe`
127+
2. Download the latest `LaTeXSnipper_setup_v2.3.1.exe`
128128
3. Run the installer
129129
4. Complete environment setup via the dependency wizard on first launch
130130
5. Start capturing, handwriting, or using the math workbench

src/backend/latex_renderer.py

Lines changed: 182 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,26 @@
1212
import shutil
1313
import os
1414
import re
15+
from dataclasses import dataclass, field
1516
from pathlib import Path
1617
from typing import Optional, Dict, Tuple
1718
import 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+
2035
def _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

206371
def synctex_edit_from_pdf(

src/deps_bootstrap.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,7 @@ def _fix_critical_versions(pyexe: str, log_fn=None, use_mirror: bool = False):
832832
import lxml
833833
# BASIC 仅验证非 GUI 运行依赖
834834
import onnxruntime as onnxruntime
835+
import argostranslate
835836
print("BASIC OK")
836837
""",
837838
"CORE": """
@@ -1160,6 +1161,7 @@ def _repair_torch_stack(
11601161
"pillow~=11.0.0", "pyperclip~=1.11.0", "packaging~=26.0",
11611162
"requests~=2.32.5",
11621163
"numpy>=1.26.4", "filelock~=3.13.1",
1164+
"argostranslate~=1.9.6",
11631165
"pydantic~=2.9.2", "regex~=2024.9.11",
11641166
"safetensors~=0.6.2", "sentencepiece~=0.1.99",
11651167
"certifi~=2024.2.2", "idna~=3.6", "urllib3~=2.5.0",

0 commit comments

Comments
 (0)