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,66 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Any, ClassVar
7
+
8
+ from texsmith.core.fragments.base import BaseFragment, FragmentPiece
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class IndexConfig:
13
+ has_index: bool
14
+
15
+ @classmethod
16
+ def from_context(cls, context: Mapping[str, Any]) -> IndexConfig:
17
+ has_flag = context.get("has_index")
18
+ entries = context.get("index_terms")
19
+ return cls(has_index=bool(has_flag) or bool(entries))
20
+
21
+ def inject_into(self, context: dict[str, Any]) -> None:
22
+ context["ts_index_enabled"] = self.has_index
23
+
24
+
25
+ class IndexFragment(BaseFragment[IndexConfig]):
26
+ name: ClassVar[str] = "ts-index"
27
+ description: ClassVar[str] = "Index helpers with backmatter insertion."
28
+ pieces: ClassVar[list[FragmentPiece]] = [
29
+ FragmentPiece(
30
+ template_path=Path(__file__).with_name("ts-index.jinja.sty"),
31
+ kind="package",
32
+ slot="extra_packages",
33
+ ),
34
+ FragmentPiece(
35
+ template_path=Path(__file__).with_name("ts-index-backmatter.jinja.tex"),
36
+ kind="inline",
37
+ slot="fragment_backmatter",
38
+ ),
39
+ ]
40
+ attributes: ClassVar[dict[str, Any]] = {}
41
+ config_cls: ClassVar[type[IndexConfig]] = IndexConfig
42
+ source: ClassVar[Path] = Path(__file__).with_name("ts-index.jinja.sty")
43
+ context_defaults: ClassVar[dict[str, Any]] = {"extra_packages": "", "fragment_backmatter": ""}
44
+
45
+ def build_config(
46
+ self, context: Mapping[str, Any], overrides: Mapping[str, Any] | None = None
47
+ ) -> IndexConfig:
48
+ _ = overrides
49
+ return self.config_cls.from_context(context)
50
+
51
+ def inject(
52
+ self,
53
+ config: IndexConfig,
54
+ context: dict[str, Any],
55
+ overrides: Mapping[str, Any] | None = None,
56
+ ) -> None:
57
+ _ = overrides
58
+ config.inject_into(context)
59
+
60
+ def should_render(self, config: IndexConfig) -> bool:
61
+ return config.has_index
62
+
63
+
64
+ fragment = IndexFragment()
65
+
66
+ __all__ = ["IndexConfig", "IndexFragment", "fragment"]
@@ -0,0 +1,3 @@
1
+ name = "ts-index"
2
+ description = "Index helpers with backmatter insertion."
3
+ entrypoint = "texsmith.fragments.index:fragment"
@@ -0,0 +1,3 @@
1
+ \BLOCK{ if index_entries or has_index }
2
+ \printindex
3
+ \BLOCK{ endif }
@@ -0,0 +1,12 @@
1
+ \ProvidesPackage{ts-index}[2025/11/21 TeXSmith index helpers (generated)]
2
+
3
+ \BLOCK{ set resolved_index_engine = index_engine|default("makeindex")|lower }
4
+ \BLOCK{ if resolved_index_engine in ["texindy", "pyxindy"] }
5
+ \RequirePackage[xindy]{imakeidx}
6
+ \BLOCK{ else }
7
+ \RequirePackage{imakeidx}
8
+ \BLOCK{ endif }
9
+
10
+ \makeindex
11
+
12
+ \endinput
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Any, ClassVar
7
+
8
+ from texsmith.core.fragments.base import BaseFragment, FragmentPiece
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class KeystrokesConfig:
13
+ uses_keystrokes: bool
14
+
15
+ @classmethod
16
+ def from_context(cls, context: Mapping[str, Any]) -> KeystrokesConfig:
17
+ return cls(uses_keystrokes=_detect_keystrokes(context))
18
+
19
+ def inject_into(self, context: dict[str, Any]) -> None:
20
+ context["ts_keystrokes_enabled"] = self.uses_keystrokes
21
+
22
+
23
+ class KeystrokesFragment(BaseFragment[KeystrokesConfig]):
24
+ name: ClassVar[str] = "ts-keystrokes"
25
+ description: ClassVar[str] = "Keyboard shortcut rendering helpers loaded only when needed."
26
+ pieces: ClassVar[list[FragmentPiece]] = [
27
+ FragmentPiece(
28
+ template_path=Path(__file__).with_name("ts-keystrokes.jinja.sty"),
29
+ kind="package",
30
+ slot="extra_packages",
31
+ )
32
+ ]
33
+ attributes: ClassVar[dict[str, Any]] = {}
34
+ config_cls: ClassVar[type[KeystrokesConfig]] = KeystrokesConfig
35
+ source: ClassVar[Path] = Path(__file__).with_name("ts-keystrokes.jinja.sty")
36
+ context_defaults: ClassVar[dict[str, Any]] = {"extra_packages": ""}
37
+
38
+ def build_config(
39
+ self, context: Mapping[str, Any], overrides: Mapping[str, Any] | None = None
40
+ ) -> KeystrokesConfig:
41
+ _ = overrides
42
+ return self.config_cls.from_context(context)
43
+
44
+ def inject(
45
+ self,
46
+ config: KeystrokesConfig,
47
+ context: dict[str, Any],
48
+ overrides: Mapping[str, Any] | None = None,
49
+ ) -> None:
50
+ _ = overrides
51
+ config.inject_into(context)
52
+
53
+ def should_render(self, config: KeystrokesConfig) -> bool:
54
+ return config.uses_keystrokes
55
+
56
+
57
+ def _detect_keystrokes(context: Mapping[str, Any]) -> bool:
58
+ tokens = ("\\keystroke{", "\\keystrokes{")
59
+ for value in context.values():
60
+ if not isinstance(value, str):
61
+ continue
62
+ if any(token in value for token in tokens):
63
+ return True
64
+ return False
65
+
66
+
67
+ fragment = KeystrokesFragment()
68
+
69
+ __all__ = ["KeystrokesConfig", "KeystrokesFragment", "fragment"]
@@ -0,0 +1,3 @@
1
+ name = "ts-keystrokes"
2
+ description = "Conditional keystroke helpers."
3
+ entrypoint = "texsmith.fragments.keystrokes:fragment"
@@ -0,0 +1,20 @@
1
+ \NeedsTeXFormat{LaTeX2e}
2
+ \ProvidesPackage{ts-keystrokes}[2025/11/21 TeXSmith keystroke helpers]
3
+
4
+ \RequirePackage{tikz}
5
+ \usetikzlibrary{shadows}
6
+
7
+ \newcommand*\keystroke[1]{%
8
+ \tikz[baseline=(key.base)]%
9
+ \node[%
10
+ draw,
11
+ fill=gray!10,
12
+ drop shadow={shadow xshift=0.25ex,shadow yshift=-0.25ex,fill=black!70,opacity=0.75},
13
+ rectangle,
14
+ rounded corners=1.5pt,
15
+ inner ysep=0.1em,
16
+ inner xsep=0.5em,
17
+ line width=0.5pt,
18
+ font=\scriptsize\sffamily
19
+ ](key) {#1\strut};%
20
+ }
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Any, ClassVar
7
+
8
+ from texsmith.core.fragments.base import BaseFragment, FragmentPiece
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class TodolistConfig:
13
+ uses_todolist: bool
14
+
15
+ @classmethod
16
+ def from_context(cls, context: Mapping[str, Any]) -> TodolistConfig:
17
+ return cls(uses_todolist=_detect_todolist(context))
18
+
19
+ def inject_into(self, context: dict[str, Any]) -> None:
20
+ context["ts_todolist_enabled"] = self.uses_todolist
21
+
22
+
23
+ class TodolistFragment(BaseFragment[TodolistConfig]):
24
+ name: ClassVar[str] = "ts-todolist"
25
+ description: ClassVar[str] = (
26
+ "Todolist helper commands loaded when checklist macros are present."
27
+ )
28
+ pieces: ClassVar[list[FragmentPiece]] = [
29
+ FragmentPiece(
30
+ template_path=Path(__file__).with_name("ts-todolist.jinja.sty"),
31
+ kind="package",
32
+ slot="extra_packages",
33
+ )
34
+ ]
35
+ attributes: ClassVar[dict[str, Any]] = {}
36
+ config_cls: ClassVar[type[TodolistConfig]] = TodolistConfig
37
+ source: ClassVar[Path] = Path(__file__).with_name("ts-todolist.jinja.sty")
38
+ context_defaults: ClassVar[dict[str, Any]] = {"extra_packages": ""}
39
+
40
+ def build_config(
41
+ self, context: Mapping[str, Any], overrides: Mapping[str, Any] | None = None
42
+ ) -> TodolistConfig:
43
+ _ = overrides
44
+ return self.config_cls.from_context(context)
45
+
46
+ def inject(
47
+ self,
48
+ config: TodolistConfig,
49
+ context: dict[str, Any],
50
+ overrides: Mapping[str, Any] | None = None,
51
+ ) -> None:
52
+ _ = overrides
53
+ config.inject_into(context)
54
+
55
+ def should_render(self, config: TodolistConfig) -> bool:
56
+ return config.uses_todolist
57
+
58
+
59
+ def _detect_todolist(context: Mapping[str, Any]) -> bool:
60
+ tokens = ("\\done", "\\wontfix", "\\begin{todolist}", "\\todolist")
61
+ for value in context.values():
62
+ if not isinstance(value, str):
63
+ continue
64
+ if any(token in value for token in tokens):
65
+ return True
66
+ return False
67
+
68
+
69
+ fragment = TodolistFragment()
70
+
71
+ __all__ = ["TodolistConfig", "TodolistFragment", "fragment"]
@@ -0,0 +1,3 @@
1
+ name = "ts-todolist"
2
+ description = "Conditional todolist helpers."
3
+ entrypoint = "texsmith.fragments.todolist:fragment"
@@ -0,0 +1,21 @@
1
+ \NeedsTeXFormat{LaTeX2e}
2
+ \ProvidesPackage{ts-todolist}[2025/11/21 TeXSmith todolist helpers]
3
+
4
+ \RequirePackage{enumitem}
5
+ \RequirePackage{amssymb}
6
+ \RequirePackage{pifont}
7
+
8
+ \newlist{todolist}{itemize}{2}
9
+ \setlist[todolist]{label=$\square$, leftmargin=*}
10
+
11
+ \newcommand{\cmark}{\ding{51}}% check mark
12
+ \newcommand{\xmark}{\ding{55}}% cross mark
13
+
14
+ \newcommand{\done}{%
15
+ \rlap{$\square$}{\raisebox{2pt}{\large\hspace{1pt}\cmark}}%
16
+ \hspace{-2.5pt}%
17
+ }
18
+
19
+ \newcommand{\wontfix}{%
20
+ \rlap{$\square$}{\large\hspace{1pt}\xmark}%
21
+ }
@@ -0,0 +1,214 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Any, ClassVar
7
+
8
+ from texsmith.core.fragments.base import BaseFragment, FragmentPiece
9
+ from texsmith.core.templates.manifest import TemplateAttributeSpec, TemplateError
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class TypesettingConfig:
14
+ indent_mode: str | None
15
+ parskip: str | None
16
+ leading_mode: str | None
17
+ leading_value: str | None
18
+ lineno_enabled: bool
19
+
20
+ @classmethod
21
+ def from_context(cls, context: Mapping[str, Any]) -> TypesettingConfig:
22
+ paragraph = context.get("typesetting_paragraph")
23
+ leading = context.get("typesetting_leading")
24
+ lineno = bool(context.get("typesetting_lineno"))
25
+
26
+ indent_mode, parskip = _normalise_paragraph(paragraph)
27
+ leading_mode, leading_value = _normalise_leading(leading)
28
+
29
+ return cls(
30
+ indent_mode=indent_mode,
31
+ parskip=parskip,
32
+ leading_mode=leading_mode,
33
+ leading_value=leading_value,
34
+ lineno_enabled=lineno,
35
+ )
36
+
37
+ def enabled(self) -> bool:
38
+ return (
39
+ any(
40
+ option is not None for option in (self.indent_mode, self.parskip, self.leading_mode)
41
+ )
42
+ or self.lineno_enabled
43
+ )
44
+
45
+ def inject_into(self, context: dict[str, Any]) -> None:
46
+ context["ts_typesetting_indent_mode"] = self.indent_mode
47
+ context["ts_typesetting_parskip"] = self.parskip
48
+ context["ts_typesetting_leading_mode"] = self.leading_mode
49
+ context["ts_typesetting_leading_value"] = self.leading_value
50
+ context["ts_typesetting_enable_lineno"] = self.lineno_enabled
51
+ context["ts_typesetting_enabled"] = self.enabled()
52
+
53
+
54
+ class TypesettingFragment(BaseFragment[TypesettingConfig]):
55
+ name: ClassVar[str] = "ts-typesetting"
56
+ description: ClassVar[str] = "Paragraph spacing, line spacing, and line numbers controls."
57
+ pieces: ClassVar[list[FragmentPiece]] = [
58
+ FragmentPiece(
59
+ template_path=Path(__file__).with_name("ts-typesetting.tex.jinja"),
60
+ kind="inline",
61
+ slot="extra_packages",
62
+ )
63
+ ]
64
+ attributes: ClassVar[dict[str, TemplateAttributeSpec]] = {
65
+ "typesetting_paragraph": TemplateAttributeSpec(
66
+ default=None,
67
+ type="mapping",
68
+ sources=[
69
+ "typesetting.paragraph",
70
+ "paragraph",
71
+ ],
72
+ ),
73
+ "typesetting_leading": TemplateAttributeSpec(
74
+ default=None,
75
+ type="string",
76
+ allow_empty=True,
77
+ sources=[
78
+ "typesetting.leading",
79
+ "leading",
80
+ ],
81
+ ),
82
+ "typesetting_lineno": TemplateAttributeSpec(
83
+ default=None,
84
+ type="boolean",
85
+ sources=[
86
+ "typesetting.lineno",
87
+ "lineno",
88
+ ],
89
+ ),
90
+ }
91
+
92
+ config_cls: ClassVar[type[TypesettingConfig]] = TypesettingConfig
93
+ source: ClassVar[Path] = Path(__file__).with_name("ts-typesetting.tex.jinja")
94
+ context_defaults: ClassVar[dict[str, Any]] = {"extra_packages": ""}
95
+
96
+ def build_config(
97
+ self, context: Mapping[str, Any], overrides: Mapping[str, Any] | None = None
98
+ ) -> TypesettingConfig:
99
+ _ = overrides
100
+ return self.config_cls.from_context(context)
101
+
102
+ def inject(
103
+ self,
104
+ config: TypesettingConfig,
105
+ context: dict[str, Any],
106
+ overrides: Mapping[str, Any] | None = None,
107
+ ) -> None:
108
+ _ = overrides
109
+ config.inject_into(context)
110
+
111
+ def should_render(self, config: TypesettingConfig) -> bool:
112
+ return config.enabled()
113
+
114
+
115
+ def _normalise_paragraph(payload: Any) -> tuple[str | None, str | None]:
116
+ indent_mode: str | None = None
117
+ spacing: str | None = None
118
+
119
+ if payload is None:
120
+ return (indent_mode, spacing)
121
+
122
+ if isinstance(payload, Mapping):
123
+ indent_mode = _normalise_indent(payload.get("indent"))
124
+ spacing = _normalise_spacing(payload.get("spacing"))
125
+ return (indent_mode, spacing)
126
+
127
+ indent_mode = _normalise_indent(payload)
128
+ return (indent_mode, spacing)
129
+
130
+
131
+ def _normalise_indent(value: Any) -> str | None:
132
+ if value is None:
133
+ return None
134
+
135
+ if isinstance(value, bool):
136
+ return "always" if value else "none"
137
+
138
+ if isinstance(value, (int, float)):
139
+ return "always" if value else "none"
140
+
141
+ if isinstance(value, str):
142
+ token = value.strip().lower()
143
+ if not token:
144
+ return None
145
+ if token == "auto":
146
+ return "auto"
147
+ if token in {"true", "yes", "on", "indent", "indented"}:
148
+ return "always"
149
+ if token in {"false", "no", "off", "none"}:
150
+ return "none"
151
+ raise TemplateError("paragraph.indent must be one of: true, false, auto.")
152
+
153
+
154
+ def _normalise_spacing(value: Any) -> str | None:
155
+ if value is None:
156
+ return None
157
+
158
+ if isinstance(value, (int, float)):
159
+ formatted = _format_number(float(value))
160
+ return f"{formatted}pt"
161
+
162
+ if isinstance(value, str):
163
+ trimmed = value.strip()
164
+ return trimmed or None
165
+
166
+ raise TemplateError("paragraph.spacing must be a string or number.")
167
+
168
+
169
+ def _normalise_leading(value: Any) -> tuple[str | None, str | None]:
170
+ if value is None:
171
+ return (None, None)
172
+
173
+ if isinstance(value, (int, float)):
174
+ factor = float(value)
175
+ if factor <= 0:
176
+ raise TemplateError("leading must be a positive value.")
177
+ return ("factor", _format_number(factor))
178
+
179
+ if isinstance(value, str):
180
+ trimmed = value.strip()
181
+ if not trimmed:
182
+ return (None, None)
183
+
184
+ token = trimmed.lower()
185
+ if token in {"single", "singlespace", "singlespacing"}:
186
+ return ("single", None)
187
+ if token in {"onehalf", "one-half", "onehalfspacing", "one half"}:
188
+ return ("onehalf", None)
189
+ if token in {"double", "doublespacing", "double-spacing"}:
190
+ return ("double", None)
191
+
192
+ try:
193
+ factor = float(trimmed)
194
+ except ValueError:
195
+ return ("length", trimmed)
196
+
197
+ if factor <= 0:
198
+ raise TemplateError("leading must be a positive value.")
199
+ return ("factor", _format_number(factor))
200
+
201
+ raise TemplateError(
202
+ "leading must be 'single', 'onehalf', 'double', a length, or a positive number."
203
+ )
204
+
205
+
206
+ def _format_number(value: float) -> str:
207
+ rounded = f"{value:.3f}"
208
+ cleaned = rounded.rstrip("0").rstrip(".")
209
+ return cleaned or "0"
210
+
211
+
212
+ fragment = TypesettingFragment()
213
+
214
+ __all__ = ["TypesettingConfig", "TypesettingFragment", "fragment"]
@@ -0,0 +1,3 @@
1
+ name = "ts-typesetting"
2
+ description = "Paragraph spacing, leading, and line numbering helpers."
3
+ entrypoint = "texsmith.fragments.typesetting:fragment"
@@ -0,0 +1,81 @@
1
+ \BLOCK{ if ts_typesetting_enabled }
2
+ % Typesetting controls (paragraphs, leading, lineno)
3
+ \makeatletter
4
+ \BLOCK{ if ts_typesetting_indent_mode }
5
+ \BLOCK{ if ts_typesetting_indent_mode == "always" }
6
+ \@afterindenttrue
7
+ \BLOCK{ elif ts_typesetting_indent_mode == "auto" }
8
+ \@afterindentfalse
9
+ \BLOCK{ elif ts_typesetting_indent_mode == "none" }
10
+ \setlength{\parindent}{0pt}
11
+ \@afterindentfalse
12
+ \BLOCK{ endif }
13
+ \BLOCK{ endif }
14
+ \makeatother
15
+
16
+ \BLOCK{ if ts_typesetting_parskip }
17
+ \setlength{\parskip}{\VAR{ts_typesetting_parskip}}
18
+ \BLOCK{ endif }
19
+
20
+ \BLOCK{ if ts_typesetting_leading_mode }
21
+ \makeatletter
22
+ \@ifclassloaded{memoir}{%
23
+ \providecommand{\tsTypesettingApplySpacing}[1]{%
24
+ \ifcsname Spacing\endcsname
25
+ \Spacing{##1}%
26
+ \else
27
+ \renewcommand{\baselinestretch}{##1}\selectfont
28
+ \fi
29
+ }%
30
+ \providecommand{\tsTypesettingSingle}{%
31
+ \ifcsname SingleSpacing\endcsname
32
+ \SingleSpacing
33
+ \else
34
+ \tsTypesettingApplySpacing{1}%
35
+ \fi
36
+ }%
37
+ \providecommand{\tsTypesettingOnehalf}{%
38
+ \ifcsname OnehalfSpacing\endcsname
39
+ \OnehalfSpacing
40
+ \else
41
+ \tsTypesettingApplySpacing{1.5}%
42
+ \fi
43
+ }%
44
+ \providecommand{\tsTypesettingDouble}{%
45
+ \ifcsname DoubleSpacing\endcsname
46
+ \DoubleSpacing
47
+ \else
48
+ \tsTypesettingApplySpacing{2}%
49
+ \fi
50
+ }%
51
+ }{%
52
+ \@ifpackageloaded{setspace}{}{%
53
+ \RequirePackage{setspace}%
54
+ }%
55
+ \providecommand{\tsTypesettingApplySpacing}[1]{\setstretch{##1}}%
56
+ \providecommand{\tsTypesettingSingle}{\singlespacing}%
57
+ \providecommand{\tsTypesettingOnehalf}{\onehalfspacing}%
58
+ \providecommand{\tsTypesettingDouble}{\doublespacing}%
59
+ }
60
+ \makeatother
61
+
62
+ \BLOCK{ if ts_typesetting_leading_mode == "single" }
63
+ \tsTypesettingSingle
64
+ \BLOCK{ elif ts_typesetting_leading_mode == "onehalf" }
65
+ \tsTypesettingOnehalf
66
+ \BLOCK{ elif ts_typesetting_leading_mode == "double" }
67
+ \tsTypesettingDouble
68
+ \BLOCK{ elif ts_typesetting_leading_mode == "factor" }
69
+ \tsTypesettingApplySpacing{\VAR{ts_typesetting_leading_value}}
70
+ \BLOCK{ elif ts_typesetting_leading_mode == "length" }
71
+ \setlength{\baselineskip}{\VAR{ts_typesetting_leading_value}}
72
+ \BLOCK{ endif }
73
+ \BLOCK{ endif }
74
+
75
+ \BLOCK{ if ts_typesetting_enable_lineno }
76
+ \@ifpackageloaded{lineno}{}{%
77
+ \RequirePackage{lineno}%
78
+ }
79
+ \AtBeginDocument{\linenumbers}
80
+ \BLOCK{ endif }
81
+ \BLOCK{ endif }
texsmith/index.py ADDED
@@ -0,0 +1,26 @@
1
+ """Public API exposing TeXSmith's hashtag index extension."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .extensions.index import (
6
+ IndexEntry,
7
+ IndexRegistry,
8
+ TexsmithIndexExtension,
9
+ clear_registry,
10
+ get_registry,
11
+ )
12
+ from .extensions.index.markdown import makeExtension
13
+ from .extensions.index.mkdocs_plugin import IndexPlugin
14
+ from .extensions.index.renderer import register as register_renderer
15
+
16
+
17
+ __all__ = [
18
+ "IndexEntry",
19
+ "IndexPlugin",
20
+ "IndexRegistry",
21
+ "TexsmithIndexExtension",
22
+ "clear_registry",
23
+ "get_registry",
24
+ "makeExtension",
25
+ "register_renderer",
26
+ ]
@@ -0,0 +1,14 @@
1
+ """Compatibility layer exposing plugin integrations to documentation.
2
+
3
+ TeXSmith consolidated its plugin utilities under
4
+ ``texsmith.adapters.plugins``. mkdocstrings still references the historical
5
+ ``texsmith.plugins`` namespace, so we re-export the maintained modules here to
6
+ keep the public import path available.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from texsmith.adapters.plugins import material, snippet
12
+
13
+
14
+ __all__ = ["material", "snippet"]
@@ -0,0 +1,8 @@
1
+ """Public entry points for the progress bar Markdown extension."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .extensions.progressbar import ProgressBarExtension, makeExtension, register_renderer
6
+
7
+
8
+ __all__ = ["ProgressBarExtension", "makeExtension", "register_renderer"]
texsmith/quotes.py ADDED
@@ -0,0 +1,40 @@
1
+ """Markdown extension converting straight double quotes into semantic ``<q>`` tags."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from re import Match
6
+ import xml.etree.ElementTree as ElementTree
7
+
8
+ from markdown import Extension, Markdown
9
+ from markdown.inlinepatterns import InlineProcessor
10
+ from markdown.util import AtomicString
11
+
12
+
13
+ class _QuoteInlineProcessor(InlineProcessor):
14
+ """Wrap straight double quotes in ``<q>`` elements."""
15
+
16
+ def handleMatch( # type: ignore[override] # noqa: N802
17
+ self,
18
+ match: Match[str],
19
+ data: str,
20
+ ) -> tuple[ElementTree.Element, int, int]:
21
+ text = match.group(1)
22
+ element = ElementTree.Element("q")
23
+ element.text = AtomicString(text)
24
+ return element, match.start(0), match.end(0)
25
+
26
+
27
+ class TexsmithQuotesExtension(Extension):
28
+ """Register the quote inline processor."""
29
+
30
+ def extendMarkdown(self, md: Markdown) -> None: # type: ignore[override] # noqa: N802
31
+ pattern = r'(?<!\\)"([^"\n]+?)"'
32
+ processor = _QuoteInlineProcessor(pattern, md)
33
+ md.inlinePatterns.register(processor, "texsmith_quotes", 65)
34
+
35
+
36
+ def makeExtension(**kwargs: object) -> TexsmithQuotesExtension: # noqa: N802
37
+ return TexsmithQuotesExtension(**kwargs)
38
+
39
+
40
+ __all__ = ["TexsmithQuotesExtension", "makeExtension"]