pytex-preprocessor 0.2.0__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 (132) hide show
  1. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/PKG-INFO +86 -2
  2. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/README.md +85 -1
  3. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/pyproject.toml +2 -1
  4. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/__init__.py +2 -0
  5. pytex_preprocessor-0.3.0/src/pytex/model/comment.py +26 -0
  6. pytex_preprocessor-0.3.0/src/pytex/model/concat.py +55 -0
  7. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/model/control_sequence.py +5 -0
  8. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/model/document.py +5 -0
  9. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/model/math.py +7 -0
  10. pytex_preprocessor-0.3.0/src/pytex_analyze/__init__.py +16 -0
  11. pytex_preprocessor-0.3.0/src/pytex_analyze/analyze.py +106 -0
  12. pytex_preprocessor-0.3.0/src/pytex_analyze/optimize.py +181 -0
  13. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_builder/build.py +111 -2
  14. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_builder/console.py +3 -3
  15. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_builder/render.py +24 -16
  16. pytex_preprocessor-0.3.0/src/pytex_builder/tex2py.py +159 -0
  17. pytex_preprocessor-0.3.0/src/pytex_builder/tree.py +197 -0
  18. pytex_preprocessor-0.3.0/src/pytex_builder/variants.py +178 -0
  19. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/document.py +5 -2
  20. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_preprocessor.egg-info/PKG-INFO +86 -2
  21. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_preprocessor.egg-info/SOURCES.txt +7 -0
  22. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_preprocessor.egg-info/entry_points.txt +1 -0
  23. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_preprocessor.egg-info/top_level.txt +1 -0
  24. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_protocol/__init__.py +2 -1
  25. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_protocol/document.py +24 -6
  26. pytex_preprocessor-0.2.0/src/pytex/model/concat.py +0 -31
  27. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/setup.cfg +0 -0
  28. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/commands/__init__.py +0 -0
  29. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/commands/biblatex.py +0 -0
  30. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/commands/builtin.py +0 -0
  31. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/commands/captions.py +0 -0
  32. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/commands/cleveref.py +0 -0
  33. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/commands/colors.py +0 -0
  34. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/commands/conditionals.py +0 -0
  35. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/commands/counters.py +0 -0
  36. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/commands/definitions.py +0 -0
  37. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/commands/floats.py +0 -0
  38. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/commands/font.py +0 -0
  39. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/commands/fontawesome.py +0 -0
  40. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/commands/fontspec.py +0 -0
  41. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/commands/geometry.py +0 -0
  42. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/commands/glossaries.py +0 -0
  43. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/commands/graphics.py +0 -0
  44. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/commands/hooks.py +0 -0
  45. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/commands/hyperref.py +0 -0
  46. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/commands/lengths.py +0 -0
  47. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/commands/listings.py +0 -0
  48. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/commands/mdframed.py +0 -0
  49. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/commands/picture.py +0 -0
  50. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/commands/setspace.py +0 -0
  51. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/commands/tables.py +0 -0
  52. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/helpers/__init__.py +0 -0
  53. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/helpers/coerce.py +0 -0
  54. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/helpers/parenting.py +0 -0
  55. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/helpers/sanitize.py +0 -0
  56. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/helpers/with_package.py +0 -0
  57. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/interface/__init__.py +0 -0
  58. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/interface/control_sequence.py +0 -0
  59. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/interface/package.py +0 -0
  60. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/interface/tex.py +0 -0
  61. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/model/__init__.py +0 -0
  62. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/model/color.py +0 -0
  63. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/model/document_class.py +0 -0
  64. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/model/empty.py +0 -0
  65. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/model/environment.py +0 -0
  66. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/model/image.py +0 -0
  67. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/model/include.py +0 -0
  68. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/model/length.py +0 -0
  69. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/model/package.py +0 -0
  70. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/model/raw.py +0 -0
  71. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/packages.py +0 -0
  72. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex/registry.py +0 -0
  73. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_builder/__init__.py +0 -0
  74. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_builder/tectonic.py +0 -0
  75. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/__init__.py +0 -0
  76. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Bold.ttf +0 -0
  77. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-BoldItalic.ttf +0 -0
  78. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Book.ttf +0 -0
  79. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-BookItalic.ttf +0 -0
  80. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Medium.ttf +0 -0
  81. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-MediumItalic.ttf +0 -0
  82. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Strong.ttf +0 -0
  83. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Thin.ttf +0 -0
  84. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-ThinItalic.ttf +0 -0
  85. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Black.ttf +0 -0
  86. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Bold.ttf +0 -0
  87. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-BoldItalic.ttf +0 -0
  88. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Italic.ttf +0 -0
  89. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Medium.ttf +0 -0
  90. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Regular.ttf +0 -0
  91. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/fonts/Times New Roman.ttf +0 -0
  92. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/logos/ASTA.svg +0 -0
  93. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/logos/DUMMY.png +0 -0
  94. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/logos/DUMMY_FOOT.png +0 -0
  95. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/logos/ECHO.svg +0 -0
  96. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/logos/HSRT.pdf +0 -0
  97. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/logos/INF.pdf +0 -0
  98. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/logos/STUPA.pdf +0 -0
  99. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/assets/logos/Skyline.pdf +0 -0
  100. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/boxes.py +0 -0
  101. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/citations.py +0 -0
  102. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/cleveref_names.py +0 -0
  103. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/colors.py +0 -0
  104. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/fonts.py +0 -0
  105. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/glossary.py +0 -0
  106. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/hyperref_config.py +0 -0
  107. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/listings.py +0 -0
  108. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/logos.py +0 -0
  109. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/pagebreak.py +0 -0
  110. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/pagesetup.py +0 -0
  111. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/tex/pagesetup.tex +0 -0
  112. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/titlepage.py +0 -0
  113. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/variants.py +0 -0
  114. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/voting.py +0 -0
  115. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/watermark.py +0 -0
  116. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_hsrtreport/wordcount.py +0 -0
  117. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_koma/__init__.py +0 -0
  118. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_koma/commands.py +0 -0
  119. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_koma/document.py +0 -0
  120. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_markdown/__init__.py +0 -0
  121. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_markdown/convert.py +0 -0
  122. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_markdown/escape.py +0 -0
  123. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_preprocessor.egg-info/dependency_links.txt +0 -0
  124. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_preprocessor.egg-info/requires.txt +0 -0
  125. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_protocol/convert.py +0 -0
  126. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_protocol/entries.py +0 -0
  127. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_protocol/frontmatter.py +0 -0
  128. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_protocol/header.py +0 -0
  129. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_protocol/shortcodes.py +0 -0
  130. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_protocol/signatures.py +0 -0
  131. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_tikz/__init__.py +0 -0
  132. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.3.0}/src/pytex_tikz/tikz.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytex-preprocessor
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Type-safe LaTeX document generation with Python
5
5
  Author-email: Frederik Beimgraben <frederik@beimgraben.net>
6
6
  Requires-Python: >=3.13
@@ -90,7 +90,7 @@ The input file is dispatched by extension:
90
90
  | --- | --- |
91
91
  | `.py` | imported as a module; its `__pytex__` node is rendered. Convention: name it `<doc>.tex.py`. |
92
92
  | `.tex` | wrapped in `IncludeTeX`; inline `\iffalse{pytex(...)}\fi` markers are evaluated, then rendered. Convention: `<doc>.py.tex`. |
93
- | `.md` / `.markdown` | converted to nodes via `IncludeMarkdown`. Frontmatter with `gremium:` or `typ: protokoll` routes to the protocol renderer instead. |
93
+ | `.md` / `.markdown` | converted to nodes and wrapped in a document according to `--variant` (see below). Without `--variant` the style is auto-detected. |
94
94
 
95
95
  ### Inline replacements in `.tex`
96
96
 
@@ -111,6 +111,10 @@ Plain Python works too: $3^2 = \iffalse{pytex(3 ** 2)}\fi$.
111
111
  | `-b`, `--build` | off | compile the rendered `.tex` to PDF with tectonic |
112
112
  | `--build-dir DIR` | `build` | directory for artifacts and tectonic output |
113
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 |
114
118
 
115
119
  Shell-escape is on by default because inline images decode their base64
116
120
  payloads at compile time. The build runs tectonic, then `makeindex` (for
@@ -120,6 +124,42 @@ Output is minimal and color-tagged (`==>`, `note:`, `warning:`, `error:`),
120
124
  following tectonic's style; on failure it points at the likely cause and the
121
125
  log file. Set `NO_COLOR` to disable color.
122
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
+
123
163
  ## Packages
124
164
 
125
165
  `pytex` is the core; the rest are optional and build on it.
@@ -130,6 +170,7 @@ log file. Set `NO_COLOR` to disable color.
130
170
  | `pytex_koma` | KOMA-Script classes and commands (`Addchap`, `Minisec`, `KOMAoptions`, ...). |
131
171
  | `pytex_tikz` | TikZ pictures and primitives (`TikzPicture`, `Draw`, `Node`, `Circle`, ...). |
132
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. |
133
174
  | `pytex_hsrtreport` | HSRT report document class, colored callout boxes, title pages, glossary/citation helpers. |
134
175
  | `pytex_protocol` | STUPA/AStA meeting minutes from Markdown, built on `pytex_hsrtreport`. |
135
176
 
@@ -157,6 +198,49 @@ GitHub-style callouts become HSRT colored boxes (so the module depends on
157
198
  Both factories are registered, so they work in `\iffalse{pytex(...)}\fi`
158
199
  replacements in `.tex` sources too.
159
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
+
160
244
  ## Examples
161
245
 
162
246
  See `examples/` for one minimal input per kind (`.tex.py`, `.py.tex`, `.md`,
@@ -76,7 +76,7 @@ The input file is dispatched by extension:
76
76
  | --- | --- |
77
77
  | `.py` | imported as a module; its `__pytex__` node is rendered. Convention: name it `<doc>.tex.py`. |
78
78
  | `.tex` | wrapped in `IncludeTeX`; inline `\iffalse{pytex(...)}\fi` markers are evaluated, then rendered. Convention: `<doc>.py.tex`. |
79
- | `.md` / `.markdown` | converted to nodes via `IncludeMarkdown`. Frontmatter with `gremium:` or `typ: protokoll` routes to the protocol renderer instead. |
79
+ | `.md` / `.markdown` | converted to nodes and wrapped in a document according to `--variant` (see below). Without `--variant` the style is auto-detected. |
80
80
 
81
81
  ### Inline replacements in `.tex`
82
82
 
@@ -97,6 +97,10 @@ Plain Python works too: $3^2 = \iffalse{pytex(3 ** 2)}\fi$.
97
97
  | `-b`, `--build` | off | compile the rendered `.tex` to PDF with tectonic |
98
98
  | `--build-dir DIR` | `build` | directory for artifacts and tectonic output |
99
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 |
100
104
 
101
105
  Shell-escape is on by default because inline images decode their base64
102
106
  payloads at compile time. The build runs tectonic, then `makeindex` (for
@@ -106,6 +110,42 @@ Output is minimal and color-tagged (`==>`, `note:`, `warning:`, `error:`),
106
110
  following tectonic's style; on failure it points at the likely cause and the
107
111
  log file. Set `NO_COLOR` to disable color.
108
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
+
109
149
  ## Packages
110
150
 
111
151
  `pytex` is the core; the rest are optional and build on it.
@@ -116,6 +156,7 @@ log file. Set `NO_COLOR` to disable color.
116
156
  | `pytex_koma` | KOMA-Script classes and commands (`Addchap`, `Minisec`, `KOMAoptions`, ...). |
117
157
  | `pytex_tikz` | TikZ pictures and primitives (`TikzPicture`, `Draw`, `Node`, `Circle`, ...). |
118
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. |
119
160
  | `pytex_hsrtreport` | HSRT report document class, colored callout boxes, title pages, glossary/citation helpers. |
120
161
  | `pytex_protocol` | STUPA/AStA meeting minutes from Markdown, built on `pytex_hsrtreport`. |
121
162
 
@@ -143,6 +184,49 @@ GitHub-style callouts become HSRT colored boxes (so the module depends on
143
184
  Both factories are registered, so they work in `\iffalse{pytex(...)}\fi`
144
185
  replacements in `.tex` sources too.
145
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
+
146
230
  ## Examples
147
231
 
148
232
  See `examples/` for one minimal input per kind (`.tex.py`, `.py.tex`, `.md`,
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pytex-preprocessor"
7
- version = "0.2.0"
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",
@@ -0,0 +1,26 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Final, override
3
+
4
+ from ..interface.tex import TeX
5
+ from ..registry import Registry
6
+
7
+ __all__ = ["Comment"]
8
+
9
+
10
+ @Registry.add
11
+ @dataclass(frozen=True)
12
+ class Comment(TeX):
13
+ """A LaTeX comment: ``%`` followed by text and the terminating newline.
14
+
15
+ `text` is the content between the ``%`` and the end of the line (the
16
+ leading space in ``% note`` is part of it). The trailing newline *is* part
17
+ of the rendered output, so a comment ends its line as written.
18
+ """
19
+
20
+ text: Final[str]
21
+ _parent: "TeX | None" = field(default=None, init=False, compare=False, repr=False)
22
+
23
+ @property
24
+ @override
25
+ def rendered(self) -> str:
26
+ return f"%{self.text}\n"
@@ -0,0 +1,55 @@
1
+ from dataclasses import dataclass
2
+ from typing import Final, override
3
+
4
+ from ..helpers.coerce import coerce_tex
5
+ from ..helpers.parenting import attach
6
+ from ..interface.tex import TeX
7
+ from ..registry import Registry
8
+ from .empty import Empty, EmptyTeX
9
+ from .raw import Raw
10
+
11
+ __all__ = ["Concat"]
12
+
13
+
14
+ def _is_empty(node: TeX) -> bool:
15
+ """A node that renders to nothing and can be dropped from a `Concat`."""
16
+ return isinstance(node, EmptyTeX) or (isinstance(node, Raw) and node.content == "")
17
+
18
+
19
+ @Registry.add
20
+ @dataclass(frozen=True, init=False)
21
+ class Concat(TeX):
22
+ elements: Final[tuple[TeX, ...]]
23
+
24
+ def __new__(cls, *elements: TeX | str) -> TeX:
25
+ coerced = tuple(
26
+ node for node in map(coerce_tex, elements) if not _is_empty(node)
27
+ )
28
+ # Collapse trivial concatenations: nothing -> Empty, a single child ->
29
+ # that child unwrapped. (`__init__` is a no-op, so returning a node of
30
+ # any type here is safe.)
31
+ if not coerced:
32
+ return Empty
33
+ if len(coerced) == 1:
34
+ return coerced[0]
35
+ instance = super().__new__(cls)
36
+ object.__setattr__(instance, "elements", coerced)
37
+ object.__setattr__(instance, "_parent", None)
38
+ attach(instance, *coerced)
39
+ return instance
40
+
41
+ def __init__(self, *elements: TeX | str) -> None:
42
+ # All construction happens in `__new__`; this keeps the call signature
43
+ # and prevents dataclass from generating an `__init__` that would
44
+ # overwrite the already-built instance.
45
+ pass
46
+
47
+ @property
48
+ @override
49
+ def children(self) -> tuple[TeX, ...]:
50
+ return self.elements
51
+
52
+ @property
53
+ @override
54
+ def rendered(self) -> str:
55
+ return "".join(str(e) for e in self.elements)
@@ -60,6 +60,11 @@ class ControlSequence[P: Parameters](TeX):
60
60
  if self.params is not None:
61
61
  attach(self, *self.params)
62
62
 
63
+ @property
64
+ @override
65
+ def children(self) -> tuple[TeX, ...]:
66
+ return tuple(self.params or ())
67
+
63
68
  @property
64
69
  @override
65
70
  def requires(self) -> frozenset[PackageProtocol]:
@@ -29,6 +29,11 @@ class Document(TeX):
29
29
  def __post_init__(self) -> None:
30
30
  attach(self, self.body, self.preamble)
31
31
 
32
+ @property
33
+ @override
34
+ def children(self) -> tuple[TeX, ...]:
35
+ return (coerce_tex(self.preamble), coerce_tex(self.body))
36
+
32
37
  @property
33
38
  def packages(self) -> frozenset[PackageProtocol]:
34
39
  def get_packages(obj: TeX, found: set[PackageProtocol]) -> None:
@@ -28,6 +28,7 @@ __all__ = [
28
28
  "Hat",
29
29
  "IIInt",
30
30
  "IInt",
31
+ "InlineMath",
31
32
  "Int",
32
33
  "LabelM",
33
34
  "Lim",
@@ -75,6 +76,12 @@ def Math(body: TeX | str) -> TeX:
75
76
  )
76
77
 
77
78
 
79
+ @Registry.add
80
+ def InlineMath(body: TeX | str) -> TeX:
81
+ """Dollar-delimited inline math: ``$body$``."""
82
+ return Concat(Raw("$"), body, Raw("$"))
83
+
84
+
78
85
  @Registry.add
79
86
  def DisplayMath(body: TeX | str) -> TeX:
80
87
  return Concat(
@@ -0,0 +1,16 @@
1
+ """Static analysis of a `TeX` node tree.
2
+
3
+ Walks the document AST and reports likely problems before the source is handed
4
+ to tectonic: references to undefined labels, labels defined more than once, and
5
+ `\\includegraphics` paths that do not exist on disk.
6
+
7
+ from pytex_analyze import analyze, Severity
8
+
9
+ for issue in analyze(node):
10
+ print(issue.severity, issue.message)
11
+ """
12
+
13
+ from .analyze import Issue, Severity, analyze
14
+ from .optimize import Optimize
15
+
16
+ __all__ = ["Issue", "Optimize", "Severity", "analyze"]
@@ -0,0 +1,106 @@
1
+ """Checks run over a `TeX` node tree to flag problems before compilation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import Counter
6
+ from dataclasses import dataclass
7
+ from enum import Enum
8
+ from typing import TYPE_CHECKING, cast
9
+
10
+ from pytex.model.control_sequence import ControlSequence, Parameter
11
+ from pytex.model.image import IncludeImage
12
+ from pytex.model.raw import Raw
13
+
14
+ if TYPE_CHECKING:
15
+ from collections.abc import Iterator
16
+
17
+ from pytex.interface.control_sequence import Parameters
18
+ from pytex.interface.tex import TeX
19
+
20
+ __all__ = ["Issue", "Severity", "analyze"]
21
+
22
+ # Control sequences that reference a label by name. Each takes a single
23
+ # (possibly comma-separated) argument of label names.
24
+ _REF_COMMANDS = frozenset(
25
+ {"ref", "pageref", "nameref", "autoref", "eqref", "vref", "cref", "Cref"}
26
+ )
27
+ _LABEL_COMMANDS = frozenset({"label"})
28
+
29
+
30
+ class Severity(Enum):
31
+ WARNING = "warning"
32
+ ERROR = "error"
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class Issue:
37
+ severity: Severity
38
+ message: str
39
+
40
+
41
+ def _walk(node: TeX) -> Iterator[TeX]:
42
+ yield node
43
+ for child in node.children or ():
44
+ yield from _walk(child)
45
+
46
+
47
+ def _first_required_text(cs: ControlSequence[Parameters]) -> str | None:
48
+ """Text of the first non-optional parameter of `cs`, if it is plain text."""
49
+ for param in cs.params or ():
50
+ if isinstance(param, Parameter) and not param.optional:
51
+ value = param.value
52
+ if isinstance(value, str):
53
+ return value
54
+ if isinstance(value, Raw):
55
+ return value.content
56
+ return None
57
+ return None
58
+
59
+
60
+ def analyze(node: TeX) -> list[Issue]:
61
+ """Return the problems found in the tree rooted at `node`.
62
+
63
+ Pure and side-effect free: only reads the tree (and, for images, checks
64
+ whether source files exist on disk).
65
+ """
66
+ label_counts: Counter[str] = Counter()
67
+ references: list[str] = []
68
+ issues: list[Issue] = []
69
+
70
+ for current in _walk(node):
71
+ if isinstance(current, ControlSequence):
72
+ cs = cast("ControlSequence[Parameters]", current)
73
+ if (
74
+ cs.name in _LABEL_COMMANDS
75
+ and (text := _first_required_text(cs)) is not None
76
+ ):
77
+ label_counts[text] += 1
78
+ elif (
79
+ cs.name in _REF_COMMANDS
80
+ and (text := _first_required_text(cs)) is not None
81
+ ):
82
+ references.extend(
83
+ name.strip() for name in text.split(",") if name.strip()
84
+ )
85
+ elif isinstance(current, IncludeImage) and not current.source_path.exists():
86
+ issues.append(
87
+ Issue(
88
+ Severity.ERROR,
89
+ f"image file not found: {current.source_path}",
90
+ )
91
+ )
92
+
93
+ for label, count in sorted(label_counts.items()):
94
+ if count > 1:
95
+ issues.append(
96
+ Issue(Severity.WARNING, f"label {label!r} defined {count} times")
97
+ )
98
+
99
+ defined = set(label_counts)
100
+ for name in sorted(set(references)):
101
+ if name not in defined:
102
+ issues.append(
103
+ Issue(Severity.WARNING, f"reference to undefined label {name!r}")
104
+ )
105
+
106
+ return issues