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,663 @@
|
|
|
1
|
+
"""Rich-aware presenters for CLI output and diagnostics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
import contextlib
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from texsmith.adapters.latex.engines import (
|
|
13
|
+
LatexMessage,
|
|
14
|
+
LatexMessageSeverity,
|
|
15
|
+
parse_latex_log,
|
|
16
|
+
)
|
|
17
|
+
from texsmith.api.pipeline import ConversionBundle
|
|
18
|
+
from texsmith.api.templates import TemplateRenderResult
|
|
19
|
+
|
|
20
|
+
from .state import CLIState
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
24
|
+
from rich.console import Console
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _get_console(state: CLIState, *, stderr: bool = False) -> Console | None:
|
|
28
|
+
"""Retrieve the active Rich console from the CLI state if available.
|
|
29
|
+
|
|
30
|
+
This helper ensures that we respect the user's stream preference (stdout vs stderr)
|
|
31
|
+
and gracefully handle cases where the console hasn't been initialized or is
|
|
32
|
+
running in a non-interactive environment.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
console = state.err_console if stderr else state.console
|
|
36
|
+
except Exception: # pragma: no cover - fallback when Rich unavailable
|
|
37
|
+
return None
|
|
38
|
+
if getattr(console, "is_terminal", False):
|
|
39
|
+
return console
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _rich_components() -> tuple[Any, Any, Any, Any] | None:
|
|
44
|
+
"""Import and return Rich components if the library is installed.
|
|
45
|
+
|
|
46
|
+
This allows the CLI to degrade gracefully on systems where `rich` is missing,
|
|
47
|
+
falling back to plain text output instead of crashing.
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
from rich import box
|
|
51
|
+
from rich.panel import Panel
|
|
52
|
+
from rich.table import Table
|
|
53
|
+
from rich.text import Text
|
|
54
|
+
except ImportError: # pragma: no cover - Rich absent
|
|
55
|
+
return None
|
|
56
|
+
return box, Panel, Table, Text
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _build_table(
|
|
60
|
+
*,
|
|
61
|
+
title: str | None,
|
|
62
|
+
columns: Sequence[str],
|
|
63
|
+
header_style: str = "bold cyan",
|
|
64
|
+
box_style: Any | None = None,
|
|
65
|
+
) -> tuple[Any, Any, Any] | tuple[None, None, None]:
|
|
66
|
+
"""Create a Rich table with the house style."""
|
|
67
|
+
components = _rich_components()
|
|
68
|
+
if components is None:
|
|
69
|
+
return None, None, None
|
|
70
|
+
box_module, _panel_cls, table_cls, text_cls = components
|
|
71
|
+
table = table_cls(
|
|
72
|
+
title=title or None,
|
|
73
|
+
box=box_style or box_module.SQUARE,
|
|
74
|
+
show_edge=True,
|
|
75
|
+
header_style=header_style,
|
|
76
|
+
)
|
|
77
|
+
for col in columns:
|
|
78
|
+
table.add_column(col)
|
|
79
|
+
return table, text_cls, box_module
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _format_path(path: Path) -> str:
|
|
83
|
+
"""Format a path relative to the current working directory for display.
|
|
84
|
+
|
|
85
|
+
Using relative paths reduces visual noise in console output, making it easier
|
|
86
|
+
for users to identify files within their project structure.
|
|
87
|
+
"""
|
|
88
|
+
resolved = path.resolve()
|
|
89
|
+
try:
|
|
90
|
+
return str(resolved.relative_to(Path.cwd()))
|
|
91
|
+
except ValueError:
|
|
92
|
+
return str(resolved)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _colorize_location(text_cls: Any, *, artifact: str, location: str) -> Any:
|
|
96
|
+
"""Apply per-artifact coloring to locations for Rich tables."""
|
|
97
|
+
lower_loc = location.lower()
|
|
98
|
+
suffixes = {
|
|
99
|
+
"tex": "bright_cyan",
|
|
100
|
+
"pdf": "bright_green",
|
|
101
|
+
"sty": "yellow",
|
|
102
|
+
"cls": "yellow",
|
|
103
|
+
}
|
|
104
|
+
image_suffixes = {"png", "jpg", "jpeg", "gif", "svg", "bmp", "webp"}
|
|
105
|
+
style: str | None = None
|
|
106
|
+
for suffix, mapped in suffixes.items():
|
|
107
|
+
if lower_loc.endswith(f".{suffix}"):
|
|
108
|
+
style = mapped
|
|
109
|
+
break
|
|
110
|
+
if style is None and (
|
|
111
|
+
any(lower_loc.endswith(f".{ext}") for ext in image_suffixes)
|
|
112
|
+
or artifact.lower() == "asset"
|
|
113
|
+
or "/assets/" in lower_loc
|
|
114
|
+
or lower_loc.startswith("assets")
|
|
115
|
+
):
|
|
116
|
+
style = "magenta"
|
|
117
|
+
return text_cls(location, style=style) if style else text_cls(location)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _size_details(path: Path) -> str:
|
|
121
|
+
"""Return a human-readable size for a file if it exists."""
|
|
122
|
+
try:
|
|
123
|
+
stat = path.stat()
|
|
124
|
+
except OSError:
|
|
125
|
+
return ""
|
|
126
|
+
if not path.is_file():
|
|
127
|
+
return ""
|
|
128
|
+
size = stat.st_size
|
|
129
|
+
if size >= 1024 * 1024:
|
|
130
|
+
return f"{size / (1024 * 1024):.2f} MiB"
|
|
131
|
+
if size >= 1024:
|
|
132
|
+
return f"{size / 1024:.1f} KiB"
|
|
133
|
+
return f"{size} B"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _align_size_rows(rows: Sequence[tuple[str, str, str]]) -> list[tuple[str, str, str]]:
|
|
137
|
+
"""Pad size strings so decimal points line up."""
|
|
138
|
+
enriched: list[tuple[str, str, str, str | None, str | None, str | None]] = []
|
|
139
|
+
max_left = 0
|
|
140
|
+
max_right = 0
|
|
141
|
+
for artifact, location, detail in rows:
|
|
142
|
+
if detail:
|
|
143
|
+
tokens = detail.split()
|
|
144
|
+
num = tokens[0]
|
|
145
|
+
tail = " ".join(tokens[1:]) if len(tokens) > 1 else ""
|
|
146
|
+
if "." in num:
|
|
147
|
+
left, right = num.split(".", 1)
|
|
148
|
+
else:
|
|
149
|
+
left, right = num, ""
|
|
150
|
+
max_left = max(max_left, len(left))
|
|
151
|
+
max_right = max(max_right, len(right))
|
|
152
|
+
enriched.append((artifact, location, detail, left, right, tail))
|
|
153
|
+
else:
|
|
154
|
+
enriched.append((artifact, location, detail, None, None, None))
|
|
155
|
+
|
|
156
|
+
if max_left == 0 and max_right == 0:
|
|
157
|
+
return list(rows)
|
|
158
|
+
|
|
159
|
+
aligned: list[tuple[str, str, str]] = []
|
|
160
|
+
for artifact, location, detail, left, right, tail in enriched:
|
|
161
|
+
if detail and left is not None and right is not None:
|
|
162
|
+
padded_left = left.rjust(max_left)
|
|
163
|
+
padded_right = right.ljust(max_right)
|
|
164
|
+
if max_right:
|
|
165
|
+
if right:
|
|
166
|
+
number = f"{padded_left}.{padded_right}".rstrip()
|
|
167
|
+
else:
|
|
168
|
+
number = f"{padded_left} {' ' * (max_right + 1)}".rstrip()
|
|
169
|
+
else:
|
|
170
|
+
number = padded_left
|
|
171
|
+
padded = f"{number} {tail}".rstrip()
|
|
172
|
+
aligned.append((artifact, location, padded))
|
|
173
|
+
else:
|
|
174
|
+
aligned.append((artifact, location, detail))
|
|
175
|
+
return aligned
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _render_summary(state: CLIState, title: str, rows: Sequence[tuple[str, str, str]]) -> None:
|
|
179
|
+
"""Display a summary table of generated artifacts.
|
|
180
|
+
|
|
181
|
+
This provides the user with a high-level overview of what was created and where,
|
|
182
|
+
saving them from having to manually check the output directory.
|
|
183
|
+
"""
|
|
184
|
+
rows = _align_size_rows(rows)
|
|
185
|
+
console = _get_console(state)
|
|
186
|
+
components = _rich_components()
|
|
187
|
+
has_details = any(bool(details) for _, _, details in rows)
|
|
188
|
+
if console is not None and components is not None:
|
|
189
|
+
box_module, _panel_cls, table_cls, text_cls = components
|
|
190
|
+
table = table_cls(box=box_module.SQUARE, header_style="bold cyan")
|
|
191
|
+
if title:
|
|
192
|
+
table.title = title
|
|
193
|
+
table.add_column("Artifact", style="cyan")
|
|
194
|
+
table.add_column("Location")
|
|
195
|
+
if has_details:
|
|
196
|
+
table.add_column("Filesize", style="magenta", justify="right", no_wrap=True)
|
|
197
|
+
for artifact, location, details in rows:
|
|
198
|
+
location_cell = _colorize_location(text_cls, artifact=artifact, location=location)
|
|
199
|
+
if has_details:
|
|
200
|
+
table.add_row(artifact, location_cell, details)
|
|
201
|
+
else:
|
|
202
|
+
table.add_row(artifact, location_cell)
|
|
203
|
+
console.print(table)
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
# Plain-text fallback
|
|
207
|
+
if title:
|
|
208
|
+
typer.echo(title)
|
|
209
|
+
for artifact, location, details in rows:
|
|
210
|
+
suffix = f" — {details}" if details else ""
|
|
211
|
+
typer.echo(f" * {artifact}: {location}{suffix}")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def present_rule_descriptions(state: CLIState, rules: Sequence[Mapping[str, Any]]) -> None:
|
|
215
|
+
"""Render a diagnostic view of registered render rules."""
|
|
216
|
+
if not rules:
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
console = _get_console(state)
|
|
220
|
+
if console is not None:
|
|
221
|
+
table, _text_cls, box_module = _build_table(
|
|
222
|
+
title="Registered Rules",
|
|
223
|
+
columns=["Phase", "Tag", "Name", "Priority", "Before", "After"],
|
|
224
|
+
)
|
|
225
|
+
if table is not None and box_module is not None:
|
|
226
|
+
for entry in rules:
|
|
227
|
+
table.add_row(
|
|
228
|
+
str(entry.get("phase", "")),
|
|
229
|
+
str(entry.get("tag", "")),
|
|
230
|
+
str(entry.get("name", "")),
|
|
231
|
+
str(entry.get("priority", "")),
|
|
232
|
+
", ".join(entry.get("before", []) or []),
|
|
233
|
+
", ".join(entry.get("after", []) or []),
|
|
234
|
+
)
|
|
235
|
+
console.print(table)
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
typer.echo("Registered Rules:")
|
|
239
|
+
for entry in rules:
|
|
240
|
+
before = ", ".join(entry.get("before", []) or [])
|
|
241
|
+
after = ", ".join(entry.get("after", []) or [])
|
|
242
|
+
typer.echo(
|
|
243
|
+
f" - {entry.get('phase', '')}/{entry.get('tag', '')}: {entry.get('name', '')} "
|
|
244
|
+
f"(priority={entry.get('priority', '')}, before=[{before}], after=[{after}])"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _normalise_script_usage(
|
|
249
|
+
usage: Sequence[Mapping[str, Any]] | None,
|
|
250
|
+
) -> list[Mapping[str, Any]]:
|
|
251
|
+
normalised: list[Mapping[str, Any]] = []
|
|
252
|
+
for entry in usage or []:
|
|
253
|
+
slug = entry.get("slug") if isinstance(entry, Mapping) else None
|
|
254
|
+
if not slug:
|
|
255
|
+
continue
|
|
256
|
+
normalised.append(entry)
|
|
257
|
+
return normalised
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def present_fonts_info(state: CLIState, render_result: TemplateRenderResult) -> None:
|
|
261
|
+
"""Display a table of detected fallback fonts."""
|
|
262
|
+
template_context = getattr(render_result, "context", {}) or {}
|
|
263
|
+
fonts_section = template_context.get("fonts") if isinstance(template_context, Mapping) else {}
|
|
264
|
+
usage = _normalise_script_usage(
|
|
265
|
+
fonts_section.get("script_usage") if isinstance(fonts_section, Mapping) else []
|
|
266
|
+
)
|
|
267
|
+
if not usage:
|
|
268
|
+
usage = _normalise_script_usage(getattr(render_result.document_state, "script_usage", []))
|
|
269
|
+
if not usage:
|
|
270
|
+
console = _get_console(state)
|
|
271
|
+
components = _rich_components()
|
|
272
|
+
if console is not None and components is not None:
|
|
273
|
+
box_module, panel_cls, _table_cls, text_cls = components
|
|
274
|
+
console.print(
|
|
275
|
+
panel_cls(
|
|
276
|
+
text_cls("No fallback fonts detected; base fonts only.", style="dim"),
|
|
277
|
+
box=box_module.SIMPLE,
|
|
278
|
+
border_style="cyan",
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
return
|
|
282
|
+
typer.echo("No fallback fonts detected; base fonts only.")
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
console = _get_console(state)
|
|
286
|
+
if console is not None:
|
|
287
|
+
table, _text_cls, _box_module = _build_table(
|
|
288
|
+
title="Fallback Fonts",
|
|
289
|
+
columns=["Script", "Text Cmd", "Font Cmd", "Codepoints", "Font Name"],
|
|
290
|
+
)
|
|
291
|
+
if table is not None:
|
|
292
|
+
for entry in usage:
|
|
293
|
+
count = entry.get("count")
|
|
294
|
+
count_text = str(int(count)) if isinstance(count, (int, float)) else ""
|
|
295
|
+
table.add_row(
|
|
296
|
+
str(entry.get("group") or entry.get("slug") or ""),
|
|
297
|
+
f"\\{entry.get('text_command')}" if entry.get("text_command") else "",
|
|
298
|
+
f"\\{entry.get('font_command')}" if entry.get("font_command") else "",
|
|
299
|
+
count_text,
|
|
300
|
+
str(entry.get("font_name") or ""),
|
|
301
|
+
)
|
|
302
|
+
console.print(table)
|
|
303
|
+
return
|
|
304
|
+
|
|
305
|
+
typer.echo("Fallback Fonts:")
|
|
306
|
+
for entry in usage:
|
|
307
|
+
script = entry.get("group") or entry.get("slug") or ""
|
|
308
|
+
text_cmd = entry.get("text_command") or ""
|
|
309
|
+
font_cmd = entry.get("font_command") or ""
|
|
310
|
+
font_name = entry.get("font_name") or ""
|
|
311
|
+
count = entry.get("count")
|
|
312
|
+
count_text = f" [{int(count)}]" if isinstance(count, (int, float)) else ""
|
|
313
|
+
typer.echo(
|
|
314
|
+
f" - {script}: \\{text_cmd or '?'} -> \\{font_cmd or '?'} ({font_name}){count_text}"
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def present_context_attributes(state: CLIState, render_result: TemplateRenderResult) -> None:
|
|
319
|
+
"""Display resolved context attributes with emitters and consumers."""
|
|
320
|
+
|
|
321
|
+
entries = getattr(render_result, "context_attributes", []) or []
|
|
322
|
+
if not entries:
|
|
323
|
+
typer.echo("No context attributes recorded.")
|
|
324
|
+
return
|
|
325
|
+
|
|
326
|
+
console = _get_console(state)
|
|
327
|
+
components = _rich_components()
|
|
328
|
+
if console is not None and components is not None:
|
|
329
|
+
table, _text_cls, _box_module = _build_table(
|
|
330
|
+
title="Template Context",
|
|
331
|
+
columns=["Key", "Emitters", "Consumers", "Value"],
|
|
332
|
+
)
|
|
333
|
+
if table is not None:
|
|
334
|
+
for entry in entries:
|
|
335
|
+
emitters = "\n".join(entry.get("emitters", []) or [])
|
|
336
|
+
consumers = "\n".join(entry.get("consumers", []) or [])
|
|
337
|
+
table.add_row(
|
|
338
|
+
str(entry.get("name", "")),
|
|
339
|
+
emitters or "-",
|
|
340
|
+
consumers or "-",
|
|
341
|
+
str(entry.get("value", "")),
|
|
342
|
+
)
|
|
343
|
+
console.print(table)
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
typer.echo("Context attributes:")
|
|
347
|
+
for entry in entries:
|
|
348
|
+
emitters_list = entry.get("emitters", []) or ["-"]
|
|
349
|
+
consumers_list = entry.get("consumers", []) or ["-"]
|
|
350
|
+
emitters = "\n ".join(emitters_list)
|
|
351
|
+
consumers = "\n ".join(consumers_list)
|
|
352
|
+
typer.echo(
|
|
353
|
+
f" - {entry.get('name', '')}:\n"
|
|
354
|
+
f" emitters: {emitters}\n"
|
|
355
|
+
f" consumers: {consumers}\n"
|
|
356
|
+
f" value: {entry.get('value', '')}"
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _detect_assets(directory: Path) -> list[Path]:
|
|
361
|
+
"""Find asset files in the given directory.
|
|
362
|
+
|
|
363
|
+
We report these to the user so they know which static resources (images, fonts)
|
|
364
|
+
were successfully copied or generated alongside their document.
|
|
365
|
+
"""
|
|
366
|
+
assets_dir = directory / "assets"
|
|
367
|
+
if not assets_dir.is_dir():
|
|
368
|
+
return []
|
|
369
|
+
return sorted(
|
|
370
|
+
(path for path in assets_dir.rglob("*") if path.is_file()),
|
|
371
|
+
key=lambda p: p.relative_to(assets_dir).as_posix().lower(),
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _detect_ts_packages(directory: Path) -> list[Path]:
|
|
376
|
+
"""Detect generated ts-* packages to highlight them in the summary."""
|
|
377
|
+
candidates = [
|
|
378
|
+
directory / "ts-fonts.sty",
|
|
379
|
+
directory / "ts-glossary.sty",
|
|
380
|
+
]
|
|
381
|
+
return [path for path in candidates if path.is_file()]
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _detect_manifests(directory: Path) -> list[Path]:
|
|
385
|
+
"""Find manifest files in the given directory.
|
|
386
|
+
|
|
387
|
+
Manifests contain machine-readable metadata about the build. Detecting and
|
|
388
|
+
reporting them confirms to the user that the build metadata was correctly persisted.
|
|
389
|
+
"""
|
|
390
|
+
return sorted(path for path in directory.glob("*.json") if "manifest" in path.name.lower())
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _detect_debug_html(directory: Path) -> list[Path]:
|
|
394
|
+
"""Find debug HTML snapshots in the given directory.
|
|
395
|
+
|
|
396
|
+
These snapshots are crucial for troubleshooting rendering issues. Pointing them
|
|
397
|
+
out explicitly helps users find the diagnostic information they need.
|
|
398
|
+
"""
|
|
399
|
+
return sorted(directory.glob("*.debug.html"))
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def present_conversion_summary(
|
|
403
|
+
*,
|
|
404
|
+
state: CLIState,
|
|
405
|
+
output_mode: str,
|
|
406
|
+
bundle: ConversionBundle | None,
|
|
407
|
+
output_path: Path | None,
|
|
408
|
+
render_result: TemplateRenderResult | None,
|
|
409
|
+
) -> None:
|
|
410
|
+
rows: list[tuple[str, str, str]] = []
|
|
411
|
+
|
|
412
|
+
if render_result is not None:
|
|
413
|
+
main_dir = render_result.main_tex_path.parent
|
|
414
|
+
rows.append(
|
|
415
|
+
(
|
|
416
|
+
"Main document",
|
|
417
|
+
_format_path(render_result.main_tex_path),
|
|
418
|
+
_size_details(render_result.main_tex_path),
|
|
419
|
+
)
|
|
420
|
+
)
|
|
421
|
+
for fragment in render_result.fragment_paths:
|
|
422
|
+
rows.append(("Fragment", _format_path(fragment), _size_details(fragment)))
|
|
423
|
+
if render_result.bibliography_path is not None:
|
|
424
|
+
rows.append(
|
|
425
|
+
(
|
|
426
|
+
"Bibliography",
|
|
427
|
+
_format_path(render_result.bibliography_path),
|
|
428
|
+
_size_details(render_result.bibliography_path),
|
|
429
|
+
)
|
|
430
|
+
)
|
|
431
|
+
for package in _detect_ts_packages(main_dir):
|
|
432
|
+
rows.append(
|
|
433
|
+
(
|
|
434
|
+
package.stem,
|
|
435
|
+
_format_path(package),
|
|
436
|
+
_size_details(package),
|
|
437
|
+
)
|
|
438
|
+
)
|
|
439
|
+
for manifest in _detect_manifests(main_dir):
|
|
440
|
+
rows.append(("Manifest", _format_path(manifest), _size_details(manifest)))
|
|
441
|
+
for asset in _detect_assets(main_dir):
|
|
442
|
+
rows.append(("Asset", _format_path(asset), _size_details(asset)))
|
|
443
|
+
for debug_html in _detect_debug_html(main_dir):
|
|
444
|
+
rows.append(("Debug HTML", _format_path(debug_html), _size_details(debug_html)))
|
|
445
|
+
_render_summary(state, "", rows)
|
|
446
|
+
return
|
|
447
|
+
|
|
448
|
+
if output_mode == "file" and output_path is not None:
|
|
449
|
+
rows.append(("LaTeX", _format_path(output_path), _size_details(output_path)))
|
|
450
|
+
elif output_mode == "directory" and bundle is not None:
|
|
451
|
+
for fragment in bundle.fragments:
|
|
452
|
+
path = fragment.output_path or (
|
|
453
|
+
output_path / f"{fragment.stem}.tex" if output_path else None
|
|
454
|
+
)
|
|
455
|
+
if path is None:
|
|
456
|
+
continue
|
|
457
|
+
rows.append(("Fragment", _format_path(Path(path)), _size_details(Path(path))))
|
|
458
|
+
if output_path is not None:
|
|
459
|
+
for manifest in _detect_manifests(output_path):
|
|
460
|
+
rows.append(("Manifest", _format_path(manifest), _size_details(manifest)))
|
|
461
|
+
for asset in _detect_assets(output_path):
|
|
462
|
+
rows.append(("Asset", _format_path(asset), _size_details(asset)))
|
|
463
|
+
for debug_html in _detect_debug_html(output_path):
|
|
464
|
+
rows.append(("Debug HTML", _format_path(debug_html), _size_details(debug_html)))
|
|
465
|
+
|
|
466
|
+
if rows:
|
|
467
|
+
_render_summary(state, "Conversion Summary", rows)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def present_html_summary(
|
|
471
|
+
*,
|
|
472
|
+
state: CLIState,
|
|
473
|
+
output_mode: str,
|
|
474
|
+
output_paths: list[Path],
|
|
475
|
+
) -> None:
|
|
476
|
+
rows: list[tuple[str, str, str]] = []
|
|
477
|
+
if output_mode == "file" and output_paths:
|
|
478
|
+
rows.append(("HTML", _format_path(output_paths[0]), _size_details(output_paths[0])))
|
|
479
|
+
elif output_mode in {"directory", "template"}:
|
|
480
|
+
for path in output_paths:
|
|
481
|
+
rows.append(("HTML", _format_path(path), _size_details(path)))
|
|
482
|
+
if rows:
|
|
483
|
+
_render_summary(state, "HTML Output", rows)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def present_build_summary(
|
|
487
|
+
*,
|
|
488
|
+
state: CLIState,
|
|
489
|
+
render_result: TemplateRenderResult,
|
|
490
|
+
pdf_path: Path,
|
|
491
|
+
) -> None:
|
|
492
|
+
rows = [
|
|
493
|
+
(
|
|
494
|
+
"Main document",
|
|
495
|
+
_format_path(render_result.main_tex_path),
|
|
496
|
+
_size_details(render_result.main_tex_path),
|
|
497
|
+
),
|
|
498
|
+
("PDF", _format_path(pdf_path), _size_details(pdf_path)),
|
|
499
|
+
]
|
|
500
|
+
for fragment in render_result.fragment_paths:
|
|
501
|
+
rows.append(("Fragment", _format_path(fragment), _size_details(fragment)))
|
|
502
|
+
if render_result.bibliography_path is not None:
|
|
503
|
+
rows.append(
|
|
504
|
+
(
|
|
505
|
+
"Bibliography",
|
|
506
|
+
_format_path(render_result.bibliography_path),
|
|
507
|
+
_size_details(render_result.bibliography_path),
|
|
508
|
+
)
|
|
509
|
+
)
|
|
510
|
+
build_dir = render_result.main_tex_path.parent
|
|
511
|
+
for package in _detect_ts_packages(build_dir):
|
|
512
|
+
rows.append((package.stem, _format_path(package), _size_details(package)))
|
|
513
|
+
for manifest in _detect_manifests(build_dir):
|
|
514
|
+
rows.append(("Manifest", _format_path(manifest), _size_details(manifest)))
|
|
515
|
+
for asset in _detect_assets(build_dir):
|
|
516
|
+
rows.append(("Asset", _format_path(asset), _size_details(asset)))
|
|
517
|
+
for debug_html in _detect_debug_html(build_dir):
|
|
518
|
+
rows.append(("Debug HTML", _format_path(debug_html), _size_details(debug_html)))
|
|
519
|
+
_render_summary(state, "", rows)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _format_message_entry(message: LatexMessage) -> str:
|
|
523
|
+
"""Format a LaTeX log message for display in the failure panel.
|
|
524
|
+
|
|
525
|
+
This condenses complex LaTeX log entries into a single, readable line,
|
|
526
|
+
stripping unnecessary details to help the user focus on the primary error.
|
|
527
|
+
"""
|
|
528
|
+
details = "; ".join(message.details[:2])
|
|
529
|
+
if details:
|
|
530
|
+
return f"{message.summary} ({details})"
|
|
531
|
+
return message.summary
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _render_failure_panel(
|
|
535
|
+
state: CLIState,
|
|
536
|
+
title: str,
|
|
537
|
+
rows: Sequence[tuple[str, str]],
|
|
538
|
+
) -> None:
|
|
539
|
+
"""Display a failure diagnostic panel.
|
|
540
|
+
|
|
541
|
+
This panel highlights the critical error and suggests next steps, helping
|
|
542
|
+
users diagnose build failures quickly without wading through raw logs.
|
|
543
|
+
"""
|
|
544
|
+
console = _get_console(state, stderr=True)
|
|
545
|
+
components = _rich_components()
|
|
546
|
+
if console is not None and components is not None:
|
|
547
|
+
box_module, panel_cls, table_cls, text_cls = components
|
|
548
|
+
table = table_cls(box=box_module.SQUARE, show_header=False)
|
|
549
|
+
for label, value in rows:
|
|
550
|
+
table.add_row(text_cls(label, style="bold red"), text_cls(value, style="yellow"))
|
|
551
|
+
console.print(panel_cls(table, box=box_module.SQUARE, title=title, border_style="red"))
|
|
552
|
+
return
|
|
553
|
+
|
|
554
|
+
typer.echo(title, err=True)
|
|
555
|
+
for label, value in rows:
|
|
556
|
+
typer.echo(f" {label}: {value}", err=True)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def present_latex_failure(
|
|
560
|
+
*,
|
|
561
|
+
state: CLIState,
|
|
562
|
+
log_path: Path,
|
|
563
|
+
messages: Sequence[LatexMessage],
|
|
564
|
+
open_log: bool,
|
|
565
|
+
) -> None:
|
|
566
|
+
errors = [msg for msg in messages if msg.severity is LatexMessageSeverity.ERROR]
|
|
567
|
+
warnings = [msg for msg in messages if msg.severity is LatexMessageSeverity.WARNING]
|
|
568
|
+
rows: list[tuple[str, str]] = []
|
|
569
|
+
|
|
570
|
+
primary_entry: str | None = None
|
|
571
|
+
if errors:
|
|
572
|
+
primary_entry = _format_message_entry(errors[0])
|
|
573
|
+
elif warnings:
|
|
574
|
+
primary_entry = _format_message_entry(warnings[-1])
|
|
575
|
+
else:
|
|
576
|
+
parsed = parse_latex_log(log_path)
|
|
577
|
+
if parsed:
|
|
578
|
+
primary_entry = _format_message_entry(parsed[0])
|
|
579
|
+
|
|
580
|
+
if primary_entry:
|
|
581
|
+
label = "Primary error" if errors or not warnings else "Last warning"
|
|
582
|
+
rows.append((label, primary_entry))
|
|
583
|
+
|
|
584
|
+
rows.append(("Log file", _format_path(log_path)))
|
|
585
|
+
rows.append(("Next steps", "Inspect the log or re-run with --classic-output"))
|
|
586
|
+
|
|
587
|
+
_render_failure_panel(state, "LaTeX failure", rows)
|
|
588
|
+
|
|
589
|
+
if open_log and log_path.exists(): # pragma: no cover - depends on platform
|
|
590
|
+
with contextlib.suppress(Exception):
|
|
591
|
+
typer.launch(str(log_path))
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def consume_event_diagnostics(state: CLIState) -> list[str]:
|
|
595
|
+
verbosity = state.verbosity
|
|
596
|
+
if verbosity <= 0 or not state.events:
|
|
597
|
+
state.events.clear()
|
|
598
|
+
return []
|
|
599
|
+
|
|
600
|
+
output_lines: list[str] = []
|
|
601
|
+
|
|
602
|
+
if verbosity >= 1:
|
|
603
|
+
slot_events = state.events.get("slot_assignments", [])
|
|
604
|
+
for event in slot_events:
|
|
605
|
+
assignments = event.get("entries", [])
|
|
606
|
+
if not assignments:
|
|
607
|
+
continue
|
|
608
|
+
output_lines.append("Slot assignments:")
|
|
609
|
+
for assignment in assignments:
|
|
610
|
+
document = assignment.get("document", "")
|
|
611
|
+
slot = assignment.get("slot", "")
|
|
612
|
+
selector = assignment.get("selector") or "*"
|
|
613
|
+
include = "include" if assignment.get("include_document") else "extract"
|
|
614
|
+
output_lines.append(f" - {slot} ← {document} ({selector}, {include})")
|
|
615
|
+
|
|
616
|
+
for event in state.events.get("doi_fetch", []):
|
|
617
|
+
value = event.get("value")
|
|
618
|
+
key = event.get("key")
|
|
619
|
+
output_lines.append(f"Fetched DOI {value} for entry '{key}'")
|
|
620
|
+
|
|
621
|
+
if verbosity >= 2:
|
|
622
|
+
for event in state.events.get("parser_fallback", []):
|
|
623
|
+
preferred = event.get("preferred", "unknown")
|
|
624
|
+
fallback = event.get("fallback", "unknown")
|
|
625
|
+
output_lines.append(f"Parser fallback: {preferred} → {fallback}")
|
|
626
|
+
|
|
627
|
+
for event in state.events.get("template_overrides", []):
|
|
628
|
+
overrides = event.get("values", {})
|
|
629
|
+
if overrides:
|
|
630
|
+
output_lines.append("Template overrides:")
|
|
631
|
+
for key, value in sorted(overrides.items()):
|
|
632
|
+
output_lines.append(f" - {key}: {value}")
|
|
633
|
+
|
|
634
|
+
for event in state.events.get("conversion_settings", []):
|
|
635
|
+
parser = event.get("parser", "auto")
|
|
636
|
+
copy_assets = event.get("copy_assets")
|
|
637
|
+
manifest = event.get("manifest")
|
|
638
|
+
fallback_enabled = event.get("fallback_converters_enabled")
|
|
639
|
+
output_lines.append(
|
|
640
|
+
f"Settings: parser={parser}, copy_assets={copy_assets}, manifest={manifest}, fallback_converters={fallback_enabled}"
|
|
641
|
+
)
|
|
642
|
+
for event in state.events.get("font_requirements", []):
|
|
643
|
+
required = event.get("required", [])
|
|
644
|
+
missing = event.get("missing", [])
|
|
645
|
+
present = event.get("present", [])
|
|
646
|
+
if missing:
|
|
647
|
+
output_lines.append(f"Font gaps: {', '.join(missing)}")
|
|
648
|
+
output_lines.append(f"Font fallbacks: {', '.join(required) or '<none>'}")
|
|
649
|
+
if present:
|
|
650
|
+
output_lines.append(f"Detected locally: {', '.join(present)}")
|
|
651
|
+
|
|
652
|
+
state.events.clear()
|
|
653
|
+
return output_lines
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
__all__ = [
|
|
657
|
+
"consume_event_diagnostics",
|
|
658
|
+
"present_build_summary",
|
|
659
|
+
"present_conversion_summary",
|
|
660
|
+
"present_html_summary",
|
|
661
|
+
"present_latex_failure",
|
|
662
|
+
"present_rule_descriptions",
|
|
663
|
+
]
|