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
+ """Markdown extension that converts ``--``/``---`` into proper dashes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import ClassVar
7
+ import xml.etree.ElementTree as ElementTree
8
+
9
+ from markdown import Extension, Markdown
10
+ from markdown.treeprocessors import Treeprocessor
11
+
12
+
13
+ _DASH_PATTERN = re.compile(r"---|--")
14
+
15
+
16
+ def _replace_dashes(text: str) -> str:
17
+ """Replace ASCII dash sequences with typographic counterparts."""
18
+
19
+ def _swap(match: re.Match[str]) -> str:
20
+ payload = match.group(0)
21
+ return "\u2014" if payload == "---" else "\u2013"
22
+
23
+ return _DASH_PATTERN.sub(_swap, text)
24
+
25
+
26
+ class _SmartDashesTreeprocessor(Treeprocessor):
27
+ """Replace double/triple hyphens outside code with typographic dashes."""
28
+
29
+ _SKIP_TAGS: ClassVar[set[str]] = {"code", "pre", "kbd", "script", "style"}
30
+
31
+ def run(self, root: ElementTree.Element) -> None: # type: ignore[override]
32
+ self._process(root)
33
+
34
+ def _process(self, element: ElementTree.Element) -> None:
35
+ if self._is_skipped(element):
36
+ return
37
+
38
+ if element.text:
39
+ element.text = _replace_dashes(element.text)
40
+
41
+ for child in list(element):
42
+ self._process(child)
43
+ if child.tail:
44
+ child.tail = _replace_dashes(child.tail)
45
+
46
+ def _is_skipped(self, element: ElementTree.Element) -> bool:
47
+ tag = element.tag
48
+ if isinstance(tag, str):
49
+ tag = tag.lower()
50
+ return tag in self._SKIP_TAGS
51
+
52
+
53
+ class TexsmithSmartDashesExtension(Extension):
54
+ """Register the smart dash tree-processor."""
55
+
56
+ def extendMarkdown(self, md: Markdown) -> None: # type: ignore[override] # noqa: N802
57
+ md.treeprocessors.register(
58
+ _SmartDashesTreeprocessor(md), "texsmith_smart_dashes", priority=16
59
+ )
60
+
61
+
62
+ def makeExtension(**kwargs: object) -> TexsmithSmartDashesExtension: # noqa: N802
63
+ return TexsmithSmartDashesExtension(**kwargs)
64
+
65
+
66
+ __all__ = ["TexsmithSmartDashesExtension", "makeExtension"]
@@ -0,0 +1,3 @@
1
+ """Built-in templates shipped with TeXSmith."""
2
+
3
+ __all__ = []
@@ -0,0 +1,33 @@
1
+ # TeXSmith Article Template
2
+
3
+ This directory hosts the built-in `article` layout bundled with [TeXSmith](https://github.com/yves-chevallier/texsmith). The CLI exposes it via `--template article` (or `-tarticle`), so no extra installation is required.
4
+
5
+ ```bash
6
+ texsmith intro.md --template article --output-dir build/article
7
+ ```
8
+
9
+ To customise the template, copy this directory (or extract it via `importlib.resources`) and adjust the manifest, assets, or LaTeX entry point before packaging it as your own distribution.
10
+
11
+ ## Fonts
12
+
13
+ Markdown is usually rendered on web broswers that supports a wide range of unicode characters. In LaTeX few fonts support such a wide range of characters. This template uses the most comprehensive font available: [Noto](https://notofonts.github.io/). However you need to install the font on your system first. You can download it from [Google Fonts](https://fonts.google.com/noto) or install it through your system package manager. For example on Ubuntu:
14
+
15
+ ```bash
16
+ sudo apt update
17
+ sudo apt install fonts-noto-core
18
+ sudo apt install fonts-noto-cjk
19
+ sudo apt install fonts-noto-extra
20
+ ```
21
+
22
+ For black and white emojis, the `Symbola` font is used. For example on Ubuntu:
23
+
24
+ ```bash
25
+ sudo apt update
26
+ sudo apt install fonts-symbola
27
+ ```
28
+
29
+ ## Template Details
30
+
31
+ - Engine: LuaLaTeX (shell escape required)
32
+ - TeX Live year: 2023
33
+ - tlmgr packages: `babel`, `geometry`, `hyperref`, `microtype`, `fontspec`
@@ -0,0 +1,281 @@
1
+ """Article template integration for texsmith."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable, Mapping
6
+ from pathlib import Path
7
+ from typing import Any, ClassVar
8
+ import unicodedata
9
+
10
+ from texsmith.adapters.latex.utils import escape_latex_chars
11
+ from texsmith.core.templates import TemplateError, WrappableTemplate
12
+
13
+
14
+ _PACKAGE_ROOT = Path(__file__).parent.resolve()
15
+
16
+
17
+ class Template(WrappableTemplate):
18
+ """Expose the article template as a wrappable template instance."""
19
+
20
+ _LATIN_RANGE_LIMIT: ClassVar[int] = 0x024F
21
+ _TEXTCOMP_CHARACTERS: ClassVar[set[str]] = {
22
+ "\N{EURO SIGN}",
23
+ "\N{POUND SIGN}",
24
+ "\N{YEN SIGN}",
25
+ "\N{SECTION SIGN}",
26
+ "\N{PILCROW SIGN}",
27
+ "\N{DEGREE SIGN}",
28
+ "\N{PLUS-MINUS SIGN}",
29
+ "\N{MICRO SIGN}",
30
+ "\N{MULTIPLICATION SIGN}",
31
+ "\N{DIVISION SIGN}",
32
+ "\N{COPYRIGHT SIGN}",
33
+ "\N{REGISTERED SIGN}",
34
+ "\N{TRADE MARK SIGN}",
35
+ "\N{VULGAR FRACTION ONE HALF}",
36
+ "\N{VULGAR FRACTION ONE QUARTER}",
37
+ "\N{VULGAR FRACTION THREE QUARTERS}",
38
+ "\N{PER MILLE SIGN}",
39
+ }
40
+ _ALLOWED_PUNCTUATION: ClassVar[set[str]] = {
41
+ "\N{LEFT-POINTING DOUBLE ANGLE QUOTATION MARK}",
42
+ "\N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}",
43
+ "\N{SINGLE LEFT-POINTING ANGLE QUOTATION MARK}",
44
+ "\N{SINGLE RIGHT-POINTING ANGLE QUOTATION MARK}",
45
+ "\N{DOUBLE LOW-9 QUOTATION MARK}",
46
+ "\N{LEFT DOUBLE QUOTATION MARK}",
47
+ "\N{RIGHT DOUBLE QUOTATION MARK}",
48
+ "\N{RIGHT SINGLE QUOTATION MARK}",
49
+ "\N{SINGLE LOW-9 QUOTATION MARK}",
50
+ "\N{MIDDLE DOT}",
51
+ "\N{EN DASH}",
52
+ "\N{EM DASH}",
53
+ "\N{HORIZONTAL ELLIPSIS}",
54
+ }
55
+
56
+ def __init__(self) -> None:
57
+ try:
58
+ super().__init__(_PACKAGE_ROOT)
59
+ except TemplateError as exc:
60
+ raise TemplateError(f"Failed to initialise article template: {exc}") from exc
61
+ config_path = (self.root / "template" / "mermaid-config.json").resolve()
62
+ self.extras = {"mermaid_config": str(config_path)}
63
+
64
+ def prepare_context(
65
+ self,
66
+ latex_body: str,
67
+ *,
68
+ overrides: Mapping[str, Any] | None = None,
69
+ ) -> dict[str, Any]:
70
+ context = super().prepare_context(latex_body, overrides=overrides)
71
+ context["ts_extra_disable_hyperref"] = True
72
+
73
+ # Transform author metadata into a LaTeX-ready string while preserving defaults.
74
+ raw_authors = context.pop("authors", None)
75
+ author_value = self._format_authors(raw_authors)
76
+ if author_value:
77
+ context["author"] = author_value
78
+ else:
79
+ fallback_author = self._coerce_string(context.get("author"))
80
+ if fallback_author:
81
+ context["author"] = fallback_author
82
+ else:
83
+ context.pop("author", None)
84
+
85
+ context.pop("press", None)
86
+
87
+ engine_default = self.info.engine or "pdflatex"
88
+
89
+ context.setdefault("latex_engine", engine_default)
90
+ context.setdefault("requires_unicode_engine", False)
91
+ context.setdefault("unicode_chars", "")
92
+ context.setdefault("unicode_problematic_chars", "")
93
+ context.setdefault("pdflatex_extra_packages", [])
94
+
95
+ candidate_emoji = context.get("emoji")
96
+ if not candidate_emoji:
97
+ fonts_cfg = context.get("fonts")
98
+ if isinstance(fonts_cfg, Mapping):
99
+ candidate_emoji = fonts_cfg.get("emoji")
100
+ if not candidate_emoji:
101
+ press_cfg = context.get("press")
102
+ if isinstance(press_cfg, Mapping):
103
+ press_fonts = press_cfg.get("fonts")
104
+ if isinstance(press_fonts, Mapping):
105
+ candidate_emoji = press_fonts.get("emoji")
106
+
107
+ emoji_mode = self._normalise_emoji_mode(candidate_emoji)
108
+ context["emoji"] = emoji_mode
109
+ if emoji_mode in {"symbola", "color"}:
110
+ context["latex_engine"] = "lualatex"
111
+ context["requires_unicode_engine"] = True
112
+
113
+ return context
114
+
115
+ def _coerce_string(self, value: Any) -> str | None:
116
+ if value is None:
117
+ return None
118
+ candidate = value.strip() if isinstance(value, str) else str(value).strip()
119
+ return candidate or None
120
+
121
+ def _format_authors(self, payload: Any) -> str | None:
122
+ if not payload:
123
+ return None
124
+ if not isinstance(payload, Iterable) or isinstance(payload, (str, bytes)):
125
+ return None
126
+
127
+ formatted: list[str] = []
128
+ for item in payload:
129
+ if not isinstance(item, Mapping):
130
+ continue
131
+ name_value = self._coerce_string(item.get("name"))
132
+ if not name_value:
133
+ continue
134
+ name_tex = escape_latex_chars(name_value)
135
+ affiliation_value = self._coerce_string(item.get("affiliation"))
136
+ if affiliation_value:
137
+ affiliation_tex = escape_latex_chars(affiliation_value)
138
+ formatted.append(f"{name_tex}\\thanks{{{affiliation_tex}}}")
139
+ else:
140
+ formatted.append(name_tex)
141
+
142
+ if not formatted:
143
+ return None
144
+ return " \\and ".join(formatted)
145
+
146
+ def _normalise_emoji_mode(self, value: Any) -> str:
147
+ candidate = self._coerce_string(value)
148
+ if candidate:
149
+ candidate_stripped = candidate.strip()
150
+ else:
151
+ default_value = self.info.get_attribute_default("emoji") or "black"
152
+ candidate = self._coerce_string(default_value)
153
+ candidate_stripped = candidate.strip() if candidate else "black"
154
+
155
+ if not candidate_stripped:
156
+ return "black"
157
+
158
+ lowered = candidate_stripped.lower()
159
+ if lowered in {"artifact", "symbola", "color", "black", "twemoji"}:
160
+ return lowered
161
+ return candidate_stripped
162
+
163
+ def wrap_document(
164
+ self,
165
+ latex_body: str,
166
+ *,
167
+ overrides: Mapping[str, Any] | None = None,
168
+ context: Mapping[str, Any] | None = None,
169
+ ) -> str:
170
+ if context is None:
171
+ prepared = self.prepare_context(latex_body, overrides=overrides)
172
+ context_ref: dict[str, Any] | None = prepared
173
+ else:
174
+ prepared = dict(context)
175
+ context_ref = context if isinstance(context, dict) else None
176
+
177
+ preamble_override: str | None = None
178
+ if overrides:
179
+ press_section = overrides.get("press")
180
+ if isinstance(press_section, Mapping):
181
+ override_section = press_section.get("override")
182
+ if isinstance(override_section, Mapping):
183
+ candidate = override_section.get("preamble")
184
+ if isinstance(candidate, str) and candidate.strip():
185
+ preamble_override = candidate.strip()
186
+ if preamble_override is None:
187
+ direct_override = overrides.get("preamble")
188
+ if isinstance(direct_override, str) and direct_override.strip():
189
+ preamble_override = direct_override.strip()
190
+
191
+ if preamble_override:
192
+ existing_extra = prepared.get("extra_packages") or ""
193
+ package_parts = [existing_extra] if existing_extra else []
194
+ package_parts.append(preamble_override)
195
+ prepared["extra_packages"] = "\n".join(part for part in package_parts if part)
196
+
197
+ self._analyse_unicode_payload(prepared)
198
+
199
+ if context_ref is not prepared and context_ref is not None:
200
+ context_ref.clear()
201
+ context_ref.update(prepared)
202
+
203
+ return super().wrap_document(latex_body, overrides=overrides, context=prepared)
204
+
205
+ def _analyse_unicode_payload(self, context: dict[str, Any]) -> None:
206
+ unicode_chars = self._collect_unicode_characters(context)
207
+ problematic = []
208
+ extra_packages: set[str] = set()
209
+
210
+ base_engine = context.get("latex_engine") or "pdflatex"
211
+
212
+ for char in unicode_chars:
213
+ classification = self._classify_character(char)
214
+ if classification == "textcomp":
215
+ extra_packages.add("textcomp")
216
+ elif classification == "unsupported":
217
+ problematic.append(char)
218
+
219
+ context["unicode_chars"] = "".join(sorted(unicode_chars, key=ord))
220
+ context["unicode_problematic_chars"] = "".join(sorted(problematic, key=ord))
221
+ context["pdflatex_extra_packages"] = sorted(extra_packages)
222
+
223
+ requires_unicode_engine = bool(problematic)
224
+ context["requires_unicode_engine"] = requires_unicode_engine
225
+ context["latex_engine"] = "lualatex" if requires_unicode_engine else base_engine
226
+
227
+ def _collect_unicode_characters(self, payload: Any) -> set[str]:
228
+ collected: set[str] = set()
229
+ visited: set[int] = set()
230
+
231
+ def _walk(value: Any) -> None:
232
+ if isinstance(value, str):
233
+ for char in value:
234
+ if ord(char) > 0x7F:
235
+ collected.add(char)
236
+ return
237
+
238
+ if isinstance(value, Mapping):
239
+ identifier = id(value)
240
+ if identifier in visited:
241
+ return
242
+ visited.add(identifier)
243
+ for key, item in value.items():
244
+ if isinstance(key, str) and key == "callouts_definitions":
245
+ continue
246
+ _walk(item)
247
+ return
248
+
249
+ if isinstance(value, Iterable) and not isinstance(value, (bytes, bytearray)):
250
+ identifier = id(value)
251
+ if identifier in visited:
252
+ return
253
+ visited.add(identifier)
254
+ for item in value:
255
+ _walk(item)
256
+
257
+ _walk(payload)
258
+ return collected
259
+
260
+ def _classify_character(self, char: str) -> str:
261
+ codepoint = ord(char)
262
+ if codepoint <= self._LATIN_RANGE_LIMIT:
263
+ return "latin"
264
+ if char in self._ALLOWED_PUNCTUATION:
265
+ return "punctuation"
266
+ if char in self._TEXTCOMP_CHARACTERS:
267
+ return "textcomp"
268
+
269
+ name: str | None
270
+ try:
271
+ name = unicodedata.name(char)
272
+ except ValueError:
273
+ name = None
274
+
275
+ if name and "LATIN" in name:
276
+ return "latin"
277
+
278
+ return "unsupported"
279
+
280
+
281
+ __all__ = ["Template"]
@@ -0,0 +1,125 @@
1
+ [compat]
2
+ texsmith = ">=0.1,<1.0"
3
+
4
+ [latex.template]
5
+ name = "article"
6
+ version = "0.1.0"
7
+ entrypoint = "template/template.tex"
8
+ engine = "lualatex"
9
+ shell_escape = false
10
+ texlive_year = 2023
11
+ tlmgr_packages = [
12
+ "babel",
13
+ "geometry",
14
+ "hyperref",
15
+ "microtype",
16
+ "lmodern",
17
+ "textcomp",
18
+ "fontspec",
19
+ "biblatex",
20
+ ]
21
+ fragments = [
22
+ "ts-geometry",
23
+ "ts-typesetting",
24
+ "ts-fonts",
25
+ "ts-extra",
26
+ "ts-keystrokes",
27
+ "ts-callouts",
28
+ "ts-code",
29
+ "ts-glossary",
30
+ "ts-index",
31
+ "ts-bibliography",
32
+ "ts-todolist",
33
+ ]
34
+
35
+ [latex.template.attributes.title]
36
+ default = ""
37
+ type = "string"
38
+ escape = "latex"
39
+ allow_empty = true
40
+ sources = ["title"]
41
+ description = "The title of the document. If empty, first heading in document will be used."
42
+
43
+ [latex.template.attributes.subtitle]
44
+ default = ""
45
+ type = "string"
46
+ escape = "latex"
47
+ sources = ["subtitle"]
48
+ description = "The subtitle of the document."
49
+
50
+ [latex.template.attributes.toc]
51
+ default = false
52
+ type = "boolean"
53
+ sources = ["toc"]
54
+ description = "Whether to include a table of contents."
55
+
56
+ [latex.template.attributes.authors]
57
+ default = []
58
+ type = "list"
59
+ sources = ["authors"]
60
+ description = "List of authors of the document. Use plain strings or objects with 'name', 'affiliation', and 'email' fields. You may include multiple authors."
61
+
62
+ [latex.template.attributes.date]
63
+ default = ""
64
+ type = "string"
65
+ escape = "latex"
66
+ sources = ["date"]
67
+ description = "The date of the document, use ISO format (YYYY-MM-DD) or any string. If empty, the current date will be used."
68
+
69
+ [latex.template.attributes.language]
70
+ default = "english"
71
+ type = "string"
72
+ allow_empty = false
73
+ normaliser = "babel_language"
74
+ description = "The language for hyphenation, typesettings and document terms (e.g., 'Table of Contents')."
75
+
76
+ [latex.template.attributes.columns]
77
+ default = 1
78
+ type = "integer"
79
+ description = "Number of columns for the document. When >1, uses the documentclass twocolumn option."
80
+
81
+ [latex.template.attributes.page_numbers]
82
+ default = true
83
+ type = "boolean"
84
+ sources = ["page_numbers"]
85
+ description = "Whether to include page numbers in the document."
86
+
87
+ [latex.template.attributes.numbering]
88
+ default = true
89
+ type = "boolean"
90
+ sources = ["numbering"]
91
+ description = "Whether to include section numbering in the document."
92
+
93
+ [latex.template.attributes.glossary_style]
94
+ default = "list"
95
+ type = "string"
96
+ allow_empty = false
97
+ sources = ["glossary_style"]
98
+ description = "The style used for the glossary section."
99
+
100
+ [latex.template.attributes.bibliography_style]
101
+ default = "numeric"
102
+ type = "string"
103
+ description = "The bibliography style to use. See BibLaTeX documentation for available styles."
104
+
105
+ # Slots definition:
106
+ # -----------------
107
+ # Each slot represents a section of the document that can be filled with content.
108
+ # The 'depth' parameter indicates the LaTeX sectioning command to use.
109
+ [latex.template.slots.mainmatter]
110
+ default = true
111
+ depth = "section"
112
+ description = "The main content of the document."
113
+
114
+ [latex.template.slots.abstract]
115
+ depth = "section"
116
+ strip_heading = true
117
+ description = "The abstract or summary of the document."
118
+
119
+ [latex.template.slots.appendix]
120
+ depth = "section"
121
+ description = "Appendix content inserted before backmatter."
122
+
123
+ [latex.template.slots.backmatter]
124
+ depth = "section"
125
+ strip_heading = true
@@ -0,0 +1,18 @@
1
+ {
2
+ "theme": "base",
3
+ "themeVariables": {
4
+ "primaryColor": "white",
5
+ "primaryTextColor": "black",
6
+ "primaryBorderColor": "black",
7
+ "secondaryColor": "transparent",
8
+ "secondaryTextColor": "black",
9
+ "tertiaryColor": "transparent",
10
+ "lineColor": "black",
11
+ "background": "red",
12
+ "clusterBkg": "white",
13
+ "clusterBorder": "black",
14
+ "fontFamily": "Latin Modern Roman, Computer Modern Serif, Times New Roman, serif",
15
+ "edgeLabelBackground": "#ffffff"
16
+ },
17
+ "themeCSS": ".node rect, .node circle, .node ellipse, .node polygon, .node path { stroke-width: 2px; }\n .edgePath .path { stroke-width: 2px !important; }\n.packetBlock { fill: #ffffff !important; stroke: #000000 !important; }\n.packet rect, .packet polygon { fill: #ffffff !important; stroke: #000000 !important; }\n.packet text, .packetLabel, .packetByte, .packetTitle { fill: #000000 !important; }\n.node rect, .node polygon, .node ellipse { fill: white !important; }"
18
+ }
@@ -0,0 +1,74 @@
1
+ \BLOCK{ set base_docclass_options = documentclass_options|default('', true) }
2
+ \BLOCK{ set combined_docclass_options = base_docclass_options[1:-1] if base_docclass_options else '' }
3
+ \BLOCK{ set column_count = columns|default(1)|int }
4
+ \BLOCK{ set option_parts = [] }
5
+ \BLOCK{ if combined_docclass_options }\BLOCK{ set option_parts = option_parts + [combined_docclass_options] }\BLOCK{ endif }
6
+ \BLOCK{ if column_count > 1 }\BLOCK{ set option_parts = option_parts + ['twocolumn'] }\BLOCK{ endif }
7
+ \BLOCK{ set effective_documentclass_options = '[' ~ option_parts|join(',') ~ ']' if option_parts else '' }
8
+ \documentclass\VAR{effective_documentclass_options}{article}
9
+
10
+ \usepackage[svgnames,dvipsnames,x11names]{xcolor}
11
+ \VAR{extra_packages}
12
+ \usepackage{graphicx}
13
+ \usepackage[\VAR{language|default('english')}]{babel}
14
+ \usepackage[colorlinks=true, linkcolor=NavyBlue, urlcolor=NavyBlue, citecolor=NavyBlue]{hyperref}
15
+
16
+ \usepackage{lastpage} % pour le label LastPage
17
+ \usepackage{refcount} % pour \getpagerefnumber
18
+ \makeatletter
19
+ \def\ps@plain{%
20
+ \let\@oddhead\@empty
21
+ \let\@evenhead\@empty
22
+ \def\@oddfoot{%
23
+ \hfil
24
+ % Afficher le numéro seulement si le document a plus d’une page
25
+ \ifnum\getpagerefnumber{LastPage}>1\relax
26
+ \thepage
27
+ \fi
28
+ \hfil}%
29
+ \let\@evenfoot\@oddfoot
30
+ }
31
+ \makeatother
32
+
33
+ \BLOCK{ set has_title_metadata = title|default('')|trim }
34
+ \BLOCK{ set effective_date = date|default('')|trim }
35
+ \BLOCK{ if has_title_metadata }
36
+ \title{\VAR{title}\BLOCK{ if subtitle }\\\large \VAR{subtitle}\BLOCK{ endif }}
37
+ \BLOCK{ if author }\author{\VAR{author}}\BLOCK{ else }\author{}\BLOCK{ endif }
38
+ \BLOCK{ if effective_date }\date{\VAR{date}}\BLOCK{ else }\date{\today}\BLOCK{ endif }
39
+ \BLOCK{ endif }
40
+
41
+ \begin{document}
42
+
43
+ \BLOCK{ if has_title_metadata }
44
+ \maketitle
45
+ \BLOCK{ endif }
46
+ \BLOCK{ if not page_numbers }
47
+ \pagenumbering{gobble}
48
+ \thispagestyle{empty}
49
+ \pagestyle{empty}
50
+ \BLOCK{ endif }
51
+ \BLOCK{ if numbering is defined and not numbering }
52
+ \setcounter{secnumdepth}{-1}
53
+ \BLOCK{ endif }
54
+
55
+ \BLOCK{ if abstract }
56
+ \begin{abstract}
57
+ \VAR{abstract}
58
+ \end{abstract}
59
+ \BLOCK{ endif }
60
+ \BLOCK{ if toc }
61
+ \tableofcontents
62
+ \BLOCK{ endif }
63
+
64
+ \VAR{mainmatter}
65
+
66
+ \BLOCK{ if appendix }
67
+ \appendix
68
+ \VAR{appendix}
69
+ \BLOCK{ endif }
70
+
71
+ \VAR{backmatter}
72
+ \VAR{fragment_backmatter}
73
+
74
+ \end{document}
@@ -0,0 +1,26 @@
1
+ # TeXSmith Book Template
2
+
3
+ The built-in `book` template targets long-form documents with front matter, chapters, appendices, and clean back matter. It wraps `memoir` defaults, modern callouts, keystroke helpers, todo lists, glossary/index hooks, and bibliography support.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ texsmith manuscript.md --template book \
9
+ --slot frontmatter:intro.md \
10
+ --slot backmatter:appendix.md
11
+ ```
12
+
13
+ Slots:
14
+ - `frontmatter`, `preface`, `dedication`
15
+ - `mainmatter` (default)
16
+ - `appendix`
17
+ - `backmatter`, `colophon`
18
+
19
+ ## Template Details
20
+
21
+ - Engine: LuaLaTeX
22
+ - TeX Live year: 2023
23
+ - tlmgr packages: `babel`, `babel-french`, `csquotes`, `fontspec`, `fancyvrb`, `geometry`, `hyperref`, `longtable`, `microtype`, `titlesec`, `titletoc`, `xcolor`, `xunicode`
24
+ - Built-in fragments: geometry, typesetting, fonts, extra, keystrokes, callouts, code, glossary, index, bibliography, todolist
25
+ - Attributes: title/subtitle/authors/email/date/language/documentclass, hyperref options, edition/publisher/imprint*, glossary toggles, list-of figures/tables, `part` flag for part/chapter structure.
26
+ - Assets: none (latexmkrc is injected by core).