texmodel 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. {texmodel-0.2.0 → texmodel-0.3.0}/PKG-INFO +108 -11
  2. {texmodel-0.2.0 → texmodel-0.3.0}/README.md +107 -10
  3. {texmodel-0.2.0 → texmodel-0.3.0}/pyproject.toml +1 -1
  4. texmodel-0.3.0/src/texmodel/_internal/render.py +519 -0
  5. texmodel-0.3.0/src/texmodel/_internal/scan.py +106 -0
  6. texmodel-0.3.0/src/texmodel/_internal/values.py +174 -0
  7. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/body.py +2 -0
  8. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/cli.py +31 -12
  9. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/components.py +213 -95
  10. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/config.py +3 -1
  11. texmodel-0.3.0/src/texmodel/interface.py +293 -0
  12. texmodel-0.3.0/src/texmodel/keys.py +36 -0
  13. texmodel-0.3.0/src/texmodel/keyvals.py +231 -0
  14. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/package.py +157 -34
  15. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/pointer.py +31 -1
  16. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/spec.py +19 -12
  17. texmodel-0.3.0/src/texmodel/std/adjustbox.py +56 -0
  18. texmodel-0.3.0/src/texmodel/std/calc.py +387 -0
  19. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/std/etoolbox.py +57 -1
  20. texmodel-0.3.0/src/texmodel/std/geometry.py +49 -0
  21. texmodel-0.3.0/src/texmodel/std/graphicx.py +45 -0
  22. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/std/latex.py +85 -9
  23. texmodel-0.3.0/src/texmodel/std/tcolorbox.py +68 -0
  24. texmodel-0.3.0/src/texmodel/std/tikz.py +61 -0
  25. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/testing.py +20 -8
  26. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/types.py +108 -4
  27. texmodel-0.3.0/tests/test_docs.py +22 -0
  28. texmodel-0.3.0/tests/test_std/test_calc.py +313 -0
  29. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_std/test_headers.py +86 -17
  30. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/test_adopted.py +2 -2
  31. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/test_build_check.py +101 -1
  32. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/test_document.py +3 -3
  33. texmodel-0.3.0/tests/test_texmodel/test_interface.py +546 -0
  34. texmodel-0.3.0/tests/test_texmodel/test_interface_tex.py +198 -0
  35. texmodel-0.3.0/tests/test_texmodel/test_keyvals.py +342 -0
  36. texmodel-0.3.0/tests/test_texmodel/test_parts.py +304 -0
  37. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/test_public_api.py +3 -0
  38. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/test_render.py +3 -3
  39. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/test_text_values.py +8 -6
  40. texmodel-0.3.0/tests/test_texmodel/test_value_kinds_and_meta.py +204 -0
  41. {texmodel-0.2.0 → texmodel-0.3.0}/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.0}/LICENSE +0 -0
  51. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/__init__.py +0 -0
  52. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/_internal/__init__.py +0 -0
  53. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/bundle.py +0 -0
  54. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/drift.py +0 -0
  55. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/errors.py +0 -0
  56. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/plan.py +0 -0
  57. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/preamble.py +0 -0
  58. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/py.typed +0 -0
  59. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/std/__init__.py +0 -0
  60. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/std/anyfontsize.py +0 -0
  61. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/std/enumitem.py +0 -0
  62. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/std/eso_pic.py +0 -0
  63. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/std/fontspec.py +0 -0
  64. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/std/hyphenat.py +0 -0
  65. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/std/microtype.py +0 -0
  66. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/std/pdfrender.py +0 -0
  67. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel/std/xcolor.py +0 -0
  68. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel_build/__init__.py +0 -0
  69. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel_build/_internal/__init__.py +0 -0
  70. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel_build/_internal/converge.py +0 -0
  71. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel_build/_internal/env.py +0 -0
  72. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel_build/_internal/logparse.py +0 -0
  73. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel_build/_internal/proofpdf.py +0 -0
  74. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel_build/_internal/spawn.py +0 -0
  75. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel_build/_internal/styscan.py +0 -0
  76. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel_build/cli.py +0 -0
  77. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel_build/compiler.py +0 -0
  78. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel_build/config.py +0 -0
  79. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel_build/diagnostics.py +0 -0
  80. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel_build/engines.py +0 -0
  81. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel_build/errors.py +0 -0
  82. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel_build/py.typed +0 -0
  83. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel_build/result.py +0 -0
  84. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel_build/roots.py +0 -0
  85. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel_build/targets.py +0 -0
  86. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel_build/testkit.py +0 -0
  87. {texmodel-0.2.0 → texmodel-0.3.0}/src/texmodel_build/verify.py +0 -0
  88. {texmodel-0.2.0 → texmodel-0.3.0}/tests/consumers/card_deck/__init__.py +0 -0
  89. {texmodel-0.2.0 → texmodel-0.3.0}/tests/consumers/card_deck/cards.toml +0 -0
  90. {texmodel-0.2.0 → texmodel-0.3.0}/tests/consumers/card_deck/convert.py +0 -0
  91. {texmodel-0.2.0 → texmodel-0.3.0}/tests/consumers/card_deck/fit.py +0 -0
  92. {texmodel-0.2.0 → texmodel-0.3.0}/tests/consumers/card_deck/legacy.py +0 -0
  93. {texmodel-0.2.0 → texmodel-0.3.0}/tests/consumers/card_deck/model.py +0 -0
  94. {texmodel-0.2.0 → texmodel-0.3.0}/tests/consumers/card_deck/old/card-deck.sty +0 -0
  95. {texmodel-0.2.0 → texmodel-0.3.0}/tests/consumers/card_deck/project.toml +0 -0
  96. {texmodel-0.2.0 → texmodel-0.3.0}/tests/consumers/card_deck/res/card-deck.sty +0 -0
  97. {texmodel-0.2.0 → texmodel-0.3.0}/tests/examples/README.md +0 -0
  98. {texmodel-0.2.0 → texmodel-0.3.0}/tests/examples/business_card/__init__.py +0 -0
  99. {texmodel-0.2.0 → texmodel-0.3.0}/tests/examples/business_card/card.begin.tex +0 -0
  100. {texmodel-0.2.0 → texmodel-0.3.0}/tests/examples/business_card/card.end.tex +0 -0
  101. {texmodel-0.2.0 → texmodel-0.3.0}/tests/examples/business_card/contact.tex +0 -0
  102. {texmodel-0.2.0 → texmodel-0.3.0}/tests/examples/business_card/expected/ada.tex +0 -0
  103. {texmodel-0.2.0 → texmodel-0.3.0}/tests/examples/business_card/expected/business-card.sty +0 -0
  104. {texmodel-0.2.0 → texmodel-0.3.0}/tests/examples/business_card/expected/compact.tex +1 -1
  105. {texmodel-0.2.0 → texmodel-0.3.0}/tests/examples/business_card/name.tex +0 -0
  106. {texmodel-0.2.0 → texmodel-0.3.0}/tests/examples/composed/__init__.py +0 -0
  107. {texmodel-0.2.0 → texmodel-0.3.0}/tests/examples/composed/expected/base-kit.sty +0 -0
  108. {texmodel-0.2.0 → texmodel-0.3.0}/tests/examples/composed/expected/composed-kit.sty +0 -0
  109. {texmodel-0.2.0 → texmodel-0.3.0}/tests/examples/composed/expected/composed.tex +0 -0
  110. {texmodel-0.2.0 → texmodel-0.3.0}/tests/examples/expl3/__init__.py +0 -0
  111. {texmodel-0.2.0 → texmodel-0.3.0}/tests/examples/expl3/expected/expl3-kit.sty +0 -0
  112. {texmodel-0.2.0 → texmodel-0.3.0}/tests/examples/expl3/expected/expl3.tex +0 -0
  113. {texmodel-0.2.0 → texmodel-0.3.0}/tests/examples/hello/__init__.py +0 -0
  114. {texmodel-0.2.0 → texmodel-0.3.0}/tests/examples/hello/expected/hello.sty +0 -0
  115. {texmodel-0.2.0 → texmodel-0.3.0}/tests/examples/hello/expected/hello.tex +0 -0
  116. {texmodel-0.2.0 → texmodel-0.3.0}/tests/examples/letter/__init__.py +0 -0
  117. {texmodel-0.2.0 → texmodel-0.3.0}/tests/examples/letter/expected/invitation.tex +1 -1
  118. {texmodel-0.2.0 → texmodel-0.3.0}/tests/examples/letter/expected/letter-kit.sty +0 -0
  119. {texmodel-0.2.0 → texmodel-0.3.0}/tests/examples/letter/expected/terse.tex +0 -0
  120. {texmodel-0.2.0 → texmodel-0.3.0}/tests/examples/letter/letter.begin.tex +0 -0
  121. {texmodel-0.2.0 → texmodel-0.3.0}/tests/examples/letter/letter.end.tex +0 -0
  122. {texmodel-0.2.0 → texmodel-0.3.0}/tests/fixtures/acme_cards/__init__.py +0 -0
  123. {texmodel-0.2.0 → texmodel-0.3.0}/tests/fixtures/acme_cards/card_back.begin.tex +0 -0
  124. {texmodel-0.2.0 → texmodel-0.3.0}/tests/fixtures/acme_cards/card_back.end.tex +0 -0
  125. {texmodel-0.2.0 → texmodel-0.3.0}/tests/fixtures/acme_cards/fact.tex +0 -0
  126. {texmodel-0.2.0 → texmodel-0.3.0}/tests/fixtures/acme_cards/generated/acme-cards.sty +0 -0
  127. {texmodel-0.2.0 → texmodel-0.3.0}/tests/fixtures/acme_cards/name.tex +0 -0
  128. {texmodel-0.2.0 → texmodel-0.3.0}/tests/fixtures/acme_cards/slot.tex +0 -0
  129. {texmodel-0.2.0 → texmodel-0.3.0}/tests/fixtures/logs/diagnostics.latexmk.txt +0 -0
  130. {texmodel-0.2.0 → texmodel-0.3.0}/tests/fixtures/logs/diagnostics.log +0 -0
  131. {texmodel-0.2.0 → texmodel-0.3.0}/tests/fixtures/logs/miss.log +0 -0
  132. {texmodel-0.2.0 → texmodel-0.3.0}/tests/fixtures/logs/ok.log +0 -0
  133. {texmodel-0.2.0 → texmodel-0.3.0}/tests/fixtures/logs/overset.log +0 -0
  134. {texmodel-0.2.0 → texmodel-0.3.0}/tests/fixtures/logs/pkg.log +0 -0
  135. {texmodel-0.2.0 → texmodel-0.3.0}/tests/fixtures/logs/sty.log +0 -0
  136. {texmodel-0.2.0 → texmodel-0.3.0}/tests/fixtures/logs/undefined_in_argument.log +0 -0
  137. {texmodel-0.2.0 → texmodel-0.3.0}/tests/fixtures/tex/bad.tex +0 -0
  138. {texmodel-0.2.0 → texmodel-0.3.0}/tests/fixtures/tex/bad_font.tex +0 -0
  139. {texmodel-0.2.0 → texmodel-0.3.0}/tests/fixtures/tex/missing_file.tex +0 -0
  140. {texmodel-0.2.0 → texmodel-0.3.0}/tests/fixtures/tex/missing_package.tex +0 -0
  141. {texmodel-0.2.0 → texmodel-0.3.0}/tests/fixtures/tex/ok.tex +0 -0
  142. {texmodel-0.2.0 → texmodel-0.3.0}/tests/fixtures/tex/shell_escape.tex +0 -0
  143. {texmodel-0.2.0 → texmodel-0.3.0}/tests/fixtures/tex/unstable.tex +0 -0
  144. {texmodel-0.2.0 → texmodel-0.3.0}/tests/fixtures/tex/uses_acme.tex +0 -0
  145. {texmodel-0.2.0 → texmodel-0.3.0}/tests/fixtures/tex/uses_values.tex +0 -0
  146. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_consumers/__init__.py +0 -0
  147. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_consumers/test_card_deck.py +0 -0
  148. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_consumers/test_card_deck_fit.py +0 -0
  149. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_consumers/test_card_deck_tex.py +0 -0
  150. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_std/__init__.py +0 -0
  151. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/__init__.py +0 -0
  152. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/block.tex +0 -0
  153. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/conftest.py +0 -0
  154. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/test_body.py +0 -0
  155. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/test_bundle_drift.py +0 -0
  156. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/test_check_documents.py +0 -0
  157. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/test_choice.py +0 -0
  158. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/test_comment.py +0 -0
  159. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/test_cs.py +0 -0
  160. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/test_emitter.py +0 -0
  161. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/test_errors_config_cli.py +0 -0
  162. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/test_examples.py +0 -0
  163. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/test_expl3.py +0 -0
  164. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/test_expl3_tex.py +0 -0
  165. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/test_grammar_hatches.py +0 -0
  166. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/test_import.py +0 -0
  167. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/test_pdf_string.py +0 -0
  168. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/test_plan.py +0 -0
  169. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/test_preamble.py +0 -0
  170. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/test_refs.py +0 -0
  171. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/test_schema.py +0 -0
  172. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/test_spec.py +0 -0
  173. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/test_stubs.py +0 -0
  174. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel/test_types.py +0 -0
  175. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel_build/__init__.py +0 -0
  176. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel_build/conftest.py +0 -0
  177. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel_build/test_cli.py +0 -0
  178. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel_build/test_compile_tex.py +0 -0
  179. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel_build/test_compiler.py +0 -0
  180. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel_build/test_config_doctor.py +0 -0
  181. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel_build/test_diagnostics_logparse.py +0 -0
  182. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel_build/test_doctor_sty.py +0 -0
  183. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel_build/test_proofpdf.py +0 -0
  184. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel_build/test_public_api.py +0 -0
  185. {texmodel-0.2.0 → texmodel-0.3.0}/tests/test_texmodel_build/test_support.py +0 -0
  186. {texmodel-0.2.0 → texmodel-0.3.0}/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.0
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
@@ -242,8 +255,14 @@ class FitSize(Command):
242
255
  available: Raw
243
256
  ```
244
257
 
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)`
258
+ A component with `defined_by` has no body (`TM-M032`) and its block belongs to the same package (`TM-M033`). `texmodel check`
259
+ reads each block (`Package.check_parts()`, no TeX needed) and reports a declared component the block does not define with that
260
+ signature (`TM-C010`, `TM-C011`), a public `<prefix>Name` the block defines that nothing declares (`TM-C012`), and a name
261
+ defined twice (`TM-C013`; generation itself refuses it, `TM-P012`). The reading is regex-level (`\newcommand`, `\providecommand`,
262
+ `\DeclareRobustCommand`, `\newenvironment`, the xparse family, `\newlength`, `\newtoggle`, `\newif`, `\definecolor`) and
263
+ skips an expl3 block. A flag, toggle, length, colour or text setting a block defines is a `Value` with `defined_by` (no default;
264
+ `cs = "..."` gives the whole control sequence, `colour_name` the colour's name). For the
265
+ rest, whether TeX really defines it, with that many arguments, is checked by compiling: `texmodel_build.testkit.check_library(compiler, pkg)`
247
266
  builds `pkg.test_document(...)` with `export_probes(pkg)` (it writes `texmodel:probe:def.<cs>=yes|no` and
248
267
  `arity.<cs>=<n>` for each such component), compiles it, and returns `TM-C010` for a name the block does not define and
249
268
  `TM-C011` for another number of arguments (`?` is checked for existence only). The arity is the number of `#` in the macro's
@@ -254,6 +273,83 @@ The PDF Info strings need no expl3 in a document: `PdfInfo(Title="...", Author="
254
273
  writes each value PDF-escaped through `\csname pdfmanagement_add:nnn\endcsname`, and needs `RequireMetadata` in `before_class`
255
274
  (`TM-C009`). A key is letters only (`TM-M035`).
256
275
 
276
+ ### Parts filled from a model
277
+
278
+ A package that starts from TeX you already have is a list of parts: `pkg.library(name, TemplateRef("brand.tex"), fill=model)`.
279
+ `fill` is a mapping, a dataclass or a pydantic model, and the text's `<<name>>` holes (`<<a.b>>` into a nested model) are
280
+ filled by type: `str` is escaped, `Raw` passes through, a `Length`, a colour model, a number or a flag is written as it is.
281
+ 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
282
+ than the part uses. Without `fill` a block is carried exactly as it is. Components with `defined_by` then give decks typed
283
+ calls to what the parts define. [The cookbook](docs/cookbook.md) has a worked example.
284
+
285
+ ### Arguments: slots, value types and key lists
286
+
287
+ A field is an argument; its type says what it holds and `arg(...)` says how it is written (`texmodel.interface`).
288
+ Without `arg()`, a required field is `{...}` and an optional one (a default) is TeX's one optional argument, `[...]`,
289
+ written first. With `arg()` the slot keeps its declaration order, so any signature can be stated:
290
+
291
+ ```python
292
+ from texmodel.interface import PAREN, SQUARE, Gap, SlotGroup, Star, arg
293
+
294
+ class MakeBox(Command): # \makebox[3cm][c]{x}
295
+ cs = "makebox"
296
+ width: Length | None = arg(SQUARE, default=None, fill=Gap.REFUSE) # `[c]` alone is not valid: refused
297
+ align: BoxAlign | None = arg(SQUARE, default=None)
298
+ text: Raw
299
+
300
+ class Section(Command): # \section*[short]{long}
301
+ cs = "section"
302
+ starred: Star = arg(default=False) # a token: `*`
303
+ short: Text | None = arg(SQUARE, default=None)
304
+ title: Text
305
+
306
+ class Put(Command): # \put(1,2){x}
307
+ cs = "put"
308
+ at: tuple[int, int] = arg(PAREN) # a tuple, written `1,2` (`sep=` changes the comma)
309
+ text: Raw
310
+ ```
311
+
312
+ - **Slots.** `{}` `[]` `()` `<>` or your own `Delimiter`; a `Star` or other token; a slot left out is not written, and an
313
+ optional slot left out before a later optional one writes `fill=` (say `""` for empty brackets) or is refused
314
+ (`Gap.REFUSE`); declaring two optionals without saying which is `TM-M037`. A `SlotGroup` is written together or not at all.
315
+ Optional and token fields are keyword-only in Python wherever they stand, so give an optional one `default=None`.
316
+ - **Value types.** `Text` (escaped), `PdfString`, `Raw`, `Length` (`Length("2mm")`, or `Length.expr(r"\linewidth")` for a TeX
317
+ expression), a colour model (`CMYK`, `RGB`, `Gray`) or a `ColourName` (`blue!50`), `int`, `float`, `bool` (`true`/`false`),
318
+ a `Choice`, a tuple, `NamedTuple`, dataclass or model of those, a `list`, and a `KeyVals`. A wrong value is a violation
319
+ with the field's path.
320
+ - **Definitions.** A signature `\newcommand` can define keeps it. Several optional arguments, other delimiters or a token
321
+ are defined with `\NewDocumentCommand` / `\NewDocumentEnvironment` (xparse, in the LaTeX kernel; the arguments are
322
+ `#1`... in TeX order, an optional one left out is empty and a token is `\BooleanTrue` / `\BooleanFalse`). Stubs
323
+ and `check_interface` follow: `check_interface` reads `\NewDocumentCommand` family definitions as xparse letters.
324
+
325
+ A key list (`\adjustbox{max width=\linewidth, center}{...}`) is a `KeyVals`, a frozen pydantic model: its field types say
326
+ how each item is written, and its `KeyRules` say how the library reads it.
327
+
328
+ ```python
329
+ from texmodel.interface import Flag, Item, OptionalValue, Switch
330
+ from texmodel.keys import PGFKEYS
331
+ from texmodel.keyvals import KeyVals, key
332
+
333
+ class AdjustOptions(KeyVals, rules=PGFKEYS):
334
+ max_width: Length | None = key("max width") # max width=3cm
335
+ center: Flag = False # center
336
+ clip: Switch | None = None # clip=true / clip=false
337
+ valign: VAlign | None = None # valign=t (a Choice)
338
+ rounded: OptionalValue[Length] | None = key("rounded corners") # rounded corners / rounded corners=2pt
339
+ style: Inner | None = key("every node/.style") # every node/.style={draw=red} (another KeyVals)
340
+ tags: list[Text] | None = None # tags=a,tags=b (a list repeats the key)
341
+ extra: Item[Raw] | None = None # anything else, as written (an Item has no key)
342
+
343
+ AdjustBox(AdjustOptions(max_width=Length.expr(r"\linewidth"), center=True), Raw("x"))
344
+ ```
345
+
346
+ Items are written in declaration order and unset ones are left out. A `Text` value the library would split (`,` `=` `]`) is
347
+ put in braces, and `exclusive` / `requires` on the class refuse combinations. Presets in `texmodel.keys` (`PGFKEYS`,
348
+ `XKEYVAL`, `KVOPTIONS`, `L3KEYS`, `KEYVAL`) hold what a library does; `dataclasses.replace(PGFKEYS, booleans=("on", "off"))`
349
+ makes another. They were measured, not assumed: `tests/test_texmodel/test_interface_tex.py` passes awkward values through
350
+ pgfkeys, xkeyval, keyval and l3keys. All four read `,` `=` `]` safely when braced and strip a space at either end of a
351
+ 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.
352
+
257
353
  ### Standard libraries
258
354
 
259
355
  `texmodel.std` has a header for each TeX library a deck loads: `latex` (the kernel), `graphicx`, `xcolor`, `geometry`,
@@ -265,16 +361,17 @@ class IncludeGraphics(Command):
265
361
  r"""Place a picture: `\includegraphics[width=3cm]{a.png}`."""
266
362
  cs = "includegraphics" # the TeX name, stated outright; without `cs` it is prefix + class name
267
363
  path: Raw
268
- options: Raw | None = None # TeX's one optional argument, written first
364
+ options: IncludeGraphicsOptions | None = None # a KeyVals: TeX's one optional argument, written first
269
365
  ```
270
366
 
271
- A body that calls one (`IncludeGraphics.call(Hole("path"), options=Raw(r"width=\linewidth"))`) makes the generated `.sty`
367
+ A body that calls one (`IncludeGraphics.call(Hole("path"), options=IncludeGraphicsOptions(width=Length.expr(r"\linewidth")))`) makes the generated `.sty`
272
368
  load the package; the kernel header (`builtin=True`) never does. A library whose commands no body calls, or that takes
273
369
  options, is loaded with `pkg.require(microtype, options=["protrusion=true"])`: `\RequirePackage[protrusion=true]{microtype}`,
274
370
  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`.
371
+ reference image, so a header that names a command TeX does not have fails, with the path of the call. `adjustbox`, `graphicx`,
372
+ `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
373
+ the sectioning commands; a library's own syntax (TikZ paths such as `\draw (0,0) -- (1,1);`) is written as `Passthrough`
374
+ or `Raw`.
278
375
 
279
376
  ### A package that is already written in TeX
280
377
 
@@ -328,7 +425,7 @@ pkg.document(card, settings=[DeckName("R&D")]) # \renewcommand{\acDeckName}{R
328
425
  ```
329
426
 
330
427
  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.
428
+ 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
429
 
333
430
  A `Value[Raw]` is the same for a setting that is TeX (`\newcommand{\acFooter}{\textit{draft}}`, renewed by `settings=[Footer(Raw(...))]`).
334
431
  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
@@ -223,8 +236,14 @@ class FitSize(Command):
223
236
  available: Raw
224
237
  ```
225
238
 
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)`
239
+ A component with `defined_by` has no body (`TM-M032`) and its block belongs to the same package (`TM-M033`). `texmodel check`
240
+ reads each block (`Package.check_parts()`, no TeX needed) and reports a declared component the block does not define with that
241
+ signature (`TM-C010`, `TM-C011`), a public `<prefix>Name` the block defines that nothing declares (`TM-C012`), and a name
242
+ defined twice (`TM-C013`; generation itself refuses it, `TM-P012`). The reading is regex-level (`\newcommand`, `\providecommand`,
243
+ `\DeclareRobustCommand`, `\newenvironment`, the xparse family, `\newlength`, `\newtoggle`, `\newif`, `\definecolor`) and
244
+ skips an expl3 block. A flag, toggle, length, colour or text setting a block defines is a `Value` with `defined_by` (no default;
245
+ `cs = "..."` gives the whole control sequence, `colour_name` the colour's name). For the
246
+ rest, whether TeX really defines it, with that many arguments, is checked by compiling: `texmodel_build.testkit.check_library(compiler, pkg)`
228
247
  builds `pkg.test_document(...)` with `export_probes(pkg)` (it writes `texmodel:probe:def.<cs>=yes|no` and
229
248
  `arity.<cs>=<n>` for each such component), compiles it, and returns `TM-C010` for a name the block does not define and
230
249
  `TM-C011` for another number of arguments (`?` is checked for existence only). The arity is the number of `#` in the macro's
@@ -235,6 +254,83 @@ The PDF Info strings need no expl3 in a document: `PdfInfo(Title="...", Author="
235
254
  writes each value PDF-escaped through `\csname pdfmanagement_add:nnn\endcsname`, and needs `RequireMetadata` in `before_class`
236
255
  (`TM-C009`). A key is letters only (`TM-M035`).
237
256
 
257
+ ### Parts filled from a model
258
+
259
+ A package that starts from TeX you already have is a list of parts: `pkg.library(name, TemplateRef("brand.tex"), fill=model)`.
260
+ `fill` is a mapping, a dataclass or a pydantic model, and the text's `<<name>>` holes (`<<a.b>>` into a nested model) are
261
+ filled by type: `str` is escaped, `Raw` passes through, a `Length`, a colour model, a number or a flag is written as it is.
262
+ 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
263
+ than the part uses. Without `fill` a block is carried exactly as it is. Components with `defined_by` then give decks typed
264
+ calls to what the parts define. [The cookbook](docs/cookbook.md) has a worked example.
265
+
266
+ ### Arguments: slots, value types and key lists
267
+
268
+ A field is an argument; its type says what it holds and `arg(...)` says how it is written (`texmodel.interface`).
269
+ Without `arg()`, a required field is `{...}` and an optional one (a default) is TeX's one optional argument, `[...]`,
270
+ written first. With `arg()` the slot keeps its declaration order, so any signature can be stated:
271
+
272
+ ```python
273
+ from texmodel.interface import PAREN, SQUARE, Gap, SlotGroup, Star, arg
274
+
275
+ class MakeBox(Command): # \makebox[3cm][c]{x}
276
+ cs = "makebox"
277
+ width: Length | None = arg(SQUARE, default=None, fill=Gap.REFUSE) # `[c]` alone is not valid: refused
278
+ align: BoxAlign | None = arg(SQUARE, default=None)
279
+ text: Raw
280
+
281
+ class Section(Command): # \section*[short]{long}
282
+ cs = "section"
283
+ starred: Star = arg(default=False) # a token: `*`
284
+ short: Text | None = arg(SQUARE, default=None)
285
+ title: Text
286
+
287
+ class Put(Command): # \put(1,2){x}
288
+ cs = "put"
289
+ at: tuple[int, int] = arg(PAREN) # a tuple, written `1,2` (`sep=` changes the comma)
290
+ text: Raw
291
+ ```
292
+
293
+ - **Slots.** `{}` `[]` `()` `<>` or your own `Delimiter`; a `Star` or other token; a slot left out is not written, and an
294
+ optional slot left out before a later optional one writes `fill=` (say `""` for empty brackets) or is refused
295
+ (`Gap.REFUSE`); declaring two optionals without saying which is `TM-M037`. A `SlotGroup` is written together or not at all.
296
+ Optional and token fields are keyword-only in Python wherever they stand, so give an optional one `default=None`.
297
+ - **Value types.** `Text` (escaped), `PdfString`, `Raw`, `Length` (`Length("2mm")`, or `Length.expr(r"\linewidth")` for a TeX
298
+ expression), a colour model (`CMYK`, `RGB`, `Gray`) or a `ColourName` (`blue!50`), `int`, `float`, `bool` (`true`/`false`),
299
+ a `Choice`, a tuple, `NamedTuple`, dataclass or model of those, a `list`, and a `KeyVals`. A wrong value is a violation
300
+ with the field's path.
301
+ - **Definitions.** A signature `\newcommand` can define keeps it. Several optional arguments, other delimiters or a token
302
+ are defined with `\NewDocumentCommand` / `\NewDocumentEnvironment` (xparse, in the LaTeX kernel; the arguments are
303
+ `#1`... in TeX order, an optional one left out is empty and a token is `\BooleanTrue` / `\BooleanFalse`). Stubs
304
+ and `check_interface` follow: `check_interface` reads `\NewDocumentCommand` family definitions as xparse letters.
305
+
306
+ A key list (`\adjustbox{max width=\linewidth, center}{...}`) is a `KeyVals`, a frozen pydantic model: its field types say
307
+ how each item is written, and its `KeyRules` say how the library reads it.
308
+
309
+ ```python
310
+ from texmodel.interface import Flag, Item, OptionalValue, Switch
311
+ from texmodel.keys import PGFKEYS
312
+ from texmodel.keyvals import KeyVals, key
313
+
314
+ class AdjustOptions(KeyVals, rules=PGFKEYS):
315
+ max_width: Length | None = key("max width") # max width=3cm
316
+ center: Flag = False # center
317
+ clip: Switch | None = None # clip=true / clip=false
318
+ valign: VAlign | None = None # valign=t (a Choice)
319
+ rounded: OptionalValue[Length] | None = key("rounded corners") # rounded corners / rounded corners=2pt
320
+ style: Inner | None = key("every node/.style") # every node/.style={draw=red} (another KeyVals)
321
+ tags: list[Text] | None = None # tags=a,tags=b (a list repeats the key)
322
+ extra: Item[Raw] | None = None # anything else, as written (an Item has no key)
323
+
324
+ AdjustBox(AdjustOptions(max_width=Length.expr(r"\linewidth"), center=True), Raw("x"))
325
+ ```
326
+
327
+ Items are written in declaration order and unset ones are left out. A `Text` value the library would split (`,` `=` `]`) is
328
+ put in braces, and `exclusive` / `requires` on the class refuse combinations. Presets in `texmodel.keys` (`PGFKEYS`,
329
+ `XKEYVAL`, `KVOPTIONS`, `L3KEYS`, `KEYVAL`) hold what a library does; `dataclasses.replace(PGFKEYS, booleans=("on", "off"))`
330
+ makes another. They were measured, not assumed: `tests/test_texmodel/test_interface_tex.py` passes awkward values through
331
+ pgfkeys, xkeyval, keyval and l3keys. All four read `,` `=` `]` safely when braced and strip a space at either end of a
332
+ 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.
333
+
238
334
  ### Standard libraries
239
335
 
240
336
  `texmodel.std` has a header for each TeX library a deck loads: `latex` (the kernel), `graphicx`, `xcolor`, `geometry`,
@@ -246,16 +342,17 @@ class IncludeGraphics(Command):
246
342
  r"""Place a picture: `\includegraphics[width=3cm]{a.png}`."""
247
343
  cs = "includegraphics" # the TeX name, stated outright; without `cs` it is prefix + class name
248
344
  path: Raw
249
- options: Raw | None = None # TeX's one optional argument, written first
345
+ options: IncludeGraphicsOptions | None = None # a KeyVals: TeX's one optional argument, written first
250
346
  ```
251
347
 
252
- A body that calls one (`IncludeGraphics.call(Hole("path"), options=Raw(r"width=\linewidth"))`) makes the generated `.sty`
348
+ A body that calls one (`IncludeGraphics.call(Hole("path"), options=IncludeGraphicsOptions(width=Length.expr(r"\linewidth")))`) makes the generated `.sty`
253
349
  load the package; the kernel header (`builtin=True`) never does. A library whose commands no body calls, or that takes
254
350
  options, is loaded with `pkg.require(microtype, options=["protrusion=true"])`: `\RequirePackage[protrusion=true]{microtype}`,
255
351
  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`.
352
+ reference image, so a header that names a command TeX does not have fails, with the path of the call. `adjustbox`, `graphicx`,
353
+ `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
354
+ the sectioning commands; a library's own syntax (TikZ paths such as `\draw (0,0) -- (1,1);`) is written as `Passthrough`
355
+ or `Raw`.
259
356
 
260
357
  ### A package that is already written in TeX
261
358
 
@@ -309,7 +406,7 @@ pkg.document(card, settings=[DeckName("R&D")]) # \renewcommand{\acDeckName}{R
309
406
  ```
310
407
 
311
408
  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.
409
+ 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
410
 
314
411
  A `Value[Raw]` is the same for a setting that is TeX (`\newcommand{\acFooter}{\textit{draft}}`, renewed by `settings=[Footer(Raw(...))]`).
315
412
  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.0"
24
24
 
25
25
  [project.optional-dependencies]
26
26
  proof-pdf = [