pytex-preprocessor 0.3.0__tar.gz → 0.4.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 (132) hide show
  1. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/PKG-INFO +31 -1
  2. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/README.md +30 -0
  3. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/pyproject.toml +4 -1
  4. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/__init__.py +9 -0
  5. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/model/math.py +38 -3
  6. pytex_preprocessor-0.4.1/src/pytex/template.py +90 -0
  7. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_analyze/optimize.py +21 -3
  8. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_builder/build.py +11 -0
  9. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_preprocessor.egg-info/PKG-INFO +31 -1
  10. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_preprocessor.egg-info/SOURCES.txt +1 -0
  11. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/setup.cfg +0 -0
  12. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/commands/__init__.py +0 -0
  13. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/commands/biblatex.py +0 -0
  14. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/commands/builtin.py +0 -0
  15. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/commands/captions.py +0 -0
  16. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/commands/cleveref.py +0 -0
  17. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/commands/colors.py +0 -0
  18. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/commands/conditionals.py +0 -0
  19. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/commands/counters.py +0 -0
  20. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/commands/definitions.py +0 -0
  21. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/commands/floats.py +0 -0
  22. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/commands/font.py +0 -0
  23. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/commands/fontawesome.py +0 -0
  24. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/commands/fontspec.py +0 -0
  25. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/commands/geometry.py +0 -0
  26. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/commands/glossaries.py +0 -0
  27. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/commands/graphics.py +0 -0
  28. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/commands/hooks.py +0 -0
  29. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/commands/hyperref.py +0 -0
  30. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/commands/lengths.py +0 -0
  31. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/commands/listings.py +0 -0
  32. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/commands/mdframed.py +0 -0
  33. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/commands/picture.py +0 -0
  34. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/commands/setspace.py +0 -0
  35. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/commands/tables.py +0 -0
  36. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/helpers/__init__.py +0 -0
  37. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/helpers/coerce.py +0 -0
  38. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/helpers/parenting.py +0 -0
  39. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/helpers/sanitize.py +0 -0
  40. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/helpers/with_package.py +0 -0
  41. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/interface/__init__.py +0 -0
  42. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/interface/control_sequence.py +0 -0
  43. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/interface/package.py +0 -0
  44. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/interface/tex.py +0 -0
  45. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/model/__init__.py +0 -0
  46. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/model/color.py +0 -0
  47. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/model/comment.py +0 -0
  48. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/model/concat.py +0 -0
  49. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/model/control_sequence.py +0 -0
  50. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/model/document.py +0 -0
  51. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/model/document_class.py +0 -0
  52. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/model/empty.py +0 -0
  53. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/model/environment.py +0 -0
  54. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/model/image.py +0 -0
  55. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/model/include.py +0 -0
  56. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/model/length.py +0 -0
  57. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/model/package.py +0 -0
  58. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/model/raw.py +0 -0
  59. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/packages.py +0 -0
  60. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex/registry.py +0 -0
  61. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_analyze/__init__.py +0 -0
  62. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_analyze/analyze.py +0 -0
  63. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_builder/__init__.py +0 -0
  64. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_builder/console.py +0 -0
  65. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_builder/render.py +0 -0
  66. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_builder/tectonic.py +0 -0
  67. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_builder/tex2py.py +0 -0
  68. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_builder/tree.py +0 -0
  69. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_builder/variants.py +0 -0
  70. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/__init__.py +0 -0
  71. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Bold.ttf +0 -0
  72. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-BoldItalic.ttf +0 -0
  73. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Book.ttf +0 -0
  74. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-BookItalic.ttf +0 -0
  75. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Medium.ttf +0 -0
  76. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-MediumItalic.ttf +0 -0
  77. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Strong.ttf +0 -0
  78. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Thin.ttf +0 -0
  79. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-ThinItalic.ttf +0 -0
  80. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Black.ttf +0 -0
  81. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Bold.ttf +0 -0
  82. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-BoldItalic.ttf +0 -0
  83. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Italic.ttf +0 -0
  84. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Medium.ttf +0 -0
  85. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Regular.ttf +0 -0
  86. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/assets/fonts/Times New Roman.ttf +0 -0
  87. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/assets/logos/ASTA.svg +0 -0
  88. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/assets/logos/DUMMY.png +0 -0
  89. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/assets/logos/DUMMY_FOOT.png +0 -0
  90. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/assets/logos/ECHO.svg +0 -0
  91. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/assets/logos/HSRT.pdf +0 -0
  92. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/assets/logos/INF.pdf +0 -0
  93. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/assets/logos/STUPA.pdf +0 -0
  94. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/assets/logos/Skyline.pdf +0 -0
  95. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/boxes.py +0 -0
  96. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/citations.py +0 -0
  97. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/cleveref_names.py +0 -0
  98. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/colors.py +0 -0
  99. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/document.py +0 -0
  100. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/fonts.py +0 -0
  101. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/glossary.py +0 -0
  102. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/hyperref_config.py +0 -0
  103. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/listings.py +0 -0
  104. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/logos.py +0 -0
  105. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/pagebreak.py +0 -0
  106. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/pagesetup.py +0 -0
  107. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/tex/pagesetup.tex +0 -0
  108. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/titlepage.py +0 -0
  109. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/variants.py +0 -0
  110. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/voting.py +0 -0
  111. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/watermark.py +0 -0
  112. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_hsrtreport/wordcount.py +0 -0
  113. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_koma/__init__.py +0 -0
  114. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_koma/commands.py +0 -0
  115. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_koma/document.py +0 -0
  116. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_markdown/__init__.py +0 -0
  117. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_markdown/convert.py +0 -0
  118. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_markdown/escape.py +0 -0
  119. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_preprocessor.egg-info/dependency_links.txt +0 -0
  120. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_preprocessor.egg-info/entry_points.txt +0 -0
  121. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_preprocessor.egg-info/requires.txt +0 -0
  122. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_preprocessor.egg-info/top_level.txt +0 -0
  123. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_protocol/__init__.py +0 -0
  124. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_protocol/convert.py +0 -0
  125. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_protocol/document.py +0 -0
  126. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_protocol/entries.py +0 -0
  127. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_protocol/frontmatter.py +0 -0
  128. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_protocol/header.py +0 -0
  129. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_protocol/shortcodes.py +0 -0
  130. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_protocol/signatures.py +0 -0
  131. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_tikz/__init__.py +0 -0
  132. {pytex_preprocessor-0.3.0 → pytex_preprocessor-0.4.1}/src/pytex_tikz/tikz.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytex-preprocessor
3
- Version: 0.3.0
3
+ Version: 0.4.1
4
4
  Summary: Type-safe LaTeX document generation with Python
5
5
  Author-email: Frederik Beimgraben <frederik@beimgraben.net>
6
6
  Requires-Python: >=3.13
@@ -28,6 +28,20 @@ uses.
28
28
 
29
29
  ## Install
30
30
 
31
+ ### Prebuilt binary
32
+
33
+ Each release attaches standalone `pytex` binaries (Linux/macOS/Windows) — no
34
+ Python or `pip` needed. Download one from the
35
+ [Releases](https://github.com/frederikbeimgraben/PyTeX-Preprocessor/releases)
36
+ page, make it executable, and run it. The binary bundles its own interpreter
37
+ plus common data packages (numpy, pandas, openpyxl/calamine for spreadsheets,
38
+ Pillow, PyYAML), so documents can `import` those without installing anything;
39
+ see [`packaging/`](packaging/). It is built on Python 3.14, so documents may use
40
+ `tex(t"...")` even on machines without 3.14. (`--build` still needs `tectonic`,
41
+ which pytex downloads on first use.)
42
+
43
+ ### From PyPI
44
+
31
45
  To use the `pytex` command anywhere, install it as an isolated tool with
32
46
  [pipx](https://pipx.pypa.io/):
33
47
 
@@ -82,6 +96,22 @@ pytex example.tex.py --build # render + compile -> build/example.out.pdf
82
96
 
83
97
  Bare strings are coerced to text nodes and LaTeX-escaped.
84
98
 
99
+ ### Template strings (Python 3.14+)
100
+
101
+ On Python 3.14, `pytex.tex` accepts a [PEP 750](https://peps.python.org/pep-0750/)
102
+ template string and builds a `TeX` tree from it. Static parts are literal LaTeX;
103
+ interpolations are LaTeX-escaped when they are plain values and spliced as-is
104
+ when they are `TeX` nodes (nested template strings and lists are handled too):
105
+
106
+ ```py
107
+ from pytex import tex
108
+
109
+ name = "Q&A: 50%"
110
+ body = tex(t"{Bold('Heading')} — {name}") # node spliced; name -> "Q\&A: 50\%"
111
+ ```
112
+
113
+ `tex` is only exported on 3.14+; the rest of the library runs on 3.13.
114
+
85
115
  ## The `pytex` command
86
116
 
87
117
  The input file is dispatched by extension:
@@ -14,6 +14,20 @@ uses.
14
14
 
15
15
  ## Install
16
16
 
17
+ ### Prebuilt binary
18
+
19
+ Each release attaches standalone `pytex` binaries (Linux/macOS/Windows) — no
20
+ Python or `pip` needed. Download one from the
21
+ [Releases](https://github.com/frederikbeimgraben/PyTeX-Preprocessor/releases)
22
+ page, make it executable, and run it. The binary bundles its own interpreter
23
+ plus common data packages (numpy, pandas, openpyxl/calamine for spreadsheets,
24
+ Pillow, PyYAML), so documents can `import` those without installing anything;
25
+ see [`packaging/`](packaging/). It is built on Python 3.14, so documents may use
26
+ `tex(t"...")` even on machines without 3.14. (`--build` still needs `tectonic`,
27
+ which pytex downloads on first use.)
28
+
29
+ ### From PyPI
30
+
17
31
  To use the `pytex` command anywhere, install it as an isolated tool with
18
32
  [pipx](https://pipx.pypa.io/):
19
33
 
@@ -68,6 +82,22 @@ pytex example.tex.py --build # render + compile -> build/example.out.pdf
68
82
 
69
83
  Bare strings are coerced to text nodes and LaTeX-escaped.
70
84
 
85
+ ### Template strings (Python 3.14+)
86
+
87
+ On Python 3.14, `pytex.tex` accepts a [PEP 750](https://peps.python.org/pep-0750/)
88
+ template string and builds a `TeX` tree from it. Static parts are literal LaTeX;
89
+ interpolations are LaTeX-escaped when they are plain values and spliced as-is
90
+ when they are `TeX` nodes (nested template strings and lists are handled too):
91
+
92
+ ```py
93
+ from pytex import tex
94
+
95
+ name = "Q&A: 50%"
96
+ body = tex(t"{Bold('Heading')} — {name}") # node spliced; name -> "Q\&A: 50\%"
97
+ ```
98
+
99
+ `tex` is only exported on 3.14+; the rest of the library runs on 3.13.
100
+
71
101
  ## The `pytex` command
72
102
 
73
103
  The input file is dispatched by extension:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pytex-preprocessor"
7
- version = "0.3.0"
7
+ version = "0.4.1"
8
8
  authors = [
9
9
  { name="Frederik Beimgraben", email="frederik@beimgraben.net" },
10
10
  ]
@@ -37,6 +37,9 @@ addopts = "--import-mode=importlib"
37
37
  line-length = 88
38
38
  target-version = "py313"
39
39
  src = ["src", "tests"]
40
+ # Uses Python 3.14 t-string syntax; the py313 target cannot parse it. Excluded
41
+ # from lint/format (it is exercised by pytest on 3.14).
42
+ extend-exclude = ["tests/pytex/test_template.py"]
40
43
 
41
44
  [tool.ruff.lint]
42
45
  select = [
@@ -1,3 +1,5 @@
1
+ import sys
2
+
1
3
  from . import packages
2
4
  from .commands import (
3
5
  biblatex,
@@ -87,3 +89,10 @@ __all__ = [
87
89
  "tables",
88
90
  "with_package",
89
91
  ]
92
+
93
+ # `tex(t"...")` needs PEP 750 template strings (Python 3.14+). Exposed only
94
+ # there; the rest of the library stays importable on 3.13.
95
+ if sys.version_info >= (3, 14):
96
+ from .template import tex as tex # pyright: ignore[reportUnreachable]
97
+
98
+ __all__.append("tex")
@@ -67,11 +67,46 @@ __all__ = [
67
67
  ]
68
68
 
69
69
 
70
+ def _restrip(raw: Raw, how: str) -> Raw:
71
+ content = raw.content.lstrip() if how == "l" else raw.content.rstrip()
72
+ return Raw(content, raw.namespace, raw.allow_replacements)
73
+
74
+
75
+ def _is_blank(node: TeX | str) -> bool:
76
+ return isinstance(node, Raw) and node.content.strip() == ""
77
+
78
+
79
+ def _trim(body: TeX | str) -> TeX | str:
80
+ """Strip whitespace just inside the math delimiters.
81
+
82
+ TeX math mode ignores leading/trailing spaces and newlines, so this is
83
+ render-equivalent; it also avoids a blank line (`\\par`) sneaking inside the
84
+ delimiters, which is a hard LaTeX error. Only the outer boundary is touched
85
+ (interior whitespace can be significant, e.g. in `\\text{}` or alignments).
86
+ """
87
+ if isinstance(body, str):
88
+ return body.strip()
89
+ if isinstance(body, Raw):
90
+ return Raw(body.content.strip(), body.namespace, body.allow_replacements)
91
+ if isinstance(body, Concat):
92
+ elements = list(body.elements)
93
+ while elements and _is_blank(elements[0]):
94
+ elements.pop(0)
95
+ while elements and _is_blank(elements[-1]):
96
+ elements.pop()
97
+ if elements and isinstance(elements[0], Raw):
98
+ elements[0] = _restrip(elements[0], "l")
99
+ if elements and isinstance(elements[-1], Raw):
100
+ elements[-1] = _restrip(elements[-1], "r")
101
+ return Concat(*elements)
102
+ return body
103
+
104
+
70
105
  @Registry.add
71
106
  def Math(body: TeX | str) -> TeX:
72
107
  return Concat(
73
108
  ControlSequence("(", ()),
74
- body,
109
+ _trim(body),
75
110
  ControlSequence(")", ()),
76
111
  )
77
112
 
@@ -79,14 +114,14 @@ def Math(body: TeX | str) -> TeX:
79
114
  @Registry.add
80
115
  def InlineMath(body: TeX | str) -> TeX:
81
116
  """Dollar-delimited inline math: ``$body$``."""
82
- return Concat(Raw("$"), body, Raw("$"))
117
+ return Concat(Raw("$"), _trim(body), Raw("$"))
83
118
 
84
119
 
85
120
  @Registry.add
86
121
  def DisplayMath(body: TeX | str) -> TeX:
87
122
  return Concat(
88
123
  ControlSequence("[", ()),
89
- body,
124
+ _trim(body),
90
125
  ControlSequence("]", ()),
91
126
  )
92
127
 
@@ -0,0 +1,90 @@
1
+ r"""`tex(t"...")` — build a `TeX` tree from a template string (PEP 750).
2
+
3
+ Requires Python 3.14 (the ``t"..."`` syntax and :mod:`string.templatelib`).
4
+ This module is import-safe on older versions — it contains no t-string literals
5
+ and guards the runtime import — but :func:`tex` can only be called with a real
6
+ ``Template``, which cannot exist before 3.14. ``pytex`` only re-exports it on
7
+ 3.14+.
8
+
9
+ The rendering model mirrors the escape boundary a LaTeX document needs:
10
+
11
+ * static template parts are literal LaTeX (author-written, trusted);
12
+ * interpolations are escaped when they are plain values, spliced as-is when
13
+ they are `TeX` nodes, and recursed when they are nested template strings or
14
+ iterables of the above.
15
+
16
+ name = "Q&A: 50%"
17
+ tex(t"{Bold('Heading')} - {name}") # node spliced; name -> "Q\&A: 50\%"
18
+ """
19
+
20
+ # `string.templatelib` has no type stub before 3.14, so a type-checker running
21
+ # on an older Python cannot type the `Template` import; silence that noise for
22
+ # this module only (its logic is exercised on 3.14 in CI / the docker test).
23
+ # pyright: reportMissingImports=false, reportUnknownVariableType=false
24
+ from __future__ import annotations
25
+
26
+ from typing import TYPE_CHECKING, cast
27
+
28
+ from .helpers.sanitize import escape_latex
29
+ from .interface.tex import TeX
30
+ from .model.concat import Concat
31
+ from .model.empty import Empty
32
+ from .model.raw import Raw
33
+
34
+ if TYPE_CHECKING:
35
+ from collections.abc import Iterator
36
+ from typing import Protocol
37
+
38
+ class _Interpolation(Protocol):
39
+ @property
40
+ def value(self) -> object: ...
41
+ @property
42
+ def conversion(self) -> str | None: ...
43
+ @property
44
+ def format_spec(self) -> str: ...
45
+
46
+ class _Template(Protocol):
47
+ def __iter__(self) -> Iterator[str | _Interpolation]: ...
48
+
49
+
50
+ # Real `Template` class at runtime (3.14+) for the nested-template check; an
51
+ # empty tuple on older versions makes the isinstance test always False.
52
+ try:
53
+ from string.templatelib import Template
54
+
55
+ _template_classes = (Template,)
56
+ except ImportError: # Python < 3.14
57
+ _template_classes = ()
58
+
59
+ _TEMPLATE_TYPES: tuple[type, ...] = _template_classes
60
+
61
+ __all__ = ["tex"]
62
+
63
+ _CONVERSIONS = {"r": repr, "s": str, "a": ascii}
64
+
65
+
66
+ def tex(template: _Template) -> TeX:
67
+ """Render a t-string into a `TeX` tree (see the module docstring)."""
68
+ parts: list[TeX] = []
69
+ for item in template:
70
+ if isinstance(item, str):
71
+ parts.append(Raw(item)) # literal LaTeX
72
+ else:
73
+ parts.append(_coerce(item.value, item.conversion, item.format_spec))
74
+ return Concat(*parts)
75
+
76
+
77
+ def _coerce(value: object, conversion: str | None = None, spec: object = "") -> TeX:
78
+ if value is None:
79
+ return Empty
80
+ if isinstance(value, TeX):
81
+ return value
82
+ if isinstance(value, _TEMPLATE_TYPES):
83
+ return tex(cast("_Template", value))
84
+ if isinstance(value, (list, tuple)):
85
+ items = cast("tuple[object, ...] | list[object]", value)
86
+ return Concat(*(_coerce(item) for item in items))
87
+ if conversion in _CONVERSIONS:
88
+ value = _CONVERSIONS[conversion](value)
89
+ text = format(value, spec if isinstance(spec, str) else "")
90
+ return Raw(escape_latex(text))
@@ -130,10 +130,24 @@ def _native(raw: Raw) -> TeX:
130
130
  return raw
131
131
 
132
132
 
133
+ _MATH_OPEN = re.compile(r"(\\[\[(])\s+")
134
+ _MATH_CLOSE = re.compile(r"\s+(\\[\])])")
135
+
136
+
137
+ def _strip_math_ws(text: str) -> str:
138
+ r"""Drop whitespace just inside ``\[..\]`` and ``\(..\)``.
139
+
140
+ TeX math mode ignores it, so two strings that differ only there render the
141
+ same — this lets the tokenizer accept a `DisplayMath`/`Math` candidate
142
+ (which trims that whitespace) as equivalent to the original.
143
+ """
144
+ return _MATH_CLOSE.sub(r"\1", _MATH_OPEN.sub(r"\1", text))
145
+
146
+
133
147
  def _tokenize(raw: Raw) -> TeX | None:
134
148
  """Split a `Raw` into literal text and the native nodes for the constructs
135
- in `_TOKEN`. Returns `None` when nothing matches or the result would not
136
- render identically."""
149
+ in `_TOKEN`. Returns `None` when nothing matches or the result would change
150
+ the rendered output (math-delimiter whitespace aside — TeX ignores that)."""
137
151
  content = raw.content
138
152
  namespace = pytex_namespace(raw.namespace or {})
139
153
  parts: list[TeX] = []
@@ -148,7 +162,11 @@ def _tokenize(raw: Raw) -> TeX | None:
148
162
  if cursor < len(content):
149
163
  parts.append(Raw(content[cursor:], allow_replacements=False))
150
164
  candidate = Concat(*(_optimize(part) for part in parts))
151
- return candidate if candidate.rendered == raw.rendered else None
165
+ rendered = candidate.rendered
166
+ target = raw.rendered
167
+ if rendered == target or _strip_math_ws(rendered) == _strip_math_ws(target):
168
+ return candidate
169
+ return None
152
170
 
153
171
 
154
172
  def _token_node(
@@ -12,6 +12,7 @@ import json
12
12
  import re
13
13
  import sys
14
14
  from dataclasses import dataclass
15
+ from importlib.metadata import PackageNotFoundError, version
15
16
  from pathlib import Path
16
17
  from typing import TYPE_CHECKING, cast
17
18
 
@@ -76,11 +77,21 @@ def _slug(name: str) -> str:
76
77
  return name or "document"
77
78
 
78
79
 
80
+ def _version() -> str:
81
+ try:
82
+ return version("pytex-preprocessor")
83
+ except PackageNotFoundError:
84
+ return "unknown"
85
+
86
+
79
87
  def _parse_args(argv: list[str]) -> Config:
80
88
  parser = argparse.ArgumentParser(
81
89
  prog="pytex",
82
90
  description="Render PyTeX (.py) or LaTeX (.tex) sources and build PDFs.",
83
91
  )
92
+ _ = parser.add_argument(
93
+ "--version", action="version", version=f"pytex {_version()}"
94
+ )
84
95
  _ = parser.add_argument(
85
96
  "input",
86
97
  type=Path,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytex-preprocessor
3
- Version: 0.3.0
3
+ Version: 0.4.1
4
4
  Summary: Type-safe LaTeX document generation with Python
5
5
  Author-email: Frederik Beimgraben <frederik@beimgraben.net>
6
6
  Requires-Python: >=3.13
@@ -28,6 +28,20 @@ uses.
28
28
 
29
29
  ## Install
30
30
 
31
+ ### Prebuilt binary
32
+
33
+ Each release attaches standalone `pytex` binaries (Linux/macOS/Windows) — no
34
+ Python or `pip` needed. Download one from the
35
+ [Releases](https://github.com/frederikbeimgraben/PyTeX-Preprocessor/releases)
36
+ page, make it executable, and run it. The binary bundles its own interpreter
37
+ plus common data packages (numpy, pandas, openpyxl/calamine for spreadsheets,
38
+ Pillow, PyYAML), so documents can `import` those without installing anything;
39
+ see [`packaging/`](packaging/). It is built on Python 3.14, so documents may use
40
+ `tex(t"...")` even on machines without 3.14. (`--build` still needs `tectonic`,
41
+ which pytex downloads on first use.)
42
+
43
+ ### From PyPI
44
+
31
45
  To use the `pytex` command anywhere, install it as an isolated tool with
32
46
  [pipx](https://pipx.pypa.io/):
33
47
 
@@ -82,6 +96,22 @@ pytex example.tex.py --build # render + compile -> build/example.out.pdf
82
96
 
83
97
  Bare strings are coerced to text nodes and LaTeX-escaped.
84
98
 
99
+ ### Template strings (Python 3.14+)
100
+
101
+ On Python 3.14, `pytex.tex` accepts a [PEP 750](https://peps.python.org/pep-0750/)
102
+ template string and builds a `TeX` tree from it. Static parts are literal LaTeX;
103
+ interpolations are LaTeX-escaped when they are plain values and spliced as-is
104
+ when they are `TeX` nodes (nested template strings and lists are handled too):
105
+
106
+ ```py
107
+ from pytex import tex
108
+
109
+ name = "Q&A: 50%"
110
+ body = tex(t"{Bold('Heading')} — {name}") # node spliced; name -> "Q\&A: 50\%"
111
+ ```
112
+
113
+ `tex` is only exported on 3.14+; the rest of the library runs on 3.13.
114
+
85
115
  ## The `pytex` command
86
116
 
87
117
  The input file is dispatched by extension:
@@ -3,6 +3,7 @@ pyproject.toml
3
3
  src/pytex/__init__.py
4
4
  src/pytex/packages.py
5
5
  src/pytex/registry.py
6
+ src/pytex/template.py
6
7
  src/pytex/commands/__init__.py
7
8
  src/pytex/commands/biblatex.py
8
9
  src/pytex/commands/builtin.py