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,267 @@
|
|
|
1
|
+
"""Handlers for generic admonition and callout markup."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
from bs4.element import NavigableString, Tag
|
|
9
|
+
|
|
10
|
+
from texsmith.core.callouts import DEFAULT_CALLOUTS
|
|
11
|
+
from texsmith.core.context import RenderContext
|
|
12
|
+
from texsmith.core.diagnostics import DiagnosticEmitter
|
|
13
|
+
from texsmith.core.rules import RenderPhase, renders
|
|
14
|
+
|
|
15
|
+
from ._helpers import gather_classes, mark_processed
|
|
16
|
+
from .code import (
|
|
17
|
+
render_code_blocks as _render_code_block,
|
|
18
|
+
render_preformatted_code as _render_preformatted_code,
|
|
19
|
+
render_standalone_code_blocks as _render_standalone_code_block,
|
|
20
|
+
)
|
|
21
|
+
from .inline import render_inline_code as _render_inline_code
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
IGNORED_CLASSES = {
|
|
25
|
+
"admonition",
|
|
26
|
+
"annotate",
|
|
27
|
+
"inline",
|
|
28
|
+
"end",
|
|
29
|
+
"left",
|
|
30
|
+
"right",
|
|
31
|
+
"checkbox",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _trim_paragraph_prefix(paragraph: Tag, length: int) -> None:
|
|
36
|
+
"""Remove the first ``length`` characters from a paragraph in-place."""
|
|
37
|
+
remaining = length
|
|
38
|
+
for node in list(paragraph.descendants):
|
|
39
|
+
if not isinstance(node, NavigableString):
|
|
40
|
+
continue
|
|
41
|
+
text = str(node)
|
|
42
|
+
if remaining >= len(text):
|
|
43
|
+
remaining -= len(text)
|
|
44
|
+
node.extract()
|
|
45
|
+
if remaining == 0:
|
|
46
|
+
break
|
|
47
|
+
continue
|
|
48
|
+
node.replace_with(NavigableString(text[remaining:]))
|
|
49
|
+
remaining = 0
|
|
50
|
+
break
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@contextmanager
|
|
54
|
+
def _use_tcolorbox_figures(context: RenderContext):
|
|
55
|
+
"""Render nested figures inside callouts with the tcolorbox template."""
|
|
56
|
+
previous = context.runtime.get("figure_template")
|
|
57
|
+
context.runtime["figure_template"] = "figure_tcolorbox"
|
|
58
|
+
try:
|
|
59
|
+
yield
|
|
60
|
+
finally:
|
|
61
|
+
if previous is None:
|
|
62
|
+
context.runtime.pop("figure_template", None)
|
|
63
|
+
else:
|
|
64
|
+
context.runtime["figure_template"] = previous
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _extract_title(node: Tag | None) -> str:
|
|
68
|
+
"""Extract and remove the title paragraph produced by MkDocs Material."""
|
|
69
|
+
if node is None:
|
|
70
|
+
return ""
|
|
71
|
+
if label := node.find("span", class_="exercise-label"):
|
|
72
|
+
label.decompose()
|
|
73
|
+
title = node.get_text(strip=True)
|
|
74
|
+
node.decompose()
|
|
75
|
+
return title
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
CALLOUT_ALIASES = {"seealso": "info"}
|
|
79
|
+
DEFAULT_CALLOUT_SET = set(DEFAULT_CALLOUTS)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _runtime_emitter(context: RenderContext) -> DiagnosticEmitter | None:
|
|
83
|
+
emitter = context.runtime.get("emitter")
|
|
84
|
+
return emitter if getattr(emitter, "warning", None) else None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _prepare_callout_content(element: Tag, context: RenderContext) -> None:
|
|
88
|
+
"""Normalise inline and block code inside callouts before flattening."""
|
|
89
|
+
for highlight in list(element.find_all("div")):
|
|
90
|
+
highlight_classes = gather_classes(highlight.get("class"))
|
|
91
|
+
if "highlight" in highlight_classes or "codehilite" in highlight_classes:
|
|
92
|
+
_render_code_block(highlight, context)
|
|
93
|
+
|
|
94
|
+
for pre in list(element.find_all("pre")):
|
|
95
|
+
_render_preformatted_code(pre, context)
|
|
96
|
+
|
|
97
|
+
for code_block in list(element.find_all("code")):
|
|
98
|
+
_render_standalone_code_block(code_block, context)
|
|
99
|
+
|
|
100
|
+
for code in list(element.find_all("code")):
|
|
101
|
+
if code.find_parent("pre"):
|
|
102
|
+
continue
|
|
103
|
+
_render_inline_code(code, context)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _promote_callout(
|
|
107
|
+
element: Tag, context: RenderContext, *, classes: list[str], title: str
|
|
108
|
+
) -> None:
|
|
109
|
+
"""Re-tag a node so it can be converted after its children render."""
|
|
110
|
+
element["data-callout-title"] = title
|
|
111
|
+
element["class"] = classes
|
|
112
|
+
element.name = "texsmith-callout"
|
|
113
|
+
_prepare_callout_content(element, context)
|
|
114
|
+
element.attrs["data-callout-prepared"] = True
|
|
115
|
+
context.mark_processed(element, phase=RenderPhase.POST)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _render_admonition(
|
|
119
|
+
element: Tag, context: RenderContext, *, classes: list[str], title: str
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Convert a generic admonition container into a LaTeX callout."""
|
|
122
|
+
admonition_classes = [cls for cls in classes if cls not in IGNORED_CLASSES]
|
|
123
|
+
admonition_type = admonition_classes[0] if admonition_classes else "note"
|
|
124
|
+
admonition_type = CALLOUT_ALIASES.get(admonition_type, admonition_type)
|
|
125
|
+
callouts_definitions = context.runtime.get("callouts_definitions") or DEFAULT_CALLOUTS
|
|
126
|
+
if admonition_type not in callouts_definitions:
|
|
127
|
+
emitter = _runtime_emitter(context)
|
|
128
|
+
if emitter:
|
|
129
|
+
emitter.warning(f"Unknown callout/admonition type '{admonition_type}', using default.")
|
|
130
|
+
admonition_type = "default"
|
|
131
|
+
|
|
132
|
+
if not element.attrs.pop("data-callout-prepared", False):
|
|
133
|
+
_prepare_callout_content(element, context)
|
|
134
|
+
|
|
135
|
+
with _use_tcolorbox_figures(context):
|
|
136
|
+
content = element.get_text(strip=False).strip()
|
|
137
|
+
|
|
138
|
+
context.state.callouts_used = True
|
|
139
|
+
latex = context.formatter.callout(content, title=title, type=admonition_type)
|
|
140
|
+
parent = element.parent
|
|
141
|
+
replacement = mark_processed(NavigableString(latex))
|
|
142
|
+
if parent is None:
|
|
143
|
+
element.decompose()
|
|
144
|
+
context.document.append(replacement)
|
|
145
|
+
else:
|
|
146
|
+
element.replace_with(replacement)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@renders(
|
|
150
|
+
"div",
|
|
151
|
+
phase=RenderPhase.POST,
|
|
152
|
+
priority=50,
|
|
153
|
+
name="admonitions",
|
|
154
|
+
nestable=True,
|
|
155
|
+
auto_mark=False,
|
|
156
|
+
)
|
|
157
|
+
def render_div_admonitions(element: Tag, context: RenderContext) -> None:
|
|
158
|
+
"""Handle MkDocs Material admonition blocks."""
|
|
159
|
+
classes = gather_classes(element.get("class"))
|
|
160
|
+
if "admonition" not in classes:
|
|
161
|
+
return
|
|
162
|
+
if "exercise" in classes:
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
title = _extract_title(element.find("p", class_="admonition-title"))
|
|
166
|
+
_promote_callout(element, context, classes=classes, title=title)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@renders(
|
|
170
|
+
"blockquote",
|
|
171
|
+
phase=RenderPhase.POST,
|
|
172
|
+
priority=15,
|
|
173
|
+
name="blockquote_callouts",
|
|
174
|
+
nestable=True,
|
|
175
|
+
auto_mark=False,
|
|
176
|
+
)
|
|
177
|
+
def render_blockquote_callouts(element: Tag, context: RenderContext) -> None:
|
|
178
|
+
"""Handle Obsidian/Docusaurus style blockquote callouts."""
|
|
179
|
+
first_paragraph = element.find("p")
|
|
180
|
+
if first_paragraph is None:
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
first_text_node = next(
|
|
184
|
+
(child for child in first_paragraph.contents if isinstance(child, NavigableString)),
|
|
185
|
+
None,
|
|
186
|
+
)
|
|
187
|
+
if first_text_node is None:
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
raw_text = str(first_text_node)
|
|
191
|
+
stripped = raw_text.lstrip()
|
|
192
|
+
offset = len(raw_text) - len(stripped)
|
|
193
|
+
match = CALLOUT_PATTERN.match(stripped)
|
|
194
|
+
if not match:
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
callout_type = match.group("kind").lower()
|
|
198
|
+
remainder = match.group("content") or ""
|
|
199
|
+
remainder = remainder.lstrip()
|
|
200
|
+
|
|
201
|
+
first_text_node.replace_with(NavigableString(raw_text[:offset] + remainder))
|
|
202
|
+
|
|
203
|
+
lines_with_endings = remainder.splitlines(keepends=True)
|
|
204
|
+
lines = [line.strip() for line in remainder.splitlines() if line.strip()]
|
|
205
|
+
title = lines[0] if lines else callout_type.capitalize()
|
|
206
|
+
|
|
207
|
+
if len(lines_with_endings) > 1:
|
|
208
|
+
prefix_length = len(raw_text[:offset]) + len(lines_with_endings[0])
|
|
209
|
+
_trim_paragraph_prefix(first_paragraph, prefix_length)
|
|
210
|
+
first_text = next(
|
|
211
|
+
(child for child in first_paragraph.contents if isinstance(child, NavigableString)),
|
|
212
|
+
None,
|
|
213
|
+
)
|
|
214
|
+
if first_text is not None:
|
|
215
|
+
first_text.replace_with(NavigableString(str(first_text).lstrip()))
|
|
216
|
+
else:
|
|
217
|
+
first_paragraph.decompose()
|
|
218
|
+
|
|
219
|
+
classes = gather_classes(element.get("class"))
|
|
220
|
+
preserved = [cls for cls in classes if cls != "admonition"]
|
|
221
|
+
new_classes = ["admonition", callout_type]
|
|
222
|
+
for cls in preserved:
|
|
223
|
+
if cls not in new_classes:
|
|
224
|
+
new_classes.append(cls)
|
|
225
|
+
element["class"] = new_classes
|
|
226
|
+
_promote_callout(element, context, classes=new_classes, title=title)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@renders(
|
|
230
|
+
"details",
|
|
231
|
+
phase=RenderPhase.POST,
|
|
232
|
+
priority=55,
|
|
233
|
+
name="details_admonitions",
|
|
234
|
+
nestable=True,
|
|
235
|
+
auto_mark=False,
|
|
236
|
+
)
|
|
237
|
+
def render_details_admonitions(element: Tag, context: RenderContext) -> None:
|
|
238
|
+
"""Handle collapsible callouts converted from MkDocs Material's details blocks."""
|
|
239
|
+
classes = gather_classes(element.get("class"))
|
|
240
|
+
if "exercise" in classes:
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
title = ""
|
|
244
|
+
if summary := element.find("summary"):
|
|
245
|
+
title = summary.get_text(strip=True)
|
|
246
|
+
summary.decompose()
|
|
247
|
+
|
|
248
|
+
_promote_callout(element, context, classes=classes, title=title or "")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@renders(
|
|
252
|
+
"texsmith-callout",
|
|
253
|
+
phase=RenderPhase.POST,
|
|
254
|
+
priority=140,
|
|
255
|
+
name="finalize_callouts",
|
|
256
|
+
nestable=False,
|
|
257
|
+
auto_mark=False,
|
|
258
|
+
after_children=True,
|
|
259
|
+
)
|
|
260
|
+
def render_texsmith_callouts(element: Tag, context: RenderContext) -> None:
|
|
261
|
+
"""Convert promoted callout nodes once their children have rendered."""
|
|
262
|
+
classes = gather_classes(element.get("class"))
|
|
263
|
+
title = element.attrs.pop("data-callout-title", "")
|
|
264
|
+
_render_admonition(element, context, classes=classes, title=title)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
CALLOUT_PATTERN = re.compile(r"^\s*\[!(?P<kind>[A-Za-z0-9_-]+)\]\s*(?P<content>.*)$", re.DOTALL)
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""Built-in baseline handlers used by the renderer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from bs4.element import NavigableString, Tag
|
|
6
|
+
from slugify import slugify
|
|
7
|
+
|
|
8
|
+
from texsmith.core.context import RenderContext
|
|
9
|
+
from texsmith.core.rules import RenderPhase, renders
|
|
10
|
+
from texsmith.fonts.scripts import render_moving_text
|
|
11
|
+
|
|
12
|
+
from ._helpers import coerce_attribute, gather_classes, mark_processed
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
UNWANTED_NODES: tuple[tuple[str, tuple[str, ...], str], ...] = (
|
|
16
|
+
("html", (), "unwrap"),
|
|
17
|
+
("head", (), "decompose"),
|
|
18
|
+
("body", (), "unwrap"),
|
|
19
|
+
("a", ("headerlink", "footnote-backref"), "extract"),
|
|
20
|
+
("a", ("glightbox",), "unwrap"),
|
|
21
|
+
("div", ("latex-ignore",), "extract"),
|
|
22
|
+
("span", ("exercise-title",), "unwrap"),
|
|
23
|
+
("div", ("exercise-checkbox",), "extract"),
|
|
24
|
+
("figure", ("mermaid-figure",), "extract"),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _merge_strip_rules(
|
|
29
|
+
context: RenderContext,
|
|
30
|
+
) -> list[tuple[str, tuple[str, ...], str]]:
|
|
31
|
+
"""Merge default strip rules with runtime overrides."""
|
|
32
|
+
rules: dict[tuple[str, tuple[str, ...]], str] = {
|
|
33
|
+
(tag, classes): mode for tag, classes, mode in UNWANTED_NODES
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
runtime_rules = context.runtime.get("strip_tags") or {}
|
|
37
|
+
if isinstance(runtime_rules, dict):
|
|
38
|
+
for tag_name, payload in runtime_rules.items():
|
|
39
|
+
if isinstance(payload, str):
|
|
40
|
+
mode = payload
|
|
41
|
+
classes: tuple[str, ...] = ()
|
|
42
|
+
elif isinstance(payload, dict):
|
|
43
|
+
mode = payload.get("mode", "unwrap")
|
|
44
|
+
classes_value = payload.get("classes", ())
|
|
45
|
+
if isinstance(classes_value, str):
|
|
46
|
+
classes = (classes_value,)
|
|
47
|
+
else:
|
|
48
|
+
classes = tuple(classes_value)
|
|
49
|
+
else:
|
|
50
|
+
continue
|
|
51
|
+
if mode not in {"unwrap", "extract", "decompose"}:
|
|
52
|
+
continue
|
|
53
|
+
rules[(tag_name, classes)] = mode
|
|
54
|
+
|
|
55
|
+
merged = [(tag, classes, rules[(tag, classes)]) for tag, classes in rules]
|
|
56
|
+
return merged
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@renders(phase=RenderPhase.PRE, auto_mark=False, name="discard_unwanted")
|
|
60
|
+
def discard_unwanted(root: Tag, context: RenderContext) -> None:
|
|
61
|
+
"""Discard or unwrap nodes that must not reach later phases."""
|
|
62
|
+
for tag_name, classes, mode in _merge_strip_rules(context):
|
|
63
|
+
class_filter = list(classes)
|
|
64
|
+
candidates = (
|
|
65
|
+
root.find_all(tag_name, class_=class_filter)
|
|
66
|
+
if class_filter
|
|
67
|
+
else root.find_all(tag_name)
|
|
68
|
+
)
|
|
69
|
+
for node in candidates:
|
|
70
|
+
if mode == "unwrap":
|
|
71
|
+
node.unwrap()
|
|
72
|
+
elif mode == "extract":
|
|
73
|
+
node.extract()
|
|
74
|
+
elif mode == "decompose":
|
|
75
|
+
node.decompose()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@renders("hr", phase=RenderPhase.BLOCK, name="render_horizontal_rule")
|
|
79
|
+
def render_horizontal_rule(element: Tag, context: RenderContext) -> None:
|
|
80
|
+
"""Render ``<hr>`` nodes as LaTeX horizontal rules."""
|
|
81
|
+
latex = context.formatter.horizontal_rule()
|
|
82
|
+
element.replace_with(mark_processed(NavigableString(latex)))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@renders("br", phase=RenderPhase.INLINE, name="line_breaks")
|
|
86
|
+
def replace_line_breaks(element: Tag, _context: RenderContext) -> None:
|
|
87
|
+
"""Convert ``<br>`` tags into explicit LaTeX line breaks."""
|
|
88
|
+
element.replace_with(mark_processed(NavigableString("\\")))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@renders("ins", phase=RenderPhase.INLINE, name="inline_underline", after_children=True)
|
|
92
|
+
def render_inline_underline(element: Tag, context: RenderContext) -> None:
|
|
93
|
+
"""Render ``<ins>`` tags using the formatter."""
|
|
94
|
+
text = element.get_text(strip=False)
|
|
95
|
+
latex = context.formatter.underline(text=text)
|
|
96
|
+
element.replace_with(NavigableString(latex))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@renders("span", phase=RenderPhase.INLINE, name="inline_smallcaps", auto_mark=False)
|
|
100
|
+
def render_inline_smallcaps(element: Tag, context: RenderContext) -> None:
|
|
101
|
+
"""Render ``<span class=\"texsmith-smallcaps\">`` nodes as small capitals."""
|
|
102
|
+
classes = gather_classes(element.get("class"))
|
|
103
|
+
if "texsmith-smallcaps" not in classes:
|
|
104
|
+
return
|
|
105
|
+
text = element.get_text(strip=False)
|
|
106
|
+
latex = context.formatter.smallcaps(text=text)
|
|
107
|
+
element.replace_with(NavigableString(latex))
|
|
108
|
+
context.mark_processed(element)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@renders("strong", phase=RenderPhase.INLINE, name="inline_strong", after_children=True)
|
|
112
|
+
def render_inline_strong(element: Tag, context: RenderContext) -> None:
|
|
113
|
+
"""Render ``<strong>`` tags using bold template."""
|
|
114
|
+
text = element.get_text(strip=False)
|
|
115
|
+
latex = context.formatter.strong(text=text)
|
|
116
|
+
element.replace_with(NavigableString(latex))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@renders("em", phase=RenderPhase.INLINE, name="inline_emphasis", after_children=True)
|
|
120
|
+
def render_inline_emphasis(element: Tag, context: RenderContext) -> None:
|
|
121
|
+
"""Render ``<em>`` tags using emphasis template."""
|
|
122
|
+
text = element.get_text(strip=False)
|
|
123
|
+
latex = context.formatter.italic(text=text)
|
|
124
|
+
element.replace_with(NavigableString(latex))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@renders("del", phase=RenderPhase.INLINE, name="inline_deletion", after_children=True)
|
|
128
|
+
def render_inline_deletion(element: Tag, context: RenderContext) -> None:
|
|
129
|
+
"""Render ``<del>`` tags using strikethrough template."""
|
|
130
|
+
text = element.get_text(strip=False)
|
|
131
|
+
latex = context.formatter.strikethrough(text=text)
|
|
132
|
+
element.replace_with(NavigableString(latex))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@renders("mark", phase=RenderPhase.INLINE, name="inline_mark", after_children=True)
|
|
136
|
+
def render_inline_mark(element: Tag, context: RenderContext) -> None:
|
|
137
|
+
"""Render ``<mark>`` tags using highlight template."""
|
|
138
|
+
text = element.get_text(strip=False)
|
|
139
|
+
latex = context.formatter.highlight(text=text)
|
|
140
|
+
element.replace_with(NavigableString(latex))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@renders("sub", phase=RenderPhase.INLINE, name="inline_subscript", after_children=True)
|
|
144
|
+
def render_inline_subscript(element: Tag, context: RenderContext) -> None:
|
|
145
|
+
"""Render ``<sub>`` tags."""
|
|
146
|
+
text = element.get_text(strip=False)
|
|
147
|
+
latex = context.formatter.subscript(text=text)
|
|
148
|
+
element.replace_with(NavigableString(latex))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@renders("sup", phase=RenderPhase.INLINE, name="inline_superscript", after_children=True)
|
|
152
|
+
def render_inline_superscript(element: Tag, context: RenderContext) -> None:
|
|
153
|
+
"""Render ``<sup>`` tags, skipping footnote references."""
|
|
154
|
+
if element.get("id"):
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
text = element.get_text(strip=False)
|
|
158
|
+
latex = context.formatter.superscript(text=text)
|
|
159
|
+
element.replace_with(NavigableString(latex))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@renders("q", phase=RenderPhase.INLINE, name="inline_quote", after_children=True)
|
|
163
|
+
def render_inline_quote(element: Tag, context: RenderContext) -> None:
|
|
164
|
+
"""Render inline quotations using \\enquote{}."""
|
|
165
|
+
text = element.get_text(strip=False)
|
|
166
|
+
latex = context.formatter.enquote(text=text)
|
|
167
|
+
element.replace_with(NavigableString(latex))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@renders("div", phase=RenderPhase.BLOCK, name="grid_cards", auto_mark=False)
|
|
171
|
+
def unwrap_grid_cards(element: Tag, _context: RenderContext) -> None:
|
|
172
|
+
"""Unwrap ``div.grid-cards`` containers."""
|
|
173
|
+
classes = element.get("class") or []
|
|
174
|
+
if "grid-cards" in classes:
|
|
175
|
+
element.unwrap()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@renders(
|
|
179
|
+
"h1",
|
|
180
|
+
"h2",
|
|
181
|
+
"h3",
|
|
182
|
+
"h4",
|
|
183
|
+
"h5",
|
|
184
|
+
"h6",
|
|
185
|
+
phase=RenderPhase.POST,
|
|
186
|
+
name="render_headings",
|
|
187
|
+
)
|
|
188
|
+
def render_headings(element: Tag, context: RenderContext) -> None:
|
|
189
|
+
"""Convert HTML headings to LaTeX sectioning commands."""
|
|
190
|
+
# Drop anchor tags within headings
|
|
191
|
+
for anchor in element.find_all("a"):
|
|
192
|
+
anchor.unwrap()
|
|
193
|
+
|
|
194
|
+
drop_title = context.runtime.get("drop_title")
|
|
195
|
+
if drop_title:
|
|
196
|
+
context.runtime["drop_title"] = False
|
|
197
|
+
latex = context.formatter.pagestyle(text="plain")
|
|
198
|
+
element.replace_with(mark_processed(NavigableString(latex)))
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
raw_text = element.get_text(strip=False)
|
|
202
|
+
text = render_moving_text(
|
|
203
|
+
raw_text,
|
|
204
|
+
context,
|
|
205
|
+
legacy_accents=getattr(context.config, "legacy_latex_accents", False),
|
|
206
|
+
escape="\\" not in raw_text,
|
|
207
|
+
wrap_scripts=True,
|
|
208
|
+
)
|
|
209
|
+
plain_text = element.get_text(strip=True)
|
|
210
|
+
level = int(element.name[1:])
|
|
211
|
+
base_level = context.runtime.get("base_level", 0)
|
|
212
|
+
rendered_level = level + base_level - 1
|
|
213
|
+
ref = coerce_attribute(element.get("id"))
|
|
214
|
+
if not ref:
|
|
215
|
+
slug = slugify(plain_text, separator="-")
|
|
216
|
+
ref = slug or None
|
|
217
|
+
numbered = context.runtime.get("numbered", True)
|
|
218
|
+
|
|
219
|
+
latex = context.formatter.heading(
|
|
220
|
+
text=text,
|
|
221
|
+
level=rendered_level,
|
|
222
|
+
ref=ref,
|
|
223
|
+
numbered=numbered,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
element.replace_with(mark_processed(NavigableString(latex)))
|
|
227
|
+
|
|
228
|
+
context.state.add_heading(level=rendered_level, text=plain_text, ref=ref)
|