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.
Files changed (252) hide show
  1. texsmith/__init__.py +107 -0
  2. texsmith/_alias.py +59 -0
  3. texsmith/adapters/__init__.py +6 -0
  4. texsmith/adapters/docker.py +258 -0
  5. texsmith/adapters/handlers/__init__.py +3 -0
  6. texsmith/adapters/handlers/_assets.py +363 -0
  7. texsmith/adapters/handlers/_helpers.py +62 -0
  8. texsmith/adapters/handlers/_mermaid.py +122 -0
  9. texsmith/adapters/handlers/admonitions.py +267 -0
  10. texsmith/adapters/handlers/basic.py +228 -0
  11. texsmith/adapters/handlers/blocks.py +851 -0
  12. texsmith/adapters/handlers/code.py +309 -0
  13. texsmith/adapters/handlers/inline.py +937 -0
  14. texsmith/adapters/handlers/links.py +239 -0
  15. texsmith/adapters/handlers/media.py +380 -0
  16. texsmith/adapters/latex/__init__.py +10 -0
  17. texsmith/adapters/latex/engines/__init__.py +726 -0
  18. texsmith/adapters/latex/engines/latex/__init__.py +26 -0
  19. texsmith/adapters/latex/engines/latex/log.py +720 -0
  20. texsmith/adapters/latex/engines/latex/runner.py +28 -0
  21. texsmith/adapters/latex/engines/tectonic/__init__.py +38 -0
  22. texsmith/adapters/latex/formatter.py +297 -0
  23. texsmith/adapters/latex/latexmk.py +148 -0
  24. texsmith/adapters/latex/partials/acronym.tex +1 -0
  25. texsmith/adapters/latex/partials/add.tex +1 -0
  26. texsmith/adapters/latex/partials/addition.tex +1 -0
  27. texsmith/adapters/latex/partials/blockquote.tex +3 -0
  28. texsmith/adapters/latex/partials/callout.tex +3 -0
  29. texsmith/adapters/latex/partials/choices.tex +5 -0
  30. texsmith/adapters/latex/partials/citation.tex +1 -0
  31. texsmith/adapters/latex/partials/codeblock.tex +7 -0
  32. texsmith/adapters/latex/partials/codeblock_listings.tex +5 -0
  33. texsmith/adapters/latex/partials/codeblock_pygments.tex +5 -0
  34. texsmith/adapters/latex/partials/codeblock_verbatim.tex +12 -0
  35. texsmith/adapters/latex/partials/codeinline.tex +1 -0
  36. texsmith/adapters/latex/partials/codeinlinett.tex +1 -0
  37. texsmith/adapters/latex/partials/comment.tex +1 -0
  38. texsmith/adapters/latex/partials/del.tex +1 -0
  39. texsmith/adapters/latex/partials/deletion.tex +1 -0
  40. texsmith/adapters/latex/partials/description_list.tex +5 -0
  41. texsmith/adapters/latex/partials/enquote.tex +1 -0
  42. texsmith/adapters/latex/partials/epigraph.tex +1 -0
  43. texsmith/adapters/latex/partials/exercises_solutions.tex +7 -0
  44. texsmith/adapters/latex/partials/figure.tex +33 -0
  45. texsmith/adapters/latex/partials/figure_tcolorbox.tex +15 -0
  46. texsmith/adapters/latex/partials/footnote.tex +3 -0
  47. texsmith/adapters/latex/partials/glossary.tex +1 -0
  48. texsmith/adapters/latex/partials/heading.tex +14 -0
  49. texsmith/adapters/latex/partials/highlight.tex +25 -0
  50. texsmith/adapters/latex/partials/horizontal_rule.tex +1 -0
  51. texsmith/adapters/latex/partials/href.tex +1 -0
  52. texsmith/adapters/latex/partials/icon.tex +1 -0
  53. texsmith/adapters/latex/partials/include.tex +1 -0
  54. texsmith/adapters/latex/partials/index.tex +1 -0
  55. texsmith/adapters/latex/partials/italic.tex +1 -0
  56. texsmith/adapters/latex/partials/keystroke.tex +46 -0
  57. texsmith/adapters/latex/partials/label.tex +1 -0
  58. texsmith/adapters/latex/partials/list_acronyms.tex +5 -0
  59. texsmith/adapters/latex/partials/list_glossary.tex +8 -0
  60. texsmith/adapters/latex/partials/multicolumn.tex +3 -0
  61. texsmith/adapters/latex/partials/ordered_list.tex +5 -0
  62. texsmith/adapters/latex/partials/pagestyle.tex +1 -0
  63. texsmith/adapters/latex/partials/ref.tex +1 -0
  64. texsmith/adapters/latex/partials/regex.tex +1 -0
  65. texsmith/adapters/latex/partials/smallcaps.tex +1 -0
  66. texsmith/adapters/latex/partials/strikethrough.tex +1 -0
  67. texsmith/adapters/latex/partials/strong.tex +1 -0
  68. texsmith/adapters/latex/partials/subscript.tex +1 -0
  69. texsmith/adapters/latex/partials/substitution.tex +1 -0
  70. texsmith/adapters/latex/partials/superscript.tex +1 -0
  71. texsmith/adapters/latex/partials/tabbed.tex +3 -0
  72. texsmith/adapters/latex/partials/table.tex +48 -0
  73. texsmith/adapters/latex/partials/underline.tex +1 -0
  74. texsmith/adapters/latex/partials/unordered_list.tex +5 -0
  75. texsmith/adapters/latex/partials/url.tex +1 -0
  76. texsmith/adapters/latex/pygments.py +98 -0
  77. texsmith/adapters/latex/pyxindy.py +64 -0
  78. texsmith/adapters/latex/renderer.py +220 -0
  79. texsmith/adapters/latex/tectonic.py +366 -0
  80. texsmith/adapters/latex/utils.py +86 -0
  81. texsmith/adapters/markdown/__init__.py +342 -0
  82. texsmith/adapters/plugins/__init__.py +8 -0
  83. texsmith/adapters/plugins/material.py +174 -0
  84. texsmith/adapters/plugins/snippet.py +1734 -0
  85. texsmith/adapters/transformers/__init__.py +122 -0
  86. texsmith/adapters/transformers/base.py +140 -0
  87. texsmith/adapters/transformers/strategies.py +1456 -0
  88. texsmith/adapters/transformers/utils.py +59 -0
  89. texsmith/api/__init__.py +80 -0
  90. texsmith/api/_utils.py +56 -0
  91. texsmith/api/document.py +645 -0
  92. texsmith/api/pipeline.py +229 -0
  93. texsmith/api/service.py +648 -0
  94. texsmith/api/templates.py +287 -0
  95. texsmith/core/__init__.py +6 -0
  96. texsmith/core/bibliography/__init__.py +57 -0
  97. texsmith/core/bibliography/collection.py +354 -0
  98. texsmith/core/bibliography/doi.py +194 -0
  99. texsmith/core/bibliography/issues.py +15 -0
  100. texsmith/core/bibliography/parsing.py +45 -0
  101. texsmith/core/callouts.py +99 -0
  102. texsmith/core/config.py +191 -0
  103. texsmith/core/context.py +242 -0
  104. texsmith/core/conversion/__init__.py +103 -0
  105. texsmith/core/conversion/core.py +780 -0
  106. texsmith/core/conversion/debug.py +87 -0
  107. texsmith/core/conversion/inputs.py +474 -0
  108. texsmith/core/conversion/renderer.py +578 -0
  109. texsmith/core/conversion/templates.py +646 -0
  110. texsmith/core/conversion_contexts.py +95 -0
  111. texsmith/core/diagnostics.py +118 -0
  112. texsmith/core/exceptions.py +41 -0
  113. texsmith/core/fonts/__init__.py +3 -0
  114. texsmith/core/fragments/__init__.py +716 -0
  115. texsmith/core/fragments/base.py +117 -0
  116. texsmith/core/metadata.py +280 -0
  117. texsmith/core/mustache.py +86 -0
  118. texsmith/core/partials.py +20 -0
  119. texsmith/core/rules.py +394 -0
  120. texsmith/core/templates/__init__.py +58 -0
  121. texsmith/core/templates/base.py +343 -0
  122. texsmith/core/templates/builtins.py +68 -0
  123. texsmith/core/templates/context_usage.py +137 -0
  124. texsmith/core/templates/loader.py +303 -0
  125. texsmith/core/templates/manifest.py +861 -0
  126. texsmith/core/templates/runtime.py +347 -0
  127. texsmith/core/templates/text.py +14 -0
  128. texsmith/core/templates/wrapper.py +340 -0
  129. texsmith/core/user_dir.py +179 -0
  130. texsmith/devtools.py +28 -0
  131. texsmith/extensions/__init__.py +162 -0
  132. texsmith/extensions/index/__init__.py +21 -0
  133. texsmith/extensions/index/markdown.py +94 -0
  134. texsmith/extensions/index/mkdocs_plugin.py +136 -0
  135. texsmith/extensions/index/registry.py +57 -0
  136. texsmith/extensions/index/renderer.py +183 -0
  137. texsmith/extensions/index/templates/index.tex +1 -0
  138. texsmith/extensions/latex_raw.py +115 -0
  139. texsmith/extensions/latex_text.py +117 -0
  140. texsmith/extensions/mermaid.py +267 -0
  141. texsmith/extensions/missing_footnotes.py +125 -0
  142. texsmith/extensions/multi_citations.py +54 -0
  143. texsmith/extensions/progressbar/__init__.py +9 -0
  144. texsmith/extensions/progressbar/markdown.py +212 -0
  145. texsmith/extensions/progressbar/renderer.py +117 -0
  146. texsmith/extensions/smallcaps.py +48 -0
  147. texsmith/extensions/texlogos/__init__.py +10 -0
  148. texsmith/extensions/texlogos/markdown.py +236 -0
  149. texsmith/extensions/texlogos/renderer.py +81 -0
  150. texsmith/extensions/texlogos/specs.py +66 -0
  151. texsmith/fonts/__init__.py +57 -0
  152. texsmith/fonts/cache.py +46 -0
  153. texsmith/fonts/constants.py +60 -0
  154. texsmith/fonts/coverage.py +275 -0
  155. texsmith/fonts/downloader.py +103 -0
  156. texsmith/fonts/fallback.py +438 -0
  157. texsmith/fonts/html_scripts.py +168 -0
  158. texsmith/fonts/logging.py +127 -0
  159. texsmith/fonts/pipeline.py +338 -0
  160. texsmith/fonts/scripts.py +493 -0
  161. texsmith/fonts/ucharclasses.py +164 -0
  162. texsmith/fragments/__init__.py +11 -0
  163. texsmith/fragments/bibliography/__init__.py +65 -0
  164. texsmith/fragments/bibliography/fragment.toml +3 -0
  165. texsmith/fragments/bibliography/ts-bibliography-backmatter.jinja.tex +35 -0
  166. texsmith/fragments/bibliography/ts-bibliography.jinja.tex +10 -0
  167. texsmith/fragments/callouts/__init__.py +88 -0
  168. texsmith/fragments/callouts/fragment.toml +3 -0
  169. texsmith/fragments/callouts/ts-callouts.jinja.sty +129 -0
  170. texsmith/fragments/code/__init__.py +82 -0
  171. texsmith/fragments/code/fragment.toml +3 -0
  172. texsmith/fragments/code/ts-code.jinja.sty +140 -0
  173. texsmith/fragments/extra/__init__.py +180 -0
  174. texsmith/fragments/extra/fragment.toml +3 -0
  175. texsmith/fragments/extra/ts-extra.jinja.tex +13 -0
  176. texsmith/fragments/fonts/__init__.py +880 -0
  177. texsmith/fragments/fonts/fragment.toml +3 -0
  178. texsmith/fragments/fonts/ts-fonts.jinja.sty +292 -0
  179. texsmith/fragments/frame/__init__.py +170 -0
  180. texsmith/fragments/frame/fragment.toml +3 -0
  181. texsmith/fragments/frame/ts-frame.tex.jinja +49 -0
  182. texsmith/fragments/geometry/__init__.py +169 -0
  183. texsmith/fragments/geometry/fragment.toml +3 -0
  184. texsmith/fragments/geometry/paper.py +526 -0
  185. texsmith/fragments/geometry/ts_geometry.tex.jinja +53 -0
  186. texsmith/fragments/glossary/__init__.py +67 -0
  187. texsmith/fragments/glossary/fragment.toml +3 -0
  188. texsmith/fragments/glossary/ts-glossary-backmatter.jinja.tex +5 -0
  189. texsmith/fragments/glossary/ts-glossary.jinja.sty +28 -0
  190. texsmith/fragments/index/__init__.py +66 -0
  191. texsmith/fragments/index/fragment.toml +3 -0
  192. texsmith/fragments/index/ts-index-backmatter.jinja.tex +3 -0
  193. texsmith/fragments/index/ts-index.jinja.sty +12 -0
  194. texsmith/fragments/keystrokes/__init__.py +69 -0
  195. texsmith/fragments/keystrokes/fragment.toml +3 -0
  196. texsmith/fragments/keystrokes/ts-keystrokes.jinja.sty +20 -0
  197. texsmith/fragments/todolist/__init__.py +71 -0
  198. texsmith/fragments/todolist/fragment.toml +3 -0
  199. texsmith/fragments/todolist/ts-todolist.jinja.sty +21 -0
  200. texsmith/fragments/typesetting/__init__.py +214 -0
  201. texsmith/fragments/typesetting/fragment.toml +3 -0
  202. texsmith/fragments/typesetting/ts-typesetting.tex.jinja +81 -0
  203. texsmith/index.py +26 -0
  204. texsmith/plugins/__init__.py +14 -0
  205. texsmith/progressbar.py +8 -0
  206. texsmith/quotes.py +40 -0
  207. texsmith/smart_dashes.py +66 -0
  208. texsmith/templates/__init__.py +3 -0
  209. texsmith/templates/article/README.md +33 -0
  210. texsmith/templates/article/__init__.py +281 -0
  211. texsmith/templates/article/template/manifest.toml +125 -0
  212. texsmith/templates/article/template/mermaid-config.json +18 -0
  213. texsmith/templates/article/template/template.tex +74 -0
  214. texsmith/templates/book/README.md +26 -0
  215. texsmith/templates/book/__init__.py +75 -0
  216. texsmith/templates/book/overrides/codeblock.tex +7 -0
  217. texsmith/templates/book/overrides/codeinline.tex +1 -0
  218. texsmith/templates/book/template/fixtoc.sty +81 -0
  219. texsmith/templates/book/template/manifest.toml +198 -0
  220. texsmith/templates/book/template/template.tex +314 -0
  221. texsmith/templates/common/__init__.py +1 -0
  222. texsmith/templates/common/latexmkrc +46 -0
  223. texsmith/templates/letter/README.md +59 -0
  224. texsmith/templates/letter/__init__.py +495 -0
  225. texsmith/templates/letter/demo.md +31 -0
  226. texsmith/templates/letter/fonts/modernline bold.otf +0 -0
  227. texsmith/templates/letter/fonts/modernline.otf +0 -0
  228. texsmith/templates/letter/manifest.toml +197 -0
  229. texsmith/templates/letter/template/callouts.jinja.sty +290 -0
  230. texsmith/templates/letter/template/template.tex +123 -0
  231. texsmith/templates/snippet/README.md +21 -0
  232. texsmith/templates/snippet/__init__.py +80 -0
  233. texsmith/templates/snippet/template/manifest.toml +66 -0
  234. texsmith/templates/snippet/template/template.tex +47 -0
  235. texsmith/texlogos.py +15 -0
  236. texsmith/ui/__init__.py +6 -0
  237. texsmith/ui/cli/__init__.py +22 -0
  238. texsmith/ui/cli/_options.py +338 -0
  239. texsmith/ui/cli/app.py +65 -0
  240. texsmith/ui/cli/bibliography.py +300 -0
  241. texsmith/ui/cli/commands/__init__.py +14 -0
  242. texsmith/ui/cli/commands/render.py +1128 -0
  243. texsmith/ui/cli/commands/templates.py +397 -0
  244. texsmith/ui/cli/diagnostics.py +36 -0
  245. texsmith/ui/cli/presenter.py +663 -0
  246. texsmith/ui/cli/state.py +263 -0
  247. texsmith/ui/cli/utils.py +235 -0
  248. texsmith-0.0.2.dev0.dist-info/METADATA +187 -0
  249. texsmith-0.0.2.dev0.dist-info/RECORD +252 -0
  250. texsmith-0.0.2.dev0.dist-info/WHEEL +4 -0
  251. texsmith-0.0.2.dev0.dist-info/entry_points.txt +22 -0
  252. 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,6 @@
1
+ """Core domain layer for TexSmith."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ __all__ = []
@@ -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
+ ]