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