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,287 @@
|
|
|
1
|
+
"""Template orchestration helpers exposed by the TeXSmith API.
|
|
2
|
+
|
|
3
|
+
Architecture
|
|
4
|
+
: `TemplateSession` owns lifecycle management: it accepts documents, applies
|
|
5
|
+
metadata overrides, and delegates aggregation to :class:`TemplateRenderer`.
|
|
6
|
+
: `TemplateRenderer` combines conversion bundles into slot-aware LaTeX output,
|
|
7
|
+
copies template assets, and prepares the final context consumed by wrappers.
|
|
8
|
+
: `TemplateOptions` is a thin wrapper around a mutable mapping that keeps
|
|
9
|
+
user-supplied overrides isolated from the template defaults. Its API is
|
|
10
|
+
intentionally dictionary-like to integrate smoothly with CLI parsing.
|
|
11
|
+
: `TemplateRenderResult` captures the products of a render pass, including
|
|
12
|
+
computed context, bibliography location, and shell-escape requirements so
|
|
13
|
+
downstream tools can decide how to compile the LaTeX output.
|
|
14
|
+
|
|
15
|
+
Implementation Rationale
|
|
16
|
+
: Splitting lifecycle coordination (`TemplateSession`) from rendering concerns
|
|
17
|
+
(`TemplateRenderer`) keeps slot aggregation logic in a single place and
|
|
18
|
+
reduces duplication across front ends.
|
|
19
|
+
: Options are separated from documents to prevent accidental mutation of the
|
|
20
|
+
default template metadata. Copy semantics are explicit, enabling safe reuse of
|
|
21
|
+
sessions with different overrides.
|
|
22
|
+
|
|
23
|
+
Usage Example
|
|
24
|
+
:
|
|
25
|
+
>>> from types import SimpleNamespace
|
|
26
|
+
>>> from texsmith.api.templates import TemplateSession
|
|
27
|
+
>>> from texsmith.core.templates import TemplateRuntime, TemplateSlot
|
|
28
|
+
>>> dummy_info = SimpleNamespace(attributes={"cover_color": "indigo"})
|
|
29
|
+
>>> dummy_template = SimpleNamespace(info=dummy_info)
|
|
30
|
+
>>> runtime = TemplateRuntime(
|
|
31
|
+
... instance=dummy_template,
|
|
32
|
+
... name="demo",
|
|
33
|
+
... engine=None,
|
|
34
|
+
... requires_shell_escape=False,
|
|
35
|
+
... slots={"mainmatter": TemplateSlot(default=True)},
|
|
36
|
+
... default_slot="mainmatter",
|
|
37
|
+
... formatter_overrides={},
|
|
38
|
+
... base_level=None,
|
|
39
|
+
... )
|
|
40
|
+
>>> session = TemplateSession(runtime=runtime)
|
|
41
|
+
>>> session.get_default_options().to_dict()["cover_color"]
|
|
42
|
+
'indigo'
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
from collections.abc import Mapping, Sequence
|
|
48
|
+
import copy
|
|
49
|
+
from dataclasses import dataclass, field
|
|
50
|
+
from pathlib import Path
|
|
51
|
+
from typing import Any
|
|
52
|
+
|
|
53
|
+
from texsmith.core.context import DocumentState
|
|
54
|
+
from texsmith.core.fragments import collect_fragment_attribute_defaults
|
|
55
|
+
from texsmith.core.metadata import PressMetadataError, normalise_press_metadata
|
|
56
|
+
from texsmith.core.templates import (
|
|
57
|
+
TemplateError,
|
|
58
|
+
TemplateRuntime,
|
|
59
|
+
load_template_runtime,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
from ..core.conversion.debug import ensure_emitter
|
|
63
|
+
from ..core.conversion.renderer import TemplateRenderer
|
|
64
|
+
from ..core.diagnostics import DiagnosticEmitter
|
|
65
|
+
from .document import Document
|
|
66
|
+
from .pipeline import RenderSettings, convert_documents, to_template_fragments
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
__all__ = [
|
|
70
|
+
"TemplateOptions",
|
|
71
|
+
"TemplateRenderResult",
|
|
72
|
+
"TemplateSession",
|
|
73
|
+
"get_template",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(slots=True)
|
|
78
|
+
class TemplateOptions:
|
|
79
|
+
"""Mutable mapping of template-level overrides."""
|
|
80
|
+
|
|
81
|
+
_values: dict[str, Any] = field(default_factory=dict)
|
|
82
|
+
|
|
83
|
+
def __getattr__(self, name: str) -> Any:
|
|
84
|
+
try:
|
|
85
|
+
return self._values[name]
|
|
86
|
+
except KeyError as exc: # pragma: no cover - attribute passthrough
|
|
87
|
+
raise AttributeError(name) from exc
|
|
88
|
+
|
|
89
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
90
|
+
if name == "_values":
|
|
91
|
+
object.__setattr__(self, name, value)
|
|
92
|
+
else:
|
|
93
|
+
self._values[name] = value
|
|
94
|
+
|
|
95
|
+
def to_dict(self) -> dict[str, Any]:
|
|
96
|
+
"""Return a deep copy of the underlying values so callers can mutate safely."""
|
|
97
|
+
return copy.deepcopy(self._values)
|
|
98
|
+
|
|
99
|
+
def copy(self) -> TemplateOptions:
|
|
100
|
+
"""Return a deep copy of the option set to isolate options between sessions."""
|
|
101
|
+
return TemplateOptions(self.to_dict())
|
|
102
|
+
|
|
103
|
+
def update(self, values: Mapping[str, Any] | None = None, **extra: Any) -> None:
|
|
104
|
+
"""Update the option set in-place, mirroring dict semantics for familiarity."""
|
|
105
|
+
if values:
|
|
106
|
+
self._values.update(values)
|
|
107
|
+
if extra:
|
|
108
|
+
self._values.update(extra)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(slots=True)
|
|
112
|
+
class TemplateRenderResult:
|
|
113
|
+
"""Artifacts yielded by a :class:`TemplateSession` render pass."""
|
|
114
|
+
|
|
115
|
+
main_tex_path: Path
|
|
116
|
+
fragment_paths: list[Path]
|
|
117
|
+
context: dict[str, Any]
|
|
118
|
+
template_runtime: TemplateRuntime
|
|
119
|
+
document_state: DocumentState
|
|
120
|
+
bibliography_path: Path | None
|
|
121
|
+
template_engine: str | None
|
|
122
|
+
requires_shell_escape: bool
|
|
123
|
+
rule_descriptions: list[dict[str, Any]] = field(default_factory=list)
|
|
124
|
+
asset_paths: list[Path] = field(default_factory=list)
|
|
125
|
+
asset_sources: list[Path] = field(default_factory=list)
|
|
126
|
+
asset_map: dict[str, Path] = field(default_factory=dict)
|
|
127
|
+
context_attributes: list[dict[str, Any]] = field(default_factory=list)
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def has_bibliography(self) -> bool:
|
|
131
|
+
"""Indicate whether a bibliography was generated so callers can choose engines accordingly."""
|
|
132
|
+
return bool(self.bibliography_path)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class TemplateSession:
|
|
136
|
+
"""Manage template state while delegating rendering to :class:`TemplateRenderer`."""
|
|
137
|
+
|
|
138
|
+
def __init__(
|
|
139
|
+
self,
|
|
140
|
+
runtime: TemplateRuntime,
|
|
141
|
+
*,
|
|
142
|
+
settings: RenderSettings | None = None,
|
|
143
|
+
emitter: DiagnosticEmitter | None = None,
|
|
144
|
+
) -> None:
|
|
145
|
+
self.runtime = runtime
|
|
146
|
+
attributes: dict[str, Any] = {}
|
|
147
|
+
if runtime.instance:
|
|
148
|
+
attributes = runtime.instance.info.attribute_defaults()
|
|
149
|
+
attributes.update(runtime.instance.info.emit_defaults())
|
|
150
|
+
fragment_defaults: dict[str, Any] = {}
|
|
151
|
+
if runtime.extras:
|
|
152
|
+
fragment_defaults = collect_fragment_attribute_defaults(
|
|
153
|
+
runtime.extras.get("fragments") or []
|
|
154
|
+
)
|
|
155
|
+
merged_defaults = copy.deepcopy(attributes)
|
|
156
|
+
merged_defaults.update(fragment_defaults)
|
|
157
|
+
self._defaults: dict[str, Any] = copy.deepcopy(merged_defaults)
|
|
158
|
+
self._overrides = TemplateOptions()
|
|
159
|
+
self._documents: list[Document] = []
|
|
160
|
+
self._bibliography_files: list[Path] = []
|
|
161
|
+
self.settings = settings.copy() if settings else RenderSettings()
|
|
162
|
+
self.emitter = ensure_emitter(emitter)
|
|
163
|
+
|
|
164
|
+
def _prepare_document(self, document: Document) -> Document:
|
|
165
|
+
"""Return a document copy with template overrides applied to keep the session immutable."""
|
|
166
|
+
return document.copy()
|
|
167
|
+
|
|
168
|
+
def _collect_option_overrides(self) -> dict[str, Any]:
|
|
169
|
+
"""Compute overrides that differ from the template defaults to minimise render payloads."""
|
|
170
|
+
overrides = self._overrides.to_dict()
|
|
171
|
+
if not overrides:
|
|
172
|
+
return {}
|
|
173
|
+
try:
|
|
174
|
+
normalise_press_metadata(overrides)
|
|
175
|
+
except PressMetadataError as exc:
|
|
176
|
+
raise TemplateError(str(exc)) from exc
|
|
177
|
+
return overrides
|
|
178
|
+
|
|
179
|
+
def get_default_options(self) -> TemplateOptions:
|
|
180
|
+
"""Return a copy of the default template options for caller inspection without mutation."""
|
|
181
|
+
return TemplateOptions(copy.deepcopy(self._defaults))
|
|
182
|
+
|
|
183
|
+
def set_options(self, options: TemplateOptions | Mapping[str, Any]) -> None:
|
|
184
|
+
"""Replace the current template overrides, allowing bulk configuration resets."""
|
|
185
|
+
if isinstance(options, TemplateOptions):
|
|
186
|
+
self._overrides = options.copy()
|
|
187
|
+
else:
|
|
188
|
+
self._overrides = TemplateOptions(dict(options))
|
|
189
|
+
|
|
190
|
+
def update_options(self, values: Mapping[str, Any] | None = None, **extra: Any) -> None:
|
|
191
|
+
"""Update the current template overrides to adjust session metadata incrementally."""
|
|
192
|
+
self._overrides.update(values, **extra)
|
|
193
|
+
|
|
194
|
+
def add_bibliography(self, *paths: Path) -> None:
|
|
195
|
+
"""Register bibliography files applied to all documents so renderers include them once."""
|
|
196
|
+
for path in paths:
|
|
197
|
+
resolved = Path(path)
|
|
198
|
+
if resolved not in self._bibliography_files:
|
|
199
|
+
self._bibliography_files.append(resolved)
|
|
200
|
+
|
|
201
|
+
def add_document(
|
|
202
|
+
self,
|
|
203
|
+
document: Document,
|
|
204
|
+
*,
|
|
205
|
+
slot: str | None = None,
|
|
206
|
+
selector: str | None = None,
|
|
207
|
+
include_document: bool | None = None,
|
|
208
|
+
) -> Document:
|
|
209
|
+
"""Register a document for rendering, storing a copy to prevent caller mutations."""
|
|
210
|
+
doc = document.copy()
|
|
211
|
+
if slot is not None:
|
|
212
|
+
doc.assign_slot(slot, selector=selector, include_document=include_document)
|
|
213
|
+
self._documents.append(doc)
|
|
214
|
+
return doc
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def documents(self) -> Sequence[Document]:
|
|
218
|
+
"""Return the registered documents as an immutable tuple to discourage in-place edits."""
|
|
219
|
+
return tuple(self._documents)
|
|
220
|
+
|
|
221
|
+
def render(self, output_dir: Path, *, embed_fragments: bool = True) -> TemplateRenderResult:
|
|
222
|
+
"""Render the registered documents into a LaTeX project, preparing outputs on disk for compilers."""
|
|
223
|
+
if not self._documents:
|
|
224
|
+
raise ValueError("At least one document must be added before rendering.")
|
|
225
|
+
|
|
226
|
+
output_dir = output_dir.resolve()
|
|
227
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
228
|
+
|
|
229
|
+
prepared_documents = [self._prepare_document(document) for document in self._documents]
|
|
230
|
+
option_overrides = self._collect_option_overrides()
|
|
231
|
+
|
|
232
|
+
bundle = convert_documents(
|
|
233
|
+
prepared_documents,
|
|
234
|
+
output_dir=output_dir,
|
|
235
|
+
settings=self.settings,
|
|
236
|
+
emitter=self.emitter,
|
|
237
|
+
bibliography_files=self._bibliography_files,
|
|
238
|
+
template=self.runtime.name,
|
|
239
|
+
template_runtime=self.runtime,
|
|
240
|
+
template_overrides=option_overrides or None,
|
|
241
|
+
wrap_document=False,
|
|
242
|
+
write_fragments=False,
|
|
243
|
+
)
|
|
244
|
+
fragments = to_template_fragments(bundle)
|
|
245
|
+
|
|
246
|
+
renderer = TemplateRenderer(self.runtime, emitter=self.emitter)
|
|
247
|
+
try:
|
|
248
|
+
rendered = renderer.render(
|
|
249
|
+
fragments,
|
|
250
|
+
output_dir=output_dir,
|
|
251
|
+
overrides=option_overrides or None,
|
|
252
|
+
copy_assets=self.settings.copy_assets,
|
|
253
|
+
embed_fragments=embed_fragments,
|
|
254
|
+
)
|
|
255
|
+
except TemplateError as exc:
|
|
256
|
+
message = str(exc)
|
|
257
|
+
self.emitter.error(message, exc)
|
|
258
|
+
raise
|
|
259
|
+
|
|
260
|
+
return TemplateRenderResult(
|
|
261
|
+
main_tex_path=rendered.main_tex_path,
|
|
262
|
+
fragment_paths=rendered.fragment_paths,
|
|
263
|
+
context=rendered.template_context,
|
|
264
|
+
template_runtime=self.runtime,
|
|
265
|
+
document_state=rendered.document_state,
|
|
266
|
+
bibliography_path=rendered.bibliography_path,
|
|
267
|
+
template_engine=rendered.template_engine,
|
|
268
|
+
requires_shell_escape=rendered.requires_shell_escape,
|
|
269
|
+
rule_descriptions=rendered.rule_descriptions,
|
|
270
|
+
asset_paths=rendered.asset_paths,
|
|
271
|
+
asset_sources=rendered.asset_sources,
|
|
272
|
+
asset_map=rendered.asset_map,
|
|
273
|
+
context_attributes=rendered.context_attributes,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def get_template(identifier: str | Path, **kwargs: Any) -> TemplateSession:
|
|
278
|
+
"""Instantiate a :class:`TemplateSession` for the requested template."""
|
|
279
|
+
runtime = load_template_runtime(str(identifier))
|
|
280
|
+
settings = kwargs.pop("settings", None)
|
|
281
|
+
emitter = kwargs.pop("emitter", None)
|
|
282
|
+
if "callbacks" in kwargs:
|
|
283
|
+
raise TypeError("'callbacks' is no longer supported; provide an emitter instead.")
|
|
284
|
+
if kwargs:
|
|
285
|
+
unexpected = ", ".join(sorted(kwargs))
|
|
286
|
+
raise TypeError(f"Unexpected keyword arguments: {unexpected}")
|
|
287
|
+
return TemplateSession(runtime, settings=settings, emitter=emitter)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Bibliography facade exposed through the TeXSmith public API.
|
|
2
|
+
|
|
3
|
+
Architecture
|
|
4
|
+
: `BibliographyCollection` centralises reference merging, deduplication, and
|
|
5
|
+
portable export so higher layers can treat bibliographies as immutable
|
|
6
|
+
dictionaries. The collection records provenance internally so API consumers
|
|
7
|
+
can report issues with source context.
|
|
8
|
+
: `DoiBibliographyFetcher` encapsulates remote lookups so IO code remains outside
|
|
9
|
+
the pure transformation layers. Callers provide a DOI and receive parsed
|
|
10
|
+
BibTeX data ready to inject into the collection.
|
|
11
|
+
: `bibliography_data_from_string` accepts inline BibTeX payloads and converts
|
|
12
|
+
them into `BibliographyData` objects, enabling templating systems to embed
|
|
13
|
+
references alongside content.
|
|
14
|
+
|
|
15
|
+
Implementation Rationale
|
|
16
|
+
: The public API needs a stable, documented entry point that is decoupled from
|
|
17
|
+
the evolving internal package layout. Re-exporting the curated primitives keeps
|
|
18
|
+
backward compatibility guarantees manageable.
|
|
19
|
+
: Aggregation logic lives in `collection.py` so both the CLI and the programmatic
|
|
20
|
+
API can reuse it. By funnelling access through this module we expose
|
|
21
|
+
documentation and doctest examples close to the import surface users reach for
|
|
22
|
+
first.
|
|
23
|
+
|
|
24
|
+
Usage Example
|
|
25
|
+
|
|
26
|
+
```pycon
|
|
27
|
+
>>> from texsmith.core.bibliography import BibliographyCollection, bibliography_data_from_string
|
|
28
|
+
>>> collection = BibliographyCollection()
|
|
29
|
+
>>> payload = \"\"\"@article{doe2023,
|
|
30
|
+
... author = {Doe, Jane},
|
|
31
|
+
... title = {A Minimal Example},
|
|
32
|
+
... year = {2023},
|
|
33
|
+
... }\"\"\"
|
|
34
|
+
>>> inline = bibliography_data_from_string(payload, "doe2023")
|
|
35
|
+
>>> collection.load_data(inline, source="inline.bib")
|
|
36
|
+
>>> reference = collection.find("doe2023")
|
|
37
|
+
>>> reference["fields"]["title"]
|
|
38
|
+
'A Minimal Example'
|
|
39
|
+
```
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
from .collection import BibliographyCollection
|
|
45
|
+
from .doi import DoiBibliographyFetcher, DoiLookupError
|
|
46
|
+
from .issues import BibliographyIssue
|
|
47
|
+
from .parsing import bibliography_data_from_inline_entry, bibliography_data_from_string
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"BibliographyCollection",
|
|
52
|
+
"BibliographyIssue",
|
|
53
|
+
"DoiBibliographyFetcher",
|
|
54
|
+
"DoiLookupError",
|
|
55
|
+
"bibliography_data_from_inline_entry",
|
|
56
|
+
"bibliography_data_from_string",
|
|
57
|
+
]
|