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,526 @@
|
|
|
1
|
+
"""Geometry helpers scoped to the ts-geometry fragment."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
from pint import DimensionalityError, UndefinedUnitError, UnitRegistry
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator, model_validator
|
|
11
|
+
|
|
12
|
+
from texsmith.core.templates.manifest import TemplateError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
DEFAULT_MARGIN: str | None = None
|
|
16
|
+
_FORMAT_SUFFIX = "paper"
|
|
17
|
+
_KNOWN_FORMATS = {f"{family}{index}" for family in ("a", "b", "c") for index in range(0, 7)} | {
|
|
18
|
+
"letter",
|
|
19
|
+
"legal",
|
|
20
|
+
"executive",
|
|
21
|
+
}
|
|
22
|
+
_UNIT_REGISTRY = UnitRegistry()
|
|
23
|
+
_LENGTH_DIMENSION = _UNIT_REGISTRY.mm.dimensionality
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _format_magnitude(value: float) -> str:
|
|
27
|
+
if value.is_integer():
|
|
28
|
+
return str(int(value))
|
|
29
|
+
return f"{value:.6g}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _normalise_dimension(value: Any) -> str:
|
|
33
|
+
"""Return a LaTeX dimension string, converting bare numbers to millimetres."""
|
|
34
|
+
if isinstance(value, (int, float)):
|
|
35
|
+
return f"{value}mm"
|
|
36
|
+
if isinstance(value, str):
|
|
37
|
+
stripped = value.strip()
|
|
38
|
+
if not stripped:
|
|
39
|
+
return ""
|
|
40
|
+
try:
|
|
41
|
+
numeric_value = float(stripped)
|
|
42
|
+
except ValueError:
|
|
43
|
+
numeric_value = None
|
|
44
|
+
else:
|
|
45
|
+
return f"{_format_magnitude(numeric_value)}mm"
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
quantity = _UNIT_REGISTRY(stripped)
|
|
49
|
+
except (UndefinedUnitError, DimensionalityError):
|
|
50
|
+
quantity = None
|
|
51
|
+
else:
|
|
52
|
+
if quantity.check(_LENGTH_DIMENSION):
|
|
53
|
+
converted = quantity.to(_UNIT_REGISTRY.mm).magnitude
|
|
54
|
+
return f"{_format_magnitude(float(converted))}mm"
|
|
55
|
+
try:
|
|
56
|
+
float(stripped)
|
|
57
|
+
except ValueError:
|
|
58
|
+
raise TemplateError(f"Unsupported dimension value '{value}'.") from None
|
|
59
|
+
return f"{stripped}mm"
|
|
60
|
+
return str(value)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _normalise_margin(value: Any) -> str | dict[str, str] | None:
|
|
64
|
+
"""Return a margin specification string or per-side mapping."""
|
|
65
|
+
if value is None:
|
|
66
|
+
return None
|
|
67
|
+
if isinstance(value, (int, float)):
|
|
68
|
+
return f"{value}mm"
|
|
69
|
+
if isinstance(value, str):
|
|
70
|
+
cleaned = value.strip()
|
|
71
|
+
if not cleaned:
|
|
72
|
+
return None
|
|
73
|
+
lowered = cleaned.lower()
|
|
74
|
+
if lowered == "narrow":
|
|
75
|
+
return _normalise_dimension("1.5cm")
|
|
76
|
+
if lowered == "wide":
|
|
77
|
+
return _normalise_dimension("3.0cm")
|
|
78
|
+
dim = _normalise_dimension(cleaned)
|
|
79
|
+
return dim or None
|
|
80
|
+
if isinstance(value, dict):
|
|
81
|
+
formatted: dict[str, str] = {}
|
|
82
|
+
for key, raw in value.items():
|
|
83
|
+
key_str = str(key).strip()
|
|
84
|
+
if not key_str:
|
|
85
|
+
continue
|
|
86
|
+
dim = _normalise_dimension(raw)
|
|
87
|
+
if dim:
|
|
88
|
+
formatted[key_str] = dim
|
|
89
|
+
return formatted or None
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class PaperSpec(BaseModel):
|
|
94
|
+
"""Validated paper settings parsed from front matter."""
|
|
95
|
+
|
|
96
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
97
|
+
|
|
98
|
+
format: str | None = None
|
|
99
|
+
width: str | None = None
|
|
100
|
+
height: str | None = None
|
|
101
|
+
orientation: Literal["portrait", "landscape"] = "portrait"
|
|
102
|
+
margin: str | dict[str, str] | None = None
|
|
103
|
+
marks: bool = False
|
|
104
|
+
binding_offset: str | None = Field(default=None, alias="binding")
|
|
105
|
+
duplex: bool = True
|
|
106
|
+
watermark: str | None = None
|
|
107
|
+
extra: dict[str, Any] = Field(default_factory=dict)
|
|
108
|
+
|
|
109
|
+
@model_validator(mode="before")
|
|
110
|
+
@classmethod
|
|
111
|
+
def _coerce_payload(cls, data: Any) -> Any:
|
|
112
|
+
if isinstance(data, str):
|
|
113
|
+
return {"format": data}
|
|
114
|
+
if not isinstance(data, dict):
|
|
115
|
+
return {}
|
|
116
|
+
payload = dict(data)
|
|
117
|
+
if "paper" in payload and "format" not in payload:
|
|
118
|
+
payload["format"] = payload.pop("paper")
|
|
119
|
+
if "binding" in payload and "binding_offset" not in payload:
|
|
120
|
+
payload["binding_offset"] = payload.pop("binding")
|
|
121
|
+
if "frame" in payload and "marks" not in payload:
|
|
122
|
+
payload["marks"] = payload.pop("frame")
|
|
123
|
+
if "duplex" in payload:
|
|
124
|
+
payload["duplex"] = bool(payload["duplex"])
|
|
125
|
+
return payload
|
|
126
|
+
|
|
127
|
+
@field_validator("format")
|
|
128
|
+
@classmethod
|
|
129
|
+
def _normalise_format(cls, value: str | None) -> str | None:
|
|
130
|
+
if value is None:
|
|
131
|
+
return None
|
|
132
|
+
lowered = value.strip().lower()
|
|
133
|
+
if not lowered:
|
|
134
|
+
return None
|
|
135
|
+
if lowered.endswith(_FORMAT_SUFFIX):
|
|
136
|
+
lowered = lowered[: -len(_FORMAT_SUFFIX)]
|
|
137
|
+
if lowered and lowered not in _KNOWN_FORMATS:
|
|
138
|
+
raise TemplateError(
|
|
139
|
+
f"Unsupported paper format '{value}'. Expected one of: "
|
|
140
|
+
f"{', '.join(sorted(_KNOWN_FORMATS))}."
|
|
141
|
+
)
|
|
142
|
+
return lowered
|
|
143
|
+
|
|
144
|
+
@field_validator("width", "height")
|
|
145
|
+
@classmethod
|
|
146
|
+
def _format_dimensions(cls, value: Any) -> str | None:
|
|
147
|
+
if value is None:
|
|
148
|
+
return None
|
|
149
|
+
dim = _normalise_dimension(value)
|
|
150
|
+
return dim or None
|
|
151
|
+
|
|
152
|
+
@field_validator("orientation", mode="before")
|
|
153
|
+
@classmethod
|
|
154
|
+
def _format_orientation(cls, value: Any) -> Any:
|
|
155
|
+
if value is None:
|
|
156
|
+
return value
|
|
157
|
+
if isinstance(value, str):
|
|
158
|
+
candidate = value.strip().lower()
|
|
159
|
+
if candidate in {"vertical", "portrait"}:
|
|
160
|
+
return "portrait"
|
|
161
|
+
if candidate in {"horizontal", "landscape"}:
|
|
162
|
+
return "landscape"
|
|
163
|
+
return value
|
|
164
|
+
|
|
165
|
+
@field_validator("binding_offset", mode="before")
|
|
166
|
+
@classmethod
|
|
167
|
+
def _format_binding(cls, value: Any) -> str | None:
|
|
168
|
+
if value is None:
|
|
169
|
+
return None
|
|
170
|
+
dim = _normalise_dimension(value)
|
|
171
|
+
return dim or None
|
|
172
|
+
|
|
173
|
+
@field_validator("margin", mode="before")
|
|
174
|
+
@classmethod
|
|
175
|
+
def _format_margin(cls, value: Any) -> Any:
|
|
176
|
+
return _normalise_margin(value)
|
|
177
|
+
|
|
178
|
+
def to_documentclass_options(self) -> tuple[str | None, str | None]:
|
|
179
|
+
paper_option = f"{self.format}{_FORMAT_SUFFIX}" if self.format else None
|
|
180
|
+
orientation_option = "landscape" if self.orientation == "landscape" else None
|
|
181
|
+
return paper_option, orientation_option
|
|
182
|
+
|
|
183
|
+
def geometry_options(self, *, default_margin: str | None = DEFAULT_MARGIN) -> list[str]:
|
|
184
|
+
options: list[str] = []
|
|
185
|
+
margin_value = self.margin or _normalise_margin(default_margin)
|
|
186
|
+
if margin_value:
|
|
187
|
+
if isinstance(margin_value, str):
|
|
188
|
+
options.append(f"margin={margin_value}")
|
|
189
|
+
elif isinstance(margin_value, dict):
|
|
190
|
+
for key, dim in margin_value.items():
|
|
191
|
+
if dim:
|
|
192
|
+
options.append(f"{key}={dim}")
|
|
193
|
+
|
|
194
|
+
paper_option, orientation_option = self.to_documentclass_options()
|
|
195
|
+
if paper_option:
|
|
196
|
+
options.append(paper_option)
|
|
197
|
+
if orientation_option:
|
|
198
|
+
options.append(orientation_option)
|
|
199
|
+
if self.width:
|
|
200
|
+
options.append(f"paperwidth={self.width}")
|
|
201
|
+
if self.height:
|
|
202
|
+
options.append(f"paperheight={self.height}")
|
|
203
|
+
|
|
204
|
+
if self.marks:
|
|
205
|
+
options.append("showframe")
|
|
206
|
+
if self.binding_offset:
|
|
207
|
+
options.append(f"bindingoffset={self.binding_offset}")
|
|
208
|
+
|
|
209
|
+
for key, value in self.extra.items():
|
|
210
|
+
key_str = str(key).strip()
|
|
211
|
+
if not key_str:
|
|
212
|
+
continue
|
|
213
|
+
if isinstance(value, bool):
|
|
214
|
+
if value:
|
|
215
|
+
options.append(key_str)
|
|
216
|
+
continue
|
|
217
|
+
val_str = _normalise_dimension(value)
|
|
218
|
+
if val_str:
|
|
219
|
+
options.append(f"{key_str}={val_str}")
|
|
220
|
+
|
|
221
|
+
return options
|
|
222
|
+
|
|
223
|
+
def extra_options(self) -> list[str]:
|
|
224
|
+
"""Return only explicitly provided extra geometry options."""
|
|
225
|
+
options: list[str] = []
|
|
226
|
+
for key, value in self.extra.items():
|
|
227
|
+
key_str = str(key).strip()
|
|
228
|
+
if not key_str:
|
|
229
|
+
continue
|
|
230
|
+
if isinstance(value, bool):
|
|
231
|
+
if value:
|
|
232
|
+
options.append(key_str)
|
|
233
|
+
continue
|
|
234
|
+
val_str = _normalise_dimension(value)
|
|
235
|
+
if val_str:
|
|
236
|
+
options.append(f"{key_str}={val_str}")
|
|
237
|
+
return options
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@dataclass(slots=True)
|
|
241
|
+
class GeometryResolution:
|
|
242
|
+
"""Resolved geometry options injected into templates and fragments."""
|
|
243
|
+
|
|
244
|
+
documentclass_options: str
|
|
245
|
+
paper_option: str | None
|
|
246
|
+
orientation_option: str | None
|
|
247
|
+
geometry_options: str
|
|
248
|
+
geometry_options_list: list[str]
|
|
249
|
+
geometry_extra_options: str
|
|
250
|
+
watermark: str | None
|
|
251
|
+
duplex: bool
|
|
252
|
+
page_width: str | None
|
|
253
|
+
page_height: str | None
|
|
254
|
+
margin_all: str | None
|
|
255
|
+
margin_left: str | None
|
|
256
|
+
margin_right: str | None
|
|
257
|
+
margin_top: str | None
|
|
258
|
+
margin_bottom: str | None
|
|
259
|
+
margin_inner: str | None
|
|
260
|
+
margin_outer: str | None
|
|
261
|
+
binding_offset: str | None
|
|
262
|
+
spec: PaperSpec
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def resolve_geometry_settings(
|
|
266
|
+
context: Mapping[str, Any], overrides: Mapping[str, Any] | None = None
|
|
267
|
+
) -> GeometryResolution:
|
|
268
|
+
"""Resolve geometry settings from template context and overrides."""
|
|
269
|
+
press_section = overrides.get("press") if isinstance(overrides, Mapping) else {}
|
|
270
|
+
documentclass_override = (
|
|
271
|
+
press_section.get("documentclass") if isinstance(press_section, Mapping) else None
|
|
272
|
+
)
|
|
273
|
+
documentclass_context = context.get("documentclass")
|
|
274
|
+
documentclass = documentclass_override or documentclass_context
|
|
275
|
+
is_memoir = str(documentclass).strip().lower() == "memoir" if documentclass else False
|
|
276
|
+
paper_raw = press_section.get("paper") if isinstance(press_section, Mapping) else None
|
|
277
|
+
geometry_extra = press_section.get("geometry") if isinstance(press_section, Mapping) else None
|
|
278
|
+
margin_raw = press_section.get("margin") if isinstance(press_section, Mapping) else None
|
|
279
|
+
orientation_raw = (
|
|
280
|
+
press_section.get("orientation") if isinstance(press_section, Mapping) else None
|
|
281
|
+
)
|
|
282
|
+
watermark_raw = press_section.get("watermark") if isinstance(press_section, Mapping) else None
|
|
283
|
+
duplex_raw = press_section.get("duplex") if isinstance(press_section, Mapping) else None
|
|
284
|
+
binding_raw = press_section.get("binding") if isinstance(press_section, Mapping) else None
|
|
285
|
+
marks_raw = press_section.get("marks") if isinstance(press_section, Mapping) else None
|
|
286
|
+
|
|
287
|
+
if paper_raw is None:
|
|
288
|
+
paper_raw = context.get("paper")
|
|
289
|
+
if geometry_extra is None:
|
|
290
|
+
geometry_extra = context.get("geometry")
|
|
291
|
+
if margin_raw is None:
|
|
292
|
+
margin_raw = context.get("margin")
|
|
293
|
+
if orientation_raw is None:
|
|
294
|
+
orientation_raw = context.get("orientation")
|
|
295
|
+
if watermark_raw is None:
|
|
296
|
+
watermark_raw = context.get("watermark")
|
|
297
|
+
if duplex_raw is None:
|
|
298
|
+
duplex_raw = context.get("duplex")
|
|
299
|
+
if binding_raw is None:
|
|
300
|
+
binding_raw = context.get("binding")
|
|
301
|
+
if marks_raw is None:
|
|
302
|
+
marks_raw = context.get("marks")
|
|
303
|
+
marks_flag = _coerce_marks_flag(marks_raw)
|
|
304
|
+
|
|
305
|
+
payload: dict[str, Any] = {}
|
|
306
|
+
if isinstance(paper_raw, Mapping):
|
|
307
|
+
payload.update(paper_raw)
|
|
308
|
+
elif paper_raw is not None:
|
|
309
|
+
payload["format"] = paper_raw
|
|
310
|
+
|
|
311
|
+
if "margin" not in payload and margin_raw is not None:
|
|
312
|
+
payload["margin"] = margin_raw
|
|
313
|
+
if "orientation" not in payload and orientation_raw is not None:
|
|
314
|
+
payload["orientation"] = orientation_raw
|
|
315
|
+
if "watermark" not in payload and watermark_raw is not None:
|
|
316
|
+
payload["watermark"] = watermark_raw
|
|
317
|
+
if "duplex" not in payload and duplex_raw is not None:
|
|
318
|
+
payload["duplex"] = duplex_raw
|
|
319
|
+
if "binding" not in payload and binding_raw is not None:
|
|
320
|
+
payload["binding"] = binding_raw
|
|
321
|
+
if "marks" not in payload and marks_flag is not None:
|
|
322
|
+
payload["marks"] = marks_flag
|
|
323
|
+
|
|
324
|
+
if isinstance(geometry_extra, Mapping):
|
|
325
|
+
formatted_extra: dict[str, Any] = {}
|
|
326
|
+
for key, raw in geometry_extra.items():
|
|
327
|
+
key_str = str(key).strip()
|
|
328
|
+
if not key_str:
|
|
329
|
+
continue
|
|
330
|
+
if isinstance(raw, bool):
|
|
331
|
+
if raw:
|
|
332
|
+
formatted_extra[key_str] = True # type: ignore[assignment]
|
|
333
|
+
continue
|
|
334
|
+
val = _normalise_dimension(raw)
|
|
335
|
+
if val:
|
|
336
|
+
formatted_extra[key_str] = val
|
|
337
|
+
existing_extra = payload.get("extra") if isinstance(payload.get("extra"), Mapping) else {}
|
|
338
|
+
merged_extra = dict(existing_extra)
|
|
339
|
+
merged_extra.update(formatted_extra)
|
|
340
|
+
if merged_extra:
|
|
341
|
+
payload["extra"] = merged_extra
|
|
342
|
+
|
|
343
|
+
try:
|
|
344
|
+
spec = PaperSpec.model_validate(payload)
|
|
345
|
+
except ValidationError as exc:
|
|
346
|
+
raise TemplateError(f"Invalid paper settings: {exc}") from exc
|
|
347
|
+
geometry_options_list = spec.geometry_options() if not is_memoir else []
|
|
348
|
+
geometry_options = ",".join(geometry_options_list)
|
|
349
|
+
geometry_extra_options = ",".join(spec.extra_options()) if not is_memoir else ""
|
|
350
|
+
paper_option, orientation_option = spec.to_documentclass_options()
|
|
351
|
+
options: list[str] = []
|
|
352
|
+
if not is_memoir:
|
|
353
|
+
options = [opt for opt in (paper_option, orientation_option) if opt]
|
|
354
|
+
if spec.marks and "showframe" not in options:
|
|
355
|
+
options.append("showframe")
|
|
356
|
+
if spec.duplex and "twoside" not in options:
|
|
357
|
+
options.append("twoside")
|
|
358
|
+
elif not spec.duplex and "twoside" in options:
|
|
359
|
+
pass
|
|
360
|
+
documentclass_options = f"[{','.join(options)}]" if options else ""
|
|
361
|
+
|
|
362
|
+
if is_memoir:
|
|
363
|
+
paper_option = None
|
|
364
|
+
orientation_option = None
|
|
365
|
+
|
|
366
|
+
page_width, page_height = _resolve_page_dimensions(spec)
|
|
367
|
+
margin_all, margin_left, margin_right, margin_top, margin_bottom = _resolve_margins(spec)
|
|
368
|
+
if all(
|
|
369
|
+
value is None
|
|
370
|
+
for value in (margin_all, margin_left, margin_right, margin_top, margin_bottom)
|
|
371
|
+
):
|
|
372
|
+
margin_all = "2cm"
|
|
373
|
+
margin_inner = margin_left
|
|
374
|
+
margin_outer = margin_right
|
|
375
|
+
if spec.binding_offset:
|
|
376
|
+
if margin_inner:
|
|
377
|
+
margin_inner = f"\\dimexpr {margin_inner} + {spec.binding_offset}\\relax"
|
|
378
|
+
else:
|
|
379
|
+
margin_inner = spec.binding_offset
|
|
380
|
+
|
|
381
|
+
return GeometryResolution(
|
|
382
|
+
documentclass_options=documentclass_options,
|
|
383
|
+
paper_option=paper_option,
|
|
384
|
+
orientation_option=orientation_option,
|
|
385
|
+
geometry_options=geometry_options,
|
|
386
|
+
geometry_options_list=geometry_options_list,
|
|
387
|
+
geometry_extra_options=geometry_extra_options,
|
|
388
|
+
watermark=spec.watermark,
|
|
389
|
+
duplex=bool(spec.duplex),
|
|
390
|
+
page_width=page_width,
|
|
391
|
+
page_height=page_height,
|
|
392
|
+
margin_all=margin_all,
|
|
393
|
+
margin_left=margin_left,
|
|
394
|
+
margin_right=margin_right,
|
|
395
|
+
margin_top=margin_top,
|
|
396
|
+
margin_bottom=margin_bottom,
|
|
397
|
+
margin_inner=margin_inner,
|
|
398
|
+
margin_outer=margin_outer,
|
|
399
|
+
binding_offset=spec.binding_offset,
|
|
400
|
+
spec=spec,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def inject_geometry_context(
|
|
405
|
+
context: dict[str, Any], overrides: Mapping[str, Any] | None = None
|
|
406
|
+
) -> GeometryResolution:
|
|
407
|
+
"""Populate a context dictionary with resolved geometry settings."""
|
|
408
|
+
resolution = resolve_geometry_settings(context, overrides)
|
|
409
|
+
context["documentclass_options"] = resolution.documentclass_options
|
|
410
|
+
context["paper_option"] = resolution.paper_option
|
|
411
|
+
context["orientation_option"] = resolution.orientation_option
|
|
412
|
+
context["geometry_options"] = resolution.geometry_options
|
|
413
|
+
context["geometry_options_list"] = resolution.geometry_options_list
|
|
414
|
+
context["geometry_extra_options"] = resolution.geometry_extra_options
|
|
415
|
+
context["geometry_page_width"] = resolution.page_width
|
|
416
|
+
context["geometry_page_height"] = resolution.page_height
|
|
417
|
+
context["geometry_margin_all"] = resolution.margin_all
|
|
418
|
+
context["geometry_margin_left"] = resolution.margin_left
|
|
419
|
+
context["geometry_margin_right"] = resolution.margin_right
|
|
420
|
+
context["geometry_margin_top"] = resolution.margin_top
|
|
421
|
+
context["geometry_margin_bottom"] = resolution.margin_bottom
|
|
422
|
+
context["geometry_margin_inner"] = resolution.margin_inner
|
|
423
|
+
context["geometry_margin_outer"] = resolution.margin_outer
|
|
424
|
+
context["geometry_binding_offset"] = resolution.binding_offset
|
|
425
|
+
context["geometry_duplex"] = resolution.duplex
|
|
426
|
+
if resolution.watermark:
|
|
427
|
+
context["geometry_watermark"] = resolution.watermark
|
|
428
|
+
else:
|
|
429
|
+
context.pop("geometry_watermark", None)
|
|
430
|
+
return resolution
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _resolve_page_dimensions(spec: PaperSpec) -> tuple[str | None, str | None]:
|
|
434
|
+
"""Return paper width/height strings with orientation applied."""
|
|
435
|
+
width = spec.width
|
|
436
|
+
height = spec.height
|
|
437
|
+
if not width or not height:
|
|
438
|
+
format_dims = _format_dimensions(spec.format)
|
|
439
|
+
if format_dims is not None:
|
|
440
|
+
width, height = format_dims
|
|
441
|
+
if spec.orientation == "landscape" and width and height:
|
|
442
|
+
width, height = height, width
|
|
443
|
+
return width, height
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _format_dimensions(format_name: str | None) -> tuple[str, str] | None:
|
|
447
|
+
if not format_name:
|
|
448
|
+
return None
|
|
449
|
+
lookup = format_name.lower()
|
|
450
|
+
sizes_mm: dict[str, tuple[float, float]] = {
|
|
451
|
+
"a0": (841, 1189),
|
|
452
|
+
"a1": (594, 841),
|
|
453
|
+
"a2": (420, 594),
|
|
454
|
+
"a3": (297, 420),
|
|
455
|
+
"a4": (210, 297),
|
|
456
|
+
"a5": (148, 210),
|
|
457
|
+
"a6": (105, 148),
|
|
458
|
+
"b0": (1000, 1414),
|
|
459
|
+
"b1": (707, 1000),
|
|
460
|
+
"b2": (500, 707),
|
|
461
|
+
"b3": (353, 500),
|
|
462
|
+
"b4": (250, 353),
|
|
463
|
+
"b5": (176, 250),
|
|
464
|
+
"b6": (125, 176),
|
|
465
|
+
"c0": (917, 1297),
|
|
466
|
+
"c1": (648, 917),
|
|
467
|
+
"c2": (458, 648),
|
|
468
|
+
"c3": (324, 458),
|
|
469
|
+
"c4": (229, 324),
|
|
470
|
+
"c5": (162, 229),
|
|
471
|
+
"c6": (114, 162),
|
|
472
|
+
"letter": (215.9, 279.4),
|
|
473
|
+
"legal": (215.9, 355.6),
|
|
474
|
+
"executive": (184.15, 266.7),
|
|
475
|
+
}
|
|
476
|
+
dims = sizes_mm.get(lookup)
|
|
477
|
+
if dims is None:
|
|
478
|
+
return None
|
|
479
|
+
width, height = dims
|
|
480
|
+
return (f"{width}mm", f"{height}mm")
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _resolve_margins(
|
|
484
|
+
spec: PaperSpec,
|
|
485
|
+
) -> tuple[str | None, str | None, str | None, str | None, str | None]:
|
|
486
|
+
margin_all: str | None = None
|
|
487
|
+
margin_left: str | None = None
|
|
488
|
+
margin_right: str | None = None
|
|
489
|
+
margin_top: str | None = None
|
|
490
|
+
margin_bottom: str | None = None
|
|
491
|
+
if isinstance(spec.margin, str):
|
|
492
|
+
margin_all = spec.margin
|
|
493
|
+
elif isinstance(spec.margin, dict):
|
|
494
|
+
margin_left = spec.margin.get("left") or spec.margin.get("l")
|
|
495
|
+
margin_right = spec.margin.get("right") or spec.margin.get("r")
|
|
496
|
+
margin_top = spec.margin.get("top") or spec.margin.get("t")
|
|
497
|
+
margin_bottom = spec.margin.get("bottom") or spec.margin.get("b")
|
|
498
|
+
margin_inner = spec.margin.get("inner")
|
|
499
|
+
margin_outer = spec.margin.get("outer")
|
|
500
|
+
margin_left = margin_inner or margin_left
|
|
501
|
+
margin_right = margin_outer or margin_right
|
|
502
|
+
return margin_all, margin_left, margin_right, margin_top, margin_bottom
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _coerce_marks_flag(value: Any) -> bool | None:
|
|
506
|
+
"""Return a boolean marks flag when the payload resembles one."""
|
|
507
|
+
if isinstance(value, bool):
|
|
508
|
+
return value
|
|
509
|
+
if isinstance(value, (int, float)):
|
|
510
|
+
return bool(value)
|
|
511
|
+
if isinstance(value, str):
|
|
512
|
+
token = value.strip().lower()
|
|
513
|
+
if not token or token in {"false", "no", "off", "0", "none"}:
|
|
514
|
+
return False
|
|
515
|
+
if token in {"true", "yes", "on", "1", "showframe"}:
|
|
516
|
+
return True
|
|
517
|
+
return None
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
__all__ = [
|
|
521
|
+
"DEFAULT_MARGIN",
|
|
522
|
+
"GeometryResolution",
|
|
523
|
+
"PaperSpec",
|
|
524
|
+
"inject_geometry_context",
|
|
525
|
+
"resolve_geometry_settings",
|
|
526
|
+
]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
\BLOCK{ set is_memoir = (documentclass|default('')|lower == 'memoir') }
|
|
2
|
+
\BLOCK{ set duplex = geometry_duplex|default(false) }
|
|
3
|
+
\BLOCK{ set options_list = geometry_options_list|default([]) }
|
|
4
|
+
\BLOCK{ set fallback = options_list|join(',') }
|
|
5
|
+
\BLOCK{ set options = geometry_options|default(fallback)|trim }
|
|
6
|
+
\BLOCK{ if is_memoir }
|
|
7
|
+
\makeatletter
|
|
8
|
+
\BLOCK{ if duplex }
|
|
9
|
+
\@twosidetrue
|
|
10
|
+
\@mparswitchtrue
|
|
11
|
+
\BLOCK{ else }
|
|
12
|
+
\@twosidefalse
|
|
13
|
+
\@mparswitchfalse
|
|
14
|
+
\BLOCK{ endif }
|
|
15
|
+
\makeatother
|
|
16
|
+
\BLOCK{ if geometry_page_height and geometry_page_width }
|
|
17
|
+
\setstocksize{\VAR{geometry_page_height}}{\VAR{geometry_page_width}}
|
|
18
|
+
\settrimmedsize{\VAR{geometry_page_height}}{\VAR{geometry_page_width}}{*}
|
|
19
|
+
\BLOCK{ endif }
|
|
20
|
+
\BLOCK{ if geometry_margin_all|default('', true)|trim }
|
|
21
|
+
\setlrmarginsandblock{\VAR{geometry_margin_all}}{\VAR{geometry_margin_all}}{*}
|
|
22
|
+
\setulmarginsandblock{\VAR{geometry_margin_all}}{\VAR{geometry_margin_all}}{*}
|
|
23
|
+
\BLOCK{ else }
|
|
24
|
+
\BLOCK{ set left_margin = geometry_margin_inner or geometry_margin_left }
|
|
25
|
+
\BLOCK{ set right_margin = geometry_margin_outer or geometry_margin_right or left_margin }
|
|
26
|
+
\BLOCK{ set top_margin = geometry_margin_top }
|
|
27
|
+
\BLOCK{ set bottom_margin = geometry_margin_bottom or top_margin }
|
|
28
|
+
\BLOCK{ if left_margin or right_margin or top_margin or bottom_margin }
|
|
29
|
+
\setlrmarginsandblock{\VAR{left_margin|default('2cm', true)}}{\VAR{right_margin|default('2cm', true)}}{*}
|
|
30
|
+
\setulmarginsandblock{\VAR{top_margin|default('2cm', true)}}{\VAR{bottom_margin|default('2cm', true)}}{*}
|
|
31
|
+
\BLOCK{ endif }
|
|
32
|
+
\BLOCK{ endif }
|
|
33
|
+
\BLOCK{ if geometry_binding_offset|default('', true)|trim }
|
|
34
|
+
\setbinding{\VAR{geometry_binding_offset}}
|
|
35
|
+
\BLOCK{ endif }
|
|
36
|
+
\checkandfixthelayout
|
|
37
|
+
\BLOCK{ else }
|
|
38
|
+
\BLOCK{ if options }
|
|
39
|
+
\usepackage[\VAR{options}]{geometry}
|
|
40
|
+
\BLOCK{ else }
|
|
41
|
+
\usepackage{geometry}
|
|
42
|
+
\BLOCK{ endif }
|
|
43
|
+
\BLOCK{ endif }
|
|
44
|
+
\BLOCK{ if geometry_watermark|default('')|trim }
|
|
45
|
+
\usepackage{tikz}
|
|
46
|
+
% Note: xwatermark behaves poorly with LuaLaTeX/XeLaTeX and draftwatermark can
|
|
47
|
+
% conflict with dynamic geometry changes; implement watermark with TikZ instead.
|
|
48
|
+
\AddToHook{shipout/background}{%
|
|
49
|
+
\begin{tikzpicture}[remember picture,overlay]
|
|
50
|
+
\node[rotate=45,scale=7,text opacity=0.06] at (current page.center) {\textbf{\textsf{\VAR{geometry_watermark}}}};
|
|
51
|
+
\end{tikzpicture}%
|
|
52
|
+
}
|
|
53
|
+
\BLOCK{ endif }
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, ClassVar
|
|
7
|
+
|
|
8
|
+
from texsmith.core.fragments.base import BaseFragment, FragmentPiece
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class GlossaryConfig:
|
|
13
|
+
has_entries: bool
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def from_context(cls, context: Mapping[str, Any]) -> GlossaryConfig:
|
|
17
|
+
glossary = context.get("glossary")
|
|
18
|
+
acronyms = context.get("acronyms")
|
|
19
|
+
has_entries = bool(glossary) or bool(acronyms)
|
|
20
|
+
return cls(has_entries=has_entries)
|
|
21
|
+
|
|
22
|
+
def inject_into(self, context: dict[str, Any]) -> None:
|
|
23
|
+
context["ts_glossary_enabled"] = self.has_entries
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class GlossaryFragment(BaseFragment[GlossaryConfig]):
|
|
27
|
+
name: ClassVar[str] = "ts-glossary"
|
|
28
|
+
description: ClassVar[str] = "Glossary and acronym helpers."
|
|
29
|
+
pieces: ClassVar[list[FragmentPiece]] = [
|
|
30
|
+
FragmentPiece(
|
|
31
|
+
template_path=Path(__file__).with_name("ts-glossary.jinja.sty"),
|
|
32
|
+
kind="package",
|
|
33
|
+
slot="extra_packages",
|
|
34
|
+
),
|
|
35
|
+
FragmentPiece(
|
|
36
|
+
template_path=Path(__file__).with_name("ts-glossary-backmatter.jinja.tex"),
|
|
37
|
+
kind="inline",
|
|
38
|
+
slot="fragment_backmatter",
|
|
39
|
+
),
|
|
40
|
+
]
|
|
41
|
+
attributes: ClassVar[dict[str, Any]] = {}
|
|
42
|
+
config_cls: ClassVar[type[GlossaryConfig]] = GlossaryConfig
|
|
43
|
+
source: ClassVar[Path] = Path(__file__).with_name("ts-glossary.jinja.sty")
|
|
44
|
+
context_defaults: ClassVar[dict[str, Any]] = {"extra_packages": "", "fragment_backmatter": ""}
|
|
45
|
+
|
|
46
|
+
def build_config(
|
|
47
|
+
self, context: Mapping[str, Any], overrides: Mapping[str, Any] | None = None
|
|
48
|
+
) -> GlossaryConfig:
|
|
49
|
+
_ = overrides
|
|
50
|
+
return self.config_cls.from_context(context)
|
|
51
|
+
|
|
52
|
+
def inject(
|
|
53
|
+
self,
|
|
54
|
+
config: GlossaryConfig,
|
|
55
|
+
context: dict[str, Any],
|
|
56
|
+
overrides: Mapping[str, Any] | None = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
_ = overrides
|
|
59
|
+
config.inject_into(context)
|
|
60
|
+
|
|
61
|
+
def should_render(self, config: GlossaryConfig) -> bool:
|
|
62
|
+
return config.has_entries
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
fragment = GlossaryFragment()
|
|
66
|
+
|
|
67
|
+
__all__ = ["GlossaryConfig", "GlossaryFragment", "fragment"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
\ProvidesPackage{ts-glossary}[2025/11/21 TeXSmith glossary helpers (generated)]
|
|
2
|
+
|
|
3
|
+
\BLOCK{ set resolved_index_engine = index_engine|default("makeindex")|lower }
|
|
4
|
+
\RequirePackage{makeidx}
|
|
5
|
+
\RequirePackage[acronym,nopostdot\BLOCK{ if resolved_index_engine in ["texindy", "pyxindy"] },xindy\BLOCK{ endif }]{glossaries}
|
|
6
|
+
|
|
7
|
+
% Resolve glossary style (supports overriding via press.glossary.style or glossary_style).
|
|
8
|
+
\BLOCK{ set default_style = default_glossary_style|default("list") }
|
|
9
|
+
\BLOCK{ set raw_style = glossary_style|default("") }
|
|
10
|
+
\BLOCK{ set raw_glossary = glossary|default(false) }
|
|
11
|
+
\BLOCK{ set acronyms_map = acronyms|default({}) }
|
|
12
|
+
\BLOCK{ set resolved_style = raw_style if raw_style else (raw_glossary if raw_glossary is string else default_style) }
|
|
13
|
+
\BLOCK{ set should_activate = raw_glossary or acronyms_map }
|
|
14
|
+
|
|
15
|
+
\BLOCK{ if should_activate }
|
|
16
|
+
\makeglossaries
|
|
17
|
+
\setglossarystyle{\VAR{resolved_style}}
|
|
18
|
+
\BLOCK{ endif }
|
|
19
|
+
|
|
20
|
+
% Acronym definitions
|
|
21
|
+
\BLOCK{ if acronyms_map }
|
|
22
|
+
\BLOCK{ for key, entry in acronyms_map.items() }
|
|
23
|
+
\BLOCK{ set pair = entry if (entry is sequence and not entry is string) else [key, entry] }
|
|
24
|
+
\newacronym{\VAR{key}}{\VAR{pair[0]|latex_escape}}{\VAR{pair[1]|latex_escape}}
|
|
25
|
+
\BLOCK{ endfor }
|
|
26
|
+
\BLOCK{ endif }
|
|
27
|
+
|
|
28
|
+
\endinput
|