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,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
|
+
]
|
texsmith/fonts/cache.py
ADDED
|
@@ -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"]
|