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,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"]
|