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,3 @@
1
+ name = "ts-fonts"
2
+ description = "Font selection driven by fonts.family (XeLaTeX/LuaLaTeX compatible)."
3
+ entrypoint = "texsmith.fragments.fonts:fragment"
@@ -0,0 +1,292 @@
1
+ \BLOCK{ set font_family = fonts_family | default("lm") }
2
+ \BLOCK{ set fallback = fonts.get("fallback") if fonts is defined and fonts else {} }
3
+ \ProvidesPackage{ts-fonts}[2025/01/10 TeXSmith font selection (generated)]
4
+
5
+ \RequirePackage{iftex}
6
+ % Fallback definition replaced when emoji fonts are available.
7
+ \providecommand{\texsmithEmoji}[1]{\textbf{!}}
8
+ \catcode"2135=\active
9
+ \protected\def^^^^2135{\ensuremath{\aleph}}
10
+
11
+ \ifPDFTeX
12
+ \PackageError{ts-fonts}{ts-fonts requires XeLaTeX or LuaLaTeX}{Switch to xelatex or lualatex.}
13
+ \else
14
+ \RequirePackage{fontspec}
15
+ \defaultfontfeatures{Ligatures = TeX, Scale = MatchLowercase}
16
+
17
+ \makeatletter
18
+ \newcommand{\tsDeclareFontSet}[2]{%
19
+ \expandafter\gdef\csname tssetupfonts@#1\endcsname{#2}%
20
+ }
21
+ \newcommand{\tsUseFontSet}[1]{%
22
+ \@ifundefined{tssetupfonts@#1}{%
23
+ \PackageError{ts-fonts}{Unknown font family '#1'}%
24
+ {Use one of: lm, lm-sans, bonum, libertinus, pagella, termes, schola, heros, adventor, cursor, plex, pennstander.}%
25
+ }{%
26
+ \csname tssetupfonts@#1\endcsname
27
+ }%
28
+ }
29
+ \makeatother
30
+
31
+ % Latin Modern (lm)
32
+ \tsDeclareFontSet{lm}{%
33
+ \ifLuaTeX
34
+ \setmainfont{Latin Modern Roman}%
35
+ \setsansfont{Latin Modern Sans}%
36
+ \setmonofont{Latin Modern Mono}%
37
+ \else
38
+ \setmainfont{lmroman10}[%
39
+ Extension = .otf,
40
+ UprightFont = lmroman10-regular,
41
+ ItalicFont = lmroman10-italic,
42
+ BoldFont = lmroman10-bold,
43
+ BoldItalicFont = lmroman10-bolditalic,
44
+ SmallCapsFont = lmromancaps10-regular,
45
+ SlantedFont = lmromanslant10-regular,
46
+ BoldSlantedFont = lmromanslant10-bold,
47
+ Ligatures = TeX,
48
+ ]%
49
+ \setsansfont{lmsans10}[%
50
+ Extension = .otf,
51
+ UprightFont = *-regular,
52
+ ItalicFont = *-oblique,
53
+ BoldFont = *-bold,
54
+ BoldItalicFont = *-boldoblique,
55
+ ]%
56
+ \setmonofont{lmmono10}[%
57
+ Extension = .otf,
58
+ UprightFont = *-regular,
59
+ ItalicFont = *-italic,
60
+ BoldFont = *-regular,
61
+ BoldFeatures = {FakeBold=2},
62
+ BoldItalicFont = *-italic,
63
+ BoldItalicFeatures = {FakeBold=2},
64
+ SmallCapsFont = lmmonocaps10-regular,
65
+ SmallCapsFeatures = {Letters=SmallCaps},
66
+ ]%
67
+ \fi
68
+ }
69
+
70
+ % Latin Modern Sans Serif (lm-sans)
71
+ \tsDeclareFontSet{lm-sans}{%
72
+ \ifLuaTeX
73
+ \setmainfont{Latin Modern Sans}%
74
+ \setsansfont{Latin Modern Sans}%
75
+ \setmonofont{Latin Modern Mono}%
76
+ \else
77
+ \setmainfont{lmsans10}[%
78
+ Extension = .otf,
79
+ UprightFont = *-regular,
80
+ ItalicFont = *-oblique,
81
+ BoldFont = *-bold,
82
+ BoldItalicFont = *-boldoblique,
83
+ Ligatures = TeX,
84
+ ]%
85
+ \setsansfont{lmsans10}[%
86
+ Extension = .otf,
87
+ UprightFont = *-regular,
88
+ ItalicFont = *-oblique,
89
+ BoldFont = *-bold,
90
+ BoldItalicFont = *-boldoblique,
91
+ ]%
92
+ \setmonofont{IBMPlexMono}[%
93
+ Extension = .otf,
94
+ UprightFont = *-Regular,
95
+ ItalicFont = *-Italic,
96
+ BoldFont = *-Bold,
97
+ BoldItalicFont = *-BoldItalic,
98
+ Scale=0.90,
99
+ FakeStretch = 0.92,
100
+ ]%
101
+ \fi
102
+ }
103
+
104
+ % Tex Gyre Bonum (bonum)
105
+ \tsDeclareFontSet{bonum}{%
106
+ \RequirePackage{bonum-otf}%
107
+ }
108
+
109
+ % Libertinus (libertinus)
110
+ \tsDeclareFontSet{libertinus}{%
111
+ \RequirePackage{libertinus}%
112
+ }
113
+
114
+ % Tex Gyre Pagella (pagella)
115
+ \tsDeclareFontSet{pagella}{%
116
+ \RequirePackage{pagella-otf}%
117
+ }
118
+
119
+ % Tex Gyre Termes (termes)
120
+ \tsDeclareFontSet{termes}{%
121
+ \RequirePackage{termes-otf}%
122
+ }
123
+
124
+ % TeX Gyre Schola (schola)
125
+ \tsDeclareFontSet{schola}{%
126
+ \RequirePackage{schola-otf}%
127
+ }
128
+
129
+ % TeX Gyre Heros (heros)
130
+ \tsDeclareFontSet{heros}{%
131
+ \RequirePackage{heros-otf}%
132
+ }
133
+
134
+ % TeX Gyre Adventor (adventor)
135
+ \tsDeclareFontSet{adventor}{%
136
+ \RequirePackage{adventor-otf}%
137
+ }
138
+
139
+ % TeX Gyre Cursor (cursor)
140
+ \tsDeclareFontSet{cursor}{%
141
+ \RequirePackage{cursor-otf}%
142
+ }
143
+
144
+ % IBM Plex (plex)
145
+ \tsDeclareFontSet{plex}{%
146
+ \setmainfont{IBMPlexSerif}[%
147
+ Path=./fonts/plex-otf/,%
148
+ UprightFont=*-Regular,%
149
+ ItalicFont=*-Italic,%
150
+ BoldFont=*-Bold,%
151
+ BoldItalicFont=*-BoldItalic,%
152
+ ]%
153
+ \setsansfont{IBMPlexSans}[%
154
+ Path=./fonts/plex-otf/,%
155
+ UprightFont=*-Regular,%
156
+ ItalicFont=*-Italic,%
157
+ BoldFont=*-Bold,%
158
+ BoldItalicFont=*-BoldItalic,%
159
+ ]%
160
+ \setmonofont{IBMPlexMono}[%
161
+ Path=./fonts/plex-otf/,%
162
+ UprightFont=*-Regular,%
163
+ ItalicFont=*-Italic,%
164
+ BoldFont=*-Bold,%
165
+ BoldItalicFont=*-BoldItalic,%
166
+ ]%
167
+ }
168
+
169
+ % Penn Stander (pennstander)
170
+ \tsDeclareFontSet{pennstander}{%
171
+ \setmainfont{Latin Modern Roman}%
172
+ \setsansfont{Latin Modern Sans}%
173
+ \setmonofont{Latin Modern Mono}%
174
+ }
175
+
176
+ \tsUseFontSet{\VAR{font_family}}
177
+
178
+ \BLOCK{- if fallback and fallback.get("entries") }
179
+ % Fallback script fonts
180
+ \makeatletter
181
+ \BLOCK{ for entry in fallback.entries }
182
+ \BLOCK{ set font_cmd = '\\' + entry.font_command }
183
+ \BLOCK{ set text_cmd = '\\' + entry.text_command }
184
+ % Declare font family for \VAR{entry.font_name}
185
+ \newfontfamily\VAR{font_cmd}[
186
+ Path = fonts/,
187
+ Extension = \VAR{entry.extension},
188
+ UprightFont = \VAR{entry.upright},
189
+ \BLOCK{ if entry.has_bold }
190
+ BoldFont = \VAR{entry.bold},
191
+ \BLOCK{ else }
192
+ FakeBold = 2,
193
+ \BLOCK{ endif }
194
+ Scale = MatchLowercase
195
+ ]{\VAR{entry.font_name}}%
196
+ \DeclareRobustCommand\VAR{text_cmd}[1]{{\VAR{font_cmd}#1}}%
197
+
198
+ \BLOCK{ endfor }
199
+ \makeatother
200
+
201
+ \BLOCK{- if fallback and fallback.get("entries") }
202
+ % Ensure hyperref can build PDF strings without font selection commands.
203
+ \makeatletter
204
+ \AtBeginDocument{%
205
+ \BLOCK{ for entry in fallback.entries }
206
+ \BLOCK{ set text_cmd = '\\' + entry.text_command }
207
+ \pdfstringdefDisableCommands{%
208
+ \def\VAR{text_cmd}#1{#1}%
209
+ }%
210
+ \BLOCK{ endfor }
211
+ }
212
+ \makeatother
213
+ \BLOCK{- endif }
214
+
215
+ \BLOCK{- if fallback and fallback.get("missing_commands") }
216
+ % Define no-op wrappers for scripts without an available fallback font.
217
+ \BLOCK{ for text_cmd in fallback.missing_commands }
218
+ \providecommand\VAR{'\\' + text_cmd}[1]{#1}%
219
+ \BLOCK{ endfor }
220
+ \BLOCK{- endif }
221
+
222
+ \ifLuaTeX
223
+ \directlua{
224
+ luaotfload.add_fallback("texsmith-fallback-reg", {
225
+ \BLOCK{ for font in fallback.lua_regular }
226
+ "[fonts/\VAR{font}]:mode=harf",
227
+ \BLOCK{ endfor }
228
+ })
229
+ luaotfload.add_fallback("texsmith-fallback-bold", {
230
+ \BLOCK{ for font in fallback.lua_bold }
231
+ "[fonts/\VAR{font}]:mode=harf",
232
+ \BLOCK{ endfor }
233
+ })
234
+ }
235
+ \defaultfontfeatures{
236
+ Renderer = HarfBuzz,
237
+ RawFeature = {fallback=texsmith-fallback-reg},
238
+ BoldFeatures = {RawFeature={fallback=texsmith-fallback-bold}},
239
+ ItalicFeatures = {RawFeature={fallback=texsmith-fallback-reg}},
240
+ BoldItalicFeatures = {RawFeature={fallback=texsmith-fallback-bold}},
241
+ }
242
+ \addfontfeatures{RawFeature={fallback=texsmith-fallback-reg}}
243
+ \ExplSyntaxOn
244
+ \cs_new_protected:Npn \texsmith_apply_fallback_to_family:nn #1#2
245
+ {
246
+ \prop_get:cnNTF { g__fontspec_fontinfo_ #2 _prop } { fontname } \l_tmpa_tl
247
+ {
248
+ #1[
249
+ RawFeature = {fallback=texsmith-fallback-reg},
250
+ BoldFeatures = {RawFeature={fallback=texsmith-fallback-bold}},
251
+ ItalicFeatures = {RawFeature={fallback=texsmith-fallback-reg}},
252
+ BoldItalicFeatures = {RawFeature={fallback=texsmith-fallback-bold}},
253
+ ]{\l_tmpa_tl}
254
+ }{}
255
+ }
256
+ \texsmith_apply_fallback_to_family:nn \setmainfont \rmdefault
257
+ \texsmith_apply_fallback_to_family:nn \setsansfont \sfdefault
258
+ \texsmith_apply_fallback_to_family:nn \setmonofont \ttdefault
259
+ \ExplSyntaxOff
260
+ \else\ifXeTeX
261
+ \BLOCK{- if fallback and fallback.get("transitions") }
262
+ \BLOCK{ set uc_options = ['Latin'] + (fallback.package_options if fallback else []) + ['Devanagari'] }
263
+ \RequirePackage[
264
+ \VAR{ uc_options | join(',\n ') }
265
+ ]{ucharclasses}
266
+ \makeatletter
267
+ \newcommand{\texsmithFallbackFamily}{\rmfamily}
268
+ \newcommand{\texsmithSetFallbackFamily}[1]{\def\texsmithFallbackFamily{#1}}
269
+ \g@addto@macro\rmfamily{\texsmithSetFallbackFamily{\rmfamily}}
270
+ \g@addto@macro\sffamily{\texsmithSetFallbackFamily{\sffamily}}
271
+ \g@addto@macro\ttfamily{\texsmithSetFallbackFamily{\ttfamily}}
272
+ \BLOCK{ for entry in fallback.entries }
273
+ \expandafter\g@addto@macro\csname \VAR{entry.font_command}\endcsname{%
274
+ \texsmithSetFallbackFamily{\csname \VAR{entry.font_command}\endcsname}%
275
+ }
276
+ \BLOCK{ endfor }
277
+ \makeatother
278
+ \texsmithSetFallbackFamily{\rmfamily}
279
+ \setDefaultTransitions{\texsmithFallbackFamily}{\texsmithFallbackFamily}%
280
+ \BLOCK{ for transition in fallback.transitions }
281
+ \VAR{transition}
282
+ \BLOCK{ endfor }
283
+ \enableTransitionRules
284
+ \ifdefined\XeTeXinterchartoksstate
285
+ \XeTeXinterchartoksstate=1%
286
+ \AtBeginDocument{\XeTeXinterchartoksstate=1}%
287
+ \fi
288
+ \AtBeginDocument{\enableTransitionRules}%
289
+ \BLOCK{- endif }
290
+ \fi\fi
291
+ \BLOCK{- endif }
292
+ \fi
@@ -0,0 +1,170 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from pathlib import Path
5
+ from typing import Any, ClassVar
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator, model_validator
8
+
9
+ from texsmith.core.fragments.base import BaseFragment, FragmentPiece
10
+ from texsmith.core.templates.manifest import TemplateAttributeSpec, TemplateError
11
+
12
+
13
+ _DEFAULT_MARGIN = "0pt"
14
+ _DEFAULT_FOLD_SIZE = "10mm"
15
+
16
+
17
+ def _coerce_bool(value: Any, *, default: bool = False) -> bool:
18
+ if value is None:
19
+ return default
20
+ if isinstance(value, bool):
21
+ return value
22
+ if isinstance(value, (int, float)):
23
+ return bool(value)
24
+ if isinstance(value, str):
25
+ token = value.strip().lower()
26
+ if not token:
27
+ return default
28
+ if token in {"true", "yes", "on", "1", "dogear"}:
29
+ return True
30
+ if token in {"false", "no", "off", "0", "border"}:
31
+ return False
32
+ return default
33
+
34
+
35
+ class FrameConfig(BaseModel):
36
+ """Validated representation of the press.frame options."""
37
+
38
+ model_config = ConfigDict(extra="ignore", populate_by_name=True)
39
+
40
+ enabled: bool = False
41
+ dogear: bool = False
42
+ margin: str | None = None
43
+ fold_size: str | None = Field(default=None, alias="fold-size")
44
+
45
+ @model_validator(mode="before")
46
+ @classmethod
47
+ def _coerce_payload(cls, data: Any) -> Any:
48
+ if isinstance(data, FrameConfig):
49
+ return data.model_dump()
50
+ if data is None:
51
+ return {"enabled": False}
52
+ if isinstance(data, Mapping):
53
+ payload = dict(data)
54
+ enabled = _coerce_bool(payload.get("enabled"), default=True)
55
+ mode = payload.get("mode")
56
+ dogear_value = payload.get("dogear")
57
+ if isinstance(mode, str):
58
+ token = mode.strip().lower()
59
+ if token == "border":
60
+ dogear_value = False
61
+ enabled = True
62
+ elif token in {"dogear", "fold"}:
63
+ dogear_value = True
64
+ enabled = True
65
+ dogear_flag = _coerce_bool(dogear_value, default=True)
66
+ fold_size = payload.get("fold-size") or payload.get("fold_size") or payload.get("fold")
67
+ margin = payload.get("margin")
68
+ if not enabled:
69
+ return {"enabled": False}
70
+ return {
71
+ "enabled": True,
72
+ "dogear": dogear_flag,
73
+ "margin": margin,
74
+ "fold_size": fold_size,
75
+ }
76
+ if isinstance(data, (bool, int, float)):
77
+ flag = bool(data)
78
+ return {"enabled": flag, "dogear": flag}
79
+ if isinstance(data, str):
80
+ token = data.strip().lower()
81
+ if not token or token in {"false", "off", "no", "0", "none"}:
82
+ return {"enabled": False}
83
+ if token == "border":
84
+ return {"enabled": True, "dogear": False}
85
+ if token in {"dogear", "true", "yes", "on", "1"}:
86
+ return {"enabled": True, "dogear": True}
87
+ raise TemplateError("press.frame accepts false, true, 'border', or 'dogear'.")
88
+ raise TemplateError("press.frame must be a boolean, string, or mapping.")
89
+
90
+ @field_validator("margin", "fold_size", mode="before")
91
+ @classmethod
92
+ def _normalise_length(cls, value: Any) -> str | None:
93
+ if value is None:
94
+ return None
95
+ if isinstance(value, (int, float)):
96
+ return f"{value}mm"
97
+ if isinstance(value, str):
98
+ trimmed = value.strip()
99
+ return trimmed or None
100
+ return str(value)
101
+
102
+ @model_validator(mode="after")
103
+ def _disable_when_empty(self) -> FrameConfig:
104
+ if not self.enabled:
105
+ object.__setattr__(self, "dogear", False)
106
+ return self
107
+
108
+ def effective_margin(self) -> str:
109
+ return self.margin or _DEFAULT_MARGIN
110
+
111
+ def effective_fold_size(self) -> str:
112
+ return self.fold_size or _DEFAULT_FOLD_SIZE
113
+
114
+
115
+ class FrameFragment(BaseFragment[FrameConfig]):
116
+ """Optional page frame with an optional folded corner."""
117
+
118
+ name: ClassVar[str] = "ts-frame"
119
+ description: ClassVar[str] = "Draw a page frame (with optional dogear) on each page."
120
+ pieces: ClassVar[list[FragmentPiece]] = [
121
+ FragmentPiece(
122
+ template_path=Path(__file__).with_name("ts-frame.tex.jinja"),
123
+ kind="inline",
124
+ slot="extra_packages",
125
+ )
126
+ ]
127
+ attributes: ClassVar[dict[str, TemplateAttributeSpec]] = {
128
+ "frame_spec": TemplateAttributeSpec(
129
+ default=None,
130
+ sources=["frame"],
131
+ )
132
+ }
133
+ config_cls: ClassVar[type[FrameConfig]] = FrameConfig
134
+ source: ClassVar[Path] = Path(__file__).with_name("ts-frame.tex.jinja")
135
+ context_defaults: ClassVar[dict[str, Any]] = {
136
+ "ts_frame_enabled": False,
137
+ "ts_frame_dogear": False,
138
+ "ts_frame_margin": _DEFAULT_MARGIN,
139
+ "ts_frame_fold_size": _DEFAULT_FOLD_SIZE,
140
+ }
141
+
142
+ def build_config(
143
+ self, context: Mapping[str, Any], overrides: Mapping[str, Any] | None = None
144
+ ) -> FrameConfig:
145
+ _ = overrides
146
+ raw_value = context.get("frame_spec") or context.get("frame")
147
+ try:
148
+ return self.config_cls.model_validate(raw_value)
149
+ except ValidationError as exc:
150
+ raise TemplateError(f"Invalid frame settings: {exc}") from exc
151
+
152
+ def inject(
153
+ self,
154
+ config: FrameConfig,
155
+ context: dict[str, Any],
156
+ overrides: Mapping[str, Any] | None = None,
157
+ ) -> None:
158
+ _ = overrides
159
+ context["ts_frame_enabled"] = config.enabled
160
+ context["ts_frame_dogear"] = bool(config.enabled and config.dogear)
161
+ context["ts_frame_margin"] = config.effective_margin()
162
+ context["ts_frame_fold_size"] = config.effective_fold_size()
163
+
164
+ def should_render(self, config: FrameConfig) -> bool:
165
+ return bool(config.enabled)
166
+
167
+
168
+ fragment = FrameFragment()
169
+
170
+ __all__ = ["FrameConfig", "FrameFragment", "fragment"]
@@ -0,0 +1,3 @@
1
+ name = "ts-frame"
2
+ description = "Optional page frame with an optional folded corner."
3
+ entrypoint = "texsmith.fragments.frame:fragment"
@@ -0,0 +1,49 @@
1
+ % Page frame fragment (ts-frame)
2
+ \usepackage{tikz}
3
+ \usetikzlibrary{calc}
4
+ \usepackage{tikzpagenodes}
5
+
6
+ \newif\iftsframe
7
+ \newif\iftsframedogear
8
+ \BLOCK{ if ts_frame_enabled }\tsframetrue\BLOCK{ else }\tsframefalse\BLOCK{ endif }
9
+ \BLOCK{ if ts_frame_dogear }\tsframedogeartrue\BLOCK{ else }\tsframedogearfalse\BLOCK{ endif }
10
+
11
+ \newlength\TSFrameInset
12
+ \setlength\TSFrameInset{\VAR{ts_frame_margin|default('0pt')}}
13
+ \newlength\TSFrameFoldSize
14
+ \setlength\TSFrameFoldSize{\VAR{ts_frame_fold_size|default('10mm')}}
15
+ \newcommand\TSFrameColor{black}
16
+ \newcommand\TSFrameLineWidth{0.6pt}
17
+
18
+ \AddToHook{shipout/background}{%
19
+ \iftsframe
20
+ \begin{tikzpicture}[remember picture,overlay]
21
+ \coordinate (A) at ($(current page.north west)+(\TSFrameInset,-\TSFrameInset)$);
22
+ \coordinate (B) at ($(current page.north east)+(-\TSFrameInset,-\TSFrameInset)$);
23
+ \coordinate (C) at ($(current page.south east)+(-\TSFrameInset,\TSFrameInset)$);
24
+ \coordinate (D) at ($(current page.south west)+(\TSFrameInset,\TSFrameInset)$);
25
+
26
+ \coordinate (Bdown) at ($(B)-(0,\TSFrameFoldSize)$);
27
+ \coordinate (Bleft) at ($(B)-(\TSFrameFoldSize,0)$);
28
+ \coordinate (Corner) at ($(B)-(\TSFrameFoldSize,\TSFrameFoldSize)$);
29
+
30
+ \iftsframedogear
31
+ \draw[line width=\TSFrameLineWidth,draw=\TSFrameColor]
32
+ (A) -- (Bleft) -- (Bdown) -- (C) -- (D) -- cycle;
33
+ \draw[line width=\TSFrameLineWidth,draw=\TSFrameColor]
34
+ (Corner)
35
+ .. controls ($(Corner)+(0.3*\TSFrameFoldSize,0.1*\TSFrameFoldSize)$)
36
+ and ($(Bdown)+(-0.3*\TSFrameFoldSize,-0.1*\TSFrameFoldSize)$) ..
37
+ (Bdown);
38
+ \draw[line width=\TSFrameLineWidth,draw=\TSFrameColor]
39
+ (Bleft)
40
+ .. controls ($(Bleft)+(0.1*\TSFrameFoldSize,-0.3*\TSFrameFoldSize)$)
41
+ and ($(Corner)+(-0.1*\TSFrameFoldSize,0.3*\TSFrameFoldSize)$) ..
42
+ (Corner);
43
+ \else
44
+ \draw[line width=\TSFrameLineWidth,draw=\TSFrameColor]
45
+ (A) rectangle (C);
46
+ \fi
47
+ \end{tikzpicture}%
48
+ \fi
49
+ }
@@ -0,0 +1,169 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from pathlib import Path
5
+ from typing import Any, ClassVar
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
8
+
9
+ from texsmith.core.fragments.base import BaseFragment, FragmentPiece
10
+ from texsmith.core.templates.base import _build_environment
11
+
12
+ from .paper import (
13
+ GeometryResolution,
14
+ _coerce_marks_flag,
15
+ inject_geometry_context,
16
+ resolve_geometry_settings,
17
+ )
18
+
19
+
20
+ class GeometryFragmentConfig(BaseModel):
21
+ """Validated payload accepted by the ts-geometry fragment."""
22
+
23
+ paper: Mapping[str, Any] | None = None
24
+ geometry: dict[str, Any] = Field(default_factory=dict)
25
+ duplex: bool | None = None
26
+ margin: Any | None = None
27
+ orientation: Any | None = None
28
+ binding: Any | None = None
29
+ marks: bool | None = None
30
+ watermark: str | None = None
31
+
32
+ model_config = ConfigDict(extra="allow")
33
+
34
+ @model_validator(mode="before")
35
+ @classmethod
36
+ def _fold_paper(cls, data: Any) -> Any:
37
+ if data is None:
38
+ return {}
39
+ if not isinstance(data, Mapping):
40
+ return {"paper": data}
41
+
42
+ payload = dict(data)
43
+ paper_payload = payload.get("paper")
44
+ if isinstance(paper_payload, Mapping):
45
+ paper_data: dict[str, Any] = dict(paper_payload)
46
+ elif paper_payload is None:
47
+ paper_data = {}
48
+ else:
49
+ paper_data = {"format": paper_payload}
50
+
51
+ if "frame" in payload and "marks" not in payload:
52
+ payload["marks"] = payload.pop("frame")
53
+
54
+ for key in ("margin", "orientation", "binding", "marks", "watermark", "duplex"):
55
+ if key in payload and key not in paper_data:
56
+ paper_data[key] = payload[key]
57
+
58
+ extra_payload = payload.get("geometry") or payload.get("extra")
59
+ if isinstance(extra_payload, Mapping):
60
+ merged_extra = dict(paper_data.get("extra") or {})
61
+ merged_extra.update(extra_payload)
62
+ paper_data["extra"] = merged_extra
63
+
64
+ payload["paper"] = paper_data
65
+ return payload
66
+
67
+ def to_context(self) -> dict[str, Any]:
68
+ """Return a context mapping suitable for ``resolve_geometry_settings``."""
69
+ context: dict[str, Any] = {}
70
+ if self.paper is not None:
71
+ context["paper"] = self.paper
72
+ if self.geometry:
73
+ context["geometry"] = self.geometry
74
+ if self.duplex is not None:
75
+ context["duplex"] = self.duplex
76
+ if self.margin is not None:
77
+ context["margin"] = self.margin
78
+ if self.orientation is not None:
79
+ context["orientation"] = self.orientation
80
+ if self.binding is not None:
81
+ context["binding"] = self.binding
82
+ if self.marks is not None:
83
+ context["marks"] = self.marks
84
+ if self.watermark is not None:
85
+ context["watermark"] = self.watermark
86
+ return context
87
+
88
+ def resolve(self) -> GeometryResolution:
89
+ """Return a resolved geometry payload for this configuration."""
90
+ return resolve_geometry_settings(self.to_context())
91
+
92
+
93
+ class GeometryFragment(BaseFragment[GeometryFragmentConfig]):
94
+ """Programmatic renderer and fragment adapter for the ts-geometry fragment."""
95
+
96
+ name: ClassVar[str] = "ts-geometry"
97
+ description: ClassVar[str] = "Page layout setup driven by press.paper."
98
+ pieces: ClassVar[list[FragmentPiece]] = [
99
+ FragmentPiece(
100
+ template_path=Path(__file__).with_name("ts_geometry.tex.jinja"),
101
+ kind="inline",
102
+ slot="extra_packages",
103
+ )
104
+ ]
105
+ attributes: ClassVar[dict[str, Any]] = {}
106
+ config_cls: ClassVar[type[GeometryFragmentConfig]] = GeometryFragmentConfig
107
+ source: ClassVar[Path] = Path(__file__).with_name("ts_geometry.tex.jinja")
108
+ context_defaults: ClassVar[dict[str, Any]] = {
109
+ "extra_packages": "",
110
+ "documentclass_options": "",
111
+ "geometry_options": "",
112
+ "geometry_options_list": [],
113
+ "geometry_extra_options": "",
114
+ "geometry_duplex": True,
115
+ "paper": {"format": "a4"},
116
+ }
117
+
118
+ def __init__(self, payload: Any | None = None) -> None:
119
+ self.config = GeometryFragmentConfig.model_validate(payload or {})
120
+ self.template_path = Path(__file__).with_name("ts_geometry.tex.jinja")
121
+
122
+ def build_config(
123
+ self, context: Mapping[str, Any], overrides: Mapping[str, Any] | None = None
124
+ ) -> GeometryFragmentConfig:
125
+ _ = overrides
126
+ payload: dict[str, Any] = {}
127
+ for key in ("paper", "geometry", "duplex", "margin", "orientation", "binding", "watermark"):
128
+ if key in context:
129
+ payload[key] = context.get(key)
130
+ marks_flag = _coerce_marks_flag(context.get("marks"))
131
+ if marks_flag is not None:
132
+ payload["marks"] = marks_flag
133
+ return self.config_cls.model_validate(payload)
134
+
135
+ def inject(
136
+ self,
137
+ config: GeometryFragmentConfig,
138
+ context: dict[str, Any],
139
+ overrides: Mapping[str, Any] | None = None,
140
+ ) -> None:
141
+ # Ensure raw config fields are present before resolution.
142
+ context.update(config.to_context())
143
+ inject_geometry_context(context, overrides)
144
+
145
+ def should_render(self, config: GeometryFragmentConfig) -> bool:
146
+ _ = config
147
+ return True
148
+
149
+ # Convenience renderer for direct use
150
+ def render(self) -> str:
151
+ context = self.config.to_context()
152
+ inject_geometry_context(context)
153
+ environment = _build_environment(self.template_path.parent)
154
+ template = environment.get_template(self.template_path.name)
155
+ return template.render(**context)
156
+
157
+ def get_latex(self) -> str:
158
+ """Return the rendered LaTeX fragment."""
159
+ return self.render()
160
+
161
+ def getLatex(self) -> str: # noqa: N802 - API compatibility
162
+ """CamelCase alias for front-end callers."""
163
+ return self.render()
164
+
165
+
166
+ fragment = GeometryFragment()
167
+
168
+
169
+ __all__ = ["GeometryFragment", "GeometryFragmentConfig", "fragment"]
@@ -0,0 +1,3 @@
1
+ name = "ts-geometry"
2
+ description = "Page layout setup driven by press.paper."
3
+ entrypoint = "texsmith.fragments.geometry:fragment"