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,578 @@
|
|
|
1
|
+
"""Template rendering utilities that aggregate conversion fragments."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, MutableMapping, Sequence
|
|
6
|
+
import copy
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from texsmith.adapters.latex.latexmk import build_latexmkrc_content
|
|
12
|
+
from texsmith.fonts import FallbackManager, FontCache, FontPipelineLogger
|
|
13
|
+
from texsmith.fonts.fallback import merge_fallback_summaries
|
|
14
|
+
from texsmith.fonts.scripts import fallback_summary_to_usage, merge_script_usage
|
|
15
|
+
|
|
16
|
+
from ..context import DocumentState
|
|
17
|
+
from ..diagnostics import DiagnosticEmitter
|
|
18
|
+
from ..templates import TemplateError, TemplateRuntime, wrap_template_document
|
|
19
|
+
from ..templates.context_usage import summarise_context_usage
|
|
20
|
+
from .debug import debug_enabled, ensure_emitter, raise_conversion_error, record_event
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(slots=True)
|
|
24
|
+
class TemplateFragment:
|
|
25
|
+
"""Contract describing the artefacts required by TemplateRenderer."""
|
|
26
|
+
|
|
27
|
+
stem: str
|
|
28
|
+
latex: str
|
|
29
|
+
default_slot: str
|
|
30
|
+
slot_outputs: Mapping[str, str]
|
|
31
|
+
slot_includes: set[str] = field(default_factory=set)
|
|
32
|
+
document_state: DocumentState | None = None
|
|
33
|
+
bibliography_path: Path | None = None
|
|
34
|
+
template_engine: str | None = None
|
|
35
|
+
requires_shell_escape: bool = False
|
|
36
|
+
template_overrides: Mapping[str, Any] | None = None
|
|
37
|
+
output_path: Path | None = None
|
|
38
|
+
front_matter: Mapping[str, Any] | None = None
|
|
39
|
+
source_path: Path | None = None
|
|
40
|
+
rule_descriptions: list[dict[str, Any]] | None = None
|
|
41
|
+
assets: dict[str, Path] | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(slots=True)
|
|
45
|
+
class TemplateRendererResult:
|
|
46
|
+
"""Structured artefacts produced after aggregating slot content."""
|
|
47
|
+
|
|
48
|
+
main_tex_path: Path
|
|
49
|
+
fragment_paths: list[Path]
|
|
50
|
+
template_context: dict[str, Any]
|
|
51
|
+
slot_content: dict[str, str]
|
|
52
|
+
document_state: DocumentState
|
|
53
|
+
bibliography_path: Path | None
|
|
54
|
+
template_engine: str | None
|
|
55
|
+
requires_shell_escape: bool
|
|
56
|
+
template_overrides: dict[str, Any] = field(default_factory=dict)
|
|
57
|
+
rule_descriptions: list[dict[str, Any]] = field(default_factory=list)
|
|
58
|
+
asset_paths: list[Path] = field(default_factory=list)
|
|
59
|
+
asset_sources: list[Path] = field(default_factory=list)
|
|
60
|
+
asset_map: dict[str, Path] = field(default_factory=dict)
|
|
61
|
+
context_attributes: list[dict[str, Any]] = field(default_factory=list)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
_SOFT_OVERRIDE_KEYS = {"press._source_dir", "press._source_path", "_source_dir", "_source_path"}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class FragmentOverrideError(TemplateError):
|
|
68
|
+
"""Raised when fragment template overrides disagree."""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _merge_overrides(
|
|
72
|
+
target: MutableMapping[str, Any],
|
|
73
|
+
source: Mapping[str, Any],
|
|
74
|
+
*,
|
|
75
|
+
namespace: str = "",
|
|
76
|
+
) -> None:
|
|
77
|
+
for key, value in source.items():
|
|
78
|
+
if key not in target:
|
|
79
|
+
target[key] = copy.deepcopy(value)
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
existing = target[key]
|
|
83
|
+
if isinstance(existing, MutableMapping) and isinstance(value, Mapping):
|
|
84
|
+
_merge_overrides(
|
|
85
|
+
existing,
|
|
86
|
+
value,
|
|
87
|
+
namespace=f"{namespace}.{key}" if namespace else key,
|
|
88
|
+
)
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
if existing == value:
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
conflict_path = f"{namespace}.{key}" if namespace else key
|
|
95
|
+
if conflict_path in _SOFT_OVERRIDE_KEYS:
|
|
96
|
+
continue
|
|
97
|
+
raise FragmentOverrideError(
|
|
98
|
+
f"Conflicting template override for '{conflict_path}': {existing!r} vs {value!r}"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _validate_slots(runtime: TemplateRuntime, aggregated_slots: Mapping[str, Any]) -> None:
|
|
103
|
+
"""Ensure that fragments only target declared template slots."""
|
|
104
|
+
declared = set(runtime.slots.keys()) | {runtime.default_slot}
|
|
105
|
+
unknown = sorted(slot for slot in aggregated_slots if slot not in declared)
|
|
106
|
+
if unknown:
|
|
107
|
+
allowed = ", ".join(sorted(declared))
|
|
108
|
+
raise TemplateError(
|
|
109
|
+
f"Fragments target unknown slot(s) {', '.join(unknown)} for template '{runtime.name}'. "
|
|
110
|
+
f"Declared slots: {allowed or '(none)'}."
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class TemplateRenderer:
|
|
115
|
+
"""Aggregate conversion fragments and wrap them with a template."""
|
|
116
|
+
|
|
117
|
+
def __init__(
|
|
118
|
+
self,
|
|
119
|
+
runtime: TemplateRuntime,
|
|
120
|
+
*,
|
|
121
|
+
emitter: DiagnosticEmitter | None = None,
|
|
122
|
+
) -> None:
|
|
123
|
+
self.runtime = runtime
|
|
124
|
+
self.emitter = ensure_emitter(emitter)
|
|
125
|
+
|
|
126
|
+
def _write_latexmkrc(
|
|
127
|
+
self,
|
|
128
|
+
*,
|
|
129
|
+
output_dir: Path,
|
|
130
|
+
main_tex_path: Path,
|
|
131
|
+
template_engine: str | None,
|
|
132
|
+
requires_shell_escape: bool,
|
|
133
|
+
document_state: DocumentState,
|
|
134
|
+
template_context: Mapping[str, Any] | None,
|
|
135
|
+
bibliography_present: bool,
|
|
136
|
+
) -> Path | None:
|
|
137
|
+
latexmkrc_path = output_dir / ".latexmkrc"
|
|
138
|
+
if latexmkrc_path.exists():
|
|
139
|
+
return latexmkrc_path
|
|
140
|
+
|
|
141
|
+
index_engine: str | None = None
|
|
142
|
+
if template_context:
|
|
143
|
+
raw_engine = template_context.get("index_engine")
|
|
144
|
+
if isinstance(raw_engine, str):
|
|
145
|
+
candidate = raw_engine.strip()
|
|
146
|
+
if candidate:
|
|
147
|
+
index_engine = candidate
|
|
148
|
+
|
|
149
|
+
has_index = bool(
|
|
150
|
+
getattr(document_state, "has_index_entries", False)
|
|
151
|
+
or getattr(document_state, "index_entries", [])
|
|
152
|
+
)
|
|
153
|
+
has_glossary = bool(
|
|
154
|
+
getattr(document_state, "acronyms", {}) or getattr(document_state, "glossary", {})
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
content = build_latexmkrc_content(
|
|
158
|
+
root_filename=main_tex_path.stem,
|
|
159
|
+
engine=template_engine,
|
|
160
|
+
requires_shell_escape=requires_shell_escape,
|
|
161
|
+
bibliography=bibliography_present,
|
|
162
|
+
index_engine=index_engine,
|
|
163
|
+
has_index=has_index,
|
|
164
|
+
has_glossary=has_glossary,
|
|
165
|
+
)
|
|
166
|
+
try:
|
|
167
|
+
latexmkrc_path.write_text(content, encoding="utf-8")
|
|
168
|
+
except OSError as exc:
|
|
169
|
+
self.emitter.warning(f"Failed to write latexmkrc: {exc}")
|
|
170
|
+
return None
|
|
171
|
+
return latexmkrc_path
|
|
172
|
+
|
|
173
|
+
def render(
|
|
174
|
+
self,
|
|
175
|
+
fragments: Sequence[TemplateFragment],
|
|
176
|
+
*,
|
|
177
|
+
output_dir: Path,
|
|
178
|
+
overrides: Mapping[str, Any] | None = None,
|
|
179
|
+
copy_assets: bool = True,
|
|
180
|
+
embed_fragments: bool = True,
|
|
181
|
+
) -> TemplateRendererResult:
|
|
182
|
+
if not fragments:
|
|
183
|
+
raise TemplateError("No fragments available for template rendering.")
|
|
184
|
+
|
|
185
|
+
output_dir = output_dir.resolve()
|
|
186
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
187
|
+
|
|
188
|
+
aggregated_slots: dict[str, list[str]] = {}
|
|
189
|
+
default_slot = self.runtime.default_slot
|
|
190
|
+
aggregated_slots.setdefault(default_slot, [])
|
|
191
|
+
|
|
192
|
+
template_overrides: dict[str, Any] | None = None
|
|
193
|
+
|
|
194
|
+
shared_state: DocumentState | None = None
|
|
195
|
+
aggregated_script_usage: list[dict[str, Any]] = []
|
|
196
|
+
aggregated_fallback_summary: list[dict[str, Any]] = []
|
|
197
|
+
bibliography_path: Path | None = None
|
|
198
|
+
template_engine: str | None = None
|
|
199
|
+
requires_shell_escape = bool(self.runtime.requires_shell_escape)
|
|
200
|
+
rule_descriptions: list[dict[str, Any]] = []
|
|
201
|
+
|
|
202
|
+
document_metadata: list[dict[str, Any]] = []
|
|
203
|
+
asset_paths: set[Path] = set()
|
|
204
|
+
asset_sources: set[Path] = set()
|
|
205
|
+
asset_map: dict[str, Path] = {}
|
|
206
|
+
|
|
207
|
+
for fragment in fragments:
|
|
208
|
+
fragment_default_slot = fragment.default_slot or default_slot
|
|
209
|
+
slot_outputs = dict(fragment.slot_outputs)
|
|
210
|
+
if fragment_default_slot not in slot_outputs:
|
|
211
|
+
slot_outputs[fragment_default_slot] = fragment.latex
|
|
212
|
+
fragment_record: dict[str, Any] = {
|
|
213
|
+
"stem": fragment.stem,
|
|
214
|
+
"default_slot": fragment_default_slot,
|
|
215
|
+
"slot_outputs": dict(slot_outputs),
|
|
216
|
+
"index": len(document_metadata),
|
|
217
|
+
}
|
|
218
|
+
if fragment.front_matter:
|
|
219
|
+
fragment_record["front_matter"] = copy.deepcopy(dict(fragment.front_matter))
|
|
220
|
+
if fragment.source_path:
|
|
221
|
+
fragment_record["source_path"] = str(fragment.source_path)
|
|
222
|
+
document_metadata.append(fragment_record)
|
|
223
|
+
if fragment.assets:
|
|
224
|
+
for key, path in fragment.assets.items():
|
|
225
|
+
target_path = Path(path).resolve()
|
|
226
|
+
asset_map.setdefault(key, target_path)
|
|
227
|
+
asset_paths.add(target_path)
|
|
228
|
+
source_candidate = Path(key)
|
|
229
|
+
if source_candidate.exists():
|
|
230
|
+
asset_sources.add(source_candidate.resolve())
|
|
231
|
+
|
|
232
|
+
for slot_name, latex in slot_outputs.items():
|
|
233
|
+
if not latex:
|
|
234
|
+
continue
|
|
235
|
+
aggregated_slots.setdefault(slot_name, []).append(latex)
|
|
236
|
+
|
|
237
|
+
slot_inclusions = set(fragment.slot_includes or set())
|
|
238
|
+
if slot_inclusions:
|
|
239
|
+
for slot_name in slot_inclusions:
|
|
240
|
+
aggregated_slots.setdefault(slot_name, [])
|
|
241
|
+
for slot_name in slot_inclusions:
|
|
242
|
+
if slot_name == fragment_default_slot:
|
|
243
|
+
continue
|
|
244
|
+
if fragment.latex:
|
|
245
|
+
aggregated_slots[slot_name].append(fragment.latex)
|
|
246
|
+
if (
|
|
247
|
+
fragment_default_slot in slot_inclusions
|
|
248
|
+
and fragment_default_slot not in slot_outputs
|
|
249
|
+
and fragment.latex
|
|
250
|
+
):
|
|
251
|
+
aggregated_slots[fragment_default_slot].append(fragment.latex)
|
|
252
|
+
|
|
253
|
+
fragment_overrides = fragment.template_overrides
|
|
254
|
+
if fragment_overrides:
|
|
255
|
+
if template_overrides is None:
|
|
256
|
+
template_overrides = dict(fragment_overrides)
|
|
257
|
+
else:
|
|
258
|
+
_merge_overrides(template_overrides, fragment_overrides)
|
|
259
|
+
|
|
260
|
+
if fragment.document_state is not None:
|
|
261
|
+
if shared_state is None:
|
|
262
|
+
shared_state = fragment.document_state
|
|
263
|
+
else:
|
|
264
|
+
# Preserve the last state for other fields while merging script usage.
|
|
265
|
+
pass
|
|
266
|
+
aggregated_script_usage = merge_script_usage(
|
|
267
|
+
aggregated_script_usage, getattr(fragment.document_state, "script_usage", [])
|
|
268
|
+
)
|
|
269
|
+
aggregated_fallback_summary = merge_fallback_summaries(
|
|
270
|
+
aggregated_fallback_summary,
|
|
271
|
+
getattr(fragment.document_state, "fallback_summary", []),
|
|
272
|
+
)
|
|
273
|
+
if fragment.bibliography_path is not None:
|
|
274
|
+
bibliography_path = fragment.bibliography_path
|
|
275
|
+
if template_engine is None and fragment.template_engine is not None:
|
|
276
|
+
template_engine = fragment.template_engine
|
|
277
|
+
requires_shell_escape = requires_shell_escape or fragment.requires_shell_escape
|
|
278
|
+
if fragment.rule_descriptions and not rule_descriptions:
|
|
279
|
+
rule_descriptions = list(fragment.rule_descriptions)
|
|
280
|
+
|
|
281
|
+
if shared_state is None:
|
|
282
|
+
shared_state = DocumentState()
|
|
283
|
+
if aggregated_script_usage:
|
|
284
|
+
shared_state.script_usage = merge_script_usage(
|
|
285
|
+
getattr(shared_state, "script_usage", []), aggregated_script_usage
|
|
286
|
+
)
|
|
287
|
+
if aggregated_fallback_summary:
|
|
288
|
+
shared_state.fallback_summary = merge_fallback_summaries(
|
|
289
|
+
getattr(shared_state, "fallback_summary", []), aggregated_fallback_summary
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
slot_content: dict[str, str] = {}
|
|
293
|
+
render_slot_content: dict[str, str] = {
|
|
294
|
+
slot: "\n\n".join(chunks for chunks in content if chunks)
|
|
295
|
+
for slot, content in aggregated_slots.items()
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
written_fragment_paths: list[Path] = []
|
|
299
|
+
|
|
300
|
+
slot_output_overrides: dict[str, str] | None = None
|
|
301
|
+
|
|
302
|
+
if embed_fragments:
|
|
303
|
+
slot_content = dict(render_slot_content)
|
|
304
|
+
else:
|
|
305
|
+
slot_inputs: dict[str, list[str]] = {}
|
|
306
|
+
for fragment in fragments:
|
|
307
|
+
fragment_default_slot = fragment.default_slot or default_slot
|
|
308
|
+
slot_outputs = dict(fragment.slot_outputs)
|
|
309
|
+
if fragment_default_slot not in slot_outputs:
|
|
310
|
+
slot_outputs[fragment_default_slot] = fragment.latex
|
|
311
|
+
|
|
312
|
+
for slot_name, latex in slot_outputs.items():
|
|
313
|
+
if not latex:
|
|
314
|
+
continue
|
|
315
|
+
if slot_name == default_slot or slot_name == fragment.stem:
|
|
316
|
+
filename = f"{fragment.stem}.tex"
|
|
317
|
+
else:
|
|
318
|
+
filename = f"{fragment.stem}.{slot_name}.tex"
|
|
319
|
+
target_path = output_dir / filename
|
|
320
|
+
try:
|
|
321
|
+
target_path.write_text(latex, encoding="utf-8")
|
|
322
|
+
written_fragment_paths.append(target_path)
|
|
323
|
+
slot_inputs.setdefault(slot_name, []).append(
|
|
324
|
+
f"\\input{{{target_path.name}}}"
|
|
325
|
+
)
|
|
326
|
+
except OSError as exc:
|
|
327
|
+
raise TemplateError(
|
|
328
|
+
f"Failed to write fragment '{target_path}': {exc}"
|
|
329
|
+
) from exc
|
|
330
|
+
|
|
331
|
+
slot_inclusions = set(fragment.slot_includes or set())
|
|
332
|
+
if slot_inclusions:
|
|
333
|
+
for slot_name in slot_inclusions:
|
|
334
|
+
if (
|
|
335
|
+
slot_name == fragment_default_slot
|
|
336
|
+
and fragment_default_slot not in slot_outputs
|
|
337
|
+
):
|
|
338
|
+
latex = fragment.latex
|
|
339
|
+
if latex:
|
|
340
|
+
filename = f"{fragment.stem}.tex"
|
|
341
|
+
target_path = output_dir / filename
|
|
342
|
+
try:
|
|
343
|
+
target_path.write_text(latex, encoding="utf-8")
|
|
344
|
+
written_fragment_paths.append(target_path)
|
|
345
|
+
except OSError as exc:
|
|
346
|
+
raise TemplateError(
|
|
347
|
+
f"Failed to write fragment '{target_path}': {exc}"
|
|
348
|
+
) from exc
|
|
349
|
+
slot_inputs.setdefault(slot_name, []).append(
|
|
350
|
+
f"\\input{{{target_path.name}}}"
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
slot_content = {
|
|
354
|
+
slot: "\n".join(entries for entries in slot_inputs.get(slot, []))
|
|
355
|
+
for slot in aggregated_slots
|
|
356
|
+
}
|
|
357
|
+
slot_output_overrides = dict(slot_content)
|
|
358
|
+
aggregated_slots.clear()
|
|
359
|
+
aggregated_slots.update(slot_inputs)
|
|
360
|
+
|
|
361
|
+
_validate_slots(self.runtime, render_slot_content)
|
|
362
|
+
|
|
363
|
+
# Compute fallback summary across all slot content for font fragments.
|
|
364
|
+
concatenated_text = "".join(render_slot_content.values())
|
|
365
|
+
if concatenated_text:
|
|
366
|
+
try:
|
|
367
|
+
raw_summary = FallbackManager(
|
|
368
|
+
cache=FontCache(), logger=FontPipelineLogger()
|
|
369
|
+
).scan_text(concatenated_text)
|
|
370
|
+
if raw_summary:
|
|
371
|
+
aggregated_fallback_summary = merge_fallback_summaries(
|
|
372
|
+
aggregated_fallback_summary, raw_summary
|
|
373
|
+
)
|
|
374
|
+
usage = fallback_summary_to_usage(raw_summary)
|
|
375
|
+
if usage:
|
|
376
|
+
aggregated_script_usage = merge_script_usage(aggregated_script_usage, usage)
|
|
377
|
+
shared_state.script_usage = merge_script_usage(
|
|
378
|
+
getattr(shared_state, "script_usage", []), aggregated_script_usage
|
|
379
|
+
)
|
|
380
|
+
shared_state.fallback_summary = merge_fallback_summaries(
|
|
381
|
+
getattr(shared_state, "fallback_summary", []),
|
|
382
|
+
aggregated_fallback_summary,
|
|
383
|
+
)
|
|
384
|
+
except Exception:
|
|
385
|
+
# Best-effort: fallback detection should never block rendering.
|
|
386
|
+
pass
|
|
387
|
+
|
|
388
|
+
template_instance = self.runtime.instance
|
|
389
|
+
if template_instance is None: # pragma: no cover - defensive path
|
|
390
|
+
raise TemplateError("Template runtime is missing an instance implementation.")
|
|
391
|
+
|
|
392
|
+
if template_overrides is None:
|
|
393
|
+
template_overrides = {}
|
|
394
|
+
if overrides:
|
|
395
|
+
override_dict = dict(overrides)
|
|
396
|
+
_merge_overrides(template_overrides, override_dict)
|
|
397
|
+
|
|
398
|
+
if document_metadata:
|
|
399
|
+
if template_overrides is None:
|
|
400
|
+
template_overrides = {}
|
|
401
|
+
template_overrides["documents"] = document_metadata
|
|
402
|
+
|
|
403
|
+
if template_overrides:
|
|
404
|
+
record_event(
|
|
405
|
+
self.emitter,
|
|
406
|
+
"template_overrides",
|
|
407
|
+
{"values": dict(template_overrides)},
|
|
408
|
+
)
|
|
409
|
+
if aggregated_script_usage:
|
|
410
|
+
template_overrides = template_overrides or {}
|
|
411
|
+
template_overrides.setdefault("fonts", {})
|
|
412
|
+
if isinstance(template_overrides["fonts"], dict):
|
|
413
|
+
template_overrides["fonts"].setdefault("script_usage", aggregated_script_usage)
|
|
414
|
+
|
|
415
|
+
def _iter_strings(value: Any) -> list[str]:
|
|
416
|
+
if isinstance(value, str):
|
|
417
|
+
return [value]
|
|
418
|
+
if isinstance(value, Mapping):
|
|
419
|
+
collected: list[str] = []
|
|
420
|
+
for nested in value.values():
|
|
421
|
+
collected.extend(_iter_strings(nested))
|
|
422
|
+
return collected
|
|
423
|
+
if isinstance(value, (list, tuple, set)):
|
|
424
|
+
collected: list[str] = []
|
|
425
|
+
for nested in value:
|
|
426
|
+
collected.extend(_iter_strings(nested))
|
|
427
|
+
return collected
|
|
428
|
+
return []
|
|
429
|
+
|
|
430
|
+
metadata_strings: list[str] = []
|
|
431
|
+
for record in document_metadata:
|
|
432
|
+
front_matter = record.get("front_matter")
|
|
433
|
+
if isinstance(front_matter, Mapping):
|
|
434
|
+
metadata_strings.extend(_iter_strings(front_matter))
|
|
435
|
+
if template_overrides:
|
|
436
|
+
metadata_strings.extend(_iter_strings(template_overrides))
|
|
437
|
+
|
|
438
|
+
metadata_blob = " ".join(part for part in metadata_strings if part.strip())
|
|
439
|
+
if metadata_blob:
|
|
440
|
+
try:
|
|
441
|
+
metadata_raw = FallbackManager(
|
|
442
|
+
cache=FontCache(), logger=FontPipelineLogger()
|
|
443
|
+
).scan_text(metadata_blob)
|
|
444
|
+
except Exception:
|
|
445
|
+
metadata_raw = []
|
|
446
|
+
if metadata_raw:
|
|
447
|
+
aggregated_fallback_summary = merge_fallback_summaries(
|
|
448
|
+
aggregated_fallback_summary, metadata_raw
|
|
449
|
+
)
|
|
450
|
+
metadata_usage = fallback_summary_to_usage(metadata_raw)
|
|
451
|
+
if metadata_usage:
|
|
452
|
+
aggregated_script_usage = merge_script_usage(
|
|
453
|
+
aggregated_script_usage, metadata_usage
|
|
454
|
+
)
|
|
455
|
+
shared_state.script_usage = merge_script_usage(
|
|
456
|
+
getattr(shared_state, "script_usage", []), aggregated_script_usage
|
|
457
|
+
)
|
|
458
|
+
shared_state.fallback_summary = merge_fallback_summaries(
|
|
459
|
+
getattr(shared_state, "fallback_summary", []), aggregated_fallback_summary
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
if aggregated_script_usage or aggregated_fallback_summary:
|
|
463
|
+
template_overrides = template_overrides or {}
|
|
464
|
+
template_overrides.setdefault("fonts", {})
|
|
465
|
+
if isinstance(template_overrides["fonts"], dict):
|
|
466
|
+
if aggregated_script_usage:
|
|
467
|
+
existing_usage = template_overrides["fonts"].get("script_usage", [])
|
|
468
|
+
template_overrides["fonts"]["script_usage"] = merge_script_usage(
|
|
469
|
+
existing_usage if isinstance(existing_usage, list) else [],
|
|
470
|
+
aggregated_script_usage,
|
|
471
|
+
)
|
|
472
|
+
if aggregated_fallback_summary:
|
|
473
|
+
existing_fallback = template_overrides["fonts"].get("fallback_summary", [])
|
|
474
|
+
template_overrides["fonts"]["fallback_summary"] = merge_fallback_summaries(
|
|
475
|
+
existing_fallback if isinstance(existing_fallback, list) else [],
|
|
476
|
+
aggregated_fallback_summary,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
main_name = self._resolve_main_name(fragments)
|
|
480
|
+
try:
|
|
481
|
+
wrap_result = wrap_template_document(
|
|
482
|
+
template=template_instance,
|
|
483
|
+
default_slot=default_slot,
|
|
484
|
+
slot_outputs=render_slot_content,
|
|
485
|
+
slot_output_overrides=slot_output_overrides,
|
|
486
|
+
document_state=shared_state,
|
|
487
|
+
template_overrides=template_overrides if template_overrides else None,
|
|
488
|
+
output_dir=output_dir,
|
|
489
|
+
copy_assets=copy_assets,
|
|
490
|
+
output_name=main_name,
|
|
491
|
+
bibliography_path=bibliography_path,
|
|
492
|
+
emitter=self.emitter,
|
|
493
|
+
fragments=list(
|
|
494
|
+
template_overrides.get("fragments", self.runtime.extras.get("fragments", []))
|
|
495
|
+
),
|
|
496
|
+
template_runtime=self.runtime,
|
|
497
|
+
)
|
|
498
|
+
except TemplateError as exc:
|
|
499
|
+
if debug_enabled(self.emitter):
|
|
500
|
+
raise
|
|
501
|
+
raise_conversion_error(self.emitter, str(exc), exc)
|
|
502
|
+
|
|
503
|
+
template_context = wrap_result.template_context or {}
|
|
504
|
+
main_tex_path = wrap_result.output_path or (output_dir / main_name)
|
|
505
|
+
|
|
506
|
+
fragment_paths: list[Path] = [
|
|
507
|
+
Path(fragment.output_path) for fragment in fragments if fragment.output_path
|
|
508
|
+
]
|
|
509
|
+
fragment_paths.extend(written_fragment_paths)
|
|
510
|
+
asset_paths.update(wrap_result.asset_paths or [])
|
|
511
|
+
for source, destination in getattr(wrap_result, "asset_pairs", []):
|
|
512
|
+
resolved_dest = Path(destination).resolve()
|
|
513
|
+
asset_paths.add(resolved_dest)
|
|
514
|
+
asset_sources.add(Path(source).resolve())
|
|
515
|
+
asset_map.setdefault(str(source), resolved_dest)
|
|
516
|
+
|
|
517
|
+
context_engine: str | None = None
|
|
518
|
+
if template_context:
|
|
519
|
+
raw_engine = template_context.get("latex_engine")
|
|
520
|
+
if isinstance(raw_engine, str):
|
|
521
|
+
stripped = raw_engine.strip()
|
|
522
|
+
if stripped:
|
|
523
|
+
context_engine = stripped
|
|
524
|
+
|
|
525
|
+
template_engine = self.runtime.engine
|
|
526
|
+
|
|
527
|
+
if context_engine and context_engine.lower() != "pdflatex":
|
|
528
|
+
template_engine = context_engine
|
|
529
|
+
if template_engine is None:
|
|
530
|
+
template_engine = context_engine or "pdflatex"
|
|
531
|
+
|
|
532
|
+
template_context.setdefault("latex_engine", template_engine)
|
|
533
|
+
|
|
534
|
+
self._write_latexmkrc(
|
|
535
|
+
output_dir=output_dir,
|
|
536
|
+
main_tex_path=main_tex_path,
|
|
537
|
+
template_engine=template_engine,
|
|
538
|
+
requires_shell_escape=requires_shell_escape,
|
|
539
|
+
document_state=shared_state,
|
|
540
|
+
template_context=template_context,
|
|
541
|
+
bibliography_present=bool(bibliography_path),
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
asset_path_list = sorted({path.resolve() for path in asset_paths})
|
|
545
|
+
asset_source_list = sorted({path.resolve() for path in asset_sources})
|
|
546
|
+
|
|
547
|
+
context_attributes = summarise_context_usage(
|
|
548
|
+
template_instance,
|
|
549
|
+
template_context,
|
|
550
|
+
fragment_names=wrap_result.rendered_fragments,
|
|
551
|
+
overrides=template_overrides,
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
return TemplateRendererResult(
|
|
555
|
+
main_tex_path=main_tex_path,
|
|
556
|
+
fragment_paths=fragment_paths,
|
|
557
|
+
template_context=template_context or {},
|
|
558
|
+
slot_content=slot_output_overrides or slot_content,
|
|
559
|
+
document_state=shared_state,
|
|
560
|
+
bibliography_path=bibliography_path,
|
|
561
|
+
template_engine=template_engine,
|
|
562
|
+
requires_shell_escape=requires_shell_escape,
|
|
563
|
+
template_overrides=template_overrides,
|
|
564
|
+
rule_descriptions=rule_descriptions,
|
|
565
|
+
asset_paths=asset_path_list,
|
|
566
|
+
asset_sources=asset_source_list,
|
|
567
|
+
asset_map=asset_map,
|
|
568
|
+
context_attributes=context_attributes,
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
@staticmethod
|
|
572
|
+
def _resolve_main_name(fragments: Sequence[TemplateFragment]) -> str:
|
|
573
|
+
if len(fragments) == 1:
|
|
574
|
+
return f"{fragments[0].stem}.tex"
|
|
575
|
+
return "main.tex"
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
__all__ = ["TemplateFragment", "TemplateRenderer", "TemplateRendererResult"]
|