tytable 0.6.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.
- tytable/__init__.py +24 -0
- tytable/_colors.py +212 -0
- tytable/_constants.py +28 -0
- tytable/_directives.py +114 -0
- tytable/_escape.py +58 -0
- tytable/_format.py +282 -0
- tytable/_groups.py +158 -0
- tytable/_images.py +270 -0
- tytable/_indices.py +226 -0
- tytable/_render_ascii.py +60 -0
- tytable/_render_html.py +227 -0
- tytable/_render_typst.py +425 -0
- tytable/_renderer.py +19 -0
- tytable/_resolve.py +375 -0
- tytable/_style_markup.py +155 -0
- tytable/_styling.py +308 -0
- tytable/_themes.py +143 -0
- tytable/_tytable.py +1074 -0
- tytable/_utils.py +21 -0
- tytable/_version.py +24 -0
- tytable/py.typed +0 -0
- tytable-0.6.0.dist-info/METADATA +115 -0
- tytable-0.6.0.dist-info/RECORD +25 -0
- tytable-0.6.0.dist-info/WHEEL +4 -0
- tytable-0.6.0.dist-info/licenses/LICENSE +595 -0
tytable/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Public exports for :mod:`tytable`.
|
|
2
|
+
|
|
3
|
+
Besides the :func:`tt` factory and :class:`TinyTable`, the package exposes
|
|
4
|
+
:data:`THEMES`, the registry of built-in theme callables.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ._themes import THEMES
|
|
8
|
+
from ._tytable import TinyTable, tt
|
|
9
|
+
|
|
10
|
+
#: Built-in theme registry mapping public names to ``theme(table)`` callables.
|
|
11
|
+
#: Pass a key to ``tt(theme=...)`` / ``TinyTable.theme()``, or inspect and call
|
|
12
|
+
#: a registry value when building a custom theme.
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from ._version import __version__
|
|
16
|
+
except ImportError:
|
|
17
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
__version__ = version("tytable")
|
|
21
|
+
except PackageNotFoundError:
|
|
22
|
+
__version__ = "0.0.0"
|
|
23
|
+
|
|
24
|
+
__all__ = ["tt", "TinyTable", "THEMES"]
|
tytable/_colors.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Color parsing → Typst color expressions. See guide 05 §8."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
|
|
8
|
+
_HEX_RE = re.compile(r"^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$")
|
|
9
|
+
_COLOR_FUNCTION_RE = re.compile(
|
|
10
|
+
r"^(?:rgb|luma|oklab|oklch|hsl|hsv)\([A-Za-z0-9.%+/,\s-]*\)$",
|
|
11
|
+
re.IGNORECASE,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
_NAMED_COLORS: dict[str, str] = {
|
|
15
|
+
"aliceblue": "#f0f8ff",
|
|
16
|
+
"antiquewhite": "#faebd7",
|
|
17
|
+
"aqua": "#00ffff",
|
|
18
|
+
"aquamarine": "#7fffd4",
|
|
19
|
+
"azure": "#f0ffff",
|
|
20
|
+
"beige": "#f5f5dc",
|
|
21
|
+
"bisque": "#ffe4c4",
|
|
22
|
+
"blanchedalmond": "#ffebcd",
|
|
23
|
+
"blue": "#0000ff",
|
|
24
|
+
"blueviolet": "#8a2be2",
|
|
25
|
+
"brown": "#a52a2a",
|
|
26
|
+
"burlywood": "#deb887",
|
|
27
|
+
"cadetblue": "#5f9ea0",
|
|
28
|
+
"chartreuse": "#7fff00",
|
|
29
|
+
"chocolate": "#d2691e",
|
|
30
|
+
"coral": "#ff7f50",
|
|
31
|
+
"cornflowerblue": "#6495ed",
|
|
32
|
+
"cornsilk": "#fff8dc",
|
|
33
|
+
"crimson": "#dc143c",
|
|
34
|
+
"cyan": "#00ffff",
|
|
35
|
+
"darkblue": "#00008b",
|
|
36
|
+
"darkcyan": "#008b8b",
|
|
37
|
+
"darkgoldenrod": "#b8860b",
|
|
38
|
+
"darkgray": "#a9a9a9",
|
|
39
|
+
"darkgrey": "#a9a9a9",
|
|
40
|
+
"darkgreen": "#006400",
|
|
41
|
+
"darkkhaki": "#bdb76b",
|
|
42
|
+
"darkmagenta": "#8b008b",
|
|
43
|
+
"darkolivegreen": "#556b2f",
|
|
44
|
+
"darkorange": "#ff8c00",
|
|
45
|
+
"darkorchid": "#9932cc",
|
|
46
|
+
"darkred": "#8b0000",
|
|
47
|
+
"darksalmon": "#e9967a",
|
|
48
|
+
"darkseagreen": "#8fbc8f",
|
|
49
|
+
"darkslateblue": "#483d8b",
|
|
50
|
+
"darkslategray": "#2f4f4f",
|
|
51
|
+
"darkslategrey": "#2f4f4f",
|
|
52
|
+
"darkturquoise": "#00ced1",
|
|
53
|
+
"darkviolet": "#9400d3",
|
|
54
|
+
"deeppink": "#ff1493",
|
|
55
|
+
"deepskyblue": "#00bfff",
|
|
56
|
+
"dimgray": "#696969",
|
|
57
|
+
"dimgrey": "#696969",
|
|
58
|
+
"dodgerblue": "#1e90ff",
|
|
59
|
+
"firebrick": "#b22222",
|
|
60
|
+
"floralwhite": "#fffaf0",
|
|
61
|
+
"forestgreen": "#228b22",
|
|
62
|
+
"fuchsia": "#ff00ff",
|
|
63
|
+
"gainsboro": "#dcdcdc",
|
|
64
|
+
"ghostwhite": "#f8f8ff",
|
|
65
|
+
"gold": "#ffd700",
|
|
66
|
+
"goldenrod": "#daa520",
|
|
67
|
+
"gray": "#808080",
|
|
68
|
+
"grey": "#808080",
|
|
69
|
+
"green": "#008000",
|
|
70
|
+
"greenyellow": "#adff2f",
|
|
71
|
+
"honeydew": "#f0fff0",
|
|
72
|
+
"hotpink": "#ff69b4",
|
|
73
|
+
"indianred": "#cd5c5c",
|
|
74
|
+
"indigo": "#4b0082",
|
|
75
|
+
"ivory": "#fffff0",
|
|
76
|
+
"khaki": "#f0e68c",
|
|
77
|
+
"lavender": "#e6e6fa",
|
|
78
|
+
"lavenderblush": "#fff0f5",
|
|
79
|
+
"lawngreen": "#7cfc00",
|
|
80
|
+
"lemonchiffon": "#fffacd",
|
|
81
|
+
"lightblue": "#add8e6",
|
|
82
|
+
"lightcoral": "#f08080",
|
|
83
|
+
"lightcyan": "#e0ffff",
|
|
84
|
+
"lightgoldenrodyellow": "#fafad2",
|
|
85
|
+
"lightgray": "#d3d3d3",
|
|
86
|
+
"lightgrey": "#d3d3d3",
|
|
87
|
+
"lightgreen": "#90ee90",
|
|
88
|
+
"lightpink": "#ffb6c1",
|
|
89
|
+
"lightsalmon": "#ffa07a",
|
|
90
|
+
"lightseagreen": "#20b2aa",
|
|
91
|
+
"lightskyblue": "#87cefa",
|
|
92
|
+
"lightslategray": "#778899",
|
|
93
|
+
"lightslategrey": "#778899",
|
|
94
|
+
"lightsteelblue": "#b0c4de",
|
|
95
|
+
"lightyellow": "#ffffe0",
|
|
96
|
+
"lime": "#00ff00",
|
|
97
|
+
"limegreen": "#32cd32",
|
|
98
|
+
"linen": "#faf0e6",
|
|
99
|
+
"magenta": "#ff00ff",
|
|
100
|
+
"maroon": "#800000",
|
|
101
|
+
"mediumaquamarine": "#66cdaa",
|
|
102
|
+
"mediumblue": "#0000cd",
|
|
103
|
+
"mediumorchid": "#ba55d3",
|
|
104
|
+
"mediumpurple": "#9370db",
|
|
105
|
+
"mediumseagreen": "#3cb371",
|
|
106
|
+
"mediumslateblue": "#7b68ee",
|
|
107
|
+
"mediumspringgreen": "#00fa9a",
|
|
108
|
+
"mediumturquoise": "#48d1cc",
|
|
109
|
+
"mediumvioletred": "#c71585",
|
|
110
|
+
"midnightblue": "#191970",
|
|
111
|
+
"mintcream": "#f5fffa",
|
|
112
|
+
"mistyrose": "#ffe4e1",
|
|
113
|
+
"moccasin": "#ffe4b5",
|
|
114
|
+
"navajowhite": "#ffdead",
|
|
115
|
+
"navy": "#000080",
|
|
116
|
+
"oldlace": "#fdf5e6",
|
|
117
|
+
"olive": "#808000",
|
|
118
|
+
"olivedrab": "#6b8e23",
|
|
119
|
+
"orange": "#ffa500",
|
|
120
|
+
"orangered": "#ff4500",
|
|
121
|
+
"orchid": "#da70d6",
|
|
122
|
+
"palegoldenrod": "#eee8aa",
|
|
123
|
+
"palegreen": "#98fb98",
|
|
124
|
+
"paleturquoise": "#afeeee",
|
|
125
|
+
"palevioletred": "#db7093",
|
|
126
|
+
"papayawhip": "#ffefd5",
|
|
127
|
+
"peachpuff": "#ffdab8",
|
|
128
|
+
"peru": "#cd853f",
|
|
129
|
+
"pink": "#ffc0cb",
|
|
130
|
+
"plum": "#dda0dd",
|
|
131
|
+
"powderblue": "#b0e0e6",
|
|
132
|
+
"purple": "#800080",
|
|
133
|
+
"rebeccapurple": "#663399",
|
|
134
|
+
"red": "#ff0000",
|
|
135
|
+
"rosybrown": "#bc8f8f",
|
|
136
|
+
"royalblue": "#4169e1",
|
|
137
|
+
"saddlebrown": "#8b4513",
|
|
138
|
+
"salmon": "#fa8072",
|
|
139
|
+
"sandybrown": "#f4a460",
|
|
140
|
+
"seagreen": "#2e8b57",
|
|
141
|
+
"seashell": "#fff5ee",
|
|
142
|
+
"sienna": "#a0522d",
|
|
143
|
+
"silver": "#c0c0c0",
|
|
144
|
+
"skyblue": "#87ceeb",
|
|
145
|
+
"slateblue": "#6a5acd",
|
|
146
|
+
"slategray": "#708090",
|
|
147
|
+
"slategrey": "#708090",
|
|
148
|
+
"snow": "#fffafa",
|
|
149
|
+
"springgreen": "#00ff7f",
|
|
150
|
+
"steelblue": "#4682b4",
|
|
151
|
+
"tan": "#d2b48c",
|
|
152
|
+
"teal": "#008080",
|
|
153
|
+
"thistle": "#d8bfd8",
|
|
154
|
+
"tomato": "#ff6347",
|
|
155
|
+
"turquoise": "#40e0d0",
|
|
156
|
+
"violet": "#ee82ee",
|
|
157
|
+
"wheat": "#f5deb3",
|
|
158
|
+
"whitesmoke": "#f5f5f5",
|
|
159
|
+
"yellow": "#ffff00",
|
|
160
|
+
"yellowgreen": "#9acd32",
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@lru_cache(maxsize=256)
|
|
165
|
+
def color_to_typst(color: str) -> str:
|
|
166
|
+
"""
|
|
167
|
+
Map a user color spec to a Typst color expression (guide 05 §8).
|
|
168
|
+
|
|
169
|
+
- "#RGB"/"#RRGGBB" -> rgb("#rrggbb") (normalize #RGB → #RRGGBB, lowercase)
|
|
170
|
+
- "#RGBA"/"#RRGGBBAA" -> rgb("#rrggbbaa") (normalize #RGBA → #RRGGBBAA, lowercase)
|
|
171
|
+
- "black"/"white" -> black / white (Typst built-ins)
|
|
172
|
+
- named -> rgb("#...") via the bundled dict
|
|
173
|
+
- safe color expression -> pass through unchanged
|
|
174
|
+
|
|
175
|
+
Raw expressions are limited to a color name or one of Typst's color
|
|
176
|
+
constructors with scalar arguments. This keeps useful expressions such as
|
|
177
|
+
``luma(50%)`` while preventing a value from breaking out of the generated
|
|
178
|
+
style signature.
|
|
179
|
+
"""
|
|
180
|
+
if not isinstance(color, str):
|
|
181
|
+
return str(color)
|
|
182
|
+
c = color.strip()
|
|
183
|
+
low = c.lower()
|
|
184
|
+
if low == "black":
|
|
185
|
+
return "black"
|
|
186
|
+
if low == "white":
|
|
187
|
+
return "white"
|
|
188
|
+
if low in _NAMED_COLORS:
|
|
189
|
+
return f'rgb("{_NAMED_COLORS[low]}")'
|
|
190
|
+
m = _HEX_RE.match(c)
|
|
191
|
+
if m:
|
|
192
|
+
digits = m.group(1)
|
|
193
|
+
if len(digits) in (3, 4):
|
|
194
|
+
digits = "".join(ch * 2 for ch in digits)
|
|
195
|
+
return f'rgb("#{digits.lower()}")'
|
|
196
|
+
_validate_color_string(c)
|
|
197
|
+
return c
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _validate_color_string(color: str) -> None:
|
|
201
|
+
"""Accept only bundled color names, hex colors, and known color constructors."""
|
|
202
|
+
if (
|
|
203
|
+
color.lower() in _NAMED_COLORS
|
|
204
|
+
or color.lower() in ("black", "white")
|
|
205
|
+
or _HEX_RE.fullmatch(color)
|
|
206
|
+
or _COLOR_FUNCTION_RE.fullmatch(color)
|
|
207
|
+
):
|
|
208
|
+
return
|
|
209
|
+
raise ValueError(
|
|
210
|
+
f"invalid color value {color!r}: expected a color name, hex color, "
|
|
211
|
+
"or supported color function"
|
|
212
|
+
)
|
tytable/_constants.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Static Typst snippets inlined into every styled table render."""
|
|
2
|
+
|
|
3
|
+
STATIC_GET_STYLE_AND_SHOW_RULE: str = """\
|
|
4
|
+
#let get-style(x, y) = {
|
|
5
|
+
let key = str(y) + "_" + str(x)
|
|
6
|
+
if key in style-dict { style-array.at(style-dict.at(key)) } else { none }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
#show table.cell: it => {
|
|
10
|
+
if style-array.len() == 0 { return it }
|
|
11
|
+
let style = get-style(it.x, it.y)
|
|
12
|
+
if style == none { return it }
|
|
13
|
+
let tmp = it
|
|
14
|
+
if ("fontsize" in style) { tmp = text(size: style.fontsize, tmp) }
|
|
15
|
+
if ("color" in style) { tmp = text(fill: style.color, tmp) }
|
|
16
|
+
if ("indent" in style) { tmp = pad(left: style.indent, tmp) }
|
|
17
|
+
if ("underline" in style) { tmp = underline(tmp) }
|
|
18
|
+
if ("italic" in style) { tmp = emph(tmp) }
|
|
19
|
+
if ("bold" in style) { tmp = strong(tmp) }
|
|
20
|
+
if ("mono" in style) { tmp = math.mono(tmp) }
|
|
21
|
+
if ("strikeout" in style) { tmp = strike(tmp) }
|
|
22
|
+
if ("smallcaps" in style) { tmp = smallcaps(tmp) }
|
|
23
|
+
if ("rotate" in style) {
|
|
24
|
+
let a = if "align" in style { style.align } else { left }
|
|
25
|
+
tmp = align(a, rotate(style.rotate, reflow: true, tmp))
|
|
26
|
+
}
|
|
27
|
+
tmp
|
|
28
|
+
}"""
|
tytable/_directives.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Frozen dataclasses recording user *intent* (styles, formats, plots, groups).
|
|
3
|
+
|
|
4
|
+
These are produced by the chaining methods on :class:`~tytable._tytable.TinyTable`
|
|
5
|
+
and replayed at render time by :func:`tytable._resolve.build`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Callable, Sequence
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
import polars as pl
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class StyleDirective:
|
|
20
|
+
"""A single ``.style()`` call: selectors plus the cell properties to apply."""
|
|
21
|
+
|
|
22
|
+
i: int | str | Sequence[int | str] | pl.Expr | pl.Series | Callable[[dict], bool] | None
|
|
23
|
+
j: int | str | Sequence[int | str] | None
|
|
24
|
+
regex: bool = False
|
|
25
|
+
bold: bool | None = None
|
|
26
|
+
italic: bool | None = None
|
|
27
|
+
underline: bool | None = None
|
|
28
|
+
strikeout: bool | None = None
|
|
29
|
+
monospace: bool | None = None
|
|
30
|
+
smallcaps: bool | None = None
|
|
31
|
+
color: str | None = None
|
|
32
|
+
background: str | None = None
|
|
33
|
+
fontsize: float | None = None
|
|
34
|
+
align: str | None = None
|
|
35
|
+
alignv: str | None = None
|
|
36
|
+
indent: float | None = None
|
|
37
|
+
colspan: int | None = None
|
|
38
|
+
rowspan: int | None = None
|
|
39
|
+
rotate: float | None = None
|
|
40
|
+
line: str | None = None
|
|
41
|
+
line_color: str | None = None
|
|
42
|
+
line_width: float | None = 0.1
|
|
43
|
+
line_trim: str | None = None
|
|
44
|
+
output: tuple[str, ...] | None = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class FormatDirective:
|
|
49
|
+
"""A single ``.fmt()`` call: selectors plus numeric/format/replace/fn transforms."""
|
|
50
|
+
|
|
51
|
+
i: int | str | Sequence[int | str] | pl.Expr | pl.Series | Callable[[dict], bool] | None
|
|
52
|
+
j: int | str | Sequence[int | str] | None
|
|
53
|
+
regex: bool = False
|
|
54
|
+
digits: int | None = None
|
|
55
|
+
num_fmt: str | None = "decimal"
|
|
56
|
+
replace: dict | str | bool | None = None
|
|
57
|
+
escape: bool | str = False
|
|
58
|
+
fn: Callable | None = None
|
|
59
|
+
output: tuple[str, ...] | None = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class PlotDirective:
|
|
64
|
+
"""A single ``.plot()`` call."""
|
|
65
|
+
|
|
66
|
+
i: int | str | Sequence[int | str] | pl.Expr | pl.Series | Callable[[dict], bool] | None
|
|
67
|
+
j: int | str | Sequence[int | str] | None
|
|
68
|
+
fun: Callable
|
|
69
|
+
regex: bool = False
|
|
70
|
+
data: list | None = None
|
|
71
|
+
color: str = "black"
|
|
72
|
+
xlim: list[float] | None = None
|
|
73
|
+
height: float = 1.0
|
|
74
|
+
height_px: int = 400
|
|
75
|
+
width_px: int = 1200
|
|
76
|
+
output: tuple[str, ...] | None = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class ImageDirective:
|
|
81
|
+
"""A single ``.images()`` call."""
|
|
82
|
+
|
|
83
|
+
i: int | str | Sequence[int | str] | pl.Expr | pl.Series | Callable[[dict], bool] | None
|
|
84
|
+
j: int | str | Sequence[int | str] | None
|
|
85
|
+
images: list[str]
|
|
86
|
+
regex: bool = False
|
|
87
|
+
height: float = 1.0
|
|
88
|
+
output: tuple[str, ...] | None = None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class RowGroup:
|
|
93
|
+
"""A row-group separator: ``label`` displayed before data row ``position``."""
|
|
94
|
+
|
|
95
|
+
label: str
|
|
96
|
+
position: int
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True)
|
|
100
|
+
class ColGroup:
|
|
101
|
+
"""A column group: ``label`` spanning ``columns`` (0-based positions)."""
|
|
102
|
+
|
|
103
|
+
label: str
|
|
104
|
+
columns: list[int]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True)
|
|
108
|
+
class Note:
|
|
109
|
+
"""A footnote; ``marker`` is auto-assigned when ``i``/``j`` target a cell."""
|
|
110
|
+
|
|
111
|
+
text: str
|
|
112
|
+
marker: str | None = None
|
|
113
|
+
i: list[int] | None = None
|
|
114
|
+
j: list[int] | None = None
|
tytable/_escape.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Escaping for Typst and HTML metacharacters in cell text and captions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
|
|
8
|
+
TYPST_ESCAPE = {
|
|
9
|
+
"\\": "\\\\",
|
|
10
|
+
"<": "\\<",
|
|
11
|
+
">": "\\>",
|
|
12
|
+
"*": "\\*",
|
|
13
|
+
"_": "\\_",
|
|
14
|
+
"@": "\\@",
|
|
15
|
+
"=": "\\=",
|
|
16
|
+
"-": "\\-",
|
|
17
|
+
"+": "\\+",
|
|
18
|
+
"/": "\\/",
|
|
19
|
+
"$": "\\$",
|
|
20
|
+
"#": "\\#",
|
|
21
|
+
"[": "\\[",
|
|
22
|
+
"]": "\\]",
|
|
23
|
+
"`": "\\`",
|
|
24
|
+
"~": "\\~",
|
|
25
|
+
}
|
|
26
|
+
TYPST_SPECIAL_RE = re.compile(r"[\\<>*_@=+/\$#\[\]`~\-]")
|
|
27
|
+
|
|
28
|
+
HTML_ESCAPE = {
|
|
29
|
+
"&": "&",
|
|
30
|
+
"<": "<",
|
|
31
|
+
">": ">",
|
|
32
|
+
}
|
|
33
|
+
HTML_SPECIAL_RE = re.compile(r"[&<>]")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def escape_typst(text: object) -> str:
|
|
37
|
+
"""Backslash-escape Typst metacharacters in ``text`` (no-op when none are present)."""
|
|
38
|
+
return _escape_typst_cached(str(text))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@lru_cache(maxsize=4096)
|
|
42
|
+
def _escape_typst_cached(text: str) -> str:
|
|
43
|
+
"""Cache escaped strings used repeatedly across table cells."""
|
|
44
|
+
if not text:
|
|
45
|
+
return text
|
|
46
|
+
if not TYPST_SPECIAL_RE.search(text):
|
|
47
|
+
return text
|
|
48
|
+
return TYPST_SPECIAL_RE.sub(lambda m: TYPST_ESCAPE[m.group(0)], text)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def escape_html(text: object) -> str:
|
|
52
|
+
"""Escape ``&``, ``<``, ``>`` for safe inclusion in HTML text."""
|
|
53
|
+
text = str(text)
|
|
54
|
+
if not text:
|
|
55
|
+
return text
|
|
56
|
+
if not HTML_SPECIAL_RE.search(text):
|
|
57
|
+
return text
|
|
58
|
+
return HTML_SPECIAL_RE.sub(lambda m: HTML_ESCAPE[m.group(0)], text)
|