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,303 @@
1
+ """Helpers for loading template instances from disk or entry points."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable, Mapping
6
+ from importlib import metadata
7
+ import importlib.util
8
+ from pathlib import Path
9
+ import shutil
10
+ from typing import Any
11
+
12
+ from texsmith.core.user_dir import get_user_dir
13
+
14
+ from .base import WrappableTemplate, load_specialised_template
15
+ from .builtins import iter_builtin_templates, load_builtin_template
16
+ from .manifest import TemplateError
17
+
18
+
19
+ def _looks_like_template_root(path: Path) -> bool:
20
+ """Return True when ``path`` contains manifest or specialised template markers."""
21
+ def _safe_exists(candidate: Path) -> bool:
22
+ try:
23
+ return candidate.exists()
24
+ except OSError:
25
+ return False
26
+
27
+ if not _safe_exists(path) or path.is_file():
28
+ return False
29
+ if _safe_exists(path / "__init__.py"):
30
+ return True
31
+ manifest_candidates = (path / "manifest.toml", path / "template" / "manifest.toml")
32
+ return any(_safe_exists(candidate) for candidate in manifest_candidates)
33
+
34
+
35
+ def load_template(identifier: str) -> WrappableTemplate:
36
+ """Load a template selected by name or filesystem path."""
37
+ path_candidate = Path(identifier).expanduser()
38
+ looks_like_path = (
39
+ path_candidate.is_absolute()
40
+ or "/" in identifier
41
+ or "\\" in identifier
42
+ or identifier.startswith(".")
43
+ )
44
+ if looks_like_path and path_candidate.exists():
45
+ return _load_path_template(path_candidate)
46
+
47
+ slug = _slug_from_identifier(identifier)
48
+
49
+ builtin = load_builtin_template(identifier)
50
+ if builtin is not None:
51
+ return builtin
52
+
53
+ packaged_root = _resolve_packaged_template_root(slug)
54
+ if packaged_root is not None:
55
+ return _load_path_template(packaged_root)
56
+
57
+ for candidate in _iter_local_candidates(slug):
58
+ if _looks_like_template_root(candidate):
59
+ return _load_path_template(candidate)
60
+
61
+ home_candidate = _home_template_candidate(slug)
62
+ if home_candidate is not None and _looks_like_template_root(home_candidate):
63
+ return _load_path_template(home_candidate)
64
+
65
+ raise TemplateError(
66
+ f"Unable to load template '{identifier}'. Provide a valid path or "
67
+ "install a package exposing a 'texsmith.templates' entry point."
68
+ )
69
+
70
+
71
+ def copy_template_assets(
72
+ template: WrappableTemplate,
73
+ output_dir: Path,
74
+ *,
75
+ context: Mapping[str, Any] | None = None,
76
+ overrides: Mapping[str, Any] | None = None,
77
+ assets: Iterable["ResolvedAsset"] | None = None,
78
+ ) -> list[Path]:
79
+ """Copy the template declared assets into the selected output directory."""
80
+ output_dir = Path(output_dir).resolve()
81
+ output_dir.mkdir(parents=True, exist_ok=True)
82
+
83
+ if context is None:
84
+ render_context = template.prepare_context("", overrides=overrides)
85
+ else:
86
+ render_context = dict(context)
87
+
88
+ written: list[Path] = []
89
+ asset_entries = list(assets) if assets is not None else list(template.iter_assets())
90
+ for asset in asset_entries:
91
+ destination_path = (output_dir / asset.destination).resolve()
92
+
93
+ if asset.template:
94
+ if asset.template_name is None: # pragma: no cover - defensive
95
+ raise TemplateError(
96
+ f"Templated asset '{asset.source}' is missing template metadata."
97
+ )
98
+ destination_path.parent.mkdir(parents=True, exist_ok=True)
99
+ rendered = template.render_template(asset.template_name, **render_context)
100
+ destination_path.write_text(
101
+ rendered,
102
+ encoding=asset.encoding or "utf-8",
103
+ )
104
+ elif asset.source.is_dir():
105
+ shutil.copytree(asset.source, destination_path, dirs_exist_ok=True)
106
+ else:
107
+ destination_path.parent.mkdir(parents=True, exist_ok=True)
108
+ shutil.copy2(asset.source, destination_path)
109
+
110
+ written.append(destination_path)
111
+
112
+ return written
113
+
114
+
115
+ def _load_path_template(path: Path) -> WrappableTemplate:
116
+ specialised = load_specialised_template(path)
117
+ if specialised is not None:
118
+ return specialised
119
+ return WrappableTemplate(path)
120
+
121
+
122
+ def _match_entry_points(
123
+ entry_points: Iterable[metadata.EntryPoint],
124
+ name: str,
125
+ ) -> Iterable[metadata.EntryPoint]:
126
+ for entry_point in entry_points:
127
+ if entry_point.name == name:
128
+ yield entry_point
129
+
130
+
131
+ def _coerce_template(value: Any) -> WrappableTemplate | None:
132
+ """Coerce an entry point payload into a ``WrappableTemplate`` instance."""
133
+ from .base import _coerce_template as base_coerce_template
134
+
135
+ return base_coerce_template(value)
136
+
137
+
138
+ def _slug_from_identifier(identifier: str) -> str:
139
+ path = Path(identifier)
140
+ slug = path.name
141
+ if slug in {".", ""}:
142
+ slug = identifier.strip("./")
143
+ return slug
144
+
145
+
146
+ def _iter_local_candidates(slug: str | None) -> Iterable[Path]:
147
+ """Yield template candidates from the current directory and its parents."""
148
+ cwd = Path.cwd().resolve()
149
+ visited_roots: set[Path] = set()
150
+ current = cwd
151
+ while current not in visited_roots:
152
+ visited_roots.add(current)
153
+ templates_root = current / "templates"
154
+ roots = (current, templates_root)
155
+ if not slug:
156
+ for root in roots:
157
+ if not root.exists():
158
+ continue
159
+ for child in root.iterdir():
160
+ if child.is_dir():
161
+ yield child
162
+ else:
163
+ distribution_name = f"texsmith-template-{slug}"
164
+ package_name = f"texsmith_template_{slug}"
165
+ for root in roots:
166
+ for candidate in (
167
+ root / slug,
168
+ root / package_name,
169
+ root / distribution_name / package_name,
170
+ root / distribution_name,
171
+ ):
172
+ yield candidate
173
+ if current.parent == current:
174
+ break
175
+ current = current.parent
176
+
177
+
178
+ def _resolve_packaged_template_root(slug: str) -> Path | None:
179
+ """Return a template root provided by an installed Python package."""
180
+ entry_points = metadata.entry_points()
181
+ group = entry_points.select(group="texsmith.templates")
182
+ for entry_point in _match_entry_points(group, slug):
183
+ loaded = entry_point.load()
184
+ template = _coerce_template(loaded)
185
+ if template is not None:
186
+ return template.root
187
+
188
+ candidate_modules = [
189
+ f"texsmith_template_{slug}",
190
+ f"texsmith-template-{slug}",
191
+ slug,
192
+ ]
193
+ for module_name in candidate_modules:
194
+ spec = importlib.util.find_spec(module_name)
195
+ if spec is None or spec.origin is None:
196
+ continue
197
+ origin_path = Path(spec.origin).resolve()
198
+ module_root = origin_path.parent if origin_path.name == "__init__.py" else origin_path
199
+ if _looks_like_template_root(module_root):
200
+ return module_root
201
+ return None
202
+
203
+
204
+ def _home_template_candidate(slug: str) -> Path | None:
205
+ """Return a template path under ~/.texsmith/templates if present."""
206
+ home_root = get_user_dir().data_dir("templates", create=False)
207
+ distribution_name = f"texsmith-template-{slug}"
208
+ package_name = f"texsmith_template_{slug}"
209
+ for candidate in (
210
+ home_root / slug,
211
+ home_root / package_name,
212
+ home_root / distribution_name / package_name,
213
+ home_root / distribution_name,
214
+ ):
215
+ if candidate.exists():
216
+ return candidate
217
+ return None
218
+
219
+
220
+ def discover_templates() -> list[dict[str, str]]:
221
+ """Return available templates in discovery order."""
222
+ entries: list[dict[str, str]] = []
223
+
224
+ def _record(origin: str, name: str, root: Path) -> None:
225
+ entries.append({"name": name, "origin": origin, "root": str(root.resolve())})
226
+
227
+ for slug in iter_builtin_templates():
228
+ try:
229
+ template = load_builtin_template(slug)
230
+ except TemplateError:
231
+ continue
232
+ if template is not None:
233
+ _record("builtin", slug, template.root)
234
+
235
+ try:
236
+ for dist in metadata.distributions():
237
+ name = dist.metadata.get("Name", "")
238
+ if not name.lower().startswith("texsmith-template-"):
239
+ continue
240
+ slug = name[len("texsmith-template-") :]
241
+ root = _resolve_packaged_template_root(slug)
242
+ if root is None:
243
+ continue
244
+ _record("package", slug, root)
245
+ except Exception:
246
+ pass
247
+
248
+ seen_locals: set[str] = set()
249
+ for candidate in _iter_local_candidates(""):
250
+ if not _looks_like_template_root(candidate):
251
+ continue
252
+ key = str(candidate.resolve())
253
+ if key in seen_locals:
254
+ continue
255
+ seen_locals.add(key)
256
+ _record("local", candidate.name, candidate)
257
+
258
+ home_root = get_user_dir().data_dir("templates", create=False)
259
+ if home_root.exists():
260
+ for child in sorted(home_root.iterdir()):
261
+ if child.is_dir() and _looks_like_template_root(child):
262
+ _record("home", child.name, child)
263
+
264
+ return sorted(entries, key=lambda entry: (entry["origin"], entry["name"]))
265
+
266
+ for candidate in _iter_local_candidates("*placeholder*"):
267
+ pass
268
+
269
+ local_candidates: list[Path] = []
270
+ for slug in set():
271
+ pass
272
+
273
+ for candidate in _iter_local_candidates("*placeholder*"):
274
+ pass
275
+
276
+ def _record(origin: str, name: str, root: Path) -> None:
277
+ key = (name, str(root))
278
+ if key in seen_paths:
279
+ return
280
+ seen_paths.add(key)
281
+ entries.append({"name": name, "origin": origin, "root": str(root)})
282
+
283
+ # Re-run local with concrete names.
284
+ visited: set[str] = set()
285
+ cwd = Path.cwd().resolve()
286
+ for candidate in _iter_local_candidates(""):
287
+ if _looks_like_template_root(candidate):
288
+ _record("local", candidate.name, candidate)
289
+
290
+ home_root = get_user_dir().data_dir("templates", create=False)
291
+ if home_root.exists():
292
+ for child in home_root.iterdir():
293
+ if child.is_dir() and _looks_like_template_root(child):
294
+ _record("home", child.name, child)
295
+
296
+ return sorted(entries, key=lambda entry: (entry["origin"], entry["name"]))
297
+
298
+
299
+ __all__ = [
300
+ "copy_template_assets",
301
+ "discover_templates",
302
+ "load_template",
303
+ ]