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
pytex/packages.py ADDED
@@ -0,0 +1,221 @@
1
+ from enum import Enum
2
+
3
+ from pytex.model.package import DefinePackage, Package
4
+
5
+ __all__ = ["Packages"]
6
+
7
+ AMSMATH: Package = DefinePackage("amsmath")
8
+ AMSSYMB: Package = DefinePackage("amssymb")
9
+ AMSFONTS: Package = DefinePackage("amsfonts")
10
+ MATHTOOLS: Package = DefinePackage("mathtools")
11
+ HYPERREF: Package = DefinePackage("hyperref")
12
+ URL: Package = DefinePackage("url")
13
+ GRAPHICX: Package = DefinePackage("graphicx")
14
+ XCOLOR: Package = DefinePackage("xcolor")
15
+ GEOMETRY: Package = DefinePackage("geometry")
16
+ FONTENC: Package = DefinePackage("fontenc")
17
+ INPUTENC: Package = DefinePackage("inputenc")
18
+ BABEL: Package = DefinePackage("babel", options={"ngerman"})
19
+ MICROTYPE: Package = DefinePackage("microtype")
20
+ CSQUOTES: Package = DefinePackage("csquotes")
21
+ BOOKTABS: Package = DefinePackage("booktabs")
22
+ ARRAY: Package = DefinePackage("array")
23
+ TABULARX: Package = DefinePackage("tabularx")
24
+ LONGTABLE: Package = DefinePackage("longtable")
25
+ MULTIROW: Package = DefinePackage("multirow")
26
+ CAPTION: Package = DefinePackage("caption")
27
+ SUBCAPTION: Package = DefinePackage("subcaption")
28
+ FLOAT: Package = DefinePackage("float")
29
+ FANCYHDR: Package = DefinePackage("fancyhdr")
30
+ TITLESEC: Package = DefinePackage("titlesec")
31
+ ENUMITEM: Package = DefinePackage("enumitem")
32
+ LISTINGS: Package = DefinePackage("listings")
33
+ MINTED: Package = DefinePackage("minted")
34
+ TIKZ: Package = DefinePackage("tikz")
35
+ PGFPLOTS: Package = DefinePackage("pgfplots")
36
+ TCOLORBOX: Package = DefinePackage("tcolorbox")
37
+ SIUNITX: Package = DefinePackage("siunitx")
38
+ GLOSSARIES: Package = DefinePackage("glossaries", options={"acronym"})
39
+ SVG: Package = DefinePackage("svg")
40
+ SOUL: Package = DefinePackage("soul")
41
+ ULEM: Package = DefinePackage("ulem")
42
+ WRAPFIG: Package = DefinePackage("wrapfig")
43
+ PARSKIP: Package = DefinePackage("parskip")
44
+ SETSPACE: Package = DefinePackage("setspace")
45
+ TIKZ_LIB_ARROWS: Package = DefinePackage("tikz-arrows", after={TIKZ})
46
+ PGF: Package = DefinePackage("pgf")
47
+ LIPSUM: Package = DefinePackage("lipsum")
48
+ BLINDTEXT: Package = DefinePackage("blindtext")
49
+ TODONOTES: Package = DefinePackage("todonotes")
50
+ COMMENT: Package = DefinePackage("comment")
51
+ IFTHEN: Package = DefinePackage("ifthen")
52
+ ETOOLBOX: Package = DefinePackage("etoolbox")
53
+ XPARSE: Package = DefinePackage("xparse")
54
+ CALC: Package = DefinePackage("calc")
55
+ MAKEIDX: Package = DefinePackage("makeidx")
56
+ APPENDIX: Package = DefinePackage("appendix")
57
+ TITLING: Package = DefinePackage("titling")
58
+ LASTPAGE: Package = DefinePackage("lastpage")
59
+ CLEVEREF: Package = DefinePackage("cleveref", after={HYPERREF, AMSMATH})
60
+ VARIOREF: Package = DefinePackage("varioref")
61
+ FRAMED: Package = DefinePackage("framed")
62
+ # framemethod=tikz must be set at load time (preamble) for mdframed's rounded
63
+ # backgrounds; it cannot be switched on per-environment mid-document.
64
+ MDFRAMED: Package = DefinePackage("mdframed", options={("framemethod", "tikz")})
65
+ PDFPAGES: Package = DefinePackage("pdfpages")
66
+ FONTSPEC: Package = DefinePackage("fontspec")
67
+ UNICODE_MATH: Package = DefinePackage("unicode-math")
68
+ BIBLATEX: Package = DefinePackage("biblatex")
69
+ TOCLOFT: Package = DefinePackage("tocloft")
70
+ NOTOCCITE: Package = DefinePackage("notoccite")
71
+ NATBIB: Package = DefinePackage("natbib")
72
+ ACRONYM: Package = DefinePackage("acronym")
73
+ NOMENCL: Package = DefinePackage("nomencl")
74
+ ALGORITHM2E: Package = DefinePackage("algorithm2e")
75
+ ALGORITHMS: Package = DefinePackage("algorithms")
76
+ ALGORITHMICX: Package = DefinePackage("algorithmicx")
77
+ LSTLISTING: Package = LISTINGS # alias
78
+ TBL_THEOREM: Package = DefinePackage("amsthm", after={AMSMATH})
79
+ AMSTHM: Package = TBL_THEOREM
80
+ THMTOOLS: Package = DefinePackage("thmtools")
81
+ ENUMERATE: Package = DefinePackage("enumerate")
82
+ PARALIST: Package = DefinePackage("paralist")
83
+ TYPEAREA: Package = DefinePackage("typearea")
84
+ SCRLAYER_SCRPAGE: Package = DefinePackage("scrlayer-scrpage")
85
+ KOMA_SCRIPT: Package = DefinePackage("koma-script")
86
+ ADJUSTBOX: Package = DefinePackage("adjustbox")
87
+ RAGGED2E: Package = DefinePackage("ragged2e")
88
+ NEEDSPACE: Package = DefinePackage("needspace")
89
+ PLACEINS: Package = DefinePackage("placeins")
90
+ FOOTMISC: Package = DefinePackage("footmisc")
91
+ ROTATING: Package = DefinePackage("rotating")
92
+ CHANGEPAGE: Package = DefinePackage("changepage")
93
+ EVERYPAGE: Package = DefinePackage("everypage")
94
+ EVERYSHI: Package = DefinePackage("everyshi")
95
+ TIKZ_LIB_POSITIONING: Package = DefinePackage("tikz-positioning", after={TIKZ})
96
+ TIKZPAGENODES: Package = DefinePackage("tikzpagenodes", after={TIKZ})
97
+ PGFFOR: Package = DefinePackage("pgffor")
98
+ ARRAYJOBX: Package = DefinePackage("arrayjobx")
99
+ BOPHOOK: Package = DefinePackage("bophook")
100
+ ENVIRON: Package = DefinePackage("environ")
101
+ EXPL3: Package = DefinePackage("expl3")
102
+ L3KEYS2E: Package = DefinePackage("l3keys2e")
103
+ PDFTEXCMDS: Package = DefinePackage("pdftexcmds")
104
+ XFP: Package = DefinePackage("xfp")
105
+ KEYVAL: Package = DefinePackage("keyval")
106
+ PIFONT: Package = DefinePackage("pifont")
107
+ FONTAWESOME: Package = DefinePackage("fontawesome")
108
+ FLOATROW: Package = DefinePackage("floatrow")
109
+ CHNGCNTR: Package = DefinePackage("chngcntr")
110
+ ACCSUPP: Package = DefinePackage("accsupp")
111
+ AFTERPAGE: Package = DefinePackage("afterpage")
112
+ ARYDSHLN: Package = DefinePackage("arydshln")
113
+ LMODERN: Package = DefinePackage("lmodern")
114
+ HYPHENAT: Package = DefinePackage("hyphenat")
115
+ FP: Package = DefinePackage("fp")
116
+
117
+
118
+ class Packages(Enum):
119
+ AMSMATH = AMSMATH
120
+ AMSSYMB = AMSSYMB
121
+ AMSFONTS = AMSFONTS
122
+ MATHTOOLS = MATHTOOLS
123
+ HYPERREF = HYPERREF
124
+ URL = URL
125
+ GRAPHICX = GRAPHICX
126
+ XCOLOR = XCOLOR
127
+ GEOMETRY = GEOMETRY
128
+ FONTENC = FONTENC
129
+ INPUTENC = INPUTENC
130
+ BABEL = BABEL
131
+ MICROTYPE = MICROTYPE
132
+ CSQUOTES = CSQUOTES
133
+ BOOKTABS = BOOKTABS
134
+ ARRAY = ARRAY
135
+ TABULARX = TABULARX
136
+ LONGTABLE = LONGTABLE
137
+ MULTIROW = MULTIROW
138
+ CAPTION = CAPTION
139
+ SUBCAPTION = SUBCAPTION
140
+ FLOAT = FLOAT
141
+ FANCYHDR = FANCYHDR
142
+ TITLESEC = TITLESEC
143
+ ENUMITEM = ENUMITEM
144
+ LISTINGS = LISTINGS
145
+ MINTED = MINTED
146
+ TIKZ = TIKZ
147
+ PGFPLOTS = PGFPLOTS
148
+ TCOLORBOX = TCOLORBOX
149
+ SIUNITX = SIUNITX
150
+ BIBLATEX = BIBLATEX
151
+ GLOSSARIES = GLOSSARIES
152
+ SVG = SVG
153
+ SOUL = SOUL
154
+ ULEM = ULEM
155
+ WRAPFIG = WRAPFIG
156
+ PARSKIP = PARSKIP
157
+ SETSPACE = SETSPACE
158
+ PGF = PGF
159
+ LIPSUM = LIPSUM
160
+ BLINDTEXT = BLINDTEXT
161
+ TODONOTES = TODONOTES
162
+ COMMENT = COMMENT
163
+ IFTHEN = IFTHEN
164
+ ETOOLBOX = ETOOLBOX
165
+ XPARSE = XPARSE
166
+ CALC = CALC
167
+ MAKEIDX = MAKEIDX
168
+ APPENDIX = APPENDIX
169
+ TITLING = TITLING
170
+ LASTPAGE = LASTPAGE
171
+ CLEVEREF = CLEVEREF
172
+ VARIOREF = VARIOREF
173
+ FRAMED = FRAMED
174
+ MDFRAMED = MDFRAMED
175
+ PDFPAGES = PDFPAGES
176
+ FONTSPEC = FONTSPEC
177
+ UNICODE_MATH = UNICODE_MATH
178
+ TOCLOFT = TOCLOFT
179
+ NOTOCCITE = NOTOCCITE
180
+ NATBIB = NATBIB
181
+ ACRONYM = ACRONYM
182
+ NOMENCL = NOMENCL
183
+ ALGORITHM2E = ALGORITHM2E
184
+ ALGORITHMS = ALGORITHMS
185
+ ALGORITHMICX = ALGORITHMICX
186
+ AMSTHM = AMSTHM
187
+ THMTOOLS = THMTOOLS
188
+ ENUMERATE = ENUMERATE
189
+ PARALIST = PARALIST
190
+ TYPEAREA = TYPEAREA
191
+ SCRLAYER_SCRPAGE = SCRLAYER_SCRPAGE
192
+ KOMA_SCRIPT = KOMA_SCRIPT
193
+ ADJUSTBOX = ADJUSTBOX
194
+ RAGGED2E = RAGGED2E
195
+ NEEDSPACE = NEEDSPACE
196
+ PLACEINS = PLACEINS
197
+ FOOTMISC = FOOTMISC
198
+ ROTATING = ROTATING
199
+ CHANGEPAGE = CHANGEPAGE
200
+ EVERYPAGE = EVERYPAGE
201
+ EVERYSHI = EVERYSHI
202
+ TIKZPAGENODES = TIKZPAGENODES
203
+ PGFFOR = PGFFOR
204
+ ARRAYJOBX = ARRAYJOBX
205
+ BOPHOOK = BOPHOOK
206
+ ENVIRON = ENVIRON
207
+ EXPL3 = EXPL3
208
+ L3KEYS2E = L3KEYS2E
209
+ PDFTEXCMDS = PDFTEXCMDS
210
+ XFP = XFP
211
+ KEYVAL = KEYVAL
212
+ PIFONT = PIFONT
213
+ FONTAWESOME = FONTAWESOME
214
+ FLOATROW = FLOATROW
215
+ CHNGCNTR = CHNGCNTR
216
+ ACCSUPP = ACCSUPP
217
+ AFTERPAGE = AFTERPAGE
218
+ ARYDSHLN = ARYDSHLN
219
+ LMODERN = LMODERN
220
+ HYPHENAT = HYPHENAT
221
+ FP = FP
pytex/registry.py ADDED
@@ -0,0 +1,49 @@
1
+ from collections.abc import Callable
2
+ from logging import Logger
3
+ from typing import ClassVar, Self
4
+
5
+ from pytex.interface.tex import TeX
6
+
7
+ __all__ = ["Registry"]
8
+
9
+ type TeXFactory = Callable[..., TeX]
10
+
11
+ type RegistryType = type[TeX] | TeXFactory
12
+
13
+
14
+ class Registry:
15
+ types: ClassVar[dict[str, RegistryType]] = {}
16
+ instance: ClassVar[Self | None] = None
17
+
18
+ def __new__(cls) -> Self:
19
+ if cls.instance is None:
20
+ cls.instance = super().__new__(cls)
21
+
22
+ return cls.instance
23
+
24
+ @classmethod
25
+ def add[O: RegistryType](cls, obj: O) -> O:
26
+ if (key := obj.__name__) in cls.types:
27
+ Logger(cls.__name__).warning(
28
+ f"Duplicate key in registry (overwritten): {key}"
29
+ )
30
+
31
+ cls.types[key] = obj
32
+
33
+ return obj
34
+
35
+ @classmethod
36
+ def get(cls, name: str) -> RegistryType:
37
+ return cls.types[name]
38
+
39
+ @classmethod
40
+ def has(cls, name: str) -> bool:
41
+ return name in cls.types
42
+
43
+ @classmethod
44
+ def names(cls) -> frozenset[str]:
45
+ return frozenset(cls.types.keys())
46
+
47
+ @classmethod
48
+ def namespace(cls) -> dict[str, RegistryType]:
49
+ return dict(cls.types)
@@ -0,0 +1,8 @@
1
+ """Build tooling for PyTeX: render ``.py``/``.tex`` sources and compile PDFs.
2
+
3
+ Exposes the ``pytex`` console script via :func:`pytex_builder.build.main`.
4
+ """
5
+
6
+ from .build import main
7
+
8
+ __all__ = ["main"]
pytex_builder/build.py ADDED
@@ -0,0 +1,175 @@
1
+ """``pytex`` command-line entry point.
2
+
3
+ Renders a ``.tex`` or ``.py`` input to a LaTeX file and, optionally, compiles
4
+ it with tectonic - running ``makeindex`` between passes so ``glossaries`` and
5
+ acronyms resolve.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import sys
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+ from typing import TYPE_CHECKING, cast
15
+
16
+ from .console import Console
17
+ from .render import get_tex_node
18
+ from .tectonic import BuildError, ensure_tectonic, run_makeindex, run_tectonic
19
+
20
+ __all__ = ["Config", "main"]
21
+
22
+ if TYPE_CHECKING:
23
+ from pytex_hsrtreport.document import HSRTReport
24
+
25
+ MAX_PASSES = 3
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class Config:
30
+ input: Path
31
+ output: Path
32
+ build: bool
33
+ build_dir: Path
34
+ shell_escape: bool
35
+
36
+
37
+ def _default_output(inp: Path, build_dir: Path) -> Path:
38
+ """Default rendered-output path inside the build directory.
39
+
40
+ The driver extension is dropped, plus a trailing ``.tex`` if the source is
41
+ named after its target (the ``name.tex.py`` convention). The result lives in
42
+ ``build_dir`` so the ``.out.tex`` and its inline assets (fonts, logos,
43
+ images) stay out of the source tree:
44
+
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``
48
+ """
49
+ base = inp
50
+ if base.suffix.lower() in {".py", ".tex"}:
51
+ base = base.with_suffix("")
52
+ if base.suffix.lower() == ".tex":
53
+ base = base.with_suffix("")
54
+ return build_dir / f"{base.name}.out.tex"
55
+
56
+
57
+ def _parse_args(argv: list[str]) -> Config:
58
+ parser = argparse.ArgumentParser(
59
+ prog="pytex",
60
+ description="Render PyTeX (.py) or LaTeX (.tex) sources and build PDFs.",
61
+ )
62
+ _ = parser.add_argument(
63
+ "input",
64
+ type=Path,
65
+ help=(
66
+ "input file: a .tex file (wrapped in IncludeTeX) or a .py file"
67
+ + " exposing a '__pytex__' TeX node"
68
+ ),
69
+ )
70
+ _ = parser.add_argument(
71
+ "-o",
72
+ "--output",
73
+ type=Path,
74
+ default=None,
75
+ help="rendered .tex output path (default: <build-dir>/<input>.out.tex)",
76
+ )
77
+ _ = parser.add_argument(
78
+ "-b",
79
+ "--build",
80
+ action="store_true",
81
+ help="compile the rendered .tex to PDF with tectonic",
82
+ )
83
+ _ = parser.add_argument(
84
+ "--build-dir",
85
+ type=Path,
86
+ default=Path("build"),
87
+ help="directory for build artifacts and tectonic output (default: build)",
88
+ )
89
+ _ = parser.add_argument(
90
+ "--no-shell-escape",
91
+ dest="shell_escape",
92
+ action="store_false",
93
+ help="disable shell-escape (on by default; needed for inline images)",
94
+ )
95
+ ns = parser.parse_args(argv)
96
+ inp = cast("Path", ns.input)
97
+ build_dir = cast("Path", ns.build_dir)
98
+ return Config(
99
+ input=inp,
100
+ output=cast("Path | None", ns.output) or _default_output(inp, build_dir),
101
+ build=cast("bool", ns.build),
102
+ build_dir=build_dir,
103
+ shell_escape=cast("bool", ns.shell_escape),
104
+ )
105
+
106
+
107
+ def _run(cfg: Config, console: Console) -> None:
108
+ if not cfg.input.exists():
109
+ raise BuildError(f"input file does not exist: {cfg.input}")
110
+
111
+ output = cfg.output
112
+ build_dir = cfg.build_dir
113
+
114
+ console.step(f"Rendering {cfg.input.name}")
115
+ tex_node = get_tex_node(cfg.input)
116
+ source = tex_node.rendered
117
+ output.parent.mkdir(parents=True, exist_ok=True)
118
+ _ = output.write_text(source)
119
+ console.detail(f"wrote {output} ({len(source):,} bytes)")
120
+
121
+ if not cfg.build:
122
+ return
123
+
124
+ build_dir.mkdir(parents=True, exist_ok=True)
125
+
126
+ # Materialise inline assets (fonts, logos, images) alongside the .tex (in
127
+ # the build dir by default) so the TeX engine can locate them by the
128
+ # relative paths in the preamble.
129
+ if hasattr(tex_node, "write_inline_fonts"):
130
+ cast("HSRTReport", tex_node).write_inline_fonts(str(output.parent))
131
+ if hasattr(tex_node, "write_inline_logos"):
132
+ cast("HSRTReport", tex_node).write_inline_logos(str(output.parent))
133
+ if hasattr(tex_node, "write_inline_images"):
134
+ cast("HSRTReport", tex_node).write_inline_images(str(output.parent))
135
+
136
+ binary = ensure_tectonic(console)
137
+
138
+ job = output.stem
139
+ for pass_no in range(1, MAX_PASSES + 1):
140
+ console.step(f"Compiling (pass {pass_no})")
141
+ run_tectonic(
142
+ binary, output, build_dir, shell_escape=cfg.shell_escape, console=console
143
+ )
144
+ # Resolve glossaries after the first pass; rerun only if it changed.
145
+ if pass_no == 1 and run_makeindex(job, build_dir, console=console):
146
+ continue
147
+ break
148
+
149
+ pdf = build_dir / f"{job}.pdf"
150
+ if pdf.exists():
151
+ console.success(f"Built {pdf}")
152
+ else:
153
+ console.warn("tectonic reported success but produced no PDF")
154
+ console.hint(f"check the log in {build_dir}")
155
+
156
+
157
+ def main(argv: list[str] | None = None) -> int:
158
+ cfg = _parse_args(sys.argv[1:] if argv is None else argv)
159
+ console = Console()
160
+ try:
161
+ _run(cfg, console)
162
+ except BuildError as exc:
163
+ head, *rest = str(exc).splitlines() or [""]
164
+ console.error(head)
165
+ for line in rest:
166
+ console.detail(line)
167
+ return 1
168
+ except KeyboardInterrupt:
169
+ console.error("interrupted")
170
+ return 130
171
+ return 0
172
+
173
+
174
+ if __name__ == "__main__":
175
+ raise SystemExit(main())
@@ -0,0 +1,77 @@
1
+ """Minimal, tectonic-style terminal output.
2
+
3
+ No emojis, no progress bars - just level-tagged lines with restrained color.
4
+ Color is disabled automatically when stderr is not a TTY or ``NO_COLOR`` is set.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import sys
11
+ from typing import Final, TextIO
12
+
13
+ __all__ = ["Console"]
14
+
15
+
16
+ def _color_enabled(stream: TextIO) -> bool:
17
+ if os.environ.get("NO_COLOR"):
18
+ return False
19
+ if os.environ.get("PYTEX_FORCE_COLOR"):
20
+ return True
21
+ return bool(getattr(stream, "isatty", lambda: False)())
22
+
23
+
24
+ class _Style:
25
+ RESET: Final = "\033[0m"
26
+ BOLD: Final = "\033[1m"
27
+ DIM: Final = "\033[2m"
28
+ RED: Final = "\033[31m"
29
+ GREEN: Final = "\033[32m"
30
+ YELLOW: Final = "\033[33m"
31
+ BLUE: Final = "\033[34m"
32
+ CYAN: Final = "\033[36m"
33
+
34
+
35
+ class Console:
36
+ """Writes level-tagged status lines to ``stderr``."""
37
+
38
+ def __init__(self, stream: TextIO | None = None) -> None:
39
+ self.stream: TextIO = stream or sys.stderr
40
+ self.color: bool = _color_enabled(self.stream)
41
+
42
+ def _paint(self, text: str, *codes: str) -> str:
43
+ if not self.color or not codes:
44
+ return text
45
+ return f"{''.join(codes)}{text}{_Style.RESET}"
46
+
47
+ def _emit(self, tag: str, message: str, *codes: str) -> None:
48
+ prefix = self._paint(tag, _Style.BOLD, *codes)
49
+ self.stream.write(f"{prefix} {message}\n")
50
+ self.stream.flush()
51
+
52
+ def step(self, message: str) -> None:
53
+ """A high-level pipeline stage (e.g. 'Rendering', 'Compiling')."""
54
+ self._emit("==>", message, _Style.GREEN)
55
+
56
+ def note(self, message: str) -> None:
57
+ self._emit("note:", message, _Style.CYAN)
58
+
59
+ def warn(self, message: str) -> None:
60
+ self._emit("warning:", message, _Style.YELLOW)
61
+
62
+ def error(self, message: str) -> None:
63
+ self._emit("error:", message, _Style.RED)
64
+
65
+ def hint(self, message: str) -> None:
66
+ """A follow-up suggestion attached to a preceding warning/error."""
67
+ bullet = self._paint(" cause:", _Style.DIM)
68
+ self.stream.write(f"{bullet} {message}\n")
69
+ self.stream.flush()
70
+
71
+ def detail(self, message: str) -> None:
72
+ """An indented, dimmed continuation line."""
73
+ self.stream.write(f"{self._paint(' ' + message, _Style.DIM)}\n")
74
+ self.stream.flush()
75
+
76
+ def success(self, message: str) -> None:
77
+ self._emit("==>", message, _Style.GREEN)
@@ -0,0 +1,90 @@
1
+ """Turn an input file into a rendered LaTeX string.
2
+
3
+ Two input kinds are supported:
4
+
5
+ * ``.tex`` - wrapped in :func:`pytex.model.include.IncludeTeX` so ``\\iffalse``
6
+ ``pytex(...)`` replacements are evaluated, then rendered.
7
+ * ``.py`` - imported as a module; its module-level ``__pytex__`` value is
8
+ rendered. It must implement the :class:`pytex.interface.tex.TeX` protocol.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import importlib.util
14
+ import sys
15
+ from typing import TYPE_CHECKING, cast
16
+
17
+ from pytex.interface.tex import TeX
18
+ from pytex.model.include import IncludeTeX
19
+
20
+ from .tectonic import BuildError
21
+
22
+ __all__ = ["get_tex_node", "render_input"]
23
+
24
+ if TYPE_CHECKING:
25
+ from pathlib import Path
26
+
27
+ PYTEX_VAR = "__pytex__"
28
+
29
+
30
+ def _render_python(path: Path) -> TeX:
31
+ spec = importlib.util.spec_from_file_location(path.stem, path)
32
+ if spec is None or spec.loader is None:
33
+ raise BuildError(f"could not load Python module from {path}")
34
+
35
+ module = importlib.util.module_from_spec(spec)
36
+ # Let the module import siblings relative to its own directory.
37
+ sys.path.insert(0, str(path.resolve().parent))
38
+ try:
39
+ spec.loader.exec_module(module)
40
+ except Exception as exc:
41
+ raise BuildError(f"error while importing {path.name}: {exc}") from exc
42
+ finally:
43
+ sys.path.pop(0)
44
+
45
+ if not hasattr(module, PYTEX_VAR):
46
+ raise BuildError(f"{path.name} defines no '{PYTEX_VAR}' variable to render")
47
+
48
+ value = cast("object", getattr(module, PYTEX_VAR))
49
+ if not isinstance(value, TeX):
50
+ raise BuildError(
51
+ f"'{PYTEX_VAR}' in {path.name} is {type(value).__name__};"
52
+ + " expected a TeX node (with a '.rendered' property)"
53
+ )
54
+ return value
55
+
56
+
57
+ def _render_markdown(path: Path) -> TeX:
58
+ # Imported lazily so plain .tex/.py builds need neither marko nor hsrt.
59
+ from pytex.model.document import Document
60
+ from pytex_markdown import IncludeMarkdown
61
+ from pytex_protocol.frontmatter import split_frontmatter
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)
70
+ return Document(IncludeMarkdown(path))
71
+
72
+
73
+ def get_tex_node(path: Path) -> TeX:
74
+ """Load ``path`` and return the TeX node without rendering."""
75
+ suffix = path.suffix.lower()
76
+ if suffix == ".tex":
77
+ return IncludeTeX(path)
78
+ if suffix == ".py":
79
+ return _render_python(path)
80
+ if suffix in (".md", ".markdown"):
81
+ return _render_markdown(path)
82
+ raise BuildError(
83
+ f"unsupported input type '{suffix or path.name}'; "
84
+ + "expected .tex, .py or .md"
85
+ )
86
+
87
+
88
+ def render_input(path: Path) -> str:
89
+ """Render ``path`` to a LaTeX source string."""
90
+ return get_tex_node(path).rendered