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,236 @@
1
+ """Markdown extension that replaces TeX logo keywords with semantic HTML."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Iterator, Mapping
6
+ import re
7
+ import xml.etree.ElementTree as ElementTree
8
+
9
+ from markdown import Markdown
10
+ from markdown.extensions import Extension
11
+ from markdown.treeprocessors import Treeprocessor
12
+
13
+ from .specs import LogoSpec, alias_mapping
14
+
15
+
16
+ class _TexLogosTreeprocessor(Treeprocessor):
17
+ """Replace textual tokens with spans that carry TeX logo metadata."""
18
+
19
+ def __init__(self, md: Markdown) -> None:
20
+ super().__init__(md)
21
+ self._alias_map = alias_mapping()
22
+ self._pattern = _compile_pattern(self._alias_map)
23
+
24
+ def run(self, root: ElementTree.Element) -> None: # type: ignore[override]
25
+ self._process_element(root)
26
+
27
+ # -- recursive traversal -------------------------------------------------
28
+ def _process_element(self, element: ElementTree.Element) -> None:
29
+ if self._is_logo_container(element):
30
+ return
31
+
32
+ if self._is_code_element(element):
33
+ for child in list(element):
34
+ if child.tail:
35
+ self._replace_tail(element, child)
36
+ return
37
+
38
+ self._replace_text_node(element)
39
+
40
+ for child in list(element):
41
+ self._process_element(child)
42
+ if child.tail:
43
+ self._replace_tail(element, child)
44
+
45
+ def _replace_text_node(self, element: ElementTree.Element) -> None:
46
+ text = element.text
47
+ if not text:
48
+ return
49
+
50
+ segments = list(self._split_segments(text))
51
+ if not any(isinstance(part, LogoSpec) for part in segments):
52
+ return
53
+
54
+ element.text = ""
55
+ insert_pos = 0
56
+ last_fragment: ElementTree.Element | None = None
57
+ for part in segments:
58
+ if isinstance(part, str):
59
+ if last_fragment is None:
60
+ element.text += part
61
+ else:
62
+ last_fragment.tail = (last_fragment.tail or "") + part
63
+ continue
64
+
65
+ fragment = self._build_fragment(part)
66
+ element.insert(insert_pos, fragment)
67
+ insert_pos += 1
68
+ last_fragment = fragment
69
+
70
+ def _replace_tail(self, parent: ElementTree.Element, child: ElementTree.Element) -> None:
71
+ tail = child.tail
72
+ if not tail:
73
+ return
74
+
75
+ segments = list(self._split_segments(tail))
76
+ if not any(isinstance(part, LogoSpec) for part in segments):
77
+ return
78
+
79
+ child.tail = ""
80
+ insert_pos = list(parent).index(child) + 1
81
+ last_fragment: ElementTree.Element | None = None
82
+ for part in segments:
83
+ if isinstance(part, str):
84
+ if last_fragment is None:
85
+ child.tail += part
86
+ else:
87
+ last_fragment.tail = (last_fragment.tail or "") + part
88
+ continue
89
+
90
+ fragment = self._build_fragment(part)
91
+ parent.insert(insert_pos, fragment)
92
+ insert_pos += 1
93
+ last_fragment = fragment
94
+
95
+ # -- helpers -------------------------------------------------------------
96
+ def _split_segments(self, value: str) -> Iterator[str | LogoSpec]:
97
+ cursor = 0
98
+ for match in self._pattern.finditer(value):
99
+ start, end = match.span()
100
+ if start > cursor:
101
+ yield value[cursor:start]
102
+ alias = match.group(0)
103
+ spec = self._alias_map.get(alias)
104
+ if (start > 0 and value[start - 1].isalnum()) or (
105
+ end < len(value) and value[end].isalnum()
106
+ ):
107
+ yield value[start:end]
108
+ elif spec:
109
+ yield spec
110
+ else: # pragma: no cover - defensive
111
+ yield alias
112
+ cursor = end
113
+ if cursor < len(value):
114
+ yield value[cursor:]
115
+
116
+ def _build_fragment(self, spec: LogoSpec) -> ElementTree.Element:
117
+ outer = ElementTree.Element(
118
+ "span",
119
+ {
120
+ "class": f"tex-logo tex-{spec.slug}",
121
+ "data-tex-command": spec.command,
122
+ "data-tex-logo": spec.slug,
123
+ "title": spec.description,
124
+ "role": "img",
125
+ "aria-label": spec.display,
126
+ "style": _BASE_STYLE,
127
+ },
128
+ )
129
+
130
+ builder = _HTML_BUILDERS.get(spec.slug, _build_plain_logo)
131
+ builder(outer, spec)
132
+ return outer
133
+
134
+ def _is_code_element(self, element: ElementTree.Element) -> bool:
135
+ tag = element.tag
136
+ if isinstance(tag, str):
137
+ tag = tag.lower()
138
+ return tag in {"code", "pre"}
139
+
140
+ def _is_logo_container(self, element: ElementTree.Element) -> bool:
141
+ classes = element.get("class", "")
142
+ if not isinstance(classes, str):
143
+ return False
144
+ return "tex-logo" in classes.split()
145
+
146
+
147
+ _BASE_STYLE = "font-family: 'Times New Roman', serif;"
148
+
149
+
150
+ def _build_plain_logo(container: ElementTree.Element, spec: LogoSpec) -> None:
151
+ container.text = spec.display
152
+
153
+
154
+ def _build_tex_logo(container: ElementTree.Element, _: LogoSpec, *, prefix: str = "") -> None:
155
+ container.text = f"{prefix}T"
156
+ lowered_e = ElementTree.SubElement(
157
+ container,
158
+ "span",
159
+ {"style": "position: relative; top: 0.2em; font-size: 0.8em;"},
160
+ )
161
+ lowered_e.text = "E"
162
+ lowered_e.tail = "X"
163
+
164
+
165
+ def _build_latex_logo(
166
+ container: ElementTree.Element, _: LogoSpec, *, prefix: str = ""
167
+ ) -> ElementTree.Element:
168
+ container.text = f"{prefix}L"
169
+ raised_a = ElementTree.SubElement(
170
+ container,
171
+ "span",
172
+ {"style": "position: relative; top: -0.3em; font-size: 0.8em;"},
173
+ )
174
+ raised_a.text = "A"
175
+ raised_a.tail = "T"
176
+
177
+ lowered_e = ElementTree.SubElement(
178
+ container,
179
+ "span",
180
+ {"style": "position: relative; top: 0.2em; font-size: 0.8em;"},
181
+ )
182
+ lowered_e.text = "E"
183
+ lowered_e.tail = "X"
184
+ return lowered_e
185
+
186
+
187
+ def _build_latex(container: ElementTree.Element, spec: LogoSpec) -> None:
188
+ _build_latex_logo(container, spec)
189
+
190
+
191
+ def _build_latex2e(container: ElementTree.Element, spec: LogoSpec) -> None:
192
+ lowered_e = _build_latex_logo(container, spec)
193
+
194
+ superscript_two = ElementTree.SubElement(
195
+ container,
196
+ "span",
197
+ {"style": "position: relative; top: -0.35em; font-size: 0.65em; margin-left: 0.05em;"},
198
+ )
199
+ superscript_two.text = "2"
200
+
201
+ epsilon = ElementTree.SubElement(
202
+ container,
203
+ "span",
204
+ {
205
+ "style": "position: relative; top: -0.2em; font-size: 0.7em; font-style: italic; margin-left: 0.02em;"
206
+ },
207
+ )
208
+ epsilon.text = "\u03b5"
209
+ lowered_e.tail = "X"
210
+
211
+
212
+ _HTML_BUILDERS: Mapping[str, Callable[[ElementTree.Element, LogoSpec], None]] = {
213
+ "tex": _build_tex_logo,
214
+ "latex": _build_latex,
215
+ "latex2e": _build_latex2e,
216
+ }
217
+
218
+
219
+ class TexLogosExtension(Extension):
220
+ """Register the TeX logo tree-processor."""
221
+
222
+ def extendMarkdown(self, md: Markdown) -> None: # type: ignore[override] # noqa: N802
223
+ md.treeprocessors.register(_TexLogosTreeprocessor(md), "texsmith_texlogos", priority=14)
224
+
225
+
226
+ def makeExtension(**_: object) -> TexLogosExtension: # pragma: no cover - API hook # noqa: N802
227
+ return TexLogosExtension()
228
+
229
+
230
+ def _compile_pattern(mapping: Mapping[str, LogoSpec]) -> re.Pattern[str]:
231
+ aliases = sorted(mapping.keys(), key=len, reverse=True)
232
+ pattern = "|".join(re.escape(alias) for alias in aliases)
233
+ return re.compile(pattern)
234
+
235
+
236
+ __all__ = ["TexLogosExtension", "makeExtension"]
@@ -0,0 +1,81 @@
1
+ """Renderer hooks to convert TeX logo spans into LaTeX macros."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+
7
+ from bs4 import NavigableString, Tag
8
+
9
+ from texsmith.adapters.handlers._helpers import gather_classes, mark_processed
10
+ from texsmith.core.context import RenderContext
11
+ from texsmith.core.rules import RenderPhase, renders
12
+
13
+ from .specs import LogoSpec, alias_mapping, iter_specs
14
+
15
+
16
+ _COMMAND_LOOKUP: Mapping[str, LogoSpec] = {spec.command: spec for spec in iter_specs()}
17
+ _SLUG_LOOKUP: Mapping[str, LogoSpec] = {spec.slug: spec for spec in iter_specs()}
18
+ _ALIAS_LOOKUP = alias_mapping()
19
+ _REGISTER_ATTR = "_texsmith_texlogos_registered"
20
+
21
+
22
+ @renders(
23
+ "span",
24
+ phase=RenderPhase.INLINE,
25
+ priority=32,
26
+ name="texsmith_tex_logo",
27
+ nestable=False,
28
+ auto_mark=False,
29
+ )
30
+ def render_tex_logo_span(element: Tag, context: RenderContext) -> None:
31
+ """Process ``<span class="tex-logo">`` placeholders."""
32
+
33
+ classes = gather_classes(element.get("class"))
34
+ if "tex-logo" not in classes:
35
+ return
36
+
37
+ spec = _deduce_spec(element)
38
+ if spec is None:
39
+ return
40
+
41
+ latex = mark_processed(NavigableString(spec.command))
42
+ context.mark_processed(element)
43
+ context.suppress_children(element)
44
+ element.replace_with(latex)
45
+
46
+
47
+ def _deduce_spec(element: Tag) -> LogoSpec | None:
48
+ command = element.get("data-tex-command")
49
+ if command:
50
+ spec = _COMMAND_LOOKUP.get(command)
51
+ if spec:
52
+ return spec
53
+
54
+ slug = element.get("data-tex-logo")
55
+ if slug:
56
+ spec = _SLUG_LOOKUP.get(slug)
57
+ if spec:
58
+ return spec
59
+
60
+ text = element.get_text(strip=True)
61
+ if text:
62
+ return _ALIAS_LOOKUP.get(text)
63
+
64
+ return None
65
+
66
+
67
+ def register(renderer: object) -> None:
68
+ """Convenience helper to register all logo handlers on a renderer."""
69
+
70
+ register_callable = getattr(renderer, "register", None)
71
+ if callable(register_callable):
72
+ if getattr(renderer, _REGISTER_ATTR, False):
73
+ return
74
+ register_callable(render_tex_logo_span)
75
+ setattr(renderer, _REGISTER_ATTR, True)
76
+ return
77
+
78
+ raise TypeError("Renderer object does not expose a 'register' method")
79
+
80
+
81
+ __all__ = ["register", "render_tex_logo_span"]
@@ -0,0 +1,66 @@
1
+ """Shared specifications for the TeX logo renderer and Markdown extension."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable, Mapping
6
+ from dataclasses import dataclass
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class LogoSpec:
11
+ """Describe how to recognise and render a TeX logo token."""
12
+
13
+ command: str
14
+ display: str
15
+ description: str
16
+ slug: str
17
+ aliases: tuple[str, ...]
18
+
19
+ def match_values(self) -> tuple[str, ...]:
20
+ """Get the strings that should trigger this logo."""
21
+
22
+ return self.aliases or (self.display,)
23
+
24
+
25
+ _SPECS: tuple[LogoSpec, ...] = (
26
+ LogoSpec(
27
+ command=r"\TeX{}",
28
+ display="TeX",
29
+ description="Le moteur de base cree par Donald Knuth",
30
+ slug="tex",
31
+ aliases=("TeX",),
32
+ ),
33
+ LogoSpec(
34
+ command=r"\LaTeX{}",
35
+ display="LaTeX",
36
+ description="Le systeme de composition base sur TeX",
37
+ slug="latex",
38
+ aliases=("LaTeX",),
39
+ ),
40
+ LogoSpec(
41
+ command=r"\LaTeXe{}",
42
+ display="LaTeX2\u03b5",
43
+ description="La version moderne de LaTeX",
44
+ slug="latex2e",
45
+ aliases=("LaTeX2e", "LaTeX2\u03b5", "LaTeXe"),
46
+ ),
47
+ )
48
+
49
+
50
+ def iter_specs() -> Iterable[LogoSpec]:
51
+ """Iterate over the known logo specifications."""
52
+
53
+ return _SPECS
54
+
55
+
56
+ def alias_mapping() -> Mapping[str, LogoSpec]:
57
+ """Build a lookup from alias to spec."""
58
+
59
+ mapping: dict[str, LogoSpec] = {}
60
+ for spec in _SPECS:
61
+ for alias in spec.match_values():
62
+ mapping[alias] = spec
63
+ return mapping
64
+
65
+
66
+ __all__ = ["LogoSpec", "alias_mapping", "iter_specs"]
@@ -0,0 +1,57 @@
1
+ """Font toolchain façade used to prepare reliable LaTeX fallbacks.
2
+
3
+ Architecture
4
+ : `FontCache` pins downloads of Noto metadata, ucharclasses tables, and computed
5
+ fallback indexes to a single cache root so repeated renders never thrash the
6
+ network. The cache also carries signatures so on-disk data can be trusted.
7
+ : Builders such as `NotoCoverageBuilder` and `UCharClassesBuilder` transform
8
+ upstream datasets into structured coverage ranges. `FallbackBuilder` consumes
9
+ that data to calculate the minimal set of fonts required for arbitrary text.
10
+ : `FallbackManager` wraps the pipeline with memoisation, exposing simple lookup
11
+ helpers while persisting results via `FallbackRepository` for reuse.
12
+ : Convenience generators (``generate_fallback_entries``/``generate_noto_metadata``/
13
+ ``generate_ucharclasses_data``) orchestrate the above pieces and return
14
+ ready-to-embed artifacts for renderers and CLI tooling.
15
+
16
+ Goal
17
+ : Provide deterministic, cache-aware font selection so TeXSmith can emit
18
+ LaTeX that compiles on machines without full Unicode font coverage while
19
+ keeping downloads minimal and reproducible.
20
+ """
21
+
22
+ from texsmith.fonts.cache import FontCache
23
+ from texsmith.fonts.coverage import NotoCoverage, NotoCoverageBuilder
24
+ from texsmith.fonts.fallback import (
25
+ FallbackBuilder,
26
+ FallbackEntry,
27
+ FallbackIndex,
28
+ FallbackLookup,
29
+ FallbackRepository,
30
+ )
31
+ from texsmith.fonts.logging import FontPipelineLogger
32
+ from texsmith.fonts.pipeline import (
33
+ FallbackManager,
34
+ generate_fallback_entries,
35
+ generate_noto_metadata,
36
+ generate_ucharclasses_data,
37
+ )
38
+ from texsmith.fonts.ucharclasses import UCharClass, UCharClassesBuilder
39
+
40
+
41
+ __all__ = [
42
+ "FallbackBuilder",
43
+ "FallbackEntry",
44
+ "FallbackIndex",
45
+ "FallbackLookup",
46
+ "FallbackManager",
47
+ "FallbackRepository",
48
+ "FontCache",
49
+ "FontPipelineLogger",
50
+ "NotoCoverage",
51
+ "NotoCoverageBuilder",
52
+ "UCharClass",
53
+ "UCharClassesBuilder",
54
+ "generate_fallback_entries",
55
+ "generate_noto_metadata",
56
+ "generate_ucharclasses_data",
57
+ ]
@@ -0,0 +1,46 @@
1
+ """Lightweight cache utilities for font-related assets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator
6
+ from contextlib import contextmanager
7
+ from pathlib import Path
8
+ import tempfile
9
+
10
+ from texsmith.core.user_dir import get_user_dir
11
+
12
+
13
+ DEFAULT_ROOT = get_user_dir().data_dir("fonts", create=False)
14
+
15
+
16
+ class FontCache:
17
+ """Resolve persistent and temporary paths for font tooling.
18
+
19
+ The cache defaults to ``~/.texsmith/fonts`` so that repeated runs of the
20
+ toolchain can reuse downloaded assets (CTAN packages, lookup indexes, ...).
21
+ """
22
+
23
+ def __init__(self, root: Path | None = None) -> None:
24
+ self.root = root or get_user_dir().data_dir("fonts", create=False)
25
+
26
+ def ensure(self) -> Path:
27
+ """Ensure the cache root exists and return it."""
28
+ self.root.mkdir(parents=True, exist_ok=True)
29
+ return self.root
30
+
31
+ def path(self, *parts: str | Path) -> Path:
32
+ """Return a path under the cache root, creating parent directories."""
33
+ base = self.ensure()
34
+ target = base.joinpath(*parts)
35
+ target.parent.mkdir(parents=True, exist_ok=True)
36
+ return target
37
+
38
+ @contextmanager
39
+ def tempdir(self) -> Iterator[Path]:
40
+ """Provide a temporary directory inside the cache root."""
41
+ self.ensure()
42
+ with tempfile.TemporaryDirectory(dir=self.root) as tmp:
43
+ yield Path(tmp)
44
+
45
+
46
+ __all__ = ["DEFAULT_ROOT", "FontCache"]
@@ -0,0 +1,60 @@
1
+ """Shared constants and helpers for Noto font handling."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ _STYLE_SUFFIXES = {
7
+ "regular": "Regular",
8
+ "italic": "Italic",
9
+ "bold": "Bold",
10
+ "bolditalic": "BoldItalic",
11
+ }
12
+
13
+ _CJK_PATHS = {
14
+ "jp": "Sans/OTF/Japanese/",
15
+ "kr": "Sans/OTF/Korean/",
16
+ "sc": "Sans/OTF/SimplifiedChinese/",
17
+ "tc": "Sans/OTF/TraditionalChinese/",
18
+ "hk": "Sans/OTF/TraditionalChineseHK/",
19
+ }
20
+
21
+ CJK_ALIASES = {
22
+ "NotoSansJP": ("NotoSansCJKjp", _CJK_PATHS["jp"]),
23
+ "NotoSansKR": ("NotoSansCJKkr", _CJK_PATHS["kr"]),
24
+ "NotoSansSC": ("NotoSansCJKsc", _CJK_PATHS["sc"]),
25
+ "NotoSansTC": ("NotoSansCJKtc", _CJK_PATHS["tc"]),
26
+ "NotoSansHK": ("NotoSansCJKhk", _CJK_PATHS["hk"]),
27
+ }
28
+
29
+
30
+ def style_suffix(style: str) -> str:
31
+ """Return the canonical filename suffix for a Noto style."""
32
+ return _STYLE_SUFFIXES.get(style.lower(), style.title())
33
+
34
+
35
+ def filename_base(filename: str) -> str:
36
+ """Return the base name of a Noto font file, stripping style suffixes."""
37
+ return filename.split("-")[0]
38
+
39
+
40
+ def guess_cjk_path(filename: str) -> str:
41
+ """Guess the CJK subpath for a Noto CJK font filename."""
42
+ base = filename_base(filename)
43
+ if base in CJK_ALIASES:
44
+ return CJK_ALIASES[base][1]
45
+
46
+ lowered = base.lower()
47
+ if "cjkjp" in lowered:
48
+ return _CJK_PATHS["jp"]
49
+ if "cjkkr" in lowered:
50
+ return _CJK_PATHS["kr"]
51
+ if "cjksc" in lowered:
52
+ return _CJK_PATHS["sc"]
53
+ if "cjktc" in lowered:
54
+ return _CJK_PATHS["tc"]
55
+ if "cjkhk" in lowered:
56
+ return _CJK_PATHS["hk"]
57
+ return ""
58
+
59
+
60
+ __all__ = ["CJK_ALIASES", "filename_base", "guess_cjk_path", "style_suffix"]