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.
Files changed (186) hide show
  1. {texmodel-0.2.0 → texmodel-0.3.1}/PKG-INFO +126 -11
  2. {texmodel-0.2.0 → texmodel-0.3.1}/README.md +125 -10
  3. {texmodel-0.2.0 → texmodel-0.3.1}/pyproject.toml +1 -1
  4. texmodel-0.3.1/src/texmodel/_internal/render.py +519 -0
  5. texmodel-0.3.1/src/texmodel/_internal/scan.py +106 -0
  6. texmodel-0.3.1/src/texmodel/_internal/values.py +174 -0
  7. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/body.py +2 -0
  8. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/cli.py +31 -12
  9. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/components.py +253 -111
  10. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/config.py +3 -1
  11. texmodel-0.3.1/src/texmodel/interface.py +293 -0
  12. texmodel-0.3.1/src/texmodel/keys.py +36 -0
  13. texmodel-0.3.1/src/texmodel/keyvals.py +231 -0
  14. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/package.py +171 -34
  15. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/pointer.py +31 -1
  16. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/spec.py +19 -12
  17. texmodel-0.3.1/src/texmodel/std/adjustbox.py +56 -0
  18. texmodel-0.3.1/src/texmodel/std/calc.py +387 -0
  19. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/std/etoolbox.py +57 -1
  20. texmodel-0.3.1/src/texmodel/std/geometry.py +49 -0
  21. texmodel-0.3.1/src/texmodel/std/graphicx.py +45 -0
  22. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/std/latex.py +85 -9
  23. texmodel-0.3.1/src/texmodel/std/tcolorbox.py +68 -0
  24. texmodel-0.3.1/src/texmodel/std/tikz.py +61 -0
  25. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/testing.py +20 -8
  26. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/types.py +108 -4
  27. texmodel-0.3.1/tests/test_docs.py +22 -0
  28. texmodel-0.3.1/tests/test_std/test_calc.py +313 -0
  29. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_std/test_headers.py +86 -17
  30. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_adopted.py +2 -2
  31. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_build_check.py +101 -1
  32. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_document.py +3 -3
  33. texmodel-0.3.1/tests/test_texmodel/test_interface.py +546 -0
  34. texmodel-0.3.1/tests/test_texmodel/test_interface_tex.py +198 -0
  35. texmodel-0.3.1/tests/test_texmodel/test_keyvals.py +342 -0
  36. texmodel-0.3.1/tests/test_texmodel/test_parts.py +403 -0
  37. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_public_api.py +3 -0
  38. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_render.py +3 -3
  39. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_text_values.py +8 -6
  40. texmodel-0.3.1/tests/test_texmodel/test_value_kinds_and_meta.py +204 -0
  41. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_values.py +1 -1
  42. texmodel-0.2.0/src/texmodel/_internal/render.py +0 -323
  43. texmodel-0.2.0/src/texmodel/_internal/scan.py +0 -23
  44. texmodel-0.2.0/src/texmodel/std/adjustbox.py +0 -19
  45. texmodel-0.2.0/src/texmodel/std/calc.py +0 -7
  46. texmodel-0.2.0/src/texmodel/std/geometry.py +0 -17
  47. texmodel-0.2.0/src/texmodel/std/graphicx.py +0 -26
  48. texmodel-0.2.0/src/texmodel/std/tcolorbox.py +0 -43
  49. texmodel-0.2.0/src/texmodel/std/tikz.py +0 -36
  50. {texmodel-0.2.0 → texmodel-0.3.1}/LICENSE +0 -0
  51. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/__init__.py +0 -0
  52. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/_internal/__init__.py +0 -0
  53. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/bundle.py +0 -0
  54. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/drift.py +0 -0
  55. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/errors.py +0 -0
  56. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/plan.py +0 -0
  57. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/preamble.py +0 -0
  58. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/py.typed +0 -0
  59. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/std/__init__.py +0 -0
  60. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/std/anyfontsize.py +0 -0
  61. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/std/enumitem.py +0 -0
  62. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/std/eso_pic.py +0 -0
  63. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/std/fontspec.py +0 -0
  64. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/std/hyphenat.py +0 -0
  65. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/std/microtype.py +0 -0
  66. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/std/pdfrender.py +0 -0
  67. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel/std/xcolor.py +0 -0
  68. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/__init__.py +0 -0
  69. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/_internal/__init__.py +0 -0
  70. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/_internal/converge.py +0 -0
  71. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/_internal/env.py +0 -0
  72. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/_internal/logparse.py +0 -0
  73. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/_internal/proofpdf.py +0 -0
  74. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/_internal/spawn.py +0 -0
  75. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/_internal/styscan.py +0 -0
  76. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/cli.py +0 -0
  77. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/compiler.py +0 -0
  78. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/config.py +0 -0
  79. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/diagnostics.py +0 -0
  80. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/engines.py +0 -0
  81. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/errors.py +0 -0
  82. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/py.typed +0 -0
  83. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/result.py +0 -0
  84. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/roots.py +0 -0
  85. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/targets.py +0 -0
  86. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/testkit.py +0 -0
  87. {texmodel-0.2.0 → texmodel-0.3.1}/src/texmodel_build/verify.py +0 -0
  88. {texmodel-0.2.0 → texmodel-0.3.1}/tests/consumers/card_deck/__init__.py +0 -0
  89. {texmodel-0.2.0 → texmodel-0.3.1}/tests/consumers/card_deck/cards.toml +0 -0
  90. {texmodel-0.2.0 → texmodel-0.3.1}/tests/consumers/card_deck/convert.py +0 -0
  91. {texmodel-0.2.0 → texmodel-0.3.1}/tests/consumers/card_deck/fit.py +0 -0
  92. {texmodel-0.2.0 → texmodel-0.3.1}/tests/consumers/card_deck/legacy.py +0 -0
  93. {texmodel-0.2.0 → texmodel-0.3.1}/tests/consumers/card_deck/model.py +0 -0
  94. {texmodel-0.2.0 → texmodel-0.3.1}/tests/consumers/card_deck/old/card-deck.sty +0 -0
  95. {texmodel-0.2.0 → texmodel-0.3.1}/tests/consumers/card_deck/project.toml +0 -0
  96. {texmodel-0.2.0 → texmodel-0.3.1}/tests/consumers/card_deck/res/card-deck.sty +0 -0
  97. {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/README.md +0 -0
  98. {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/business_card/__init__.py +0 -0
  99. {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/business_card/card.begin.tex +0 -0
  100. {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/business_card/card.end.tex +0 -0
  101. {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/business_card/contact.tex +0 -0
  102. {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/business_card/expected/ada.tex +0 -0
  103. {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/business_card/expected/business-card.sty +0 -0
  104. {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/business_card/expected/compact.tex +1 -1
  105. {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/business_card/name.tex +0 -0
  106. {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/composed/__init__.py +0 -0
  107. {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/composed/expected/base-kit.sty +0 -0
  108. {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/composed/expected/composed-kit.sty +0 -0
  109. {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/composed/expected/composed.tex +0 -0
  110. {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/expl3/__init__.py +0 -0
  111. {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/expl3/expected/expl3-kit.sty +0 -0
  112. {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/expl3/expected/expl3.tex +0 -0
  113. {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/hello/__init__.py +0 -0
  114. {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/hello/expected/hello.sty +0 -0
  115. {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/hello/expected/hello.tex +0 -0
  116. {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/letter/__init__.py +0 -0
  117. {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/letter/expected/invitation.tex +1 -1
  118. {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/letter/expected/letter-kit.sty +0 -0
  119. {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/letter/expected/terse.tex +0 -0
  120. {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/letter/letter.begin.tex +0 -0
  121. {texmodel-0.2.0 → texmodel-0.3.1}/tests/examples/letter/letter.end.tex +0 -0
  122. {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/__init__.py +0 -0
  123. {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/card_back.begin.tex +0 -0
  124. {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/card_back.end.tex +0 -0
  125. {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/fact.tex +0 -0
  126. {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/generated/acme-cards.sty +0 -0
  127. {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/name.tex +0 -0
  128. {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/acme_cards/slot.tex +0 -0
  129. {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/logs/diagnostics.latexmk.txt +0 -0
  130. {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/logs/diagnostics.log +0 -0
  131. {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/logs/miss.log +0 -0
  132. {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/logs/ok.log +0 -0
  133. {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/logs/overset.log +0 -0
  134. {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/logs/pkg.log +0 -0
  135. {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/logs/sty.log +0 -0
  136. {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/logs/undefined_in_argument.log +0 -0
  137. {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/tex/bad.tex +0 -0
  138. {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/tex/bad_font.tex +0 -0
  139. {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/tex/missing_file.tex +0 -0
  140. {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/tex/missing_package.tex +0 -0
  141. {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/tex/ok.tex +0 -0
  142. {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/tex/shell_escape.tex +0 -0
  143. {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/tex/unstable.tex +0 -0
  144. {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/tex/uses_acme.tex +0 -0
  145. {texmodel-0.2.0 → texmodel-0.3.1}/tests/fixtures/tex/uses_values.tex +0 -0
  146. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_consumers/__init__.py +0 -0
  147. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_consumers/test_card_deck.py +0 -0
  148. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_consumers/test_card_deck_fit.py +0 -0
  149. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_consumers/test_card_deck_tex.py +0 -0
  150. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_std/__init__.py +0 -0
  151. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/__init__.py +0 -0
  152. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/block.tex +0 -0
  153. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/conftest.py +0 -0
  154. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_body.py +0 -0
  155. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_bundle_drift.py +0 -0
  156. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_check_documents.py +0 -0
  157. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_choice.py +0 -0
  158. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_comment.py +0 -0
  159. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_cs.py +0 -0
  160. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_emitter.py +0 -0
  161. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_errors_config_cli.py +0 -0
  162. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_examples.py +0 -0
  163. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_expl3.py +0 -0
  164. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_expl3_tex.py +0 -0
  165. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_grammar_hatches.py +0 -0
  166. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_import.py +0 -0
  167. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_pdf_string.py +0 -0
  168. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_plan.py +0 -0
  169. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_preamble.py +0 -0
  170. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_refs.py +0 -0
  171. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_schema.py +0 -0
  172. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_spec.py +0 -0
  173. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_stubs.py +0 -0
  174. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel/test_types.py +0 -0
  175. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel_build/__init__.py +0 -0
  176. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel_build/conftest.py +0 -0
  177. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_cli.py +0 -0
  178. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_compile_tex.py +0 -0
  179. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_compiler.py +0 -0
  180. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_config_doctor.py +0 -0
  181. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_diagnostics_logparse.py +0 -0
  182. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_doctor_sty.py +0 -0
  183. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_proofpdf.py +0 -0
  184. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_public_api.py +0 -0
  185. {texmodel-0.2.0 → texmodel-0.3.1}/tests/test_texmodel_build/test_support.py +0 -0
  186. {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.2.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: 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)).
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`). Whether TeX
246
- really defines it, with that many arguments, is checked by compiling: `texmodel_build.testkit.check_library(compiler, pkg)`
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: Raw | None = None # TeX's one optional argument, written first
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=Raw(r"width=\linewidth"))`) makes the generated `.sty`
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. 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`.
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. Length, colour and flag settings are written in the document body as before.
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: 0.1.0, 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)).
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`). Whether TeX
227
- really defines it, with that many arguments, is checked by compiling: `texmodel_build.testkit.check_library(compiler, pkg)`
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: Raw | None = None # TeX's one optional argument, written first
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=Raw(r"width=\linewidth"))`) makes the generated `.sty`
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. A header covers
257
- required `{}` arguments and one leading `[]`; a library's own syntax (TikZ paths such as `\draw (0,0) -- (1,1);`, key-value
258
- declarations) is written as `Passthrough` or `Raw`.
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. Length, colour and flag settings are written in the document body as before.
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)}`):
@@ -20,7 +20,7 @@ dependencies = [
20
20
  "typer>=0.27.3",
21
21
  "pydantic>=2.14.1",
22
22
  ]
23
- version = "0.2.0"
23
+ version = "0.3.1"
24
24
 
25
25
  [project.optional-dependencies]
26
26
  proof-pdf = [