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 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
+ "&": "&amp;",
30
+ "<": "&lt;",
31
+ ">": "&gt;",
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)