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,183 @@
|
|
|
1
|
+
"""TeXSmith renderer hooks converting hashtag spans into LaTeX index entries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
from bs4 import NavigableString, Tag
|
|
9
|
+
|
|
10
|
+
from texsmith.adapters.handlers._helpers import (
|
|
11
|
+
coerce_attribute,
|
|
12
|
+
gather_classes,
|
|
13
|
+
mark_processed,
|
|
14
|
+
)
|
|
15
|
+
from texsmith.adapters.latex.utils import escape_latex_chars
|
|
16
|
+
from texsmith.core.context import RenderContext
|
|
17
|
+
from texsmith.core.rules import RenderPhase, renders
|
|
18
|
+
|
|
19
|
+
from .registry import get_registry
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
TEMPLATE_DIR = Path(__file__).resolve().parent / "templates"
|
|
23
|
+
INDEX_TEMPLATE = TEMPLATE_DIR / "index.tex"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _collect_tags(element: Tag) -> list[str]:
|
|
27
|
+
tags: list[str] = []
|
|
28
|
+
index = 0
|
|
29
|
+
while True:
|
|
30
|
+
key = "data-tag" if index == 0 else f"data-tag{index}"
|
|
31
|
+
value = coerce_attribute(element.get(key))
|
|
32
|
+
if not value:
|
|
33
|
+
break
|
|
34
|
+
cleaned = str(value).strip()
|
|
35
|
+
if cleaned:
|
|
36
|
+
tags.append(cleaned)
|
|
37
|
+
index += 1
|
|
38
|
+
return tags
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _strip_formatting(text: str) -> str:
|
|
42
|
+
"""Remove markdown formatting from text."""
|
|
43
|
+
text = re.sub(r"\*\*\*(.*?)\*\*\*|___(.*?)___", r"\1\2", text)
|
|
44
|
+
text = re.sub(r"\*\*(.*?)\*\*|__(.*?)__", r"\1\2", text)
|
|
45
|
+
text = re.sub(r"\*(.*?)\*|_(.*?)_", r"\1\2", text)
|
|
46
|
+
return text
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _format_tag(text: str, legacy: bool) -> str:
|
|
50
|
+
"""Convert markdown formatting to LaTeX."""
|
|
51
|
+
parts = re.split(r"(\*\*\*.*?\*\*\*)", text)
|
|
52
|
+
processed = []
|
|
53
|
+
for part in parts:
|
|
54
|
+
if part.startswith("***") and part.endswith("***"):
|
|
55
|
+
content = part[3:-3]
|
|
56
|
+
processed.append(
|
|
57
|
+
f"\\textbf{{\\textit{{{escape_latex_chars(content, legacy_accents=legacy)}}}}}"
|
|
58
|
+
)
|
|
59
|
+
else:
|
|
60
|
+
subparts = re.split(r"(\*\*.*?\*\*)", part)
|
|
61
|
+
for subpart in subparts:
|
|
62
|
+
if subpart.startswith("**") and subpart.endswith("**"):
|
|
63
|
+
content = subpart[2:-2]
|
|
64
|
+
processed.append(
|
|
65
|
+
f"\\textbf{{{escape_latex_chars(content, legacy_accents=legacy)}}}"
|
|
66
|
+
)
|
|
67
|
+
else:
|
|
68
|
+
subsubparts = re.split(r"(\*.*?\*)", subpart)
|
|
69
|
+
for subsubpart in subsubparts:
|
|
70
|
+
if subsubpart.startswith("*") and subsubpart.endswith("*"):
|
|
71
|
+
content = subsubpart[1:-1]
|
|
72
|
+
processed.append(
|
|
73
|
+
f"\\textit{{{escape_latex_chars(content, legacy_accents=legacy)}}}"
|
|
74
|
+
)
|
|
75
|
+
else:
|
|
76
|
+
processed.append(escape_latex_chars(subsubpart, legacy_accents=legacy))
|
|
77
|
+
return "".join(processed)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _normalise_style(value: str | None) -> str:
|
|
81
|
+
if not value:
|
|
82
|
+
return ""
|
|
83
|
+
cleaned = str(value).strip().lower()
|
|
84
|
+
if cleaned == "ib":
|
|
85
|
+
cleaned = "bi"
|
|
86
|
+
if cleaned in {"b", "i", "bi"}:
|
|
87
|
+
return cleaned
|
|
88
|
+
return ""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _apply_style(fragment: str, style: str) -> str:
|
|
92
|
+
if style == "b":
|
|
93
|
+
return f"\\textbf{{{fragment}}}"
|
|
94
|
+
if style == "i":
|
|
95
|
+
return f"\\textit{{{fragment}}}"
|
|
96
|
+
if style == "bi":
|
|
97
|
+
return f"\\textbf{{\\textit{{{fragment}}}}}"
|
|
98
|
+
return fragment
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
_REGISTER_ATTR = "_texsmith_index_registered"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@renders(
|
|
105
|
+
"span",
|
|
106
|
+
phase=RenderPhase.INLINE,
|
|
107
|
+
priority=44,
|
|
108
|
+
name="texsmith_index",
|
|
109
|
+
nestable=False,
|
|
110
|
+
auto_mark=False,
|
|
111
|
+
)
|
|
112
|
+
def render_index(element: Tag, context: RenderContext) -> None:
|
|
113
|
+
"""Convert ``<span class="ts-hashtag">`` elements into LaTeX index commands."""
|
|
114
|
+
classes = gather_classes(element.get("class"))
|
|
115
|
+
if "ts-hashtag" not in classes and "ts-index" not in classes:
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
tags = _collect_tags(element)
|
|
119
|
+
if not tags:
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
registry_name = coerce_attribute(element.get("data-registry"))
|
|
123
|
+
style_value = _normalise_style(coerce_attribute(element.get("data-style")))
|
|
124
|
+
legacy = getattr(context.config, "legacy_latex_accents", False)
|
|
125
|
+
|
|
126
|
+
formatted_tags = [_format_tag(tag, legacy=legacy) for tag in tags]
|
|
127
|
+
if style_value and formatted_tags:
|
|
128
|
+
formatted_tags[-1] = _apply_style(formatted_tags[-1], style_value)
|
|
129
|
+
|
|
130
|
+
sort_tags = [_strip_formatting(tag) for tag in tags]
|
|
131
|
+
|
|
132
|
+
entries = []
|
|
133
|
+
for f_tag, s_tag in zip(formatted_tags, sort_tags, strict=True):
|
|
134
|
+
escaped_s_tag = escape_latex_chars(s_tag, legacy_accents=legacy)
|
|
135
|
+
if f_tag == escaped_s_tag:
|
|
136
|
+
entries.append(f_tag)
|
|
137
|
+
else:
|
|
138
|
+
entries.append(f"{escaped_s_tag}@{f_tag}")
|
|
139
|
+
|
|
140
|
+
entry_str = "!".join(entries)
|
|
141
|
+
|
|
142
|
+
visible_text = element.get_text(strip=False) or ""
|
|
143
|
+
|
|
144
|
+
latex = context.formatter.index(
|
|
145
|
+
visible_text,
|
|
146
|
+
entry=entry_str,
|
|
147
|
+
style="",
|
|
148
|
+
styled_entry=None,
|
|
149
|
+
registry=registry_name,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
registry = get_registry()
|
|
153
|
+
registry.add(tuple(sort_tags))
|
|
154
|
+
|
|
155
|
+
state = context.state
|
|
156
|
+
state.has_index_entries = True
|
|
157
|
+
index_entries = getattr(state, "index_entries", None)
|
|
158
|
+
if isinstance(index_entries, list):
|
|
159
|
+
index_entries.append(tuple(sort_tags))
|
|
160
|
+
|
|
161
|
+
node = mark_processed(NavigableString(latex))
|
|
162
|
+
context.mark_processed(element)
|
|
163
|
+
context.suppress_children(element)
|
|
164
|
+
element.replace_with(node)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def register(renderer: object) -> None:
|
|
168
|
+
"""Register the index renderer on the provided TeXSmith renderer."""
|
|
169
|
+
register_callable = getattr(renderer, "register", None)
|
|
170
|
+
if not callable(register_callable):
|
|
171
|
+
raise TypeError("Renderer does not expose a 'register' method.")
|
|
172
|
+
if getattr(renderer, _REGISTER_ATTR, False):
|
|
173
|
+
return
|
|
174
|
+
register_callable(render_index)
|
|
175
|
+
|
|
176
|
+
formatter = getattr(renderer, "formatter", None)
|
|
177
|
+
override_template = getattr(formatter, "override_template", None)
|
|
178
|
+
if callable(override_template):
|
|
179
|
+
override_template("index", INDEX_TEMPLATE)
|
|
180
|
+
setattr(renderer, _REGISTER_ATTR, True)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
__all__ = ["register", "render_index"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
\VAR{text}\index\BLOCK{ if registry }[\VAR{registry}]\BLOCK{ endif }{\BLOCK{ if styled_entry }\VAR{styled_entry}\BLOCK{ elif style == 'b' }\textbf{\VAR{entry}}\BLOCK{ elif style == 'i' }\textit{\VAR{entry}}\BLOCK{ elif style in ('bi', 'ib') }\textbf{\textit{\VAR{entry}}}\BLOCK{ else }\VAR{entry}\BLOCK{ endif }}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Markdown extension adding support for raw LaTeX fence blocks and inline snippets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from html import escape
|
|
6
|
+
import re
|
|
7
|
+
from xml.etree import ElementTree
|
|
8
|
+
|
|
9
|
+
from markdown import Markdown
|
|
10
|
+
from markdown.extensions import Extension
|
|
11
|
+
from markdown.inlinepatterns import InlineProcessor
|
|
12
|
+
from markdown.preprocessors import Preprocessor
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _LatexRawPreprocessor(Preprocessor):
|
|
16
|
+
"""Transform custom `/// latex` fences into hidden HTML nodes."""
|
|
17
|
+
|
|
18
|
+
_START_RE = re.compile(r"^\s*///\s+latex\s*$")
|
|
19
|
+
_END_RE = re.compile(r"^\s*///\s*$")
|
|
20
|
+
_FENCE_RE = re.compile(r"^\s{0,3}(`{3,}|~{3,})(.*)$")
|
|
21
|
+
|
|
22
|
+
def run(self, lines: list[str]) -> list[str]:
|
|
23
|
+
result: list[str] = []
|
|
24
|
+
index = 0
|
|
25
|
+
total = len(lines)
|
|
26
|
+
in_fence = False
|
|
27
|
+
fence_char: str | None = None
|
|
28
|
+
fence_len = 0
|
|
29
|
+
|
|
30
|
+
while index < total:
|
|
31
|
+
line = lines[index]
|
|
32
|
+
|
|
33
|
+
fence_match = self._FENCE_RE.match(line)
|
|
34
|
+
if fence_match:
|
|
35
|
+
fence_token = fence_match.group(1)
|
|
36
|
+
token_char = fence_token[0]
|
|
37
|
+
token_len = len(fence_token)
|
|
38
|
+
if not in_fence:
|
|
39
|
+
in_fence = True
|
|
40
|
+
fence_char = token_char
|
|
41
|
+
fence_len = token_len
|
|
42
|
+
else:
|
|
43
|
+
if token_char == fence_char and token_len >= fence_len:
|
|
44
|
+
in_fence = False
|
|
45
|
+
fence_char = None
|
|
46
|
+
fence_len = 0
|
|
47
|
+
result.append(line)
|
|
48
|
+
index += 1
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
if in_fence:
|
|
52
|
+
result.append(line)
|
|
53
|
+
index += 1
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
if not self._START_RE.match(line):
|
|
57
|
+
result.append(line)
|
|
58
|
+
index += 1
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
start_index = index
|
|
62
|
+
index += 1
|
|
63
|
+
contents: list[str] = []
|
|
64
|
+
|
|
65
|
+
while index < total and not self._END_RE.match(lines[index]):
|
|
66
|
+
contents.append(lines[index])
|
|
67
|
+
index += 1
|
|
68
|
+
|
|
69
|
+
if index >= total:
|
|
70
|
+
# No closing fence; fall back to raw lines
|
|
71
|
+
result.extend(lines[start_index:])
|
|
72
|
+
break
|
|
73
|
+
|
|
74
|
+
escaped = escape("\n".join(contents), quote=False)
|
|
75
|
+
result.append(f'<p class="latex-raw" style="display:none;">{escaped}</p>')
|
|
76
|
+
index += 1 # Skip closing fence
|
|
77
|
+
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class _LatexInlineProcessor(InlineProcessor):
|
|
82
|
+
"""Inline handler for ``{latex}[payload]`` markers."""
|
|
83
|
+
|
|
84
|
+
def handleMatch( # noqa: N802 - Markdown inline API requires camelCase
|
|
85
|
+
self,
|
|
86
|
+
match: re.Match[str],
|
|
87
|
+
data: str,
|
|
88
|
+
) -> tuple[ElementTree.Element | None, int, int]: # type: ignore[override]
|
|
89
|
+
del data
|
|
90
|
+
payload = match.group("payload")
|
|
91
|
+
if payload is None:
|
|
92
|
+
return None, match.start(0), match.end(0)
|
|
93
|
+
|
|
94
|
+
element = ElementTree.Element("span")
|
|
95
|
+
element.set("class", "latex-raw")
|
|
96
|
+
element.set("style", "display:none;")
|
|
97
|
+
element.text = escape(payload, quote=False)
|
|
98
|
+
return element, match.start(0), match.end(0)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class LatexRawExtension(Extension):
|
|
102
|
+
"""Register the raw LaTeX block preprocessor."""
|
|
103
|
+
|
|
104
|
+
def extendMarkdown(self, md: Markdown) -> None: # type: ignore[override] # noqa: N802
|
|
105
|
+
md.preprocessors.register(_LatexRawPreprocessor(md), "texsmith_latex_raw", priority=27)
|
|
106
|
+
pattern = r"\{latex\}\[(?P<payload>[^\]]+)\]"
|
|
107
|
+
processor = _LatexInlineProcessor(pattern, md)
|
|
108
|
+
md.inlinePatterns.register(processor, "texsmith_latex_inline", 181)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def makeExtension(**_: object) -> LatexRawExtension: # pragma: no cover - API hook # noqa: N802
|
|
112
|
+
return LatexRawExtension()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
__all__ = ["LatexRawExtension", "makeExtension"]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Markdown extension to stylize the literal ``LaTeX`` token inside paragraphs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import xml.etree.ElementTree as ElementTree
|
|
6
|
+
|
|
7
|
+
from markdown import Markdown
|
|
8
|
+
from markdown.extensions import Extension
|
|
9
|
+
from markdown.treeprocessors import Treeprocessor
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_TARGET = "LaTeX"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _LatexTextTreeprocessor(Treeprocessor):
|
|
16
|
+
"""Replace plain ``LaTeX`` occurrences with a styled HTML fragment."""
|
|
17
|
+
|
|
18
|
+
def run(self, root: ElementTree.Element) -> None: # type: ignore[override]
|
|
19
|
+
for paragraph in root.iter("p"):
|
|
20
|
+
self._process_element(paragraph)
|
|
21
|
+
|
|
22
|
+
# -- internals -----------------------------------------------------
|
|
23
|
+
def _process_element(self, element: ElementTree.Element) -> None:
|
|
24
|
+
self._replace_text_node(element)
|
|
25
|
+
for child in list(element):
|
|
26
|
+
if self._is_code_element(child):
|
|
27
|
+
if child.tail:
|
|
28
|
+
self._replace_tail(element, child)
|
|
29
|
+
continue
|
|
30
|
+
|
|
31
|
+
self._process_element(child)
|
|
32
|
+
if child.tail:
|
|
33
|
+
self._replace_tail(element, child)
|
|
34
|
+
|
|
35
|
+
def _replace_text_node(self, element: ElementTree.Element) -> None:
|
|
36
|
+
text = element.text
|
|
37
|
+
if not text or _TARGET not in text:
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
parts = text.split(_TARGET)
|
|
41
|
+
element.text = parts[0]
|
|
42
|
+
for insert_pos, remainder in enumerate(parts[1:]):
|
|
43
|
+
fragment = self._build_fragment()
|
|
44
|
+
element.insert(insert_pos, fragment)
|
|
45
|
+
if remainder:
|
|
46
|
+
fragment.tail = remainder
|
|
47
|
+
|
|
48
|
+
def _replace_tail(self, parent: ElementTree.Element, child: ElementTree.Element) -> None:
|
|
49
|
+
tail = child.tail
|
|
50
|
+
if not tail or _TARGET not in tail:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
parts = tail.split(_TARGET)
|
|
54
|
+
child.tail = parts[0]
|
|
55
|
+
base_index = list(parent).index(child) + 1
|
|
56
|
+
for offset, remainder in enumerate(parts[1:]):
|
|
57
|
+
fragment = self._build_fragment()
|
|
58
|
+
parent.insert(base_index + offset, fragment)
|
|
59
|
+
if remainder:
|
|
60
|
+
fragment.tail = remainder
|
|
61
|
+
|
|
62
|
+
def _is_code_element(self, element: ElementTree.Element) -> bool:
|
|
63
|
+
tag = element.tag
|
|
64
|
+
if isinstance(tag, str):
|
|
65
|
+
tag = tag.lower()
|
|
66
|
+
return tag in {"code", "pre"}
|
|
67
|
+
|
|
68
|
+
def _build_fragment(self) -> ElementTree.Element:
|
|
69
|
+
outer = ElementTree.Element(
|
|
70
|
+
"span",
|
|
71
|
+
{
|
|
72
|
+
"class": "latex-text",
|
|
73
|
+
"style": "font-family: 'Times New Roman', serif;",
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
outer.text = "L"
|
|
77
|
+
|
|
78
|
+
lowered_a = ElementTree.SubElement(
|
|
79
|
+
outer,
|
|
80
|
+
"span",
|
|
81
|
+
{"style": ("position: relative; top: 0.2em; left: -0.05em; font-size: 0.9em;")},
|
|
82
|
+
)
|
|
83
|
+
lowered_a.text = "a"
|
|
84
|
+
lowered_a.tail = "T"
|
|
85
|
+
|
|
86
|
+
small_caps_e = ElementTree.SubElement(
|
|
87
|
+
outer,
|
|
88
|
+
"span",
|
|
89
|
+
{
|
|
90
|
+
"style": "font-variant: small-caps;",
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
small_caps_e.text = "e"
|
|
94
|
+
|
|
95
|
+
ElementTree.SubElement(
|
|
96
|
+
outer,
|
|
97
|
+
"span",
|
|
98
|
+
{
|
|
99
|
+
"style": "font-variant: small-caps; letter-spacing: -0.05em;",
|
|
100
|
+
},
|
|
101
|
+
).text = "X"
|
|
102
|
+
|
|
103
|
+
return outer
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class LatexTextExtension(Extension):
|
|
107
|
+
"""Register the ``LaTeX`` paragraph replacement processor."""
|
|
108
|
+
|
|
109
|
+
def extendMarkdown(self, md: Markdown) -> None: # type: ignore[override] # noqa: N802
|
|
110
|
+
md.treeprocessors.register(_LatexTextTreeprocessor(md), "texsmith_latex_text", priority=15)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def makeExtension(**_: object) -> LatexTextExtension: # pragma: no cover - API hook # noqa: N802
|
|
114
|
+
return LatexTextExtension()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
__all__ = ["LatexTextExtension", "makeExtension"]
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Markdown extension that inlines Mermaid diagrams referenced via images."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable, Sequence
|
|
6
|
+
from pathlib import Path
|
|
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 ..adapters.handlers._mermaid import (
|
|
14
|
+
MERMAID_FILE_SUFFIXES,
|
|
15
|
+
extract_mermaid_live_diagram,
|
|
16
|
+
looks_like_mermaid,
|
|
17
|
+
)
|
|
18
|
+
from ..core.exceptions import InvalidNodeError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _collapse_relative_path(src: str) -> str:
|
|
22
|
+
"""Return a relative path with ``..`` segments normalised away."""
|
|
23
|
+
if not src or src.startswith(("/", "\\")):
|
|
24
|
+
return src
|
|
25
|
+
|
|
26
|
+
segments: list[str] = []
|
|
27
|
+
for segment in src.split("/"):
|
|
28
|
+
if not segment or segment == ".":
|
|
29
|
+
continue
|
|
30
|
+
if segment == "..":
|
|
31
|
+
if segments:
|
|
32
|
+
segments.pop()
|
|
33
|
+
continue
|
|
34
|
+
segments.append(segment)
|
|
35
|
+
return "/".join(segments)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _MermaidImageTreeprocessor(Treeprocessor):
|
|
39
|
+
"""Replace standalone Mermaid image references with code blocks."""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
md: Markdown,
|
|
44
|
+
*,
|
|
45
|
+
extra_base_paths: Sequence[Path] | None = None,
|
|
46
|
+
) -> None:
|
|
47
|
+
super().__init__(md)
|
|
48
|
+
self._extra_base_paths = [path for path in (extra_base_paths or []) if path]
|
|
49
|
+
|
|
50
|
+
def run(self, root: ElementTree.Element) -> ElementTree.Element: # type: ignore[override]
|
|
51
|
+
base_path_value = getattr(self.md, "texsmith_mermaid_base_path", None)
|
|
52
|
+
base_path: Path | None = None
|
|
53
|
+
if base_path_value:
|
|
54
|
+
try:
|
|
55
|
+
base_path = Path(base_path_value).resolve()
|
|
56
|
+
except OSError:
|
|
57
|
+
base_path = Path(base_path_value)
|
|
58
|
+
|
|
59
|
+
parent_map: dict[ElementTree.Element, ElementTree.Element] = {}
|
|
60
|
+
for parent in root.iter():
|
|
61
|
+
for child in list(parent):
|
|
62
|
+
parent_map[child] = parent
|
|
63
|
+
|
|
64
|
+
for paragraph in list(root.iter("p")):
|
|
65
|
+
image, link_href = self._extract_image(paragraph)
|
|
66
|
+
if image is None:
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
diagram_payload = self._load_diagram(image, base_path)
|
|
70
|
+
if diagram_payload is None:
|
|
71
|
+
continue
|
|
72
|
+
diagram, source, trusted = diagram_payload
|
|
73
|
+
|
|
74
|
+
caption = (image.get("alt") or image.get("title") or "").strip()
|
|
75
|
+
body = self._apply_caption(diagram, caption)
|
|
76
|
+
if not trusted and not looks_like_mermaid(body):
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
replacement = self._build_mermaid_block(body, link_href, source)
|
|
80
|
+
wrapper = self._wrap_with_link(replacement, link_href)
|
|
81
|
+
self._replace_paragraph(parent_map.get(paragraph), paragraph, wrapper)
|
|
82
|
+
|
|
83
|
+
return root
|
|
84
|
+
|
|
85
|
+
def _extract_image(
|
|
86
|
+
self, paragraph: ElementTree.Element
|
|
87
|
+
) -> tuple[ElementTree.Element | None, str | None]:
|
|
88
|
+
if any((node.tail or "").strip() for node in paragraph):
|
|
89
|
+
return None, None
|
|
90
|
+
paragraph_text = (paragraph.text or "").strip()
|
|
91
|
+
if paragraph_text:
|
|
92
|
+
return None, None
|
|
93
|
+
if len(paragraph) != 1:
|
|
94
|
+
return None, None
|
|
95
|
+
|
|
96
|
+
node = paragraph[0]
|
|
97
|
+
if node.tag == "img":
|
|
98
|
+
if (node.tail or "").strip():
|
|
99
|
+
return None, None
|
|
100
|
+
return node, None
|
|
101
|
+
|
|
102
|
+
if node.tag == "a" and len(node) == 1 and node[0].tag == "img":
|
|
103
|
+
image = node[0]
|
|
104
|
+
if (node.text or "").strip() or (image.tail or "").strip():
|
|
105
|
+
return None, None
|
|
106
|
+
href = node.get("href")
|
|
107
|
+
return image, href
|
|
108
|
+
|
|
109
|
+
return None, None
|
|
110
|
+
|
|
111
|
+
def _load_diagram(
|
|
112
|
+
self,
|
|
113
|
+
image: ElementTree.Element,
|
|
114
|
+
base_path: Path | None,
|
|
115
|
+
) -> tuple[str, str, bool] | None:
|
|
116
|
+
raw_src = image.get("src")
|
|
117
|
+
if not raw_src:
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
cleaned = raw_src.replace("\n", "").replace("\r", "").strip()
|
|
121
|
+
if not cleaned:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
source_hint = cleaned
|
|
125
|
+
simplified = cleaned.split("?", 1)[0].split("#", 1)[0].lower()
|
|
126
|
+
if simplified.endswith(MERMAID_FILE_SUFFIXES):
|
|
127
|
+
if cleaned.startswith(("http://", "https://")):
|
|
128
|
+
return None
|
|
129
|
+
resolved = self._resolve_path(cleaned, base_path)
|
|
130
|
+
if resolved is None:
|
|
131
|
+
return None
|
|
132
|
+
try:
|
|
133
|
+
payload = resolved.read_text(encoding="utf-8")
|
|
134
|
+
except OSError:
|
|
135
|
+
return None
|
|
136
|
+
return payload, str(resolved), True
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
diagram = extract_mermaid_live_diagram(cleaned)
|
|
140
|
+
except InvalidNodeError:
|
|
141
|
+
return None
|
|
142
|
+
if diagram is not None:
|
|
143
|
+
return diagram, source_hint, False
|
|
144
|
+
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
def _resolve_path(self, src: str, base_path: Path | None) -> Path | None:
|
|
148
|
+
normalised = _collapse_relative_path(src.replace("\\", "/"))
|
|
149
|
+
candidate = Path(normalised)
|
|
150
|
+
if candidate.is_absolute():
|
|
151
|
+
return candidate if candidate.exists() else None
|
|
152
|
+
|
|
153
|
+
search_roots: list[Path] = []
|
|
154
|
+
if base_path is not None:
|
|
155
|
+
search_roots.append(base_path)
|
|
156
|
+
for fallback in self._extra_base_paths:
|
|
157
|
+
if fallback not in search_roots:
|
|
158
|
+
search_roots.append(fallback)
|
|
159
|
+
|
|
160
|
+
for root in search_roots:
|
|
161
|
+
resolved = (root / normalised).resolve()
|
|
162
|
+
if resolved.exists():
|
|
163
|
+
return resolved
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
def _apply_caption(self, diagram: str, caption: str) -> str:
|
|
167
|
+
if not caption:
|
|
168
|
+
return diagram
|
|
169
|
+
|
|
170
|
+
meaningful_line = None
|
|
171
|
+
for line in diagram.splitlines():
|
|
172
|
+
stripped = line.strip()
|
|
173
|
+
if not stripped:
|
|
174
|
+
continue
|
|
175
|
+
meaningful_line = stripped
|
|
176
|
+
break
|
|
177
|
+
|
|
178
|
+
if meaningful_line and meaningful_line.startswith("%%"):
|
|
179
|
+
return diagram
|
|
180
|
+
|
|
181
|
+
sanitized = caption.strip()
|
|
182
|
+
if not sanitized:
|
|
183
|
+
return diagram
|
|
184
|
+
|
|
185
|
+
body = diagram.lstrip("\ufeff")
|
|
186
|
+
return f"%% {sanitized}\n{body}"
|
|
187
|
+
|
|
188
|
+
def _build_mermaid_block(
|
|
189
|
+
self,
|
|
190
|
+
diagram: str,
|
|
191
|
+
link_href: str | None,
|
|
192
|
+
source_hint: str,
|
|
193
|
+
) -> ElementTree.Element:
|
|
194
|
+
pre = ElementTree.Element("pre", {"class": "mermaid"})
|
|
195
|
+
if link_href:
|
|
196
|
+
pre.set("data-mermaid-link", link_href)
|
|
197
|
+
if source_hint:
|
|
198
|
+
pre.set("data-mermaid-source", source_hint)
|
|
199
|
+
pre.text = diagram
|
|
200
|
+
return pre
|
|
201
|
+
|
|
202
|
+
def _wrap_with_link(
|
|
203
|
+
self,
|
|
204
|
+
node: ElementTree.Element,
|
|
205
|
+
link_href: str | None,
|
|
206
|
+
) -> ElementTree.Element:
|
|
207
|
+
if not link_href:
|
|
208
|
+
return node
|
|
209
|
+
|
|
210
|
+
anchor = ElementTree.Element("a", {"href": link_href})
|
|
211
|
+
anchor.append(node)
|
|
212
|
+
return anchor
|
|
213
|
+
|
|
214
|
+
def _replace_paragraph(
|
|
215
|
+
self,
|
|
216
|
+
parent: ElementTree.Element | None,
|
|
217
|
+
paragraph: ElementTree.Element,
|
|
218
|
+
replacement: ElementTree.Element,
|
|
219
|
+
) -> None:
|
|
220
|
+
replacement.tail = paragraph.tail
|
|
221
|
+
if parent is None:
|
|
222
|
+
return
|
|
223
|
+
for index, child in enumerate(list(parent)):
|
|
224
|
+
if child is paragraph:
|
|
225
|
+
parent.insert(index, replacement)
|
|
226
|
+
parent.remove(paragraph)
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class MermaidExtension(Extension):
|
|
231
|
+
"""Register the Mermaid image treeprocessor."""
|
|
232
|
+
|
|
233
|
+
def __init__(self, **kwargs: object) -> None:
|
|
234
|
+
self.config = {
|
|
235
|
+
"base_paths": [(), "Additional directories searched for Mermaid files."],
|
|
236
|
+
}
|
|
237
|
+
super().__init__(**kwargs)
|
|
238
|
+
|
|
239
|
+
@staticmethod
|
|
240
|
+
def _coerce_paths(paths: Iterable[object]) -> list[Path]:
|
|
241
|
+
resolved: list[Path] = []
|
|
242
|
+
for entry in paths:
|
|
243
|
+
if entry is None:
|
|
244
|
+
continue
|
|
245
|
+
try:
|
|
246
|
+
candidate = Path(entry).expanduser()
|
|
247
|
+
except TypeError:
|
|
248
|
+
continue
|
|
249
|
+
try:
|
|
250
|
+
resolved.append(candidate.resolve())
|
|
251
|
+
except OSError:
|
|
252
|
+
resolved.append(candidate)
|
|
253
|
+
return resolved
|
|
254
|
+
|
|
255
|
+
def extendMarkdown(self, md: Markdown) -> None: # type: ignore[override] # noqa: N802
|
|
256
|
+
extra_paths = self._coerce_paths(self.getConfig("base_paths") or ())
|
|
257
|
+
processor = _MermaidImageTreeprocessor(md, extra_base_paths=extra_paths)
|
|
258
|
+
md.treeprocessors.register(processor, "texsmith_mermaid_images", priority=15)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def makeExtension( # noqa: N802 - Markdown expects this entry point name
|
|
262
|
+
**kwargs: object,
|
|
263
|
+
) -> MermaidExtension: # pragma: no cover - entry point
|
|
264
|
+
return MermaidExtension(**kwargs)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
__all__ = ["MermaidExtension", "makeExtension"]
|