texmodel 0.2.0__tar.gz → 0.3.1__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.
- {texmodel-0.2.0 → texmodel-0.3.1}/PKG-INFO +126 -11
- {texmodel-0.2.0 → texmodel-0.3.1}/README.md +125 -10
- {texmodel-0.2.0 → texmodel-0.3.1}/pyproject.toml +1 -1
- texmodel-0.3.1/src/texmodel/_internal/render.py +519 -0
- texmodel-0.3.1/src/texmodel/_internal/scan.py +106 -0
- texmodel-0.3.1/src/texmodel/_internal/values.py +174 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/body.py +2 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/cli.py +31 -12
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/components.py +253 -111
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/config.py +3 -1
- texmodel-0.3.1/src/texmodel/interface.py +293 -0
- texmodel-0.3.1/src/texmodel/keys.py +36 -0
- texmodel-0.3.1/src/texmodel/keyvals.py +231 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/package.py +171 -34
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/pointer.py +31 -1
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/spec.py +19 -12
- texmodel-0.3.1/src/texmodel/std/adjustbox.py +56 -0
- texmodel-0.3.1/src/texmodel/std/calc.py +387 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/std/etoolbox.py +57 -1
- texmodel-0.3.1/src/texmodel/std/geometry.py +49 -0
- texmodel-0.3.1/src/texmodel/std/graphicx.py +45 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/std/latex.py +85 -9
- texmodel-0.3.1/src/texmodel/std/tcolorbox.py +68 -0
- texmodel-0.3.1/src/texmodel/std/tikz.py +61 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/testing.py +20 -8
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/types.py +108 -4
- texmodel-0.3.1/tests/test_docs.py +22 -0
- texmodel-0.3.1/tests/test_std/test_calc.py +313 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_std/test_headers.py +86 -17
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_adopted.py +2 -2
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_build_check.py +101 -1
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_document.py +3 -3
- texmodel-0.3.1/tests/test_texmodel/test_interface.py +546 -0
- texmodel-0.3.1/tests/test_texmodel/test_interface_tex.py +198 -0
- texmodel-0.3.1/tests/test_texmodel/test_keyvals.py +342 -0
- texmodel-0.3.1/tests/test_texmodel/test_parts.py +403 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_public_api.py +3 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_render.py +3 -3
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_text_values.py +8 -6
- texmodel-0.3.1/tests/test_texmodel/test_value_kinds_and_meta.py +204 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_values.py +1 -1
- texmodel-0.2.0/src/texmodel/_internal/render.py +0 -323
- texmodel-0.2.0/src/texmodel/_internal/scan.py +0 -23
- texmodel-0.2.0/src/texmodel/std/adjustbox.py +0 -19
- texmodel-0.2.0/src/texmodel/std/calc.py +0 -7
- texmodel-0.2.0/src/texmodel/std/geometry.py +0 -17
- texmodel-0.2.0/src/texmodel/std/graphicx.py +0 -26
- texmodel-0.2.0/src/texmodel/std/tcolorbox.py +0 -43
- texmodel-0.2.0/src/texmodel/std/tikz.py +0 -36
- {texmodel-0.2.0 → texmodel-0.3.1}/LICENSE +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/__init__.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/_internal/__init__.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/bundle.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/drift.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/errors.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/plan.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/preamble.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/py.typed +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/std/__init__.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/std/anyfontsize.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/std/enumitem.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/std/eso_pic.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/std/fontspec.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/std/hyphenat.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/std/microtype.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/std/pdfrender.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/std/xcolor.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/__init__.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/_internal/__init__.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/_internal/converge.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/_internal/env.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/_internal/logparse.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/_internal/proofpdf.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/_internal/spawn.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/_internal/styscan.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/cli.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/compiler.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/config.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/diagnostics.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/engines.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/errors.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/py.typed +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/result.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/roots.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/targets.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/testkit.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/verify.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/consumers/card_deck/__init__.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/consumers/card_deck/cards.toml +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/consumers/card_deck/convert.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/consumers/card_deck/fit.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/consumers/card_deck/legacy.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/consumers/card_deck/model.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/consumers/card_deck/old/card-deck.sty +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/consumers/card_deck/project.toml +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/consumers/card_deck/res/card-deck.sty +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/README.md +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/business_card/__init__.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/business_card/card.begin.tex +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/business_card/card.end.tex +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/business_card/contact.tex +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/business_card/expected/ada.tex +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/business_card/expected/business-card.sty +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/business_card/expected/compact.tex +1 -1
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/business_card/name.tex +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/composed/__init__.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/composed/expected/base-kit.sty +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/composed/expected/composed-kit.sty +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/composed/expected/composed.tex +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/expl3/__init__.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/expl3/expected/expl3-kit.sty +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/expl3/expected/expl3.tex +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/hello/__init__.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/hello/expected/hello.sty +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/hello/expected/hello.tex +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/letter/__init__.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/letter/expected/invitation.tex +1 -1
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/letter/expected/letter-kit.sty +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/letter/expected/terse.tex +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/letter/letter.begin.tex +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/letter/letter.end.tex +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/__init__.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/card_back.begin.tex +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/card_back.end.tex +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/fact.tex +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/generated/acme-cards.sty +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/name.tex +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/slot.tex +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/logs/diagnostics.latexmk.txt +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/logs/diagnostics.log +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/logs/miss.log +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/logs/ok.log +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/logs/overset.log +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/logs/pkg.log +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/logs/sty.log +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/logs/undefined_in_argument.log +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/tex/bad.tex +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/tex/bad_font.tex +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/tex/missing_file.tex +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/tex/missing_package.tex +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/tex/ok.tex +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/tex/shell_escape.tex +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/tex/unstable.tex +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/tex/uses_acme.tex +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/tex/uses_values.tex +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_consumers/__init__.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_consumers/test_card_deck.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_consumers/test_card_deck_fit.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_consumers/test_card_deck_tex.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_std/__init__.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/__init__.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/block.tex +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/conftest.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_body.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_bundle_drift.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_check_documents.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_choice.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_comment.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_cs.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_emitter.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_errors_config_cli.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_examples.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_expl3.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_expl3_tex.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_grammar_hatches.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_import.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_pdf_string.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_plan.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_preamble.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_refs.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_schema.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_spec.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_stubs.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_types.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel_build/__init__.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel_build/conftest.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_cli.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_compile_tex.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_compiler.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_config_doctor.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_diagnostics_logparse.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_doctor_sty.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_proofpdf.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_public_api.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_support.py +0 -0
- {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_verify.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: texmodel
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Typed document-family models rendered to TeX, with an optional latexmk driver and verifier plugins.
|
|
5
5
|
Author-Email: Ilja Orlovs <ilja@release.art>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -25,8 +25,9 @@ Description-Content-Type: text/markdown
|
|
|
25
25
|
Declare a LaTeX document family once in Python types; get plain, readable, git-trackable `.sty` source out of it. The output
|
|
26
26
|
is ordinary TeX: commit it, diff it, compile it with whatever LaTeX setup you already have.
|
|
27
27
|
|
|
28
|
-
> **Status:
|
|
29
|
-
> and sandbox layers are planned, not built (see [the plan](docs/texmodel-plan.md)).
|
|
28
|
+
> **Status: 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)). What is missing today:
|
|
30
|
+
> [known limitations](docs/known-limitations.md).
|
|
30
31
|
|
|
31
32
|
```zsh
|
|
32
33
|
pip install texmodel # the library and the texmodel-build command
|
|
@@ -42,6 +43,8 @@ One distribution, two packages:
|
|
|
42
43
|
|
|
43
44
|
## texmodel
|
|
44
45
|
|
|
46
|
+
Worked examples and practices: [the cookbook](docs/cookbook.md).
|
|
47
|
+
|
|
45
48
|
```python
|
|
46
49
|
# acme_cards/__init__.py
|
|
47
50
|
from texmodel.body import TemplateRef
|
|
@@ -50,6 +53,7 @@ from texmodel.package import Package
|
|
|
50
53
|
from texmodel.types import CMYK, Colour, Length, Text
|
|
51
54
|
|
|
52
55
|
pkg = Package("acme-cards", prefix="ac", version="1.4.0", date="2026/10/21")
|
|
56
|
+
# `date=None` leaves the date out of \ProvidesPackage; `header="..."` adds comment lines under the generator's line
|
|
53
57
|
|
|
54
58
|
@pkg.value
|
|
55
59
|
class FactGap(Value[Length]):
|
|
@@ -164,6 +168,15 @@ A pointer (`package =`, and the argument of `texmodel render`) is `dotted.module
|
|
|
164
168
|
package = "acme_cards.decks:Decks.birds" # or "acme_cards.decks:make_package()"
|
|
165
169
|
```
|
|
166
170
|
|
|
171
|
+
A family of packages (a core, its bases, one per series) is a list of pointers, or one pointer to an iterable of
|
|
172
|
+
`Package`s; `build` and `check` cover them all together (a package name must be unique, as it is a file):
|
|
173
|
+
|
|
174
|
+
```toml
|
|
175
|
+
package = ["acme_cards.core:pkg", "acme_cards.birds:pkg"] # or "acme_cards.family:all_packages()"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`texmodel schema` describes one package's data, so it needs a single `package`.
|
|
179
|
+
|
|
167
180
|
```zsh
|
|
168
181
|
texmodel build # writes generated/acme-cards.sty
|
|
169
182
|
texmodel render docs:card # writes card.tex from `card = pkg.document(...)` (or a function returning one); -o to choose
|
|
@@ -192,6 +205,22 @@ texmodel check # exit 1 if a committed generated file is stale, missing or o
|
|
|
192
205
|
is not a field, an argument the body never uses, unbalanced braces, a name collision.
|
|
193
206
|
- **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
207
|
|
|
208
|
+
### Shared bases
|
|
209
|
+
|
|
210
|
+
Components that repeat a body or a grammar share a base declared `abstract=True`. It carries fields, `body` (or `begin` and
|
|
211
|
+
`end`) and `children`; it needs no docstring and may have any Python name (`_Image`); it is never registered
|
|
212
|
+
(`TM-M043`), and its subclasses inherit all of it:
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
class _Image(Command, abstract=True):
|
|
216
|
+
src: Text
|
|
217
|
+
def body(self): return r"\includegraphics{#src}"
|
|
218
|
+
|
|
219
|
+
@pkg.command
|
|
220
|
+
class Photo(_Image):
|
|
221
|
+
'''A photo.'''
|
|
222
|
+
```
|
|
223
|
+
|
|
195
224
|
### Generating a body
|
|
196
225
|
|
|
197
226
|
A body can also be built from pieces that are well-formed by construction, so a generator cannot write an unbalanced brace or
|
|
@@ -242,8 +271,14 @@ class FitSize(Command):
|
|
|
242
271
|
available: Raw
|
|
243
272
|
```
|
|
244
273
|
|
|
245
|
-
A component with `defined_by` has no body (`TM-M032`) and its block belongs to the same package (`TM-M033`).
|
|
246
|
-
|
|
274
|
+
A component with `defined_by` has no body (`TM-M032`) and its block belongs to the same package (`TM-M033`). `texmodel check`
|
|
275
|
+
reads each block (`Package.check_parts()`, no TeX needed) and reports a declared component the block does not define with that
|
|
276
|
+
signature (`TM-C010`, `TM-C011`), a public `<prefix>Name` the block defines that nothing declares (`TM-C012`), and a name
|
|
277
|
+
defined twice (`TM-C013`; generation itself refuses it, `TM-P012`). The reading is regex-level (`\newcommand`, `\providecommand`,
|
|
278
|
+
`\DeclareRobustCommand`, `\newenvironment`, the xparse family, `\newlength`, `\newtoggle`, `\newif`, `\definecolor`) and
|
|
279
|
+
skips an expl3 block. A flag, toggle, length, colour or text setting a block defines is a `Value` with `defined_by` (no default;
|
|
280
|
+
`cs = "..."` gives the whole control sequence, `colour_name` the colour's name). For the
|
|
281
|
+
rest, whether TeX really defines it, with that many arguments, is checked by compiling: `texmodel_build.testkit.check_library(compiler, pkg)`
|
|
247
282
|
builds `pkg.test_document(...)` with `export_probes(pkg)` (it writes `texmodel:probe:def.<cs>=yes|no` and
|
|
248
283
|
`arity.<cs>=<n>` for each such component), compiles it, and returns `TM-C010` for a name the block does not define and
|
|
249
284
|
`TM-C011` for another number of arguments (`?` is checked for existence only). The arity is the number of `#` in the macro's
|
|
@@ -254,6 +289,85 @@ The PDF Info strings need no expl3 in a document: `PdfInfo(Title="...", Author="
|
|
|
254
289
|
writes each value PDF-escaped through `\csname pdfmanagement_add:nnn\endcsname`, and needs `RequireMetadata` in `before_class`
|
|
255
290
|
(`TM-C009`). A key is letters only (`TM-M035`).
|
|
256
291
|
|
|
292
|
+
### Parts filled from a model
|
|
293
|
+
|
|
294
|
+
A package that starts from TeX you already have is a list of parts: `pkg.library(name, TemplateRef("brand.tex"), fill=model)`.
|
|
295
|
+
`fill` is a mapping, a dataclass or a pydantic model, and the text's `<<name>>` holes (`<<a.b>>` into a nested model) are
|
|
296
|
+
filled by type: `str` is escaped, `Raw` passes through, a `Length`, a colour model, a number or a flag is written as it is.
|
|
297
|
+
A hole the model has no field for is an error (`TM-M041`), as is a value of another type (`TM-M040`); a model may hold more
|
|
298
|
+
than the part uses. Without `fill` a block is carried exactly as it is. Components with `defined_by` then give decks typed
|
|
299
|
+
calls to what the parts define. [The cookbook](docs/cookbook.md) has a worked example.
|
|
300
|
+
|
|
301
|
+
### Arguments: slots, value types and key lists
|
|
302
|
+
|
|
303
|
+
A field is an argument; its type says what it holds and `arg(...)` says how it is written (`texmodel.interface`).
|
|
304
|
+
Without `arg()`, a required field is `{...}` and an optional one (a default) is TeX's one optional argument, `[...]`,
|
|
305
|
+
written first. With `arg()` the slot keeps its declaration order, so any signature can be stated:
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
from texmodel.interface import PAREN, SQUARE, Gap, SlotGroup, Star, arg
|
|
309
|
+
|
|
310
|
+
class MakeBox(Command): # \makebox[3cm][c]{x}
|
|
311
|
+
cs = "makebox"
|
|
312
|
+
width: Length | None = arg(SQUARE, default=None, fill=Gap.REFUSE) # `[c]` alone is not valid: refused
|
|
313
|
+
align: BoxAlign | None = arg(SQUARE, default=None)
|
|
314
|
+
text: Raw
|
|
315
|
+
|
|
316
|
+
class Section(Command): # \section*[short]{long}
|
|
317
|
+
cs = "section"
|
|
318
|
+
starred: Star = arg(default=False) # a token: `*`
|
|
319
|
+
short: Text | None = arg(SQUARE, default=None)
|
|
320
|
+
title: Text
|
|
321
|
+
|
|
322
|
+
class Put(Command): # \put(1,2){x}
|
|
323
|
+
cs = "put"
|
|
324
|
+
at: tuple[int, int] = arg(PAREN) # a tuple, written `1,2` (`sep=` changes the comma)
|
|
325
|
+
text: Raw
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
- **Slots.** `{}` `[]` `()` `<>` or your own `Delimiter`; a `Star` or other token; a slot left out is not written, and an
|
|
329
|
+
optional slot left out before a later optional one writes `fill=` (say `""` for empty brackets) or is refused
|
|
330
|
+
(`Gap.REFUSE`); declaring two optionals without saying which is `TM-M037`. A `SlotGroup` is written together or not at all.
|
|
331
|
+
Optional and token fields are keyword-only in Python wherever they stand. Write an optional field with `arg(default=...)`
|
|
332
|
+
(`label: Text | None = arg(default=None)`): a type checker then reads it as keyword-only and accepts a required field after
|
|
333
|
+
it, where a plain `= None` is reported as a field-order error although it runs.
|
|
334
|
+
- **Value types.** `Text` (escaped), `PdfString`, `Raw`, `Length` (`Length("2mm")`, or `Length.expr(r"\linewidth")` for a TeX
|
|
335
|
+
expression), a colour model (`CMYK`, `RGB`, `Gray`) or a `ColourName` (`blue!50`), `int`, `float`, `bool` (`true`/`false`),
|
|
336
|
+
a `Choice`, a tuple, `NamedTuple`, dataclass or model of those, a `list`, and a `KeyVals`. A wrong value is a violation
|
|
337
|
+
with the field's path.
|
|
338
|
+
- **Definitions.** A signature `\newcommand` can define keeps it. Several optional arguments, other delimiters or a token
|
|
339
|
+
are defined with `\NewDocumentCommand` / `\NewDocumentEnvironment` (xparse, in the LaTeX kernel; the arguments are
|
|
340
|
+
`#1`... in TeX order, an optional one left out is empty and a token is `\BooleanTrue` / `\BooleanFalse`). Stubs
|
|
341
|
+
and `check_interface` follow: `check_interface` reads `\NewDocumentCommand` family definitions as xparse letters.
|
|
342
|
+
|
|
343
|
+
A key list (`\adjustbox{max width=\linewidth, center}{...}`) is a `KeyVals`, a frozen pydantic model: its field types say
|
|
344
|
+
how each item is written, and its `KeyRules` say how the library reads it.
|
|
345
|
+
|
|
346
|
+
```python
|
|
347
|
+
from texmodel.interface import Flag, Item, OptionalValue, Switch
|
|
348
|
+
from texmodel.keys import PGFKEYS
|
|
349
|
+
from texmodel.keyvals import KeyVals, key
|
|
350
|
+
|
|
351
|
+
class AdjustOptions(KeyVals, rules=PGFKEYS):
|
|
352
|
+
max_width: Length | None = key("max width") # max width=3cm
|
|
353
|
+
center: Flag = False # center
|
|
354
|
+
clip: Switch | None = None # clip=true / clip=false
|
|
355
|
+
valign: VAlign | None = None # valign=t (a Choice)
|
|
356
|
+
rounded: OptionalValue[Length] | None = key("rounded corners") # rounded corners / rounded corners=2pt
|
|
357
|
+
style: Inner | None = key("every node/.style") # every node/.style={draw=red} (another KeyVals)
|
|
358
|
+
tags: list[Text] | None = None # tags=a,tags=b (a list repeats the key)
|
|
359
|
+
extra: Item[Raw] | None = None # anything else, as written (an Item has no key)
|
|
360
|
+
|
|
361
|
+
AdjustBox(AdjustOptions(max_width=Length.expr(r"\linewidth"), center=True), Raw("x"))
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Items are written in declaration order and unset ones are left out. A `Text` value the library would split (`,` `=` `]`) is
|
|
365
|
+
put in braces, and `exclusive` / `requires` on the class refuse combinations. Presets in `texmodel.keys` (`PGFKEYS`,
|
|
366
|
+
`XKEYVAL`, `KVOPTIONS`, `L3KEYS`, `KEYVAL`) hold what a library does; `dataclasses.replace(PGFKEYS, booleans=("on", "off"))`
|
|
367
|
+
makes another. They were measured, not assumed: `tests/test_texmodel/test_interface_tex.py` passes awkward values through
|
|
368
|
+
pgfkeys, xkeyval, keyval and l3keys. All four read `,` `=` `]` safely when braced and strip a space at either end of a
|
|
369
|
+
value, which `{{ a }}` keeps in three of them (`l3keys` trims whatever it is given, `KeyRules.edge_spaces`). `KVOPTIONS` is `XKEYVAL` without nesting and was not measured.
|
|
370
|
+
|
|
257
371
|
### Standard libraries
|
|
258
372
|
|
|
259
373
|
`texmodel.std` has a header for each TeX library a deck loads: `latex` (the kernel), `graphicx`, `xcolor`, `geometry`,
|
|
@@ -265,16 +379,17 @@ class IncludeGraphics(Command):
|
|
|
265
379
|
r"""Place a picture: `\includegraphics[width=3cm]{a.png}`."""
|
|
266
380
|
cs = "includegraphics" # the TeX name, stated outright; without `cs` it is prefix + class name
|
|
267
381
|
path: Raw
|
|
268
|
-
options:
|
|
382
|
+
options: IncludeGraphicsOptions | None = None # a KeyVals: TeX's one optional argument, written first
|
|
269
383
|
```
|
|
270
384
|
|
|
271
|
-
A body that calls one (`IncludeGraphics.call(Hole("path"), options=
|
|
385
|
+
A body that calls one (`IncludeGraphics.call(Hole("path"), options=IncludeGraphicsOptions(width=Length.expr(r"\linewidth")))`) makes the generated `.sty`
|
|
272
386
|
load the package; the kernel header (`builtin=True`) never does. A library whose commands no body calls, or that takes
|
|
273
387
|
options, is loaded with `pkg.require(microtype, options=["protrusion=true"])`: `\RequirePackage[protrusion=true]{microtype}`,
|
|
274
388
|
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.
|
|
276
|
-
|
|
277
|
-
|
|
389
|
+
reference image, so a header that names a command TeX does not have fails, with the path of the call. `adjustbox`, `graphicx`,
|
|
390
|
+
`geometry`, `tcolorbox` and `tikz` type their key lists, `calc` is a typed expression language (`dimen(Length("2cm")) * real(0.5) + widthof("abc")`, with `maxof`, `minof`, `ratio`, `glue`, `integer`, `counter_value`), and the kernel types `\vspace*`, `\makebox`, `\raisebox` and
|
|
391
|
+
the sectioning commands; a library's own syntax (TikZ paths such as `\draw (0,0) -- (1,1);`) is written as `Passthrough`
|
|
392
|
+
or `Raw`.
|
|
278
393
|
|
|
279
394
|
### A package that is already written in TeX
|
|
280
395
|
|
|
@@ -328,7 +443,7 @@ pkg.document(card, settings=[DeckName("R&D")]) # \renewcommand{\acDeckName}{R
|
|
|
328
443
|
```
|
|
329
444
|
|
|
330
445
|
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.
|
|
446
|
+
package to define the macro with no arguments. Every setting, of any type, is written right after `\usepackage`, in the order given, before `after_package`.
|
|
332
447
|
|
|
333
448
|
A `Value[Raw]` is the same for a setting that is TeX (`\newcommand{\acFooter}{\textit{draft}}`, renewed by `settings=[Footer(Raw(...))]`).
|
|
334
449
|
A `PdfString` field holds text for the inside of a PDF literal string you write yourself (`\pdfextension info{/Subject (#text)}`):
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
Declare a LaTeX document family once in Python types; get plain, readable, git-trackable `.sty` source out of it. The output
|
|
7
7
|
is ordinary TeX: commit it, diff it, compile it with whatever LaTeX setup you already have.
|
|
8
8
|
|
|
9
|
-
> **Status:
|
|
10
|
-
> and sandbox layers are planned, not built (see [the plan](docs/texmodel-plan.md)).
|
|
9
|
+
> **Status: alpha.** The public API may change before 1.0. Release migrations, `texmodel-print` and the trust
|
|
10
|
+
> and sandbox layers are planned, not built (see [the plan](docs/texmodel-plan.md)). What is missing today:
|
|
11
|
+
> [known limitations](docs/known-limitations.md).
|
|
11
12
|
|
|
12
13
|
```zsh
|
|
13
14
|
pip install texmodel # the library and the texmodel-build command
|
|
@@ -23,6 +24,8 @@ One distribution, two packages:
|
|
|
23
24
|
|
|
24
25
|
## texmodel
|
|
25
26
|
|
|
27
|
+
Worked examples and practices: [the cookbook](docs/cookbook.md).
|
|
28
|
+
|
|
26
29
|
```python
|
|
27
30
|
# acme_cards/__init__.py
|
|
28
31
|
from texmodel.body import TemplateRef
|
|
@@ -31,6 +34,7 @@ from texmodel.package import Package
|
|
|
31
34
|
from texmodel.types import CMYK, Colour, Length, Text
|
|
32
35
|
|
|
33
36
|
pkg = Package("acme-cards", prefix="ac", version="1.4.0", date="2026/10/21")
|
|
37
|
+
# `date=None` leaves the date out of \ProvidesPackage; `header="..."` adds comment lines under the generator's line
|
|
34
38
|
|
|
35
39
|
@pkg.value
|
|
36
40
|
class FactGap(Value[Length]):
|
|
@@ -145,6 +149,15 @@ A pointer (`package =`, and the argument of `texmodel render`) is `dotted.module
|
|
|
145
149
|
package = "acme_cards.decks:Decks.birds" # or "acme_cards.decks:make_package()"
|
|
146
150
|
```
|
|
147
151
|
|
|
152
|
+
A family of packages (a core, its bases, one per series) is a list of pointers, or one pointer to an iterable of
|
|
153
|
+
`Package`s; `build` and `check` cover them all together (a package name must be unique, as it is a file):
|
|
154
|
+
|
|
155
|
+
```toml
|
|
156
|
+
package = ["acme_cards.core:pkg", "acme_cards.birds:pkg"] # or "acme_cards.family:all_packages()"
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`texmodel schema` describes one package's data, so it needs a single `package`.
|
|
160
|
+
|
|
148
161
|
```zsh
|
|
149
162
|
texmodel build # writes generated/acme-cards.sty
|
|
150
163
|
texmodel render docs:card # writes card.tex from `card = pkg.document(...)` (or a function returning one); -o to choose
|
|
@@ -173,6 +186,22 @@ texmodel check # exit 1 if a committed generated file is stale, missing or o
|
|
|
173
186
|
is not a field, an argument the body never uses, unbalanced braces, a name collision.
|
|
174
187
|
- **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`.
|
|
175
188
|
|
|
189
|
+
### Shared bases
|
|
190
|
+
|
|
191
|
+
Components that repeat a body or a grammar share a base declared `abstract=True`. It carries fields, `body` (or `begin` and
|
|
192
|
+
`end`) and `children`; it needs no docstring and may have any Python name (`_Image`); it is never registered
|
|
193
|
+
(`TM-M043`), and its subclasses inherit all of it:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
class _Image(Command, abstract=True):
|
|
197
|
+
src: Text
|
|
198
|
+
def body(self): return r"\includegraphics{#src}"
|
|
199
|
+
|
|
200
|
+
@pkg.command
|
|
201
|
+
class Photo(_Image):
|
|
202
|
+
'''A photo.'''
|
|
203
|
+
```
|
|
204
|
+
|
|
176
205
|
### Generating a body
|
|
177
206
|
|
|
178
207
|
A body can also be built from pieces that are well-formed by construction, so a generator cannot write an unbalanced brace or
|
|
@@ -223,8 +252,14 @@ class FitSize(Command):
|
|
|
223
252
|
available: Raw
|
|
224
253
|
```
|
|
225
254
|
|
|
226
|
-
A component with `defined_by` has no body (`TM-M032`) and its block belongs to the same package (`TM-M033`).
|
|
227
|
-
|
|
255
|
+
A component with `defined_by` has no body (`TM-M032`) and its block belongs to the same package (`TM-M033`). `texmodel check`
|
|
256
|
+
reads each block (`Package.check_parts()`, no TeX needed) and reports a declared component the block does not define with that
|
|
257
|
+
signature (`TM-C010`, `TM-C011`), a public `<prefix>Name` the block defines that nothing declares (`TM-C012`), and a name
|
|
258
|
+
defined twice (`TM-C013`; generation itself refuses it, `TM-P012`). The reading is regex-level (`\newcommand`, `\providecommand`,
|
|
259
|
+
`\DeclareRobustCommand`, `\newenvironment`, the xparse family, `\newlength`, `\newtoggle`, `\newif`, `\definecolor`) and
|
|
260
|
+
skips an expl3 block. A flag, toggle, length, colour or text setting a block defines is a `Value` with `defined_by` (no default;
|
|
261
|
+
`cs = "..."` gives the whole control sequence, `colour_name` the colour's name). For the
|
|
262
|
+
rest, whether TeX really defines it, with that many arguments, is checked by compiling: `texmodel_build.testkit.check_library(compiler, pkg)`
|
|
228
263
|
builds `pkg.test_document(...)` with `export_probes(pkg)` (it writes `texmodel:probe:def.<cs>=yes|no` and
|
|
229
264
|
`arity.<cs>=<n>` for each such component), compiles it, and returns `TM-C010` for a name the block does not define and
|
|
230
265
|
`TM-C011` for another number of arguments (`?` is checked for existence only). The arity is the number of `#` in the macro's
|
|
@@ -235,6 +270,85 @@ The PDF Info strings need no expl3 in a document: `PdfInfo(Title="...", Author="
|
|
|
235
270
|
writes each value PDF-escaped through `\csname pdfmanagement_add:nnn\endcsname`, and needs `RequireMetadata` in `before_class`
|
|
236
271
|
(`TM-C009`). A key is letters only (`TM-M035`).
|
|
237
272
|
|
|
273
|
+
### Parts filled from a model
|
|
274
|
+
|
|
275
|
+
A package that starts from TeX you already have is a list of parts: `pkg.library(name, TemplateRef("brand.tex"), fill=model)`.
|
|
276
|
+
`fill` is a mapping, a dataclass or a pydantic model, and the text's `<<name>>` holes (`<<a.b>>` into a nested model) are
|
|
277
|
+
filled by type: `str` is escaped, `Raw` passes through, a `Length`, a colour model, a number or a flag is written as it is.
|
|
278
|
+
A hole the model has no field for is an error (`TM-M041`), as is a value of another type (`TM-M040`); a model may hold more
|
|
279
|
+
than the part uses. Without `fill` a block is carried exactly as it is. Components with `defined_by` then give decks typed
|
|
280
|
+
calls to what the parts define. [The cookbook](docs/cookbook.md) has a worked example.
|
|
281
|
+
|
|
282
|
+
### Arguments: slots, value types and key lists
|
|
283
|
+
|
|
284
|
+
A field is an argument; its type says what it holds and `arg(...)` says how it is written (`texmodel.interface`).
|
|
285
|
+
Without `arg()`, a required field is `{...}` and an optional one (a default) is TeX's one optional argument, `[...]`,
|
|
286
|
+
written first. With `arg()` the slot keeps its declaration order, so any signature can be stated:
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
from texmodel.interface import PAREN, SQUARE, Gap, SlotGroup, Star, arg
|
|
290
|
+
|
|
291
|
+
class MakeBox(Command): # \makebox[3cm][c]{x}
|
|
292
|
+
cs = "makebox"
|
|
293
|
+
width: Length | None = arg(SQUARE, default=None, fill=Gap.REFUSE) # `[c]` alone is not valid: refused
|
|
294
|
+
align: BoxAlign | None = arg(SQUARE, default=None)
|
|
295
|
+
text: Raw
|
|
296
|
+
|
|
297
|
+
class Section(Command): # \section*[short]{long}
|
|
298
|
+
cs = "section"
|
|
299
|
+
starred: Star = arg(default=False) # a token: `*`
|
|
300
|
+
short: Text | None = arg(SQUARE, default=None)
|
|
301
|
+
title: Text
|
|
302
|
+
|
|
303
|
+
class Put(Command): # \put(1,2){x}
|
|
304
|
+
cs = "put"
|
|
305
|
+
at: tuple[int, int] = arg(PAREN) # a tuple, written `1,2` (`sep=` changes the comma)
|
|
306
|
+
text: Raw
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
- **Slots.** `{}` `[]` `()` `<>` or your own `Delimiter`; a `Star` or other token; a slot left out is not written, and an
|
|
310
|
+
optional slot left out before a later optional one writes `fill=` (say `""` for empty brackets) or is refused
|
|
311
|
+
(`Gap.REFUSE`); declaring two optionals without saying which is `TM-M037`. A `SlotGroup` is written together or not at all.
|
|
312
|
+
Optional and token fields are keyword-only in Python wherever they stand. Write an optional field with `arg(default=...)`
|
|
313
|
+
(`label: Text | None = arg(default=None)`): a type checker then reads it as keyword-only and accepts a required field after
|
|
314
|
+
it, where a plain `= None` is reported as a field-order error although it runs.
|
|
315
|
+
- **Value types.** `Text` (escaped), `PdfString`, `Raw`, `Length` (`Length("2mm")`, or `Length.expr(r"\linewidth")` for a TeX
|
|
316
|
+
expression), a colour model (`CMYK`, `RGB`, `Gray`) or a `ColourName` (`blue!50`), `int`, `float`, `bool` (`true`/`false`),
|
|
317
|
+
a `Choice`, a tuple, `NamedTuple`, dataclass or model of those, a `list`, and a `KeyVals`. A wrong value is a violation
|
|
318
|
+
with the field's path.
|
|
319
|
+
- **Definitions.** A signature `\newcommand` can define keeps it. Several optional arguments, other delimiters or a token
|
|
320
|
+
are defined with `\NewDocumentCommand` / `\NewDocumentEnvironment` (xparse, in the LaTeX kernel; the arguments are
|
|
321
|
+
`#1`... in TeX order, an optional one left out is empty and a token is `\BooleanTrue` / `\BooleanFalse`). Stubs
|
|
322
|
+
and `check_interface` follow: `check_interface` reads `\NewDocumentCommand` family definitions as xparse letters.
|
|
323
|
+
|
|
324
|
+
A key list (`\adjustbox{max width=\linewidth, center}{...}`) is a `KeyVals`, a frozen pydantic model: its field types say
|
|
325
|
+
how each item is written, and its `KeyRules` say how the library reads it.
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
from texmodel.interface import Flag, Item, OptionalValue, Switch
|
|
329
|
+
from texmodel.keys import PGFKEYS
|
|
330
|
+
from texmodel.keyvals import KeyVals, key
|
|
331
|
+
|
|
332
|
+
class AdjustOptions(KeyVals, rules=PGFKEYS):
|
|
333
|
+
max_width: Length | None = key("max width") # max width=3cm
|
|
334
|
+
center: Flag = False # center
|
|
335
|
+
clip: Switch | None = None # clip=true / clip=false
|
|
336
|
+
valign: VAlign | None = None # valign=t (a Choice)
|
|
337
|
+
rounded: OptionalValue[Length] | None = key("rounded corners") # rounded corners / rounded corners=2pt
|
|
338
|
+
style: Inner | None = key("every node/.style") # every node/.style={draw=red} (another KeyVals)
|
|
339
|
+
tags: list[Text] | None = None # tags=a,tags=b (a list repeats the key)
|
|
340
|
+
extra: Item[Raw] | None = None # anything else, as written (an Item has no key)
|
|
341
|
+
|
|
342
|
+
AdjustBox(AdjustOptions(max_width=Length.expr(r"\linewidth"), center=True), Raw("x"))
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Items are written in declaration order and unset ones are left out. A `Text` value the library would split (`,` `=` `]`) is
|
|
346
|
+
put in braces, and `exclusive` / `requires` on the class refuse combinations. Presets in `texmodel.keys` (`PGFKEYS`,
|
|
347
|
+
`XKEYVAL`, `KVOPTIONS`, `L3KEYS`, `KEYVAL`) hold what a library does; `dataclasses.replace(PGFKEYS, booleans=("on", "off"))`
|
|
348
|
+
makes another. They were measured, not assumed: `tests/test_texmodel/test_interface_tex.py` passes awkward values through
|
|
349
|
+
pgfkeys, xkeyval, keyval and l3keys. All four read `,` `=` `]` safely when braced and strip a space at either end of a
|
|
350
|
+
value, which `{{ a }}` keeps in three of them (`l3keys` trims whatever it is given, `KeyRules.edge_spaces`). `KVOPTIONS` is `XKEYVAL` without nesting and was not measured.
|
|
351
|
+
|
|
238
352
|
### Standard libraries
|
|
239
353
|
|
|
240
354
|
`texmodel.std` has a header for each TeX library a deck loads: `latex` (the kernel), `graphicx`, `xcolor`, `geometry`,
|
|
@@ -246,16 +360,17 @@ class IncludeGraphics(Command):
|
|
|
246
360
|
r"""Place a picture: `\includegraphics[width=3cm]{a.png}`."""
|
|
247
361
|
cs = "includegraphics" # the TeX name, stated outright; without `cs` it is prefix + class name
|
|
248
362
|
path: Raw
|
|
249
|
-
options:
|
|
363
|
+
options: IncludeGraphicsOptions | None = None # a KeyVals: TeX's one optional argument, written first
|
|
250
364
|
```
|
|
251
365
|
|
|
252
|
-
A body that calls one (`IncludeGraphics.call(Hole("path"), options=
|
|
366
|
+
A body that calls one (`IncludeGraphics.call(Hole("path"), options=IncludeGraphicsOptions(width=Length.expr(r"\linewidth")))`) makes the generated `.sty`
|
|
253
367
|
load the package; the kernel header (`builtin=True`) never does. A library whose commands no body calls, or that takes
|
|
254
368
|
options, is loaded with `pkg.require(microtype, options=["protrusion=true"])`: `\RequirePackage[protrusion=true]{microtype}`,
|
|
255
369
|
in that order, before the packages the bodies need. `tests/test_std` compiles a call of every declared name against the
|
|
256
|
-
reference image, so a header that names a command TeX does not have fails, with the path of the call.
|
|
257
|
-
|
|
258
|
-
|
|
370
|
+
reference image, so a header that names a command TeX does not have fails, with the path of the call. `adjustbox`, `graphicx`,
|
|
371
|
+
`geometry`, `tcolorbox` and `tikz` type their key lists, `calc` is a typed expression language (`dimen(Length("2cm")) * real(0.5) + widthof("abc")`, with `maxof`, `minof`, `ratio`, `glue`, `integer`, `counter_value`), and the kernel types `\vspace*`, `\makebox`, `\raisebox` and
|
|
372
|
+
the sectioning commands; a library's own syntax (TikZ paths such as `\draw (0,0) -- (1,1);`) is written as `Passthrough`
|
|
373
|
+
or `Raw`.
|
|
259
374
|
|
|
260
375
|
### A package that is already written in TeX
|
|
261
376
|
|
|
@@ -309,7 +424,7 @@ pkg.document(card, settings=[DeckName("R&D")]) # \renewcommand{\acDeckName}{R
|
|
|
309
424
|
```
|
|
310
425
|
|
|
311
426
|
A generated package defines `\newcommand{\acDeckName}{Untitled}`; for an adopted one `texmodel check` expects the hand-written
|
|
312
|
-
package to define the macro with no arguments.
|
|
427
|
+
package to define the macro with no arguments. Every setting, of any type, is written right after `\usepackage`, in the order given, before `after_package`.
|
|
313
428
|
|
|
314
429
|
A `Value[Raw]` is the same for a setting that is TeX (`\newcommand{\acFooter}{\textit{draft}}`, renewed by `settings=[Footer(Raw(...))]`).
|
|
315
430
|
A `PdfString` field holds text for the inside of a PDF literal string you write yourself (`\pdfextension info{/Subject (#text)}`):
|