pytex-preprocessor 0.1.0rc1__tar.gz → 0.1.2__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 (124) hide show
  1. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/PKG-INFO +1 -1
  2. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/pyproject.toml +4 -1
  3. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_builder/build.py +17 -4
  4. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_builder/render.py +8 -0
  5. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_builder/tectonic.py +83 -20
  6. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/citations.py +4 -2
  7. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/document.py +5 -0
  8. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/hyperref_config.py +5 -1
  9. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/tex/pagesetup.tex +2 -2
  10. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/titlepage.py +64 -52
  11. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_markdown/convert.py +6 -3
  12. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_preprocessor.egg-info/PKG-INFO +1 -1
  13. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_preprocessor.egg-info/SOURCES.txt +8 -0
  14. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_preprocessor.egg-info/top_level.txt +1 -0
  15. pytex_preprocessor-0.1.2/src/pytex_protocol/__init__.py +37 -0
  16. pytex_preprocessor-0.1.2/src/pytex_protocol/convert.py +202 -0
  17. pytex_preprocessor-0.1.2/src/pytex_protocol/document.py +154 -0
  18. pytex_preprocessor-0.1.2/src/pytex_protocol/entries.py +96 -0
  19. pytex_preprocessor-0.1.2/src/pytex_protocol/frontmatter.py +80 -0
  20. pytex_preprocessor-0.1.2/src/pytex_protocol/header.py +139 -0
  21. pytex_preprocessor-0.1.2/src/pytex_protocol/shortcodes.py +130 -0
  22. pytex_preprocessor-0.1.2/src/pytex_protocol/signatures.py +84 -0
  23. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/README.md +0 -0
  24. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/setup.cfg +0 -0
  25. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/__init__.py +0 -0
  26. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/commands/__init__.py +0 -0
  27. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/commands/biblatex.py +0 -0
  28. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/commands/builtin.py +0 -0
  29. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/commands/captions.py +0 -0
  30. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/commands/cleveref.py +0 -0
  31. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/commands/colors.py +0 -0
  32. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/commands/conditionals.py +0 -0
  33. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/commands/counters.py +0 -0
  34. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/commands/definitions.py +0 -0
  35. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/commands/floats.py +0 -0
  36. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/commands/font.py +0 -0
  37. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/commands/fontawesome.py +0 -0
  38. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/commands/fontspec.py +0 -0
  39. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/commands/geometry.py +0 -0
  40. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/commands/glossaries.py +0 -0
  41. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/commands/graphics.py +0 -0
  42. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/commands/hooks.py +0 -0
  43. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/commands/hyperref.py +0 -0
  44. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/commands/lengths.py +0 -0
  45. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/commands/listings.py +0 -0
  46. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/commands/mdframed.py +0 -0
  47. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/commands/picture.py +0 -0
  48. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/commands/setspace.py +0 -0
  49. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/commands/tables.py +0 -0
  50. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/helpers/__init__.py +0 -0
  51. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/helpers/coerce.py +0 -0
  52. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/helpers/parenting.py +0 -0
  53. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/helpers/sanitize.py +0 -0
  54. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/helpers/with_package.py +0 -0
  55. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/interface/__init__.py +0 -0
  56. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/interface/control_sequence.py +0 -0
  57. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/interface/package.py +0 -0
  58. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/interface/tex.py +0 -0
  59. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/model/__init__.py +0 -0
  60. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/model/color.py +0 -0
  61. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/model/concat.py +0 -0
  62. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/model/control_sequence.py +0 -0
  63. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/model/document.py +0 -0
  64. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/model/document_class.py +0 -0
  65. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/model/empty.py +0 -0
  66. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/model/environment.py +0 -0
  67. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/model/image.py +0 -0
  68. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/model/include.py +0 -0
  69. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/model/length.py +0 -0
  70. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/model/math.py +0 -0
  71. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/model/package.py +0 -0
  72. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/model/raw.py +0 -0
  73. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/packages.py +0 -0
  74. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex/registry.py +0 -0
  75. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_builder/__init__.py +0 -0
  76. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_builder/console.py +0 -0
  77. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/__init__.py +0 -0
  78. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Bold.ttf +0 -0
  79. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-BoldItalic.ttf +0 -0
  80. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Book.ttf +0 -0
  81. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-BookItalic.ttf +0 -0
  82. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Medium.ttf +0 -0
  83. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-MediumItalic.ttf +0 -0
  84. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Strong.ttf +0 -0
  85. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-Thin.ttf +0 -0
  86. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/assets/fonts/Blender/Blender-ThinItalic.ttf +0 -0
  87. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Black.ttf +0 -0
  88. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Bold.ttf +0 -0
  89. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-BoldItalic.ttf +0 -0
  90. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Italic.ttf +0 -0
  91. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Medium.ttf +0 -0
  92. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/assets/fonts/DIN/DIN-Regular.ttf +0 -0
  93. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/assets/fonts/Times New Roman.ttf +0 -0
  94. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/assets/logos/ASTA.svg +0 -0
  95. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/assets/logos/DUMMY.png +0 -0
  96. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/assets/logos/DUMMY_FOOT.png +0 -0
  97. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/assets/logos/ECHO.svg +0 -0
  98. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/assets/logos/HSRT.pdf +0 -0
  99. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/assets/logos/INF.pdf +0 -0
  100. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/assets/logos/STUPA.pdf +0 -0
  101. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/assets/logos/Skyline.pdf +0 -0
  102. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/boxes.py +0 -0
  103. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/cleveref_names.py +0 -0
  104. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/colors.py +0 -0
  105. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/fonts.py +0 -0
  106. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/glossary.py +0 -0
  107. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/listings.py +0 -0
  108. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/logos.py +0 -0
  109. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/pagebreak.py +0 -0
  110. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/pagesetup.py +0 -0
  111. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/variants.py +0 -0
  112. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/voting.py +0 -0
  113. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/watermark.py +0 -0
  114. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_hsrtreport/wordcount.py +0 -0
  115. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_koma/__init__.py +0 -0
  116. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_koma/commands.py +0 -0
  117. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_koma/document.py +0 -0
  118. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_markdown/__init__.py +0 -0
  119. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_markdown/escape.py +0 -0
  120. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_preprocessor.egg-info/dependency_links.txt +0 -0
  121. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_preprocessor.egg-info/entry_points.txt +0 -0
  122. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_preprocessor.egg-info/requires.txt +0 -0
  123. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/src/pytex_tikz/__init__.py +0 -0
  124. {pytex_preprocessor-0.1.0rc1 → pytex_preprocessor-0.1.2}/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.0rc1
3
+ Version: 0.1.2
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.0rc1"
7
+ version = "0.1.2"
8
8
  authors = [
9
9
  { name="Frederik Beimgraben", email="frederik@beimgraben.net" },
10
10
  ]
@@ -72,6 +72,9 @@ ignore = ["N802"]
72
72
  # glossary holds raw LaTeX template constants (column types, style, labels)
73
73
  # that exceed the line length and cannot be reflowed.
74
74
  "src/pytex_hsrtreport/glossary.py" = ["E501"]
75
+ # tectonic pins a table of 64-char biber SHA256 checksums; the hash literals
76
+ # are unbreakable.
77
+ "src/pytex_builder/tectonic.py" = ["E501"]
75
78
 
76
79
  [tool.ruff.lint.isort]
77
80
  known-first-party = ["pytex", "pytex_builder", "pytex_koma", "pytex_tikz", "pytex_hsrtreport", "pytex_markdown"]
@@ -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:
@@ -58,7 +58,15 @@ def _render_markdown(path: Path) -> TeX:
58
58
  # Imported lazily so plain .tex/.py builds need neither marko nor hsrt.
59
59
  from pytex.model.document import Document
60
60
  from pytex_markdown import IncludeMarkdown
61
+ from pytex_protocol.frontmatter import split_frontmatter
61
62
 
63
+ text = path.read_text()
64
+ meta, _ = split_frontmatter(text)
65
+ # A `gremium:` frontmatter key marks a STUPA/AStA meeting protocol.
66
+ if "gremium" in meta or meta.get("typ") == "protokoll":
67
+ from pytex_protocol import render_protocol
68
+
69
+ return render_protocol(text)
62
70
  return Document(IncludeMarkdown(path))
63
71
 
64
72
 
@@ -7,6 +7,7 @@ its working directory - we point that at the cache dir.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import hashlib
10
11
  import os
11
12
  import platform
12
13
  import shutil
@@ -43,6 +44,30 @@ BIBER_RELEASE_URL = (
43
44
  "biblatex-biber/{version}/binaries/{sf_dir}/{filename}/download"
44
45
  )
45
46
 
47
+ # Mirror of the upstream biber binaries, hosted as release assets so builds
48
+ # survive SourceForge outages (it periodically gates downloads behind a
49
+ # Cloudflare challenge that curl cannot pass). Tried before SourceForge.
50
+ BIBER_MIRROR_URL = (
51
+ "https://github.com/frederikbeimgraben/PyTeX-Preprocessor"
52
+ "/releases/download/biber-binaries/{asset}"
53
+ )
54
+
55
+ # SHA256 of each upstream biber tarball, keyed by the versioned mirror asset
56
+ # name. Used to verify downloads from either source and to reject HTML error
57
+ # pages a CDN might serve with a 200 status. Linux x86_64 only for now; other
58
+ # platforms download from SourceForge without a pinned checksum.
59
+ BIBER_SHA256: dict[str, str] = {
60
+ "biber-2.11-linux_x86_64.tar.gz": "7fcb51491fb24151810a92b2e2d03b7a1291823c0f8d6fb53183af391fca42e7",
61
+ "biber-2.12-linux_x86_64.tar.gz": "fd0b5145cc908c400a701b583330635d533d750b73a272d1d5ea47e10b2fbf71",
62
+ "biber-2.13-linux_x86_64.tar.gz": "03101f418d46f4666272b68a4318d9e4b7a840d9dfa05d93ddc490d491157a75",
63
+ "biber-2.14-linux_x86_64.tar.gz": "dab3177f03322b5529d07d47d21d9e573a90c23d86eaaf11591b2d155316ee1b",
64
+ "biber-2.15-linux_x86_64.tar.gz": "653c8add18d2e94a233a6b9aae6d8144f965c2ce13fb7b4e66502b55fcd06e06",
65
+ "biber-2.16-linux_x86_64.tar.gz": "3afb97a42d2cf272d3c0b51663725e55339c4e6f3d594cd52e16c39fa9fcfb13",
66
+ "biber-2.17-linux_x86_64.tar.gz": "129d2e0332a57e985ffa253e5e9fbd28ef99af5a068d1b141145211969aa8999",
67
+ "biber-2.18-linux_x86_64.tar.gz": "2a6b4cd15a1139907799da0d23cd4ddcce8341af3960d2b3d1d3e4b4a9f1fb53",
68
+ "biber-2.19-linux_x86_64.tar.gz": "e2eda3e6ea7ac7e78d60e99a0e2aeb1096829f95791c06b768ed31a12889e58e",
69
+ }
70
+
46
71
 
47
72
  class BuildError(RuntimeError):
48
73
  """Raised when an external tool is missing or exits non-zero."""
@@ -109,33 +134,67 @@ def _biber_cached(version: str) -> Path:
109
134
  return CACHE_DIR / "biber" / version / "biber"
110
135
 
111
136
 
137
+ def _mirror_asset(version: str, filename: str) -> str:
138
+ """Versioned mirror asset name, e.g. ``biber-2.17-linux_x86_64.tar.gz``."""
139
+ return filename.replace("biber-", f"biber-{version}-", 1)
140
+
141
+
142
+ def _biber_sources(version: str) -> list[tuple[str, str | None]]:
143
+ """(url, expected_sha256 or None) pairs to try in order: mirror, then SourceForge."""
144
+ sf_dir, filename = _biber_sf_path()
145
+ asset = _mirror_asset(version, filename)
146
+ sha = BIBER_SHA256.get(asset)
147
+ return [
148
+ (BIBER_MIRROR_URL.format(asset=asset), sha),
149
+ (
150
+ BIBER_RELEASE_URL.format(version=version, sf_dir=sf_dir, filename=filename),
151
+ sha,
152
+ ),
153
+ ]
154
+
155
+
156
+ def _download_to(url: str, dest: Path, sha: str | None, console: Console) -> bool:
157
+ """Fetch *url* into *dest*; return True only on success and matching checksum."""
158
+ proc = subprocess.run(
159
+ ["curl", "-fsSL", "-o", str(dest), url],
160
+ capture_output=True,
161
+ text=True,
162
+ )
163
+ if proc.returncode != 0 or not dest.exists():
164
+ return False
165
+ if sha is not None:
166
+ actual = hashlib.sha256(dest.read_bytes()).hexdigest()
167
+ if actual != sha:
168
+ console.warn(f"checksum mismatch from {url}; discarding")
169
+ dest.unlink(missing_ok=True)
170
+ return False
171
+ return True
172
+
173
+
112
174
  def _ensure_biber(version: str, console: Console) -> Path:
113
- """Return a path to biber *version*, downloading from SourceForge if needed."""
175
+ """Return a path to biber *version*, downloading from the mirror or SourceForge."""
114
176
  cached = _biber_cached(version)
115
177
  if cached.exists():
116
178
  return cached
117
179
 
118
- sf_dir, filename = _biber_sf_path()
119
- url = BIBER_RELEASE_URL.format(version=version, sf_dir=sf_dir, filename=filename)
120
- console.step(f"Downloading biber {version}")
121
- console.detail(f"source: {url}")
180
+ if not shutil.which("curl"):
181
+ raise BuildError(
182
+ "biber is not installed and cannot be downloaded without 'curl' on PATH"
183
+ )
122
184
 
185
+ console.step(f"Downloading biber {version}")
123
186
  cached.parent.mkdir(parents=True, exist_ok=True)
124
- tmp = cached.parent / filename
187
+ tmp = cached.parent / "biber.download"
125
188
  try:
126
- if not shutil.which("curl"):
127
- raise BuildError(
128
- "biber is not installed and cannot be downloaded without 'curl' on PATH"
129
- )
130
- proc = subprocess.run(
131
- ["curl", "-fsSL", "-o", str(tmp), url],
132
- capture_output=True,
133
- text=True,
134
- )
135
- if proc.returncode != 0 or not tmp.exists():
189
+ downloaded = False
190
+ for url, sha in _biber_sources(version):
191
+ console.detail(f"source: {url}")
192
+ if _download_to(url, tmp, sha, console):
193
+ downloaded = True
194
+ break
195
+ if not downloaded:
136
196
  raise BuildError(
137
- f"failed to download biber {version}:\n"
138
- + (proc.stderr.strip() or "no output from curl")
197
+ f"failed to download biber {version} from the mirror or SourceForge"
139
198
  )
140
199
  with tarfile.open(tmp) as tf:
141
200
  member = next(
@@ -147,10 +206,14 @@ def _ensure_biber(version: str, console: Console) -> Path:
147
206
  None,
148
207
  )
149
208
  if member is None:
150
- raise BuildError(f"biber binary not found inside {filename}")
209
+ raise BuildError(
210
+ f"biber binary not found inside the biber {version} archive"
211
+ )
151
212
  src = tf.extractfile(member)
152
213
  if src is None:
153
- raise BuildError(f"could not read biber from {filename}")
214
+ raise BuildError(
215
+ f"could not read biber from the biber {version} archive"
216
+ )
154
217
  cached.write_bytes(src.read())
155
218
  except Exception:
156
219
  if cached.exists():
@@ -9,11 +9,13 @@ __all__ = ["Fcite"]
9
9
 
10
10
  @Registry.add
11
11
  def Fcite(key: str) -> TeX:
12
- """Full clickable citation: `\\hyperlink{cite.KEY}{Author, Year}`.
12
+ """Full clickable citation: `\\hyperlink{cite.0@KEY}{Author, Year}`.
13
13
 
14
14
  Mirrors HSRTReport's `\\fcite` macro — author + year in one clickable link.
15
+ biblatex names the citation anchor ``cite.0@KEY`` (the ``0@`` is refsection
16
+ 0); targeting ``cite.KEY`` would dangle.
15
17
  """
16
18
  return Hyperlink(
17
- f"cite.{key}",
19
+ f"cite.0@{key}",
18
20
  Concat(Citeauthor(key), ", ", Citeyear(key)),
19
21
  )
@@ -295,6 +295,11 @@ class HSRTReport(KomaDocument):
295
295
  def rendered(self) -> str:
296
296
  return Concat(
297
297
  DocumentClass(self.document_class, self.document_class_options),
298
+ # hyperfootnotes is only honoured as a hyperref load option, so it
299
+ # must be queued before \usepackage{hyperref}. The HSRT footnote
300
+ # setup never places hyperref's Hfootnote destination, so leaving
301
+ # footnote-mark links enabled produces dangling links.
302
+ Raw(r"\PassOptionsToPackage{hyperfootnotes=false}{hyperref}"),
298
303
  *self.ordered_packages(),
299
304
  self.inline_image_block,
300
305
  self.preamble,
@@ -19,7 +19,11 @@ HSRT_HYPER_OPTIONS: Final[dict[str, HyperOption]] = {
19
19
  "pdfpagemode": "UseOutlines",
20
20
  "bookmarksopen": True,
21
21
  "bookmarksopenlevel": 0,
22
- "hypertexnames": False,
22
+ # plainpages=false + hypertexnames=true gives roman frontmatter pages their
23
+ # own named anchors (page.i, ...) so glossary/index \hyperpage links to
24
+ # those pages resolve instead of dangling against absolute arabic anchors.
25
+ "plainpages": False,
26
+ "hypertexnames": True,
23
27
  "colorlinks": True,
24
28
  "citecolor": HSRT_CITE_COLOR,
25
29
  "linkcolor": HSRT_LINK_COLOR,
@@ -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
@@ -10,7 +10,7 @@ this module depends on ``pytex_hsrtreport``.
10
10
  from __future__ import annotations
11
11
 
12
12
  import re
13
- from typing import TYPE_CHECKING, Any, Final, cast, final
13
+ from typing import TYPE_CHECKING, Any, Final, cast
14
14
 
15
15
  from pytex.commands.builtin import (
16
16
  Bold,
@@ -91,9 +91,12 @@ def _text(node: object) -> str | None:
91
91
  return ch if isinstance(ch, str) else None
92
92
 
93
93
 
94
- @final
95
94
  class MarkdownConverter:
96
- """Walk a marko AST, producing a single ``TeX`` tree."""
95
+ """Walk a marko AST, producing a single ``TeX`` tree.
96
+
97
+ Subclass to add domain-specific blocks/inlines (see
98
+ ``pytex_protocol.convert.ProtocolConverter``).
99
+ """
97
100
 
98
101
  base_level: int
99
102
  callouts: bool
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytex-preprocessor
3
- Version: 0.1.0rc1
3
+ Version: 0.1.2
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
@@ -110,5 +110,13 @@ src/pytex_preprocessor.egg-info/dependency_links.txt
110
110
  src/pytex_preprocessor.egg-info/entry_points.txt
111
111
  src/pytex_preprocessor.egg-info/requires.txt
112
112
  src/pytex_preprocessor.egg-info/top_level.txt
113
+ src/pytex_protocol/__init__.py
114
+ src/pytex_protocol/convert.py
115
+ src/pytex_protocol/document.py
116
+ src/pytex_protocol/entries.py
117
+ src/pytex_protocol/frontmatter.py
118
+ src/pytex_protocol/header.py
119
+ src/pytex_protocol/shortcodes.py
120
+ src/pytex_protocol/signatures.py
113
121
  src/pytex_tikz/__init__.py
114
122
  src/pytex_tikz/tikz.py
@@ -3,4 +3,5 @@ pytex_builder
3
3
  pytex_hsrtreport
4
4
  pytex_koma
5
5
  pytex_markdown
6
+ pytex_protocol
6
7
  pytex_tikz
@@ -0,0 +1,37 @@
1
+ """STUPA/AStA meeting-protocol rendering, built on top of ``pytex_hsrtreport``.
2
+
3
+ Write the minutes in Obsidian-flavoured Markdown - YAML frontmatter for the
4
+ meeting header, ``> [!beschluss]`` / ``> [!abstimmung]`` / ``> [!aufgabe]``
5
+ callouts and inline ``{{shortcodes}}`` for the protocol-specific bits - and
6
+ render it to a PDF that matches the HSRTReport look.
7
+
8
+ from pytex_protocol import IncludeProtocol
9
+ __pytex__ = IncludeProtocol("sitzung.md")
10
+ """
11
+
12
+ from .convert import ProtocolConverter
13
+ from .document import IncludeProtocol, Protocol, render_protocol
14
+ from .entries import ActionItem, Deadline, Decision, Timestamp, Vote
15
+ from .frontmatter import split_frontmatter
16
+ from .header import ProtocolHeader, header_from_meta
17
+ from .shortcodes import expand_inline_shortcodes, expand_shortcode
18
+ from .signatures import SignatureLines, signature_block_from_meta
19
+
20
+ __all__ = [
21
+ "ActionItem",
22
+ "Deadline",
23
+ "Decision",
24
+ "IncludeProtocol",
25
+ "Protocol",
26
+ "ProtocolConverter",
27
+ "ProtocolHeader",
28
+ "SignatureLines",
29
+ "Timestamp",
30
+ "Vote",
31
+ "expand_inline_shortcodes",
32
+ "expand_shortcode",
33
+ "header_from_meta",
34
+ "render_protocol",
35
+ "signature_block_from_meta",
36
+ "split_frontmatter",
37
+ ]