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.
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/PKG-INFO +116 -2
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/README.md +115 -1
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/pyproject.toml +5 -1
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/__init__.py +11 -0
- pytex_preprocessor-0.4.0/src/pytex/model/comment.py +26 -0
- pytex_preprocessor-0.4.0/src/pytex/model/concat.py +55 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/model/control_sequence.py +5 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/model/document.py +5 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/model/math.py +7 -0
- pytex_preprocessor-0.4.0/src/pytex/template.py +90 -0
- pytex_preprocessor-0.4.0/src/pytex_analyze/__init__.py +16 -0
- pytex_preprocessor-0.4.0/src/pytex_analyze/analyze.py +106 -0
- pytex_preprocessor-0.4.0/src/pytex_analyze/optimize.py +181 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_builder/build.py +122 -2
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_builder/console.py +3 -3
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_builder/render.py +24 -16
- pytex_preprocessor-0.4.0/src/pytex_builder/tex2py.py +159 -0
- pytex_preprocessor-0.4.0/src/pytex_builder/tree.py +197 -0
- pytex_preprocessor-0.4.0/src/pytex_builder/variants.py +178 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/document.py +5 -2
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_preprocessor.egg-info/PKG-INFO +116 -2
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_preprocessor.egg-info/SOURCES.txt +8 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_preprocessor.egg-info/entry_points.txt +1 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_preprocessor.egg-info/top_level.txt +1 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_protocol/__init__.py +2 -1
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_protocol/document.py +24 -6
- pytex_preprocessor-0.2.0/src/pytex/model/concat.py +0 -31
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/setup.cfg +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/__init__.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/biblatex.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/builtin.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/captions.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/cleveref.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/colors.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/conditionals.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/counters.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/definitions.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/floats.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/font.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/fontawesome.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/fontspec.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/geometry.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/glossaries.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/graphics.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/hooks.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/hyperref.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/lengths.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/listings.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/mdframed.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/picture.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/setspace.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/commands/tables.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/helpers/__init__.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/helpers/coerce.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/helpers/parenting.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/helpers/sanitize.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/helpers/with_package.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/interface/__init__.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/interface/control_sequence.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/interface/package.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/interface/tex.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/model/__init__.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/model/color.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/model/document_class.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/model/empty.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/model/environment.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/model/image.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/model/include.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/model/length.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/model/package.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/model/raw.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/packages.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex/registry.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_builder/__init__.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_builder/tectonic.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/__init__.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Bold.ttf +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-BoldItalic.ttf +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Book.ttf +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-BookItalic.ttf +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Medium.ttf +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-MediumItalic.ttf +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Strong.ttf +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Thin.ttf +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-ThinItalic.ttf +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Black.ttf +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Bold.ttf +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-BoldItalic.ttf +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Italic.ttf +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Medium.ttf +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Regular.ttf +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/fonts/Times New Roman.ttf +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/logos/ASTA.svg +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/logos/DUMMY.png +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/logos/DUMMY_FOOT.png +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/logos/ECHO.svg +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/logos/HSRT.pdf +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/logos/INF.pdf +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/logos/STUPA.pdf +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/assets/logos/Skyline.pdf +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/boxes.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/citations.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/cleveref_names.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/colors.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/fonts.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/glossary.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/hyperref_config.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/listings.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/logos.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/pagebreak.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/pagesetup.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/tex/pagesetup.tex +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/titlepage.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/variants.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/voting.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/watermark.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_hsrtreport/wordcount.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_koma/__init__.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_koma/commands.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_koma/document.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_markdown/__init__.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_markdown/convert.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_markdown/escape.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_preprocessor.egg-info/dependency_links.txt +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_preprocessor.egg-info/requires.txt +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_protocol/convert.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_protocol/entries.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_protocol/frontmatter.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_protocol/header.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_protocol/shortcodes.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_protocol/signatures.py +0 -0
- {pytex_preprocessor-0.2.0 → pytex_preprocessor-0.4.0}/src/pytex_tikz/__init__.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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.
|
|
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"]
|