pytex-preprocessor 0.1.0__tar.gz → 0.1.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 (125) hide show
  1. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/PKG-INFO +1 -1
  2. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/pyproject.toml +1 -1
  3. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_builder/build.py +17 -4
  4. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/tex/pagesetup.tex +2 -2
  5. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/titlepage.py +64 -52
  6. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_preprocessor.egg-info/PKG-INFO +1 -1
  7. pytex_preprocessor-0.1.1/src/pytex_protocol/document.py +154 -0
  8. pytex_preprocessor-0.1.0/src/pytex_protocol/document.py +0 -91
  9. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/README.md +0 -0
  10. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/setup.cfg +0 -0
  11. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/__init__.py +0 -0
  12. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/commands/__init__.py +0 -0
  13. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/commands/biblatex.py +0 -0
  14. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/commands/builtin.py +0 -0
  15. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/commands/captions.py +0 -0
  16. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/commands/cleveref.py +0 -0
  17. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/commands/colors.py +0 -0
  18. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/commands/conditionals.py +0 -0
  19. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/commands/counters.py +0 -0
  20. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/commands/definitions.py +0 -0
  21. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/commands/floats.py +0 -0
  22. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/commands/font.py +0 -0
  23. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/commands/fontawesome.py +0 -0
  24. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/commands/fontspec.py +0 -0
  25. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/commands/geometry.py +0 -0
  26. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/commands/glossaries.py +0 -0
  27. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/commands/graphics.py +0 -0
  28. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/commands/hooks.py +0 -0
  29. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/commands/hyperref.py +0 -0
  30. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/commands/lengths.py +0 -0
  31. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/commands/listings.py +0 -0
  32. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/commands/mdframed.py +0 -0
  33. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/commands/picture.py +0 -0
  34. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/commands/setspace.py +0 -0
  35. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/commands/tables.py +0 -0
  36. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/helpers/__init__.py +0 -0
  37. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/helpers/coerce.py +0 -0
  38. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/helpers/parenting.py +0 -0
  39. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/helpers/sanitize.py +0 -0
  40. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/helpers/with_package.py +0 -0
  41. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/interface/__init__.py +0 -0
  42. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/interface/control_sequence.py +0 -0
  43. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/interface/package.py +0 -0
  44. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/interface/tex.py +0 -0
  45. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/model/__init__.py +0 -0
  46. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/model/color.py +0 -0
  47. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/model/concat.py +0 -0
  48. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/model/control_sequence.py +0 -0
  49. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/model/document.py +0 -0
  50. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/model/document_class.py +0 -0
  51. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/model/empty.py +0 -0
  52. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/model/environment.py +0 -0
  53. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/model/image.py +0 -0
  54. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/model/include.py +0 -0
  55. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/model/length.py +0 -0
  56. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/model/math.py +0 -0
  57. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/model/package.py +0 -0
  58. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/model/raw.py +0 -0
  59. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/packages.py +0 -0
  60. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex/registry.py +0 -0
  61. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_builder/__init__.py +0 -0
  62. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_builder/console.py +0 -0
  63. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_builder/render.py +0 -0
  64. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_builder/tectonic.py +0 -0
  65. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/__init__.py +0 -0
  66. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Bold.ttf +0 -0
  67. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-BoldItalic.ttf +0 -0
  68. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Book.ttf +0 -0
  69. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-BookItalic.ttf +0 -0
  70. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Medium.ttf +0 -0
  71. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-MediumItalic.ttf +0 -0
  72. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Strong.ttf +0 -0
  73. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Thin.ttf +0 -0
  74. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-ThinItalic.ttf +0 -0
  75. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Black.ttf +0 -0
  76. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Bold.ttf +0 -0
  77. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-BoldItalic.ttf +0 -0
  78. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Italic.ttf +0 -0
  79. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Medium.ttf +0 -0
  80. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Regular.ttf +0 -0
  81. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/assets/fonts/Times New Roman.ttf +0 -0
  82. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/assets/logos/ASTA.svg +0 -0
  83. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/assets/logos/DUMMY.png +0 -0
  84. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/assets/logos/DUMMY_FOOT.png +0 -0
  85. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/assets/logos/ECHO.svg +0 -0
  86. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/assets/logos/HSRT.pdf +0 -0
  87. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/assets/logos/INF.pdf +0 -0
  88. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/assets/logos/STUPA.pdf +0 -0
  89. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/assets/logos/Skyline.pdf +0 -0
  90. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/boxes.py +0 -0
  91. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/citations.py +0 -0
  92. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/cleveref_names.py +0 -0
  93. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/colors.py +0 -0
  94. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/document.py +0 -0
  95. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/fonts.py +0 -0
  96. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/glossary.py +0 -0
  97. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/hyperref_config.py +0 -0
  98. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/listings.py +0 -0
  99. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/logos.py +0 -0
  100. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/pagebreak.py +0 -0
  101. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/pagesetup.py +0 -0
  102. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/variants.py +0 -0
  103. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/voting.py +0 -0
  104. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/watermark.py +0 -0
  105. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_hsrtreport/wordcount.py +0 -0
  106. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_koma/__init__.py +0 -0
  107. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_koma/commands.py +0 -0
  108. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_koma/document.py +0 -0
  109. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_markdown/__init__.py +0 -0
  110. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_markdown/convert.py +0 -0
  111. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_markdown/escape.py +0 -0
  112. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_preprocessor.egg-info/SOURCES.txt +0 -0
  113. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_preprocessor.egg-info/dependency_links.txt +0 -0
  114. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_preprocessor.egg-info/entry_points.txt +0 -0
  115. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_preprocessor.egg-info/requires.txt +0 -0
  116. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_preprocessor.egg-info/top_level.txt +0 -0
  117. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_protocol/__init__.py +0 -0
  118. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_protocol/convert.py +0 -0
  119. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_protocol/entries.py +0 -0
  120. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_protocol/frontmatter.py +0 -0
  121. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_protocol/header.py +0 -0
  122. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_protocol/shortcodes.py +0 -0
  123. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_protocol/signatures.py +0 -0
  124. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.1}/src/pytex_tikz/__init__.py +0 -0
  125. {pytex_preprocessor-0.1.0 → pytex_preprocessor-0.1.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.1.0
3
+ Version: 0.1.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pytex-preprocessor"
7
- version = "0.1.0"
7
+ version = "0.1.1"
8
8
  authors = [
9
9
  { name="Frederik Beimgraben", email="frederik@beimgraben.net" },
10
10
  ]
@@ -8,6 +8,7 @@ acronyms resolve.
8
8
  from __future__ import annotations
9
9
 
10
10
  import argparse
11
+ import re
11
12
  import sys
12
13
  from dataclasses import dataclass
13
14
  from pathlib import Path
@@ -42,16 +43,28 @@ def _default_output(inp: Path, build_dir: Path) -> Path:
42
43
  ``build_dir`` so the ``.out.tex`` and its inline assets (fonts, logos,
43
44
  images) stay out of the source tree:
44
45
 
45
- * ``example.tex.py`` -> ``<build_dir>/example.out.tex``
46
- * ``report.py`` -> ``<build_dir>/report.out.tex``
47
- * ``paper.tex`` -> ``<build_dir>/paper.out.tex``
46
+ The stem is also slugified (whitespace and shell/TeX-hostile characters
47
+ become ``_``) because it becomes the TeX ``\\jobname``; spaces there break
48
+ tectonic's biber/makeindex steps (the ``.bcf`` path cannot be opened):
49
+
50
+ * ``example.tex.py`` -> ``<build_dir>/example.out.tex``
51
+ * ``report.py`` -> ``<build_dir>/report.out.tex``
52
+ * ``2026-06-15 STUPA.md`` -> ``<build_dir>/2026-06-15_STUPA.md.out.tex``
48
53
  """
49
54
  base = inp
50
55
  if base.suffix.lower() in {".py", ".tex"}:
51
56
  base = base.with_suffix("")
52
57
  if base.suffix.lower() == ".tex":
53
58
  base = base.with_suffix("")
54
- return build_dir / f"{base.name}.out.tex"
59
+ return build_dir / f"{_slug(base.name)}.out.tex"
60
+
61
+
62
+ def _slug(name: str) -> str:
63
+ """Make `name` safe as a TeX jobname: collapse whitespace and drop
64
+ characters that confuse tectonic/biber/makeindex."""
65
+ name = re.sub(r"\s+", "_", name.strip())
66
+ name = re.sub(r"[^\w.\-]", "", name)
67
+ return name or "document"
55
68
 
56
69
 
57
70
  def _parse_args(argv: list[str]) -> Config:
@@ -1,3 +1,4 @@
1
+ \usepackage{lastpage}
1
2
  \makeatletter
2
3
  \def\@title{}
3
4
  \def\@author{}
@@ -12,14 +13,13 @@
12
13
  \def\chaptermark#1{\def\Chaptername{#1}\Chaptermark{#1}}
13
14
  \let\Sectionmark\sectionmark
14
15
  \def\sectionmark#1{\def\Sectionname{#1}\Sectionmark{#1}}
15
- \AtEndDocument{\immediate\write\@auxout{\string\gdef\string\@lastpage{\thepage}}}
16
16
  \clearpairofpagestyles
17
17
  \setkomafont{pageheadfoot}{\color{gray}\blenderfont}
18
18
  \setkomafont{pagenumber}{\color{gray}\blenderfont}
19
19
  \setlength{\footskip}{20pt}
20
20
  \ohead*{\ifHSRTBackMatter\else\ifnum\value{chapter}>0\relax\Roman{\thechapter}~–~\Chaptername\fi\fi}
21
21
  \ifoot{\@author}
22
- \cfoot{\ifHSRTBackMatter\else Seite~\thepage\if@mainmatter\@ifundefined{@lastpage}{}{~von~\@lastpage}\fi\fi}
22
+ \cfoot{\ifHSRTBackMatter\else Seite~\thepage\if@mainmatter~von~\pageref{LastPage}\fi\fi}
23
23
  \ohead{\ifHSRTBackMatter\else\ifnum\value{chapter}>0\relax\thechapter~–~\Chaptername\fi\fi}
24
24
  \ihead{\@title}
25
25
  \pagestyle{scrheadings}
@@ -1,3 +1,4 @@
1
+ from collections.abc import Iterator
1
2
  from dataclasses import dataclass, field
2
3
  from typing import Final, override
3
4
 
@@ -20,12 +21,22 @@ from pytex.commands.tables import Tabular
20
21
  from pytex.helpers.parenting import attach
21
22
  from pytex.interface.tex import TeX
22
23
  from pytex.model.concat import Concat
24
+ from pytex.model.empty import Empty
23
25
  from pytex.model.environment import Environment
24
26
  from pytex.model.raw import Raw
25
27
  from pytex.registry import Registry
26
28
 
27
29
  from .logos import titlepage_logo_overlay
28
30
 
31
+
32
+ def _is_blank(node: TeX | str) -> bool:
33
+ """True for an empty string or the `Empty` node — used to skip empty
34
+ title-page sections (a protocol has no abstract or keywords)."""
35
+ if isinstance(node, str):
36
+ return not node.strip()
37
+ return node is Empty
38
+
39
+
29
40
  __all__ = ["TitlePage", "TitlePageDataLine"]
30
41
 
31
42
 
@@ -75,62 +86,63 @@ class TitlePage(TeX):
75
86
  )
76
87
  return tuple(v for v in candidates if isinstance(v, TeX))
77
88
 
78
- @property
79
- @override
80
- def rendered(self) -> str:
89
+ def _title_block(self) -> TeX:
90
+ return Environment(
91
+ "flushleft",
92
+ Concat(
93
+ Raw(r"\hyphenpenalty=10000\exhyphenpenalty=10000"),
94
+ Noindent(),
95
+ SelectColor("black"),
96
+ Textbf(
97
+ Concat(
98
+ Blenderfont(),
99
+ HugeBig(),
100
+ Hspace("-2.5pt", star=True),
101
+ self.title,
102
+ )
103
+ ),
104
+ Raw(r"\par"),
105
+ Vspace("-0.5em"),
106
+ Rule(r"\textwidth", "0.5mm"),
107
+ ),
108
+ )
109
+
110
+ def _content(self) -> Iterator[TeX | str]:
81
111
  # Tikz overlay: logos chained left-to-right from the top-left corner,
82
112
  # matching tmp/Pages/Titlepage.tex but with the foreach unrolled here.
83
- logo_overlay = Raw(
84
- titlepage_logo_overlay(self.logo_names),
85
- allow_replacements=False,
113
+ yield Raw(titlepage_logo_overlay(self.logo_names), allow_replacements=False)
114
+ yield Vspace("4cm")
115
+ yield self._title_block()
116
+ yield Vspace("2em")
117
+ yield Setstretch("1.0")
118
+ # Abstract and keywords are optional — skip the labels when empty
119
+ # (e.g. a meeting protocol has neither).
120
+ if not _is_blank(self.abstract):
121
+ yield SectionStar("Abstract")
122
+ yield Vspace("-1em")
123
+ yield self.abstract
124
+ yield Vspace("1em", star=True)
125
+ if not _is_blank(self.keywords):
126
+ yield Raw(r"\par\noindent ")
127
+ yield Textbf("Keywords")
128
+ yield Raw(r"\par\noindent ")
129
+ yield self.keywords
130
+ yield Vfill()
131
+ yield Noindent()
132
+ yield Setstretch("1.0")
133
+ yield Tabular(
134
+ r"@{} p{30mm} p{\dimexpr\textwidth-30mm-2\tabcolsep\relax} @{}",
135
+ _data_table_body(self.data_lines),
86
136
  )
87
- # Flag true while the titlepage ships out so footer_logo_hook
88
- # suppresses the bottom-right footer logos on this page only.
89
- # Reset after \end{titlepage} (which \clearpages, shipping the page
90
- # while the flag is still true).
137
+
138
+ @property
139
+ @override
140
+ def rendered(self) -> str:
141
+ # `\HSRTTitlePagetrue` while the titlepage ships out so footer_logo_hook
142
+ # suppresses the bottom-right footer logos on this page only; reset
143
+ # after \end{titlepage} (which \clearpages while the flag is still set).
91
144
  return Concat(
92
145
  Raw(r"\HSRTTitlePagetrue", allow_replacements=False),
93
- TitlepageEnv(
94
- Concat(
95
- logo_overlay,
96
- Vspace("4cm"),
97
- Environment(
98
- "flushleft",
99
- Concat(
100
- Raw(r"\hyphenpenalty=10000\exhyphenpenalty=10000"),
101
- Noindent(),
102
- SelectColor("black"),
103
- Textbf(
104
- Concat(
105
- Blenderfont(),
106
- HugeBig(),
107
- Hspace("-2.5pt", star=True),
108
- self.title,
109
- )
110
- ),
111
- Raw(r"\par"),
112
- Vspace("-0.5em"),
113
- Rule(r"\textwidth", "0.5mm"),
114
- ),
115
- ),
116
- Vspace("2em"),
117
- Setstretch("1.0"),
118
- SectionStar("Abstract"),
119
- Vspace("-1em"),
120
- self.abstract,
121
- Vspace("1em", star=True),
122
- Raw(r"\par\noindent "),
123
- Textbf("Keywords"),
124
- Raw(r"\par\noindent "),
125
- self.keywords,
126
- Vfill(),
127
- Noindent(),
128
- Setstretch("1.0"),
129
- Tabular(
130
- r"@{} p{30mm} p{\dimexpr\textwidth-30mm-2\tabcolsep\relax} @{}",
131
- _data_table_body(self.data_lines),
132
- ),
133
- )
134
- ),
146
+ TitlepageEnv(Concat(*self._content())),
135
147
  Raw(r"\HSRTTitlePagefalse", allow_replacements=False),
136
148
  ).rendered
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytex-preprocessor
3
- Version: 0.1.0
3
+ Version: 0.1.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
@@ -0,0 +1,154 @@
1
+ """Render a STUPA/AStA meeting protocol from Obsidian-flavoured Markdown.
2
+
3
+ The protocol is an :class:`~pytex_hsrtreport.document.HSRTReport`: a full HSRT
4
+ title page carries the meeting metadata as data lines, agenda items become
5
+ numbered sections, and protocol entries (decisions, votes, action items) render
6
+ as HSRT callout boxes. An optional signature block closes the document.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from pathlib import Path
13
+ from typing import TYPE_CHECKING
14
+
15
+ import marko
16
+
17
+ from pytex.model.concat import Concat
18
+ from pytex.model.raw import Raw
19
+ from pytex.registry import Registry
20
+ from pytex_hsrtreport.document import HSRTReport
21
+ from pytex_hsrtreport.titlepage import TitlePageDataLine
22
+ from pytex_hsrtreport.variants import Variant
23
+
24
+ from .convert import ProtocolConverter
25
+ from .frontmatter import FrontmatterValue, split_frontmatter
26
+ from .signatures import signature_block_from_meta
27
+
28
+ if TYPE_CHECKING:
29
+ from collections.abc import Mapping
30
+
31
+ from pytex.interface.tex import TeX
32
+
33
+ __all__ = ["IncludeProtocol", "Protocol", "render_protocol"]
34
+
35
+ _PARSER = marko.Markdown()
36
+
37
+ _VARIANTS: dict[str, Variant] = {
38
+ "stupa": Variant.STUPA,
39
+ "asta": Variant.ASTA,
40
+ "echo": Variant.ECHO,
41
+ }
42
+
43
+ # Title-page rows: (label, frontmatter keys to try), single-value fields first.
44
+ _SCALAR_ROWS: tuple[tuple[str, tuple[str, ...]], ...] = (
45
+ ("Datum", ("datum", "date")),
46
+ ("Ort", ("ort",)),
47
+ ("Sitzungsleitung", ("sitzungsleitung",)),
48
+ ("Protokoll", ("protokoll",)),
49
+ )
50
+ _LIST_ROWS: tuple[tuple[str, tuple[str, ...]], ...] = (
51
+ ("Anwesend", ("anwesend",)),
52
+ ("Entschuldigt", ("entschuldigt",)),
53
+ ("Abwesend", ("abwesend",)),
54
+ ("Gäste", ("gaeste", "gäste")),
55
+ )
56
+
57
+
58
+ def _scalar(meta: Mapping[str, FrontmatterValue], *keys: str) -> str:
59
+ for key in keys:
60
+ value = meta.get(key)
61
+ if isinstance(value, str) and value:
62
+ return value
63
+ return ""
64
+
65
+
66
+ def _joined(meta: Mapping[str, FrontmatterValue], *keys: str) -> str:
67
+ for key in keys:
68
+ value = meta.get(key)
69
+ if isinstance(value, list) and value:
70
+ return ", ".join(value)
71
+ if isinstance(value, str) and value:
72
+ return value
73
+ return ""
74
+
75
+
76
+ def _variant(meta: Mapping[str, FrontmatterValue]) -> Variant:
77
+ return _VARIANTS.get(_scalar(meta, "gremium").lower(), Variant.STUPA)
78
+
79
+
80
+ def _format_date(value: str) -> str:
81
+ """ISO ``YYYY-MM-DD`` -> German ``DD.MM.YYYY``; anything else is kept as-is."""
82
+ match = re.fullmatch(r"(\d{4})-(\d{2})-(\d{2})", value)
83
+ return f"{match[3]}.{match[2]}.{match[1]}" if match else value
84
+
85
+
86
+ def _title(meta: Mapping[str, FrontmatterValue]) -> str:
87
+ gremium = _scalar(meta, "gremium")
88
+ datum = _format_date(_scalar(meta, "datum", "date"))
89
+ if gremium and datum:
90
+ return f"Protokoll der Sitzung des {gremium} vom {datum}"
91
+ if gremium:
92
+ return f"Protokoll der Sitzung des {gremium}"
93
+ return "Sitzungsprotokoll"
94
+
95
+
96
+ def _data_lines(meta: Mapping[str, FrontmatterValue]) -> tuple[TitlePageDataLine, ...]:
97
+ lines: list[TitlePageDataLine] = []
98
+ for label, keys in _SCALAR_ROWS:
99
+ value = _scalar(meta, *keys)
100
+ if value:
101
+ lines.append(TitlePageDataLine(label, value))
102
+ if label == "Datum":
103
+ # Append the time range right after the date when present.
104
+ span = " – ".join( # noqa: RUF001 (EN DASH between start/end)
105
+ t
106
+ for t in (
107
+ _scalar(meta, "beginn", "start"),
108
+ _scalar(meta, "ende", "end"),
109
+ )
110
+ if t
111
+ )
112
+ if span:
113
+ lines.append(TitlePageDataLine("Zeit", span))
114
+ for label, keys in _LIST_ROWS:
115
+ value = _joined(meta, *keys)
116
+ if value:
117
+ lines.append(TitlePageDataLine(f"{label} ({value.count(',') + 1})", value))
118
+ return tuple(lines)
119
+
120
+
121
+ def render_protocol(text: str, *, base_level: int = 0) -> HSRTReport:
122
+ """Build an `HSRTReport` from the Markdown source of a protocol."""
123
+ meta, body_md = split_frontmatter(text)
124
+ converter = ProtocolConverter(meta=meta, base_level=base_level)
125
+ converted = converter.block(_PARSER.parse(body_md))
126
+ # Optional sign-off block, appended when the frontmatter lists `unterschriften`.
127
+ signatures = signature_block_from_meta(meta)
128
+ tail = (Raw("\n\n"), signatures) if signatures is not None else ()
129
+ return HSRTReport(
130
+ variant=_variant(meta),
131
+ document_class="scrbook",
132
+ show_titlepage=True,
133
+ show_toc=False,
134
+ show_footer_logos=True,
135
+ title=_title(meta),
136
+ data_lines=_data_lines(meta),
137
+ # Agenda items are top-level `#` headings -> \section. In scrbook a
138
+ # chapterless section would number as "0.1"; flatten it to a plain
139
+ # arabic counter so TOPs read 1, 2, 3, ...
140
+ user_preamble=Raw(r"\renewcommand*{\thesection}{\arabic{section}}"),
141
+ body=Concat(converted, *tail),
142
+ )
143
+
144
+
145
+ @Registry.add
146
+ def Protocol(text: str, *, base_level: int = 0) -> TeX:
147
+ """Convert a protocol Markdown string to a renderable `HSRTReport`."""
148
+ return render_protocol(text, base_level=base_level)
149
+
150
+
151
+ @Registry.add
152
+ def IncludeProtocol(path: str | Path, *, encoding: str = "utf-8") -> TeX:
153
+ """Read a protocol Markdown file and render it (see :func:`render_protocol`)."""
154
+ return render_protocol(Path(path).read_text(encoding=encoding))
@@ -1,91 +0,0 @@
1
- """Render a STUPA/AStA meeting protocol from Obsidian-flavoured Markdown.
2
-
3
- The protocol is an :class:`~pytex_hsrtreport.document.HSRTReport` configured for
4
- a short minutes document: no title page, a compact header block (built from the
5
- frontmatter) at the top, agenda items as numbered sections, and protocol
6
- entries (decisions, votes, action items) rendered as HSRT callout boxes.
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- from pathlib import Path
12
- from typing import TYPE_CHECKING
13
-
14
- import marko
15
-
16
- from pytex.model.concat import Concat
17
- from pytex.model.raw import Raw
18
- from pytex.registry import Registry
19
- from pytex_hsrtreport.document import HSRTReport
20
- from pytex_hsrtreport.variants import Variant
21
-
22
- from .convert import ProtocolConverter
23
- from .frontmatter import FrontmatterValue, split_frontmatter
24
- from .header import header_from_meta
25
- from .signatures import signature_block_from_meta
26
-
27
- if TYPE_CHECKING:
28
- from pytex.interface.tex import TeX
29
-
30
- __all__ = ["IncludeProtocol", "Protocol", "render_protocol"]
31
-
32
- _PARSER = marko.Markdown()
33
-
34
- _VARIANTS: dict[str, Variant] = {
35
- "stupa": Variant.STUPA,
36
- "asta": Variant.ASTA,
37
- "echo": Variant.ECHO,
38
- }
39
-
40
-
41
- def _variant(meta: dict[str, FrontmatterValue]) -> Variant:
42
- gremium = meta.get("gremium", "")
43
- key = gremium.lower().strip() if isinstance(gremium, str) else ""
44
- return _VARIANTS.get(key, Variant.STUPA)
45
-
46
-
47
- def _document_title(meta: dict[str, FrontmatterValue]) -> str:
48
- gremium = meta.get("gremium")
49
- datum = meta.get("datum")
50
- head = (
51
- f"{gremium} — Protokoll"
52
- if isinstance(gremium, str) and gremium
53
- else "Protokoll"
54
- )
55
- return f"{head} ({datum})" if isinstance(datum, str) and datum else head
56
-
57
-
58
- def render_protocol(text: str, *, base_level: int = 0) -> HSRTReport:
59
- """Build an `HSRTReport` from the Markdown source of a protocol."""
60
- meta, body_md = split_frontmatter(text)
61
- converter = ProtocolConverter(meta=meta, base_level=base_level)
62
- converted = converter.block(_PARSER.parse(body_md))
63
- header = header_from_meta(meta)
64
- # Optional sign-off block, appended when the frontmatter lists `unterschriften`.
65
- signatures = signature_block_from_meta(meta)
66
- tail = (Raw("\n\n"), signatures) if signatures is not None else ()
67
- return HSRTReport(
68
- variant=_variant(meta),
69
- document_class="scrbook",
70
- show_titlepage=False,
71
- show_toc=False,
72
- show_footer_logos=True,
73
- title=_document_title(meta),
74
- # Agenda items are top-level `#` headings -> \section. In scrbook a
75
- # chapterless section would number as "0.1"; flatten it to a plain
76
- # arabic counter so TOPs read 1, 2, 3, ...
77
- user_preamble=Raw(r"\renewcommand*{\thesection}{\arabic{section}}"),
78
- body=Concat(header, Raw("\n\n"), converted, *tail),
79
- )
80
-
81
-
82
- @Registry.add
83
- def Protocol(text: str, *, base_level: int = 0) -> TeX:
84
- """Convert a protocol Markdown string to a renderable `HSRTReport`."""
85
- return render_protocol(text, base_level=base_level)
86
-
87
-
88
- @Registry.add
89
- def IncludeProtocol(path: str | Path, *, encoding: str = "utf-8") -> TeX:
90
- """Read a protocol Markdown file and render it (see :func:`render_protocol`)."""
91
- return render_protocol(Path(path).read_text(encoding=encoding))