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,125 @@
|
|
|
1
|
+
"""Extension to capture undefined Markdown footnotes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from types import MethodType
|
|
6
|
+
from typing import Any
|
|
7
|
+
import xml.etree.ElementTree as ElementTree
|
|
8
|
+
|
|
9
|
+
from markdown.extensions import Extension
|
|
10
|
+
from markdown.extensions.footnotes import FootnoteExtension
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MissingFootnotesExtension(Extension):
|
|
14
|
+
"""Detect footnote references lacking explicit definitions."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
17
|
+
self.config = {
|
|
18
|
+
"element": ["texsmith-missing-footnote", "Tag inserted for missing notes."],
|
|
19
|
+
"text_template": [
|
|
20
|
+
"{id}",
|
|
21
|
+
"Fallback text rendered for missing notes (can reference {id}).",
|
|
22
|
+
],
|
|
23
|
+
"css_class": ["", "CSS class applied to placeholder nodes."],
|
|
24
|
+
"link_to_list": [
|
|
25
|
+
False,
|
|
26
|
+
"When true, link to the generated footnote list despite the absence.",
|
|
27
|
+
],
|
|
28
|
+
"data_attribute": [
|
|
29
|
+
"data-footnote-id",
|
|
30
|
+
"Custom attribute storing the missing footnote identifier.",
|
|
31
|
+
],
|
|
32
|
+
}
|
|
33
|
+
super().__init__(**kwargs)
|
|
34
|
+
self._footnotes_ext: FootnoteExtension | None = None
|
|
35
|
+
self._patched_pattern = False
|
|
36
|
+
self.missing_ids: set[str] = set()
|
|
37
|
+
|
|
38
|
+
def reset(self) -> None:
|
|
39
|
+
"""Reset cached state before each Markdown conversion."""
|
|
40
|
+
self.missing_ids.clear()
|
|
41
|
+
|
|
42
|
+
# -- internals ---------------------------------------------------------
|
|
43
|
+
def _get_footnotes_extension(self, md: Any) -> FootnoteExtension | None:
|
|
44
|
+
if self._footnotes_ext is not None:
|
|
45
|
+
return self._footnotes_ext
|
|
46
|
+
|
|
47
|
+
for extension in getattr(md, "registeredExtensions", []):
|
|
48
|
+
if isinstance(extension, FootnoteExtension):
|
|
49
|
+
self._footnotes_ext = extension
|
|
50
|
+
break
|
|
51
|
+
return self._footnotes_ext
|
|
52
|
+
|
|
53
|
+
def extendMarkdown(self, md: Any) -> None: # noqa: N802 - markdown hook
|
|
54
|
+
"""Patch the inline footnote processor to capture missing notes."""
|
|
55
|
+
md.registerExtension(self)
|
|
56
|
+
if self._patched_pattern:
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
pattern = self._resolve_footnote_pattern(md)
|
|
60
|
+
if pattern is None:
|
|
61
|
+
raise RuntimeError(
|
|
62
|
+
"MissingFootnotesExtension requires the 'footnotes' extension to be "
|
|
63
|
+
"registered beforehand."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
original_handle = pattern.handleMatch
|
|
67
|
+
extension = self
|
|
68
|
+
|
|
69
|
+
def patched_handle(self_pattern: Any, match: Any, data: Any) -> Any:
|
|
70
|
+
result = original_handle(match, data)
|
|
71
|
+
if result and result[0] is not None:
|
|
72
|
+
return result
|
|
73
|
+
|
|
74
|
+
footnote_id = match.group(1)
|
|
75
|
+
extension.missing_ids.add(footnote_id)
|
|
76
|
+
node = extension.build_placeholder(footnote_id, self_pattern)
|
|
77
|
+
return node, match.start(0), match.end(0)
|
|
78
|
+
|
|
79
|
+
pattern.handleMatch = MethodType(patched_handle, pattern)
|
|
80
|
+
self._patched_pattern = True
|
|
81
|
+
|
|
82
|
+
def _resolve_footnote_pattern(self, md: Any) -> Any:
|
|
83
|
+
patterns = getattr(md.inlinePatterns, "items", None)
|
|
84
|
+
if callable(patterns):
|
|
85
|
+
for name, pattern in md.inlinePatterns.items():
|
|
86
|
+
if name == "footnote":
|
|
87
|
+
return pattern
|
|
88
|
+
else:
|
|
89
|
+
try:
|
|
90
|
+
return md.inlinePatterns["footnote"]
|
|
91
|
+
except KeyError:
|
|
92
|
+
return None
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
def build_placeholder(self, identifier: str, pattern: Any) -> ElementTree.Element:
|
|
96
|
+
"""Construct the XML placeholder inserted for missing footnotes."""
|
|
97
|
+
element_name = self.getConfig("element")
|
|
98
|
+
node = ElementTree.Element(element_name)
|
|
99
|
+
|
|
100
|
+
css_class = self.getConfig("css_class")
|
|
101
|
+
if css_class:
|
|
102
|
+
node.set("class", css_class)
|
|
103
|
+
|
|
104
|
+
data_attribute = self.getConfig("data_attribute")
|
|
105
|
+
if data_attribute:
|
|
106
|
+
node.set(data_attribute, identifier)
|
|
107
|
+
|
|
108
|
+
text = self.getConfig("text_template").format(id=identifier)
|
|
109
|
+
if self.getConfig("link_to_list"):
|
|
110
|
+
footnote_extension = self._get_footnotes_extension(pattern.md)
|
|
111
|
+
separator = footnote_extension.get_separator() if footnote_extension else ":"
|
|
112
|
+
anchor = ElementTree.SubElement(node, "a")
|
|
113
|
+
if css_class:
|
|
114
|
+
anchor.set("class", css_class)
|
|
115
|
+
anchor.set("href", f"#fn{separator}{identifier}")
|
|
116
|
+
anchor.text = text
|
|
117
|
+
else:
|
|
118
|
+
node.text = text
|
|
119
|
+
|
|
120
|
+
return node
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def makeExtension(**kwargs: Any) -> MissingFootnotesExtension: # noqa: N802 - markdown hook
|
|
124
|
+
"""Entry point exposed to Python-Markdown."""
|
|
125
|
+
return MissingFootnotesExtension(**kwargs)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Markdown preprocessor normalising inline citation references."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from markdown import Markdown
|
|
8
|
+
from markdown.extensions import Extension
|
|
9
|
+
from markdown.preprocessors import Preprocessor
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_DOI_PATTERN = r"10\.\d{4,9}/[^\s\]]+"
|
|
13
|
+
_TOKEN_RE = re.compile(rf"^(?:{_DOI_PATTERN}|[0-9A-Za-z_:-]+)$")
|
|
14
|
+
_PATTERN = re.compile(r"\^\[(?P<keys>[^\]]+)\]")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _clean_keys(payload: str) -> list[str]:
|
|
18
|
+
candidates = [part.strip() for part in payload.split(",")]
|
|
19
|
+
cleaned = [key for key in candidates if key and _TOKEN_RE.match(key)]
|
|
20
|
+
return cleaned
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _MultiCitationPreprocessor(Preprocessor):
|
|
24
|
+
"""Convert ``^[A,B]`` references into ``[^A,B]`` before footnote processing."""
|
|
25
|
+
|
|
26
|
+
def run(self, lines: list[str]) -> list[str]:
|
|
27
|
+
return [_PATTERN.sub(self._replace_match, line) for line in lines]
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def _replace_match(match: re.Match[str]) -> str:
|
|
31
|
+
cleaned = _clean_keys(match.group("keys"))
|
|
32
|
+
if not cleaned:
|
|
33
|
+
return match.group(0)
|
|
34
|
+
return f"[^{','.join(cleaned)}]"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MultiCitationExtension(Extension):
|
|
38
|
+
"""Register the multi-citation preprocessor before the footnotes extension."""
|
|
39
|
+
|
|
40
|
+
def extendMarkdown(self, md: Markdown) -> None: # type: ignore[override] # noqa: N802
|
|
41
|
+
md.preprocessors.register(
|
|
42
|
+
_MultiCitationPreprocessor(),
|
|
43
|
+
"texsmith_multi_citations",
|
|
44
|
+
priority=5,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def makeExtension( # noqa: N802
|
|
49
|
+
**kwargs: object,
|
|
50
|
+
) -> MultiCitationExtension: # pragma: no cover - API hook
|
|
51
|
+
return MultiCitationExtension(**kwargs)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
__all__ = ["MultiCitationExtension", "makeExtension"]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Public entry points for the Markdown progress bar extension."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .markdown import ProgressBarExtension, makeExtension
|
|
6
|
+
from .renderer import register as register_renderer
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
__all__ = ["ProgressBarExtension", "makeExtension", "register_renderer"]
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Markdown extension transforming `[=80% "Label"]` blocks into progress bars."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import math
|
|
7
|
+
import re
|
|
8
|
+
import shlex
|
|
9
|
+
from xml.etree import ElementTree
|
|
10
|
+
|
|
11
|
+
from markdown import Markdown
|
|
12
|
+
from markdown.extensions import Extension
|
|
13
|
+
from markdown.preprocessors import Preprocessor
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
PROGRESS_LINE = re.compile(
|
|
17
|
+
r"""
|
|
18
|
+
^\s*
|
|
19
|
+
\[=
|
|
20
|
+
(?P<value>-?\d+(?:\.\d+)?)
|
|
21
|
+
\s*%
|
|
22
|
+
\s*
|
|
23
|
+
(?:
|
|
24
|
+
"(?P<label>[^"]*)"
|
|
25
|
+
)?
|
|
26
|
+
\]
|
|
27
|
+
(?P<attrs>\{:[^}]+\})?
|
|
28
|
+
\s*$
|
|
29
|
+
""",
|
|
30
|
+
re.VERBOSE,
|
|
31
|
+
)
|
|
32
|
+
ATTR_LINE = re.compile(r"^\s*\{:[^}]+\}\s*$")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(slots=True)
|
|
36
|
+
class _AttributePayload:
|
|
37
|
+
classes: list[str]
|
|
38
|
+
identifier: str | None
|
|
39
|
+
attributes: dict[str, str]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class _ProgressBarPreprocessor(Preprocessor):
|
|
43
|
+
"""Convert the Pymdown-like shorthand into semantic HTML."""
|
|
44
|
+
|
|
45
|
+
def run(self, lines: list[str]) -> list[str]: # type: ignore[override]
|
|
46
|
+
result: list[str] = []
|
|
47
|
+
index = 0
|
|
48
|
+
length = len(lines)
|
|
49
|
+
in_fence = False
|
|
50
|
+
fence_char: str | None = None
|
|
51
|
+
fence_len = 0
|
|
52
|
+
|
|
53
|
+
while index < length:
|
|
54
|
+
line = lines[index]
|
|
55
|
+
stripped = line.lstrip()
|
|
56
|
+
|
|
57
|
+
if not in_fence and self._looks_like_fence_start(stripped):
|
|
58
|
+
fence_char = stripped[0]
|
|
59
|
+
fence_len = self._fence_length(stripped)
|
|
60
|
+
if fence_len >= 3:
|
|
61
|
+
in_fence = True
|
|
62
|
+
result.append(line)
|
|
63
|
+
index += 1
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
if in_fence:
|
|
67
|
+
result.append(line)
|
|
68
|
+
if self._is_fence_end(stripped, fence_char, fence_len):
|
|
69
|
+
in_fence = False
|
|
70
|
+
index += 1
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
match = PROGRESS_LINE.match(line)
|
|
74
|
+
attrs_text = None
|
|
75
|
+
|
|
76
|
+
if match:
|
|
77
|
+
attrs_text = match.group("attrs")
|
|
78
|
+
if attrs_text is None and index + 1 < length:
|
|
79
|
+
next_candidate = lines[index + 1]
|
|
80
|
+
if ATTR_LINE.match(next_candidate):
|
|
81
|
+
attrs_text = next_candidate.strip()
|
|
82
|
+
index += 1
|
|
83
|
+
|
|
84
|
+
payload = self._render_progressbar(match, attrs_text)
|
|
85
|
+
result.append(payload)
|
|
86
|
+
index += 1
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
result.append(line)
|
|
90
|
+
index += 1
|
|
91
|
+
|
|
92
|
+
return result
|
|
93
|
+
|
|
94
|
+
def _looks_like_fence_start(self, stripped: str) -> bool:
|
|
95
|
+
if not stripped:
|
|
96
|
+
return False
|
|
97
|
+
char = stripped[0]
|
|
98
|
+
if char not in {"`", "~"}:
|
|
99
|
+
return False
|
|
100
|
+
return stripped.startswith(char * 3)
|
|
101
|
+
|
|
102
|
+
def _is_fence_end(
|
|
103
|
+
self,
|
|
104
|
+
stripped: str,
|
|
105
|
+
fence_char: str | None,
|
|
106
|
+
fence_len: int,
|
|
107
|
+
) -> bool:
|
|
108
|
+
if not fence_char or fence_len < 3:
|
|
109
|
+
return False
|
|
110
|
+
return stripped.startswith(fence_char * fence_len)
|
|
111
|
+
|
|
112
|
+
def _fence_length(self, stripped: str) -> int:
|
|
113
|
+
if not stripped:
|
|
114
|
+
return 0
|
|
115
|
+
char = stripped[0]
|
|
116
|
+
count = 0
|
|
117
|
+
for ch in stripped:
|
|
118
|
+
if ch == char:
|
|
119
|
+
count += 1
|
|
120
|
+
else:
|
|
121
|
+
break
|
|
122
|
+
return count
|
|
123
|
+
|
|
124
|
+
def _render_progressbar(
|
|
125
|
+
self,
|
|
126
|
+
match: re.Match[str],
|
|
127
|
+
attrs_text: str | None,
|
|
128
|
+
) -> str:
|
|
129
|
+
percent = float(match.group("value"))
|
|
130
|
+
percent = max(0.0, min(100.0, percent))
|
|
131
|
+
label = match.group("label")
|
|
132
|
+
if label is None or not label.strip():
|
|
133
|
+
label = f"{percent:g}%"
|
|
134
|
+
attrs = self._parse_attributes(attrs_text)
|
|
135
|
+
|
|
136
|
+
classes = ["progress", _progress_bucket_class(percent), *attrs.classes]
|
|
137
|
+
fraction = percent / 100.0
|
|
138
|
+
fraction_str = _format_decimal(fraction, precision=4)
|
|
139
|
+
|
|
140
|
+
container = ElementTree.Element("div")
|
|
141
|
+
container.set("class", " ".join(filter(None, classes)))
|
|
142
|
+
container.set("data-progress-percent", _format_decimal(percent, precision=2))
|
|
143
|
+
container.set("data-progress-fraction", fraction_str)
|
|
144
|
+
if attrs.identifier:
|
|
145
|
+
container.set("id", attrs.identifier)
|
|
146
|
+
for key, value in attrs.attributes.items():
|
|
147
|
+
container.set(key, value)
|
|
148
|
+
|
|
149
|
+
bar = ElementTree.SubElement(container, "div", {"class": "progress-bar"})
|
|
150
|
+
bar.set("style", f"width:{_format_decimal(percent, precision=2)}%;")
|
|
151
|
+
bar.set("data-progress-fraction", fraction_str)
|
|
152
|
+
|
|
153
|
+
label_node = ElementTree.SubElement(bar, "p", {"class": "progress-label"})
|
|
154
|
+
label_node.text = label
|
|
155
|
+
|
|
156
|
+
return ElementTree.tostring(container, encoding="unicode")
|
|
157
|
+
|
|
158
|
+
def _parse_attributes(self, raw: str | None) -> _AttributePayload:
|
|
159
|
+
if not raw:
|
|
160
|
+
return _AttributePayload([], None, {})
|
|
161
|
+
body = raw.strip()
|
|
162
|
+
if body.startswith("{"):
|
|
163
|
+
body = body[2:-1].strip()
|
|
164
|
+
|
|
165
|
+
classes: list[str] = []
|
|
166
|
+
attributes: dict[str, str] = {}
|
|
167
|
+
identifier: str | None = None
|
|
168
|
+
|
|
169
|
+
lexer = shlex.shlex(body, posix=True)
|
|
170
|
+
lexer.whitespace_split = True
|
|
171
|
+
lexer.commenters = ""
|
|
172
|
+
|
|
173
|
+
for token in lexer:
|
|
174
|
+
if not token:
|
|
175
|
+
continue
|
|
176
|
+
if token.startswith("."):
|
|
177
|
+
classes.append(token[1:])
|
|
178
|
+
elif token.startswith("#"):
|
|
179
|
+
identifier = token[1:] or identifier
|
|
180
|
+
elif "=" in token:
|
|
181
|
+
key, value = token.split("=", 1)
|
|
182
|
+
attributes[key] = value
|
|
183
|
+
|
|
184
|
+
return _AttributePayload(classes, identifier, attributes)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _progress_bucket_class(percent: float) -> str:
|
|
188
|
+
bucket = int(math.floor(percent / 5.0) * 5)
|
|
189
|
+
bucket = max(0, min(100, bucket))
|
|
190
|
+
return f"progress-{bucket}plus"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _format_decimal(value: float, *, precision: int) -> str:
|
|
194
|
+
formatted = f"{value:.{precision}f}"
|
|
195
|
+
return formatted.rstrip("0").rstrip(".") or "0"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class ProgressBarExtension(Extension):
|
|
199
|
+
"""Register the preprocessor on the Markdown pipeline."""
|
|
200
|
+
|
|
201
|
+
def extendMarkdown(self, md: Markdown) -> None: # noqa: N802
|
|
202
|
+
processor = _ProgressBarPreprocessor(md)
|
|
203
|
+
md.preprocessors.register(processor, "texsmith_progressbar", 27)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def makeExtension( # noqa: N802
|
|
207
|
+
**kwargs: object,
|
|
208
|
+
) -> ProgressBarExtension: # pragma: no cover - Markdown hook
|
|
209
|
+
return ProgressBarExtension(**kwargs)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
__all__ = ["ProgressBarExtension", "makeExtension"]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Renderer hooks converting HTML progress bars into LaTeX commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from bs4 import NavigableString, Tag
|
|
8
|
+
|
|
9
|
+
from texsmith.adapters.handlers._helpers import coerce_attribute, gather_classes, mark_processed
|
|
10
|
+
from texsmith.adapters.latex.utils import escape_latex_chars
|
|
11
|
+
from texsmith.core.context import RenderContext
|
|
12
|
+
from texsmith.core.rules import RenderPhase, renders
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
DEFAULT_HEIGHT = "12pt"
|
|
16
|
+
THIN_HEIGHT = "6pt"
|
|
17
|
+
DEFAULT_OPTIONS = {
|
|
18
|
+
"width": "9cm",
|
|
19
|
+
"heighta": DEFAULT_HEIGHT,
|
|
20
|
+
"roundnessr": "0.1",
|
|
21
|
+
"borderwidth": "1pt",
|
|
22
|
+
"linecolor": "black",
|
|
23
|
+
"filledcolor": "black!60",
|
|
24
|
+
"emptycolor": "black!10",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@renders(
|
|
29
|
+
"div",
|
|
30
|
+
phase=RenderPhase.BLOCK,
|
|
31
|
+
priority=58,
|
|
32
|
+
name="texsmith_progressbar",
|
|
33
|
+
nestable=False,
|
|
34
|
+
auto_mark=False,
|
|
35
|
+
)
|
|
36
|
+
def render_progressbar(element: Tag, context: RenderContext) -> None:
|
|
37
|
+
r"""Convert ``<div class="progress">`` nodes into ``\progressbar`` calls."""
|
|
38
|
+
classes = gather_classes(element.get("class"))
|
|
39
|
+
if "progress" not in classes:
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
bar = element.find("div", class_="progress-bar")
|
|
43
|
+
if bar is None:
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
percent = _extract_percent(element, bar)
|
|
47
|
+
fraction = max(0.0, min(1.0, percent / 100.0))
|
|
48
|
+
|
|
49
|
+
label_node = bar.find("p", class_="progress-label")
|
|
50
|
+
label_text = label_node.get_text(strip=True) if label_node else f"{percent:g}%"
|
|
51
|
+
legacy_accents = getattr(context.config, "legacy_latex_accents", False)
|
|
52
|
+
escaped_label = escape_latex_chars(label_text, legacy_accents=legacy_accents)
|
|
53
|
+
|
|
54
|
+
thin = "thin" in classes or "progress-thin" in classes
|
|
55
|
+
latex = _compose_latex(fraction, escaped_label, thin=thin)
|
|
56
|
+
|
|
57
|
+
node = mark_processed(NavigableString(latex))
|
|
58
|
+
context.mark_processed(element)
|
|
59
|
+
context.suppress_children(element)
|
|
60
|
+
element.replace_with(node)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _extract_percent(element: Tag, bar: Tag) -> float:
|
|
64
|
+
for attr in ("data-progress-percent", "data-progress", "data-progress-value"):
|
|
65
|
+
value = coerce_attribute(element.get(attr)) or coerce_attribute(bar.get(attr))
|
|
66
|
+
if value:
|
|
67
|
+
try:
|
|
68
|
+
return float(value)
|
|
69
|
+
except ValueError:
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
style = coerce_attribute(bar.get("style")) or ""
|
|
73
|
+
match = re.search(r"width:\s*([0-9.]+)", style)
|
|
74
|
+
if match:
|
|
75
|
+
try:
|
|
76
|
+
return float(match.group(1))
|
|
77
|
+
except ValueError:
|
|
78
|
+
return 0.0
|
|
79
|
+
|
|
80
|
+
fraction_attr = coerce_attribute(bar.get("data-progress-fraction"))
|
|
81
|
+
if fraction_attr:
|
|
82
|
+
try:
|
|
83
|
+
return float(fraction_attr) * 100.0
|
|
84
|
+
except ValueError:
|
|
85
|
+
return 0.0
|
|
86
|
+
|
|
87
|
+
return 0.0
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _compose_latex(fraction: float, label: str, *, thin: bool) -> str:
|
|
91
|
+
value = max(0.0, min(1.0, fraction))
|
|
92
|
+
height = THIN_HEIGHT if thin else DEFAULT_HEIGHT
|
|
93
|
+
options = dict(DEFAULT_OPTIONS)
|
|
94
|
+
options["heighta"] = height
|
|
95
|
+
options_payload = ",".join(f"{key}={value}" for key, value in options.items())
|
|
96
|
+
value_payload = _format_fraction(value)
|
|
97
|
+
return f"{{\\progressbar[{options_payload}]{{{value_payload}}} {label}}}\\par\n"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _format_fraction(value: float) -> str:
|
|
101
|
+
formatted = f"{value:.4f}"
|
|
102
|
+
trimmed = formatted.rstrip("0").rstrip(".")
|
|
103
|
+
return trimmed or "0"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def register(renderer: object) -> None:
|
|
107
|
+
"""Register the progress bar handler with the renderer."""
|
|
108
|
+
register_callable = getattr(renderer, "register", None)
|
|
109
|
+
if not callable(register_callable):
|
|
110
|
+
raise TypeError("Renderer does not expose a 'register' method.")
|
|
111
|
+
if getattr(renderer, "_texsmith_progressbar_registered", False):
|
|
112
|
+
return
|
|
113
|
+
register_callable(render_progressbar)
|
|
114
|
+
renderer._texsmith_progressbar_registered = True # noqa: SLF001
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
__all__ = ["register", "render_progressbar"]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Markdown extension that maps double underscores to small caps spans."""
|
|
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.inlinepatterns import InlineProcessor
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_SMALL_CAPS_PATTERN = r"(?<!_)__(?!_)(.+?)__(?!_)"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _SmallCapsInlineProcessor(InlineProcessor):
|
|
16
|
+
"""Inline processor converting ``__text__`` into a span marker."""
|
|
17
|
+
|
|
18
|
+
def handleMatch( # type: ignore[override] # noqa: N802 - Markdown API requires camelCase
|
|
19
|
+
self,
|
|
20
|
+
match, # type: ignore[override] # noqa: ANN001
|
|
21
|
+
data: str,
|
|
22
|
+
) -> tuple[ElementTree.Element | None, int | None, int | None]:
|
|
23
|
+
content = match.group(1)
|
|
24
|
+
if not content:
|
|
25
|
+
return None, None, None
|
|
26
|
+
|
|
27
|
+
element = ElementTree.Element("span", {"class": "texsmith-smallcaps"})
|
|
28
|
+
parser = getattr(self, "parser", None)
|
|
29
|
+
if parser is not None:
|
|
30
|
+
parser.parseInline(element, content)
|
|
31
|
+
else: # pragma: no cover - parser is provided by Markdown
|
|
32
|
+
element.text = content
|
|
33
|
+
return element, match.start(0), match.end(0)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SmallCapsExtension(Extension):
|
|
37
|
+
"""Register the ``__text__`` → ``<span class=\"texsmith-smallcaps\">`` processor."""
|
|
38
|
+
|
|
39
|
+
def extendMarkdown(self, md: Markdown) -> None: # type: ignore[override] # noqa: N802
|
|
40
|
+
processor = _SmallCapsInlineProcessor(_SMALL_CAPS_PATTERN, md)
|
|
41
|
+
md.inlinePatterns.register(processor, "texsmith_smallcaps", 185)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def makeExtension(**_: object) -> SmallCapsExtension: # pragma: no cover - API hook # noqa: N802
|
|
45
|
+
return SmallCapsExtension()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
__all__ = ["SmallCapsExtension", "makeExtension"]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""TeXSmith extension providing extra TeX logo commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .markdown import TexLogosExtension, makeExtension
|
|
6
|
+
from .renderer import register as register_renderer
|
|
7
|
+
from .specs import LogoSpec, iter_specs
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
__all__ = ["LogoSpec", "TexLogosExtension", "iter_specs", "makeExtension", "register_renderer"]
|