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
texsmith/core/rules.py
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"""Rule declaration and execution engine for the LaTeX renderer.
|
|
2
|
+
|
|
3
|
+
This module implements the rule-based architecture that powers Texsmith's
|
|
4
|
+
HTML-to-LaTeX pipeline. Handlers declare their intent via the ``@renders``
|
|
5
|
+
decorator, which records structural metadata (phase, priority, targeted tags).
|
|
6
|
+
At runtime the :class:`RenderEngine` collects those declarations, organises them
|
|
7
|
+
per :class:`RenderPhase`, and walks the BeautifulSoup DOM ensuring that each
|
|
8
|
+
pass is executed in a predictable, stable order.
|
|
9
|
+
|
|
10
|
+
Architecture
|
|
11
|
+
|
|
12
|
+
`Declaration layer`
|
|
13
|
+
: ``@renders`` stores a lightweight :class:`RuleDefinition` on every handler.
|
|
14
|
+
|
|
15
|
+
`Registry layer`
|
|
16
|
+
: :class:`RenderRegistry` collates definitions into sortable :class:`RenderRule`
|
|
17
|
+
instances grouped by phase/tag.
|
|
18
|
+
|
|
19
|
+
`Execution layer`
|
|
20
|
+
: :class:`RenderEngine` coordinates multi-pass traversal using the private
|
|
21
|
+
:class:`_DOMVisitor` to apply handlers depth-first while respecting
|
|
22
|
+
auto-marking and child-suppression semantics.
|
|
23
|
+
|
|
24
|
+
This separation keeps rule authors focused on transformations while the engine
|
|
25
|
+
handles ordering, deduplication, and orchestration concerns.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from collections import deque
|
|
31
|
+
from collections.abc import Callable, Iterable
|
|
32
|
+
from dataclasses import dataclass
|
|
33
|
+
from enum import Enum, auto
|
|
34
|
+
from typing import TYPE_CHECKING, Any, Protocol, cast
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
38
|
+
from bs4.element import Tag
|
|
39
|
+
|
|
40
|
+
from .context import RenderContext
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class RenderPhase(Enum):
|
|
44
|
+
"""Ordered passes executed while mutating the parsed HTML tree.
|
|
45
|
+
|
|
46
|
+
The renderer performs multiple sweeps over the DOM instead of a single
|
|
47
|
+
monolithic traversal. Each phase isolates a category of mutations so that
|
|
48
|
+
earlier transformations stabilise before later ones begin. This drastically
|
|
49
|
+
reduces coupling between handlers and makes ordering guarantees explicit.
|
|
50
|
+
|
|
51
|
+
Phases progress from coarse structural edits to fine-grained formatting:
|
|
52
|
+
|
|
53
|
+
``PRE``
|
|
54
|
+
: normalise the tree and discard unwanted nodes before any heavy lifting
|
|
55
|
+
occurs.
|
|
56
|
+
|
|
57
|
+
``BLOCK``
|
|
58
|
+
: build block-level LaTeX (paragraphs, lists, figures) once the structure is
|
|
59
|
+
stable.
|
|
60
|
+
|
|
61
|
+
``INLINE``
|
|
62
|
+
: apply inline formatting after blocks have established their final shape.
|
|
63
|
+
|
|
64
|
+
``POST``
|
|
65
|
+
: run cleanup or bookkeeping steps that depend on previous phases, such as
|
|
66
|
+
final numbering, synthetic nodes, or state aggregation.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
PRE = auto()
|
|
70
|
+
"""DOM normalisation pass: strip/reshape nodes before structural work begins."""
|
|
71
|
+
|
|
72
|
+
BLOCK = auto()
|
|
73
|
+
"""Block transformation pass: convert paragraphs, lists, figures, etc."""
|
|
74
|
+
|
|
75
|
+
INLINE = auto()
|
|
76
|
+
"""Inline formatting pass: apply emphasis, links, inline math once blocks exist."""
|
|
77
|
+
|
|
78
|
+
POST = auto()
|
|
79
|
+
"""Finalisation pass: run cleanup that depends on earlier transformations."""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
RuleCallable = Callable[[Any, "RenderContext"], None]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class RuleFactory(Protocol):
|
|
86
|
+
"""Protocol implemented by rule decorators.
|
|
87
|
+
|
|
88
|
+
Decorators return lightweight factory objects instead of immediately
|
|
89
|
+
constructing :class:`RenderRule` instances. This indirection lets us bind
|
|
90
|
+
metadata once (at decoration time) while deferring handler resolution until
|
|
91
|
+
the registry collects rules. The factory pattern keeps the decorator API
|
|
92
|
+
ergonomic, avoids premature instantiation, and allows the same definition
|
|
93
|
+
to be rebound for different callables (e.g. class/static methods) without
|
|
94
|
+
duplicating registration logic.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def bind(self, handler: RuleCallable) -> RenderRule:
|
|
98
|
+
"""Create a concrete render rule for the decorated handler."""
|
|
99
|
+
...
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
DOCUMENT_NODE = "__document__"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class RenderRule:
|
|
107
|
+
"""Concrete rendering rule registered in the engine."""
|
|
108
|
+
|
|
109
|
+
priority: int
|
|
110
|
+
phase: RenderPhase
|
|
111
|
+
tags: tuple[str, ...]
|
|
112
|
+
name: str
|
|
113
|
+
handler: RuleCallable
|
|
114
|
+
auto_mark: bool = True
|
|
115
|
+
nestable: bool = True
|
|
116
|
+
after_children: bool = False
|
|
117
|
+
before: tuple[str, ...] = ()
|
|
118
|
+
after: tuple[str, ...] = ()
|
|
119
|
+
|
|
120
|
+
def applies_to_document(self) -> bool:
|
|
121
|
+
"""Return True when the rule targets the synthetic document node."""
|
|
122
|
+
return self.tags == (DOCUMENT_NODE,)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass(frozen=True)
|
|
126
|
+
class RuleDefinition:
|
|
127
|
+
"""Descriptor installed on handler callables by the decorator."""
|
|
128
|
+
|
|
129
|
+
phase: RenderPhase
|
|
130
|
+
tags: tuple[str, ...]
|
|
131
|
+
priority: int = 0
|
|
132
|
+
name: str | None = None
|
|
133
|
+
auto_mark: bool = True
|
|
134
|
+
nestable: bool = True
|
|
135
|
+
after_children: bool = False
|
|
136
|
+
before: tuple[str, ...] = ()
|
|
137
|
+
after: tuple[str, ...] = ()
|
|
138
|
+
|
|
139
|
+
def bind(self, handler: RuleCallable) -> RenderRule:
|
|
140
|
+
"""Create a concrete rule instance bound to the callable."""
|
|
141
|
+
name = self.name or getattr(handler, "__name__", handler.__class__.__name__)
|
|
142
|
+
return RenderRule(
|
|
143
|
+
phase=self.phase,
|
|
144
|
+
tags=self.tags,
|
|
145
|
+
priority=self.priority,
|
|
146
|
+
name=name,
|
|
147
|
+
handler=handler,
|
|
148
|
+
auto_mark=self.auto_mark,
|
|
149
|
+
nestable=self.nestable,
|
|
150
|
+
after_children=self.after_children,
|
|
151
|
+
before=self.before,
|
|
152
|
+
after=self.after,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class RenderRegistry:
|
|
157
|
+
"""Container used to gather render rules before execution."""
|
|
158
|
+
|
|
159
|
+
def __init__(self) -> None:
|
|
160
|
+
self._rules: dict[RenderPhase, dict[str, list[RenderRule]]] = {}
|
|
161
|
+
self._rule_sources: dict[int, str] = {}
|
|
162
|
+
|
|
163
|
+
def register(self, rule: RenderRule) -> None:
|
|
164
|
+
"""Register a rule for later execution."""
|
|
165
|
+
phase_bucket = self._rules.setdefault(rule.phase, {})
|
|
166
|
+
self._rule_sources.setdefault(id(rule), rule.name)
|
|
167
|
+
if rule.applies_to_document():
|
|
168
|
+
tag_bucket = phase_bucket.setdefault(DOCUMENT_NODE, [])
|
|
169
|
+
tag_bucket.append(rule)
|
|
170
|
+
tag_bucket[:] = self._sort_rules(tag_bucket)
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
for tag in rule.tags:
|
|
174
|
+
tag_bucket = phase_bucket.setdefault(tag, [])
|
|
175
|
+
tag_bucket.append(rule)
|
|
176
|
+
tag_bucket[:] = self._sort_rules(tag_bucket)
|
|
177
|
+
|
|
178
|
+
def iter_phase(self, phase: RenderPhase) -> Iterable[RenderRule]:
|
|
179
|
+
"""Iterate over rules for the provided phase."""
|
|
180
|
+
buckets = self._rules.get(phase, {})
|
|
181
|
+
for tag_rules in buckets.values():
|
|
182
|
+
yield from tag_rules
|
|
183
|
+
|
|
184
|
+
def rules_for_phase(self, phase: RenderPhase) -> dict[str, tuple[RenderRule, ...]]:
|
|
185
|
+
"""Return the rule mapping for the requested phase."""
|
|
186
|
+
phase_bucket = self._rules.get(phase, {})
|
|
187
|
+
return {tag: tuple(rules) for tag, rules in phase_bucket.items()}
|
|
188
|
+
|
|
189
|
+
def describe(self) -> list[dict[str, object]]:
|
|
190
|
+
"""Return a serialisable snapshot of the registered rules."""
|
|
191
|
+
entries: list[dict[str, object]] = []
|
|
192
|
+
for phase in RenderPhase:
|
|
193
|
+
for tag, rules in sorted(self.rules_for_phase(phase).items(), key=lambda item: item[0]):
|
|
194
|
+
for order, rule in enumerate(rules):
|
|
195
|
+
entries.append(
|
|
196
|
+
{
|
|
197
|
+
"phase": phase.name,
|
|
198
|
+
"tag": tag,
|
|
199
|
+
"name": rule.name,
|
|
200
|
+
"priority": rule.priority,
|
|
201
|
+
"before": list(rule.before),
|
|
202
|
+
"after": list(rule.after),
|
|
203
|
+
"order": order,
|
|
204
|
+
}
|
|
205
|
+
)
|
|
206
|
+
return entries
|
|
207
|
+
|
|
208
|
+
def _sort_rules(self, rules: list[RenderRule]) -> list[RenderRule]:
|
|
209
|
+
"""Return rules ordered deterministically using before/after constraints."""
|
|
210
|
+
if len(rules) <= 1:
|
|
211
|
+
return list(rules)
|
|
212
|
+
|
|
213
|
+
name_to_index: dict[str, int] = {}
|
|
214
|
+
for index, rule in enumerate(rules):
|
|
215
|
+
name_to_index.setdefault(rule.name, index)
|
|
216
|
+
|
|
217
|
+
adjacency: dict[int, set[int]] = {index: set() for index in range(len(rules))}
|
|
218
|
+
indegree: dict[int, int] = dict.fromkeys(range(len(rules)), 0)
|
|
219
|
+
|
|
220
|
+
def _add_edge(source: int, target: int) -> None:
|
|
221
|
+
if target in adjacency[source]:
|
|
222
|
+
return
|
|
223
|
+
adjacency[source].add(target)
|
|
224
|
+
indegree[target] += 1
|
|
225
|
+
|
|
226
|
+
for current_index, rule in enumerate(rules):
|
|
227
|
+
for target_name in rule.before:
|
|
228
|
+
target_index = name_to_index.get(target_name)
|
|
229
|
+
if target_index is not None:
|
|
230
|
+
_add_edge(current_index, target_index)
|
|
231
|
+
for target_name in rule.after:
|
|
232
|
+
target_index = name_to_index.get(target_name)
|
|
233
|
+
if target_index is not None:
|
|
234
|
+
_add_edge(target_index, current_index)
|
|
235
|
+
|
|
236
|
+
queue: deque[int] = deque(
|
|
237
|
+
sorted(
|
|
238
|
+
(index for index, count in indegree.items() if count == 0),
|
|
239
|
+
key=lambda idx: (rules[idx].priority, rules[idx].name, idx),
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
ordered: list[int] = []
|
|
243
|
+
|
|
244
|
+
while queue:
|
|
245
|
+
current = queue.popleft()
|
|
246
|
+
ordered.append(current)
|
|
247
|
+
for neighbour in sorted(
|
|
248
|
+
adjacency[current], key=lambda idx: (rules[idx].priority, rules[idx].name, idx)
|
|
249
|
+
):
|
|
250
|
+
indegree[neighbour] -= 1
|
|
251
|
+
if indegree[neighbour] == 0:
|
|
252
|
+
queue.append(neighbour)
|
|
253
|
+
|
|
254
|
+
queue = deque(
|
|
255
|
+
sorted(queue, key=lambda idx: (rules[idx].priority, rules[idx].name, idx))
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if len(ordered) != len(rules): # pragma: no cover - defensive
|
|
259
|
+
cycle_names = sorted(
|
|
260
|
+
rule.name for index, rule in enumerate(rules) if index not in ordered
|
|
261
|
+
)
|
|
262
|
+
raise RuntimeError(
|
|
263
|
+
"Cyclic render rule dependencies detected: " + ", ".join(cycle_names)
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
return [rules[index] for index in ordered]
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def renders(
|
|
270
|
+
*tags: str,
|
|
271
|
+
phase: RenderPhase = RenderPhase.BLOCK,
|
|
272
|
+
priority: int = 0,
|
|
273
|
+
name: str | None = None,
|
|
274
|
+
auto_mark: bool = True,
|
|
275
|
+
nestable: bool = True,
|
|
276
|
+
after_children: bool = False,
|
|
277
|
+
before: Iterable[str] = (),
|
|
278
|
+
after: Iterable[str] = (),
|
|
279
|
+
) -> Callable[[RuleCallable], RuleCallable]:
|
|
280
|
+
"""Decorator used to register element handlers."""
|
|
281
|
+
selected_tags = tags or (DOCUMENT_NODE,)
|
|
282
|
+
definition = RuleDefinition(
|
|
283
|
+
phase=phase,
|
|
284
|
+
tags=tuple(selected_tags),
|
|
285
|
+
priority=priority,
|
|
286
|
+
name=name,
|
|
287
|
+
auto_mark=auto_mark,
|
|
288
|
+
nestable=nestable,
|
|
289
|
+
after_children=after_children,
|
|
290
|
+
before=tuple(before),
|
|
291
|
+
after=tuple(after),
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def decorator(handler: RuleCallable) -> RuleCallable:
|
|
295
|
+
cast(Any, handler).__render_rule__ = definition
|
|
296
|
+
return handler
|
|
297
|
+
|
|
298
|
+
return decorator
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class RenderEngine:
|
|
302
|
+
"""Execution engine that orchestrates the registered rules."""
|
|
303
|
+
|
|
304
|
+
def __init__(self, registry: RenderRegistry | None = None) -> None:
|
|
305
|
+
self.registry = registry or RenderRegistry()
|
|
306
|
+
|
|
307
|
+
def collect_from(self, owner: Any) -> None:
|
|
308
|
+
"""Collect decorated callables from an object or module."""
|
|
309
|
+
for attribute in dir(owner):
|
|
310
|
+
handler = getattr(owner, attribute)
|
|
311
|
+
definition = getattr(handler, "__render_rule__", None)
|
|
312
|
+
if definition is None and hasattr(handler, "__func__"):
|
|
313
|
+
definition = getattr(handler.__func__, "__render_rule__", None)
|
|
314
|
+
if isinstance(definition, RuleDefinition):
|
|
315
|
+
self.registry.register(definition.bind(handler))
|
|
316
|
+
|
|
317
|
+
def register(self, handler: RuleCallable) -> None:
|
|
318
|
+
"""Register a standalone callable decorated with ``@renders``."""
|
|
319
|
+
definition = getattr(handler, "__render_rule__", None)
|
|
320
|
+
if not isinstance(definition, RuleDefinition):
|
|
321
|
+
msg = "Handler must be decorated with @renders"
|
|
322
|
+
raise TypeError(msg)
|
|
323
|
+
self.registry.register(definition.bind(handler))
|
|
324
|
+
|
|
325
|
+
def run(self, root: Tag, context: RenderContext) -> None:
|
|
326
|
+
"""Execute all registered rules against the provided DOM root."""
|
|
327
|
+
for phase in RenderPhase:
|
|
328
|
+
context.enter_phase(phase)
|
|
329
|
+
phase_rules = self.registry.rules_for_phase(phase)
|
|
330
|
+
document_rules = phase_rules.get(DOCUMENT_NODE, ())
|
|
331
|
+
|
|
332
|
+
for rule in document_rules:
|
|
333
|
+
self._execute_rule(rule, root, context)
|
|
334
|
+
|
|
335
|
+
visitor = _DOMVisitor(phase, phase_rules, context)
|
|
336
|
+
visitor.walk(root)
|
|
337
|
+
|
|
338
|
+
def _execute_rule(self, rule: RenderRule, node: Any, context: RenderContext) -> None:
|
|
339
|
+
"""Execute a rule against a specific node applying bookkeeping."""
|
|
340
|
+
if rule.auto_mark and context.is_processed(node):
|
|
341
|
+
return
|
|
342
|
+
rule.handler(node, context)
|
|
343
|
+
if rule.auto_mark:
|
|
344
|
+
context.mark_processed(node)
|
|
345
|
+
if not rule.nestable:
|
|
346
|
+
context.suppress_children(node)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class _DOMVisitor:
|
|
350
|
+
"""Depth-first visitor applying rules by tag while traversing the DOM tree."""
|
|
351
|
+
|
|
352
|
+
def __init__(
|
|
353
|
+
self,
|
|
354
|
+
phase: RenderPhase,
|
|
355
|
+
rules_by_tag: dict[str, tuple[RenderRule, ...]],
|
|
356
|
+
context: RenderContext,
|
|
357
|
+
) -> None:
|
|
358
|
+
self.phase = phase
|
|
359
|
+
self.rules_by_tag = rules_by_tag
|
|
360
|
+
self.context = context
|
|
361
|
+
|
|
362
|
+
def walk(self, node: Tag) -> None:
|
|
363
|
+
"""Traverse descendants depth-first and apply matching rules."""
|
|
364
|
+
self._dispatch(node, after_children=False)
|
|
365
|
+
if self.context.should_skip_children(node, phase=self.phase):
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
# Copy the children list to avoid concurrent modification issues
|
|
369
|
+
for child in list(getattr(node, "children", ())):
|
|
370
|
+
# Only Tags should be traversed recursively
|
|
371
|
+
if getattr(child, "name", None):
|
|
372
|
+
self.walk(child)
|
|
373
|
+
|
|
374
|
+
self._dispatch(node, after_children=True)
|
|
375
|
+
|
|
376
|
+
def _dispatch(self, node: Tag, *, after_children: bool) -> None:
|
|
377
|
+
"""Dispatch node to registered rules for its tag name."""
|
|
378
|
+
tag_name = getattr(node, "name", None)
|
|
379
|
+
if not tag_name:
|
|
380
|
+
return
|
|
381
|
+
|
|
382
|
+
for rule in self.rules_by_tag.get(tag_name, ()):
|
|
383
|
+
if rule.after_children != after_children:
|
|
384
|
+
continue
|
|
385
|
+
if rule.tags == (DOCUMENT_NODE,):
|
|
386
|
+
# Document handlers already executed
|
|
387
|
+
continue
|
|
388
|
+
if rule.auto_mark and self.context.is_processed(node):
|
|
389
|
+
continue
|
|
390
|
+
rule.handler(node, self.context)
|
|
391
|
+
if rule.auto_mark:
|
|
392
|
+
self.context.mark_processed(node)
|
|
393
|
+
if not rule.nestable:
|
|
394
|
+
self.context.suppress_children(node)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Public template helpers shared across the conversion pipeline."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .base import BaseTemplate, ResolvedAsset, WrappableTemplate
|
|
6
|
+
from .loader import copy_template_assets, discover_templates, load_template
|
|
7
|
+
from .manifest import (
|
|
8
|
+
DEFAULT_TEMPLATE_LANGUAGE,
|
|
9
|
+
LATEX_HEADING_LEVELS,
|
|
10
|
+
TemplateAttributeSpec,
|
|
11
|
+
TemplateAsset,
|
|
12
|
+
TemplateError,
|
|
13
|
+
TemplateInfo,
|
|
14
|
+
TemplateManifest,
|
|
15
|
+
TemplateSlot,
|
|
16
|
+
)
|
|
17
|
+
from .runtime import (
|
|
18
|
+
TemplateBinding,
|
|
19
|
+
TemplateRuntime,
|
|
20
|
+
build_template_overrides,
|
|
21
|
+
coerce_base_level,
|
|
22
|
+
extract_base_level_override,
|
|
23
|
+
extract_language_from_front_matter,
|
|
24
|
+
load_template_runtime,
|
|
25
|
+
normalise_template_language,
|
|
26
|
+
resolve_template_binding,
|
|
27
|
+
resolve_template_language,
|
|
28
|
+
)
|
|
29
|
+
from .wrapper import TemplateWrapResult, wrap_template_document
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"BaseTemplate",
|
|
33
|
+
"DEFAULT_TEMPLATE_LANGUAGE",
|
|
34
|
+
"LATEX_HEADING_LEVELS",
|
|
35
|
+
"ResolvedAsset",
|
|
36
|
+
"TemplateAttributeSpec",
|
|
37
|
+
"TemplateAsset",
|
|
38
|
+
"TemplateBinding",
|
|
39
|
+
"TemplateError",
|
|
40
|
+
"TemplateInfo",
|
|
41
|
+
"TemplateManifest",
|
|
42
|
+
"TemplateRuntime",
|
|
43
|
+
"TemplateSlot",
|
|
44
|
+
"TemplateWrapResult",
|
|
45
|
+
"WrappableTemplate",
|
|
46
|
+
"build_template_overrides",
|
|
47
|
+
"coerce_base_level",
|
|
48
|
+
"copy_template_assets",
|
|
49
|
+
"discover_templates",
|
|
50
|
+
"extract_base_level_override",
|
|
51
|
+
"extract_language_from_front_matter",
|
|
52
|
+
"load_template",
|
|
53
|
+
"load_template_runtime",
|
|
54
|
+
"normalise_template_language",
|
|
55
|
+
"wrap_template_document",
|
|
56
|
+
"resolve_template_binding",
|
|
57
|
+
"resolve_template_language",
|
|
58
|
+
]
|