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,95 @@
1
+ """Context objects used during document conversion."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ from .config import BookConfig
11
+ from .templates import TemplateBinding
12
+
13
+
14
+ if TYPE_CHECKING: # pragma: no cover - typing only
15
+ from .bibliography.collection import BibliographyCollection
16
+
17
+
18
+ @dataclass(slots=True)
19
+ class AssetMapping:
20
+ """Describe how a source asset should be persisted for LaTeX generation."""
21
+
22
+ source: Path
23
+ target: Path
24
+ kind: str | None = None
25
+
26
+
27
+ @dataclass(slots=True)
28
+ class GenerationStrategy:
29
+ """Rendering strategy toggles shared across conversion workflows."""
30
+
31
+ copy_assets: bool = True
32
+ convert_assets: bool = False
33
+ hash_assets: bool = False
34
+ prefer_inputs: bool = False
35
+ persist_manifest: bool = False
36
+
37
+
38
+ @dataclass(slots=True)
39
+ class SegmentContext:
40
+ """Represent a fragment destined for insertion into a template slot."""
41
+
42
+ name: str
43
+ html: str
44
+ base_level: int
45
+ metadata: Mapping[str, Any] = field(default_factory=dict)
46
+ bibliography: Mapping[str, Any] = field(default_factory=dict)
47
+ assets: list[AssetMapping] = field(default_factory=list)
48
+ destination: Path | None = None
49
+
50
+
51
+ @dataclass(slots=True)
52
+ class DocumentContext:
53
+ """Document-level context produced before rendering."""
54
+
55
+ name: str
56
+ source_path: Path
57
+ html: str
58
+ base_level: int
59
+ numbered: bool
60
+ drop_title: bool
61
+ title_from_heading: bool
62
+ extracted_title: str | None = None
63
+ front_matter: dict[str, Any] = field(default_factory=dict)
64
+ slot_requests: dict[str, str] = field(default_factory=dict)
65
+ slot_inclusions: set[str] = field(default_factory=set)
66
+ language: str | None = None
67
+ bibliography: dict[str, Any] = field(default_factory=dict)
68
+ assets: list[AssetMapping] = field(default_factory=list)
69
+ segments: dict[str, list[SegmentContext]] = field(default_factory=dict)
70
+
71
+
72
+ @dataclass(slots=True)
73
+ class BinderContext:
74
+ """Binder-level context describing template binding and global state."""
75
+
76
+ output_dir: Path
77
+ config: BookConfig
78
+ strategy: GenerationStrategy
79
+ language: str
80
+ slot_requests: dict[str, str]
81
+ template_overrides: dict[str, Any]
82
+ bibliography_map: dict[str, dict[str, Any]] = field(default_factory=dict)
83
+ bibliography_collection: BibliographyCollection | None = None
84
+ template_binding: TemplateBinding | None = None
85
+ documents: list[DocumentContext] = field(default_factory=list)
86
+ bound_segments: dict[str, list[SegmentContext]] = field(default_factory=dict)
87
+
88
+
89
+ __all__ = [
90
+ "AssetMapping",
91
+ "BinderContext",
92
+ "DocumentContext",
93
+ "GenerationStrategy",
94
+ "SegmentContext",
95
+ ]
@@ -0,0 +1,118 @@
1
+ """Diagnostic abstractions shared across the conversion pipeline."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ import logging
7
+ from typing import Any, Protocol, runtime_checkable
8
+
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ @runtime_checkable
14
+ class DiagnosticEmitter(Protocol):
15
+ """Interface used to surface warnings, errors, and structured events."""
16
+
17
+ debug_enabled: bool
18
+
19
+ def warning(self, message: str, exc: BaseException | None = None) -> None: ...
20
+
21
+ def error(self, message: str, exc: BaseException | None = None) -> None: ...
22
+
23
+ def event(self, name: str, payload: Mapping[str, Any]) -> None: ...
24
+
25
+
26
+ class NullEmitter:
27
+ """Emitter that ignores every diagnostic."""
28
+
29
+ debug_enabled: bool = False
30
+
31
+ def warning(self, message: str, exc: BaseException | None = None) -> None:
32
+ return
33
+
34
+ def error(self, message: str, exc: BaseException | None = None) -> None:
35
+ return
36
+
37
+ def event(self, name: str, payload: Mapping[str, Any]) -> None:
38
+ return
39
+
40
+
41
+ class LoggingEmitter:
42
+ """Emitter that forwards diagnostics to the standard logging module."""
43
+
44
+ def __init__(
45
+ self, *, logger_obj: logging.Logger | None = None, debug_enabled: bool = False
46
+ ) -> None:
47
+ self._logger = logger_obj or logger
48
+ self.debug_enabled = debug_enabled
49
+
50
+ def warning(self, message: str, exc: BaseException | None = None) -> None:
51
+ if exc is not None:
52
+ self._logger.warning(message, exc_info=exc)
53
+ else:
54
+ self._logger.warning(message)
55
+
56
+ def error(self, message: str, exc: BaseException | None = None) -> None:
57
+ if exc is not None:
58
+ self._logger.error(message, exc_info=exc)
59
+ else:
60
+ self._logger.error(message)
61
+
62
+ def event(self, name: str, payload: Mapping[str, Any]) -> None:
63
+ message = format_event_message(name, payload)
64
+ if message:
65
+ self._logger.info(message)
66
+ return
67
+ try:
68
+ self._logger.debug("diagnostic event %s: %s", name, dict(payload))
69
+ except Exception: # pragma: no cover - defensive
70
+ self._logger.debug("failed to log diagnostic event %s", name, exc_info=True)
71
+
72
+
73
+ def format_event_message(name: str, payload: Mapping[str, Any]) -> str | None:
74
+ """Return a human-friendly summary for selected diagnostic events."""
75
+ try:
76
+ data = dict(payload)
77
+ except Exception: # pragma: no cover - defensive
78
+ data = {}
79
+
80
+ if name == "asset_fetch":
81
+ url = data.get("url") or "<unknown>"
82
+ convert = data.get("convert")
83
+ suffix_hint = data.get("suffix_hint")
84
+ details: list[str] = []
85
+ if convert:
86
+ details.append("convert")
87
+ if suffix_hint:
88
+ details.append(f"suffix={suffix_hint}")
89
+ suffix = f" ({', '.join(details)})" if details else ""
90
+ return f"Fetching: {url}{suffix}"
91
+
92
+ if name == "asset_fetch_cached":
93
+ url = data.get("url") or "<unknown>"
94
+ reason = data.get("reason") or "cache"
95
+ return f"Reusing cached remote image: {url} ({reason})"
96
+
97
+ if name == "doi_fetch":
98
+ doi_value = data.get("value") or data.get("doi") or "<unknown>"
99
+ key = data.get("key") or "<unknown>"
100
+ mode = data.get("mode")
101
+ source = data.get("source") or data.get("resolved_source")
102
+ details: list[str] = []
103
+ if mode:
104
+ details.append(str(mode))
105
+ if source:
106
+ details.append(str(source))
107
+ suffix = f" ({', '.join(details)})" if details else ""
108
+ return f"Resolved DOI {doi_value} for entry '{key}'{suffix}"
109
+
110
+ return None
111
+
112
+
113
+ __all__ = [
114
+ "DiagnosticEmitter",
115
+ "LoggingEmitter",
116
+ "NullEmitter",
117
+ "format_event_message",
118
+ ]
@@ -0,0 +1,41 @@
1
+ """Custom exception hierarchy for the LaTeX rendering pipeline."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class LatexRenderingError(RuntimeError):
7
+ """Base exception for LaTeX rendering failures."""
8
+
9
+
10
+ class AssetMissingError(LatexRenderingError):
11
+ """Raised when an expected asset cannot be located or generated."""
12
+
13
+
14
+ class TransformerExecutionError(LatexRenderingError):
15
+ """Raised when an external converter fails to execute properly."""
16
+
17
+
18
+ class InvalidNodeError(LatexRenderingError):
19
+ """Raised when a handler receives an unexpected DOM node shape."""
20
+
21
+
22
+ def exception_messages(exc: BaseException) -> list[str]:
23
+ """Return the collected message chain for an exception and its causes."""
24
+ messages: list[str] = []
25
+ visited: set[int] = set()
26
+ current: BaseException | None = exc
27
+ while current is not None and id(current) not in visited:
28
+ visited.add(id(current))
29
+ text = str(current).strip()
30
+ if text:
31
+ first_line = text.splitlines()[0].strip()
32
+ if first_line:
33
+ messages.append(first_line)
34
+ current = current.__cause__ or current.__context__
35
+ return messages
36
+
37
+
38
+ def exception_hint(exc: BaseException) -> str | None:
39
+ """Return the most specific message available for an exception chain."""
40
+ messages = exception_messages(exc)
41
+ return messages[-1] if messages else None
@@ -0,0 +1,3 @@
1
+ """Font utilities used by TexSmith's core rendering pipeline."""
2
+
3
+ __all__ = []