pytex-preprocessor 0.1.0__py3-none-any.whl

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 (119) hide show
  1. pytex/__init__.py +87 -0
  2. pytex/commands/__init__.py +51 -0
  3. pytex/commands/biblatex.py +98 -0
  4. pytex/commands/builtin.py +598 -0
  5. pytex/commands/captions.py +56 -0
  6. pytex/commands/cleveref.py +43 -0
  7. pytex/commands/colors.py +60 -0
  8. pytex/commands/conditionals.py +62 -0
  9. pytex/commands/counters.py +85 -0
  10. pytex/commands/definitions.py +109 -0
  11. pytex/commands/floats.py +93 -0
  12. pytex/commands/font.py +138 -0
  13. pytex/commands/fontawesome.py +88 -0
  14. pytex/commands/fontspec.py +75 -0
  15. pytex/commands/geometry.py +25 -0
  16. pytex/commands/glossaries.py +126 -0
  17. pytex/commands/graphics.py +68 -0
  18. pytex/commands/hooks.py +58 -0
  19. pytex/commands/hyperref.py +57 -0
  20. pytex/commands/lengths.py +200 -0
  21. pytex/commands/listings.py +63 -0
  22. pytex/commands/mdframed.py +43 -0
  23. pytex/commands/picture.py +32 -0
  24. pytex/commands/setspace.py +38 -0
  25. pytex/commands/tables.py +123 -0
  26. pytex/helpers/__init__.py +3 -0
  27. pytex/helpers/coerce.py +13 -0
  28. pytex/helpers/parenting.py +13 -0
  29. pytex/helpers/sanitize.py +54 -0
  30. pytex/helpers/with_package.py +61 -0
  31. pytex/interface/__init__.py +3 -0
  32. pytex/interface/control_sequence.py +29 -0
  33. pytex/interface/package.py +52 -0
  34. pytex/interface/tex.py +41 -0
  35. pytex/model/__init__.py +25 -0
  36. pytex/model/color.py +203 -0
  37. pytex/model/concat.py +31 -0
  38. pytex/model/control_sequence.py +72 -0
  39. pytex/model/document.py +120 -0
  40. pytex/model/document_class.py +29 -0
  41. pytex/model/empty.py +19 -0
  42. pytex/model/environment.py +30 -0
  43. pytex/model/image.py +137 -0
  44. pytex/model/include.py +21 -0
  45. pytex/model/length.py +54 -0
  46. pytex/model/math.py +401 -0
  47. pytex/model/package.py +132 -0
  48. pytex/model/raw.py +61 -0
  49. pytex/packages.py +221 -0
  50. pytex/registry.py +49 -0
  51. pytex_builder/__init__.py +8 -0
  52. pytex_builder/build.py +175 -0
  53. pytex_builder/console.py +77 -0
  54. pytex_builder/render.py +90 -0
  55. pytex_builder/tectonic.py +370 -0
  56. pytex_hsrtreport/__init__.py +116 -0
  57. pytex_hsrtreport/assets/fonts/Blender/Blender-Bold.ttf +0 -0
  58. pytex_hsrtreport/assets/fonts/Blender/Blender-BoldItalic.ttf +0 -0
  59. pytex_hsrtreport/assets/fonts/Blender/Blender-Book.ttf +0 -0
  60. pytex_hsrtreport/assets/fonts/Blender/Blender-BookItalic.ttf +0 -0
  61. pytex_hsrtreport/assets/fonts/Blender/Blender-Medium.ttf +0 -0
  62. pytex_hsrtreport/assets/fonts/Blender/Blender-MediumItalic.ttf +0 -0
  63. pytex_hsrtreport/assets/fonts/Blender/Blender-Strong.ttf +0 -0
  64. pytex_hsrtreport/assets/fonts/Blender/Blender-Thin.ttf +0 -0
  65. pytex_hsrtreport/assets/fonts/Blender/Blender-ThinItalic.ttf +0 -0
  66. pytex_hsrtreport/assets/fonts/DIN/DIN-Black.ttf +0 -0
  67. pytex_hsrtreport/assets/fonts/DIN/DIN-Bold.ttf +0 -0
  68. pytex_hsrtreport/assets/fonts/DIN/DIN-BoldItalic.ttf +0 -0
  69. pytex_hsrtreport/assets/fonts/DIN/DIN-Italic.ttf +0 -0
  70. pytex_hsrtreport/assets/fonts/DIN/DIN-Medium.ttf +0 -0
  71. pytex_hsrtreport/assets/fonts/DIN/DIN-Regular.ttf +0 -0
  72. pytex_hsrtreport/assets/fonts/Times New Roman.ttf +0 -0
  73. pytex_hsrtreport/assets/logos/ASTA.svg +79 -0
  74. pytex_hsrtreport/assets/logos/DUMMY.png +0 -0
  75. pytex_hsrtreport/assets/logos/DUMMY_FOOT.png +0 -0
  76. pytex_hsrtreport/assets/logos/ECHO.svg +226 -0
  77. pytex_hsrtreport/assets/logos/HSRT.pdf +0 -0
  78. pytex_hsrtreport/assets/logos/INF.pdf +0 -0
  79. pytex_hsrtreport/assets/logos/STUPA.pdf +0 -0
  80. pytex_hsrtreport/assets/logos/Skyline.pdf +0 -0
  81. pytex_hsrtreport/boxes.py +215 -0
  82. pytex_hsrtreport/citations.py +21 -0
  83. pytex_hsrtreport/cleveref_names.py +47 -0
  84. pytex_hsrtreport/colors.py +30 -0
  85. pytex_hsrtreport/document.py +307 -0
  86. pytex_hsrtreport/fonts.py +66 -0
  87. pytex_hsrtreport/glossary.py +61 -0
  88. pytex_hsrtreport/hyperref_config.py +49 -0
  89. pytex_hsrtreport/listings.py +90 -0
  90. pytex_hsrtreport/logos.py +234 -0
  91. pytex_hsrtreport/pagebreak.py +67 -0
  92. pytex_hsrtreport/pagesetup.py +33 -0
  93. pytex_hsrtreport/tex/pagesetup.tex +76 -0
  94. pytex_hsrtreport/titlepage.py +136 -0
  95. pytex_hsrtreport/variants.py +24 -0
  96. pytex_hsrtreport/voting.py +96 -0
  97. pytex_hsrtreport/watermark.py +63 -0
  98. pytex_hsrtreport/wordcount.py +33 -0
  99. pytex_koma/__init__.py +90 -0
  100. pytex_koma/commands.py +296 -0
  101. pytex_koma/document.py +138 -0
  102. pytex_markdown/__init__.py +62 -0
  103. pytex_markdown/convert.py +271 -0
  104. pytex_markdown/escape.py +11 -0
  105. pytex_preprocessor-0.1.0.dist-info/METADATA +82 -0
  106. pytex_preprocessor-0.1.0.dist-info/RECORD +119 -0
  107. pytex_preprocessor-0.1.0.dist-info/WHEEL +5 -0
  108. pytex_preprocessor-0.1.0.dist-info/entry_points.txt +2 -0
  109. pytex_preprocessor-0.1.0.dist-info/top_level.txt +7 -0
  110. pytex_protocol/__init__.py +37 -0
  111. pytex_protocol/convert.py +202 -0
  112. pytex_protocol/document.py +91 -0
  113. pytex_protocol/entries.py +96 -0
  114. pytex_protocol/frontmatter.py +80 -0
  115. pytex_protocol/header.py +139 -0
  116. pytex_protocol/shortcodes.py +130 -0
  117. pytex_protocol/signatures.py +84 -0
  118. pytex_tikz/__init__.py +25 -0
  119. pytex_tikz/tikz.py +272 -0
@@ -0,0 +1,234 @@
1
+ from collections.abc import Iterator
2
+ from importlib.resources import files
3
+ from pathlib import Path
4
+ from typing import Final
5
+
6
+ from pytex.commands.colors import SelectColor
7
+ from pytex.commands.graphics import Includegraphics
8
+ from pytex.interface.tex import TeX
9
+ from pytex.model.concat import Concat
10
+ from pytex.model.image import IncludeImage
11
+ from pytex.registry import Registry
12
+
13
+ from .variants import Variant
14
+
15
+ LOGO_DIR: Final[Path] = Path(str(files("pytex_hsrtreport").joinpath("assets/logos")))
16
+
17
+
18
+ KNOWN_LOGOS: Final[dict[str, str]] = {
19
+ "HSRT": "HSRT.pdf",
20
+ "INF": "INF.pdf",
21
+ "ASTA": "ASTA.svg",
22
+ "STUPA": "STUPA.pdf",
23
+ "ECHO": "ECHO.svg",
24
+ "Skyline": "Skyline.pdf",
25
+ }
26
+
27
+
28
+ def logo_path(name: str) -> Path:
29
+ if name not in KNOWN_LOGOS:
30
+ raise ValueError(f"unknown HSRT logo {name!r}; known: {sorted(KNOWN_LOGOS)}")
31
+ return LOGO_DIR / KNOWN_LOGOS[name]
32
+
33
+
34
+ # Output dir for logos referenced by the tikz overlays, relative to the .tex
35
+ # file. `HSRTReport.write_inline_logos` copies the (svg->pdf converted) files
36
+ # here. The overlays emit these relative paths instead of absolute source
37
+ # paths so tectonic can read them (absolute paths outside the build cwd trip
38
+ # tectonic's "non-reproducible" warning and forbid reading the .bb bbox file).
39
+ LOGO_OUTPUT_DIR: Final[str] = "logos"
40
+
41
+
42
+ def logo_output_name(name: str) -> str:
43
+ """Filename of a logo as materialised in the output ``logos/`` dir.
44
+
45
+ SVG sources are converted to PDF, so they get a ``.pdf`` suffix.
46
+ """
47
+ src = logo_path(name)
48
+ if src.suffix.lower() == ".svg":
49
+ return f"{src.stem}.pdf"
50
+ return src.name
51
+
52
+
53
+ def logo_output_rel(name: str) -> str:
54
+ """Path of a logo relative to the .tex file (e.g. ``logos/INF.pdf``)."""
55
+ return f"{LOGO_OUTPUT_DIR}/{logo_output_name(name)}"
56
+
57
+
58
+ @Registry.add
59
+ def Logo(
60
+ name: str,
61
+ scale: float = 1.0,
62
+ height: str | None = None,
63
+ inline_base64: bool = True,
64
+ ) -> TeX:
65
+ """Vendored HSRT logo via `IncludeImage` (auto-bakes base64 by default)."""
66
+ return IncludeImage(
67
+ path=logo_path(name),
68
+ inline_base64=inline_base64,
69
+ scale=None if height is not None else str(scale),
70
+ height=height,
71
+ )
72
+
73
+
74
+ @Registry.add
75
+ def LogoStrip(
76
+ names: tuple[str, ...],
77
+ scale: float = 1.0,
78
+ height: str | None = None,
79
+ separator: str = "\\hspace{0.5cm}",
80
+ inline_base64: bool = True,
81
+ ) -> TeX:
82
+ """Horizontal sequence of vendored logos."""
83
+ if not names:
84
+ from pytex.model.empty import Empty
85
+
86
+ return Empty
87
+ logos = (
88
+ Logo(name, scale=scale, height=height, inline_base64=inline_base64)
89
+ for name in names
90
+ )
91
+ sep = _sep(separator)
92
+ # Interleave a separator before every logo except the first.
93
+ return Concat(
94
+ *(
95
+ piece
96
+ for i, logo in enumerate(logos)
97
+ for piece in ((sep, logo) if i else (logo,))
98
+ )
99
+ )
100
+
101
+
102
+ def _sep(text: str) -> TeX:
103
+ from pytex.model.raw import Raw
104
+
105
+ return Raw(text)
106
+
107
+
108
+ @Registry.add
109
+ def DefaultLogos(
110
+ variant: Variant,
111
+ scale: float = 1.0,
112
+ height: str | None = None,
113
+ inline_base64: bool = True,
114
+ ) -> TeX:
115
+ from .variants import default_logo_names
116
+
117
+ return LogoStrip(
118
+ default_logo_names(variant),
119
+ scale=scale,
120
+ height=height,
121
+ inline_base64=inline_base64,
122
+ )
123
+
124
+
125
+ def titlepage_logo_overlay(
126
+ names: tuple[str, ...],
127
+ logo_height: str = "1.4cm",
128
+ xshift: str = "1.5cm",
129
+ yshift: str = "-1.5cm",
130
+ node_sep: str = "0.5cm",
131
+ prefix: str = "tplogo",
132
+ ) -> str:
133
+ """Raw LaTeX: tikz overlay placing logos at the top-left of the page.
134
+
135
+ Mirrors the ``\\foreach`` loop in ``tmp/Pages/Titlepage.tex`` but
136
+ unrolled in Python so no LaTeX arrays or counters are needed.
137
+ """
138
+ if not names:
139
+ return ""
140
+
141
+ def lines() -> Iterator[str]:
142
+ yield r"\begin{tikzpicture}[overlay, remember picture]"
143
+ # Invisible dummy anchor at the top-left page corner — the chain
144
+ # start (logo0), mirroring the DUMMY_FOOT node in the original.
145
+ yield (
146
+ " \\node[anchor=north west, inner sep=0pt, "
147
+ + f"xshift={xshift}, yshift={yshift}, opacity=0] ({prefix}0) "
148
+ + f"at (current page.north west) {{\\rule{{0pt}}{{{logo_height}}}}};"
149
+ )
150
+ for i, name in enumerate(names, 1):
151
+ path = logo_output_rel(name)
152
+ yield (
153
+ f" \\node[anchor=west, inner sep=0pt, xshift={node_sep}] "
154
+ + f"({prefix}{i}) at ({prefix}{i - 1}.east) "
155
+ + f"{{\\includegraphics[height={logo_height}]{{{path}}}}};"
156
+ )
157
+ yield r"\end{tikzpicture}"
158
+
159
+ return "\n".join(lines())
160
+
161
+
162
+ def footer_logo_hook(
163
+ names: tuple[str, ...],
164
+ logo_height: str = "0.8cm",
165
+ xshift: str = "-2cm",
166
+ yshift: str = "1.5em",
167
+ node_sep: str = "-0.3cm",
168
+ prefix: str = "fllogo",
169
+ skyline: bool = True,
170
+ ) -> str:
171
+ """Raw LaTeX: ``\\AddToHook{shipout/background}`` block placing logos at
172
+ the bottom-right of every page.
173
+
174
+ Mirrors the ``\\foreach`` loop in ``tmp/Modules/Layout/Logos.tex`` but
175
+ unrolled in Python. Logos are chained right-to-left from a dummy anchor
176
+ at the page's south-east corner. The skyline graphic is placed at the
177
+ south-west corner when ``skyline=True`` (matching the original template).
178
+
179
+ The footer logos are wrapped in ``\\ifHSRTTitlePage\\else...\\fi`` so they
180
+ are suppressed on the title page (which sets ``\\HSRTTitlePagetrue``); the
181
+ skyline graphic is unconditional.
182
+ """
183
+ if not names and not skyline:
184
+ return ""
185
+
186
+ def lines() -> Iterator[str]:
187
+ yield r"\AddToHook{shipout/background}{%"
188
+ yield r" \begin{tikzpicture}[overlay, remember picture]"
189
+ if names:
190
+ # Suppress footer logos on the title page.
191
+ yield r" \ifHSRTTitlePage\else"
192
+ # Invisible dummy anchor at the bottom-right page corner.
193
+ yield (
194
+ " \\node[anchor=south east, inner sep=0pt, "
195
+ + f"xshift={xshift}, yshift={yshift}, opacity=0] ({prefix}0) "
196
+ + f"at (current page.south east) {{\\rule{{0pt}}{{{logo_height}}}}};"
197
+ )
198
+ # Chain logos from right to left (anchor=east, stepping west).
199
+ for i, name in enumerate(names, 1):
200
+ path = logo_output_rel(name)
201
+ yield (
202
+ " \\node[anchor=east, inner sep=0pt, "
203
+ + f"xshift={node_sep}, yshift=2pt] ({prefix}{i}) "
204
+ + f"at ({prefix}{i - 1}.west) "
205
+ + f"{{\\includegraphics[height={logo_height}]{{{path}}}}};"
206
+ )
207
+ yield r" \fi"
208
+ if skyline:
209
+ skyline_path = logo_output_rel("Skyline")
210
+ yield (
211
+ " \\node[anchor=south west, inner sep=0pt, yshift=0em] "
212
+ + "at (current page.south west) "
213
+ + f"{{\\includegraphics[width=1.5\\paperwidth]{{{skyline_path}}}}};"
214
+ )
215
+ yield r" \end{tikzpicture}%"
216
+ yield r"}"
217
+
218
+ return "\n".join(lines())
219
+
220
+
221
+ # Keep SelectColor import alive for callers that previously imported via this module
222
+ __all__ = [
223
+ "LOGO_OUTPUT_DIR",
224
+ "DefaultLogos",
225
+ "Includegraphics",
226
+ "Logo",
227
+ "LogoStrip",
228
+ "SelectColor",
229
+ "footer_logo_hook",
230
+ "logo_output_name",
231
+ "logo_output_rel",
232
+ "logo_path",
233
+ "titlepage_logo_overlay",
234
+ ]
@@ -0,0 +1,67 @@
1
+ from pytex.interface.tex import TeX
2
+ from pytex.model.control_sequence import ControlSequence, Parameter
3
+ from pytex.model.environment import Environment
4
+ from pytex.model.raw import Raw
5
+ from pytex.registry import Registry
6
+
7
+ __all__ = [
8
+ "Conditionalpagebreak",
9
+ "Critical",
10
+ "Keeptogether",
11
+ "Smartsection",
12
+ "Smartsubsection",
13
+ ]
14
+
15
+
16
+ @Registry.add
17
+ def Keeptogether(body: TeX | str) -> TeX:
18
+ """Wraps body in a minipage to keep it on the same page."""
19
+ return Environment("minipage", body, (Parameter(r"\linewidth"),))
20
+
21
+
22
+ @Registry.add
23
+ def Conditionalpagebreak(amount: str = "10\\baselineskip") -> TeX:
24
+ return ControlSequence("needspace", (Parameter(amount),))
25
+
26
+
27
+ @Registry.add
28
+ def Critical(body: TeX | str) -> TeX:
29
+ """High-penalty samepage env: prevents widows/clubs/linebreaks inside."""
30
+ return Environment(
31
+ "samepage",
32
+ Raw(
33
+ r"\interlinepenalty=10000\widowpenalty=10000\clubpenalty=10000"
34
+ + str(body if isinstance(body, str) else body.rendered)
35
+ ),
36
+ )
37
+
38
+
39
+ @Registry.add
40
+ def Smartsection(title: TeX | str, short: TeX | str | None = None) -> TeX:
41
+ """Section that asks for `\\sectionminspace` before breaking the page."""
42
+ head = (
43
+ ControlSequence("section", (Parameter(title),))
44
+ if short is None
45
+ else ControlSequence(
46
+ "section",
47
+ (Parameter(short, optional=True), Parameter(title)),
48
+ )
49
+ )
50
+ return Raw(
51
+ r"\vfil\penalty-9999\vfilneg\needspace{\sectionminspace}" + head.rendered
52
+ )
53
+
54
+
55
+ @Registry.add
56
+ def Smartsubsection(title: TeX | str, short: TeX | str | None = None) -> TeX:
57
+ head = (
58
+ ControlSequence("subsection", (Parameter(title),))
59
+ if short is None
60
+ else ControlSequence(
61
+ "subsection",
62
+ (Parameter(short, optional=True), Parameter(title)),
63
+ )
64
+ )
65
+ return Raw(
66
+ r"\vfil\penalty-9999\vfilneg\needspace{\subsectionminspace}" + head.rendered
67
+ )
@@ -0,0 +1,33 @@
1
+ from importlib.resources import files
2
+ from pathlib import Path
3
+ from typing import Final
4
+
5
+ from pytex.interface.tex import TeX
6
+ from pytex.model.include import IncludeTeX
7
+ from pytex.registry import Registry
8
+
9
+ __all__ = ["HSRTPageSetup"]
10
+
11
+ # The full preamble lives in tex/pagesetup.tex (too large to inline as a raw
12
+ # string); it is shipped as package data and loaded verbatim at render time.
13
+ SETUP_TEX: Final[Path] = Path(
14
+ str(files("pytex_hsrtreport").joinpath("tex/pagesetup.tex"))
15
+ )
16
+
17
+
18
+ @Registry.add
19
+ def HSRTPageSetup() -> TeX:
20
+ """KOMA scrheadings, section fonts, chapter-name tracking, typography.
21
+
22
+ Mirrors ``Config/PageSetup.tex``, ``Config/Sections.tex``, and
23
+ ``Config/Typography.tex`` from the original HSRTReport template.
24
+
25
+ Must be emitted in the preamble *before* font setup: the ``\\providecommand``
26
+ fallbacks here define ``\\blenderfont``/``\\dinfont`` as safe defaults, and
27
+ ``HSRTFontSetup`` then overrides them with ``\\renewcommand`` once the real
28
+ font families are declared.
29
+
30
+ Defines ``\\ifHSRTBackMatter`` — set it to true in back matter to suppress
31
+ chapter-specific headers/footers without calling mode-sensitive KOMA commands.
32
+ """
33
+ return IncludeTeX(SETUP_TEX, allow_replacements=False)
@@ -0,0 +1,76 @@
1
+ \makeatletter
2
+ \def\@title{}
3
+ \def\@author{}
4
+ \newif\ifHSRTBackMatter
5
+ \newif\ifHSRTTitlePage
6
+ % Minimum space \Smartsection/\Smartsubsection demand before breaking a page.
7
+ \newlength{\sectionminspace}\setlength{\sectionminspace}{4\baselineskip}
8
+ \newlength{\subsectionminspace}\setlength{\subsectionminspace}{3\baselineskip}
9
+ \providecommand{\blenderfont}{\sffamily}
10
+ \providecommand{\dinfont}{\rmfamily}
11
+ \let\Chaptermark\chaptermark
12
+ \def\chaptermark#1{\def\Chaptername{#1}\Chaptermark{#1}}
13
+ \let\Sectionmark\sectionmark
14
+ \def\sectionmark#1{\def\Sectionname{#1}\Sectionmark{#1}}
15
+ \AtEndDocument{\immediate\write\@auxout{\string\gdef\string\@lastpage{\thepage}}}
16
+ \clearpairofpagestyles
17
+ \setkomafont{pageheadfoot}{\color{gray}\blenderfont}
18
+ \setkomafont{pagenumber}{\color{gray}\blenderfont}
19
+ \setlength{\footskip}{20pt}
20
+ \ohead*{\ifHSRTBackMatter\else\ifnum\value{chapter}>0\relax\Roman{\thechapter}~–~\Chaptername\fi\fi}
21
+ \ifoot{\@author}
22
+ \cfoot{\ifHSRTBackMatter\else Seite~\thepage\if@mainmatter\@ifundefined{@lastpage}{}{~von~\@lastpage}\fi\fi}
23
+ \ohead{\ifHSRTBackMatter\else\ifnum\value{chapter}>0\relax\thechapter~–~\Chaptername\fi\fi}
24
+ \ihead{\@title}
25
+ \pagestyle{scrheadings}
26
+ \renewcommand*{\chapterpagestyle}{scrheadings}
27
+ \setkomafont{disposition}{\blenderfont\bfseries}
28
+ % ToC consistency: section entries default to the main font (DIN), and their
29
+ % page numbers are wrapped in \normalfont by \@dottedtocline (also DIN), giving
30
+ % the ToC two families. Force the whole ToC into \blenderfont after its heading
31
+ % and alias \normalfont to it (locally to the ToC group) so entries, leaders and
32
+ % page numbers all match the headings. Chapter entry/number keep an explicit
33
+ % bold komafont.
34
+ \AfterTOCHead{\blenderfont\let\normalfont\blenderfont}
35
+ \setkomafont{chapterentry}{\blenderfont\bfseries}
36
+ \setkomafont{chapterentrypagenumber}{\blenderfont\bfseries}
37
+ \setkomafont{chapter}{\LARGE\blenderfont\bfseries}
38
+ \setkomafont{section}{\Large\blenderfont\bfseries}
39
+ \setkomafont{subsection}{\large\blenderfont\bfseries}
40
+ \setkomafont{subsubsection}{\large\blenderfont\bfseries}
41
+ \RedeclareSectionCommand[beforeskip=3ex plus 1ex minus 0.5ex,afterskip=1.5ex plus 0.3ex,style=section]{chapter}
42
+ \RedeclareSectionCommand[beforeskip=4.5ex plus 1.5ex minus 0.5ex,afterskip=1.5ex plus 0.3ex]{section}
43
+ \RedeclareSectionCommand[beforeskip=3.5ex plus 1ex minus 0.5ex,afterskip=1ex plus 0.2ex]{subsection}
44
+ \RedeclareSectionCommand[beforeskip=2ex plus 0.5ex minus 0.3ex,afterskip=0.8ex plus 0.1ex]{subsubsection}
45
+ \counterwithin{figure}{chapter}
46
+ \counterwithin{table}{chapter}
47
+ \counterwithout{equation}{chapter}
48
+ \renewcommand{\thefigure}{\thechapter.\arabic{figure}}
49
+ \renewcommand{\thetable}{\thechapter.\arabic{table}}
50
+ \renewcommand{\baselinestretch}{1}
51
+ \renewcommand{\arraystretch}{1.5}
52
+ \hyphenpenalty=500
53
+ \exhyphenpenalty=500
54
+ \tolerance=1000
55
+ \emergencystretch=3em
56
+ \widowpenalty=10000
57
+ \clubpenalty=10000
58
+ \raggedbottom
59
+ \setlength{\parskip}{0.5em plus 0.2em minus 0.1em}
60
+ \setlength{\parindent}{0pt}
61
+ % Prevent \mainmatter from inserting a blank page via \cleardoublepage.
62
+ % scrbook's \mainmatter calls \cleardoublepage to force chapters onto a
63
+ % right-hand page; for single-sided reports that just produces an unwanted
64
+ % blank page between the ToC and the first chapter.
65
+ \renewcommand{\mainmatter}{%
66
+ \clearpage
67
+ \@mainmattertrue
68
+ \pagenumbering{arabic}%
69
+ }
70
+ % The running head is set in \blenderfont, which is taller than the default
71
+ % 17pt \headheight; the surplus spilled into the body and triggered an overfull
72
+ % \vbox "while \output is active" on every page. Enlarge \headheight to fit.
73
+ % Done at begin-document so it survives geometry's deferred layout pass, and it
74
+ % leaves the 2cm horizontal margins untouched.
75
+ \AtBeginDocument{\setlength{\headheight}{22pt}}
76
+ \makeatother
@@ -0,0 +1,136 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Final, override
3
+
4
+ from pytex.commands.builtin import (
5
+ Blenderfont,
6
+ Hspace,
7
+ Newline,
8
+ Noindent,
9
+ Rule,
10
+ SectionStar,
11
+ Textbf,
12
+ Vfill,
13
+ Vspace,
14
+ )
15
+ from pytex.commands.colors import SelectColor
16
+ from pytex.commands.floats import Titlepage as TitlepageEnv
17
+ from pytex.commands.font import HugeBig
18
+ from pytex.commands.setspace import Setstretch
19
+ from pytex.commands.tables import Tabular
20
+ from pytex.helpers.parenting import attach
21
+ from pytex.interface.tex import TeX
22
+ from pytex.model.concat import Concat
23
+ from pytex.model.environment import Environment
24
+ from pytex.model.raw import Raw
25
+ from pytex.registry import Registry
26
+
27
+ from .logos import titlepage_logo_overlay
28
+
29
+ __all__ = ["TitlePage", "TitlePageDataLine"]
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class TitlePageDataLine:
34
+ label: str
35
+ value: TeX | str
36
+
37
+
38
+ def _data_table_body(lines: tuple[TitlePageDataLine, ...]) -> TeX:
39
+ parts: list[TeX | str] = ["&"]
40
+ for line in lines:
41
+ parts.extend((Newline(), Textbf(line.label), " & ", line.value))
42
+ return Concat(*parts)
43
+
44
+
45
+ @Registry.add
46
+ @dataclass
47
+ class TitlePage(TeX):
48
+ """HSRT titlepage with abstract, keywords, and data table.
49
+
50
+ ``logo_names`` drives a tikz overlay that chains logos left-to-right from
51
+ the top-left corner of the page, mirroring the ``\\foreach`` loop in
52
+ ``tmp/Pages/Titlepage.tex`` (loop unrolled in Python).
53
+ """
54
+
55
+ title: Final[TeX | str]
56
+ abstract: Final[TeX | str] = ""
57
+ keywords: Final[TeX | str] = ""
58
+ data_lines: Final[tuple[TitlePageDataLine, ...]] = ()
59
+ logo_names: Final[tuple[str, ...]] = ()
60
+ _parent: "TeX | None" = field(default=None, init=False, compare=False, repr=False)
61
+
62
+ def __post_init__(self) -> None:
63
+ attach(self, self.title, self.abstract, self.keywords)
64
+ for line in self.data_lines:
65
+ attach(self, line.value)
66
+
67
+ @property
68
+ @override
69
+ def children(self) -> tuple[TeX, ...]:
70
+ candidates = (
71
+ self.title,
72
+ self.abstract,
73
+ self.keywords,
74
+ *(line.value for line in self.data_lines),
75
+ )
76
+ return tuple(v for v in candidates if isinstance(v, TeX))
77
+
78
+ @property
79
+ @override
80
+ def rendered(self) -> str:
81
+ # Tikz overlay: logos chained left-to-right from the top-left corner,
82
+ # 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,
86
+ )
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).
91
+ return Concat(
92
+ 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
+ ),
135
+ Raw(r"\HSRTTitlePagefalse", allow_replacements=False),
136
+ ).rendered
@@ -0,0 +1,24 @@
1
+ from enum import Enum
2
+
3
+ __all__ = ["Variant", "default_logo_names"]
4
+
5
+
6
+ class Variant(Enum):
7
+ """HSRT report variant — picks default logo set on the title page."""
8
+
9
+ INF = "inf"
10
+ STUPA = "stupa"
11
+ ASTA = "asta"
12
+ ECHO = "echo"
13
+
14
+
15
+ DEFAULT_LOGOS: dict[Variant, tuple[str, ...]] = {
16
+ Variant.INF: ("INF",),
17
+ Variant.STUPA: ("STUPA",),
18
+ Variant.ASTA: ("ASTA",),
19
+ Variant.ECHO: ("ECHO",),
20
+ }
21
+
22
+
23
+ def default_logo_names(variant: Variant) -> tuple[str, ...]:
24
+ return DEFAULT_LOGOS.get(variant, ())
@@ -0,0 +1,96 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Final, override
3
+
4
+ from pytex.commands.builtin import Textbf, Vspace
5
+ from pytex.commands.floats import Columnbreak, Multicols
6
+ from pytex.commands.fontawesome import FaIcon
7
+ from pytex.helpers.parenting import attach
8
+ from pytex.interface.package import PackageProtocol
9
+ from pytex.interface.tex import TeX
10
+ from pytex.model.concat import Concat
11
+ from pytex.model.package import DefinePackage
12
+ from pytex.packages import FONTAWESOME, MDFRAMED, XCOLOR
13
+ from pytex.registry import Registry
14
+
15
+ from .boxes import ColoredBox, CustomBox
16
+
17
+ __all__ = ["VotingResults"]
18
+
19
+ # `multicol` is not in pytex.packages; the multicols env needs it. Declared here
20
+ # because VotingResults builds the Multicols node inside `.rendered`, so the
21
+ # command's own requirements never reach the document's package collector.
22
+ MULTICOL: Final = DefinePackage("multicol")
23
+
24
+
25
+ def _vote_color(yes: int, no: int) -> str:
26
+ if yes > no:
27
+ return "britishracinggreen"
28
+ if yes < no:
29
+ return "red"
30
+ return "eggplant"
31
+
32
+
33
+ @Registry.add
34
+ @dataclass(frozen=True)
35
+ class VotingResults(TeX):
36
+ """Voting tally box. Header color picked in Python from yes/no counts."""
37
+
38
+ yes: Final[int]
39
+ no: Final[int]
40
+ abstain: Final[int]
41
+ body: Final[TeX | str] = ""
42
+ _parent: "TeX | None" = field(default=None, init=False, compare=False, repr=False)
43
+
44
+ def __post_init__(self) -> None:
45
+ attach(self, self.body)
46
+
47
+ @property
48
+ def color(self) -> str:
49
+ return _vote_color(self.yes, self.no)
50
+
51
+ @property
52
+ @override
53
+ def children(self) -> tuple[TeX, ...]:
54
+ return (self.body,) if isinstance(self.body, TeX) else ()
55
+
56
+ @property
57
+ @override
58
+ def requires(self) -> frozenset[PackageProtocol]:
59
+ return frozenset({MDFRAMED, XCOLOR, FONTAWESOME, MULTICOL})
60
+
61
+ @property
62
+ @override
63
+ def rendered(self) -> str:
64
+ return ColoredBox(
65
+ body=Concat(
66
+ self.body,
67
+ Vspace("-2em", star=True),
68
+ Multicols(
69
+ 3,
70
+ Concat(
71
+ CustomBox(
72
+ Concat(Textbf("Ja:"), " ", str(self.yes)),
73
+ "thumbs-up",
74
+ "britishracinggreen",
75
+ ),
76
+ Columnbreak(),
77
+ CustomBox(
78
+ Concat(Textbf("Nein:"), " ", str(self.no)),
79
+ "thumbs-down",
80
+ "red",
81
+ ),
82
+ Columnbreak(),
83
+ CustomBox(
84
+ Concat(Textbf("Enthaltung:"), " ", str(self.abstain)),
85
+ None,
86
+ "eggplant",
87
+ ),
88
+ ),
89
+ ),
90
+ ),
91
+ icon=FaIcon("vote-yea"),
92
+ icon_color=self.color,
93
+ icon_size="24pt",
94
+ icon_offset_x="-0.2cm",
95
+ background_color=self.color,
96
+ ).rendered