texsmith 0.0.2.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- texsmith/__init__.py +107 -0
- texsmith/_alias.py +59 -0
- texsmith/adapters/__init__.py +6 -0
- texsmith/adapters/docker.py +258 -0
- texsmith/adapters/handlers/__init__.py +3 -0
- texsmith/adapters/handlers/_assets.py +363 -0
- texsmith/adapters/handlers/_helpers.py +62 -0
- texsmith/adapters/handlers/_mermaid.py +122 -0
- texsmith/adapters/handlers/admonitions.py +267 -0
- texsmith/adapters/handlers/basic.py +228 -0
- texsmith/adapters/handlers/blocks.py +851 -0
- texsmith/adapters/handlers/code.py +309 -0
- texsmith/adapters/handlers/inline.py +937 -0
- texsmith/adapters/handlers/links.py +239 -0
- texsmith/adapters/handlers/media.py +380 -0
- texsmith/adapters/latex/__init__.py +10 -0
- texsmith/adapters/latex/engines/__init__.py +726 -0
- texsmith/adapters/latex/engines/latex/__init__.py +26 -0
- texsmith/adapters/latex/engines/latex/log.py +720 -0
- texsmith/adapters/latex/engines/latex/runner.py +28 -0
- texsmith/adapters/latex/engines/tectonic/__init__.py +38 -0
- texsmith/adapters/latex/formatter.py +297 -0
- texsmith/adapters/latex/latexmk.py +148 -0
- texsmith/adapters/latex/partials/acronym.tex +1 -0
- texsmith/adapters/latex/partials/add.tex +1 -0
- texsmith/adapters/latex/partials/addition.tex +1 -0
- texsmith/adapters/latex/partials/blockquote.tex +3 -0
- texsmith/adapters/latex/partials/callout.tex +3 -0
- texsmith/adapters/latex/partials/choices.tex +5 -0
- texsmith/adapters/latex/partials/citation.tex +1 -0
- texsmith/adapters/latex/partials/codeblock.tex +7 -0
- texsmith/adapters/latex/partials/codeblock_listings.tex +5 -0
- texsmith/adapters/latex/partials/codeblock_pygments.tex +5 -0
- texsmith/adapters/latex/partials/codeblock_verbatim.tex +12 -0
- texsmith/adapters/latex/partials/codeinline.tex +1 -0
- texsmith/adapters/latex/partials/codeinlinett.tex +1 -0
- texsmith/adapters/latex/partials/comment.tex +1 -0
- texsmith/adapters/latex/partials/del.tex +1 -0
- texsmith/adapters/latex/partials/deletion.tex +1 -0
- texsmith/adapters/latex/partials/description_list.tex +5 -0
- texsmith/adapters/latex/partials/enquote.tex +1 -0
- texsmith/adapters/latex/partials/epigraph.tex +1 -0
- texsmith/adapters/latex/partials/exercises_solutions.tex +7 -0
- texsmith/adapters/latex/partials/figure.tex +33 -0
- texsmith/adapters/latex/partials/figure_tcolorbox.tex +15 -0
- texsmith/adapters/latex/partials/footnote.tex +3 -0
- texsmith/adapters/latex/partials/glossary.tex +1 -0
- texsmith/adapters/latex/partials/heading.tex +14 -0
- texsmith/adapters/latex/partials/highlight.tex +25 -0
- texsmith/adapters/latex/partials/horizontal_rule.tex +1 -0
- texsmith/adapters/latex/partials/href.tex +1 -0
- texsmith/adapters/latex/partials/icon.tex +1 -0
- texsmith/adapters/latex/partials/include.tex +1 -0
- texsmith/adapters/latex/partials/index.tex +1 -0
- texsmith/adapters/latex/partials/italic.tex +1 -0
- texsmith/adapters/latex/partials/keystroke.tex +46 -0
- texsmith/adapters/latex/partials/label.tex +1 -0
- texsmith/adapters/latex/partials/list_acronyms.tex +5 -0
- texsmith/adapters/latex/partials/list_glossary.tex +8 -0
- texsmith/adapters/latex/partials/multicolumn.tex +3 -0
- texsmith/adapters/latex/partials/ordered_list.tex +5 -0
- texsmith/adapters/latex/partials/pagestyle.tex +1 -0
- texsmith/adapters/latex/partials/ref.tex +1 -0
- texsmith/adapters/latex/partials/regex.tex +1 -0
- texsmith/adapters/latex/partials/smallcaps.tex +1 -0
- texsmith/adapters/latex/partials/strikethrough.tex +1 -0
- texsmith/adapters/latex/partials/strong.tex +1 -0
- texsmith/adapters/latex/partials/subscript.tex +1 -0
- texsmith/adapters/latex/partials/substitution.tex +1 -0
- texsmith/adapters/latex/partials/superscript.tex +1 -0
- texsmith/adapters/latex/partials/tabbed.tex +3 -0
- texsmith/adapters/latex/partials/table.tex +48 -0
- texsmith/adapters/latex/partials/underline.tex +1 -0
- texsmith/adapters/latex/partials/unordered_list.tex +5 -0
- texsmith/adapters/latex/partials/url.tex +1 -0
- texsmith/adapters/latex/pygments.py +98 -0
- texsmith/adapters/latex/pyxindy.py +64 -0
- texsmith/adapters/latex/renderer.py +220 -0
- texsmith/adapters/latex/tectonic.py +366 -0
- texsmith/adapters/latex/utils.py +86 -0
- texsmith/adapters/markdown/__init__.py +342 -0
- texsmith/adapters/plugins/__init__.py +8 -0
- texsmith/adapters/plugins/material.py +174 -0
- texsmith/adapters/plugins/snippet.py +1734 -0
- texsmith/adapters/transformers/__init__.py +122 -0
- texsmith/adapters/transformers/base.py +140 -0
- texsmith/adapters/transformers/strategies.py +1456 -0
- texsmith/adapters/transformers/utils.py +59 -0
- texsmith/api/__init__.py +80 -0
- texsmith/api/_utils.py +56 -0
- texsmith/api/document.py +645 -0
- texsmith/api/pipeline.py +229 -0
- texsmith/api/service.py +648 -0
- texsmith/api/templates.py +287 -0
- texsmith/core/__init__.py +6 -0
- texsmith/core/bibliography/__init__.py +57 -0
- texsmith/core/bibliography/collection.py +354 -0
- texsmith/core/bibliography/doi.py +194 -0
- texsmith/core/bibliography/issues.py +15 -0
- texsmith/core/bibliography/parsing.py +45 -0
- texsmith/core/callouts.py +99 -0
- texsmith/core/config.py +191 -0
- texsmith/core/context.py +242 -0
- texsmith/core/conversion/__init__.py +103 -0
- texsmith/core/conversion/core.py +780 -0
- texsmith/core/conversion/debug.py +87 -0
- texsmith/core/conversion/inputs.py +474 -0
- texsmith/core/conversion/renderer.py +578 -0
- texsmith/core/conversion/templates.py +646 -0
- texsmith/core/conversion_contexts.py +95 -0
- texsmith/core/diagnostics.py +118 -0
- texsmith/core/exceptions.py +41 -0
- texsmith/core/fonts/__init__.py +3 -0
- texsmith/core/fragments/__init__.py +716 -0
- texsmith/core/fragments/base.py +117 -0
- texsmith/core/metadata.py +280 -0
- texsmith/core/mustache.py +86 -0
- texsmith/core/partials.py +20 -0
- texsmith/core/rules.py +394 -0
- texsmith/core/templates/__init__.py +58 -0
- texsmith/core/templates/base.py +343 -0
- texsmith/core/templates/builtins.py +68 -0
- texsmith/core/templates/context_usage.py +137 -0
- texsmith/core/templates/loader.py +303 -0
- texsmith/core/templates/manifest.py +861 -0
- texsmith/core/templates/runtime.py +347 -0
- texsmith/core/templates/text.py +14 -0
- texsmith/core/templates/wrapper.py +340 -0
- texsmith/core/user_dir.py +179 -0
- texsmith/devtools.py +28 -0
- texsmith/extensions/__init__.py +162 -0
- texsmith/extensions/index/__init__.py +21 -0
- texsmith/extensions/index/markdown.py +94 -0
- texsmith/extensions/index/mkdocs_plugin.py +136 -0
- texsmith/extensions/index/registry.py +57 -0
- texsmith/extensions/index/renderer.py +183 -0
- texsmith/extensions/index/templates/index.tex +1 -0
- texsmith/extensions/latex_raw.py +115 -0
- texsmith/extensions/latex_text.py +117 -0
- texsmith/extensions/mermaid.py +267 -0
- texsmith/extensions/missing_footnotes.py +125 -0
- texsmith/extensions/multi_citations.py +54 -0
- texsmith/extensions/progressbar/__init__.py +9 -0
- texsmith/extensions/progressbar/markdown.py +212 -0
- texsmith/extensions/progressbar/renderer.py +117 -0
- texsmith/extensions/smallcaps.py +48 -0
- texsmith/extensions/texlogos/__init__.py +10 -0
- texsmith/extensions/texlogos/markdown.py +236 -0
- texsmith/extensions/texlogos/renderer.py +81 -0
- texsmith/extensions/texlogos/specs.py +66 -0
- texsmith/fonts/__init__.py +57 -0
- texsmith/fonts/cache.py +46 -0
- texsmith/fonts/constants.py +60 -0
- texsmith/fonts/coverage.py +275 -0
- texsmith/fonts/downloader.py +103 -0
- texsmith/fonts/fallback.py +438 -0
- texsmith/fonts/html_scripts.py +168 -0
- texsmith/fonts/logging.py +127 -0
- texsmith/fonts/pipeline.py +338 -0
- texsmith/fonts/scripts.py +493 -0
- texsmith/fonts/ucharclasses.py +164 -0
- texsmith/fragments/__init__.py +11 -0
- texsmith/fragments/bibliography/__init__.py +65 -0
- texsmith/fragments/bibliography/fragment.toml +3 -0
- texsmith/fragments/bibliography/ts-bibliography-backmatter.jinja.tex +35 -0
- texsmith/fragments/bibliography/ts-bibliography.jinja.tex +10 -0
- texsmith/fragments/callouts/__init__.py +88 -0
- texsmith/fragments/callouts/fragment.toml +3 -0
- texsmith/fragments/callouts/ts-callouts.jinja.sty +129 -0
- texsmith/fragments/code/__init__.py +82 -0
- texsmith/fragments/code/fragment.toml +3 -0
- texsmith/fragments/code/ts-code.jinja.sty +140 -0
- texsmith/fragments/extra/__init__.py +180 -0
- texsmith/fragments/extra/fragment.toml +3 -0
- texsmith/fragments/extra/ts-extra.jinja.tex +13 -0
- texsmith/fragments/fonts/__init__.py +880 -0
- texsmith/fragments/fonts/fragment.toml +3 -0
- texsmith/fragments/fonts/ts-fonts.jinja.sty +292 -0
- texsmith/fragments/frame/__init__.py +170 -0
- texsmith/fragments/frame/fragment.toml +3 -0
- texsmith/fragments/frame/ts-frame.tex.jinja +49 -0
- texsmith/fragments/geometry/__init__.py +169 -0
- texsmith/fragments/geometry/fragment.toml +3 -0
- texsmith/fragments/geometry/paper.py +526 -0
- texsmith/fragments/geometry/ts_geometry.tex.jinja +53 -0
- texsmith/fragments/glossary/__init__.py +67 -0
- texsmith/fragments/glossary/fragment.toml +3 -0
- texsmith/fragments/glossary/ts-glossary-backmatter.jinja.tex +5 -0
- texsmith/fragments/glossary/ts-glossary.jinja.sty +28 -0
- texsmith/fragments/index/__init__.py +66 -0
- texsmith/fragments/index/fragment.toml +3 -0
- texsmith/fragments/index/ts-index-backmatter.jinja.tex +3 -0
- texsmith/fragments/index/ts-index.jinja.sty +12 -0
- texsmith/fragments/keystrokes/__init__.py +69 -0
- texsmith/fragments/keystrokes/fragment.toml +3 -0
- texsmith/fragments/keystrokes/ts-keystrokes.jinja.sty +20 -0
- texsmith/fragments/todolist/__init__.py +71 -0
- texsmith/fragments/todolist/fragment.toml +3 -0
- texsmith/fragments/todolist/ts-todolist.jinja.sty +21 -0
- texsmith/fragments/typesetting/__init__.py +214 -0
- texsmith/fragments/typesetting/fragment.toml +3 -0
- texsmith/fragments/typesetting/ts-typesetting.tex.jinja +81 -0
- texsmith/index.py +26 -0
- texsmith/plugins/__init__.py +14 -0
- texsmith/progressbar.py +8 -0
- texsmith/quotes.py +40 -0
- texsmith/smart_dashes.py +66 -0
- texsmith/templates/__init__.py +3 -0
- texsmith/templates/article/README.md +33 -0
- texsmith/templates/article/__init__.py +281 -0
- texsmith/templates/article/template/manifest.toml +125 -0
- texsmith/templates/article/template/mermaid-config.json +18 -0
- texsmith/templates/article/template/template.tex +74 -0
- texsmith/templates/book/README.md +26 -0
- texsmith/templates/book/__init__.py +75 -0
- texsmith/templates/book/overrides/codeblock.tex +7 -0
- texsmith/templates/book/overrides/codeinline.tex +1 -0
- texsmith/templates/book/template/fixtoc.sty +81 -0
- texsmith/templates/book/template/manifest.toml +198 -0
- texsmith/templates/book/template/template.tex +314 -0
- texsmith/templates/common/__init__.py +1 -0
- texsmith/templates/common/latexmkrc +46 -0
- texsmith/templates/letter/README.md +59 -0
- texsmith/templates/letter/__init__.py +495 -0
- texsmith/templates/letter/demo.md +31 -0
- texsmith/templates/letter/fonts/modernline bold.otf +0 -0
- texsmith/templates/letter/fonts/modernline.otf +0 -0
- texsmith/templates/letter/manifest.toml +197 -0
- texsmith/templates/letter/template/callouts.jinja.sty +290 -0
- texsmith/templates/letter/template/template.tex +123 -0
- texsmith/templates/snippet/README.md +21 -0
- texsmith/templates/snippet/__init__.py +80 -0
- texsmith/templates/snippet/template/manifest.toml +66 -0
- texsmith/templates/snippet/template/template.tex +47 -0
- texsmith/texlogos.py +15 -0
- texsmith/ui/__init__.py +6 -0
- texsmith/ui/cli/__init__.py +22 -0
- texsmith/ui/cli/_options.py +338 -0
- texsmith/ui/cli/app.py +65 -0
- texsmith/ui/cli/bibliography.py +300 -0
- texsmith/ui/cli/commands/__init__.py +14 -0
- texsmith/ui/cli/commands/render.py +1128 -0
- texsmith/ui/cli/commands/templates.py +397 -0
- texsmith/ui/cli/diagnostics.py +36 -0
- texsmith/ui/cli/presenter.py +663 -0
- texsmith/ui/cli/state.py +263 -0
- texsmith/ui/cli/utils.py +235 -0
- texsmith-0.0.2.dev0.dist-info/METADATA +187 -0
- texsmith-0.0.2.dev0.dist-info/RECORD +252 -0
- texsmith-0.0.2.dev0.dist-info/WHEEL +4 -0
- texsmith-0.0.2.dev0.dist-info/entry_points.txt +22 -0
- texsmith-0.0.2.dev0.dist-info/licenses/LICENSE.md +21 -0
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
"""Pluggable LaTeX engine helpers (latexmk, Tectonic, log parsing)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
import io
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
from typing import Any, Literal
|
|
13
|
+
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
|
|
16
|
+
from texsmith.core.context import DocumentState
|
|
17
|
+
from texsmith.core.user_dir import get_user_dir
|
|
18
|
+
|
|
19
|
+
from ..latexmk import (
|
|
20
|
+
LatexmkEngine,
|
|
21
|
+
build_engine_command as build_pdflatex_command,
|
|
22
|
+
latexmk_pdf_flag,
|
|
23
|
+
normalise_engine_command,
|
|
24
|
+
normalise_index_engine,
|
|
25
|
+
)
|
|
26
|
+
from ..pyxindy import (
|
|
27
|
+
glossary_command_tokens as pyxindy_glossary_tokens,
|
|
28
|
+
index_command_tokens as pyxindy_index_tokens,
|
|
29
|
+
is_available as pyxindy_available,
|
|
30
|
+
)
|
|
31
|
+
from ..tectonic import select_makeglossaries
|
|
32
|
+
from .latex import (
|
|
33
|
+
LatexLogParser,
|
|
34
|
+
LatexMessage,
|
|
35
|
+
LatexMessageSeverity,
|
|
36
|
+
LatexStreamResult,
|
|
37
|
+
parse_latex_log,
|
|
38
|
+
run_latex_engine,
|
|
39
|
+
)
|
|
40
|
+
from .tectonic import run_tectonic_engine
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
EngineBackend = Literal["tectonic", "latexmk"]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(slots=True)
|
|
47
|
+
class EngineChoice:
|
|
48
|
+
"""Resolved backend and latexmk program selection."""
|
|
49
|
+
|
|
50
|
+
backend: EngineBackend
|
|
51
|
+
latexmk_engine: str | None
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def label(self) -> str:
|
|
55
|
+
return "tectonic" if self.backend == "tectonic" else "latexmk"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(slots=True)
|
|
59
|
+
class EngineFeatures:
|
|
60
|
+
"""Feature flags that influence engine configuration."""
|
|
61
|
+
|
|
62
|
+
requires_shell_escape: bool
|
|
63
|
+
bibliography: bool
|
|
64
|
+
has_index: bool
|
|
65
|
+
has_glossary: bool
|
|
66
|
+
index_engine: str | None = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(slots=True)
|
|
70
|
+
class EngineCommand:
|
|
71
|
+
"""Executable command plus metadata."""
|
|
72
|
+
|
|
73
|
+
argv: list[str]
|
|
74
|
+
log_path: Path
|
|
75
|
+
pdf_path: Path
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(slots=True)
|
|
79
|
+
class EngineResult:
|
|
80
|
+
"""Outcome of a LaTeX build run."""
|
|
81
|
+
|
|
82
|
+
returncode: int
|
|
83
|
+
messages: list[LatexMessage]
|
|
84
|
+
command: list[str]
|
|
85
|
+
log_path: Path
|
|
86
|
+
pdf_path: Path
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
_TECTONIC_RERUN_TOKENS = (
|
|
90
|
+
"rerun to get cross-references right",
|
|
91
|
+
"rerun to get cross references right",
|
|
92
|
+
"label(s) may have changed",
|
|
93
|
+
"there were undefined references",
|
|
94
|
+
"there were undefined citations",
|
|
95
|
+
"package rerunfilecheck warning",
|
|
96
|
+
"please rerun",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def resolve_engine(preference: str | None, template_engine: str | None) -> EngineChoice:
|
|
101
|
+
"""Resolve the engine backend and latexmk program to use."""
|
|
102
|
+
candidate = preference.strip().lower() if preference else ""
|
|
103
|
+
|
|
104
|
+
if candidate == "tectonic":
|
|
105
|
+
return EngineChoice(backend="tectonic", latexmk_engine=None)
|
|
106
|
+
|
|
107
|
+
if candidate in {"xelatex", "lualatex", "pdflatex"}:
|
|
108
|
+
return EngineChoice(backend="latexmk", latexmk_engine=candidate)
|
|
109
|
+
|
|
110
|
+
engine_value = template_engine if candidate == "template" else preference or template_engine
|
|
111
|
+
|
|
112
|
+
return EngineChoice(backend="latexmk", latexmk_engine=engine_value)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def compute_features(
|
|
116
|
+
*,
|
|
117
|
+
requires_shell_escape: bool,
|
|
118
|
+
bibliography: bool,
|
|
119
|
+
document_state: DocumentState | None,
|
|
120
|
+
template_context: Mapping[str, Any] | None,
|
|
121
|
+
) -> EngineFeatures:
|
|
122
|
+
"""Derive engine feature flags from render metadata."""
|
|
123
|
+
has_index = bool(
|
|
124
|
+
getattr(document_state, "has_index_entries", False)
|
|
125
|
+
or getattr(document_state, "index_entries", [])
|
|
126
|
+
)
|
|
127
|
+
has_glossary = bool(
|
|
128
|
+
getattr(document_state, "acronyms", {}) or getattr(document_state, "glossary", {})
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
index_engine: str | None = None
|
|
132
|
+
if template_context:
|
|
133
|
+
raw_engine = template_context.get("index_engine")
|
|
134
|
+
if isinstance(raw_engine, str):
|
|
135
|
+
candidate = raw_engine.strip()
|
|
136
|
+
if candidate:
|
|
137
|
+
index_engine = candidate
|
|
138
|
+
|
|
139
|
+
return EngineFeatures(
|
|
140
|
+
requires_shell_escape=requires_shell_escape,
|
|
141
|
+
bibliography=bibliography,
|
|
142
|
+
has_index=has_index,
|
|
143
|
+
has_glossary=has_glossary,
|
|
144
|
+
index_engine=index_engine,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def missing_dependencies(
|
|
149
|
+
choice: EngineChoice,
|
|
150
|
+
features: EngineFeatures,
|
|
151
|
+
*,
|
|
152
|
+
use_system_tectonic: bool = False,
|
|
153
|
+
available_binaries: Mapping[str, str | Path] | None = None,
|
|
154
|
+
) -> list[str]:
|
|
155
|
+
"""Return missing executables required by the selected engine."""
|
|
156
|
+
missing: list[str] = []
|
|
157
|
+
|
|
158
|
+
def _check(binary: str) -> None:
|
|
159
|
+
if available_binaries and binary in available_binaries:
|
|
160
|
+
candidate = Path(available_binaries[binary])
|
|
161
|
+
if candidate.exists():
|
|
162
|
+
return
|
|
163
|
+
if shutil.which(binary) is None:
|
|
164
|
+
missing.append(binary)
|
|
165
|
+
|
|
166
|
+
if choice.backend == "tectonic":
|
|
167
|
+
if use_system_tectonic:
|
|
168
|
+
_check("tectonic")
|
|
169
|
+
else:
|
|
170
|
+
_check("latexmk")
|
|
171
|
+
engine_config = normalise_engine_command(
|
|
172
|
+
choice.latexmk_engine, shell_escape=features.requires_shell_escape
|
|
173
|
+
)
|
|
174
|
+
if engine_config.command:
|
|
175
|
+
_check(engine_config.command[0])
|
|
176
|
+
|
|
177
|
+
if features.bibliography:
|
|
178
|
+
_check("biber")
|
|
179
|
+
if features.has_index:
|
|
180
|
+
index_engine = normalise_index_engine(features.index_engine)
|
|
181
|
+
if index_engine == "pyxindy":
|
|
182
|
+
if not pyxindy_available():
|
|
183
|
+
missing.append("pyxindy")
|
|
184
|
+
else:
|
|
185
|
+
_check("texindy" if index_engine == "texindy" else "makeindex")
|
|
186
|
+
if features.has_glossary and not pyxindy_available():
|
|
187
|
+
_check("makeglossaries")
|
|
188
|
+
|
|
189
|
+
return missing
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def build_engine_command(
|
|
193
|
+
choice: EngineChoice,
|
|
194
|
+
features: EngineFeatures,
|
|
195
|
+
*,
|
|
196
|
+
main_tex_path: Path,
|
|
197
|
+
tectonic_binary: str | Path | None = None,
|
|
198
|
+
) -> EngineCommand:
|
|
199
|
+
"""Construct the command to compile the LaTeX document."""
|
|
200
|
+
if choice.backend == "tectonic":
|
|
201
|
+
if tectonic_binary is None:
|
|
202
|
+
binary = "tectonic"
|
|
203
|
+
else:
|
|
204
|
+
binary_path = Path(tectonic_binary)
|
|
205
|
+
if not binary_path.drive and binary_path.anchor in {"/", "\\"}:
|
|
206
|
+
binary = binary_path.as_posix()
|
|
207
|
+
else:
|
|
208
|
+
binary = os.fspath(binary_path)
|
|
209
|
+
argv = [
|
|
210
|
+
binary,
|
|
211
|
+
"-X",
|
|
212
|
+
"compile",
|
|
213
|
+
main_tex_path.name,
|
|
214
|
+
"--keep-logs",
|
|
215
|
+
"--keep-intermediates",
|
|
216
|
+
"--synctex",
|
|
217
|
+
"--outdir",
|
|
218
|
+
".",
|
|
219
|
+
]
|
|
220
|
+
log_path = main_tex_path.with_suffix(".log")
|
|
221
|
+
pdf_path = main_tex_path.with_suffix(".pdf")
|
|
222
|
+
return EngineCommand(argv=argv, log_path=log_path, pdf_path=pdf_path)
|
|
223
|
+
|
|
224
|
+
engine_config = normalise_engine_command(
|
|
225
|
+
choice.latexmk_engine, shell_escape=features.requires_shell_escape
|
|
226
|
+
)
|
|
227
|
+
command = [
|
|
228
|
+
"latexmk",
|
|
229
|
+
latexmk_pdf_flag(engine_config.pdf_mode),
|
|
230
|
+
"-interaction=nonstopmode",
|
|
231
|
+
"-halt-on-error",
|
|
232
|
+
"-file-line-error",
|
|
233
|
+
f"-pdflatex={build_pdflatex_command(engine_config)}",
|
|
234
|
+
main_tex_path.name,
|
|
235
|
+
]
|
|
236
|
+
if features.bibliography:
|
|
237
|
+
command.insert(2, "-bibtex")
|
|
238
|
+
|
|
239
|
+
log_path = main_tex_path.with_suffix(".log")
|
|
240
|
+
pdf_path = main_tex_path.with_suffix(".pdf")
|
|
241
|
+
return EngineCommand(argv=command, log_path=log_path, pdf_path=pdf_path)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _looks_like_path(binary: str) -> bool:
|
|
245
|
+
"""Return True when ``binary`` already encodes a filesystem path."""
|
|
246
|
+
if Path(binary).is_absolute():
|
|
247
|
+
return True
|
|
248
|
+
separators = [os.sep]
|
|
249
|
+
if os.altsep:
|
|
250
|
+
separators.append(os.altsep)
|
|
251
|
+
return any(sep and sep in binary for sep in separators)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def ensure_command_paths(command: EngineCommand) -> EngineCommand:
|
|
255
|
+
"""Resolve the primary binary path for the generated command."""
|
|
256
|
+
argv = list(command.argv)
|
|
257
|
+
if argv:
|
|
258
|
+
binary = str(argv[0])
|
|
259
|
+
if not _looks_like_path(binary):
|
|
260
|
+
resolved = shutil.which(binary)
|
|
261
|
+
if resolved:
|
|
262
|
+
argv[0] = resolved
|
|
263
|
+
return EngineCommand(argv=argv, log_path=command.log_path, pdf_path=command.pdf_path)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def build_tex_env(
|
|
267
|
+
render_dir: Path,
|
|
268
|
+
*,
|
|
269
|
+
isolate_cache: bool,
|
|
270
|
+
extra_path: Path | None = None,
|
|
271
|
+
biber_path: Path | None = None,
|
|
272
|
+
) -> dict[str, str]:
|
|
273
|
+
"""Construct TeX cache environment variables (shared by all engines)."""
|
|
274
|
+
if isolate_cache:
|
|
275
|
+
tex_cache_root = (render_dir / ".texmf-cache").resolve()
|
|
276
|
+
else:
|
|
277
|
+
tex_cache_root = get_user_dir().cache_dir("texmf")
|
|
278
|
+
|
|
279
|
+
tex_cache_root.mkdir(parents=True, exist_ok=True)
|
|
280
|
+
env = os.environ.copy()
|
|
281
|
+
|
|
282
|
+
texmf_home = tex_cache_root / "texmf-home"
|
|
283
|
+
texmf_var = tex_cache_root / "texmf-var"
|
|
284
|
+
luatex_cache = tex_cache_root / "luatex-cache"
|
|
285
|
+
|
|
286
|
+
texmf_cache = tex_cache_root / "texmf-cache"
|
|
287
|
+
texmf_config = tex_cache_root / "texmf-config"
|
|
288
|
+
xdg_cache = tex_cache_root / "xdg-cache"
|
|
289
|
+
tectonic_cache = tex_cache_root / "tectonic-cache"
|
|
290
|
+
|
|
291
|
+
for cache_path in (
|
|
292
|
+
texmf_home,
|
|
293
|
+
texmf_var,
|
|
294
|
+
texmf_config,
|
|
295
|
+
luatex_cache,
|
|
296
|
+
texmf_cache,
|
|
297
|
+
xdg_cache,
|
|
298
|
+
tectonic_cache,
|
|
299
|
+
):
|
|
300
|
+
cache_path.mkdir(parents=True, exist_ok=True)
|
|
301
|
+
|
|
302
|
+
if extra_path:
|
|
303
|
+
env["PATH"] = f"{extra_path}{os.pathsep}{env.get('PATH', '')}"
|
|
304
|
+
|
|
305
|
+
env["TEXMFHOME"] = str(texmf_home)
|
|
306
|
+
env["TEXMFVAR"] = str(texmf_var)
|
|
307
|
+
env["TEXMFCONFIG"] = str(texmf_config)
|
|
308
|
+
env["LUATEXCACHE"] = str(luatex_cache)
|
|
309
|
+
env["LUAOTFLOAD_CACHE"] = str(luatex_cache)
|
|
310
|
+
env["TEXMFCACHE"] = str(texmf_cache)
|
|
311
|
+
env.setdefault("XDG_CACHE_HOME", str(xdg_cache))
|
|
312
|
+
env["TECTONIC_CACHE_DIR"] = str(tectonic_cache)
|
|
313
|
+
if biber_path:
|
|
314
|
+
env["BIBER"] = str(biber_path)
|
|
315
|
+
return env
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def run_engine_command(
|
|
319
|
+
command: EngineCommand,
|
|
320
|
+
*,
|
|
321
|
+
backend: EngineBackend,
|
|
322
|
+
workdir: Path,
|
|
323
|
+
env: Mapping[str, str],
|
|
324
|
+
console: Console | None,
|
|
325
|
+
verbosity: int = 0,
|
|
326
|
+
classic_output: bool = False,
|
|
327
|
+
features: EngineFeatures | None = None,
|
|
328
|
+
rerun_limit: int = 5,
|
|
329
|
+
) -> EngineResult:
|
|
330
|
+
"""Execute the engine command, streaming logs when requested."""
|
|
331
|
+
argv = command.argv
|
|
332
|
+
log_path = command.log_path
|
|
333
|
+
console = console or Console(file=io.StringIO())
|
|
334
|
+
|
|
335
|
+
if backend == "tectonic" and features is not None:
|
|
336
|
+
return _run_tectonic_build(
|
|
337
|
+
command,
|
|
338
|
+
features,
|
|
339
|
+
workdir=workdir,
|
|
340
|
+
env=env,
|
|
341
|
+
console=console,
|
|
342
|
+
classic_output=classic_output,
|
|
343
|
+
rerun_limit=rerun_limit,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
if classic_output:
|
|
347
|
+
process = subprocess.run(
|
|
348
|
+
argv,
|
|
349
|
+
check=False,
|
|
350
|
+
capture_output=True,
|
|
351
|
+
text=True,
|
|
352
|
+
cwd=workdir,
|
|
353
|
+
env=dict(env),
|
|
354
|
+
)
|
|
355
|
+
if process.stdout:
|
|
356
|
+
console.print(process.stdout.rstrip())
|
|
357
|
+
if process.stderr:
|
|
358
|
+
console.print(process.stderr.rstrip())
|
|
359
|
+
messages: list[LatexMessage] = []
|
|
360
|
+
if process.returncode != 0:
|
|
361
|
+
messages = parse_latex_log(log_path)
|
|
362
|
+
return EngineResult(
|
|
363
|
+
returncode=process.returncode,
|
|
364
|
+
messages=messages,
|
|
365
|
+
command=argv,
|
|
366
|
+
log_path=log_path,
|
|
367
|
+
pdf_path=command.pdf_path,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
if backend == "latexmk":
|
|
371
|
+
result: LatexStreamResult = run_latex_engine(
|
|
372
|
+
argv,
|
|
373
|
+
workdir=workdir,
|
|
374
|
+
env=env,
|
|
375
|
+
console=console,
|
|
376
|
+
verbosity=verbosity,
|
|
377
|
+
)
|
|
378
|
+
else:
|
|
379
|
+
result = run_tectonic_engine(
|
|
380
|
+
argv,
|
|
381
|
+
workdir=workdir,
|
|
382
|
+
env=env,
|
|
383
|
+
console=console,
|
|
384
|
+
)
|
|
385
|
+
return _stream_result_to_engine_result(result, command)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _stream_result_to_engine_result(
|
|
389
|
+
result: LatexStreamResult, command: EngineCommand
|
|
390
|
+
) -> EngineResult:
|
|
391
|
+
log_path = command.log_path
|
|
392
|
+
messages = result.messages if result.returncode != 0 else result.messages or []
|
|
393
|
+
if result.returncode != 0 and not messages:
|
|
394
|
+
messages = parse_latex_log(log_path)
|
|
395
|
+
return EngineResult(
|
|
396
|
+
returncode=result.returncode,
|
|
397
|
+
messages=messages,
|
|
398
|
+
command=command.argv,
|
|
399
|
+
log_path=log_path,
|
|
400
|
+
pdf_path=command.pdf_path,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
@dataclass(slots=True)
|
|
405
|
+
class _AuxiliaryToolRun:
|
|
406
|
+
returncode: int
|
|
407
|
+
stdout: str
|
|
408
|
+
stderr: str
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _invoke_auxiliary_tool(
|
|
412
|
+
label: str,
|
|
413
|
+
argv: Sequence[str],
|
|
414
|
+
*,
|
|
415
|
+
workdir: Path,
|
|
416
|
+
env: Mapping[str, str],
|
|
417
|
+
console: Console,
|
|
418
|
+
) -> _AuxiliaryToolRun:
|
|
419
|
+
console.print(f"[cyan]Running {label}…[/]")
|
|
420
|
+
try:
|
|
421
|
+
process = subprocess.run(
|
|
422
|
+
list(argv),
|
|
423
|
+
check=False,
|
|
424
|
+
capture_output=True,
|
|
425
|
+
text=True,
|
|
426
|
+
cwd=workdir,
|
|
427
|
+
env=dict(env),
|
|
428
|
+
)
|
|
429
|
+
stdout = process.stdout or ""
|
|
430
|
+
stderr = process.stderr or ""
|
|
431
|
+
except OSError as exc:
|
|
432
|
+
stdout = ""
|
|
433
|
+
stderr = str(exc)
|
|
434
|
+
console.print(stderr)
|
|
435
|
+
return _AuxiliaryToolRun(returncode=1, stdout=stdout, stderr=stderr)
|
|
436
|
+
|
|
437
|
+
if stdout:
|
|
438
|
+
console.print(stdout.rstrip())
|
|
439
|
+
if stderr:
|
|
440
|
+
console.print(stderr.rstrip())
|
|
441
|
+
return _AuxiliaryToolRun(
|
|
442
|
+
returncode=process.returncode,
|
|
443
|
+
stdout=stdout,
|
|
444
|
+
stderr=stderr,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _tool_failure_result(
|
|
449
|
+
tool: str,
|
|
450
|
+
run: _AuxiliaryToolRun,
|
|
451
|
+
*,
|
|
452
|
+
command: EngineCommand,
|
|
453
|
+
) -> EngineResult:
|
|
454
|
+
summary = f"{tool} failed with status {run.returncode}"
|
|
455
|
+
details = [segment for segment in (run.stdout.strip(), run.stderr.strip()) if segment]
|
|
456
|
+
message = LatexMessage(
|
|
457
|
+
severity=LatexMessageSeverity.ERROR,
|
|
458
|
+
summary=summary,
|
|
459
|
+
details=details,
|
|
460
|
+
)
|
|
461
|
+
return EngineResult(
|
|
462
|
+
returncode=run.returncode or 1,
|
|
463
|
+
messages=[message],
|
|
464
|
+
command=command.argv,
|
|
465
|
+
log_path=command.log_path,
|
|
466
|
+
pdf_path=command.pdf_path,
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _run_tectonic_build(
|
|
471
|
+
command: EngineCommand,
|
|
472
|
+
features: EngineFeatures,
|
|
473
|
+
*,
|
|
474
|
+
workdir: Path,
|
|
475
|
+
env: Mapping[str, str],
|
|
476
|
+
console: Console,
|
|
477
|
+
classic_output: bool,
|
|
478
|
+
rerun_limit: int,
|
|
479
|
+
) -> EngineResult:
|
|
480
|
+
job_stem = command.log_path.with_suffix("").name
|
|
481
|
+
index_engine = normalise_index_engine(features.index_engine) if features.has_index else None
|
|
482
|
+
rerun_target = max(1, rerun_limit)
|
|
483
|
+
ran_biber = False
|
|
484
|
+
ran_index = False
|
|
485
|
+
ran_glossary = False
|
|
486
|
+
|
|
487
|
+
last_result: LatexStreamResult | None = None
|
|
488
|
+
|
|
489
|
+
for pass_number in range(1, rerun_target + 1):
|
|
490
|
+
result = _run_single_tectonic_pass(
|
|
491
|
+
command,
|
|
492
|
+
workdir=workdir,
|
|
493
|
+
env=env,
|
|
494
|
+
console=console,
|
|
495
|
+
classic_output=classic_output,
|
|
496
|
+
)
|
|
497
|
+
last_result = result
|
|
498
|
+
if result.returncode != 0:
|
|
499
|
+
return _stream_result_to_engine_result(result, command)
|
|
500
|
+
|
|
501
|
+
forced_rerun = False
|
|
502
|
+
|
|
503
|
+
if features.bibliography and not ran_biber:
|
|
504
|
+
ran, failure = _maybe_run_biber(
|
|
505
|
+
job_stem,
|
|
506
|
+
workdir=workdir,
|
|
507
|
+
env=env,
|
|
508
|
+
console=console,
|
|
509
|
+
command=command,
|
|
510
|
+
)
|
|
511
|
+
if failure is not None:
|
|
512
|
+
return failure
|
|
513
|
+
if ran:
|
|
514
|
+
ran_biber = True
|
|
515
|
+
forced_rerun = True
|
|
516
|
+
|
|
517
|
+
if features.has_index and not ran_index and index_engine:
|
|
518
|
+
ran, failure = _maybe_run_index(
|
|
519
|
+
job_stem,
|
|
520
|
+
engine_name=index_engine,
|
|
521
|
+
workdir=workdir,
|
|
522
|
+
env=env,
|
|
523
|
+
console=console,
|
|
524
|
+
command=command,
|
|
525
|
+
)
|
|
526
|
+
if failure is not None:
|
|
527
|
+
return failure
|
|
528
|
+
if ran:
|
|
529
|
+
ran_index = True
|
|
530
|
+
forced_rerun = True
|
|
531
|
+
|
|
532
|
+
if features.has_glossary and not ran_glossary:
|
|
533
|
+
ran, failure = _maybe_run_glossaries(
|
|
534
|
+
job_stem,
|
|
535
|
+
workdir=workdir,
|
|
536
|
+
env=env,
|
|
537
|
+
console=console,
|
|
538
|
+
command=command,
|
|
539
|
+
)
|
|
540
|
+
if failure is not None:
|
|
541
|
+
return failure
|
|
542
|
+
if ran:
|
|
543
|
+
ran_glossary = True
|
|
544
|
+
forced_rerun = True
|
|
545
|
+
|
|
546
|
+
needs_rerun = forced_rerun or _log_requests_rerun(command.log_path)
|
|
547
|
+
if not needs_rerun:
|
|
548
|
+
break
|
|
549
|
+
|
|
550
|
+
if pass_number == rerun_target:
|
|
551
|
+
message = LatexMessage(
|
|
552
|
+
severity=LatexMessageSeverity.ERROR,
|
|
553
|
+
summary=f"Tectonic did not resolve references after {rerun_target} passes",
|
|
554
|
+
)
|
|
555
|
+
return EngineResult(
|
|
556
|
+
returncode=1,
|
|
557
|
+
messages=[message],
|
|
558
|
+
command=command.argv,
|
|
559
|
+
log_path=command.log_path,
|
|
560
|
+
pdf_path=command.pdf_path,
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
assert last_result is not None
|
|
564
|
+
return _stream_result_to_engine_result(last_result, command)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _run_single_tectonic_pass(
|
|
568
|
+
command: EngineCommand,
|
|
569
|
+
*,
|
|
570
|
+
workdir: Path,
|
|
571
|
+
env: Mapping[str, str],
|
|
572
|
+
console: Console,
|
|
573
|
+
classic_output: bool,
|
|
574
|
+
) -> LatexStreamResult:
|
|
575
|
+
if classic_output:
|
|
576
|
+
process = subprocess.run(
|
|
577
|
+
command.argv,
|
|
578
|
+
check=False,
|
|
579
|
+
capture_output=True,
|
|
580
|
+
text=True,
|
|
581
|
+
cwd=workdir,
|
|
582
|
+
env=dict(env),
|
|
583
|
+
)
|
|
584
|
+
if process.stdout:
|
|
585
|
+
console.print(process.stdout.rstrip())
|
|
586
|
+
if process.stderr:
|
|
587
|
+
console.print(process.stderr.rstrip())
|
|
588
|
+
return LatexStreamResult(returncode=process.returncode, messages=[])
|
|
589
|
+
return run_tectonic_engine(
|
|
590
|
+
command.argv,
|
|
591
|
+
workdir=workdir,
|
|
592
|
+
env=env,
|
|
593
|
+
console=console,
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _maybe_run_biber(
|
|
598
|
+
job_stem: str,
|
|
599
|
+
*,
|
|
600
|
+
workdir: Path,
|
|
601
|
+
env: Mapping[str, str],
|
|
602
|
+
console: Console,
|
|
603
|
+
command: EngineCommand,
|
|
604
|
+
) -> tuple[bool, EngineResult | None]:
|
|
605
|
+
bcf_path = workdir / f"{job_stem}.bcf"
|
|
606
|
+
if not bcf_path.exists():
|
|
607
|
+
return False, None
|
|
608
|
+
run = _invoke_auxiliary_tool(
|
|
609
|
+
"biber",
|
|
610
|
+
["biber", job_stem],
|
|
611
|
+
workdir=workdir,
|
|
612
|
+
env=env,
|
|
613
|
+
console=console,
|
|
614
|
+
)
|
|
615
|
+
if run.returncode != 0:
|
|
616
|
+
return True, _tool_failure_result("biber", run, command=command)
|
|
617
|
+
return True, None
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def _maybe_run_index(
|
|
621
|
+
job_stem: str,
|
|
622
|
+
*,
|
|
623
|
+
engine_name: str,
|
|
624
|
+
workdir: Path,
|
|
625
|
+
env: Mapping[str, str],
|
|
626
|
+
console: Console,
|
|
627
|
+
command: EngineCommand,
|
|
628
|
+
) -> tuple[bool, EngineResult | None]:
|
|
629
|
+
index_path = workdir / f"{job_stem}.idx"
|
|
630
|
+
if not index_path.exists():
|
|
631
|
+
return False, None
|
|
632
|
+
argv = [*_index_command_tokens(engine_name), index_path.name]
|
|
633
|
+
run = _invoke_auxiliary_tool(
|
|
634
|
+
argv[0],
|
|
635
|
+
argv,
|
|
636
|
+
workdir=workdir,
|
|
637
|
+
env=env,
|
|
638
|
+
console=console,
|
|
639
|
+
)
|
|
640
|
+
if run.returncode != 0:
|
|
641
|
+
return True, _tool_failure_result(argv[0], run, command=command)
|
|
642
|
+
return True, None
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _maybe_run_glossaries(
|
|
646
|
+
job_stem: str,
|
|
647
|
+
*,
|
|
648
|
+
workdir: Path,
|
|
649
|
+
env: Mapping[str, str],
|
|
650
|
+
console: Console,
|
|
651
|
+
command: EngineCommand,
|
|
652
|
+
) -> tuple[bool, EngineResult | None]:
|
|
653
|
+
glo_path = workdir / f"{job_stem}.glo"
|
|
654
|
+
acn_path = workdir / f"{job_stem}.acn"
|
|
655
|
+
if not glo_path.exists() and not acn_path.exists():
|
|
656
|
+
return False, None
|
|
657
|
+
argv = [*_glossaries_command_tokens(), job_stem]
|
|
658
|
+
run = _invoke_auxiliary_tool(
|
|
659
|
+
argv[0],
|
|
660
|
+
argv,
|
|
661
|
+
workdir=workdir,
|
|
662
|
+
env=env,
|
|
663
|
+
console=console,
|
|
664
|
+
)
|
|
665
|
+
if run.returncode != 0:
|
|
666
|
+
return True, _tool_failure_result(argv[0], run, command=command)
|
|
667
|
+
return True, None
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def _index_command_tokens(engine_name: str) -> list[str]:
|
|
671
|
+
"""Select an index command based on the requested engine."""
|
|
672
|
+
if engine_name == "pyxindy":
|
|
673
|
+
return pyxindy_index_tokens()
|
|
674
|
+
if engine_name == "texindy":
|
|
675
|
+
return ["texindy"]
|
|
676
|
+
return [engine_name]
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def _glossaries_command_tokens() -> list[str]:
|
|
680
|
+
"""Return the preferred makeglossaries command."""
|
|
681
|
+
if pyxindy_available():
|
|
682
|
+
return pyxindy_glossary_tokens()
|
|
683
|
+
|
|
684
|
+
helper: Path | None = None
|
|
685
|
+
try:
|
|
686
|
+
helper = select_makeglossaries(console=None).path
|
|
687
|
+
except Exception:
|
|
688
|
+
helper = None
|
|
689
|
+
|
|
690
|
+
if helper and helper.exists():
|
|
691
|
+
return [str(helper)]
|
|
692
|
+
return ["makeglossaries"]
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def _log_requests_rerun(log_path: Path) -> bool:
|
|
696
|
+
try:
|
|
697
|
+
with log_path.open("r", encoding="utf-8", errors="ignore") as handle:
|
|
698
|
+
for line in handle:
|
|
699
|
+
lower = line.lower()
|
|
700
|
+
if "rerunfilecheck" in lower:
|
|
701
|
+
continue
|
|
702
|
+
if any(token in lower for token in _TECTONIC_RERUN_TOKENS):
|
|
703
|
+
return True
|
|
704
|
+
except FileNotFoundError:
|
|
705
|
+
return False
|
|
706
|
+
return False
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
__all__ = [
|
|
710
|
+
"EngineChoice",
|
|
711
|
+
"EngineCommand",
|
|
712
|
+
"EngineFeatures",
|
|
713
|
+
"EngineResult",
|
|
714
|
+
"LatexLogParser",
|
|
715
|
+
"LatexMessage",
|
|
716
|
+
"LatexMessageSeverity",
|
|
717
|
+
"LatexStreamResult",
|
|
718
|
+
"build_engine_command",
|
|
719
|
+
"build_tex_env",
|
|
720
|
+
"compute_features",
|
|
721
|
+
"ensure_command_paths",
|
|
722
|
+
"missing_dependencies",
|
|
723
|
+
"parse_latex_log",
|
|
724
|
+
"resolve_engine",
|
|
725
|
+
"run_engine_command",
|
|
726
|
+
]
|