pytex-preprocessor 0.1.3__tar.gz → 0.3.0__tar.gz

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 (135) hide show
  1. pytex_preprocessor-0.3.0/PKG-INFO +254 -0
  2. pytex_preprocessor-0.3.0/README.md +240 -0
  3. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/pyproject.toml +2 -1
  4. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/__init__.py +2 -0
  5. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/commands/builtin.py +16 -13
  6. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/commands/definitions.py +47 -8
  7. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/commands/glossaries.py +4 -4
  8. pytex_preprocessor-0.3.0/src/pytex/model/comment.py +26 -0
  9. pytex_preprocessor-0.3.0/src/pytex/model/concat.py +55 -0
  10. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/model/control_sequence.py +5 -0
  11. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/model/document.py +5 -0
  12. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/model/math.py +7 -0
  13. pytex_preprocessor-0.3.0/src/pytex_analyze/__init__.py +16 -0
  14. pytex_preprocessor-0.3.0/src/pytex_analyze/analyze.py +106 -0
  15. pytex_preprocessor-0.3.0/src/pytex_analyze/optimize.py +181 -0
  16. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_builder/build.py +111 -2
  17. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_builder/console.py +3 -3
  18. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_builder/render.py +24 -16
  19. pytex_preprocessor-0.3.0/src/pytex_builder/tex2py.py +159 -0
  20. pytex_preprocessor-0.3.0/src/pytex_builder/tree.py +197 -0
  21. pytex_preprocessor-0.3.0/src/pytex_builder/variants.py +178 -0
  22. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/boxes.py +3 -3
  23. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/document.py +5 -2
  24. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/titlepage.py +4 -3
  25. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/voting.py +2 -2
  26. pytex_preprocessor-0.3.0/src/pytex_preprocessor.egg-info/PKG-INFO +254 -0
  27. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_preprocessor.egg-info/SOURCES.txt +7 -0
  28. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_preprocessor.egg-info/entry_points.txt +1 -0
  29. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_preprocessor.egg-info/top_level.txt +1 -0
  30. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_protocol/__init__.py +2 -1
  31. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_protocol/document.py +24 -6
  32. pytex_preprocessor-0.1.3/PKG-INFO +0 -82
  33. pytex_preprocessor-0.1.3/README.md +0 -68
  34. pytex_preprocessor-0.1.3/src/pytex/model/concat.py +0 -31
  35. pytex_preprocessor-0.1.3/src/pytex_preprocessor.egg-info/PKG-INFO +0 -82
  36. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/setup.cfg +0 -0
  37. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/commands/__init__.py +0 -0
  38. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/commands/biblatex.py +0 -0
  39. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/commands/captions.py +0 -0
  40. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/commands/cleveref.py +0 -0
  41. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/commands/colors.py +0 -0
  42. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/commands/conditionals.py +0 -0
  43. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/commands/counters.py +0 -0
  44. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/commands/floats.py +0 -0
  45. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/commands/font.py +0 -0
  46. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/commands/fontawesome.py +0 -0
  47. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/commands/fontspec.py +0 -0
  48. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/commands/geometry.py +0 -0
  49. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/commands/graphics.py +0 -0
  50. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/commands/hooks.py +0 -0
  51. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/commands/hyperref.py +0 -0
  52. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/commands/lengths.py +0 -0
  53. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/commands/listings.py +0 -0
  54. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/commands/mdframed.py +0 -0
  55. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/commands/picture.py +0 -0
  56. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/commands/setspace.py +0 -0
  57. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/commands/tables.py +0 -0
  58. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/helpers/__init__.py +0 -0
  59. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/helpers/coerce.py +0 -0
  60. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/helpers/parenting.py +0 -0
  61. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/helpers/sanitize.py +0 -0
  62. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/helpers/with_package.py +0 -0
  63. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/interface/__init__.py +0 -0
  64. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/interface/control_sequence.py +0 -0
  65. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/interface/package.py +0 -0
  66. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/interface/tex.py +0 -0
  67. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/model/__init__.py +0 -0
  68. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/model/color.py +0 -0
  69. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/model/document_class.py +0 -0
  70. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/model/empty.py +0 -0
  71. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/model/environment.py +0 -0
  72. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/model/image.py +0 -0
  73. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/model/include.py +0 -0
  74. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/model/length.py +0 -0
  75. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/model/package.py +0 -0
  76. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/model/raw.py +0 -0
  77. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/packages.py +0 -0
  78. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex/registry.py +0 -0
  79. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_builder/__init__.py +0 -0
  80. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_builder/tectonic.py +0 -0
  81. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/__init__.py +0 -0
  82. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Bold.ttf +0 -0
  83. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-BoldItalic.ttf +0 -0
  84. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Book.ttf +0 -0
  85. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-BookItalic.ttf +0 -0
  86. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Medium.ttf +0 -0
  87. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-MediumItalic.ttf +0 -0
  88. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Strong.ttf +0 -0
  89. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Thin.ttf +0 -0
  90. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-ThinItalic.ttf +0 -0
  91. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Black.ttf +0 -0
  92. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Bold.ttf +0 -0
  93. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-BoldItalic.ttf +0 -0
  94. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Italic.ttf +0 -0
  95. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Medium.ttf +0 -0
  96. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Regular.ttf +0 -0
  97. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/Times New Roman.ttf +0 -0
  98. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/logos/ASTA.svg +0 -0
  99. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/logos/DUMMY.png +0 -0
  100. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/logos/DUMMY_FOOT.png +0 -0
  101. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/logos/ECHO.svg +0 -0
  102. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/logos/HSRT.pdf +0 -0
  103. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/logos/INF.pdf +0 -0
  104. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/logos/STUPA.pdf +0 -0
  105. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/logos/Skyline.pdf +0 -0
  106. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/citations.py +0 -0
  107. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/cleveref_names.py +0 -0
  108. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/colors.py +0 -0
  109. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/fonts.py +0 -0
  110. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/glossary.py +0 -0
  111. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/hyperref_config.py +0 -0
  112. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/listings.py +0 -0
  113. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/logos.py +0 -0
  114. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/pagebreak.py +0 -0
  115. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/pagesetup.py +0 -0
  116. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/tex/pagesetup.tex +0 -0
  117. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/variants.py +0 -0
  118. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/watermark.py +0 -0
  119. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/wordcount.py +0 -0
  120. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_koma/__init__.py +0 -0
  121. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_koma/commands.py +0 -0
  122. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_koma/document.py +0 -0
  123. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_markdown/__init__.py +0 -0
  124. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_markdown/convert.py +0 -0
  125. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_markdown/escape.py +0 -0
  126. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_preprocessor.egg-info/dependency_links.txt +0 -0
  127. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_preprocessor.egg-info/requires.txt +0 -0
  128. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_protocol/convert.py +0 -0
  129. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_protocol/entries.py +0 -0
  130. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_protocol/frontmatter.py +0 -0
  131. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_protocol/header.py +0 -0
  132. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_protocol/shortcodes.py +0 -0
  133. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_protocol/signatures.py +0 -0
  134. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_tikz/__init__.py +0 -0
  135. {pytex_preprocessor-0.1.3 → pytex_preprocessor-0.3.0}/src/pytex_tikz/tikz.py +0 -0
@@ -0,0 +1,254 @@
1
+ Metadata-Version: 2.4
2
+ Name: pytex-preprocessor
3
+ Version: 0.3.0
4
+ Summary: Type-safe LaTeX document generation with Python
5
+ Author-email: Frederik Beimgraben <frederik@beimgraben.net>
6
+ Requires-Python: >=3.13
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: pydantic
9
+ Requires-Dist: marko
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest; extra == "dev"
12
+ Requires-Dist: ruff; extra == "dev"
13
+ Requires-Dist: basedpyright; extra == "dev"
14
+
15
+ # PyTeX
16
+
17
+ Type-safe LaTeX document generation with Python. Build a document as a tree of
18
+ typed `TeX` nodes and render it to a `.tex` file, or drop inline Python
19
+ expressions into an existing `.tex` source and have them evaluated at render
20
+ time. Requires Python 3.13+.
21
+
22
+ A `TeX` node is an immutable dataclass with a `.rendered` property. The public
23
+ API mirrors LaTeX control sequences as PascalCase factories (`Section`,
24
+ `Bold`, `Frac`, `Title`, ...), so a document reads like the LaTeX it produces
25
+ while staying checkable by a type checker. Nodes track their package
26
+ requirements, so the preamble is assembled automatically from what the body
27
+ uses.
28
+
29
+ ## Install
30
+
31
+ To use the `pytex` command anywhere, install it as an isolated tool with
32
+ [pipx](https://pipx.pypa.io/):
33
+
34
+ ```sh
35
+ pipx install pytex-preprocessor
36
+ ```
37
+
38
+ It is also available via plain `pip install pytex-preprocessor`.
39
+
40
+ For development, work in a virtualenv with an editable install instead:
41
+
42
+ ```sh
43
+ python -m venv venv && . venv/bin/activate
44
+ pip install -e . # add [dev] for pytest, ruff, basedpyright
45
+ ```
46
+
47
+ External tools, each needed only for the matching feature:
48
+
49
+ - `tectonic` — compile to PDF (`--build`). If not on `PATH`, the build
50
+ downloads a self-contained binary into a temp folder and reuses it.
51
+ - `inkscape` — `SVG` image conversion.
52
+ - `makeindex` (from a TeX distribution, e.g. TeX Live) — resolve
53
+ glossaries/acronyms.
54
+
55
+ ## Quick start
56
+
57
+ A `.tex.py` file is plain Python exposing a module-level `__pytex__` that holds
58
+ a `TeX` node:
59
+
60
+ ```py
61
+ from pytex.commands.builtin import Bold, Emph, Section, Title, MakeTitle
62
+ from pytex.model.concat import Concat
63
+ from pytex.model.document import Document
64
+ from pytex.model.math import DisplayMath, Frac
65
+
66
+ __pytex__ = Document(
67
+ preamble=Title("PyTeX Example"),
68
+ body=Concat(
69
+ MakeTitle(),
70
+ Section("Text"),
71
+ "A paragraph with ", Bold("bold"), " and ", Emph("emphasised"), " words.",
72
+ Section("Math"),
73
+ DisplayMath(Concat("x = ", Frac("-b", "2a"))),
74
+ ),
75
+ )
76
+ ```
77
+
78
+ ```sh
79
+ pytex example.tex.py # render -> build/example.out.tex
80
+ pytex example.tex.py --build # render + compile -> build/example.out.pdf
81
+ ```
82
+
83
+ Bare strings are coerced to text nodes and LaTeX-escaped.
84
+
85
+ ## The `pytex` command
86
+
87
+ The input file is dispatched by extension:
88
+
89
+ | Extension | Handling |
90
+ | --- | --- |
91
+ | `.py` | imported as a module; its `__pytex__` node is rendered. Convention: name it `<doc>.tex.py`. |
92
+ | `.tex` | wrapped in `IncludeTeX`; inline `\iffalse{pytex(...)}\fi` markers are evaluated, then rendered. Convention: `<doc>.py.tex`. |
93
+ | `.md` / `.markdown` | converted to nodes and wrapped in a document according to `--variant` (see below). Without `--variant` the style is auto-detected. |
94
+
95
+ ### Inline replacements in `.tex`
96
+
97
+ Any registered factory is in scope inside a marker. The `\iffalse ... \fi` pair
98
+ is a LaTeX no-op, so the source still compiles as-is without PyTeX:
99
+
100
+ ```tex
101
+ Today is \iffalse{pytex(Today())}\fi.
102
+ A fraction: $\iffalse{pytex(Frac("1", "2"))}\fi$.
103
+ Plain Python works too: $3^2 = \iffalse{pytex(3 ** 2)}\fi$.
104
+ ```
105
+
106
+ ### Options
107
+
108
+ | Flag | Default | Meaning |
109
+ | --- | --- | --- |
110
+ | `-o`, `--output` | `<build-dir>/<input>.out.tex` | rendered LaTeX output path |
111
+ | `-b`, `--build` | off | compile the rendered `.tex` to PDF with tectonic |
112
+ | `--build-dir DIR` | `build` | directory for artifacts and tectonic output |
113
+ | `--no-shell-escape` | shell-escape on | disable shell-escape |
114
+ | `-t`, `--tree` | off | also print the input's `TeX`-node tree (`tree`-style) before rendering/building |
115
+ | `-f`, `--force` | off | skip the optimize + analysis pass and build even if problems are found |
116
+ | `--variant STYLE` | auto-detect | Markdown output style (`plain`, `report`, `protocol-asta`, `protocol-stupa`) |
117
+ | `--config JSON` | none | JSON object of document-class params, merged over the frontmatter |
118
+
119
+ Shell-escape is on by default because inline images decode their base64
120
+ payloads at compile time. The build runs tectonic, then `makeindex` (for
121
+ `glossaries`/acronyms), then reruns tectonic when an index changed.
122
+
123
+ Output is minimal and color-tagged (`==>`, `note:`, `warning:`, `error:`),
124
+ following tectonic's style; on failure it points at the likely cause and the
125
+ log file. Set `NO_COLOR` to disable color.
126
+
127
+ ### Pre-flight optimize + analysis
128
+
129
+ Before rendering, the builder runs two render-equivalent passes over the node
130
+ tree. First `Optimize` tidies the tree (flatten nested `Concat`s, drop empty
131
+ nodes, turn whole-`Raw` LaTeX constructs into native nodes) without changing
132
+ the output (it also expands inline `pytex(...)` markers and turns `Raw`
133
+ comments and math — `\[...\]`, `\(...\)`, `$...$` — into native nodes). Then
134
+ `pytex_analyze` checks for problems that LaTeX would only surface later (or
135
+ silently):
136
+
137
+ - references (`\ref`, `\cref`, `\autoref`, ...) to a label that is never
138
+ defined,
139
+ - labels defined more than once,
140
+ - `\includegraphics` paths that do not exist on disk.
141
+
142
+ Missing-image issues are errors and abort the build; the rest are warnings.
143
+ Pass `-f`/`--force` to skip both passes and build regardless.
144
+
145
+ ### Inspecting the node tree
146
+
147
+ `--tree` prints the parsed `TeX`-node tree (then renders/builds as usual),
148
+ useful for debugging how an input maps to nodes. Nodes that require a package
149
+ are tagged with it (`[+package]`):
150
+
151
+ ```
152
+ $ pytex example.tex.py --tree
153
+ Document (article)
154
+ ├── ControlSequence \title
155
+ │ └── Parameter { }
156
+ │ └── Raw "PyTeX Example"
157
+ └── Concat
158
+ ├── ControlSequence \maketitle
159
+ ├── ControlSequence \cref [+cleveref]
160
+ └── ...
161
+ ```
162
+
163
+ ## Packages
164
+
165
+ `pytex` is the core; the rest are optional and build on it.
166
+
167
+ | Package | Provides |
168
+ | --- | --- |
169
+ | `pytex` | core node model, `Document`, math, tables, graphics, and factories for the common LaTeX packages (biblatex, cleveref, glossaries, hyperref, listings, ...). |
170
+ | `pytex_koma` | KOMA-Script classes and commands (`Addchap`, `Minisec`, `KOMAoptions`, ...). |
171
+ | `pytex_tikz` | TikZ pictures and primitives (`TikzPicture`, `Draw`, `Node`, `Circle`, ...). |
172
+ | `pytex_markdown` | Markdown -> native `TeX` conversion (see below). |
173
+ | `pytex_analyze` | static checks over the node tree (dangling refs, duplicate labels, missing images), plus `Optimize` to simplify a tree render-equivalently. |
174
+ | `pytex_hsrtreport` | HSRT report document class, colored callout boxes, title pages, glossary/citation helpers. |
175
+ | `pytex_protocol` | STUPA/AStA meeting minutes from Markdown, built on `pytex_hsrtreport`. |
176
+
177
+ ## Markdown
178
+
179
+ `pytex_markdown` converts Markdown to native `TeX` nodes (via `marko`):
180
+
181
+ ```py
182
+ from pytex_markdown import Markdown, IncludeMarkdown
183
+
184
+ body = Markdown("# Title\n\nText with **bold**, `code`, [a link](https://x).")
185
+ body = IncludeMarkdown("notes.md", base_level=-1) # base_level=-1: # -> \chapter
186
+ ```
187
+
188
+ Headings, emphasis, inline/fenced code, lists, links, images, block quotes and
189
+ thematic breaks map to the standard pytex library; text is LaTeX-escaped.
190
+ GitHub-style callouts become HSRT colored boxes (so the module depends on
191
+ `pytex_hsrtreport`):
192
+
193
+ ```md
194
+ > [!NOTE] -> InfoBox > [!IMPORTANT] -> ImportantBox
195
+ > [!TIP] -> SuccessBox > [!WARNING] -> WarningBox
196
+ ```
197
+
198
+ Both factories are registered, so they work in `\iffalse{pytex(...)}\fi`
199
+ replacements in `.tex` sources too.
200
+
201
+ ### Output variants
202
+
203
+ When the `pytex` command renders a `.md` file it wraps the converted nodes in a
204
+ document chosen by `--variant`:
205
+
206
+ | Variant | Document |
207
+ | --- | --- |
208
+ | `plain` | a bare `Document` (default class `article`); `#` -> `\section`. |
209
+ | `report` | an HSRT report with title page and table of contents; `#` -> `\chapter`. |
210
+ | `protocol-asta` | an AStA meeting protocol (HSRT report, AStA logos). |
211
+ | `protocol-stupa` | a StuPa meeting protocol (HSRT report, StuPa logos). |
212
+
213
+ Without `--variant`, protocol frontmatter (`gremium:` or `typ: protokoll`) picks
214
+ a protocol style and everything else falls back to `plain`.
215
+
216
+ Document-class parameters come from the YAML frontmatter and from `--config`
217
+ (a JSON object that overrides the frontmatter), e.g.:
218
+
219
+ ```sh
220
+ pytex notes.md --variant plain --config '{"documentclass": "scrartcl", "classoptions": ["11pt", "twocolumn"]}'
221
+ ```
222
+
223
+ `classoptions` accepts a list (`"twocolumn"`, `"DIV=12"`) or a `{key: value}`
224
+ object. For styles with a title page (`report`), the title is taken from
225
+ `title:`/`--config` if given, otherwise from the first `#` heading (which is then
226
+ not also rendered as a chapter).
227
+
228
+ ## Converting LaTeX to PyTeX
229
+
230
+ `pytex-tex2py` turns an existing `.tex` file into an equivalent `.tex.py`
231
+ source. It reads the file, runs `Optimize` over it (expanding inline
232
+ `pytex(...)` markers and recognising comments and math), and serialises the
233
+ result to Python that rebuilds the same tree:
234
+
235
+ ```sh
236
+ pytex-tex2py paper.tex # -> paper.tex.py
237
+ pytex-tex2py paper.tex -o out.py
238
+ ```
239
+
240
+ Rendering the generated `.tex.py` reproduces the original output byte-for-byte;
241
+ nodes the serialiser does not special-case fall back to a literal `Raw`, so the
242
+ conversion always round-trips.
243
+
244
+ ## Examples
245
+
246
+ See `examples/` for one minimal input per kind (`.tex.py`, `.py.tex`, `.md`,
247
+ mixed, and a full HSRT report). Run from the repository root so relative paths
248
+ resolve:
249
+
250
+ ```sh
251
+ pytex examples/document.tex.py --build
252
+ pytex examples/replacements.py.tex --build
253
+ pytex examples/notes.md --build
254
+ ```
@@ -0,0 +1,240 @@
1
+ # PyTeX
2
+
3
+ Type-safe LaTeX document generation with Python. Build a document as a tree of
4
+ typed `TeX` nodes and render it to a `.tex` file, or drop inline Python
5
+ expressions into an existing `.tex` source and have them evaluated at render
6
+ time. Requires Python 3.13+.
7
+
8
+ A `TeX` node is an immutable dataclass with a `.rendered` property. The public
9
+ API mirrors LaTeX control sequences as PascalCase factories (`Section`,
10
+ `Bold`, `Frac`, `Title`, ...), so a document reads like the LaTeX it produces
11
+ while staying checkable by a type checker. Nodes track their package
12
+ requirements, so the preamble is assembled automatically from what the body
13
+ uses.
14
+
15
+ ## Install
16
+
17
+ To use the `pytex` command anywhere, install it as an isolated tool with
18
+ [pipx](https://pipx.pypa.io/):
19
+
20
+ ```sh
21
+ pipx install pytex-preprocessor
22
+ ```
23
+
24
+ It is also available via plain `pip install pytex-preprocessor`.
25
+
26
+ For development, work in a virtualenv with an editable install instead:
27
+
28
+ ```sh
29
+ python -m venv venv && . venv/bin/activate
30
+ pip install -e . # add [dev] for pytest, ruff, basedpyright
31
+ ```
32
+
33
+ External tools, each needed only for the matching feature:
34
+
35
+ - `tectonic` — compile to PDF (`--build`). If not on `PATH`, the build
36
+ downloads a self-contained binary into a temp folder and reuses it.
37
+ - `inkscape` — `SVG` image conversion.
38
+ - `makeindex` (from a TeX distribution, e.g. TeX Live) — resolve
39
+ glossaries/acronyms.
40
+
41
+ ## Quick start
42
+
43
+ A `.tex.py` file is plain Python exposing a module-level `__pytex__` that holds
44
+ a `TeX` node:
45
+
46
+ ```py
47
+ from pytex.commands.builtin import Bold, Emph, Section, Title, MakeTitle
48
+ from pytex.model.concat import Concat
49
+ from pytex.model.document import Document
50
+ from pytex.model.math import DisplayMath, Frac
51
+
52
+ __pytex__ = Document(
53
+ preamble=Title("PyTeX Example"),
54
+ body=Concat(
55
+ MakeTitle(),
56
+ Section("Text"),
57
+ "A paragraph with ", Bold("bold"), " and ", Emph("emphasised"), " words.",
58
+ Section("Math"),
59
+ DisplayMath(Concat("x = ", Frac("-b", "2a"))),
60
+ ),
61
+ )
62
+ ```
63
+
64
+ ```sh
65
+ pytex example.tex.py # render -> build/example.out.tex
66
+ pytex example.tex.py --build # render + compile -> build/example.out.pdf
67
+ ```
68
+
69
+ Bare strings are coerced to text nodes and LaTeX-escaped.
70
+
71
+ ## The `pytex` command
72
+
73
+ The input file is dispatched by extension:
74
+
75
+ | Extension | Handling |
76
+ | --- | --- |
77
+ | `.py` | imported as a module; its `__pytex__` node is rendered. Convention: name it `<doc>.tex.py`. |
78
+ | `.tex` | wrapped in `IncludeTeX`; inline `\iffalse{pytex(...)}\fi` markers are evaluated, then rendered. Convention: `<doc>.py.tex`. |
79
+ | `.md` / `.markdown` | converted to nodes and wrapped in a document according to `--variant` (see below). Without `--variant` the style is auto-detected. |
80
+
81
+ ### Inline replacements in `.tex`
82
+
83
+ Any registered factory is in scope inside a marker. The `\iffalse ... \fi` pair
84
+ is a LaTeX no-op, so the source still compiles as-is without PyTeX:
85
+
86
+ ```tex
87
+ Today is \iffalse{pytex(Today())}\fi.
88
+ A fraction: $\iffalse{pytex(Frac("1", "2"))}\fi$.
89
+ Plain Python works too: $3^2 = \iffalse{pytex(3 ** 2)}\fi$.
90
+ ```
91
+
92
+ ### Options
93
+
94
+ | Flag | Default | Meaning |
95
+ | --- | --- | --- |
96
+ | `-o`, `--output` | `<build-dir>/<input>.out.tex` | rendered LaTeX output path |
97
+ | `-b`, `--build` | off | compile the rendered `.tex` to PDF with tectonic |
98
+ | `--build-dir DIR` | `build` | directory for artifacts and tectonic output |
99
+ | `--no-shell-escape` | shell-escape on | disable shell-escape |
100
+ | `-t`, `--tree` | off | also print the input's `TeX`-node tree (`tree`-style) before rendering/building |
101
+ | `-f`, `--force` | off | skip the optimize + analysis pass and build even if problems are found |
102
+ | `--variant STYLE` | auto-detect | Markdown output style (`plain`, `report`, `protocol-asta`, `protocol-stupa`) |
103
+ | `--config JSON` | none | JSON object of document-class params, merged over the frontmatter |
104
+
105
+ Shell-escape is on by default because inline images decode their base64
106
+ payloads at compile time. The build runs tectonic, then `makeindex` (for
107
+ `glossaries`/acronyms), then reruns tectonic when an index changed.
108
+
109
+ Output is minimal and color-tagged (`==>`, `note:`, `warning:`, `error:`),
110
+ following tectonic's style; on failure it points at the likely cause and the
111
+ log file. Set `NO_COLOR` to disable color.
112
+
113
+ ### Pre-flight optimize + analysis
114
+
115
+ Before rendering, the builder runs two render-equivalent passes over the node
116
+ tree. First `Optimize` tidies the tree (flatten nested `Concat`s, drop empty
117
+ nodes, turn whole-`Raw` LaTeX constructs into native nodes) without changing
118
+ the output (it also expands inline `pytex(...)` markers and turns `Raw`
119
+ comments and math — `\[...\]`, `\(...\)`, `$...$` — into native nodes). Then
120
+ `pytex_analyze` checks for problems that LaTeX would only surface later (or
121
+ silently):
122
+
123
+ - references (`\ref`, `\cref`, `\autoref`, ...) to a label that is never
124
+ defined,
125
+ - labels defined more than once,
126
+ - `\includegraphics` paths that do not exist on disk.
127
+
128
+ Missing-image issues are errors and abort the build; the rest are warnings.
129
+ Pass `-f`/`--force` to skip both passes and build regardless.
130
+
131
+ ### Inspecting the node tree
132
+
133
+ `--tree` prints the parsed `TeX`-node tree (then renders/builds as usual),
134
+ useful for debugging how an input maps to nodes. Nodes that require a package
135
+ are tagged with it (`[+package]`):
136
+
137
+ ```
138
+ $ pytex example.tex.py --tree
139
+ Document (article)
140
+ ├── ControlSequence \title
141
+ │ └── Parameter { }
142
+ │ └── Raw "PyTeX Example"
143
+ └── Concat
144
+ ├── ControlSequence \maketitle
145
+ ├── ControlSequence \cref [+cleveref]
146
+ └── ...
147
+ ```
148
+
149
+ ## Packages
150
+
151
+ `pytex` is the core; the rest are optional and build on it.
152
+
153
+ | Package | Provides |
154
+ | --- | --- |
155
+ | `pytex` | core node model, `Document`, math, tables, graphics, and factories for the common LaTeX packages (biblatex, cleveref, glossaries, hyperref, listings, ...). |
156
+ | `pytex_koma` | KOMA-Script classes and commands (`Addchap`, `Minisec`, `KOMAoptions`, ...). |
157
+ | `pytex_tikz` | TikZ pictures and primitives (`TikzPicture`, `Draw`, `Node`, `Circle`, ...). |
158
+ | `pytex_markdown` | Markdown -> native `TeX` conversion (see below). |
159
+ | `pytex_analyze` | static checks over the node tree (dangling refs, duplicate labels, missing images), plus `Optimize` to simplify a tree render-equivalently. |
160
+ | `pytex_hsrtreport` | HSRT report document class, colored callout boxes, title pages, glossary/citation helpers. |
161
+ | `pytex_protocol` | STUPA/AStA meeting minutes from Markdown, built on `pytex_hsrtreport`. |
162
+
163
+ ## Markdown
164
+
165
+ `pytex_markdown` converts Markdown to native `TeX` nodes (via `marko`):
166
+
167
+ ```py
168
+ from pytex_markdown import Markdown, IncludeMarkdown
169
+
170
+ body = Markdown("# Title\n\nText with **bold**, `code`, [a link](https://x).")
171
+ body = IncludeMarkdown("notes.md", base_level=-1) # base_level=-1: # -> \chapter
172
+ ```
173
+
174
+ Headings, emphasis, inline/fenced code, lists, links, images, block quotes and
175
+ thematic breaks map to the standard pytex library; text is LaTeX-escaped.
176
+ GitHub-style callouts become HSRT colored boxes (so the module depends on
177
+ `pytex_hsrtreport`):
178
+
179
+ ```md
180
+ > [!NOTE] -> InfoBox > [!IMPORTANT] -> ImportantBox
181
+ > [!TIP] -> SuccessBox > [!WARNING] -> WarningBox
182
+ ```
183
+
184
+ Both factories are registered, so they work in `\iffalse{pytex(...)}\fi`
185
+ replacements in `.tex` sources too.
186
+
187
+ ### Output variants
188
+
189
+ When the `pytex` command renders a `.md` file it wraps the converted nodes in a
190
+ document chosen by `--variant`:
191
+
192
+ | Variant | Document |
193
+ | --- | --- |
194
+ | `plain` | a bare `Document` (default class `article`); `#` -> `\section`. |
195
+ | `report` | an HSRT report with title page and table of contents; `#` -> `\chapter`. |
196
+ | `protocol-asta` | an AStA meeting protocol (HSRT report, AStA logos). |
197
+ | `protocol-stupa` | a StuPa meeting protocol (HSRT report, StuPa logos). |
198
+
199
+ Without `--variant`, protocol frontmatter (`gremium:` or `typ: protokoll`) picks
200
+ a protocol style and everything else falls back to `plain`.
201
+
202
+ Document-class parameters come from the YAML frontmatter and from `--config`
203
+ (a JSON object that overrides the frontmatter), e.g.:
204
+
205
+ ```sh
206
+ pytex notes.md --variant plain --config '{"documentclass": "scrartcl", "classoptions": ["11pt", "twocolumn"]}'
207
+ ```
208
+
209
+ `classoptions` accepts a list (`"twocolumn"`, `"DIV=12"`) or a `{key: value}`
210
+ object. For styles with a title page (`report`), the title is taken from
211
+ `title:`/`--config` if given, otherwise from the first `#` heading (which is then
212
+ not also rendered as a chapter).
213
+
214
+ ## Converting LaTeX to PyTeX
215
+
216
+ `pytex-tex2py` turns an existing `.tex` file into an equivalent `.tex.py`
217
+ source. It reads the file, runs `Optimize` over it (expanding inline
218
+ `pytex(...)` markers and recognising comments and math), and serialises the
219
+ result to Python that rebuilds the same tree:
220
+
221
+ ```sh
222
+ pytex-tex2py paper.tex # -> paper.tex.py
223
+ pytex-tex2py paper.tex -o out.py
224
+ ```
225
+
226
+ Rendering the generated `.tex.py` reproduces the original output byte-for-byte;
227
+ nodes the serialiser does not special-case fall back to a literal `Raw`, so the
228
+ conversion always round-trips.
229
+
230
+ ## Examples
231
+
232
+ See `examples/` for one minimal input per kind (`.tex.py`, `.py.tex`, `.md`,
233
+ mixed, and a full HSRT report). Run from the repository root so relative paths
234
+ resolve:
235
+
236
+ ```sh
237
+ pytex examples/document.tex.py --build
238
+ pytex examples/replacements.py.tex --build
239
+ pytex examples/notes.md --build
240
+ ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pytex-preprocessor"
7
- version = "0.1.3"
7
+ version = "0.3.0"
8
8
  authors = [
9
9
  { name="Frederik Beimgraben", email="frederik@beimgraben.net" },
10
10
  ]
@@ -18,6 +18,7 @@ dev = ["pytest", "ruff", "basedpyright"]
18
18
 
19
19
  [project.scripts]
20
20
  pytex = "pytex_builder.build:main"
21
+ pytex-tex2py = "pytex_builder.tex2py:main"
21
22
 
22
23
  [tool.setuptools.packages.find]
23
24
  where = ["src"]
@@ -27,6 +27,7 @@ from .commands import (
27
27
  from .helpers import coerce, sanitize, with_package
28
28
  from .model import (
29
29
  color,
30
+ comment,
30
31
  concat,
31
32
  control_sequence,
32
33
  document,
@@ -51,6 +52,7 @@ __all__ = [
51
52
  "coerce",
52
53
  "color",
53
54
  "colors",
55
+ "comment",
54
56
  "concat",
55
57
  "conditionals",
56
58
  "control_sequence",
@@ -33,12 +33,12 @@ __all__ = [
33
33
  "Group",
34
34
  "Hfill",
35
35
  "Hspace",
36
+ "HspaceStar",
36
37
  "Immediate",
37
38
  "Include",
38
39
  "IncludeOnly",
39
40
  "Indent",
40
41
  "Input",
41
- "Inputfile",
42
42
  "Italic",
43
43
  "Item",
44
44
  "Itemize",
@@ -93,6 +93,7 @@ __all__ = [
93
93
  "Verse",
94
94
  "Vfill",
95
95
  "Vspace",
96
+ "VspaceStar",
96
97
  "Whiledo",
97
98
  "Write18",
98
99
  ]
@@ -264,15 +265,23 @@ def Pagebreak(n: int | None = None) -> TeX:
264
265
 
265
266
 
266
267
  @Registry.add
267
- def Hspace(amount: str, star: bool = False) -> TeX:
268
- name = "hspace*" if star else "hspace"
269
- return ControlSequence(name, (Parameter(amount),))
268
+ def Hspace(amount: str) -> TeX:
269
+ return ControlSequence("hspace", (Parameter(amount),))
270
270
 
271
271
 
272
272
  @Registry.add
273
- def Vspace(amount: str, star: bool = False) -> TeX:
274
- name = "vspace*" if star else "vspace"
275
- return ControlSequence(name, (Parameter(amount),))
273
+ def HspaceStar(amount: str) -> TeX:
274
+ return ControlSequence("hspace*", (Parameter(amount),))
275
+
276
+
277
+ @Registry.add
278
+ def Vspace(amount: str) -> TeX:
279
+ return ControlSequence("vspace", (Parameter(amount),))
280
+
281
+
282
+ @Registry.add
283
+ def VspaceStar(amount: str) -> TeX:
284
+ return ControlSequence("vspace*", (Parameter(amount),))
276
285
 
277
286
 
278
287
  @Registry.add
@@ -552,12 +561,6 @@ def Verbatiminput(path: str) -> TeX:
552
561
  return ControlSequence("verbatiminput", (Parameter(path),))
553
562
 
554
563
 
555
- @Registry.add
556
- def Inputfile(path: str) -> TeX:
557
- """Alias for `\\input{path}` — `Input` already defined above as Input(path)."""
558
- return ControlSequence("input", (Parameter(path),))
559
-
560
-
561
564
  @Registry.add
562
565
  def Whiledo(condition: TeX | str, body: TeX | str) -> TeX:
563
566
  return ControlSequence("whiledo", (Parameter(condition), Parameter(body)))
@@ -5,11 +5,15 @@ from ..registry import Registry
5
5
 
6
6
  __all__ = [
7
7
  "DeclareRobustCommand",
8
+ "DeclareRobustCommandStar",
8
9
  "Def",
9
10
  "Newcommand",
11
+ "NewcommandStar",
10
12
  "Newenvironment",
11
13
  "Providecommand",
14
+ "ProvidecommandStar",
12
15
  "Renewcommand",
16
+ "RenewcommandStar",
13
17
  "Renewenvironment",
14
18
  ]
15
19
 
@@ -38,9 +42,18 @@ def Newcommand(
38
42
  body: TeX | str,
39
43
  nargs: int | None = None,
40
44
  default: str | None = None,
41
- star: bool = False,
42
45
  ) -> TeX:
43
- return _cmd("newcommand", cs, nargs, default, body, star)
46
+ return _cmd("newcommand", cs, nargs, default, body, star=False)
47
+
48
+
49
+ @Registry.add
50
+ def NewcommandStar(
51
+ cs: str,
52
+ body: TeX | str,
53
+ nargs: int | None = None,
54
+ default: str | None = None,
55
+ ) -> TeX:
56
+ return _cmd("newcommand", cs, nargs, default, body, star=True)
44
57
 
45
58
 
46
59
  @Registry.add
@@ -49,9 +62,18 @@ def Renewcommand(
49
62
  body: TeX | str,
50
63
  nargs: int | None = None,
51
64
  default: str | None = None,
52
- star: bool = False,
53
65
  ) -> TeX:
54
- return _cmd("renewcommand", cs, nargs, default, body, star)
66
+ return _cmd("renewcommand", cs, nargs, default, body, star=False)
67
+
68
+
69
+ @Registry.add
70
+ def RenewcommandStar(
71
+ cs: str,
72
+ body: TeX | str,
73
+ nargs: int | None = None,
74
+ default: str | None = None,
75
+ ) -> TeX:
76
+ return _cmd("renewcommand", cs, nargs, default, body, star=True)
55
77
 
56
78
 
57
79
  @Registry.add
@@ -60,9 +82,18 @@ def Providecommand(
60
82
  body: TeX | str,
61
83
  nargs: int | None = None,
62
84
  default: str | None = None,
63
- star: bool = False,
64
85
  ) -> TeX:
65
- return _cmd("providecommand", cs, nargs, default, body, star)
86
+ return _cmd("providecommand", cs, nargs, default, body, star=False)
87
+
88
+
89
+ @Registry.add
90
+ def ProvidecommandStar(
91
+ cs: str,
92
+ body: TeX | str,
93
+ nargs: int | None = None,
94
+ default: str | None = None,
95
+ ) -> TeX:
96
+ return _cmd("providecommand", cs, nargs, default, body, star=True)
66
97
 
67
98
 
68
99
  @Registry.add
@@ -70,9 +101,17 @@ def DeclareRobustCommand(
70
101
  cs: str,
71
102
  body: TeX | str,
72
103
  nargs: int | None = None,
73
- star: bool = False,
74
104
  ) -> TeX:
75
- return _cmd("DeclareRobustCommand", cs, nargs, None, body, star)
105
+ return _cmd("DeclareRobustCommand", cs, nargs, None, body, star=False)
106
+
107
+
108
+ @Registry.add
109
+ def DeclareRobustCommandStar(
110
+ cs: str,
111
+ body: TeX | str,
112
+ nargs: int | None = None,
113
+ ) -> TeX:
114
+ return _cmd("DeclareRobustCommand", cs, nargs, None, body, star=True)
76
115
 
77
116
 
78
117
  @Registry.add