pytex-preprocessor 0.1.0rc1__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.
- pytex/__init__.py +87 -0
- pytex/commands/__init__.py +51 -0
- pytex/commands/biblatex.py +98 -0
- pytex/commands/builtin.py +598 -0
- pytex/commands/captions.py +56 -0
- pytex/commands/cleveref.py +43 -0
- pytex/commands/colors.py +60 -0
- pytex/commands/conditionals.py +62 -0
- pytex/commands/counters.py +85 -0
- pytex/commands/definitions.py +109 -0
- pytex/commands/floats.py +93 -0
- pytex/commands/font.py +138 -0
- pytex/commands/fontawesome.py +88 -0
- pytex/commands/fontspec.py +75 -0
- pytex/commands/geometry.py +25 -0
- pytex/commands/glossaries.py +126 -0
- pytex/commands/graphics.py +68 -0
- pytex/commands/hooks.py +58 -0
- pytex/commands/hyperref.py +57 -0
- pytex/commands/lengths.py +200 -0
- pytex/commands/listings.py +63 -0
- pytex/commands/mdframed.py +43 -0
- pytex/commands/picture.py +32 -0
- pytex/commands/setspace.py +38 -0
- pytex/commands/tables.py +123 -0
- pytex/helpers/__init__.py +3 -0
- pytex/helpers/coerce.py +13 -0
- pytex/helpers/parenting.py +13 -0
- pytex/helpers/sanitize.py +54 -0
- pytex/helpers/with_package.py +61 -0
- pytex/interface/__init__.py +3 -0
- pytex/interface/control_sequence.py +29 -0
- pytex/interface/package.py +52 -0
- pytex/interface/tex.py +41 -0
- pytex/model/__init__.py +25 -0
- pytex/model/color.py +203 -0
- pytex/model/concat.py +31 -0
- pytex/model/control_sequence.py +72 -0
- pytex/model/document.py +120 -0
- pytex/model/document_class.py +29 -0
- pytex/model/empty.py +19 -0
- pytex/model/environment.py +30 -0
- pytex/model/image.py +137 -0
- pytex/model/include.py +21 -0
- pytex/model/length.py +54 -0
- pytex/model/math.py +401 -0
- pytex/model/package.py +132 -0
- pytex/model/raw.py +61 -0
- pytex/packages.py +221 -0
- pytex/registry.py +49 -0
- pytex_builder/__init__.py +8 -0
- pytex_builder/build.py +175 -0
- pytex_builder/console.py +77 -0
- pytex_builder/render.py +82 -0
- pytex_builder/tectonic.py +307 -0
- pytex_hsrtreport/__init__.py +116 -0
- pytex_hsrtreport/assets/fonts/Blender/Blender-Bold.ttf +0 -0
- pytex_hsrtreport/assets/fonts/Blender/Blender-BoldItalic.ttf +0 -0
- pytex_hsrtreport/assets/fonts/Blender/Blender-Book.ttf +0 -0
- pytex_hsrtreport/assets/fonts/Blender/Blender-BookItalic.ttf +0 -0
- pytex_hsrtreport/assets/fonts/Blender/Blender-Medium.ttf +0 -0
- pytex_hsrtreport/assets/fonts/Blender/Blender-MediumItalic.ttf +0 -0
- pytex_hsrtreport/assets/fonts/Blender/Blender-Strong.ttf +0 -0
- pytex_hsrtreport/assets/fonts/Blender/Blender-Thin.ttf +0 -0
- pytex_hsrtreport/assets/fonts/Blender/Blender-ThinItalic.ttf +0 -0
- pytex_hsrtreport/assets/fonts/DIN/DIN-Black.ttf +0 -0
- pytex_hsrtreport/assets/fonts/DIN/DIN-Bold.ttf +0 -0
- pytex_hsrtreport/assets/fonts/DIN/DIN-BoldItalic.ttf +0 -0
- pytex_hsrtreport/assets/fonts/DIN/DIN-Italic.ttf +0 -0
- pytex_hsrtreport/assets/fonts/DIN/DIN-Medium.ttf +0 -0
- pytex_hsrtreport/assets/fonts/DIN/DIN-Regular.ttf +0 -0
- pytex_hsrtreport/assets/fonts/Times New Roman.ttf +0 -0
- pytex_hsrtreport/assets/logos/ASTA.svg +79 -0
- pytex_hsrtreport/assets/logos/DUMMY.png +0 -0
- pytex_hsrtreport/assets/logos/DUMMY_FOOT.png +0 -0
- pytex_hsrtreport/assets/logos/ECHO.svg +226 -0
- pytex_hsrtreport/assets/logos/HSRT.pdf +0 -0
- pytex_hsrtreport/assets/logos/INF.pdf +0 -0
- pytex_hsrtreport/assets/logos/STUPA.pdf +0 -0
- pytex_hsrtreport/assets/logos/Skyline.pdf +0 -0
- pytex_hsrtreport/boxes.py +215 -0
- pytex_hsrtreport/citations.py +19 -0
- pytex_hsrtreport/cleveref_names.py +47 -0
- pytex_hsrtreport/colors.py +30 -0
- pytex_hsrtreport/document.py +302 -0
- pytex_hsrtreport/fonts.py +66 -0
- pytex_hsrtreport/glossary.py +61 -0
- pytex_hsrtreport/hyperref_config.py +45 -0
- pytex_hsrtreport/listings.py +90 -0
- pytex_hsrtreport/logos.py +234 -0
- pytex_hsrtreport/pagebreak.py +67 -0
- pytex_hsrtreport/pagesetup.py +33 -0
- pytex_hsrtreport/tex/pagesetup.tex +76 -0
- pytex_hsrtreport/titlepage.py +136 -0
- pytex_hsrtreport/variants.py +24 -0
- pytex_hsrtreport/voting.py +96 -0
- pytex_hsrtreport/watermark.py +63 -0
- pytex_hsrtreport/wordcount.py +33 -0
- pytex_koma/__init__.py +90 -0
- pytex_koma/commands.py +296 -0
- pytex_koma/document.py +138 -0
- pytex_markdown/__init__.py +62 -0
- pytex_markdown/convert.py +268 -0
- pytex_markdown/escape.py +11 -0
- pytex_preprocessor-0.1.0rc1.dist-info/METADATA +82 -0
- pytex_preprocessor-0.1.0rc1.dist-info/RECORD +111 -0
- pytex_preprocessor-0.1.0rc1.dist-info/WHEEL +5 -0
- pytex_preprocessor-0.1.0rc1.dist-info/entry_points.txt +2 -0
- pytex_preprocessor-0.1.0rc1.dist-info/top_level.txt +6 -0
- pytex_tikz/__init__.py +25 -0
- pytex_tikz/tikz.py +272 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from ..helpers.with_package import with_package
|
|
2
|
+
from ..interface.tex import TeX
|
|
3
|
+
from ..model.control_sequence import ControlSequence, Parameter
|
|
4
|
+
from ..model.environment import Environment
|
|
5
|
+
from ..packages import SETSPACE
|
|
6
|
+
from ..registry import Registry
|
|
7
|
+
|
|
8
|
+
__all__ = ["Doublespacing", "Onehalfspacing", "Setstretch", "Singlespacing", "Spacing"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@Registry.add
|
|
12
|
+
@with_package(SETSPACE)
|
|
13
|
+
def Setstretch(factor: str) -> TeX:
|
|
14
|
+
return ControlSequence("setstretch", (Parameter(factor),))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@Registry.add
|
|
18
|
+
@with_package(SETSPACE)
|
|
19
|
+
def Singlespacing() -> TeX:
|
|
20
|
+
return ControlSequence("singlespacing", ())
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@Registry.add
|
|
24
|
+
@with_package(SETSPACE)
|
|
25
|
+
def Onehalfspacing() -> TeX:
|
|
26
|
+
return ControlSequence("onehalfspacing", ())
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@Registry.add
|
|
30
|
+
@with_package(SETSPACE)
|
|
31
|
+
def Doublespacing() -> TeX:
|
|
32
|
+
return ControlSequence("doublespacing", ())
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@Registry.add
|
|
36
|
+
@with_package(SETSPACE)
|
|
37
|
+
def Spacing(factor: str, body: TeX | str) -> TeX:
|
|
38
|
+
return Environment("spacing", body, (Parameter(factor),))
|
pytex/commands/tables.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from ..helpers.with_package import with_package
|
|
2
|
+
from ..interface.tex import TeX
|
|
3
|
+
from ..model.control_sequence import ControlSequence, Parameter
|
|
4
|
+
from ..model.environment import Environment
|
|
5
|
+
from ..packages import BOOKTABS, LONGTABLE, MULTIROW, TABULARX
|
|
6
|
+
from ..registry import Registry
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Arraybackslash",
|
|
10
|
+
"Arraystretch",
|
|
11
|
+
"Bottomrule",
|
|
12
|
+
"Cline",
|
|
13
|
+
"Cmidrule",
|
|
14
|
+
"Hline",
|
|
15
|
+
"Longtable",
|
|
16
|
+
"Midrule",
|
|
17
|
+
"Multicolumn",
|
|
18
|
+
"Multirow",
|
|
19
|
+
"Newcolumntype",
|
|
20
|
+
"Tabular",
|
|
21
|
+
"Tabularx",
|
|
22
|
+
"Toprule",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@Registry.add
|
|
27
|
+
def Tabular(spec: str, body: TeX | str) -> TeX:
|
|
28
|
+
return Environment("tabular", body, (Parameter(spec),))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@Registry.add
|
|
32
|
+
@with_package(TABULARX)
|
|
33
|
+
def Tabularx(width: str, spec: str, body: TeX | str) -> TeX:
|
|
34
|
+
return Environment("tabularx", body, (Parameter(width), Parameter(spec)))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@Registry.add
|
|
38
|
+
@with_package(LONGTABLE)
|
|
39
|
+
def Longtable(spec: str, body: TeX | str) -> TeX:
|
|
40
|
+
return Environment("longtable", body, (Parameter(spec),))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@Registry.add
|
|
44
|
+
def Multicolumn(cols: int, align: str, body: TeX | str) -> TeX:
|
|
45
|
+
return ControlSequence(
|
|
46
|
+
"multicolumn",
|
|
47
|
+
(Parameter(str(cols)), Parameter(align), Parameter(body)),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@Registry.add
|
|
52
|
+
@with_package(MULTIROW)
|
|
53
|
+
def Multirow(rows: int, width: str, body: TeX | str) -> TeX:
|
|
54
|
+
return ControlSequence(
|
|
55
|
+
"multirow",
|
|
56
|
+
(Parameter(str(rows)), Parameter(width), Parameter(body)),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@Registry.add
|
|
61
|
+
def Hline() -> TeX:
|
|
62
|
+
return ControlSequence("hline", ())
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@Registry.add
|
|
66
|
+
def Cline(spec: str) -> TeX:
|
|
67
|
+
return ControlSequence("cline", (Parameter(spec),))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@Registry.add
|
|
71
|
+
@with_package(BOOKTABS)
|
|
72
|
+
def Toprule() -> TeX:
|
|
73
|
+
return ControlSequence("toprule", ())
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@Registry.add
|
|
77
|
+
@with_package(BOOKTABS)
|
|
78
|
+
def Midrule() -> TeX:
|
|
79
|
+
return ControlSequence("midrule", ())
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@Registry.add
|
|
83
|
+
@with_package(BOOKTABS)
|
|
84
|
+
def Bottomrule() -> TeX:
|
|
85
|
+
return ControlSequence("bottomrule", ())
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@Registry.add
|
|
89
|
+
@with_package(BOOKTABS)
|
|
90
|
+
def Cmidrule(spec: str, trim: str | None = None) -> TeX:
|
|
91
|
+
if trim is None:
|
|
92
|
+
return ControlSequence("cmidrule", (Parameter(spec),))
|
|
93
|
+
return ControlSequence(
|
|
94
|
+
"cmidrule",
|
|
95
|
+
(Parameter(trim, optional=True), Parameter(spec)),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@Registry.add
|
|
100
|
+
def Arraybackslash() -> TeX:
|
|
101
|
+
return ControlSequence("arraybackslash", ())
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@Registry.add
|
|
105
|
+
def Arraystretch(factor: str) -> TeX:
|
|
106
|
+
return ControlSequence("arraystretch", (Parameter(factor),))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@Registry.add
|
|
110
|
+
def Newcolumntype(name: str, arity: int | None, spec: str) -> TeX:
|
|
111
|
+
if arity is None:
|
|
112
|
+
return ControlSequence(
|
|
113
|
+
"newcolumntype",
|
|
114
|
+
(Parameter(name), Parameter(spec)),
|
|
115
|
+
)
|
|
116
|
+
return ControlSequence(
|
|
117
|
+
"newcolumntype",
|
|
118
|
+
(
|
|
119
|
+
Parameter(name),
|
|
120
|
+
Parameter(str(arity), optional=True),
|
|
121
|
+
Parameter(spec),
|
|
122
|
+
),
|
|
123
|
+
)
|
pytex/helpers/coerce.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from ..interface.tex import TeX
|
|
2
|
+
from ..model.raw import Raw
|
|
3
|
+
from ..registry import Registry
|
|
4
|
+
|
|
5
|
+
__all__ = ["coerce_tex"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@Registry.add
|
|
9
|
+
def coerce_tex(value: TeX | str) -> TeX:
|
|
10
|
+
if isinstance(value, TeX):
|
|
11
|
+
return value
|
|
12
|
+
|
|
13
|
+
return Raw(value)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
|
|
3
|
+
from ..interface.tex import TeX
|
|
4
|
+
|
|
5
|
+
__all__ = ["attach"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def attach(parent: TeX, *children: object) -> None:
|
|
9
|
+
"""Set `_parent` on each TeX child to `parent`. Non-TeX children are skipped."""
|
|
10
|
+
for child in children:
|
|
11
|
+
if isinstance(child, TeX):
|
|
12
|
+
with suppress(AttributeError, TypeError):
|
|
13
|
+
object.__setattr__(child, "_parent", parent)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Sanitize untrusted text before it enters a document.
|
|
2
|
+
|
|
3
|
+
Two independent concerns:
|
|
4
|
+
|
|
5
|
+
* ``tex`` - escape LaTeX-special characters so the text renders literally
|
|
6
|
+
(pytex itself does no escaping; raw strings pass straight through).
|
|
7
|
+
* ``pytex`` - disable PyTeX replacement so an embedded
|
|
8
|
+
``\\iffalse{pytex(...)}\\fi`` marker is never evaluated. The marker can run
|
|
9
|
+
arbitrary Python, so this matters for content from outside the program.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import TYPE_CHECKING, Final
|
|
15
|
+
|
|
16
|
+
from ..model.raw import Raw
|
|
17
|
+
from ..registry import Registry
|
|
18
|
+
|
|
19
|
+
__all__ = ["Sanitize", "escape_latex"]
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from ..interface.tex import TeX
|
|
23
|
+
|
|
24
|
+
# Build the result char-by-char so braces introduced by a replacement are
|
|
25
|
+
# never themselves re-escaped.
|
|
26
|
+
ESCAPES: Final[dict[str, str]] = {
|
|
27
|
+
"\\": r"\textbackslash{}",
|
|
28
|
+
"&": r"\&",
|
|
29
|
+
"%": r"\%",
|
|
30
|
+
"$": r"\$",
|
|
31
|
+
"#": r"\#",
|
|
32
|
+
"_": r"\_",
|
|
33
|
+
"{": r"\{",
|
|
34
|
+
"}": r"\}",
|
|
35
|
+
"~": r"\textasciitilde{}",
|
|
36
|
+
"^": r"\textasciicircum{}",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def escape_latex(text: str) -> str:
|
|
41
|
+
"""Return ``text`` with LaTeX-special characters escaped."""
|
|
42
|
+
return "".join(ESCAPES.get(ch, ch) for ch in text)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@Registry.add
|
|
46
|
+
def Sanitize(content: str, pytex: bool = True, tex: bool = True) -> TeX:
|
|
47
|
+
"""Wrap ``content`` as a ``TeX`` node, neutralising unsafe input.
|
|
48
|
+
|
|
49
|
+
* ``tex=True`` escapes LaTeX-special characters.
|
|
50
|
+
* ``pytex=True`` prevents any ``\\iffalse{pytex(...)}\\fi`` marker in the
|
|
51
|
+
content from being evaluated.
|
|
52
|
+
"""
|
|
53
|
+
text = escape_latex(content) if tex else content
|
|
54
|
+
return Raw(text, allow_replacements=not pytex)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from typing import override
|
|
5
|
+
|
|
6
|
+
from pytex.interface.package import PackageProtocol
|
|
7
|
+
|
|
8
|
+
from ..interface.tex import TeX
|
|
9
|
+
from ..model.package import Package
|
|
10
|
+
from ..registry import Registry
|
|
11
|
+
from .parenting import attach
|
|
12
|
+
|
|
13
|
+
__all__ = ["WithPackage", "coerce_package", "with_package"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@Registry.add
|
|
17
|
+
def coerce_package(pkg: Package | str) -> Package:
|
|
18
|
+
if isinstance(pkg, Package):
|
|
19
|
+
return pkg
|
|
20
|
+
|
|
21
|
+
return Package(pkg)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@Registry.add
|
|
25
|
+
@dataclass
|
|
26
|
+
class WithPackage[T: TeX](TeX):
|
|
27
|
+
child: T
|
|
28
|
+
package: Package | str
|
|
29
|
+
_parent: "TeX | None" = field(default=None, init=False, compare=False, repr=False)
|
|
30
|
+
|
|
31
|
+
def __post_init__(self) -> None:
|
|
32
|
+
attach(self, self.child)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
@override
|
|
36
|
+
def rendered(self) -> str:
|
|
37
|
+
return self.child.rendered
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
@override
|
|
41
|
+
def children(self) -> tuple[TeX, ...]:
|
|
42
|
+
return (self.child,)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
@override
|
|
46
|
+
def requires(self) -> frozenset[PackageProtocol] | None:
|
|
47
|
+
return frozenset(
|
|
48
|
+
{coerce_package(self.package)}
|
|
49
|
+
| (self.child.requires or set[PackageProtocol]())
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def with_package[C: Callable[..., TeX]](pkg: Package | str) -> Callable[[C], C]:
|
|
54
|
+
def decorator(func: C) -> C:
|
|
55
|
+
@wraps(func)
|
|
56
|
+
def wrapper(*args: object, **kwargs: object) -> TeX:
|
|
57
|
+
return WithPackage(func(*args, **kwargs), pkg)
|
|
58
|
+
|
|
59
|
+
return wrapper # pyright: ignore[reportReturnType]
|
|
60
|
+
|
|
61
|
+
return decorator
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import Protocol, runtime_checkable
|
|
2
|
+
|
|
3
|
+
from ..model.empty import EmptyTeX
|
|
4
|
+
from .tex import TeX
|
|
5
|
+
|
|
6
|
+
__all__ = ["ControlSequenceProtocol", "ParameterProtocol"]
|
|
7
|
+
|
|
8
|
+
type ParameterType = TeX | str | dict[str, str]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@runtime_checkable
|
|
12
|
+
class ParameterProtocol[T: ParameterType = ParameterType](TeX, Protocol):
|
|
13
|
+
@property
|
|
14
|
+
def optional(self) -> bool: ...
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def value(self) -> T: ...
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
type Parameters = tuple[ParameterProtocol | EmptyTeX, ...] | None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@runtime_checkable
|
|
24
|
+
class ControlSequenceProtocol[P: Parameters](TeX, Protocol):
|
|
25
|
+
@property
|
|
26
|
+
def name(self) -> str: ...
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def params(self) -> P: ...
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from typing import Protocol, Self, override, runtime_checkable
|
|
2
|
+
|
|
3
|
+
__all__ = ["PackageProtocol"]
|
|
4
|
+
|
|
5
|
+
type PackageOption = str | tuple[str, str]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@runtime_checkable
|
|
9
|
+
class PackageProtocol(Protocol):
|
|
10
|
+
@property
|
|
11
|
+
def name(self) -> str: ...
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def after(self) -> frozenset[Self]: ...
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def incompatible(self) -> frozenset[Self]: ...
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def options(self) -> frozenset[PackageOption]: ...
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def rendered(self) -> str:
|
|
24
|
+
"""Render this object to a valid LaTeX-String"""
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def children(self) -> tuple[Self, ...]:
|
|
29
|
+
return ()
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def requires(self) -> frozenset[Self]:
|
|
33
|
+
return self.after
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def parent(self) -> Self | None:
|
|
37
|
+
"""Parent node in the document tree, or None if root/detached."""
|
|
38
|
+
return getattr(self, "_parent", None)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def parents(self) -> tuple[Self, ...]:
|
|
42
|
+
"""Chain of ancestors from immediate parent up to root."""
|
|
43
|
+
out: list[Self] = []
|
|
44
|
+
cur = self.parent
|
|
45
|
+
while cur is not None:
|
|
46
|
+
out.append(cur)
|
|
47
|
+
cur = cur.parent
|
|
48
|
+
return tuple(out)
|
|
49
|
+
|
|
50
|
+
@override
|
|
51
|
+
def __str__(self) -> str:
|
|
52
|
+
return self.rendered
|
pytex/interface/tex.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from typing import Protocol, override, runtime_checkable
|
|
2
|
+
|
|
3
|
+
from .package import PackageProtocol
|
|
4
|
+
|
|
5
|
+
__all__ = ["TeX"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@runtime_checkable
|
|
9
|
+
class TeX(Protocol):
|
|
10
|
+
@property
|
|
11
|
+
def rendered(self) -> str:
|
|
12
|
+
"""Render this Node to a valid LaTeX-String"""
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def children(self) -> tuple["TeX", ...]:
|
|
17
|
+
"""Children of the Node"""
|
|
18
|
+
return ()
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def requires(self) -> frozenset[PackageProtocol] | None:
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def parent(self) -> "TeX | None":
|
|
26
|
+
"""Parent node in the document tree, or None if root/detached."""
|
|
27
|
+
return getattr(self, "_parent", None)
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def parents(self) -> tuple["TeX", ...]:
|
|
31
|
+
"""Chain of ancestors from immediate parent up to root."""
|
|
32
|
+
out: list[TeX] = []
|
|
33
|
+
cur = self.parent
|
|
34
|
+
while cur is not None:
|
|
35
|
+
out.append(cur)
|
|
36
|
+
cur = cur.parent
|
|
37
|
+
return tuple(out)
|
|
38
|
+
|
|
39
|
+
@override
|
|
40
|
+
def __str__(self) -> str:
|
|
41
|
+
return self.rendered
|
pytex/model/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# pyright: reportUnsupportedDunderAll=false
|
|
2
|
+
"""Model subpackage.
|
|
3
|
+
|
|
4
|
+
Modules are not eager-imported here to avoid a circular import:
|
|
5
|
+
`pytex/__init__.py` → `pytex.packages` → `pytex.model.package` →
|
|
6
|
+
`pytex.model.__init__` → `pytex.model.math` → `pytex.packages` (still loading).
|
|
7
|
+
|
|
8
|
+
Import submodules directly: `from pytex.model.math import Frac`.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"color",
|
|
13
|
+
"concat",
|
|
14
|
+
"control_sequence",
|
|
15
|
+
"document",
|
|
16
|
+
"document_class",
|
|
17
|
+
"empty",
|
|
18
|
+
"environment",
|
|
19
|
+
"image",
|
|
20
|
+
"include",
|
|
21
|
+
"length",
|
|
22
|
+
"math",
|
|
23
|
+
"package",
|
|
24
|
+
"raw",
|
|
25
|
+
]
|
pytex/model/color.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import override
|
|
3
|
+
|
|
4
|
+
from ..interface.package import PackageProtocol
|
|
5
|
+
from ..interface.tex import TeX
|
|
6
|
+
from ..registry import Registry
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Color",
|
|
10
|
+
"ColorSpec",
|
|
11
|
+
"collect_colors",
|
|
12
|
+
"is_known_color_name",
|
|
13
|
+
"register_named_color",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
NAMED_COLORS: set[str] = {
|
|
17
|
+
"black",
|
|
18
|
+
"white",
|
|
19
|
+
"red",
|
|
20
|
+
"green",
|
|
21
|
+
"blue",
|
|
22
|
+
"cyan",
|
|
23
|
+
"magenta",
|
|
24
|
+
"yellow",
|
|
25
|
+
"gray",
|
|
26
|
+
"lightgray",
|
|
27
|
+
"darkgray",
|
|
28
|
+
"orange",
|
|
29
|
+
"violet",
|
|
30
|
+
"purple",
|
|
31
|
+
"brown",
|
|
32
|
+
"pink",
|
|
33
|
+
"olive",
|
|
34
|
+
"lime",
|
|
35
|
+
"teal",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def register_named_color(name: str) -> None:
|
|
40
|
+
"""Whitelist a name so `Color.named(name)` accepts it."""
|
|
41
|
+
NAMED_COLORS.add(name)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_known_color_name(name: str) -> bool:
|
|
45
|
+
return name in NAMED_COLORS
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class ColorSpec:
|
|
50
|
+
"""Frozen colour identity: `model` + `value` for `\\definecolor`."""
|
|
51
|
+
|
|
52
|
+
model: str
|
|
53
|
+
value: str
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@Registry.add
|
|
57
|
+
class Color(TeX):
|
|
58
|
+
"""Type-safe colour reference.
|
|
59
|
+
|
|
60
|
+
Constructors:
|
|
61
|
+
Color("blue") # registered name
|
|
62
|
+
Color("#FF0000") # hex
|
|
63
|
+
Color((255, 0, 0)) # rgb 0..255
|
|
64
|
+
Color((1.0, 0.0, 0.0)) # rgb 0..1
|
|
65
|
+
Color.hex("FF0000")
|
|
66
|
+
Color.rgb255(255, 0, 0)
|
|
67
|
+
Color.rgb(1.0, 0.0, 0.0)
|
|
68
|
+
Color.named("blue")
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
name: str
|
|
72
|
+
spec: ColorSpec | None
|
|
73
|
+
_parent: "TeX | None"
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
value: "str | tuple[int, int, int] | tuple[float, float, float] | None" = None,
|
|
78
|
+
*,
|
|
79
|
+
name: str | None = None,
|
|
80
|
+
spec: ColorSpec | None = None,
|
|
81
|
+
) -> None:
|
|
82
|
+
if value is not None:
|
|
83
|
+
resolved_name, resolved_spec = _from_overload(value)
|
|
84
|
+
self.name = resolved_name
|
|
85
|
+
self.spec = resolved_spec
|
|
86
|
+
elif name is not None:
|
|
87
|
+
self.name = name
|
|
88
|
+
self.spec = spec
|
|
89
|
+
else:
|
|
90
|
+
raise TypeError("Color() requires `value` or `name`")
|
|
91
|
+
self._parent = None
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def hex(cls, value: str, name: str | None = None) -> "Color":
|
|
95
|
+
clean = value.lstrip("#").upper()
|
|
96
|
+
if len(clean) != 6 or any(c not in "0123456789ABCDEF" for c in clean):
|
|
97
|
+
raise ValueError(f"invalid hex colour: {value!r}")
|
|
98
|
+
return cls(name=name or f"c{clean}", spec=ColorSpec("HTML", clean))
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def rgb255(cls, r: int, g: int, b: int, name: str | None = None) -> "Color":
|
|
102
|
+
for v in (r, g, b):
|
|
103
|
+
if not 0 <= v <= 255:
|
|
104
|
+
raise ValueError(f"rgb255 component out of range: {v}")
|
|
105
|
+
return cls(
|
|
106
|
+
name=name or f"c{r:03d}{g:03d}{b:03d}",
|
|
107
|
+
spec=ColorSpec("RGB", f"{r},{g},{b}"),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def rgb(cls, r: float, g: float, b: float, name: str | None = None) -> "Color":
|
|
112
|
+
for v in (r, g, b):
|
|
113
|
+
if not 0.0 <= float(v) <= 1.0:
|
|
114
|
+
raise ValueError(f"rgb component out of range [0,1]: {v}")
|
|
115
|
+
return cls(
|
|
116
|
+
name=name or f"crgb{int(r * 255):03d}{int(g * 255):03d}{int(b * 255):03d}",
|
|
117
|
+
spec=ColorSpec("rgb", f"{r},{g},{b}"),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
@classmethod
|
|
121
|
+
def named(cls, name: str) -> "Color":
|
|
122
|
+
if not is_known_color_name(name):
|
|
123
|
+
raise ValueError(
|
|
124
|
+
f"unknown colour name {name!r}; register via register_named_color"
|
|
125
|
+
)
|
|
126
|
+
return cls(name=name, spec=None)
|
|
127
|
+
|
|
128
|
+
def tint(self, percent: int) -> "Color":
|
|
129
|
+
"""xcolor tint: `<self>!<percent>` (e.g. `blue!20`)."""
|
|
130
|
+
return Color(name=f"{self.name}!{percent}", spec=None)
|
|
131
|
+
|
|
132
|
+
def mix(self, other: "Color", percent: int = 50) -> "Color":
|
|
133
|
+
return Color(
|
|
134
|
+
name=f"{self.name}!{percent}!{other.name}",
|
|
135
|
+
spec=None,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def __or__(self, other: "Color") -> "Color":
|
|
139
|
+
return self.mix(other)
|
|
140
|
+
|
|
141
|
+
@override
|
|
142
|
+
def __eq__(self, other: object) -> bool:
|
|
143
|
+
return (
|
|
144
|
+
isinstance(other, Color)
|
|
145
|
+
and self.name == other.name
|
|
146
|
+
and self.spec == other.spec
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
@override
|
|
150
|
+
def __hash__(self) -> int:
|
|
151
|
+
return hash((self.name, self.spec))
|
|
152
|
+
|
|
153
|
+
@override
|
|
154
|
+
def __repr__(self) -> str:
|
|
155
|
+
return f"Color(name={self.name!r}, spec={self.spec!r})"
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
@override
|
|
159
|
+
def rendered(self) -> str:
|
|
160
|
+
return self.name
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
@override
|
|
164
|
+
def requires(self) -> frozenset[PackageProtocol]:
|
|
165
|
+
from ..packages import XCOLOR
|
|
166
|
+
|
|
167
|
+
return frozenset({XCOLOR})
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _from_overload(
|
|
171
|
+
value: "str | tuple[int, int, int] | tuple[float, float, float]",
|
|
172
|
+
) -> tuple[str, ColorSpec | None]:
|
|
173
|
+
if isinstance(value, str):
|
|
174
|
+
if value.startswith("#"):
|
|
175
|
+
c = Color.hex(value)
|
|
176
|
+
return c.name, c.spec
|
|
177
|
+
if not is_known_color_name(value):
|
|
178
|
+
raise ValueError(f"unknown colour name {value!r}")
|
|
179
|
+
return value, None
|
|
180
|
+
if len(value) == 3:
|
|
181
|
+
if all(type(v) is int for v in value):
|
|
182
|
+
r, g, b = value
|
|
183
|
+
c = Color.rgb255(int(r), int(g), int(b))
|
|
184
|
+
return c.name, c.spec
|
|
185
|
+
if all(isinstance(v, float) for v in value):
|
|
186
|
+
r, g, b = value
|
|
187
|
+
c = Color.rgb(float(r), float(g), float(b))
|
|
188
|
+
return c.name, c.spec
|
|
189
|
+
raise TypeError(f"cannot construct Color from {value!r}")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def collect_colors(root: TeX) -> tuple[Color, ...]:
|
|
193
|
+
"""Walk a TeX tree, return all unique `Color` instances with a `spec`."""
|
|
194
|
+
seen: dict[str, Color] = {}
|
|
195
|
+
|
|
196
|
+
def walk(node: TeX) -> None:
|
|
197
|
+
if isinstance(node, Color) and node.spec is not None and node.name not in seen:
|
|
198
|
+
seen[node.name] = node
|
|
199
|
+
for child in node.children or ():
|
|
200
|
+
walk(child)
|
|
201
|
+
|
|
202
|
+
walk(root)
|
|
203
|
+
return tuple(seen.values())
|
pytex/model/concat.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Final, override
|
|
3
|
+
|
|
4
|
+
from ..helpers.coerce import coerce_tex
|
|
5
|
+
from ..helpers.parenting import attach
|
|
6
|
+
from ..interface.tex import TeX
|
|
7
|
+
from ..registry import Registry
|
|
8
|
+
|
|
9
|
+
__all__ = ["Concat"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@Registry.add
|
|
13
|
+
@dataclass(frozen=True, init=False)
|
|
14
|
+
class Concat(TeX):
|
|
15
|
+
elements: Final[tuple[TeX]]
|
|
16
|
+
|
|
17
|
+
def __init__(self, *elements: TeX | str) -> None:
|
|
18
|
+
coerced = tuple(coerce_tex(e) for e in elements)
|
|
19
|
+
object.__setattr__(self, "elements", coerced)
|
|
20
|
+
object.__setattr__(self, "_parent", None)
|
|
21
|
+
attach(self, *coerced)
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
@override
|
|
25
|
+
def children(self) -> tuple[TeX, ...]:
|
|
26
|
+
return self.elements
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
@override
|
|
30
|
+
def rendered(self) -> str:
|
|
31
|
+
return "".join(str(e) for e in self.elements)
|