pytex-preprocessor 0.2.0__tar.gz → 0.4.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 (133) hide show
  1. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/PKG-INFO +116 -2
  2. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/README.md +115 -1
  3. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/pyproject.toml +5 -1
  4. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/__init__.py +11 -0
  5. pytex_preprocessor-0.4.0/src/pytex/model/comment.py +26 -0
  6. pytex_preprocessor-0.4.0/src/pytex/model/concat.py +55 -0
  7. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/model/control_sequence.py +5 -0
  8. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/model/document.py +5 -0
  9. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/model/math.py +7 -0
  10. pytex_preprocessor-0.4.0/src/pytex/template.py +90 -0
  11. pytex_preprocessor-0.4.0/src/pytex_analyze/__init__.py +16 -0
  12. pytex_preprocessor-0.4.0/src/pytex_analyze/analyze.py +106 -0
  13. pytex_preprocessor-0.4.0/src/pytex_analyze/optimize.py +181 -0
  14. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_builder/build.py +122 -2
  15. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_builder/console.py +3 -3
  16. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_builder/render.py +24 -16
  17. pytex_preprocessor-0.4.0/src/pytex_builder/tex2py.py +159 -0
  18. pytex_preprocessor-0.4.0/src/pytex_builder/tree.py +197 -0
  19. pytex_preprocessor-0.4.0/src/pytex_builder/variants.py +178 -0
  20. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/document.py +5 -2
  21. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_preprocessor.egg-info/PKG-INFO +116 -2
  22. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_preprocessor.egg-info/SOURCES.txt +8 -0
  23. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_preprocessor.egg-info/entry_points.txt +1 -0
  24. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_preprocessor.egg-info/top_level.txt +1 -0
  25. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_protocol/__init__.py +2 -1
  26. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_protocol/document.py +24 -6
  27. pytex_preprocessor-0.2.0/src/pytex/model/concat.py +0 -31
  28. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/setup.cfg +0 -0
  29. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/__init__.py +0 -0
  30. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/biblatex.py +0 -0
  31. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/builtin.py +0 -0
  32. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/captions.py +0 -0
  33. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/cleveref.py +0 -0
  34. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/colors.py +0 -0
  35. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/conditionals.py +0 -0
  36. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/counters.py +0 -0
  37. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/definitions.py +0 -0
  38. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/floats.py +0 -0
  39. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/font.py +0 -0
  40. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/fontawesome.py +0 -0
  41. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/fontspec.py +0 -0
  42. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/geometry.py +0 -0
  43. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/glossaries.py +0 -0
  44. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/graphics.py +0 -0
  45. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/hooks.py +0 -0
  46. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/hyperref.py +0 -0
  47. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/lengths.py +0 -0
  48. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/listings.py +0 -0
  49. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/mdframed.py +0 -0
  50. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/picture.py +0 -0
  51. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/setspace.py +0 -0
  52. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/tables.py +0 -0
  53. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/helpers/__init__.py +0 -0
  54. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/helpers/coerce.py +0 -0
  55. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/helpers/parenting.py +0 -0
  56. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/helpers/sanitize.py +0 -0
  57. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/helpers/with_package.py +0 -0
  58. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/interface/__init__.py +0 -0
  59. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/interface/control_sequence.py +0 -0
  60. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/interface/package.py +0 -0
  61. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/interface/tex.py +0 -0
  62. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/model/__init__.py +0 -0
  63. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/model/color.py +0 -0
  64. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/model/document_class.py +0 -0
  65. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/model/empty.py +0 -0
  66. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/model/environment.py +0 -0
  67. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/model/image.py +0 -0
  68. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/model/include.py +0 -0
  69. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/model/length.py +0 -0
  70. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/model/package.py +0 -0
  71. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/model/raw.py +0 -0
  72. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/packages.py +0 -0
  73. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/registry.py +0 -0
  74. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_builder/__init__.py +0 -0
  75. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_builder/tectonic.py +0 -0
  76. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/__init__.py +0 -0
  77. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Bold.ttf +0 -0
  78. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-BoldItalic.ttf +0 -0
  79. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Book.ttf +0 -0
  80. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-BookItalic.ttf +0 -0
  81. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Medium.ttf +0 -0
  82. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-MediumItalic.ttf +0 -0
  83. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Strong.ttf +0 -0
  84. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Thin.ttf +0 -0
  85. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-ThinItalic.ttf +0 -0
  86. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Black.ttf +0 -0
  87. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Bold.ttf +0 -0
  88. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-BoldItalic.ttf +0 -0
  89. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Italic.ttf +0 -0
  90. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Medium.ttf +0 -0
  91. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Regular.ttf +0 -0
  92. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/Times New Roman.ttf +0 -0
  93. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/logos/ASTA.svg +0 -0
  94. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/logos/DUMMY.png +0 -0
  95. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/logos/DUMMY_FOOT.png +0 -0
  96. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/logos/ECHO.svg +0 -0
  97. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/logos/HSRT.pdf +0 -0
  98. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/logos/INF.pdf +0 -0
  99. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/logos/STUPA.pdf +0 -0
  100. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/logos/Skyline.pdf +0 -0
  101. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/boxes.py +0 -0
  102. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/citations.py +0 -0
  103. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/cleveref_names.py +0 -0
  104. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/colors.py +0 -0
  105. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/fonts.py +0 -0
  106. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/glossary.py +0 -0
  107. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/hyperref_config.py +0 -0
  108. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/listings.py +0 -0
  109. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/logos.py +0 -0
  110. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/pagebreak.py +0 -0
  111. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/pagesetup.py +0 -0
  112. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/tex/pagesetup.tex +0 -0
  113. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/titlepage.py +0 -0
  114. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/variants.py +0 -0
  115. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/voting.py +0 -0
  116. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/watermark.py +0 -0
  117. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/wordcount.py +0 -0
  118. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_koma/__init__.py +0 -0
  119. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_koma/commands.py +0 -0
  120. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_koma/document.py +0 -0
  121. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_markdown/__init__.py +0 -0
  122. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_markdown/convert.py +0 -0
  123. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_markdown/escape.py +0 -0
  124. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_preprocessor.egg-info/dependency_links.txt +0 -0
  125. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_preprocessor.egg-info/requires.txt +0 -0
  126. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_protocol/convert.py +0 -0
  127. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_protocol/entries.py +0 -0
  128. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_protocol/frontmatter.py +0 -0
  129. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_protocol/header.py +0 -0
  130. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_protocol/shortcodes.py +0 -0
  131. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_protocol/signatures.py +0 -0
  132. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_tikz/__init__.py +0 -0
  133. {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.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.4.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
@@ -28,6 +28,20 @@ uses.
28
28
 
29
29
  ## Install
30
30
 
31
+ ### Prebuilt binary
32
+
33
+ Each release attaches standalone `pytex` binaries (Linux/macOS/Windows) — no
34
+ Python or `pip` needed. Download one from the
35
+ [Releases](https://github.com/frederikbeimgraben/PyTeX-Preprocessor/releases)
36
+ page, make it executable, and run it. The binary bundles its own interpreter
37
+ plus common data packages (numpy, pandas, openpyxl/calamine for spreadsheets,
38
+ Pillow, PyYAML), so documents can `import` those without installing anything;
39
+ see [`packaging/`](packaging/). It is built on Python 3.14, so documents may use
40
+ `tex(t"...")` even on machines without 3.14. (`--build` still needs `tectonic`,
41
+ which pytex downloads on first use.)
42
+
43
+ ### From PyPI
44
+
31
45
  To use the `pytex` command anywhere, install it as an isolated tool with
32
46
  [pipx](https://pipx.pypa.io/):
33
47
 
@@ -82,6 +96,22 @@ pytex example.tex.py --build # render + compile -> build/example.out.pdf
82
96
 
83
97
  Bare strings are coerced to text nodes and LaTeX-escaped.
84
98
 
99
+ ### Template strings (Python 3.14+)
100
+
101
+ On Python 3.14, `pytex.tex` accepts a [PEP 750](https://peps.python.org/pep-0750/)
102
+ template string and builds a `TeX` tree from it. Static parts are literal LaTeX;
103
+ interpolations are LaTeX-escaped when they are plain values and spliced as-is
104
+ when they are `TeX` nodes (nested template strings and lists are handled too):
105
+
106
+ ```py
107
+ from pytex import tex
108
+
109
+ name = "Q&A: 50%"
110
+ body = tex(t"{Bold('Heading')} — {name}") # node spliced; name -> "Q\&A: 50\%"
111
+ ```
112
+
113
+ `tex` is only exported on 3.14+; the rest of the library runs on 3.13.
114
+
85
115
  ## The `pytex` command
86
116
 
87
117
  The input file is dispatched by extension:
@@ -90,7 +120,7 @@ The input file is dispatched by extension:
90
120
  | --- | --- |
91
121
  | `.py` | imported as a module; its `__pytex__` node is rendered. Convention: name it `<doc>.tex.py`. |
92
122
  | `.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. |
123
+ | `.md` / `.markdown` | converted to nodes and wrapped in a document according to `--variant` (see below). Without `--variant` the style is auto-detected. |
94
124
 
95
125
  ### Inline replacements in `.tex`
96
126
 
@@ -111,6 +141,10 @@ Plain Python works too: $3^2 = \iffalse{pytex(3 ** 2)}\fi$.
111
141
  | `-b`, `--build` | off | compile the rendered `.tex` to PDF with tectonic |
112
142
  | `--build-dir DIR` | `build` | directory for artifacts and tectonic output |
113
143
  | `--no-shell-escape` | shell-escape on | disable shell-escape |
144
+ | `-t`, `--tree` | off | also print the input's `TeX`-node tree (`tree`-style) before rendering/building |
145
+ | `-f`, `--force` | off | skip the optimize + analysis pass and build even if problems are found |
146
+ | `--variant STYLE` | auto-detect | Markdown output style (`plain`, `report`, `protocol-asta`, `protocol-stupa`) |
147
+ | `--config JSON` | none | JSON object of document-class params, merged over the frontmatter |
114
148
 
115
149
  Shell-escape is on by default because inline images decode their base64
116
150
  payloads at compile time. The build runs tectonic, then `makeindex` (for
@@ -120,6 +154,42 @@ Output is minimal and color-tagged (`==>`, `note:`, `warning:`, `error:`),
120
154
  following tectonic's style; on failure it points at the likely cause and the
121
155
  log file. Set `NO_COLOR` to disable color.
122
156
 
157
+ ### Pre-flight optimize + analysis
158
+
159
+ Before rendering, the builder runs two render-equivalent passes over the node
160
+ tree. First `Optimize` tidies the tree (flatten nested `Concat`s, drop empty
161
+ nodes, turn whole-`Raw` LaTeX constructs into native nodes) without changing
162
+ the output (it also expands inline `pytex(...)` markers and turns `Raw`
163
+ comments and math — `\[...\]`, `\(...\)`, `$...$` — into native nodes). Then
164
+ `pytex_analyze` checks for problems that LaTeX would only surface later (or
165
+ silently):
166
+
167
+ - references (`\ref`, `\cref`, `\autoref`, ...) to a label that is never
168
+ defined,
169
+ - labels defined more than once,
170
+ - `\includegraphics` paths that do not exist on disk.
171
+
172
+ Missing-image issues are errors and abort the build; the rest are warnings.
173
+ Pass `-f`/`--force` to skip both passes and build regardless.
174
+
175
+ ### Inspecting the node tree
176
+
177
+ `--tree` prints the parsed `TeX`-node tree (then renders/builds as usual),
178
+ useful for debugging how an input maps to nodes. Nodes that require a package
179
+ are tagged with it (`[+package]`):
180
+
181
+ ```
182
+ $ pytex example.tex.py --tree
183
+ Document (article)
184
+ ├── ControlSequence \title
185
+ │ └── Parameter { }
186
+ │ └── Raw "PyTeX Example"
187
+ └── Concat
188
+ ├── ControlSequence \maketitle
189
+ ├── ControlSequence \cref [+cleveref]
190
+ └── ...
191
+ ```
192
+
123
193
  ## Packages
124
194
 
125
195
  `pytex` is the core; the rest are optional and build on it.
@@ -130,6 +200,7 @@ log file. Set `NO_COLOR` to disable color.
130
200
  | `pytex_koma` | KOMA-Script classes and commands (`Addchap`, `Minisec`, `KOMAoptions`, ...). |
131
201
  | `pytex_tikz` | TikZ pictures and primitives (`TikzPicture`, `Draw`, `Node`, `Circle`, ...). |
132
202
  | `pytex_markdown` | Markdown -> native `TeX` conversion (see below). |
203
+ | `pytex_analyze` | static checks over the node tree (dangling refs, duplicate labels, missing images), plus `Optimize` to simplify a tree render-equivalently. |
133
204
  | `pytex_hsrtreport` | HSRT report document class, colored callout boxes, title pages, glossary/citation helpers. |
134
205
  | `pytex_protocol` | STUPA/AStA meeting minutes from Markdown, built on `pytex_hsrtreport`. |
135
206
 
@@ -157,6 +228,49 @@ GitHub-style callouts become HSRT colored boxes (so the module depends on
157
228
  Both factories are registered, so they work in `\iffalse{pytex(...)}\fi`
158
229
  replacements in `.tex` sources too.
159
230
 
231
+ ### Output variants
232
+
233
+ When the `pytex` command renders a `.md` file it wraps the converted nodes in a
234
+ document chosen by `--variant`:
235
+
236
+ | Variant | Document |
237
+ | --- | --- |
238
+ | `plain` | a bare `Document` (default class `article`); `#` -> `\section`. |
239
+ | `report` | an HSRT report with title page and table of contents; `#` -> `\chapter`. |
240
+ | `protocol-asta` | an AStA meeting protocol (HSRT report, AStA logos). |
241
+ | `protocol-stupa` | a StuPa meeting protocol (HSRT report, StuPa logos). |
242
+
243
+ Without `--variant`, protocol frontmatter (`gremium:` or `typ: protokoll`) picks
244
+ a protocol style and everything else falls back to `plain`.
245
+
246
+ Document-class parameters come from the YAML frontmatter and from `--config`
247
+ (a JSON object that overrides the frontmatter), e.g.:
248
+
249
+ ```sh
250
+ pytex notes.md --variant plain --config '{"documentclass": "scrartcl", "classoptions": ["11pt", "twocolumn"]}'
251
+ ```
252
+
253
+ `classoptions` accepts a list (`"twocolumn"`, `"DIV=12"`) or a `{key: value}`
254
+ object. For styles with a title page (`report`), the title is taken from
255
+ `title:`/`--config` if given, otherwise from the first `#` heading (which is then
256
+ not also rendered as a chapter).
257
+
258
+ ## Converting LaTeX to PyTeX
259
+
260
+ `pytex-tex2py` turns an existing `.tex` file into an equivalent `.tex.py`
261
+ source. It reads the file, runs `Optimize` over it (expanding inline
262
+ `pytex(...)` markers and recognising comments and math), and serialises the
263
+ result to Python that rebuilds the same tree:
264
+
265
+ ```sh
266
+ pytex-tex2py paper.tex # -> paper.tex.py
267
+ pytex-tex2py paper.tex -o out.py
268
+ ```
269
+
270
+ Rendering the generated `.tex.py` reproduces the original output byte-for-byte;
271
+ nodes the serialiser does not special-case fall back to a literal `Raw`, so the
272
+ conversion always round-trips.
273
+
160
274
  ## Examples
161
275
 
162
276
  See `examples/` for one minimal input per kind (`.tex.py`, `.py.tex`, `.md`,
@@ -14,6 +14,20 @@ uses.
14
14
 
15
15
  ## Install
16
16
 
17
+ ### Prebuilt binary
18
+
19
+ Each release attaches standalone `pytex` binaries (Linux/macOS/Windows) — no
20
+ Python or `pip` needed. Download one from the
21
+ [Releases](https://github.com/frederikbeimgraben/PyTeX-Preprocessor/releases)
22
+ page, make it executable, and run it. The binary bundles its own interpreter
23
+ plus common data packages (numpy, pandas, openpyxl/calamine for spreadsheets,
24
+ Pillow, PyYAML), so documents can `import` those without installing anything;
25
+ see [`packaging/`](packaging/). It is built on Python 3.14, so documents may use
26
+ `tex(t"...")` even on machines without 3.14. (`--build` still needs `tectonic`,
27
+ which pytex downloads on first use.)
28
+
29
+ ### From PyPI
30
+
17
31
  To use the `pytex` command anywhere, install it as an isolated tool with
18
32
  [pipx](https://pipx.pypa.io/):
19
33
 
@@ -68,6 +82,22 @@ pytex example.tex.py --build # render + compile -> build/example.out.pdf
68
82
 
69
83
  Bare strings are coerced to text nodes and LaTeX-escaped.
70
84
 
85
+ ### Template strings (Python 3.14+)
86
+
87
+ On Python 3.14, `pytex.tex` accepts a [PEP 750](https://peps.python.org/pep-0750/)
88
+ template string and builds a `TeX` tree from it. Static parts are literal LaTeX;
89
+ interpolations are LaTeX-escaped when they are plain values and spliced as-is
90
+ when they are `TeX` nodes (nested template strings and lists are handled too):
91
+
92
+ ```py
93
+ from pytex import tex
94
+
95
+ name = "Q&A: 50%"
96
+ body = tex(t"{Bold('Heading')} — {name}") # node spliced; name -> "Q\&A: 50\%"
97
+ ```
98
+
99
+ `tex` is only exported on 3.14+; the rest of the library runs on 3.13.
100
+
71
101
  ## The `pytex` command
72
102
 
73
103
  The input file is dispatched by extension:
@@ -76,7 +106,7 @@ The input file is dispatched by extension:
76
106
  | --- | --- |
77
107
  | `.py` | imported as a module; its `__pytex__` node is rendered. Convention: name it `<doc>.tex.py`. |
78
108
  | `.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. |
109
+ | `.md` / `.markdown` | converted to nodes and wrapped in a document according to `--variant` (see below). Without `--variant` the style is auto-detected. |
80
110
 
81
111
  ### Inline replacements in `.tex`
82
112
 
@@ -97,6 +127,10 @@ Plain Python works too: $3^2 = \iffalse{pytex(3 ** 2)}\fi$.
97
127
  | `-b`, `--build` | off | compile the rendered `.tex` to PDF with tectonic |
98
128
  | `--build-dir DIR` | `build` | directory for artifacts and tectonic output |
99
129
  | `--no-shell-escape` | shell-escape on | disable shell-escape |
130
+ | `-t`, `--tree` | off | also print the input's `TeX`-node tree (`tree`-style) before rendering/building |
131
+ | `-f`, `--force` | off | skip the optimize + analysis pass and build even if problems are found |
132
+ | `--variant STYLE` | auto-detect | Markdown output style (`plain`, `report`, `protocol-asta`, `protocol-stupa`) |
133
+ | `--config JSON` | none | JSON object of document-class params, merged over the frontmatter |
100
134
 
101
135
  Shell-escape is on by default because inline images decode their base64
102
136
  payloads at compile time. The build runs tectonic, then `makeindex` (for
@@ -106,6 +140,42 @@ Output is minimal and color-tagged (`==>`, `note:`, `warning:`, `error:`),
106
140
  following tectonic's style; on failure it points at the likely cause and the
107
141
  log file. Set `NO_COLOR` to disable color.
108
142
 
143
+ ### Pre-flight optimize + analysis
144
+
145
+ Before rendering, the builder runs two render-equivalent passes over the node
146
+ tree. First `Optimize` tidies the tree (flatten nested `Concat`s, drop empty
147
+ nodes, turn whole-`Raw` LaTeX constructs into native nodes) without changing
148
+ the output (it also expands inline `pytex(...)` markers and turns `Raw`
149
+ comments and math — `\[...\]`, `\(...\)`, `$...$` — into native nodes). Then
150
+ `pytex_analyze` checks for problems that LaTeX would only surface later (or
151
+ silently):
152
+
153
+ - references (`\ref`, `\cref`, `\autoref`, ...) to a label that is never
154
+ defined,
155
+ - labels defined more than once,
156
+ - `\includegraphics` paths that do not exist on disk.
157
+
158
+ Missing-image issues are errors and abort the build; the rest are warnings.
159
+ Pass `-f`/`--force` to skip both passes and build regardless.
160
+
161
+ ### Inspecting the node tree
162
+
163
+ `--tree` prints the parsed `TeX`-node tree (then renders/builds as usual),
164
+ useful for debugging how an input maps to nodes. Nodes that require a package
165
+ are tagged with it (`[+package]`):
166
+
167
+ ```
168
+ $ pytex example.tex.py --tree
169
+ Document (article)
170
+ ├── ControlSequence \title
171
+ │ └── Parameter { }
172
+ │ └── Raw "PyTeX Example"
173
+ └── Concat
174
+ ├── ControlSequence \maketitle
175
+ ├── ControlSequence \cref [+cleveref]
176
+ └── ...
177
+ ```
178
+
109
179
  ## Packages
110
180
 
111
181
  `pytex` is the core; the rest are optional and build on it.
@@ -116,6 +186,7 @@ log file. Set `NO_COLOR` to disable color.
116
186
  | `pytex_koma` | KOMA-Script classes and commands (`Addchap`, `Minisec`, `KOMAoptions`, ...). |
117
187
  | `pytex_tikz` | TikZ pictures and primitives (`TikzPicture`, `Draw`, `Node`, `Circle`, ...). |
118
188
  | `pytex_markdown` | Markdown -> native `TeX` conversion (see below). |
189
+ | `pytex_analyze` | static checks over the node tree (dangling refs, duplicate labels, missing images), plus `Optimize` to simplify a tree render-equivalently. |
119
190
  | `pytex_hsrtreport` | HSRT report document class, colored callout boxes, title pages, glossary/citation helpers. |
120
191
  | `pytex_protocol` | STUPA/AStA meeting minutes from Markdown, built on `pytex_hsrtreport`. |
121
192
 
@@ -143,6 +214,49 @@ GitHub-style callouts become HSRT colored boxes (so the module depends on
143
214
  Both factories are registered, so they work in `\iffalse{pytex(...)}\fi`
144
215
  replacements in `.tex` sources too.
145
216
 
217
+ ### Output variants
218
+
219
+ When the `pytex` command renders a `.md` file it wraps the converted nodes in a
220
+ document chosen by `--variant`:
221
+
222
+ | Variant | Document |
223
+ | --- | --- |
224
+ | `plain` | a bare `Document` (default class `article`); `#` -> `\section`. |
225
+ | `report` | an HSRT report with title page and table of contents; `#` -> `\chapter`. |
226
+ | `protocol-asta` | an AStA meeting protocol (HSRT report, AStA logos). |
227
+ | `protocol-stupa` | a StuPa meeting protocol (HSRT report, StuPa logos). |
228
+
229
+ Without `--variant`, protocol frontmatter (`gremium:` or `typ: protokoll`) picks
230
+ a protocol style and everything else falls back to `plain`.
231
+
232
+ Document-class parameters come from the YAML frontmatter and from `--config`
233
+ (a JSON object that overrides the frontmatter), e.g.:
234
+
235
+ ```sh
236
+ pytex notes.md --variant plain --config '{"documentclass": "scrartcl", "classoptions": ["11pt", "twocolumn"]}'
237
+ ```
238
+
239
+ `classoptions` accepts a list (`"twocolumn"`, `"DIV=12"`) or a `{key: value}`
240
+ object. For styles with a title page (`report`), the title is taken from
241
+ `title:`/`--config` if given, otherwise from the first `#` heading (which is then
242
+ not also rendered as a chapter).
243
+
244
+ ## Converting LaTeX to PyTeX
245
+
246
+ `pytex-tex2py` turns an existing `.tex` file into an equivalent `.tex.py`
247
+ source. It reads the file, runs `Optimize` over it (expanding inline
248
+ `pytex(...)` markers and recognising comments and math), and serialises the
249
+ result to Python that rebuilds the same tree:
250
+
251
+ ```sh
252
+ pytex-tex2py paper.tex # -> paper.tex.py
253
+ pytex-tex2py paper.tex -o out.py
254
+ ```
255
+
256
+ Rendering the generated `.tex.py` reproduces the original output byte-for-byte;
257
+ nodes the serialiser does not special-case fall back to a literal `Raw`, so the
258
+ conversion always round-trips.
259
+
146
260
  ## Examples
147
261
 
148
262
  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.4.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"]
@@ -36,6 +37,9 @@ addopts = "--import-mode=importlib"
36
37
  line-length = 88
37
38
  target-version = "py313"
38
39
  src = ["src", "tests"]
40
+ # Uses Python 3.14 t-string syntax; the py313 target cannot parse it. Excluded
41
+ # from lint/format (it is exercised by pytest on 3.14).
42
+ extend-exclude = ["tests/pytex/test_template.py"]
39
43
 
40
44
  [tool.ruff.lint]
41
45
  select = [
@@ -1,3 +1,5 @@
1
+ import sys
2
+
1
3
  from . import packages
2
4
  from .commands import (
3
5
  biblatex,
@@ -27,6 +29,7 @@ from .commands import (
27
29
  from .helpers import coerce, sanitize, with_package
28
30
  from .model import (
29
31
  color,
32
+ comment,
30
33
  concat,
31
34
  control_sequence,
32
35
  document,
@@ -51,6 +54,7 @@ __all__ = [
51
54
  "coerce",
52
55
  "color",
53
56
  "colors",
57
+ "comment",
54
58
  "concat",
55
59
  "conditionals",
56
60
  "control_sequence",
@@ -85,3 +89,10 @@ __all__ = [
85
89
  "tables",
86
90
  "with_package",
87
91
  ]
92
+
93
+ # `tex(t"...")` needs PEP 750 template strings (Python 3.14+). Exposed only
94
+ # there; the rest of the library stays importable on 3.13.
95
+ if sys.version_info >= (3, 14):
96
+ from .template import tex as tex # pyright: ignore[reportUnreachable]
97
+
98
+ __all__.append("tex")
@@ -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,90 @@
1
+ r"""`tex(t"...")` — build a `TeX` tree from a template string (PEP 750).
2
+
3
+ Requires Python 3.14 (the ``t"..."`` syntax and :mod:`string.templatelib`).
4
+ This module is import-safe on older versions — it contains no t-string literals
5
+ and guards the runtime import — but :func:`tex` can only be called with a real
6
+ ``Template``, which cannot exist before 3.14. ``pytex`` only re-exports it on
7
+ 3.14+.
8
+
9
+ The rendering model mirrors the escape boundary a LaTeX document needs:
10
+
11
+ * static template parts are literal LaTeX (author-written, trusted);
12
+ * interpolations are escaped when they are plain values, spliced as-is when
13
+ they are `TeX` nodes, and recursed when they are nested template strings or
14
+ iterables of the above.
15
+
16
+ name = "Q&A: 50%"
17
+ tex(t"{Bold('Heading')} - {name}") # node spliced; name -> "Q\&A: 50\%"
18
+ """
19
+
20
+ # `string.templatelib` has no type stub before 3.14, so a type-checker running
21
+ # on an older Python cannot type the `Template` import; silence that noise for
22
+ # this module only (its logic is exercised on 3.14 in CI / the docker test).
23
+ # pyright: reportMissingImports=false, reportUnknownVariableType=false
24
+ from __future__ import annotations
25
+
26
+ from typing import TYPE_CHECKING, cast
27
+
28
+ from .helpers.sanitize import escape_latex
29
+ from .interface.tex import TeX
30
+ from .model.concat import Concat
31
+ from .model.empty import Empty
32
+ from .model.raw import Raw
33
+
34
+ if TYPE_CHECKING:
35
+ from collections.abc import Iterator
36
+ from typing import Protocol
37
+
38
+ class _Interpolation(Protocol):
39
+ @property
40
+ def value(self) -> object: ...
41
+ @property
42
+ def conversion(self) -> str | None: ...
43
+ @property
44
+ def format_spec(self) -> str: ...
45
+
46
+ class _Template(Protocol):
47
+ def __iter__(self) -> Iterator[str | _Interpolation]: ...
48
+
49
+
50
+ # Real `Template` class at runtime (3.14+) for the nested-template check; an
51
+ # empty tuple on older versions makes the isinstance test always False.
52
+ try:
53
+ from string.templatelib import Template
54
+
55
+ _template_classes = (Template,)
56
+ except ImportError: # Python < 3.14
57
+ _template_classes = ()
58
+
59
+ _TEMPLATE_TYPES: tuple[type, ...] = _template_classes
60
+
61
+ __all__ = ["tex"]
62
+
63
+ _CONVERSIONS = {"r": repr, "s": str, "a": ascii}
64
+
65
+
66
+ def tex(template: _Template) -> TeX:
67
+ """Render a t-string into a `TeX` tree (see the module docstring)."""
68
+ parts: list[TeX] = []
69
+ for item in template:
70
+ if isinstance(item, str):
71
+ parts.append(Raw(item)) # literal LaTeX
72
+ else:
73
+ parts.append(_coerce(item.value, item.conversion, item.format_spec))
74
+ return Concat(*parts)
75
+
76
+
77
+ def _coerce(value: object, conversion: str | None = None, spec: object = "") -> TeX:
78
+ if value is None:
79
+ return Empty
80
+ if isinstance(value, TeX):
81
+ return value
82
+ if isinstance(value, _TEMPLATE_TYPES):
83
+ return tex(cast("_Template", value))
84
+ if isinstance(value, (list, tuple)):
85
+ items = cast("tuple[object, ...] | list[object]", value)
86
+ return Concat(*(_coerce(item) for item in items))
87
+ if conversion in _CONVERSIONS:
88
+ value = _CONVERSIONS[conversion](value)
89
+ text = format(value, spec if isinstance(spec, str) else "")
90
+ return Raw(escape_latex(text))
@@ -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"]