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.
Files changed (252) hide show
  1. texsmith/__init__.py +107 -0
  2. texsmith/_alias.py +59 -0
  3. texsmith/adapters/__init__.py +6 -0
  4. texsmith/adapters/docker.py +258 -0
  5. texsmith/adapters/handlers/__init__.py +3 -0
  6. texsmith/adapters/handlers/_assets.py +363 -0
  7. texsmith/adapters/handlers/_helpers.py +62 -0
  8. texsmith/adapters/handlers/_mermaid.py +122 -0
  9. texsmith/adapters/handlers/admonitions.py +267 -0
  10. texsmith/adapters/handlers/basic.py +228 -0
  11. texsmith/adapters/handlers/blocks.py +851 -0
  12. texsmith/adapters/handlers/code.py +309 -0
  13. texsmith/adapters/handlers/inline.py +937 -0
  14. texsmith/adapters/handlers/links.py +239 -0
  15. texsmith/adapters/handlers/media.py +380 -0
  16. texsmith/adapters/latex/__init__.py +10 -0
  17. texsmith/adapters/latex/engines/__init__.py +726 -0
  18. texsmith/adapters/latex/engines/latex/__init__.py +26 -0
  19. texsmith/adapters/latex/engines/latex/log.py +720 -0
  20. texsmith/adapters/latex/engines/latex/runner.py +28 -0
  21. texsmith/adapters/latex/engines/tectonic/__init__.py +38 -0
  22. texsmith/adapters/latex/formatter.py +297 -0
  23. texsmith/adapters/latex/latexmk.py +148 -0
  24. texsmith/adapters/latex/partials/acronym.tex +1 -0
  25. texsmith/adapters/latex/partials/add.tex +1 -0
  26. texsmith/adapters/latex/partials/addition.tex +1 -0
  27. texsmith/adapters/latex/partials/blockquote.tex +3 -0
  28. texsmith/adapters/latex/partials/callout.tex +3 -0
  29. texsmith/adapters/latex/partials/choices.tex +5 -0
  30. texsmith/adapters/latex/partials/citation.tex +1 -0
  31. texsmith/adapters/latex/partials/codeblock.tex +7 -0
  32. texsmith/adapters/latex/partials/codeblock_listings.tex +5 -0
  33. texsmith/adapters/latex/partials/codeblock_pygments.tex +5 -0
  34. texsmith/adapters/latex/partials/codeblock_verbatim.tex +12 -0
  35. texsmith/adapters/latex/partials/codeinline.tex +1 -0
  36. texsmith/adapters/latex/partials/codeinlinett.tex +1 -0
  37. texsmith/adapters/latex/partials/comment.tex +1 -0
  38. texsmith/adapters/latex/partials/del.tex +1 -0
  39. texsmith/adapters/latex/partials/deletion.tex +1 -0
  40. texsmith/adapters/latex/partials/description_list.tex +5 -0
  41. texsmith/adapters/latex/partials/enquote.tex +1 -0
  42. texsmith/adapters/latex/partials/epigraph.tex +1 -0
  43. texsmith/adapters/latex/partials/exercises_solutions.tex +7 -0
  44. texsmith/adapters/latex/partials/figure.tex +33 -0
  45. texsmith/adapters/latex/partials/figure_tcolorbox.tex +15 -0
  46. texsmith/adapters/latex/partials/footnote.tex +3 -0
  47. texsmith/adapters/latex/partials/glossary.tex +1 -0
  48. texsmith/adapters/latex/partials/heading.tex +14 -0
  49. texsmith/adapters/latex/partials/highlight.tex +25 -0
  50. texsmith/adapters/latex/partials/horizontal_rule.tex +1 -0
  51. texsmith/adapters/latex/partials/href.tex +1 -0
  52. texsmith/adapters/latex/partials/icon.tex +1 -0
  53. texsmith/adapters/latex/partials/include.tex +1 -0
  54. texsmith/adapters/latex/partials/index.tex +1 -0
  55. texsmith/adapters/latex/partials/italic.tex +1 -0
  56. texsmith/adapters/latex/partials/keystroke.tex +46 -0
  57. texsmith/adapters/latex/partials/label.tex +1 -0
  58. texsmith/adapters/latex/partials/list_acronyms.tex +5 -0
  59. texsmith/adapters/latex/partials/list_glossary.tex +8 -0
  60. texsmith/adapters/latex/partials/multicolumn.tex +3 -0
  61. texsmith/adapters/latex/partials/ordered_list.tex +5 -0
  62. texsmith/adapters/latex/partials/pagestyle.tex +1 -0
  63. texsmith/adapters/latex/partials/ref.tex +1 -0
  64. texsmith/adapters/latex/partials/regex.tex +1 -0
  65. texsmith/adapters/latex/partials/smallcaps.tex +1 -0
  66. texsmith/adapters/latex/partials/strikethrough.tex +1 -0
  67. texsmith/adapters/latex/partials/strong.tex +1 -0
  68. texsmith/adapters/latex/partials/subscript.tex +1 -0
  69. texsmith/adapters/latex/partials/substitution.tex +1 -0
  70. texsmith/adapters/latex/partials/superscript.tex +1 -0
  71. texsmith/adapters/latex/partials/tabbed.tex +3 -0
  72. texsmith/adapters/latex/partials/table.tex +48 -0
  73. texsmith/adapters/latex/partials/underline.tex +1 -0
  74. texsmith/adapters/latex/partials/unordered_list.tex +5 -0
  75. texsmith/adapters/latex/partials/url.tex +1 -0
  76. texsmith/adapters/latex/pygments.py +98 -0
  77. texsmith/adapters/latex/pyxindy.py +64 -0
  78. texsmith/adapters/latex/renderer.py +220 -0
  79. texsmith/adapters/latex/tectonic.py +366 -0
  80. texsmith/adapters/latex/utils.py +86 -0
  81. texsmith/adapters/markdown/__init__.py +342 -0
  82. texsmith/adapters/plugins/__init__.py +8 -0
  83. texsmith/adapters/plugins/material.py +174 -0
  84. texsmith/adapters/plugins/snippet.py +1734 -0
  85. texsmith/adapters/transformers/__init__.py +122 -0
  86. texsmith/adapters/transformers/base.py +140 -0
  87. texsmith/adapters/transformers/strategies.py +1456 -0
  88. texsmith/adapters/transformers/utils.py +59 -0
  89. texsmith/api/__init__.py +80 -0
  90. texsmith/api/_utils.py +56 -0
  91. texsmith/api/document.py +645 -0
  92. texsmith/api/pipeline.py +229 -0
  93. texsmith/api/service.py +648 -0
  94. texsmith/api/templates.py +287 -0
  95. texsmith/core/__init__.py +6 -0
  96. texsmith/core/bibliography/__init__.py +57 -0
  97. texsmith/core/bibliography/collection.py +354 -0
  98. texsmith/core/bibliography/doi.py +194 -0
  99. texsmith/core/bibliography/issues.py +15 -0
  100. texsmith/core/bibliography/parsing.py +45 -0
  101. texsmith/core/callouts.py +99 -0
  102. texsmith/core/config.py +191 -0
  103. texsmith/core/context.py +242 -0
  104. texsmith/core/conversion/__init__.py +103 -0
  105. texsmith/core/conversion/core.py +780 -0
  106. texsmith/core/conversion/debug.py +87 -0
  107. texsmith/core/conversion/inputs.py +474 -0
  108. texsmith/core/conversion/renderer.py +578 -0
  109. texsmith/core/conversion/templates.py +646 -0
  110. texsmith/core/conversion_contexts.py +95 -0
  111. texsmith/core/diagnostics.py +118 -0
  112. texsmith/core/exceptions.py +41 -0
  113. texsmith/core/fonts/__init__.py +3 -0
  114. texsmith/core/fragments/__init__.py +716 -0
  115. texsmith/core/fragments/base.py +117 -0
  116. texsmith/core/metadata.py +280 -0
  117. texsmith/core/mustache.py +86 -0
  118. texsmith/core/partials.py +20 -0
  119. texsmith/core/rules.py +394 -0
  120. texsmith/core/templates/__init__.py +58 -0
  121. texsmith/core/templates/base.py +343 -0
  122. texsmith/core/templates/builtins.py +68 -0
  123. texsmith/core/templates/context_usage.py +137 -0
  124. texsmith/core/templates/loader.py +303 -0
  125. texsmith/core/templates/manifest.py +861 -0
  126. texsmith/core/templates/runtime.py +347 -0
  127. texsmith/core/templates/text.py +14 -0
  128. texsmith/core/templates/wrapper.py +340 -0
  129. texsmith/core/user_dir.py +179 -0
  130. texsmith/devtools.py +28 -0
  131. texsmith/extensions/__init__.py +162 -0
  132. texsmith/extensions/index/__init__.py +21 -0
  133. texsmith/extensions/index/markdown.py +94 -0
  134. texsmith/extensions/index/mkdocs_plugin.py +136 -0
  135. texsmith/extensions/index/registry.py +57 -0
  136. texsmith/extensions/index/renderer.py +183 -0
  137. texsmith/extensions/index/templates/index.tex +1 -0
  138. texsmith/extensions/latex_raw.py +115 -0
  139. texsmith/extensions/latex_text.py +117 -0
  140. texsmith/extensions/mermaid.py +267 -0
  141. texsmith/extensions/missing_footnotes.py +125 -0
  142. texsmith/extensions/multi_citations.py +54 -0
  143. texsmith/extensions/progressbar/__init__.py +9 -0
  144. texsmith/extensions/progressbar/markdown.py +212 -0
  145. texsmith/extensions/progressbar/renderer.py +117 -0
  146. texsmith/extensions/smallcaps.py +48 -0
  147. texsmith/extensions/texlogos/__init__.py +10 -0
  148. texsmith/extensions/texlogos/markdown.py +236 -0
  149. texsmith/extensions/texlogos/renderer.py +81 -0
  150. texsmith/extensions/texlogos/specs.py +66 -0
  151. texsmith/fonts/__init__.py +57 -0
  152. texsmith/fonts/cache.py +46 -0
  153. texsmith/fonts/constants.py +60 -0
  154. texsmith/fonts/coverage.py +275 -0
  155. texsmith/fonts/downloader.py +103 -0
  156. texsmith/fonts/fallback.py +438 -0
  157. texsmith/fonts/html_scripts.py +168 -0
  158. texsmith/fonts/logging.py +127 -0
  159. texsmith/fonts/pipeline.py +338 -0
  160. texsmith/fonts/scripts.py +493 -0
  161. texsmith/fonts/ucharclasses.py +164 -0
  162. texsmith/fragments/__init__.py +11 -0
  163. texsmith/fragments/bibliography/__init__.py +65 -0
  164. texsmith/fragments/bibliography/fragment.toml +3 -0
  165. texsmith/fragments/bibliography/ts-bibliography-backmatter.jinja.tex +35 -0
  166. texsmith/fragments/bibliography/ts-bibliography.jinja.tex +10 -0
  167. texsmith/fragments/callouts/__init__.py +88 -0
  168. texsmith/fragments/callouts/fragment.toml +3 -0
  169. texsmith/fragments/callouts/ts-callouts.jinja.sty +129 -0
  170. texsmith/fragments/code/__init__.py +82 -0
  171. texsmith/fragments/code/fragment.toml +3 -0
  172. texsmith/fragments/code/ts-code.jinja.sty +140 -0
  173. texsmith/fragments/extra/__init__.py +180 -0
  174. texsmith/fragments/extra/fragment.toml +3 -0
  175. texsmith/fragments/extra/ts-extra.jinja.tex +13 -0
  176. texsmith/fragments/fonts/__init__.py +880 -0
  177. texsmith/fragments/fonts/fragment.toml +3 -0
  178. texsmith/fragments/fonts/ts-fonts.jinja.sty +292 -0
  179. texsmith/fragments/frame/__init__.py +170 -0
  180. texsmith/fragments/frame/fragment.toml +3 -0
  181. texsmith/fragments/frame/ts-frame.tex.jinja +49 -0
  182. texsmith/fragments/geometry/__init__.py +169 -0
  183. texsmith/fragments/geometry/fragment.toml +3 -0
  184. texsmith/fragments/geometry/paper.py +526 -0
  185. texsmith/fragments/geometry/ts_geometry.tex.jinja +53 -0
  186. texsmith/fragments/glossary/__init__.py +67 -0
  187. texsmith/fragments/glossary/fragment.toml +3 -0
  188. texsmith/fragments/glossary/ts-glossary-backmatter.jinja.tex +5 -0
  189. texsmith/fragments/glossary/ts-glossary.jinja.sty +28 -0
  190. texsmith/fragments/index/__init__.py +66 -0
  191. texsmith/fragments/index/fragment.toml +3 -0
  192. texsmith/fragments/index/ts-index-backmatter.jinja.tex +3 -0
  193. texsmith/fragments/index/ts-index.jinja.sty +12 -0
  194. texsmith/fragments/keystrokes/__init__.py +69 -0
  195. texsmith/fragments/keystrokes/fragment.toml +3 -0
  196. texsmith/fragments/keystrokes/ts-keystrokes.jinja.sty +20 -0
  197. texsmith/fragments/todolist/__init__.py +71 -0
  198. texsmith/fragments/todolist/fragment.toml +3 -0
  199. texsmith/fragments/todolist/ts-todolist.jinja.sty +21 -0
  200. texsmith/fragments/typesetting/__init__.py +214 -0
  201. texsmith/fragments/typesetting/fragment.toml +3 -0
  202. texsmith/fragments/typesetting/ts-typesetting.tex.jinja +81 -0
  203. texsmith/index.py +26 -0
  204. texsmith/plugins/__init__.py +14 -0
  205. texsmith/progressbar.py +8 -0
  206. texsmith/quotes.py +40 -0
  207. texsmith/smart_dashes.py +66 -0
  208. texsmith/templates/__init__.py +3 -0
  209. texsmith/templates/article/README.md +33 -0
  210. texsmith/templates/article/__init__.py +281 -0
  211. texsmith/templates/article/template/manifest.toml +125 -0
  212. texsmith/templates/article/template/mermaid-config.json +18 -0
  213. texsmith/templates/article/template/template.tex +74 -0
  214. texsmith/templates/book/README.md +26 -0
  215. texsmith/templates/book/__init__.py +75 -0
  216. texsmith/templates/book/overrides/codeblock.tex +7 -0
  217. texsmith/templates/book/overrides/codeinline.tex +1 -0
  218. texsmith/templates/book/template/fixtoc.sty +81 -0
  219. texsmith/templates/book/template/manifest.toml +198 -0
  220. texsmith/templates/book/template/template.tex +314 -0
  221. texsmith/templates/common/__init__.py +1 -0
  222. texsmith/templates/common/latexmkrc +46 -0
  223. texsmith/templates/letter/README.md +59 -0
  224. texsmith/templates/letter/__init__.py +495 -0
  225. texsmith/templates/letter/demo.md +31 -0
  226. texsmith/templates/letter/fonts/modernline bold.otf +0 -0
  227. texsmith/templates/letter/fonts/modernline.otf +0 -0
  228. texsmith/templates/letter/manifest.toml +197 -0
  229. texsmith/templates/letter/template/callouts.jinja.sty +290 -0
  230. texsmith/templates/letter/template/template.tex +123 -0
  231. texsmith/templates/snippet/README.md +21 -0
  232. texsmith/templates/snippet/__init__.py +80 -0
  233. texsmith/templates/snippet/template/manifest.toml +66 -0
  234. texsmith/templates/snippet/template/template.tex +47 -0
  235. texsmith/texlogos.py +15 -0
  236. texsmith/ui/__init__.py +6 -0
  237. texsmith/ui/cli/__init__.py +22 -0
  238. texsmith/ui/cli/_options.py +338 -0
  239. texsmith/ui/cli/app.py +65 -0
  240. texsmith/ui/cli/bibliography.py +300 -0
  241. texsmith/ui/cli/commands/__init__.py +14 -0
  242. texsmith/ui/cli/commands/render.py +1128 -0
  243. texsmith/ui/cli/commands/templates.py +397 -0
  244. texsmith/ui/cli/diagnostics.py +36 -0
  245. texsmith/ui/cli/presenter.py +663 -0
  246. texsmith/ui/cli/state.py +263 -0
  247. texsmith/ui/cli/utils.py +235 -0
  248. texsmith-0.0.2.dev0.dist-info/METADATA +187 -0
  249. texsmith-0.0.2.dev0.dist-info/RECORD +252 -0
  250. texsmith-0.0.2.dev0.dist-info/WHEEL +4 -0
  251. texsmith-0.0.2.dev0.dist-info/entry_points.txt +22 -0
  252. texsmith-0.0.2.dev0.dist-info/licenses/LICENSE.md +21 -0
@@ -0,0 +1,397 @@
1
+ """CLI helpers for inspecting and scaffolding LaTeX templates."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from importlib import metadata
7
+ from pathlib import Path
8
+ import shutil
9
+
10
+ import typer
11
+
12
+ from texsmith.core.fragments import FRAGMENT_REGISTRY, TemplateError
13
+ from texsmith.core.templates import TemplateError as TemplateTplError, load_template
14
+ from texsmith.core.templates.builtins import iter_builtin_templates
15
+ from texsmith.core.templates.loader import _iter_local_candidates, _looks_like_template_root
16
+
17
+ from ..state import emit_error, ensure_rich_compat, get_cli_state
18
+
19
+
20
+ def _format_list(values: Iterable[str]) -> str:
21
+ """Format a sequence of strings into a comma-separated list or a placeholder.
22
+
23
+ This ensures consistent pretty-printing for user display, handling empty
24
+ sequences gracefully by returning a standard placeholder.
25
+ """
26
+ sequence = list(values)
27
+ return ", ".join(sequence) if sequence else "-"
28
+
29
+
30
+ def _discover_local_templates(base: Path | None = None) -> list[Path]:
31
+ """Scan the working directory for potential template candidates.
32
+
33
+ This enables users to use project-local templates without needing to install
34
+ them as Python packages, supporting rapid development and customization.
35
+ """
36
+ base_path = (base or Path.cwd()).resolve()
37
+ roots = {base_path, base_path / "templates"}
38
+ candidates: list[Path] = []
39
+ for root in roots:
40
+ if not root.exists() or not root.is_dir():
41
+ continue
42
+ for child in root.iterdir():
43
+ if child.is_dir() and _looks_like_template_root(child):
44
+ candidates.append(child)
45
+ return sorted({candidate.resolve() for candidate in candidates})
46
+
47
+
48
+ def list_templates() -> None:
49
+ """Print a table listing built-in, entry-point, and local templates."""
50
+
51
+ ensure_rich_compat()
52
+ try:
53
+ from rich import box
54
+ from rich.table import Table
55
+ except ImportError: # pragma: no cover - fallback when Rich is unavailable
56
+ _print_template_list_plain()
57
+ return
58
+
59
+ console = get_cli_state().console
60
+ table = Table(
61
+ title="Available Templates",
62
+ box=box.SQUARE,
63
+ show_edge=True,
64
+ header_style="bold cyan",
65
+ )
66
+ table.add_column("Name", style="magenta")
67
+ table.add_column("Origin", style="green")
68
+ table.add_column("Location")
69
+
70
+ entries = _collect_template_entries()
71
+ if not entries:
72
+ table.add_row("-", "-", "No templates found")
73
+ else:
74
+ for entry in entries:
75
+ table.add_row(entry["name"], entry["origin"], entry["root"])
76
+
77
+ console.print(table)
78
+
79
+
80
+ def _print_template_list_plain() -> None:
81
+ typer.echo("Available templates:")
82
+ entries = _collect_template_entries()
83
+ if not entries:
84
+ typer.echo(" - (none)")
85
+ return
86
+ for entry in entries:
87
+ typer.echo(f" - {entry['name']} ({entry['origin']}) -> {entry['root']}")
88
+
89
+
90
+ def _collect_template_entries() -> list[dict[str, str]]:
91
+ """Aggregate available templates from built-ins, entry points, and local paths.
92
+
93
+ This unifies multiple template sources into a single list, giving the user
94
+ a complete view of all available templates regardless of how they are installed.
95
+ """
96
+ entries: list[dict[str, str]] = []
97
+
98
+ for slug in iter_builtin_templates():
99
+ try:
100
+ template = load_template(slug)
101
+ except TemplateError:
102
+ continue
103
+ entries.append({"name": slug, "origin": "builtin", "root": str(template.root)})
104
+
105
+ try:
106
+ entry_points = metadata.entry_points().select(group="texsmith.templates")
107
+ except Exception: # pragma: no cover - extremely defensive
108
+ entry_points = ()
109
+
110
+ for entry_point in entry_points:
111
+ try:
112
+ template = load_template(entry_point.name)
113
+ except TemplateError:
114
+ continue
115
+ entries.append(
116
+ {"name": entry_point.name, "origin": "entry-point", "root": str(template.root)}
117
+ )
118
+
119
+ for local_root in _discover_local_templates():
120
+ entries.append({"name": local_root.name, "origin": "local", "root": str(local_root)})
121
+
122
+ seen: set[tuple[str, str]] = set()
123
+ unique_entries: list[dict[str, str]] = []
124
+ for entry in entries:
125
+ key = (entry["name"], entry["root"])
126
+ if key in seen:
127
+ continue
128
+ seen.add(key)
129
+ unique_entries.append(entry)
130
+ return sorted(unique_entries, key=lambda item: (item["origin"], item["name"]))
131
+
132
+
133
+ def _discover_local_templates(base: Path | None = None) -> list[Path]:
134
+ """Scan for potential template roots under cwd/parents and templates/."""
135
+ base_path = (base or Path.cwd()).resolve()
136
+ roots = {base_path, base_path / "templates"}
137
+ candidates: list[Path] = []
138
+ for root in roots:
139
+ if not root.exists() or not root.is_dir():
140
+ continue
141
+ try:
142
+ children = list(root.iterdir())
143
+ except OSError:
144
+ continue
145
+ for child in children:
146
+ if child.is_dir() and _looks_like_template_root(child):
147
+ candidates.append(child)
148
+
149
+ # Also walk ancestor templates folders via loader helper.
150
+ for candidate in _iter_local_candidates(""):
151
+ if candidate.is_dir() and _looks_like_template_root(candidate):
152
+ candidates.append(candidate)
153
+ return sorted({candidate.resolve() for candidate in candidates})
154
+
155
+
156
+ def show_template_info(identifier: str) -> None:
157
+ """Display metadata extracted from a LaTeX template manifest."""
158
+
159
+ ensure_rich_compat()
160
+ try:
161
+ from rich import box as rich_box
162
+ from rich.panel import Panel as RichPanel
163
+ from rich.pretty import Pretty as RichPretty
164
+ from rich.table import Table as RichTable
165
+ except ImportError: # pragma: no cover - fallback when Rich is stubbed
166
+ rich_box = RichPanel = RichPretty = RichTable = None # type: ignore[assignment] # noqa: N806
167
+
168
+ try:
169
+ template = load_template(identifier)
170
+ except TemplateTplError as exc:
171
+ emit_error(f"Unable to load template '{identifier}': {exc}", exception=exc)
172
+ raise typer.Exit(code=1) from exc
173
+
174
+ info = template.info
175
+ console = get_cli_state().console
176
+
177
+ if RichTable is None or RichPanel is None or RichPretty is None or rich_box is None:
178
+ typer.echo(f"Template: {identifier}")
179
+ typer.echo(f"Name: {info.name}")
180
+ typer.echo(f"Version: {info.version}")
181
+ if getattr(info, "description", None):
182
+ typer.echo(f"Description: {info.description}")
183
+ typer.echo(f"Entrypoint: {info.entrypoint}")
184
+ typer.echo(f"Engine: {info.engine or '-'}")
185
+ typer.echo(f"Shell escape: {'yes' if info.shell_escape else 'no'}")
186
+ typer.echo(f"Template root: {template.root}")
187
+ typer.echo(f"TeX Live year: {info.texlive_year or '-'}")
188
+ typer.echo(f"tlmgr packages: {_format_list(info.tlmgr_packages)}")
189
+ typer.echo(f"Formatter overrides: {_format_list(info.override)}")
190
+
191
+ if info.attributes:
192
+ typer.echo("Attributes:")
193
+ for key, value in sorted(info.attributes.items()):
194
+ desc = getattr(value, "description", None) or "-"
195
+ typer.echo(
196
+ f" - {key}: type={value.type or 'any'}, format={value.format or '-'}, "
197
+ f"default={value.default!r}, desc={desc}"
198
+ )
199
+
200
+ assets = list(template.iter_assets())
201
+ typer.echo("Assets:")
202
+ if assets:
203
+ for asset in assets:
204
+ try:
205
+ relative_source = asset.source.relative_to(template.root)
206
+ source_display = relative_source.as_posix()
207
+ except ValueError:
208
+ source_display = asset.source.as_posix()
209
+ typer.echo(
210
+ f" - {asset.destination.as_posix()} <- {source_display} "
211
+ f"(templated: {'yes' if asset.template else 'no'}, "
212
+ f"encoding: {asset.encoding or '-'})"
213
+ )
214
+ else:
215
+ typer.echo(" - No declared assets")
216
+
217
+ fragment_entries = getattr(info, "fragments", None) or []
218
+ typer.echo("Fragments:")
219
+ if fragment_entries:
220
+ for fragment_name in fragment_entries:
221
+ try:
222
+ definition = FRAGMENT_REGISTRY.resolve(fragment_name)
223
+ desc = definition.description or ""
224
+ attrs = ", ".join(sorted(definition.attributes.keys())) or "-"
225
+ typer.echo(f" - {fragment_name}: {desc} (attributes: {attrs})")
226
+ except Exception:
227
+ typer.echo(f" - {fragment_name}")
228
+ else:
229
+ typer.echo(" - None")
230
+
231
+ slots, default_slot = info.resolve_slots()
232
+ typer.echo("Slots:")
233
+ for name, slot in sorted(slots.items()):
234
+ base = slot.base_level if slot.base_level is not None else "-"
235
+ depth = slot.depth or "-"
236
+ effective = slot.resolve_level(0)
237
+ desc = getattr(slot, "description", None)
238
+ typer.echo(
239
+ f" - {name} ({'default' if name == default_slot else 'optional'}): "
240
+ f"base={base}, depth={depth}, offset={slot.offset}, "
241
+ f"effective={effective}, strip_heading={'yes' if slot.strip_heading else 'no'}"
242
+ + (f" — {desc}" if desc else "")
243
+ )
244
+ return
245
+
246
+ assert RichTable is not None
247
+ assert RichPanel is not None
248
+ assert RichPretty is not None
249
+ assert rich_box is not None
250
+
251
+ summary = RichTable.grid(padding=(0, 1))
252
+ summary.add_row("Name", info.name)
253
+ summary.add_row("Version", info.version)
254
+ summary.add_row("Entrypoint", info.entrypoint)
255
+ summary.add_row("Engine", info.engine or "-")
256
+ summary.add_row("Shell escape", "yes" if info.shell_escape else "no")
257
+ summary.add_row("Template root", str(template.root))
258
+ summary.add_row("TeX Live year", str(info.texlive_year) if info.texlive_year else "-")
259
+ summary.add_row("tlmgr packages", _format_list(info.tlmgr_packages))
260
+ summary.add_row("Formatter overrides", _format_list(info.override))
261
+
262
+ console.print(
263
+ RichPanel(
264
+ summary, box=rich_box.SQUARE, title=f"Template: {identifier}", border_style="cyan"
265
+ )
266
+ )
267
+
268
+ if info.attributes:
269
+ attrs = RichTable(
270
+ title="Attributes",
271
+ box=rich_box.SQUARE,
272
+ header_style="bold cyan",
273
+ show_edge=True,
274
+ show_lines=True,
275
+ )
276
+ attrs.add_column("Key", style="magenta")
277
+ attrs.add_column("Type", style="green")
278
+ attrs.add_column("Format", style="green")
279
+ attrs.add_column("Default")
280
+ attrs.add_column("Description")
281
+ for key, value in sorted(info.attributes.items()):
282
+ attrs.add_row(
283
+ key,
284
+ str(getattr(value, "type", None) or "any"),
285
+ str(getattr(value, "format", None) or "-"),
286
+ RichPretty(getattr(value, "default", None), indent_guides=True),
287
+ getattr(value, "description", None) or "-",
288
+ )
289
+ console.print(attrs)
290
+
291
+ assets = list(template.iter_assets())
292
+ assets_table = RichTable(
293
+ title="Assets",
294
+ box=rich_box.SQUARE,
295
+ header_style="bold cyan",
296
+ show_edge=True,
297
+ )
298
+ assets_table.add_column("Destination", style="magenta")
299
+ assets_table.add_column("Source", style="green")
300
+ assets_table.add_column("Templated", justify="center")
301
+ assets_table.add_column("Encoding", justify="center")
302
+ if assets:
303
+ for asset in assets:
304
+ try:
305
+ relative_source = asset.source.relative_to(template.root)
306
+ source_display = relative_source.as_posix()
307
+ except ValueError:
308
+ source_display = asset.source.as_posix()
309
+ assets_table.add_row(
310
+ asset.destination.as_posix(),
311
+ source_display,
312
+ "yes" if asset.template else "no",
313
+ asset.encoding or "-",
314
+ )
315
+ else:
316
+ assets_table.add_row("-", "No declared assets", "", "")
317
+ console.print(assets_table)
318
+
319
+ fragment_entries = info.fragments or []
320
+ fragments_table = RichTable(
321
+ title="Fragments",
322
+ box=rich_box.SQUARE,
323
+ header_style="bold cyan",
324
+ show_edge=True,
325
+ )
326
+ fragments_table.add_column("Name", style="magenta")
327
+ fragments_table.add_column("Description")
328
+ fragments_table.add_column("Attributes")
329
+ if fragment_entries:
330
+ for fragment_name in fragment_entries:
331
+ try:
332
+ definition = FRAGMENT_REGISTRY.resolve(fragment_name)
333
+ desc = definition.description or "-"
334
+ attrs = ", ".join(sorted(definition.attributes.keys())) or "-"
335
+ fragments_table.add_row(fragment_name, desc, attrs)
336
+ except Exception:
337
+ fragments_table.add_row(fragment_name, "-", "-")
338
+ else:
339
+ fragments_table.add_row("-", "None", "-")
340
+ console.print(fragments_table)
341
+
342
+ slots, default_slot = info.resolve_slots()
343
+ slots_table = RichTable(
344
+ title="Slots",
345
+ box=rich_box.SQUARE,
346
+ header_style="bold cyan",
347
+ show_edge=True,
348
+ )
349
+ slots_table.add_column("Name", style="magenta")
350
+ slots_table.add_column("Default", justify="center")
351
+ slots_table.add_column("Base Level", justify="right")
352
+ slots_table.add_column("Depth", justify="right")
353
+ slots_table.add_column("Offset", justify="right")
354
+ slots_table.add_column("Effective Level", justify="right")
355
+ slots_table.add_column("Strip Heading", justify="center")
356
+
357
+ for name, slot in sorted(slots.items()):
358
+ base = slot.base_level if slot.base_level is not None else "-"
359
+ depth = slot.depth or "-"
360
+ effective = slot.resolve_level(0)
361
+ slots_table.add_row(
362
+ name,
363
+ "*" if name == default_slot else "",
364
+ str(base),
365
+ str(depth),
366
+ str(slot.offset),
367
+ str(effective),
368
+ "yes" if slot.strip_heading else "no",
369
+ )
370
+
371
+ console.print(slots_table)
372
+
373
+
374
+ def scaffold_template(identifier: str, destination: Path) -> None:
375
+ """Copy the selected template into ``destination`` for customization."""
376
+
377
+ try:
378
+ template = load_template(identifier)
379
+ except TemplateError as exc:
380
+ emit_error(f"Unable to load template '{identifier}': {exc}", exception=exc)
381
+ raise typer.Exit(code=1) from exc
382
+
383
+ destination = destination.expanduser().resolve()
384
+ try:
385
+ shutil.copytree(template.root, destination, dirs_exist_ok=True)
386
+ except OSError as exc:
387
+ emit_error(f"Failed to scaffold template into '{destination}': {exc}", exception=exc)
388
+ raise typer.Exit(code=1) from exc
389
+
390
+ typer.echo(f"Scaffolded template '{identifier}' into {destination}")
391
+
392
+
393
+ __all__ = [
394
+ "list_templates",
395
+ "scaffold_template",
396
+ "show_template_info",
397
+ ]
@@ -0,0 +1,36 @@
1
+ """Diagnostic emitter bridging the core pipeline with CLI rendering utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+
8
+ from texsmith.core.diagnostics import DiagnosticEmitter, format_event_message
9
+
10
+ from .state import CLIState, emit_error, emit_warning, get_cli_state, render_message
11
+
12
+
13
+ class CliEmitter(DiagnosticEmitter):
14
+ """Emit diagnostics using the rich-enabled CLI helpers."""
15
+
16
+ def __init__(self, state: CLIState | None = None, *, debug_enabled: bool | None = None) -> None:
17
+ self._state = state or get_cli_state()
18
+ if debug_enabled is None:
19
+ debug_enabled = self._state.show_tracebacks
20
+ self.debug_enabled = bool(debug_enabled)
21
+
22
+ def warning(self, message: str, exc: BaseException | None = None) -> None:
23
+ emit_warning(message, exception=exc)
24
+
25
+ def error(self, message: str, exc: BaseException | None = None) -> None:
26
+ emit_error(message, exception=exc)
27
+
28
+ def event(self, name: str, payload: Mapping[str, Any]) -> None:
29
+ data = dict(payload)
30
+ self._state.record_event(name, data)
31
+ message = format_event_message(name, data)
32
+ if message:
33
+ render_message("info", message)
34
+
35
+
36
+ __all__ = ["CliEmitter"]