texmodel 0.2.0__py3-none-any.whl
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.
- texmodel/__init__.py +6 -0
- texmodel/_internal/__init__.py +1 -0
- texmodel/_internal/render.py +323 -0
- texmodel/_internal/scan.py +23 -0
- texmodel/body.py +364 -0
- texmodel/bundle.py +69 -0
- texmodel/cli.py +211 -0
- texmodel/components.py +757 -0
- texmodel/config.py +32 -0
- texmodel/drift.py +55 -0
- texmodel/errors.py +38 -0
- texmodel/package.py +482 -0
- texmodel/plan.py +92 -0
- texmodel/pointer.py +70 -0
- texmodel/preamble.py +110 -0
- texmodel/py.typed +0 -0
- texmodel/spec.py +235 -0
- texmodel/std/__init__.py +37 -0
- texmodel/std/adjustbox.py +19 -0
- texmodel/std/anyfontsize.py +7 -0
- texmodel/std/calc.py +7 -0
- texmodel/std/enumitem.py +18 -0
- texmodel/std/eso_pic.py +49 -0
- texmodel/std/etoolbox.py +127 -0
- texmodel/std/fontspec.py +36 -0
- texmodel/std/geometry.py +17 -0
- texmodel/std/graphicx.py +26 -0
- texmodel/std/hyphenat.py +7 -0
- texmodel/std/latex.py +516 -0
- texmodel/std/microtype.py +27 -0
- texmodel/std/pdfrender.py +17 -0
- texmodel/std/tcolorbox.py +43 -0
- texmodel/std/tikz.py +36 -0
- texmodel/std/xcolor.py +47 -0
- texmodel/testing.py +297 -0
- texmodel/types.py +264 -0
- texmodel-0.2.0.dist-info/METADATA +525 -0
- texmodel-0.2.0.dist-info/RECORD +61 -0
- texmodel-0.2.0.dist-info/WHEEL +4 -0
- texmodel-0.2.0.dist-info/entry_points.txt +9 -0
- texmodel-0.2.0.dist-info/licenses/LICENSE +21 -0
- texmodel_build/__init__.py +10 -0
- texmodel_build/_internal/__init__.py +1 -0
- texmodel_build/_internal/converge.py +19 -0
- texmodel_build/_internal/env.py +42 -0
- texmodel_build/_internal/logparse.py +170 -0
- texmodel_build/_internal/proofpdf.py +52 -0
- texmodel_build/_internal/spawn.py +36 -0
- texmodel_build/_internal/styscan.py +37 -0
- texmodel_build/cli.py +349 -0
- texmodel_build/compiler.py +341 -0
- texmodel_build/config.py +58 -0
- texmodel_build/diagnostics.py +225 -0
- texmodel_build/engines.py +35 -0
- texmodel_build/errors.py +17 -0
- texmodel_build/py.typed +0 -0
- texmodel_build/result.py +65 -0
- texmodel_build/roots.py +41 -0
- texmodel_build/targets.py +43 -0
- texmodel_build/testkit.py +59 -0
- texmodel_build/verify.py +158 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: texmodel
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Typed document-family models rendered to TeX, with an optional latexmk driver and verifier plugins.
|
|
5
|
+
Author-Email: Ilja Orlovs <ilja@release.art>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
10
|
+
Classifier: Typing :: Typed
|
|
11
|
+
Project-URL: Homepage, https://github.com/release-art/texmodel
|
|
12
|
+
Project-URL: Repository, https://github.com/release-art/texmodel
|
|
13
|
+
Requires-Python: <3.16,>=3.15
|
|
14
|
+
Requires-Dist: typer>=0.27.3
|
|
15
|
+
Requires-Dist: pydantic>=2.14.1
|
|
16
|
+
Provides-Extra: proof-pdf
|
|
17
|
+
Requires-Dist: proof-pdf>=0.3.3; extra == "proof-pdf"
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# texmodel
|
|
21
|
+
|
|
22
|
+
[](https://github.com/release-art/texmodel/actions/workflows/ci.yml)
|
|
23
|
+
[](LICENSE)
|
|
24
|
+
|
|
25
|
+
Declare a LaTeX document family once in Python types; get plain, readable, git-trackable `.sty` source out of it. The output
|
|
26
|
+
is ordinary TeX: commit it, diff it, compile it with whatever LaTeX setup you already have.
|
|
27
|
+
|
|
28
|
+
> **Status: 0.1.0, alpha.** The public API may change before 1.0. Release migrations, `texmodel-print` and the trust
|
|
29
|
+
> and sandbox layers are planned, not built (see [the plan](docs/texmodel-plan.md)).
|
|
30
|
+
|
|
31
|
+
```zsh
|
|
32
|
+
pip install texmodel # the library and the texmodel-build command
|
|
33
|
+
pip install 'texmodel[proof-pdf]' # plus the proof-pdf verifier
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
One distribution, two packages:
|
|
37
|
+
|
|
38
|
+
| Package | What it is |
|
|
39
|
+
|---|---|
|
|
40
|
+
| **`texmodel`** (`src/`) | The library: declare components, render deterministic TeX, check that committed output is current. No TeX needed, no processes spawned. |
|
|
41
|
+
| **`texmodel_build`** (command `texmodel-build`) | Optional: a thin `latexmk` driver with typed log diagnostics, and verifier plugins for built PDFs. Use it if you want compile results as data; your own `latexmk` or image works just as well. |
|
|
42
|
+
|
|
43
|
+
## texmodel
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
# acme_cards/__init__.py
|
|
47
|
+
from texmodel.body import TemplateRef
|
|
48
|
+
from texmodel.components import Command, Environment, Value
|
|
49
|
+
from texmodel.package import Package
|
|
50
|
+
from texmodel.types import CMYK, Colour, Length, Text
|
|
51
|
+
|
|
52
|
+
pkg = Package("acme-cards", prefix="ac", version="1.4.0", date="2026/10/21")
|
|
53
|
+
|
|
54
|
+
@pkg.value
|
|
55
|
+
class FactGap(Value[Length]):
|
|
56
|
+
"""Space under each fact."""
|
|
57
|
+
default = Length("2mm")
|
|
58
|
+
|
|
59
|
+
@pkg.value
|
|
60
|
+
class Ink(Value[Colour]):
|
|
61
|
+
"""The text colour."""
|
|
62
|
+
default = CMYK(0, 0, 0, 0.95)
|
|
63
|
+
|
|
64
|
+
@pkg.value
|
|
65
|
+
class Compact(Value[bool]):
|
|
66
|
+
"""Tighten the layout."""
|
|
67
|
+
default = False
|
|
68
|
+
|
|
69
|
+
@pkg.command
|
|
70
|
+
class Fact(Command):
|
|
71
|
+
"""One short sentence under the title."""
|
|
72
|
+
text: Text
|
|
73
|
+
body = TemplateRef("fact.tex") # explicit: a file next to this module; nothing is found by name
|
|
74
|
+
|
|
75
|
+
@pkg.command
|
|
76
|
+
class Slot(Command):
|
|
77
|
+
"""A labelled paragraph; the label prints as its heading."""
|
|
78
|
+
text: Text
|
|
79
|
+
label: Text | None = None # optional: keyword-only, TeX's one optional argument
|
|
80
|
+
|
|
81
|
+
@pkg.environment
|
|
82
|
+
class CardBack(Environment):
|
|
83
|
+
"""The back face of a card."""
|
|
84
|
+
children = [Name, Fact.times(2, 3), Slot.times(3)] # in order: Name once, Fact 2 to 3, Slot 0 to 3; also X.times.any(), X.times.at_least_one()
|
|
85
|
+
begin = r"\begin{minipage}{\linewidth}"
|
|
86
|
+
end = r"\end{minipage}"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
A body is TeX you point at: a `TemplateRef("fact.tex")` (relative to the defining module), a string, or a `body`
|
|
90
|
+
method returning either (dedented, so a raw triple-quoted string reads as it renders). `#text` is the argument named `text`:
|
|
91
|
+
|
|
92
|
+
```latex
|
|
93
|
+
\par\noindent{\color{ac-ink}#text}\par\vspace{\acFactGap}
|
|
94
|
+
\ifacCompact\vspace{-\acFactGap}\fi
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
A `Value[T]` needs no body: its type decides the declaration (`Length` -> `\newlength` + `\setlength`, `Colour` ->
|
|
98
|
+
`\definecolor` named `ac-ink`, `Bool` -> `\newif`), and its `default` is an instance of `T`. Packages a value needs
|
|
99
|
+
(`xcolor` for colours) are `\RequirePackage`d once, sorted. A body that names an undeclared `\acThing` is refused when
|
|
100
|
+
`render()` / `texmodel check` runs (not at registration, since a body may name a component declared after it).
|
|
101
|
+
|
|
102
|
+
A body may also be a `TexRaw` or a sequence or generator of pieces (strings, `TemplateRef`s, `TexRaw`s), each with its own
|
|
103
|
+
`Whitespace` policy: `TRIM` writes the `%` at line ends for you, `KEEP` (the default, set per package with
|
|
104
|
+
`Package(whitespace=)`) writes lines as given.
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from texmodel.body import TexRaw, Whitespace
|
|
108
|
+
|
|
109
|
+
def body(self):
|
|
110
|
+
yield TexRaw(r"""
|
|
111
|
+
\par\noindent
|
|
112
|
+
{\color{ac-ink}#text}
|
|
113
|
+
""", whitespace=Whitespace.TRIM) # \par\noindent% {\color{ac-ink}#1}%
|
|
114
|
+
yield r"\par"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
A body can call another component by Python binding. The call is checked against the target's arguments, written with the
|
|
118
|
+
target's prefix, and a target in another package makes that package a `\RequirePackage` of the generated `.sty`:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from texmodel.body import Hole
|
|
122
|
+
|
|
123
|
+
class Entry(Command):
|
|
124
|
+
"""A fact with a rule above it."""
|
|
125
|
+
text: Text
|
|
126
|
+
|
|
127
|
+
def body(self):
|
|
128
|
+
yield r"\par\hrule"
|
|
129
|
+
yield Fact.call(Hole("text")) # \bkFact{#1}; Fact belongs to base-kit, so \RequirePackage{base-kit}
|
|
130
|
+
|
|
131
|
+
class Section(Environment):
|
|
132
|
+
"""A titled panel."""
|
|
133
|
+
begin = Panel.open(title=Hole("title")) # \begin{bkPanel}{#1}
|
|
134
|
+
end = Panel.close() # \end{bkPanel}
|
|
135
|
+
title: Text
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`Fact.call(...)` takes the required arguments positionally and the optional one by keyword (`Slot.call(Hole("t"), label="x")`);
|
|
139
|
+
an environment's arguments are all keywords. A target must be registered before the body that calls it.
|
|
140
|
+
|
|
141
|
+
An environment has `begin` and `end` in the same way. A document is plain
|
|
142
|
+
Python; `Text` fields are escaped, a `Raw` field passes TeX through, and everything wrong is reported at once:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
tex = pkg.document(
|
|
146
|
+
CardBack(Name("Rex"), Fact("Friendly & loyal."), Fact("50% of $5")),
|
|
147
|
+
settings=[FactGap(Length("3mm"))], # values for the whole document
|
|
148
|
+
options=["a4paper"],
|
|
149
|
+
)
|
|
150
|
+
CardBack(Fact("x")).validate() # [Violation(TM-C002 ...), Violation(TM-C001 path='CardBack/Fact[0]' ...)]
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```toml
|
|
154
|
+
# pyproject.toml
|
|
155
|
+
[tool.texmodel]
|
|
156
|
+
package = "acme_cards:pkg"
|
|
157
|
+
output = "generated"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
A pointer (`package =`, and the argument of `texmodel render`) is `dotted.module:object`, `dotted.module:namespace.object`
|
|
161
|
+
(attributes are followed) or `dotted.module:getter()` (called with no arguments; its result is used):
|
|
162
|
+
|
|
163
|
+
```toml
|
|
164
|
+
package = "acme_cards.decks:Decks.birds" # or "acme_cards.decks:make_package()"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```zsh
|
|
168
|
+
texmodel build # writes generated/acme-cards.sty
|
|
169
|
+
texmodel render docs:card # writes card.tex from `card = pkg.document(...)` (or a function returning one); -o to choose
|
|
170
|
+
texmodel check # exit 1 if a committed generated file is stale, missing or orphaned; a stale line names its component
|
|
171
|
+
# (`changed generated/acme-cards.sty Fact: ...`)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```latex
|
|
175
|
+
% generated/acme-cards.sty (excerpt)
|
|
176
|
+
\RequirePackage{xcolor}
|
|
177
|
+
|
|
178
|
+
% Space under each fact.
|
|
179
|
+
% from acme_cards.FactGap (value: Length)
|
|
180
|
+
\newlength{\acFactGap}
|
|
181
|
+
\setlength{\acFactGap}{2mm}
|
|
182
|
+
|
|
183
|
+
% The text colour.
|
|
184
|
+
% from acme_cards.Ink (value: Colour)
|
|
185
|
+
\definecolor{ac-ink}{cmyk}{0,0,0,0.95}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
- **Deterministic**: no timestamps or hashes; the header records the generator version (`generated with texmodel X.Y.Z`),
|
|
189
|
+
so `check` can say "written by 0.1.0, current is 0.2.0".
|
|
190
|
+
- **Checked early**: a bad declaration is refused when the class is defined, with a stable code (`TM-M0xx`) and the class
|
|
191
|
+
named: more than one optional argument, a name that is not a TeX control word, a missing or ambiguous body, a `#name` that
|
|
192
|
+
is not a field, an argument the body never uses, unbalanced braces, a name collision.
|
|
193
|
+
- **Typed**: `Fact("A soft woof.")` type-checks; fields are `Text`, `Raw` or either `| None`; values are `Length`, `CMYK`/`RGB`/`Gray` (components in [0, 1], no conversion between models) or `bool`.
|
|
194
|
+
|
|
195
|
+
### Generating a body
|
|
196
|
+
|
|
197
|
+
A body can also be built from pieces that are well-formed by construction, so a generator cannot write an unbalanced brace or
|
|
198
|
+
a bad control word. `Group(...)` is `{...}`, `Seq(...)` the same without braces, `Cs("par")` a control word (letters and `@`
|
|
199
|
+
only, else `TM-M027`; `_` and `:` too in `Syntax.EXPL3`), `Lit("50% & co")` text escaped as a `Text` field is, and `Hole`, `Ref` and `Raw` go inside. A space
|
|
200
|
+
follows a control word when the next text could continue its name. Loops and conditions are plain Python:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
class Rating(Command):
|
|
204
|
+
'''Says which level it has.'''
|
|
205
|
+
level: Level # a Choice whose members are macros: \lvLow, \lvHigh
|
|
206
|
+
|
|
207
|
+
def body(self):
|
|
208
|
+
for member in Level: # one branch per member, chosen when TeX expands the call
|
|
209
|
+
yield Seq(Cs("ifx"), Hole("level"), member.value, Group(Lit(member.name)), Cs("fi"))
|
|
210
|
+
# \ifx #1\lvLow{LOW}\fi
|
|
211
|
+
# \ifx #1\lvHigh{HIGH}\fi
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
texmodel writes this TeX and never reads TeX it did not write. The pieces are lines in the package's `Whitespace` policy;
|
|
215
|
+
a `Ref` inside a `Group` still makes its package a requirement.
|
|
216
|
+
|
|
217
|
+
### expl3 and library blocks
|
|
218
|
+
|
|
219
|
+
expl3 is a syntax mode and a source kind, not a typed language: texmodel has no vocabulary of `\fp_` or `\seq_` functions, and the
|
|
220
|
+
compile tells you when one is wrong. `Syntax` (`texmodel.body`) is `LATEX2E` (the default) or `EXPL3`. A body is in one syntax
|
|
221
|
+
(`TM-M031` if its pieces differ): give each `TexRaw`, `TemplateRef` and `Cs` that is expl3 `syntax=Syntax.EXPL3`. The generated
|
|
222
|
+
definition is then wrapped in `\ExplSyntaxOn` / `\ExplSyntaxOff`, no `%` is added at line ends (a piece's `Whitespace` is ignored),
|
|
223
|
+
and a `Lit` or a text argument of a `Ref` writes `~` for a space. A component whose `cs` has `_` or `:` says
|
|
224
|
+
`syntax = Syntax.EXPL3`.
|
|
225
|
+
|
|
226
|
+
`Package.library(name, source, syntax=Syntax.EXPL3)` carries TeX into the `.sty` verbatim: dedented, blank edges removed, no `%`
|
|
227
|
+
added, `#1` and `##1` as written, in registration order among the package's items, under a provenance comment. Its only static
|
|
228
|
+
checks are balanced braces (`TM-M011`) and no `\ExplSyntaxOn` / `\ExplSyntaxOff` inside an `EXPL3` block (`TM-M034`). Every
|
|
229
|
+
line of the block has its name as its plan path, and a name is used once (`TM-M007`). `source` is a string or a `TemplateRef`.
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
sizing = pkg.library("sizing", r"""
|
|
233
|
+
\fp_new:N \l__cd_size_fp
|
|
234
|
+
\newcommand{\cdFitSize}[2]{ \fp_set:Nn \l__cd_size_fp { min ( 14 , max ( 8 , 12 * #2 / #1 ) ) } }
|
|
235
|
+
""", syntax=Syntax.EXPL3)
|
|
236
|
+
|
|
237
|
+
@pkg.command
|
|
238
|
+
class FitSize(Command):
|
|
239
|
+
'''Work out the title size.'''
|
|
240
|
+
defined_by = sizing # declared and callable (FitSize.call(...)), but no \newcommand is rendered
|
|
241
|
+
natural: Raw
|
|
242
|
+
available: Raw
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
A component with `defined_by` has no body (`TM-M032`) and its block belongs to the same package (`TM-M033`). Whether TeX
|
|
246
|
+
really defines it, with that many arguments, is checked by compiling: `texmodel_build.testkit.check_library(compiler, pkg)`
|
|
247
|
+
builds `pkg.test_document(...)` with `export_probes(pkg)` (it writes `texmodel:probe:def.<cs>=yes|no` and
|
|
248
|
+
`arity.<cs>=<n>` for each such component), compiles it, and returns `TM-C010` for a name the block does not define and
|
|
249
|
+
`TM-C011` for another number of arguments (`?` is checked for existence only). The arity is the number of `#` in the macro's
|
|
250
|
+
`\meaning`, so a command defined with `\NewDocumentCommand` or with an optional first argument reports `0`, which
|
|
251
|
+
`check_exports` flags as `TM-C011` when more arguments are declared. A block that TeX refuses fails the compile with the block's name as the diagnostic's `path`.
|
|
252
|
+
|
|
253
|
+
The PDF Info strings need no expl3 in a document: `PdfInfo(Title="...", Author="...")` in `before_package` or `after_package`
|
|
254
|
+
writes each value PDF-escaped through `\csname pdfmanagement_add:nnn\endcsname`, and needs `RequireMetadata` in `before_class`
|
|
255
|
+
(`TM-C009`). A key is letters only (`TM-M035`).
|
|
256
|
+
|
|
257
|
+
### Standard libraries
|
|
258
|
+
|
|
259
|
+
`texmodel.std` has a header for each TeX library a deck loads: `latex` (the kernel), `graphicx`, `xcolor`, `geometry`,
|
|
260
|
+
`etoolbox`, `eso_pic`, `microtype`, `hyphenat`, `anyfontsize`, `calc`, `pdfrender`, `enumitem`, `adjustbox`, `tikz`,
|
|
261
|
+
`tcolorbox`, `fontspec`. A header is an adopted package with no prefix whose components name their TeX command with `cs`:
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
class IncludeGraphics(Command):
|
|
265
|
+
r"""Place a picture: `\includegraphics[width=3cm]{a.png}`."""
|
|
266
|
+
cs = "includegraphics" # the TeX name, stated outright; without `cs` it is prefix + class name
|
|
267
|
+
path: Raw
|
|
268
|
+
options: Raw | None = None # TeX's one optional argument, written first
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
A body that calls one (`IncludeGraphics.call(Hole("path"), options=Raw(r"width=\linewidth"))`) makes the generated `.sty`
|
|
272
|
+
load the package; the kernel header (`builtin=True`) never does. A library whose commands no body calls, or that takes
|
|
273
|
+
options, is loaded with `pkg.require(microtype, options=["protrusion=true"])`: `\RequirePackage[protrusion=true]{microtype}`,
|
|
274
|
+
in that order, before the packages the bodies need. `tests/test_std` compiles a call of every declared name against the
|
|
275
|
+
reference image, so a header that names a command TeX does not have fails, with the path of the call. A header covers
|
|
276
|
+
required `{}` arguments and one leading `[]`; a library's own syntax (TikZ paths such as `\draw (0,0) -- (1,1);`, key-value
|
|
277
|
+
declarations) is written as `Passthrough` or `Raw`.
|
|
278
|
+
|
|
279
|
+
### A package that is already written in TeX
|
|
280
|
+
|
|
281
|
+
When the `.sty` is hand-written, declare what it defines instead of generating it: `adopted=True` (M9). The components
|
|
282
|
+
have no body, `build` writes no file, documents are still checked and escaped, and `texmodel check` compares each
|
|
283
|
+
declaration with the TeX it names (`\newcommand`, `\providecommand` and `\newenvironment` are read; a name defined any
|
|
284
|
+
other way counts as missing):
|
|
285
|
+
|
|
286
|
+
```python
|
|
287
|
+
pkg = Package("card-deck", prefix="cd", adopted=True) # the package name is the hand-written one
|
|
288
|
+
|
|
289
|
+
@pkg.command
|
|
290
|
+
class TalkAboutMe(Command):
|
|
291
|
+
"""A question, with an optional answer."""
|
|
292
|
+
question: Text
|
|
293
|
+
answer: Text | None = None # declared [2][]: \newcommand{\cdTalkAboutMe}[2][]{...}
|
|
294
|
+
|
|
295
|
+
@pkg.command
|
|
296
|
+
class DeckName(Command): # no fields: a setting the deck sets with \renewcommand
|
|
297
|
+
"""The deck name printed on every card."""
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
```toml
|
|
301
|
+
[tool.texmodel]
|
|
302
|
+
package = "deck.model:pkg"
|
|
303
|
+
sources = ["res/brand/*.sty"] # globs, relative to pyproject.toml
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
```zsh
|
|
307
|
+
texmodel check # TM-C010: declared, not defined; TM-C011: defined with another signature
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`Package.document(..., preamble=PreambleSlots(before_class=, before_package=, after_package=))` places `Raw` TeX where the
|
|
311
|
+
document needs it: `before_class` for `\DocumentMetadata`, `before_package` for `\input@path`, `after_package` for
|
|
312
|
+
`\graphicspath` and per-document `\renewcommand`s. A public name of the package used there (`\cdSafeInset`) that no
|
|
313
|
+
component declares is refused with its path (`TM-M015`, `preamble.after_package[0]`). Typed entries render to one `Raw` and know
|
|
314
|
+
their slot: `PassOption("xcolor", "table")`, `InputPath("res/")` (`before_package`), `GraphicsPath("images/")` (`after_package`,
|
|
315
|
+
the current directory first unless `include_cwd=False`) and `RequireMetadata(lang="en-GB")` (`before_class`); one in another
|
|
316
|
+
slot is `TM-C008`. `tests/consumers/card_deck/` is a
|
|
317
|
+
complete example: a hand-written package, the generator it replaces and a 3-card deck.
|
|
318
|
+
|
|
319
|
+
### A text setting
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
@pkg.value
|
|
323
|
+
class DeckName(Value[Text]):
|
|
324
|
+
"""The deck name printed on every card."""
|
|
325
|
+
default = "Untitled" # an adopted package may leave it out: the hand-written TeX has it
|
|
326
|
+
|
|
327
|
+
pkg.document(card, settings=[DeckName("R&D")]) # \renewcommand{\acDeckName}{R\&D}, after \usepackage
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
A generated package defines `\newcommand{\acDeckName}{Untitled}`; for an adopted one `texmodel check` expects the hand-written
|
|
331
|
+
package to define the macro with no arguments. Length, colour and flag settings are written in the document body as before.
|
|
332
|
+
|
|
333
|
+
A `Value[Raw]` is the same for a setting that is TeX (`\newcommand{\acFooter}{\textit{draft}}`, renewed by `settings=[Footer(Raw(...))]`).
|
|
334
|
+
A `PdfString` field holds text for the inside of a PDF literal string you write yourself (`\pdfextension info{/Subject (#text)}`):
|
|
335
|
+
it is written as `\detokenize{...}` with `( ) \` and every TeX special as an octal escape (`\050`), and text with a non-ASCII
|
|
336
|
+
character as UTF-16BE, so a parenthesis cannot cut the string short, `%` or `_` cannot break the TeX, and a reader gets the
|
|
337
|
+
text back. It is not for `\hypersetup{pdftitle=...}`: hyperref escapes its own input.
|
|
338
|
+
|
|
339
|
+
### Layout that is not a component
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
class Page(Environment):
|
|
343
|
+
'''A page.'''
|
|
344
|
+
children = [Name, Anywhere(Heading), Fact.times(2, 3), Passthrough.times.any()]
|
|
345
|
+
|
|
346
|
+
Page(Name("Rex"), Heading("Facts"), Fact("a"), Passthrough(Raw(r"\vspace{1mm}")), Fact("b"))
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
`Anywhere(Heading)` allows zero or more of that component anywhere among the other children; it is not counted against the
|
|
350
|
+
ordered terms and the kind cannot also be an ordered term (`TM-M029`). `Passthrough(Raw(...))` writes its TeX as it is and is
|
|
351
|
+
accepted only where a grammar lists it.
|
|
352
|
+
|
|
353
|
+
### A closed set of values
|
|
354
|
+
|
|
355
|
+
A `Choice` is an enum whose members carry their TeX: a `Raw` passes through, a `str` is escaped. Use it as a field type
|
|
356
|
+
(optionally `| None`); data names a member by its name.
|
|
357
|
+
|
|
358
|
+
```python
|
|
359
|
+
@enum.unique
|
|
360
|
+
class SlotLabel(Choice):
|
|
361
|
+
SOUND = Raw(r"\deckParamSound")
|
|
362
|
+
WHERE = Raw(r"\deckParamWhere")
|
|
363
|
+
|
|
364
|
+
@pkg.command
|
|
365
|
+
class LabelledSlot(Command):
|
|
366
|
+
"""A fact with a label."""
|
|
367
|
+
label: SlotLabel
|
|
368
|
+
text: Text
|
|
369
|
+
|
|
370
|
+
LabelledSlot(SlotLabel.SOUND, "A soft woof.") # \cdLabelledSlot{\deckParamSound}{A soft woof.}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### A document from data
|
|
374
|
+
|
|
375
|
+
`texmodel.spec` loads a document from plain data (what TOML or JSON gives), using pydantic: a child names its component with
|
|
376
|
+
`"type"`, an environment's children are its `content`, a `Raw` field takes a string.
|
|
377
|
+
|
|
378
|
+
```python
|
|
379
|
+
from texmodel.spec import from_dict, json_schema, to_dict
|
|
380
|
+
|
|
381
|
+
card = from_dict(Card, {"content": [
|
|
382
|
+
{"type": "CardFront", "content": [{"type": "CardImage", "path": "a.jpg"}]},
|
|
383
|
+
{"type": "CardBack", "content": [{"type": "Name", "text": "Fox"}, {"type": "LeadFact", "text": "..."}]},
|
|
384
|
+
]})
|
|
385
|
+
card.validate() # the grammar (counts, order) is checked here, as for any tree
|
|
386
|
+
to_dict(card) # the same shape back
|
|
387
|
+
json_schema(Card) # JSON Schema of what from_dict accepts
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Everything wrong comes at once as an `ExceptionGroup` of `ValidationError` (`TM-C020`), each with a path such as
|
|
391
|
+
`Card/CardBack[1]/LeadFact[2].text`.
|
|
392
|
+
|
|
393
|
+
### A schema for the package's data
|
|
394
|
+
|
|
395
|
+
`texmodel schema` writes `json_schema(pkg)` to `[tool.texmodel] schema_output`: one JSON Schema with a `$defs` entry for every
|
|
396
|
+
command and environment (a child is told apart by its `"type"` constant) and `Value.<Name>` for every value, for editors and
|
|
397
|
+
validators of the TOML or JSON that `from_dict` reads. A renamed field keeps loading under its old key:
|
|
398
|
+
|
|
399
|
+
```python
|
|
400
|
+
class Fact(Command):
|
|
401
|
+
'''One fact.'''
|
|
402
|
+
text: Text = field(renamed_from="body") # {"body": "..."} loads; to_dict writes "text"; the schema has x-renamed-from
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### Checking committed documents
|
|
406
|
+
|
|
407
|
+
```toml
|
|
408
|
+
[tool.texmodel.documents]
|
|
409
|
+
"deck.tex" = "deck.build:plan()" # committed file = pointer to a Plan (or a str) the model renders
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
`texmodel check` renders each pointer from today's model and compares it with the committed file, like `terraform plan`:
|
|
413
|
+
for a `Plan` every difference names the component that writes the line (`changed deck.tex Card[1]/CardBack[1]/LabelledSlot[3]: ...`),
|
|
414
|
+
so a hand edit and a stale render read the same; a `str` is compared whole. Exit 1 on any difference; `texmodel render` with no
|
|
415
|
+
pointer rewrites every configured document.
|
|
416
|
+
|
|
417
|
+
### Comments
|
|
418
|
+
|
|
419
|
+
```python
|
|
420
|
+
from texmodel.body import Comment
|
|
421
|
+
|
|
422
|
+
class CardHeader(Comment):
|
|
423
|
+
"""The audit block above a card."""
|
|
424
|
+
template = """
|
|
425
|
+
Card: #name
|
|
426
|
+
id: #id
|
|
427
|
+
"""
|
|
428
|
+
name: str
|
|
429
|
+
id: str
|
|
430
|
+
|
|
431
|
+
pkg.document(CardHeader("Red Fox", "11111111-aaaa"), Card(...)) # % Card: Red Fox / % id: 11111111-aaaa
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
Every line of a `Comment` is prefixed `% `, so a value cannot leave it; nothing is escaped (`##` is a `#` in a template). It
|
|
435
|
+
is a body piece (`yield Comment("why this is 3mm")`) and a document node: a child of an environment or a top-level node, and
|
|
436
|
+
transparent to the grammar (not counted, never listed, paths keep the real index: `Card[3]/CardHeader[0]`). It needs no
|
|
437
|
+
package registration. `plan.edits(text, ignore_comments=True)` skips lines that are comments on both sides;
|
|
438
|
+
`from_dict(cls, data, comments=[CardHeader])` and `json_schema(cls, comments=[...])` know it as a child (`"type": "CardHeader"`).
|
|
439
|
+
|
|
440
|
+
### Testing generated code with stubs
|
|
441
|
+
|
|
442
|
+
A component's body decides what it passes on to other components. `expand(node, stubs=[Fact])` (`texmodel.testing`) walks the
|
|
443
|
+
bodies with the node's values and returns the body text and a `Call` for every stubbed component, in order, with no TeX:
|
|
444
|
+
|
|
445
|
+
```python
|
|
446
|
+
from texmodel.testing import Call, expand
|
|
447
|
+
|
|
448
|
+
section = Section(Entry("First"), Entry("R&D 50%"), title="Notes")
|
|
449
|
+
assert [c for c in expand(section, stubs=[Fact]) if isinstance(c, Call)] == [
|
|
450
|
+
Call("bkFact", ("First",)), Call("bkFact", (r"R\&D 50\%",))]
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
The TeX tier checks the real macros do the same: `pkg.test_document(section, stub=[Fact])` loads the real packages and replaces
|
|
454
|
+
each stub with a macro that writes `texmodel:probe:callN=bkFact{...}` to the log; `texmodel_build.testkit.run_probes(compiler,
|
|
455
|
+
tex, files={"base-kit.sty": ...})` compiles it and returns the probes, which `Call.parse` reads. Arguments are shown as
|
|
456
|
+
`\detokenize` shows them (a space after each control word), so both tiers agree. Layout is not covered: this tests what a
|
|
457
|
+
component passes on, not how it looks.
|
|
458
|
+
|
|
459
|
+
### Where a line came from
|
|
460
|
+
|
|
461
|
+
`pkg.plan(...)` takes the arguments of `pkg.document(...)` and returns a `Plan`: every output line with the path of what wrote it
|
|
462
|
+
(`Card[1]/CardBack[1]/LabelledSlot[3]`, `preamble.after_package[0]`). `plan.text` is the document. Store `plan.to_json()` beside
|
|
463
|
+
the committed file; `Plan.from_json(saved).edits(text)` then names each hand edit by the path it had when it was written, even
|
|
464
|
+
if the model has changed since, and `plan.path_of(39)` names the component behind a log's `input line 39`. `Compiler.compile(src, out, plan=plan)` does
|
|
465
|
+
that for every diagnostic in the source: `diagnostic.path` is the component behind its line; `plans={"acme.sty": pkg.render_plan()}` does the
|
|
466
|
+
same for diagnostics in other files, by file name.
|
|
467
|
+
|
|
468
|
+
`texmodel.bundle` copies a manifest of files and refuses if any is missing (`stale` compares bytes, not timestamps);
|
|
469
|
+
`texmodel.drift` is what `check` uses; `texmodel.pointer` resolves `module:object` pointers (a package, a document, a `Plan`).
|
|
470
|
+
|
|
471
|
+
## texmodel-build
|
|
472
|
+
|
|
473
|
+
```zsh
|
|
474
|
+
texmodel-build doctor # tools on this machine, and what the configured image has
|
|
475
|
+
texmodel-build doctor --sty 'res/*.sty' # which files those packages load that the toolchain lacks (kpsewhich), with a tlmgr line; exit 3
|
|
476
|
+
texmodel-build compile doc.tex # exit 0 ok, 1 errors in the document, 3 tool or image missing
|
|
477
|
+
texmodel-build compile doc.tex --json --epoch 1700000000 --root generated
|
|
478
|
+
texmodel-build compile deck.tex --plan "deck.build:plan()" # diagnostics name the component: [Card[1]/CardBack[1]/Name[0]]
|
|
479
|
+
texmodel-build build -j 4 # every [tool.texmodel-build.targets.*]
|
|
480
|
+
texmodel-build verify out/*.pdf # verifier plugins; anything unconfirmable fails
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
```toml
|
|
484
|
+
[tool.texmodel-build]
|
|
485
|
+
image = "releaseart/lualatex-python:latest" # runs `docker run` over it; omit to run on this machine
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
- The compile log becomes typed diagnostics with stable codes: undefined control sequences (`TM-X010`), missing glyphs
|
|
489
|
+
(`TM-X011`), overfull boxes (`TM-X012`), missing files (`TM-X013`), package errors (`TM-X014`), undefined references
|
|
490
|
+
(`TM-X018`), package warnings with their continuation lines (`TM-X019`, how a hand-written package says a card does
|
|
491
|
+
not fit), non-convergence with the `.aux` entries that keep moving (`TM-X030`), and a fallback so no failure is silent.
|
|
492
|
+
- A package's "does not fit" warning becomes a typed `Overset` (`TM-X031`, an error: item, needs, has, over, in pt) when a
|
|
493
|
+
rule says how to read it; the warning is kept too. A package that prints `texmodel:probe:fit.<item>=<needs>/<has>` needs
|
|
494
|
+
no rule. Lengths take `pt`, `mm`, `cm`, `in` (a bare number is pt); an item that fits gives none.
|
|
495
|
+
|
|
496
|
+
```toml
|
|
497
|
+
[tool.texmodel-build.measures.card-fit]
|
|
498
|
+
package = "card-deck"
|
|
499
|
+
pattern = "Card '(?P<item>[^']+)' does not fit. Its body needs (?P<needs>\\S+) and the card has (?P<has>\\S+)"
|
|
500
|
+
```
|
|
501
|
+
- Reproducibility is the image's job (`--epoch` pins `SOURCE_DATE_EPOCH`); there is no sandbox or cache here.
|
|
502
|
+
- `texmodel-build verify` runs plugins from the `texmodel_build.verifiers` entry-point group. `pip install
|
|
503
|
+
'texmodel[proof-pdf]'` adds [proof-pdf](https://github.com/release-art/proofpress). A verifier that cannot run, a
|
|
504
|
+
file that cannot be opened and a profile that selects nothing are failures; the only excuse is a declared `exempt` glob.
|
|
505
|
+
- The image must contain `latexmk` (`releaseart/lualatex-python` has it since proofpress v0.3.4); `doctor` reports it if not.
|
|
506
|
+
|
|
507
|
+
## Where it is going
|
|
508
|
+
|
|
509
|
+
Open work is in [the plan](docs/texmodel-plan.md) (§19, §21a) and [the roadmap](docs/briefs/roadmap.md).
|
|
510
|
+
|
|
511
|
+
## Development
|
|
512
|
+
|
|
513
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). `bin/autoformat.sh` runs ruff, strict pyright and import-linter over both packages;
|
|
514
|
+
`bin/test.sh` runs pytest with a 100% coverage gate. The container compile suite is `pytest -m tex`; it skips without an
|
|
515
|
+
image that has `latexmk`, and `TEXMODEL_REQUIRE_TEX=1` turns that skip into a failure (`TEXMODEL_TEST_IMAGE` sets the image).
|
|
516
|
+
`bin/test-image.sh` builds `texmodel-test` from `docker/test/Dockerfile`: the published reference image plus the few TeX
|
|
517
|
+
packages the suite needs that it does not carry yet (`TEXMODEL_TEST_IMAGE=texmodel-test pdm run pytest -m tex`); CI uses it.
|
|
518
|
+
|
|
519
|
+
Public API is the top-level modules of each package; everything under `_internal` may change. Report vulnerabilities as
|
|
520
|
+
described in [SECURITY.md](SECURITY.md).
|
|
521
|
+
|
|
522
|
+
## Licence
|
|
523
|
+
|
|
524
|
+
[MIT](LICENSE). The reference images carry their own licences (Ghostscript is AGPL); `texmodel-build` runs them as a
|
|
525
|
+
separate process and ships none of their code.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
texmodel-0.2.0.dist-info/METADATA,sha256=zRxZcSfu5fVDTD6pPgFGlxiR5HuhgCDeg7xnWI6yQ_A,26431
|
|
2
|
+
texmodel-0.2.0.dist-info/WHEEL,sha256=IuIk3wbaEnFH1cx75iM2R1CmliRI3dFLXTbRakgvgFQ,90
|
|
3
|
+
texmodel-0.2.0.dist-info/entry_points.txt,sha256=puFHFL4LAGobPQQq-Ep5Sgs8gMbjGMb0TdlySPoNlTg,185
|
|
4
|
+
texmodel-0.2.0.dist-info/licenses/LICENSE,sha256=URgqTzVBt4gXkRH2QdXRAmSR42oS1Vziq6C60LxzAoc,1068
|
|
5
|
+
texmodel/__init__.py,sha256=Em5KgLZq-TmbV3BBRADVpWF6tmfer4sLFqkx0WyD_Fc,209
|
|
6
|
+
texmodel/_internal/__init__.py,sha256=l9vBn8tRgAirjUinA-cz7wNOAma0RIOd21bvis8HHko,95
|
|
7
|
+
texmodel/_internal/render.py,sha256=jA8uyj2DLc3CF_mtPmT07m1DvBSN1NKUHPO45QgHQjg,13746
|
|
8
|
+
texmodel/_internal/scan.py,sha256=37j4YmtWOZec0yPyckwgdZSWlE6DTV7wE7VQLQRccow,1064
|
|
9
|
+
texmodel/body.py,sha256=MFbqkqASy6tX_AjLGKNiwYk_34PrJLLW7Onpy4be-hE,14559
|
|
10
|
+
texmodel/bundle.py,sha256=LfR0wjPdpKzMp_3Sataa84VQs4OvdlQPBWsKlvngxRA,2931
|
|
11
|
+
texmodel/cli.py,sha256=Ge625idE1K3M9jDzK86F0B2N4ahKALieUioSqjSUdEQ,8777
|
|
12
|
+
texmodel/components.py,sha256=gHTJXBrvjsOeYYzx7xMWH-nY4Lm-9yjqHsEkrJtbtcY,31732
|
|
13
|
+
texmodel/config.py,sha256=zfFRG7yu5kqQbg8XDfK0YLT1ULdcYAu1EaKWIfBoE9k,1532
|
|
14
|
+
texmodel/drift.py,sha256=6Tff6enjsiHRbes70pgYj7N7t4XwoZpDVJLjQlG4g8A,2268
|
|
15
|
+
texmodel/errors.py,sha256=crF8niPGDsytR-u_Gje1GMNrVRt2b55VzeZTdHTT8jE,1103
|
|
16
|
+
texmodel/package.py,sha256=uy6FaLyT9VM_saeHEl4dcJWYfOqJKdwKuuPAEyGb-qE,23934
|
|
17
|
+
texmodel/plan.py,sha256=zA4Mq88EzfAAdDEAc0JXqVNvTGqy1JYE3LGXLn6lQp4,3689
|
|
18
|
+
texmodel/pointer.py,sha256=0MP1YGXYpy4Lq9r3rUXw2XAYeQEHWIYpdIwdxsEM9nE,3023
|
|
19
|
+
texmodel/preamble.py,sha256=wXK_pm6NYHCVu3OxnZ5rL3lFi0bJWHGdds_7Wzr94RU,4003
|
|
20
|
+
texmodel/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
+
texmodel/spec.py,sha256=jUmMPMl4ta5xAYVfPfM-W-dcGr19EbPsx4Y3QxlmgpE,11014
|
|
22
|
+
texmodel/std/__init__.py,sha256=m4VRcFOBaD16Dwy0bKzI0mEcKazLpvf04I2XQX8sQh8,948
|
|
23
|
+
texmodel/std/adjustbox.py,sha256=76FQxaryinz-ea9U3mIGS31O9QSjzqimfll3_9ww4Ec,617
|
|
24
|
+
texmodel/std/anyfontsize.py,sha256=6sJ_StYcDAIFHfSBBjKZqCUfvNnPIS4nRnASd9qN1Ak,298
|
|
25
|
+
texmodel/std/calc.py,sha256=uEXeMCFOX_McuC0O7rLwXersJVx7NrOE4LuRSYeYcoc,305
|
|
26
|
+
texmodel/std/enumitem.py,sha256=2B6emV84lkrNV6f0WZ78S6oNKv0_SGGxnOitqRY3GYM,479
|
|
27
|
+
texmodel/std/eso_pic.py,sha256=ljOAsfQm5oor2L-XHX6XMk1lllv_ScCo8maHJJQ9n-k,1085
|
|
28
|
+
texmodel/std/etoolbox.py,sha256=vQX0iFFai5-VV88ZPD8JJUpkQfrBGczEUGNX5ItYt2g,2128
|
|
29
|
+
texmodel/std/fontspec.py,sha256=LaeA30gZPV_SVK2iql_7mlq8odUiqmeUJ1Cj-7GohiM,820
|
|
30
|
+
texmodel/std/geometry.py,sha256=th85z2CbPn4OvLKY1xkoayFOLZN5epyb3NM0sPCai3s,457
|
|
31
|
+
texmodel/std/graphicx.py,sha256=W3EkpuUBsmDyoeybwSCwlD9OxiPYZhAF-aolcUwsubE,723
|
|
32
|
+
texmodel/std/hyphenat.py,sha256=Aq6khJYdNPEFRU1TSq0KMhZ8K6iwdF7uG2zGOWRDidA,304
|
|
33
|
+
texmodel/std/latex.py,sha256=DeNuVltylxqf-vi42-8HZJGxNxhjKgK1ChDlYmxzgmo,8355
|
|
34
|
+
texmodel/std/microtype.py,sha256=H6kFCyoDWyDE6Ipz5MGUmmOnvYe8TyRnLEc1i7N0pJA,678
|
|
35
|
+
texmodel/std/pdfrender.py,sha256=OL-U0yvNY1-zL7qiiW8LRtjoK8HZyoEsw-ageCWOxVI,500
|
|
36
|
+
texmodel/std/tcolorbox.py,sha256=6EdTb_FHQbPsY5-1vMzeDzwBEkfQkhkYTKeuNciaBhI,980
|
|
37
|
+
texmodel/std/tikz.py,sha256=8KNp7nLA7juuOpvyHpa94wM8pRUqgm2R9tObKNwkEq4,957
|
|
38
|
+
texmodel/std/xcolor.py,sha256=-0tRwRiPNkl5nNjPX1obJPqLDfQzLxAgXwV1iqPVRnM,967
|
|
39
|
+
texmodel/testing.py,sha256=SnINPP91Z2wlANgxbm22ZqQulX1zBh5sOILXdSEToWY,12672
|
|
40
|
+
texmodel/types.py,sha256=bbB5gesgHtdOtwhWWwAQCzQC96G3T_E3GWZQ9YZRRL0,9042
|
|
41
|
+
texmodel_build/__init__.py,sha256=e0PUY5lEa8uDENTmd9gg7KyvyvZOudlspPfyhRVMCmo,304
|
|
42
|
+
texmodel_build/_internal/__init__.py,sha256=l9vBn8tRgAirjUinA-cz7wNOAma0RIOd21bvis8HHko,95
|
|
43
|
+
texmodel_build/_internal/converge.py,sha256=3tz3da0uqMTQ2BBwaXnVtx-skq6RWFGau9s5VgGJ_7g,718
|
|
44
|
+
texmodel_build/_internal/env.py,sha256=dVu1cWjpE0yVZeXzdEspf2apBdmwj2jFW3UBFrunhQk,1386
|
|
45
|
+
texmodel_build/_internal/logparse.py,sha256=dh3uKSx_rWSeqwaAKV7JDg5GTgFA7fiY05cM0RI5g-I,7399
|
|
46
|
+
texmodel_build/_internal/proofpdf.py,sha256=ITcJwf5CUZcU3yB2kGIFqywsjQ2Qtoqrja91zirjnWc,1917
|
|
47
|
+
texmodel_build/_internal/spawn.py,sha256=9erYTQl7AkPDyHWbvLod5wl3jo_pq1Q0Feu29gj2Kkw,1206
|
|
48
|
+
texmodel_build/_internal/styscan.py,sha256=Krx5xQu07pVuFs6ytp8wgTdDwZ2KSRByYoE6JbCogu0,1463
|
|
49
|
+
texmodel_build/cli.py,sha256=WelPZovxcyRNFuU43hPA7R-zSuei3nApyun-sWyHiIo,13890
|
|
50
|
+
texmodel_build/compiler.py,sha256=65XQ1WAaNlBna4PaDkdl0kIFD84WON1JEVIyvg5rzrA,16525
|
|
51
|
+
texmodel_build/config.py,sha256=WHPTF2jPTZfg4Hwb2SvCJjl77O3ZL1kb2IdGY9sJGVg,2563
|
|
52
|
+
texmodel_build/diagnostics.py,sha256=YjeJa6f5Y5E1gHt6b7szL4Iw7NZ8Nh3r4bAEIoVrmN4,7003
|
|
53
|
+
texmodel_build/engines.py,sha256=VBZEXbqZQSgrcxBDy2HTK0ar1wAsC9HLx3Ofmj2I9M0,1009
|
|
54
|
+
texmodel_build/errors.py,sha256=neuVHtIgS_KcN_Lfp9HY-LBF0C4DHpm4OOYZbv-gi6I,629
|
|
55
|
+
texmodel_build/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
56
|
+
texmodel_build/result.py,sha256=txW26wSIW5lpMKvzkQhxe6LW5jV3ebWgUySDxRHTleQ,2247
|
|
57
|
+
texmodel_build/roots.py,sha256=DdmncdQUlvvk1jMF60Mh2uWab6PQqQYkuuXpLOoiayQ,1613
|
|
58
|
+
texmodel_build/targets.py,sha256=ZwXGl4IZVpzxOjwV6aRHovvrZGkX6Bh_empI1ERPZbk,1559
|
|
59
|
+
texmodel_build/testkit.py,sha256=IcCQ3Ruph5ZhzBQXXooxGtpFguWi_s303XuDwKVvT8E,3261
|
|
60
|
+
texmodel_build/verify.py,sha256=TPfYAxQQ1xxxKBG9yw1xEqgXxtoAJ4UuapzmKH9KorU,5760
|
|
61
|
+
texmodel-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ilja Orlovs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Compile and verify LaTeX built from texmodel: a thin `latexmk` driver, typed log diagnostics, verifier plugins."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from .compiler import Compiler
|
|
6
|
+
from .result import CompileResult
|
|
7
|
+
|
|
8
|
+
__all__ = ["CompileResult", "Compiler"]
|
|
9
|
+
|
|
10
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Private implementation. Nothing here is public API and any of it may change at any time."""
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Why a document did not converge (C1a): which `.aux` entries changed between the last two passes."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
_KEYED = re.compile(r"^\\(?:newlabel|bibcite)\{(?P<key>[^}]*)\}|^\\gdef\s*\\(?P<macro>[A-Za-z@]+)")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _entries(aux: str) -> dict[str, str]:
|
|
9
|
+
out: dict[str, str] = {}
|
|
10
|
+
for line in aux.splitlines():
|
|
11
|
+
m = _KEYED.match(line)
|
|
12
|
+
out[(m["key"] or m["macro"]) if m else line] = line
|
|
13
|
+
return out
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def aux_diff(before: str, after: str) -> tuple[str, ...]:
|
|
17
|
+
"""Labels (or whole lines for unkeyed entries) that differ between two `.aux` snapshots, sorted."""
|
|
18
|
+
a, b = _entries(before), _entries(after)
|
|
19
|
+
return tuple(sorted(k for k in a.keys() | b.keys() if a.get(k) != b.get(k)))
|