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,1128 @@
|
|
|
1
|
+
"""Implementation of the primary ``texsmith`` CLI command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import atexit
|
|
6
|
+
from collections.abc import Iterable, Mapping
|
|
7
|
+
import contextlib
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
from typing import Annotated, Any
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
from click.core import ParameterSource
|
|
18
|
+
import typer
|
|
19
|
+
|
|
20
|
+
from texsmith.adapters.latex.engines import (
|
|
21
|
+
EngineResult,
|
|
22
|
+
build_engine_command as build_latex_engine_command,
|
|
23
|
+
build_tex_env,
|
|
24
|
+
compute_features,
|
|
25
|
+
ensure_command_paths,
|
|
26
|
+
missing_dependencies,
|
|
27
|
+
parse_latex_log,
|
|
28
|
+
resolve_engine,
|
|
29
|
+
run_engine_command,
|
|
30
|
+
)
|
|
31
|
+
from texsmith.adapters.latex.pyxindy import is_available as pyxindy_available
|
|
32
|
+
from texsmith.adapters.latex.tectonic import (
|
|
33
|
+
BiberAcquisitionError,
|
|
34
|
+
MakeglossariesAcquisitionError,
|
|
35
|
+
TectonicAcquisitionError,
|
|
36
|
+
select_biber_binary,
|
|
37
|
+
select_makeglossaries,
|
|
38
|
+
select_tectonic_binary,
|
|
39
|
+
)
|
|
40
|
+
from texsmith.adapters.markdown import (
|
|
41
|
+
DEFAULT_MARKDOWN_EXTENSIONS,
|
|
42
|
+
resolve_markdown_extensions,
|
|
43
|
+
split_front_matter,
|
|
44
|
+
)
|
|
45
|
+
from texsmith.api.service import ConversionRequest, ConversionService
|
|
46
|
+
from texsmith.core.bibliography import BibliographyCollection
|
|
47
|
+
from texsmith.core.conversion.debug import ConversionError
|
|
48
|
+
from texsmith.core.conversion.inputs import UnsupportedInputError
|
|
49
|
+
from texsmith.core.metadata import PressMetadataError, normalise_press_metadata
|
|
50
|
+
from texsmith.core.templates import TemplateError
|
|
51
|
+
from texsmith.core.templates.runtime import coerce_base_level
|
|
52
|
+
from texsmith.fonts.html_scripts import wrap_scripts_in_html
|
|
53
|
+
|
|
54
|
+
from .._options import (
|
|
55
|
+
DIAGNOSTICS_PANEL,
|
|
56
|
+
OUTPUT_PANEL,
|
|
57
|
+
BaseLevelOption,
|
|
58
|
+
ConvertAssetsOption,
|
|
59
|
+
DebugHtmlOption,
|
|
60
|
+
DebugRulesOption,
|
|
61
|
+
DisableFallbackOption,
|
|
62
|
+
DisableFragmentOption,
|
|
63
|
+
DisableMarkdownExtensionsOption,
|
|
64
|
+
EnableFragmentOption,
|
|
65
|
+
FontsInfoOption,
|
|
66
|
+
FullDocumentOption,
|
|
67
|
+
HashAssetsOption,
|
|
68
|
+
HtmlOnlyOption,
|
|
69
|
+
InputPathArgument,
|
|
70
|
+
LanguageOption,
|
|
71
|
+
MakefileDepsOption,
|
|
72
|
+
ManifestOptionWithShort,
|
|
73
|
+
MarkdownExtensionsOption,
|
|
74
|
+
NoCopyAssetsOption,
|
|
75
|
+
NoPromoteTitleOption,
|
|
76
|
+
NoTitleOption,
|
|
77
|
+
OpenLogOption,
|
|
78
|
+
OutputPathOption,
|
|
79
|
+
ParserOption,
|
|
80
|
+
SelectorOption,
|
|
81
|
+
SlotsOption,
|
|
82
|
+
StripHeadingOption,
|
|
83
|
+
TemplateAttributeOption,
|
|
84
|
+
TemplateInfoOption,
|
|
85
|
+
TemplateOption,
|
|
86
|
+
)
|
|
87
|
+
from ..bibliography import print_bibliography_overview
|
|
88
|
+
from ..commands.templates import list_templates, scaffold_template, show_template_info
|
|
89
|
+
from ..diagnostics import CliEmitter
|
|
90
|
+
from ..presenter import (
|
|
91
|
+
consume_event_diagnostics,
|
|
92
|
+
present_build_summary,
|
|
93
|
+
present_context_attributes,
|
|
94
|
+
present_conversion_summary,
|
|
95
|
+
present_fonts_info,
|
|
96
|
+
present_html_summary,
|
|
97
|
+
present_latex_failure,
|
|
98
|
+
present_rule_descriptions,
|
|
99
|
+
)
|
|
100
|
+
from ..state import debug_enabled, emit_error, set_cli_state
|
|
101
|
+
from ..utils import determine_output_target, organise_slot_overrides, write_output_file
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
_SERVICE = ConversionService()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
_MARKDOWN_SUFFIXES = {
|
|
108
|
+
".md",
|
|
109
|
+
".markdown",
|
|
110
|
+
".mdown",
|
|
111
|
+
".mkd",
|
|
112
|
+
".mkdown",
|
|
113
|
+
".mdtxt",
|
|
114
|
+
".text",
|
|
115
|
+
".yml",
|
|
116
|
+
".yaml",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _cleanup_temp_input(path: Path) -> None:
|
|
121
|
+
"""Remove the temporary input file if it exists.
|
|
122
|
+
|
|
123
|
+
This cleanup step prevents filesystem clutter when the user pipes content
|
|
124
|
+
via stdin, ensuring that temporary files don't accumulate over time.
|
|
125
|
+
"""
|
|
126
|
+
with contextlib.suppress(OSError):
|
|
127
|
+
path.unlink(missing_ok=True)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _read_stdin_document() -> Path | None:
|
|
131
|
+
"""Write stdin content to a temporary Markdown file when piped."""
|
|
132
|
+
stream = sys.stdin
|
|
133
|
+
if stream is None or stream.closed:
|
|
134
|
+
return None
|
|
135
|
+
try:
|
|
136
|
+
if stream.isatty():
|
|
137
|
+
return None
|
|
138
|
+
except (AttributeError, ValueError):
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
payload = stream.read()
|
|
142
|
+
if not payload:
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
with tempfile.NamedTemporaryFile(
|
|
146
|
+
mode="w",
|
|
147
|
+
suffix=".md",
|
|
148
|
+
prefix="texsmith-stdin-",
|
|
149
|
+
encoding="utf-8",
|
|
150
|
+
delete=False,
|
|
151
|
+
) as handle:
|
|
152
|
+
handle.write(payload)
|
|
153
|
+
temp_path = Path(handle.name)
|
|
154
|
+
atexit.register(_cleanup_temp_input, temp_path)
|
|
155
|
+
return temp_path
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _load_front_matter(path: Path) -> Mapping[str, Any] | None:
|
|
159
|
+
"""Return parsed Markdown front matter when available."""
|
|
160
|
+
suffix = path.suffix.lower()
|
|
161
|
+
if suffix not in _MARKDOWN_SUFFIXES:
|
|
162
|
+
return None
|
|
163
|
+
try:
|
|
164
|
+
metadata, _ = split_front_matter(path.read_text(encoding="utf-8"))
|
|
165
|
+
except OSError:
|
|
166
|
+
return None
|
|
167
|
+
return metadata if isinstance(metadata, dict) else {}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _extract_press_template(metadata: Mapping[str, Any] | None) -> str | None:
|
|
171
|
+
"""Extract the template identifier declared in front matter."""
|
|
172
|
+
if not isinstance(metadata, Mapping):
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
payload = dict(metadata)
|
|
176
|
+
with contextlib.suppress(PressMetadataError):
|
|
177
|
+
normalise_press_metadata(payload)
|
|
178
|
+
|
|
179
|
+
template_value = payload.get("template")
|
|
180
|
+
if isinstance(template_value, str) and (candidate := template_value.strip()):
|
|
181
|
+
return candidate
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _format_path_for_event(path: Path) -> str:
|
|
186
|
+
try:
|
|
187
|
+
resolved = path.resolve()
|
|
188
|
+
except OSError:
|
|
189
|
+
resolved = path
|
|
190
|
+
try:
|
|
191
|
+
return str(resolved.relative_to(Path.cwd()))
|
|
192
|
+
except ValueError:
|
|
193
|
+
return str(resolved)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _relativize_path(path: Path, base: Path) -> Path:
|
|
197
|
+
"""Return a path relative to ``base`` when possible."""
|
|
198
|
+
try:
|
|
199
|
+
return path.resolve().relative_to(base)
|
|
200
|
+
except ValueError:
|
|
201
|
+
try:
|
|
202
|
+
return Path(os.path.relpath(path, base))
|
|
203
|
+
except ValueError:
|
|
204
|
+
return path.resolve()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _escape_make_path(path: Path) -> str:
|
|
208
|
+
"""Escape whitespace and backslashes for Makefile dependency entries."""
|
|
209
|
+
raw = path.as_posix()
|
|
210
|
+
raw = raw.replace("\\", "\\\\")
|
|
211
|
+
return raw.replace(" ", "\\ ")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _write_makefile_deps(target: Path, dependencies: Iterable[Path]) -> Path:
|
|
215
|
+
"""Write a Makefile-compatible .d file for the given target."""
|
|
216
|
+
base = Path.cwd()
|
|
217
|
+
resolved_target = target.resolve()
|
|
218
|
+
dep_path = resolved_target.with_suffix(resolved_target.suffix + ".d")
|
|
219
|
+
dep_path.parent.mkdir(parents=True, exist_ok=True)
|
|
220
|
+
|
|
221
|
+
seen: set[Path] = set()
|
|
222
|
+
normalised: list[Path] = []
|
|
223
|
+
for dep in dependencies:
|
|
224
|
+
if not dep:
|
|
225
|
+
continue
|
|
226
|
+
try:
|
|
227
|
+
resolved = Path(dep).resolve()
|
|
228
|
+
except OSError:
|
|
229
|
+
continue
|
|
230
|
+
if resolved == resolved_target or resolved in seen:
|
|
231
|
+
continue
|
|
232
|
+
if not resolved.exists():
|
|
233
|
+
continue
|
|
234
|
+
seen.add(resolved)
|
|
235
|
+
normalised.append(resolved)
|
|
236
|
+
|
|
237
|
+
normalised = sorted(normalised, key=lambda path: path.as_posix())
|
|
238
|
+
rel_target = _escape_make_path(_relativize_path(resolved_target, base))
|
|
239
|
+
rel_deps = [_escape_make_path(_relativize_path(dep, base)) for dep in normalised]
|
|
240
|
+
content = f"{rel_target}: {' '.join(rel_deps)}\n" if rel_deps else f"{rel_target}:\n"
|
|
241
|
+
dep_path.write_text(content, encoding="utf-8")
|
|
242
|
+
return dep_path
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _coerce_attribute_value(raw: str) -> Any:
|
|
246
|
+
"""Infer the type of a template attribute value from its string representation.
|
|
247
|
+
|
|
248
|
+
This bridges the gap between string-only CLI arguments and the typed configuration
|
|
249
|
+
expected by templates, allowing users to pass booleans and numbers naturally.
|
|
250
|
+
"""
|
|
251
|
+
candidate = raw.strip()
|
|
252
|
+
lowered = candidate.lower()
|
|
253
|
+
if lowered in {"true", "false"}:
|
|
254
|
+
return lowered == "true"
|
|
255
|
+
try:
|
|
256
|
+
if candidate.startswith(("0x", "0X")):
|
|
257
|
+
return int(candidate, 16)
|
|
258
|
+
return int(candidate)
|
|
259
|
+
except ValueError:
|
|
260
|
+
pass
|
|
261
|
+
try:
|
|
262
|
+
return float(candidate)
|
|
263
|
+
except ValueError:
|
|
264
|
+
pass
|
|
265
|
+
return candidate
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _assign_nested_value(target: dict[str, Any], path: list[str], value: Any) -> None:
|
|
269
|
+
"""Set a value in a nested dictionary structure using a list of keys.
|
|
270
|
+
|
|
271
|
+
This enables dot-notation configuration (e.g. `theme.color=red`) for complex
|
|
272
|
+
template settings, allowing deep overrides from the flat CLI interface.
|
|
273
|
+
"""
|
|
274
|
+
cursor = target
|
|
275
|
+
for key in path[:-1]:
|
|
276
|
+
if key not in cursor:
|
|
277
|
+
cursor[key] = {}
|
|
278
|
+
elif not isinstance(cursor[key], dict):
|
|
279
|
+
raise typer.BadParameter(
|
|
280
|
+
f"Invalid attribute override for '{'.'.join(path)}', "
|
|
281
|
+
f"'{key}' is already assigned to a non-mapping value."
|
|
282
|
+
)
|
|
283
|
+
cursor = cursor[key] # type: ignore[assignment]
|
|
284
|
+
cursor[path[-1]] = value
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _parse_template_attributes(values: Iterable[str] | None) -> dict[str, Any]:
|
|
288
|
+
"""Parse a list of key=value strings into a dictionary of attributes.
|
|
289
|
+
|
|
290
|
+
This transforms the flat list of CLI arguments into a structured configuration
|
|
291
|
+
dictionary that can be merged with the template's default settings.
|
|
292
|
+
"""
|
|
293
|
+
overrides: dict[str, Any] = {}
|
|
294
|
+
if not values:
|
|
295
|
+
return overrides
|
|
296
|
+
for raw in values:
|
|
297
|
+
if not isinstance(raw, str) or not raw.strip():
|
|
298
|
+
continue
|
|
299
|
+
if "=" not in raw:
|
|
300
|
+
raise typer.BadParameter(f"Invalid attribute override '{raw}', expected key=value.")
|
|
301
|
+
key, value = raw.split("=", 1)
|
|
302
|
+
key = key.strip()
|
|
303
|
+
if not key:
|
|
304
|
+
raise typer.BadParameter(f"Invalid attribute override '{raw}', empty key.")
|
|
305
|
+
parts = [chunk for chunk in key.split(".") if chunk]
|
|
306
|
+
if not parts:
|
|
307
|
+
raise typer.BadParameter(f"Invalid attribute override '{raw}', empty key.")
|
|
308
|
+
coerced = _coerce_attribute_value(value)
|
|
309
|
+
if len(parts) == 1:
|
|
310
|
+
overrides[parts[0]] = coerced
|
|
311
|
+
else:
|
|
312
|
+
_assign_nested_value(overrides, parts, coerced)
|
|
313
|
+
return overrides
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _lookup_bool(mapping: Mapping[str, Any] | None, path: tuple[str, ...]) -> bool | None:
|
|
317
|
+
"""Walk a mapping to resolve a boolean-like value."""
|
|
318
|
+
if not isinstance(mapping, Mapping):
|
|
319
|
+
return None
|
|
320
|
+
cursor: Any = mapping
|
|
321
|
+
for key in path:
|
|
322
|
+
if not isinstance(cursor, Mapping) or key not in cursor:
|
|
323
|
+
return None
|
|
324
|
+
cursor = cursor[key]
|
|
325
|
+
if isinstance(cursor, bool):
|
|
326
|
+
return cursor
|
|
327
|
+
if isinstance(cursor, (int, float)):
|
|
328
|
+
return bool(cursor)
|
|
329
|
+
if isinstance(cursor, str):
|
|
330
|
+
candidate = cursor.strip().lower()
|
|
331
|
+
if candidate in {"true", "yes", "on", "1"}:
|
|
332
|
+
return True
|
|
333
|
+
if candidate in {"false", "no", "off", "0"}:
|
|
334
|
+
return False
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def render(
|
|
339
|
+
list_extensions: Annotated[
|
|
340
|
+
bool,
|
|
341
|
+
typer.Option(
|
|
342
|
+
"--list-extensions",
|
|
343
|
+
help="List Markdown extensions enabled by default and exit.",
|
|
344
|
+
rich_help_panel=DIAGNOSTICS_PANEL,
|
|
345
|
+
),
|
|
346
|
+
] = False,
|
|
347
|
+
list_templates_flag: Annotated[
|
|
348
|
+
bool,
|
|
349
|
+
typer.Option(
|
|
350
|
+
"--list-templates",
|
|
351
|
+
help="List available templates (builtin, entry-point, and local) and exit.",
|
|
352
|
+
rich_help_panel=DIAGNOSTICS_PANEL,
|
|
353
|
+
),
|
|
354
|
+
] = False,
|
|
355
|
+
list_bibliography: Annotated[
|
|
356
|
+
bool,
|
|
357
|
+
typer.Option(
|
|
358
|
+
"--list-bibliography",
|
|
359
|
+
help="Print bibliography details from provided .bib files and exit.",
|
|
360
|
+
rich_help_panel=DIAGNOSTICS_PANEL,
|
|
361
|
+
),
|
|
362
|
+
] = False,
|
|
363
|
+
verbose: Annotated[
|
|
364
|
+
int,
|
|
365
|
+
typer.Option(
|
|
366
|
+
"--verbose",
|
|
367
|
+
"-v",
|
|
368
|
+
count=True,
|
|
369
|
+
help=("Increase CLI verbosity. Combine multiple times for additional diagnostics."),
|
|
370
|
+
rich_help_panel=DIAGNOSTICS_PANEL,
|
|
371
|
+
),
|
|
372
|
+
] = 0,
|
|
373
|
+
debug: Annotated[
|
|
374
|
+
bool,
|
|
375
|
+
typer.Option(
|
|
376
|
+
"--debug",
|
|
377
|
+
help="Show full tracebacks when an unexpected error occurs.",
|
|
378
|
+
rich_help_panel=DIAGNOSTICS_PANEL,
|
|
379
|
+
),
|
|
380
|
+
] = False,
|
|
381
|
+
debug_rules: DebugRulesOption = False,
|
|
382
|
+
inputs: InputPathArgument = None,
|
|
383
|
+
input_path: Annotated[
|
|
384
|
+
Path | None,
|
|
385
|
+
typer.Option(
|
|
386
|
+
"--input-path",
|
|
387
|
+
help="Internal helper used for programmatic invocation.",
|
|
388
|
+
hidden=True,
|
|
389
|
+
),
|
|
390
|
+
] = None,
|
|
391
|
+
output: OutputPathOption = None,
|
|
392
|
+
selector: SelectorOption = "article.md-content__inner",
|
|
393
|
+
full_document: FullDocumentOption = False,
|
|
394
|
+
base_level: BaseLevelOption = "0",
|
|
395
|
+
strip_heading: StripHeadingOption = False,
|
|
396
|
+
no_promote_title: NoPromoteTitleOption = False,
|
|
397
|
+
no_title: NoTitleOption = False,
|
|
398
|
+
parser: ParserOption = None,
|
|
399
|
+
disable_fallback_converters: DisableFallbackOption = False,
|
|
400
|
+
no_copy_assets: NoCopyAssetsOption = False,
|
|
401
|
+
convert_assets: ConvertAssetsOption = False,
|
|
402
|
+
hash_assets: HashAssetsOption = False,
|
|
403
|
+
diagrams_backend: Annotated[
|
|
404
|
+
str | None,
|
|
405
|
+
typer.Option(
|
|
406
|
+
"--diagrams-backend",
|
|
407
|
+
metavar="BACKEND",
|
|
408
|
+
help="Force the backend for diagram conversion (draw.io, mermaid): playwright, local, or docker (auto-default).",
|
|
409
|
+
case_sensitive=False,
|
|
410
|
+
),
|
|
411
|
+
] = None,
|
|
412
|
+
manifest: ManifestOptionWithShort = False,
|
|
413
|
+
make_deps: MakefileDepsOption = False,
|
|
414
|
+
template: TemplateOption = None,
|
|
415
|
+
embed_fragments: Annotated[
|
|
416
|
+
bool,
|
|
417
|
+
typer.Option(
|
|
418
|
+
"--embed",
|
|
419
|
+
help="Embed converted documents into the main document instead linking them with \\input.",
|
|
420
|
+
),
|
|
421
|
+
] = False,
|
|
422
|
+
enable_fragments: EnableFragmentOption = None,
|
|
423
|
+
disable_fragments: DisableFragmentOption = None,
|
|
424
|
+
template_attributes: TemplateAttributeOption = None,
|
|
425
|
+
debug_html: DebugHtmlOption = None,
|
|
426
|
+
classic_output: Annotated[
|
|
427
|
+
bool,
|
|
428
|
+
typer.Option(
|
|
429
|
+
"--classic-output",
|
|
430
|
+
help=("Display raw latexmk output without parsing."),
|
|
431
|
+
),
|
|
432
|
+
] = False,
|
|
433
|
+
html_only: HtmlOnlyOption = False,
|
|
434
|
+
build_pdf: Annotated[
|
|
435
|
+
bool,
|
|
436
|
+
typer.Option(
|
|
437
|
+
"-b",
|
|
438
|
+
"--build",
|
|
439
|
+
help="Invoke latexmk after rendering to compile the resulting LaTeX project.",
|
|
440
|
+
),
|
|
441
|
+
] = False,
|
|
442
|
+
engine: Annotated[
|
|
443
|
+
str,
|
|
444
|
+
typer.Option(
|
|
445
|
+
"-e",
|
|
446
|
+
"--engine",
|
|
447
|
+
help="LaTeX engine backend to use when building (tectonic, lualatex, xelatex).",
|
|
448
|
+
rich_help_panel=OUTPUT_PANEL,
|
|
449
|
+
),
|
|
450
|
+
] = "tectonic",
|
|
451
|
+
system_tectonic: Annotated[
|
|
452
|
+
bool,
|
|
453
|
+
typer.Option(
|
|
454
|
+
"-S",
|
|
455
|
+
"--system",
|
|
456
|
+
help="Use the system Tectonic binary instead of the bundled download.",
|
|
457
|
+
rich_help_panel=OUTPUT_PANEL,
|
|
458
|
+
),
|
|
459
|
+
] = False,
|
|
460
|
+
language: LanguageOption = None,
|
|
461
|
+
legacy_latex_accents: Annotated[
|
|
462
|
+
bool,
|
|
463
|
+
typer.Option(
|
|
464
|
+
"--legacy-latex-accents",
|
|
465
|
+
help=(
|
|
466
|
+
"Escape accented characters and ligatures with legacy LaTeX macros instead of "
|
|
467
|
+
"emitting Unicode glyphs (defaults to Unicode output)."
|
|
468
|
+
),
|
|
469
|
+
),
|
|
470
|
+
] = False,
|
|
471
|
+
slots: SlotsOption = None,
|
|
472
|
+
markdown_extensions: MarkdownExtensionsOption = None,
|
|
473
|
+
disable_markdown_extensions: DisableMarkdownExtensionsOption = None,
|
|
474
|
+
open_log: OpenLogOption = False,
|
|
475
|
+
isolate_cache: Annotated[
|
|
476
|
+
bool,
|
|
477
|
+
typer.Option(
|
|
478
|
+
"--isolate",
|
|
479
|
+
help=(
|
|
480
|
+
"Use a per-render TeX cache inside the output directory instead of the shared "
|
|
481
|
+
"~/.cache/texsmith cache."
|
|
482
|
+
),
|
|
483
|
+
rich_help_panel=OUTPUT_PANEL,
|
|
484
|
+
),
|
|
485
|
+
] = False,
|
|
486
|
+
template_info_flag: TemplateInfoOption = False,
|
|
487
|
+
template_scaffold: Annotated[
|
|
488
|
+
Path | None,
|
|
489
|
+
typer.Option(
|
|
490
|
+
"--template-scaffold",
|
|
491
|
+
metavar="DEST",
|
|
492
|
+
help="Copy the selected template into DEST and exit.",
|
|
493
|
+
),
|
|
494
|
+
] = None,
|
|
495
|
+
fonts_info: FontsInfoOption = False,
|
|
496
|
+
print_context: Annotated[
|
|
497
|
+
bool,
|
|
498
|
+
typer.Option(
|
|
499
|
+
"--print-context",
|
|
500
|
+
help="Print resolved template context emitters/consumers and exit.",
|
|
501
|
+
rich_help_panel=DIAGNOSTICS_PANEL,
|
|
502
|
+
),
|
|
503
|
+
] = False,
|
|
504
|
+
) -> None:
|
|
505
|
+
"""Convert MkDocs documents into LaTeX artefacts and optionally build PDFs."""
|
|
506
|
+
|
|
507
|
+
ctx = click.get_current_context(silent=True)
|
|
508
|
+
typer_ctx = ctx if isinstance(ctx, typer.Context) else None
|
|
509
|
+
state = set_cli_state(ctx=typer_ctx, verbosity=verbose, debug=debug)
|
|
510
|
+
if html_only:
|
|
511
|
+
build_pdf = False
|
|
512
|
+
template = None
|
|
513
|
+
template_info_flag = False
|
|
514
|
+
template_scaffold = None
|
|
515
|
+
if print_context:
|
|
516
|
+
build_pdf = False
|
|
517
|
+
|
|
518
|
+
if typer_ctx is not None and typer_ctx.resilient_parsing:
|
|
519
|
+
return
|
|
520
|
+
|
|
521
|
+
if list_extensions:
|
|
522
|
+
for extension in DEFAULT_MARKDOWN_EXTENSIONS:
|
|
523
|
+
typer.echo(extension)
|
|
524
|
+
raise typer.Exit()
|
|
525
|
+
|
|
526
|
+
if list_templates_flag:
|
|
527
|
+
list_templates()
|
|
528
|
+
raise typer.Exit()
|
|
529
|
+
|
|
530
|
+
verbosity_level = state.verbosity
|
|
531
|
+
if verbosity_level <= 0 and typer_ctx is not None and typer_ctx.parent is not None:
|
|
532
|
+
verbosity_level = int(typer_ctx.parent.params.get("verbose", 0) or 0)
|
|
533
|
+
if verbosity_level > 0:
|
|
534
|
+
state.verbosity = verbosity_level
|
|
535
|
+
|
|
536
|
+
document_paths = list(inputs or [])
|
|
537
|
+
if input_path is not None:
|
|
538
|
+
if document_paths:
|
|
539
|
+
raise typer.BadParameter("Provide either positional inputs or --input-path, not both.")
|
|
540
|
+
document_paths = [input_path]
|
|
541
|
+
|
|
542
|
+
if not document_paths:
|
|
543
|
+
stdin_document = _read_stdin_document()
|
|
544
|
+
if stdin_document is not None:
|
|
545
|
+
document_paths = [stdin_document]
|
|
546
|
+
|
|
547
|
+
try:
|
|
548
|
+
split_result = _SERVICE.split_inputs(document_paths)
|
|
549
|
+
except ConversionError as exc:
|
|
550
|
+
emit_error(str(exc), exception=exc)
|
|
551
|
+
raise typer.Exit(code=1) from exc
|
|
552
|
+
|
|
553
|
+
document_paths = split_result.documents
|
|
554
|
+
bibliography_files = split_result.bibliography_files
|
|
555
|
+
shared_front_matter = split_result.front_matter
|
|
556
|
+
shared_front_matter_path = split_result.front_matter_path
|
|
557
|
+
|
|
558
|
+
template_requested = template_info_flag or template_scaffold
|
|
559
|
+
|
|
560
|
+
if list_bibliography:
|
|
561
|
+
collection = BibliographyCollection()
|
|
562
|
+
if bibliography_files:
|
|
563
|
+
collection.load_files(bibliography_files)
|
|
564
|
+
print_bibliography_overview(collection)
|
|
565
|
+
raise typer.Exit()
|
|
566
|
+
|
|
567
|
+
if not document_paths:
|
|
568
|
+
if template_requested:
|
|
569
|
+
identifier = template or "article"
|
|
570
|
+
if template_info_flag:
|
|
571
|
+
show_template_info(identifier)
|
|
572
|
+
if template_scaffold:
|
|
573
|
+
scaffold_template(identifier, template_scaffold)
|
|
574
|
+
raise typer.Exit()
|
|
575
|
+
raise typer.BadParameter(
|
|
576
|
+
"Provide a Markdown (.md) or HTML (.html) source document or pipe content via stdin."
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
numbered = True
|
|
580
|
+
copy_assets = not no_copy_assets
|
|
581
|
+
primary_front_matter: Mapping[str, Any] | None = shared_front_matter
|
|
582
|
+
first_document = document_paths[0] if document_paths else None
|
|
583
|
+
if primary_front_matter is None and first_document is not None:
|
|
584
|
+
front_matter = _load_front_matter(first_document)
|
|
585
|
+
if front_matter:
|
|
586
|
+
primary_front_matter = front_matter
|
|
587
|
+
|
|
588
|
+
fm_payload: dict[str, Any] = {}
|
|
589
|
+
if isinstance(primary_front_matter, Mapping):
|
|
590
|
+
fm_payload = dict(primary_front_matter)
|
|
591
|
+
try:
|
|
592
|
+
normalise_press_metadata(fm_payload)
|
|
593
|
+
except PressMetadataError as exc:
|
|
594
|
+
raise typer.BadParameter(str(exc)) from exc
|
|
595
|
+
|
|
596
|
+
fm_numbered = _lookup_bool(fm_payload, ("numbered",))
|
|
597
|
+
if fm_numbered is not None:
|
|
598
|
+
numbered = fm_numbered
|
|
599
|
+
|
|
600
|
+
template_param_source = ctx.get_parameter_source("template") if ctx else None
|
|
601
|
+
no_promote_param_source = ctx.get_parameter_source("no_promote_title") if ctx else None
|
|
602
|
+
if (
|
|
603
|
+
template_param_source in {None, ParameterSource.DEFAULT}
|
|
604
|
+
and template is None
|
|
605
|
+
and primary_front_matter is not None
|
|
606
|
+
):
|
|
607
|
+
metadata_template = _extract_press_template(primary_front_matter)
|
|
608
|
+
if metadata_template:
|
|
609
|
+
template = metadata_template
|
|
610
|
+
|
|
611
|
+
if template is None and (build_pdf or template_requested):
|
|
612
|
+
template = "article"
|
|
613
|
+
|
|
614
|
+
template_selected = bool(template)
|
|
615
|
+
|
|
616
|
+
if template_requested:
|
|
617
|
+
identifier = template or "article"
|
|
618
|
+
if template_info_flag:
|
|
619
|
+
show_template_info(identifier)
|
|
620
|
+
if template_scaffold:
|
|
621
|
+
scaffold_template(identifier, template_scaffold)
|
|
622
|
+
raise typer.Exit()
|
|
623
|
+
|
|
624
|
+
promote_title = not no_promote_title
|
|
625
|
+
|
|
626
|
+
attribute_overrides = _parse_template_attributes(template_attributes)
|
|
627
|
+
if attribute_overrides:
|
|
628
|
+
try:
|
|
629
|
+
normalise_press_metadata(attribute_overrides)
|
|
630
|
+
except PressMetadataError as exc:
|
|
631
|
+
raise typer.BadParameter(str(exc)) from exc
|
|
632
|
+
if attribute_overrides and not template_selected:
|
|
633
|
+
raise typer.BadParameter("--attribute can only be used together with --template.")
|
|
634
|
+
if engine:
|
|
635
|
+
attribute_overrides.setdefault("_texsmith_latex_engine", engine)
|
|
636
|
+
attr_numbered = _lookup_bool(attribute_overrides, ("numbered",))
|
|
637
|
+
if attr_numbered is not None:
|
|
638
|
+
numbered = attr_numbered
|
|
639
|
+
if attribute_overrides:
|
|
640
|
+
state.record_event("template_attributes", {"values": attribute_overrides})
|
|
641
|
+
|
|
642
|
+
output_param_source = ctx.get_parameter_source("output") if ctx else None
|
|
643
|
+
pdf_output_requested = bool(output and output.suffix.lower() == ".pdf")
|
|
644
|
+
if pdf_output_requested and not build_pdf:
|
|
645
|
+
typer.echo("Enabling --build to produce PDF output.")
|
|
646
|
+
build_pdf = True
|
|
647
|
+
|
|
648
|
+
if no_title:
|
|
649
|
+
promote_title = False
|
|
650
|
+
|
|
651
|
+
if strip_heading:
|
|
652
|
+
promote_title = False
|
|
653
|
+
|
|
654
|
+
if build_pdf and not template_selected:
|
|
655
|
+
template = "article"
|
|
656
|
+
template_selected = True
|
|
657
|
+
# In CI runners like act, prefer classic output to avoid streaming latexmk.
|
|
658
|
+
if os.environ.get("ACT") == "true":
|
|
659
|
+
classic_output = True
|
|
660
|
+
|
|
661
|
+
if classic_output and not build_pdf:
|
|
662
|
+
raise typer.BadParameter("--classic-output can only be used together with --build.")
|
|
663
|
+
|
|
664
|
+
if open_log and not build_pdf:
|
|
665
|
+
raise typer.BadParameter("--open-log can only be used together with --build.")
|
|
666
|
+
if make_deps and not build_pdf:
|
|
667
|
+
raise typer.BadParameter("--makefile-deps can only be used together with --build.")
|
|
668
|
+
|
|
669
|
+
if print_context and not template_selected:
|
|
670
|
+
raise typer.BadParameter(
|
|
671
|
+
"--print-context requires a template (front matter or --template)."
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
try:
|
|
675
|
+
_, slot_assignments = organise_slot_overrides(slots, document_paths)
|
|
676
|
+
except (typer.BadParameter, ValueError) as exc:
|
|
677
|
+
raise typer.BadParameter(str(exc)) from exc
|
|
678
|
+
|
|
679
|
+
state_slot_rows: list[dict[str, object]] = []
|
|
680
|
+
for doc_path, entries in slot_assignments.items():
|
|
681
|
+
for entry in entries:
|
|
682
|
+
state_slot_rows.append(
|
|
683
|
+
{
|
|
684
|
+
"document": _format_path_for_event(doc_path),
|
|
685
|
+
"slot": entry.slot,
|
|
686
|
+
"selector": entry.selector,
|
|
687
|
+
"include_document": entry.include_document,
|
|
688
|
+
}
|
|
689
|
+
)
|
|
690
|
+
if state_slot_rows:
|
|
691
|
+
state.record_event("slot_assignments", {"entries": state_slot_rows})
|
|
692
|
+
|
|
693
|
+
base_level_param_source = ctx.get_parameter_source("base_level") if ctx else None
|
|
694
|
+
base_level_value = base_level
|
|
695
|
+
if not template_selected and base_level_param_source in {
|
|
696
|
+
None,
|
|
697
|
+
ParameterSource.DEFAULT,
|
|
698
|
+
}:
|
|
699
|
+
base_level_value = "section"
|
|
700
|
+
try:
|
|
701
|
+
resolved_base_level = coerce_base_level(base_level_value, allow_none=False)
|
|
702
|
+
except TemplateError as exc:
|
|
703
|
+
raise typer.BadParameter(str(exc)) from exc
|
|
704
|
+
|
|
705
|
+
if not template_selected and no_promote_param_source in {
|
|
706
|
+
None,
|
|
707
|
+
ParameterSource.DEFAULT,
|
|
708
|
+
}:
|
|
709
|
+
promote_title = False
|
|
710
|
+
|
|
711
|
+
resolved_markdown_extensions = resolve_markdown_extensions(
|
|
712
|
+
markdown_extensions,
|
|
713
|
+
disable_markdown_extensions,
|
|
714
|
+
)
|
|
715
|
+
extension_line = f"Extensions: {', '.join(resolved_markdown_extensions) or '(none)'}"
|
|
716
|
+
|
|
717
|
+
def _flush_diagnostics() -> None:
|
|
718
|
+
lines: list[str] = []
|
|
719
|
+
if state.verbosity >= 1:
|
|
720
|
+
lines.append(extension_line)
|
|
721
|
+
lines.extend(consume_event_diagnostics(state))
|
|
722
|
+
for line in lines:
|
|
723
|
+
typer.echo(line)
|
|
724
|
+
|
|
725
|
+
debug_snapshot = debug_html if debug_html is not None else debug_enabled()
|
|
726
|
+
|
|
727
|
+
output_mode, output_target = determine_output_target(template_selected, document_paths, output)
|
|
728
|
+
resolved_output_target = output_target.resolve() if output_target is not None else None
|
|
729
|
+
|
|
730
|
+
temp_render_dir: Path | None = None
|
|
731
|
+
cleanup_render_dir = False
|
|
732
|
+
cleanup_render_dir_path: Path | None = None
|
|
733
|
+
final_pdf_target: Path | None = None
|
|
734
|
+
|
|
735
|
+
if template_selected:
|
|
736
|
+
if output_mode == "template-pdf":
|
|
737
|
+
temp_render_dir = Path(tempfile.mkdtemp(prefix="texsmith-")).resolve()
|
|
738
|
+
typer.echo(f"Using temporary output directory: {temp_render_dir}")
|
|
739
|
+
cleanup_render_dir = True
|
|
740
|
+
cleanup_render_dir_path = temp_render_dir
|
|
741
|
+
final_pdf_target = resolved_output_target
|
|
742
|
+
elif (
|
|
743
|
+
build_pdf
|
|
744
|
+
and output_mode == "template"
|
|
745
|
+
and output_param_source in {None, ParameterSource.DEFAULT}
|
|
746
|
+
):
|
|
747
|
+
temp_render_dir = Path(tempfile.mkdtemp(prefix="texsmith-")).resolve()
|
|
748
|
+
typer.echo(f"Using temporary output directory: {temp_render_dir}")
|
|
749
|
+
cleanup_render_dir = True
|
|
750
|
+
cleanup_render_dir_path = temp_render_dir
|
|
751
|
+
primary_name = document_paths[0].stem if document_paths else "texsmith"
|
|
752
|
+
final_pdf_target = Path.cwd() / f"{primary_name}.pdf"
|
|
753
|
+
|
|
754
|
+
render_dir_path: Path | None = None
|
|
755
|
+
if template_selected:
|
|
756
|
+
render_dir_path = temp_render_dir or resolved_output_target
|
|
757
|
+
elif output_mode == "directory":
|
|
758
|
+
render_dir_path = resolved_output_target
|
|
759
|
+
|
|
760
|
+
if template_selected and render_dir_path is None:
|
|
761
|
+
raise typer.BadParameter("Unable to resolve template output directory.")
|
|
762
|
+
|
|
763
|
+
if not embed_fragments and template_selected and len(document_paths) == 1:
|
|
764
|
+
embed_fragments = True
|
|
765
|
+
|
|
766
|
+
emitter = CliEmitter(state=state, debug_enabled=debug_enabled())
|
|
767
|
+
|
|
768
|
+
request_render_dir = render_dir_path
|
|
769
|
+
|
|
770
|
+
request = ConversionRequest(
|
|
771
|
+
documents=document_paths,
|
|
772
|
+
bibliography_files=bibliography_files,
|
|
773
|
+
front_matter=shared_front_matter,
|
|
774
|
+
front_matter_path=shared_front_matter_path,
|
|
775
|
+
slot_assignments=slot_assignments,
|
|
776
|
+
selector=selector,
|
|
777
|
+
full_document=full_document,
|
|
778
|
+
base_level=resolved_base_level,
|
|
779
|
+
strip_heading_all=strip_heading if build_pdf else False,
|
|
780
|
+
strip_heading_first_document=False if build_pdf else strip_heading,
|
|
781
|
+
promote_title=promote_title,
|
|
782
|
+
suppress_title=no_title,
|
|
783
|
+
numbered=numbered,
|
|
784
|
+
markdown_extensions=resolved_markdown_extensions,
|
|
785
|
+
parser=parser,
|
|
786
|
+
disable_fallback_converters=disable_fallback_converters,
|
|
787
|
+
copy_assets=copy_assets,
|
|
788
|
+
convert_assets=convert_assets,
|
|
789
|
+
hash_assets=hash_assets,
|
|
790
|
+
manifest=manifest,
|
|
791
|
+
persist_debug_html=bool(debug_snapshot),
|
|
792
|
+
language=language,
|
|
793
|
+
legacy_latex_accents=legacy_latex_accents,
|
|
794
|
+
diagrams_backend=diagrams_backend.lower() if isinstance(diagrams_backend, str) else None,
|
|
795
|
+
template=template,
|
|
796
|
+
render_dir=request_render_dir,
|
|
797
|
+
template_options=attribute_overrides,
|
|
798
|
+
embed_fragments=embed_fragments,
|
|
799
|
+
enable_fragments=enable_fragments or [],
|
|
800
|
+
disable_fragments=disable_fragments or [],
|
|
801
|
+
emitter=emitter,
|
|
802
|
+
)
|
|
803
|
+
state.record_event(
|
|
804
|
+
"conversion_settings",
|
|
805
|
+
{
|
|
806
|
+
"parser": parser or "auto",
|
|
807
|
+
"copy_assets": copy_assets,
|
|
808
|
+
"convert_assets": convert_assets,
|
|
809
|
+
"hash_assets": hash_assets,
|
|
810
|
+
"manifest": manifest,
|
|
811
|
+
"fallback_converters_enabled": not disable_fallback_converters,
|
|
812
|
+
},
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
try:
|
|
816
|
+
prepared = _SERVICE.prepare_documents(request)
|
|
817
|
+
except UnsupportedInputError as exc:
|
|
818
|
+
emit_error(str(exc), exception=exc)
|
|
819
|
+
raise typer.Exit(code=1) from exc
|
|
820
|
+
except ConversionError as exc:
|
|
821
|
+
emit_error(str(exc), exception=exc)
|
|
822
|
+
raise typer.Exit(code=1) from exc
|
|
823
|
+
|
|
824
|
+
if html_only:
|
|
825
|
+
html_fragments = []
|
|
826
|
+
for doc in prepared.documents:
|
|
827
|
+
processed_html = doc.html
|
|
828
|
+
try:
|
|
829
|
+
processed_html, _usage, _summary = wrap_scripts_in_html(processed_html)
|
|
830
|
+
except Exception:
|
|
831
|
+
processed_html = doc.html
|
|
832
|
+
html_fragments.append((doc.source_path, processed_html))
|
|
833
|
+
if output_mode == "stdout":
|
|
834
|
+
typer.echo("\n\n".join(fragment for _, fragment in html_fragments))
|
|
835
|
+
_flush_diagnostics()
|
|
836
|
+
return
|
|
837
|
+
|
|
838
|
+
summary_paths: list[Path] = []
|
|
839
|
+
if output_mode == "file":
|
|
840
|
+
if resolved_output_target is None:
|
|
841
|
+
raise typer.BadParameter("Output path is required when writing HTML to a file.")
|
|
842
|
+
try:
|
|
843
|
+
write_output_file(resolved_output_target, html_fragments[0][1])
|
|
844
|
+
except OSError as exc:
|
|
845
|
+
emit_error(str(exc), exception=exc)
|
|
846
|
+
raise typer.Exit(code=1) from exc
|
|
847
|
+
summary_paths.append(resolved_output_target)
|
|
848
|
+
elif output_mode in {"directory", "template"}:
|
|
849
|
+
if resolved_output_target is None:
|
|
850
|
+
raise typer.BadParameter("Output directory is required when writing HTML files.")
|
|
851
|
+
resolved_output_target.mkdir(parents=True, exist_ok=True)
|
|
852
|
+
for source_path, payload in html_fragments:
|
|
853
|
+
target = resolved_output_target / f"{source_path.stem}.html"
|
|
854
|
+
try:
|
|
855
|
+
write_output_file(target, payload)
|
|
856
|
+
except OSError as exc:
|
|
857
|
+
emit_error(str(exc), exception=exc)
|
|
858
|
+
raise typer.Exit(code=1) from exc
|
|
859
|
+
summary_paths.append(target)
|
|
860
|
+
elif output_mode == "template-pdf":
|
|
861
|
+
raise typer.BadParameter("--html cannot be combined with a PDF output target.")
|
|
862
|
+
else:
|
|
863
|
+
raise RuntimeError(f"Unsupported output mode '{output_mode}' for HTML output.")
|
|
864
|
+
|
|
865
|
+
present_html_summary(
|
|
866
|
+
state=state,
|
|
867
|
+
output_mode=output_mode,
|
|
868
|
+
output_paths=summary_paths,
|
|
869
|
+
)
|
|
870
|
+
_flush_diagnostics()
|
|
871
|
+
return
|
|
872
|
+
|
|
873
|
+
engine_env_key = "TEXSMITH_SELECTED_ENGINE"
|
|
874
|
+
previous_engine_value = os.environ.get(engine_env_key)
|
|
875
|
+
if engine:
|
|
876
|
+
os.environ[engine_env_key] = engine
|
|
877
|
+
else:
|
|
878
|
+
os.environ.pop(engine_env_key, None)
|
|
879
|
+
try:
|
|
880
|
+
response = _SERVICE.execute(request, prepared=prepared)
|
|
881
|
+
except (TemplateError, ConversionError) as exc:
|
|
882
|
+
emit_error(str(exc), exception=exc)
|
|
883
|
+
raise typer.Exit(code=1) from exc
|
|
884
|
+
finally:
|
|
885
|
+
if previous_engine_value is None:
|
|
886
|
+
os.environ.pop(engine_env_key, None)
|
|
887
|
+
else:
|
|
888
|
+
os.environ[engine_env_key] = previous_engine_value
|
|
889
|
+
|
|
890
|
+
def _emit_rule_diagnostics() -> None:
|
|
891
|
+
if not debug_rules:
|
|
892
|
+
return
|
|
893
|
+
rules: list[dict[str, object]] = []
|
|
894
|
+
if response.is_template:
|
|
895
|
+
rules = getattr(response.render_result, "rule_descriptions", []) or []
|
|
896
|
+
else:
|
|
897
|
+
for fragment in response.bundle.fragments:
|
|
898
|
+
conversion = fragment.conversion
|
|
899
|
+
if conversion and conversion.rule_descriptions:
|
|
900
|
+
rules = conversion.rule_descriptions
|
|
901
|
+
break
|
|
902
|
+
if rules:
|
|
903
|
+
present_rule_descriptions(state, rules)
|
|
904
|
+
|
|
905
|
+
if not template_selected:
|
|
906
|
+
bundle = response.bundle
|
|
907
|
+
|
|
908
|
+
if output_mode == "stdout":
|
|
909
|
+
typer.echo(bundle.combined_output())
|
|
910
|
+
_emit_rule_diagnostics()
|
|
911
|
+
_flush_diagnostics()
|
|
912
|
+
return
|
|
913
|
+
|
|
914
|
+
if output_mode == "file":
|
|
915
|
+
if resolved_output_target is None:
|
|
916
|
+
raise typer.BadParameter("Output path is required when writing to a file.")
|
|
917
|
+
try:
|
|
918
|
+
write_output_file(resolved_output_target, bundle.combined_output())
|
|
919
|
+
except OSError as exc:
|
|
920
|
+
emit_error(str(exc), exception=exc)
|
|
921
|
+
raise typer.Exit(code=1) from exc
|
|
922
|
+
present_conversion_summary(
|
|
923
|
+
state=state,
|
|
924
|
+
output_mode=output_mode,
|
|
925
|
+
bundle=bundle,
|
|
926
|
+
output_path=resolved_output_target,
|
|
927
|
+
render_result=None,
|
|
928
|
+
)
|
|
929
|
+
_emit_rule_diagnostics()
|
|
930
|
+
_flush_diagnostics()
|
|
931
|
+
return
|
|
932
|
+
|
|
933
|
+
if output_mode == "directory":
|
|
934
|
+
present_conversion_summary(
|
|
935
|
+
state=state,
|
|
936
|
+
output_mode=output_mode,
|
|
937
|
+
bundle=bundle,
|
|
938
|
+
output_path=request.render_dir,
|
|
939
|
+
render_result=None,
|
|
940
|
+
)
|
|
941
|
+
_emit_rule_diagnostics()
|
|
942
|
+
_flush_diagnostics()
|
|
943
|
+
return
|
|
944
|
+
|
|
945
|
+
raise RuntimeError(f"Unsupported output mode '{output_mode}'.")
|
|
946
|
+
|
|
947
|
+
render_result = response.render_result
|
|
948
|
+
|
|
949
|
+
render_dir = render_result.main_tex_path.parent.resolve()
|
|
950
|
+
|
|
951
|
+
if not build_pdf:
|
|
952
|
+
present_conversion_summary(
|
|
953
|
+
state=state,
|
|
954
|
+
output_mode="template",
|
|
955
|
+
bundle=None,
|
|
956
|
+
output_path=render_dir,
|
|
957
|
+
render_result=render_result,
|
|
958
|
+
)
|
|
959
|
+
if print_context:
|
|
960
|
+
present_context_attributes(state=state, render_result=render_result)
|
|
961
|
+
if fonts_info:
|
|
962
|
+
present_fonts_info(state, render_result)
|
|
963
|
+
_emit_rule_diagnostics()
|
|
964
|
+
_flush_diagnostics()
|
|
965
|
+
return
|
|
966
|
+
|
|
967
|
+
engine_choice = resolve_engine(engine, render_result.template_engine)
|
|
968
|
+
template_context = getattr(render_result, "template_context", None) or getattr(
|
|
969
|
+
render_result, "context", None
|
|
970
|
+
)
|
|
971
|
+
use_system_tectonic = system_tectonic if engine_choice.backend == "tectonic" else False
|
|
972
|
+
features = compute_features(
|
|
973
|
+
requires_shell_escape=render_result.requires_shell_escape,
|
|
974
|
+
bibliography=render_result.has_bibliography,
|
|
975
|
+
document_state=render_result.document_state,
|
|
976
|
+
template_context=template_context,
|
|
977
|
+
)
|
|
978
|
+
|
|
979
|
+
tectonic_binary: Path | None = None
|
|
980
|
+
biber_binary: Path | None = None
|
|
981
|
+
makeglossaries_binary: Path | None = None
|
|
982
|
+
bundled_bin: Path | None = None
|
|
983
|
+
if engine_choice.backend == "tectonic":
|
|
984
|
+
try:
|
|
985
|
+
selection = select_tectonic_binary(use_system_tectonic, console=state.console)
|
|
986
|
+
if features.bibliography and not use_system_tectonic:
|
|
987
|
+
biber_binary = select_biber_binary(console=state.console)
|
|
988
|
+
bundled_bin = biber_binary.parent
|
|
989
|
+
if features.has_glossary and not pyxindy_available():
|
|
990
|
+
glossaries = select_makeglossaries(console=state.console)
|
|
991
|
+
makeglossaries_binary = glossaries.path
|
|
992
|
+
if glossaries.source == "bundled":
|
|
993
|
+
bundled_bin = bundled_bin or glossaries.path.parent
|
|
994
|
+
except (
|
|
995
|
+
TectonicAcquisitionError,
|
|
996
|
+
BiberAcquisitionError,
|
|
997
|
+
MakeglossariesAcquisitionError,
|
|
998
|
+
) as exc:
|
|
999
|
+
emit_error(str(exc), exception=exc)
|
|
1000
|
+
raise typer.Exit(code=1) from exc
|
|
1001
|
+
tectonic_binary = selection.path
|
|
1002
|
+
|
|
1003
|
+
available_bins: dict[str, Path] = {}
|
|
1004
|
+
if biber_binary:
|
|
1005
|
+
available_bins["biber"] = biber_binary
|
|
1006
|
+
if makeglossaries_binary:
|
|
1007
|
+
available_bins["makeglossaries"] = makeglossaries_binary
|
|
1008
|
+
|
|
1009
|
+
missing_tools = missing_dependencies(
|
|
1010
|
+
engine_choice,
|
|
1011
|
+
features,
|
|
1012
|
+
use_system_tectonic=use_system_tectonic,
|
|
1013
|
+
available_binaries=available_bins or None,
|
|
1014
|
+
)
|
|
1015
|
+
if missing_tools:
|
|
1016
|
+
formatted = ", ".join(sorted(missing_tools))
|
|
1017
|
+
emit_error(f"Missing required tools for {engine_choice.label}: {formatted}")
|
|
1018
|
+
raise typer.Exit(code=1)
|
|
1019
|
+
|
|
1020
|
+
command_plan = ensure_command_paths(
|
|
1021
|
+
build_latex_engine_command(
|
|
1022
|
+
engine_choice,
|
|
1023
|
+
features,
|
|
1024
|
+
main_tex_path=render_result.main_tex_path,
|
|
1025
|
+
tectonic_binary=tectonic_binary,
|
|
1026
|
+
)
|
|
1027
|
+
)
|
|
1028
|
+
env = build_tex_env(
|
|
1029
|
+
render_dir,
|
|
1030
|
+
isolate_cache=isolate_cache,
|
|
1031
|
+
extra_path=bundled_bin,
|
|
1032
|
+
biber_path=biber_binary,
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
state.console.print(f"[bold cyan]Running {engine_choice.label}…[/]")
|
|
1036
|
+
|
|
1037
|
+
run_engine = getattr(render, "run_engine_command", run_engine_command)
|
|
1038
|
+
|
|
1039
|
+
try:
|
|
1040
|
+
engine_result: EngineResult = run_engine(
|
|
1041
|
+
command_plan,
|
|
1042
|
+
backend=engine_choice.backend,
|
|
1043
|
+
workdir=render_dir,
|
|
1044
|
+
env=env,
|
|
1045
|
+
console=state.console,
|
|
1046
|
+
verbosity=state.verbosity,
|
|
1047
|
+
classic_output=classic_output,
|
|
1048
|
+
features=features,
|
|
1049
|
+
)
|
|
1050
|
+
except OSError as exc:
|
|
1051
|
+
if debug_enabled():
|
|
1052
|
+
raise
|
|
1053
|
+
emit_error(f"Failed to execute {engine_choice.label}: {exc}", exception=exc)
|
|
1054
|
+
raise typer.Exit(code=1) from exc
|
|
1055
|
+
|
|
1056
|
+
if engine_result.returncode != 0:
|
|
1057
|
+
messages = engine_result.messages or parse_latex_log(command_plan.log_path)
|
|
1058
|
+
present_latex_failure(
|
|
1059
|
+
state=state,
|
|
1060
|
+
log_path=command_plan.log_path,
|
|
1061
|
+
messages=messages,
|
|
1062
|
+
open_log=open_log,
|
|
1063
|
+
)
|
|
1064
|
+
emit_error(f"{engine_choice.label} exited with status {engine_result.returncode}")
|
|
1065
|
+
raise typer.Exit(code=engine_result.returncode)
|
|
1066
|
+
|
|
1067
|
+
pdf_path = command_plan.pdf_path
|
|
1068
|
+
final_pdf_path = pdf_path
|
|
1069
|
+
dep_file_path: Path | None = None
|
|
1070
|
+
|
|
1071
|
+
if final_pdf_target is not None:
|
|
1072
|
+
final_destination = final_pdf_target
|
|
1073
|
+
try:
|
|
1074
|
+
final_destination.parent.mkdir(parents=True, exist_ok=True)
|
|
1075
|
+
except OSError as exc:
|
|
1076
|
+
emit_error(
|
|
1077
|
+
f"Unable to create output directory '{final_destination.parent}': {exc}",
|
|
1078
|
+
exc,
|
|
1079
|
+
)
|
|
1080
|
+
raise typer.Exit(code=1) from exc
|
|
1081
|
+
try:
|
|
1082
|
+
shutil.copy2(pdf_path, final_destination)
|
|
1083
|
+
except OSError as exc:
|
|
1084
|
+
emit_error(f"Failed to write PDF to '{final_destination}': {exc}", exc)
|
|
1085
|
+
raise typer.Exit(code=1) from exc
|
|
1086
|
+
final_pdf_path = final_destination
|
|
1087
|
+
|
|
1088
|
+
if make_deps:
|
|
1089
|
+
dependency_paths: set[Path] = set()
|
|
1090
|
+
dependency_paths.update(document_paths)
|
|
1091
|
+
dependency_paths.update(bibliography_files)
|
|
1092
|
+
if shared_front_matter_path:
|
|
1093
|
+
dependency_paths.add(shared_front_matter_path)
|
|
1094
|
+
dependency_paths.add(render_result.main_tex_path)
|
|
1095
|
+
dependency_paths.update(render_result.fragment_paths)
|
|
1096
|
+
if render_result.bibliography_path:
|
|
1097
|
+
dependency_paths.add(render_result.bibliography_path)
|
|
1098
|
+
latexmkrc_candidate = render_dir / ".latexmkrc"
|
|
1099
|
+
if latexmkrc_candidate.exists():
|
|
1100
|
+
dependency_paths.add(latexmkrc_candidate)
|
|
1101
|
+
dependency_paths.update(getattr(render_result, "asset_paths", []))
|
|
1102
|
+
dependency_paths.update(getattr(render_result, "asset_sources", []))
|
|
1103
|
+
for key in getattr(render_result, "asset_map", {}) or {}:
|
|
1104
|
+
candidate_path = Path(key)
|
|
1105
|
+
if candidate_path.exists():
|
|
1106
|
+
dependency_paths.add(candidate_path)
|
|
1107
|
+
try:
|
|
1108
|
+
dep_file_path = _write_makefile_deps(final_pdf_path, dependency_paths)
|
|
1109
|
+
except OSError as exc:
|
|
1110
|
+
emit_error(f"Failed to write dependency file: {exc}", exc)
|
|
1111
|
+
raise typer.Exit(code=1) from exc
|
|
1112
|
+
|
|
1113
|
+
present_build_summary(state=state, render_result=render_result, pdf_path=final_pdf_path)
|
|
1114
|
+
if fonts_info:
|
|
1115
|
+
present_fonts_info(state, render_result)
|
|
1116
|
+
if dep_file_path is not None:
|
|
1117
|
+
state.console.print(f"[cyan]Dependencies written to[/] {dep_file_path}")
|
|
1118
|
+
_emit_rule_diagnostics()
|
|
1119
|
+
_flush_diagnostics()
|
|
1120
|
+
|
|
1121
|
+
if cleanup_render_dir and cleanup_render_dir_path is not None:
|
|
1122
|
+
shutil.rmtree(cleanup_render_dir_path, ignore_errors=True)
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
# Expose runtime dependencies for test monkeypatching
|
|
1126
|
+
render.shutil = shutil # type: ignore[attr-defined]
|
|
1127
|
+
render.subprocess = subprocess # type: ignore[attr-defined]
|
|
1128
|
+
render.run_engine_command = run_engine_command # type: ignore[attr-defined]
|