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,300 @@
1
+ """Bibliography-related CLI helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import Counter
6
+ from collections.abc import Iterable, Mapping, Sequence
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING, Any, cast
9
+
10
+ import typer
11
+
12
+ from texsmith.core.bibliography import BibliographyCollection
13
+
14
+ from .state import ensure_rich_compat, get_cli_state
15
+
16
+
17
+ if TYPE_CHECKING:
18
+ from rich.panel import Panel
19
+
20
+
21
+ def _iterable_items(value: object) -> Iterable[object]:
22
+ """Normalize a value into an iterable, treating strings as scalars.
23
+
24
+ This helper ensures consistent processing of bibliography fields, which may be
25
+ single values (strings) or lists of values, by wrapping single items in a tuple.
26
+ """
27
+ if isinstance(value, Iterable) and not isinstance(value, (str, bytes)):
28
+ return value
29
+ return ()
30
+
31
+
32
+ def format_bibliography_person(person: Mapping[str, object]) -> str:
33
+ """Render a bibliography person dictionary into a readable string."""
34
+ parts: list[str] = []
35
+ for field in ("first", "middle", "prelast", "last", "lineage"):
36
+ value = person.get(field)
37
+ if isinstance(value, str):
38
+ text = value.strip()
39
+ if text:
40
+ parts.append(text)
41
+ continue
42
+ for segment in _iterable_items(value):
43
+ if segment:
44
+ parts.append(str(segment))
45
+
46
+ text = " ".join(part for part in parts if part)
47
+ if text:
48
+ return text
49
+ fallback = person.get("text")
50
+ return str(fallback).strip() if isinstance(fallback, str) else ""
51
+
52
+
53
+ def format_person_list(persons: Iterable[Mapping[str, object]]) -> str:
54
+ """Join a sequence of person dictionaries into a comma-separated string."""
55
+ names = [format_bibliography_person(person) for person in persons]
56
+ return ", ".join(name for name in names if name)
57
+
58
+
59
+ def build_reference_panel(reference: Mapping[str, object]) -> Panel:
60
+ """Create a Rich panel that visualises a single bibliography entry."""
61
+ from rich import box
62
+ from rich.panel import Panel
63
+ from rich.table import Table
64
+
65
+ raw_fields = reference.get("fields")
66
+ fields: dict[str, Any]
67
+ if isinstance(raw_fields, Mapping):
68
+ fields = {str(key): value for key, value in raw_fields.items()}
69
+ else:
70
+ fields = {}
71
+ grid = Table.grid(padding=(0, 1))
72
+ grid.add_column(style="bold green", no_wrap=True)
73
+ grid.add_column()
74
+
75
+ def _pop_field(*keys: str) -> str | None:
76
+ for key in keys:
77
+ value = fields.pop(key, None)
78
+ if isinstance(value, bytes):
79
+ text = value.decode("utf-8", errors="ignore").strip()
80
+ if text:
81
+ return text
82
+ elif isinstance(value, str):
83
+ text = value.strip()
84
+ if text:
85
+ return text
86
+ elif value is not None:
87
+ return str(value)
88
+ return None
89
+
90
+ def _add_field(label: str, value: object) -> None:
91
+ if value is None:
92
+ return
93
+ if isinstance(value, str) and not value.strip():
94
+ return
95
+ grid.add_row(label, str(value))
96
+
97
+ title = _pop_field("title")
98
+ _add_field("Title", title)
99
+
100
+ year = _pop_field("year")
101
+ _add_field("Year", year)
102
+
103
+ journal = _pop_field("journal", "booktitle")
104
+ _add_field("Journal", journal)
105
+
106
+ persons_block = reference.get("persons")
107
+ if isinstance(persons_block, Mapping):
108
+ authors = persons_block.get("author")
109
+ if isinstance(authors, Iterable):
110
+ cast_authors = cast(Sequence[Mapping[str, object]], tuple(authors))
111
+ _add_field("Authors", format_person_list(cast_authors))
112
+
113
+ sources = [
114
+ str(Path(str(path))) for path in _iterable_items(reference.get("source_files")) if path
115
+ ]
116
+ if sources:
117
+ formatted_sources = ", ".join(sources)
118
+ _add_field("Sources", formatted_sources)
119
+
120
+ for key, value in sorted(fields.items()):
121
+ _add_field(key.title(), value)
122
+
123
+ key = str(reference.get("key", "Reference"))
124
+ entry_type = str(reference.get("type", "reference"))
125
+ title = f"{key} ({entry_type})"
126
+ return Panel(grid, title=title, box=box.SIMPLE)
127
+
128
+
129
+ def print_bibliography_overview(collection: BibliographyCollection) -> None:
130
+ """Render a formatted summary of bibliography files, issues, and entries."""
131
+ ensure_rich_compat()
132
+ try:
133
+ from rich import box
134
+ from rich.table import Table
135
+ from rich.text import Text
136
+ except ImportError: # pragma: no cover - fallback when Rich is unavailable
137
+ _print_bibliography_plain(collection)
138
+ return
139
+
140
+ console = get_cli_state().console
141
+ references = collection.list_references()
142
+
143
+ def _summarise_sources() -> tuple[Counter[Path], int, int, int]:
144
+ per_file: Counter[Path] = Counter()
145
+ frontmatter = 0
146
+ doi = 0
147
+ for reference in references:
148
+ raw_sources = reference.get("source_files") or []
149
+ sources = {Path(str(src)) for src in raw_sources if src}
150
+ if not sources:
151
+ continue
152
+ for src in sources:
153
+ per_file[src] += 1
154
+ names = {src.name.lower() for src in sources}
155
+ if any("frontmatter" in name for name in names):
156
+ frontmatter += 1
157
+ if any("doi" in name for name in names):
158
+ doi += 1
159
+ total = len(references)
160
+ return per_file, total, frontmatter, doi
161
+
162
+ per_file_counts, total_entries, frontmatter_entries, doi_entries = _summarise_sources()
163
+
164
+ stats = collection.file_stats
165
+ if stats:
166
+ stats_table = Table(
167
+ title="Bibliography Files",
168
+ box=box.SQUARE,
169
+ show_edge=True,
170
+ header_style="bold cyan",
171
+ )
172
+ stats_table.add_column("File", overflow="fold")
173
+ stats_table.add_column("Entries", justify="right")
174
+ for file_path, entry_count in stats:
175
+ stats_table.add_row(str(file_path), str(entry_count))
176
+ stats_table.add_row(
177
+ Text("Total", style="bold"), Text(str(sum(count for _, count in stats)))
178
+ )
179
+ console.print(stats_table)
180
+
181
+ if collection.issues:
182
+ issue_table = Table(
183
+ title="Warnings",
184
+ box=box.SQUARE,
185
+ header_style="bold cyan",
186
+ show_edge=True,
187
+ )
188
+ issue_table.add_column("Key", style="yellow", no_wrap=True)
189
+ issue_table.add_column("Message", style="yellow")
190
+ issue_table.add_column("Sources", style="yellow")
191
+ for issue in collection.issues:
192
+ issue_table.add_row(
193
+ issue.key or "—",
194
+ issue.message,
195
+ str(issue.source) if issue.source else "—",
196
+ )
197
+ console.print(issue_table)
198
+
199
+ if not references:
200
+ console.print("[dim]No references found.[/]")
201
+ summary_table = Table(
202
+ title="Bibliography Summary",
203
+ box=box.SQUARE,
204
+ header_style="bold cyan",
205
+ show_edge=True,
206
+ )
207
+ summary_table.add_column("Category", style="bold")
208
+ summary_table.add_column("Count", justify="right")
209
+ summary_table.add_row("Total entries", str(total_entries))
210
+ if per_file_counts:
211
+ for path, count in per_file_counts.most_common():
212
+ summary_table.add_row(f"From {path.name}", str(count))
213
+ summary_table.add_row("From front matter", str(frontmatter_entries))
214
+ summary_table.add_row("From DOI fetches", str(doi_entries))
215
+ console.print(summary_table)
216
+ return
217
+
218
+ for reference in references:
219
+ panel = build_reference_panel(reference)
220
+ console.print(panel)
221
+ console.print()
222
+
223
+ summary_table = Table(
224
+ title="Bibliography Summary",
225
+ box=box.SQUARE,
226
+ header_style="bold cyan",
227
+ show_edge=True,
228
+ )
229
+ summary_table.add_column("Category", style="bold")
230
+ summary_table.add_column("Count", justify="right")
231
+ summary_table.add_row("Total entries", str(total_entries))
232
+ if per_file_counts:
233
+ for path, count in per_file_counts.most_common():
234
+ summary_table.add_row(f"From {path.name}", str(count))
235
+ summary_table.add_row("From front matter", str(frontmatter_entries))
236
+ summary_table.add_row("From DOI fetches", str(doi_entries))
237
+ console.print(summary_table)
238
+
239
+
240
+ def _print_bibliography_plain(collection: BibliographyCollection) -> None:
241
+ """Simple text fallback when Rich is not available."""
242
+ typer.echo("Bibliography Files:")
243
+ stats = collection.file_stats
244
+ if stats:
245
+ for file_path, entry_count in stats:
246
+ typer.echo(f" - {file_path} ({entry_count} entries)")
247
+ else:
248
+ typer.echo(" - (none)")
249
+
250
+ if collection.issues:
251
+ typer.echo("Warnings:")
252
+ for issue in collection.issues:
253
+ key = issue.key or "-"
254
+ source = str(issue.source) if issue.source else "-"
255
+ typer.echo(f" - [{key}] {issue.message} (source: {source})")
256
+
257
+ references = collection.list_references()
258
+ total = len(references)
259
+ per_file: Counter[Path] = Counter()
260
+ frontmatter = 0
261
+ doi = 0
262
+ for reference in references:
263
+ sources = {Path(str(src)) for src in reference.get("source_files") or [] if src}
264
+ for src in sources:
265
+ per_file[src] += 1
266
+ names = {src.name.lower() for src in sources}
267
+ if any("frontmatter" in name for name in names):
268
+ frontmatter += 1
269
+ if any("doi" in name for name in names):
270
+ doi += 1
271
+
272
+ typer.echo("Entries:")
273
+ for reference in references:
274
+ key = reference.get("key", "Reference")
275
+ entry_type = reference.get("type", "reference")
276
+ typer.echo(f"- {key} ({entry_type})")
277
+ persons_block = reference.get("persons")
278
+ if isinstance(persons_block, Mapping):
279
+ authors = persons_block.get("author")
280
+ if isinstance(authors, Iterable):
281
+ cast_authors = cast(Sequence[Mapping[str, object]], tuple(authors))
282
+ typer.echo(f" Authors: {format_person_list(cast_authors)}")
283
+ fields = reference.get("fields")
284
+ if isinstance(fields, Mapping):
285
+ for field_key, field_value in sorted(fields.items()):
286
+ typer.echo(f" {field_key.title()}: {field_value}")
287
+ sources = reference.get("source_files")
288
+ entries = [str(Path(str(path))) for path in _iterable_items(sources) if path]
289
+ if entries:
290
+ typer.echo(f" Sources: {', '.join(entries)}")
291
+ typer.echo()
292
+
293
+ typer.echo("Summary:")
294
+ typer.echo(f" Total entries: {total}")
295
+ if per_file:
296
+ typer.echo(" Per-source counts:")
297
+ for path, count in per_file.most_common():
298
+ typer.echo(f" - {path}: {count}")
299
+ typer.echo(f" From front matter: {frontmatter}")
300
+ typer.echo(f" From DOI fetches: {doi}")
@@ -0,0 +1,14 @@
1
+ """CLI command implementations exposed via `texsmith.ui.cli`.
2
+
3
+ This module exists primarily to make the `texsmith.ui.cli.commands` package
4
+ importable for documentation tools such as mkdocstrings. It re-exports the
5
+ Typer command factories defined in the sibling modules so downstream code can
6
+ import them using dotted paths (e.g. ``texsmith.ui.cli.commands.render``).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from .render import render
12
+
13
+
14
+ __all__ = ["render"]