fancy-template 0.2.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.
- fancy_template/__init__.py +121 -0
- fancy_template/_dates.py +172 -0
- fancy_template/_declarations.py +188 -0
- fancy_template/_errors.py +26 -0
- fancy_template/_filters.py +118 -0
- fancy_template/_inspector.py +350 -0
- fancy_template/_json.py +207 -0
- fancy_template/_limits.py +54 -0
- fancy_template/_numbers.py +144 -0
- fancy_template/_profile.py +82 -0
- fancy_template/_renderer.py +277 -0
- fancy_template/_scanner.py +284 -0
- fancy_template/profiles/dark-slide.json +16 -0
- fancy_template/profiles/fine-print.json +18 -0
- fancy_template/profiles/holy-sheet.json +11 -0
- fancy_template/profiles/last-word.json +18 -0
- fancy_template/py.typed +0 -0
- fancy_template-0.2.0.dist-info/METADATA +67 -0
- fancy_template-0.2.0.dist-info/RECORD +21 -0
- fancy_template-0.2.0.dist-info/WHEEL +4 -0
- fancy_template-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Splits one string into text and ``{{ }}`` tokens (SPEC.md section 5).
|
|
2
|
+
|
|
3
|
+
Hand-written and single-pass: every index only moves forward, and no regular
|
|
4
|
+
expression ever runs over template text (plan section 5). Template text is
|
|
5
|
+
authored by admins and agents and arrives over the wire, and a backtracking
|
|
6
|
+
regex over it is a denial of service waiting for the right string.
|
|
7
|
+
|
|
8
|
+
It scans UTF-8 BYTES, because the token-length bound is defined in bytes
|
|
9
|
+
(SPEC.md section 10) and the three legs must refuse the same token at the same
|
|
10
|
+
place. Every delimiter is ASCII, so a split never lands inside a character.
|
|
11
|
+
|
|
12
|
+
Character classes are ASCII by hand: ``str.isalpha`` and ``str.isdigit`` are
|
|
13
|
+
Unicode-aware, so ``"é".isalpha()`` and ``"٣".isdigit()`` are True and a name
|
|
14
|
+
like ``café`` would parse here and nowhere else.
|
|
15
|
+
|
|
16
|
+
Errors are raised as TemplateError with an empty path; the caller knows which
|
|
17
|
+
field it was scanning and re-raises with the pointer.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from collections.abc import Callable
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from ._errors import TemplateError
|
|
26
|
+
|
|
27
|
+
LOOP_HEADS = ("$index", "$first", "$last")
|
|
28
|
+
|
|
29
|
+
_OPEN = ord("{")
|
|
30
|
+
_CLOSE = ord("}")
|
|
31
|
+
_QUOTE = ord('"')
|
|
32
|
+
_BACKSLASH = ord("\\")
|
|
33
|
+
_SPACE = ord(" ")
|
|
34
|
+
_TAB = ord("\t")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _is_alpha(c: int) -> bool:
|
|
38
|
+
return 0x41 <= c <= 0x5A or 0x61 <= c <= 0x7A
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _is_digit(c: int) -> bool:
|
|
42
|
+
return 0x30 <= c <= 0x39
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _is_name_char(c: int) -> bool:
|
|
46
|
+
return _is_alpha(c) or _is_digit(c) or c == 0x5F
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _is_lower(c: int) -> bool:
|
|
50
|
+
return 0x61 <= c <= 0x7A
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _is_bare(c: int) -> bool:
|
|
54
|
+
return _is_name_char(c) or c in (0x2E, 0x2B, 0x2D)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _syntax() -> TemplateError:
|
|
58
|
+
return TemplateError("TOKEN_SYNTAX", "")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class _Cursor:
|
|
62
|
+
"""A forward-only position in a byte string. Nothing here can move backwards."""
|
|
63
|
+
|
|
64
|
+
__slots__ = ("b", "i", "n")
|
|
65
|
+
|
|
66
|
+
def __init__(self, b: bytes) -> None:
|
|
67
|
+
self.b = b
|
|
68
|
+
self.i = 0
|
|
69
|
+
self.n = len(b)
|
|
70
|
+
|
|
71
|
+
def peek(self) -> int | None:
|
|
72
|
+
return self.b[self.i] if self.i < self.n else None
|
|
73
|
+
|
|
74
|
+
def advance(self) -> None:
|
|
75
|
+
self.i += 1
|
|
76
|
+
|
|
77
|
+
def done(self) -> bool:
|
|
78
|
+
return self.i >= self.n
|
|
79
|
+
|
|
80
|
+
def skip_ws(self) -> None:
|
|
81
|
+
# Spaces and tabs only. A newline is never whitespace inside a token.
|
|
82
|
+
while self.i < self.n and self.b[self.i] in (_SPACE, _TAB):
|
|
83
|
+
self.i += 1
|
|
84
|
+
|
|
85
|
+
def take_while(self, accept: Callable[[int], bool]) -> str:
|
|
86
|
+
start = self.i
|
|
87
|
+
while self.i < self.n and accept(self.b[self.i]):
|
|
88
|
+
self.i += 1
|
|
89
|
+
return self.b[start : self.i].decode("utf-8")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def tokenize(text: str, token_length: int) -> list[dict[str, Any]]:
|
|
93
|
+
if "{{" not in text:
|
|
94
|
+
return [] if text == "" else [{"text": text}]
|
|
95
|
+
|
|
96
|
+
b = text.encode("utf-8")
|
|
97
|
+
n = len(b)
|
|
98
|
+
parts: list[dict[str, Any]] = []
|
|
99
|
+
buffer = ""
|
|
100
|
+
i = 0
|
|
101
|
+
while i < n:
|
|
102
|
+
open_at = b.find(b"{{", i)
|
|
103
|
+
if open_at == -1:
|
|
104
|
+
buffer += b[i:].decode("utf-8")
|
|
105
|
+
break
|
|
106
|
+
buffer += b[i:open_at].decode("utf-8")
|
|
107
|
+
|
|
108
|
+
start = open_at + 2
|
|
109
|
+
close = _find_close(b, start, n, token_length)
|
|
110
|
+
token = _parse_token(b[start:close])
|
|
111
|
+
|
|
112
|
+
if buffer != "":
|
|
113
|
+
parts.append({"text": buffer})
|
|
114
|
+
buffer = ""
|
|
115
|
+
parts.append(token)
|
|
116
|
+
i = close + 2
|
|
117
|
+
|
|
118
|
+
if buffer != "":
|
|
119
|
+
parts.append({"text": buffer})
|
|
120
|
+
return parts
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def count_tokens(text: str, token_length: int) -> int:
|
|
124
|
+
"""How many tokens a string opens, stopping quietly at the first error (reported elsewhere)."""
|
|
125
|
+
if "{{" not in text:
|
|
126
|
+
return 0
|
|
127
|
+
b = text.encode("utf-8")
|
|
128
|
+
count = 0
|
|
129
|
+
i = 0
|
|
130
|
+
while True:
|
|
131
|
+
open_at = b.find(b"{{", i)
|
|
132
|
+
if open_at == -1:
|
|
133
|
+
break
|
|
134
|
+
count += 1
|
|
135
|
+
try:
|
|
136
|
+
i = _find_close(b, open_at + 2, len(b), token_length) + 2
|
|
137
|
+
except TemplateError:
|
|
138
|
+
break
|
|
139
|
+
return count
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _find_close(b: bytes, start: int, n: int, limit: int) -> int:
|
|
143
|
+
"""Index of the ``}}`` that closes a token whose inner text starts at ``start``."""
|
|
144
|
+
j = start
|
|
145
|
+
while True:
|
|
146
|
+
if j - start > limit:
|
|
147
|
+
raise TemplateError("TOKEN_TOO_LONG", "")
|
|
148
|
+
if j >= n:
|
|
149
|
+
raise TemplateError("TOKEN_UNCLOSED", "")
|
|
150
|
+
c = b[j]
|
|
151
|
+
if c == _QUOTE:
|
|
152
|
+
# A string literal: ``}}`` inside it does not close the token. An escape skips one byte.
|
|
153
|
+
j += 1
|
|
154
|
+
while j < n and b[j] != _QUOTE:
|
|
155
|
+
if j - start > limit:
|
|
156
|
+
raise TemplateError("TOKEN_TOO_LONG", "")
|
|
157
|
+
j += 2 if b[j] == _BACKSLASH else 1
|
|
158
|
+
if j >= n:
|
|
159
|
+
if j - start > limit:
|
|
160
|
+
raise TemplateError("TOKEN_TOO_LONG", "")
|
|
161
|
+
raise TemplateError("TOKEN_UNCLOSED", "")
|
|
162
|
+
j += 1
|
|
163
|
+
continue
|
|
164
|
+
if c == _CLOSE and j + 1 < n and b[j + 1] == _CLOSE:
|
|
165
|
+
return j
|
|
166
|
+
j += 1
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _parse_token(inner: bytes) -> dict[str, Any]:
|
|
170
|
+
if all(c in (_SPACE, _TAB) for c in inner):
|
|
171
|
+
raise TemplateError("TOKEN_EMPTY", "")
|
|
172
|
+
|
|
173
|
+
p = _Cursor(inner)
|
|
174
|
+
p.skip_ws()
|
|
175
|
+
token: dict[str, Any] = {"literal": _string(p)} if p.peek() == _QUOTE else {"path": _path(p)}
|
|
176
|
+
|
|
177
|
+
filters: list[dict[str, str]] = []
|
|
178
|
+
while True:
|
|
179
|
+
p.skip_ws()
|
|
180
|
+
if p.done():
|
|
181
|
+
break
|
|
182
|
+
if p.peek() != ord("|"):
|
|
183
|
+
raise _syntax()
|
|
184
|
+
p.advance()
|
|
185
|
+
p.skip_ws()
|
|
186
|
+
name = p.take_while(_is_lower)
|
|
187
|
+
if name == "":
|
|
188
|
+
raise _syntax()
|
|
189
|
+
filt = {"name": name}
|
|
190
|
+
p.skip_ws()
|
|
191
|
+
if p.peek() == ord(":"):
|
|
192
|
+
p.advance()
|
|
193
|
+
p.skip_ws()
|
|
194
|
+
if p.peek() == _QUOTE:
|
|
195
|
+
filt["arg"] = _string(p)
|
|
196
|
+
else:
|
|
197
|
+
arg = p.take_while(_is_bare)
|
|
198
|
+
if arg == "":
|
|
199
|
+
raise _syntax()
|
|
200
|
+
filt["arg"] = arg
|
|
201
|
+
filters.append(filt)
|
|
202
|
+
|
|
203
|
+
token["filters"] = filters
|
|
204
|
+
return token
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _path(p: _Cursor) -> list[str]:
|
|
208
|
+
segments: list[str] = []
|
|
209
|
+
if p.peek() == ord("$"):
|
|
210
|
+
p.advance()
|
|
211
|
+
head = "$" + p.take_while(_is_name_char)
|
|
212
|
+
if head not in LOOP_HEADS:
|
|
213
|
+
raise _syntax()
|
|
214
|
+
segments.append(head)
|
|
215
|
+
else:
|
|
216
|
+
name = _name(p)
|
|
217
|
+
if name is None:
|
|
218
|
+
raise _syntax()
|
|
219
|
+
segments.append(name)
|
|
220
|
+
|
|
221
|
+
while p.peek() == ord("."):
|
|
222
|
+
p.advance()
|
|
223
|
+
digits = p.take_while(_is_digit)
|
|
224
|
+
if digits != "":
|
|
225
|
+
segments.append(digits)
|
|
226
|
+
continue
|
|
227
|
+
name = _name(p)
|
|
228
|
+
if name is None:
|
|
229
|
+
raise _syntax()
|
|
230
|
+
segments.append(name)
|
|
231
|
+
return segments
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def path_only(text: str) -> list[str] | None:
|
|
235
|
+
"""Parse a whole string as a bare path, as ``$each`` takes one. None if it is not one."""
|
|
236
|
+
p = _Cursor(text.encode("utf-8"))
|
|
237
|
+
p.skip_ws()
|
|
238
|
+
try:
|
|
239
|
+
steps = _path(p)
|
|
240
|
+
except TemplateError:
|
|
241
|
+
return None
|
|
242
|
+
p.skip_ws()
|
|
243
|
+
return steps if p.done() else None
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def is_name(s: str) -> bool:
|
|
247
|
+
if s == "" or not s.isascii():
|
|
248
|
+
return False
|
|
249
|
+
b = s.encode("ascii")
|
|
250
|
+
if not (_is_alpha(b[0]) or b[0] == 0x5F):
|
|
251
|
+
return False
|
|
252
|
+
return all(_is_name_char(c) for c in b[1:])
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def is_digits(s: str) -> bool:
|
|
256
|
+
return s != "" and s.isascii() and all(_is_digit(c) for c in s.encode("ascii"))
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _name(p: _Cursor) -> str | None:
|
|
260
|
+
c = p.peek()
|
|
261
|
+
if c is None or not (_is_alpha(c) or c == 0x5F):
|
|
262
|
+
return None
|
|
263
|
+
return p.take_while(_is_name_char)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _string(p: _Cursor) -> str:
|
|
267
|
+
p.advance() # opening quote
|
|
268
|
+
out = bytearray()
|
|
269
|
+
while True:
|
|
270
|
+
c = p.peek()
|
|
271
|
+
if c is None:
|
|
272
|
+
raise _syntax()
|
|
273
|
+
p.advance()
|
|
274
|
+
if c == _QUOTE:
|
|
275
|
+
return out.decode("utf-8")
|
|
276
|
+
if c == _BACKSLASH:
|
|
277
|
+
nxt = p.peek()
|
|
278
|
+
if nxt not in (_QUOTE, _BACKSLASH):
|
|
279
|
+
raise _syntax()
|
|
280
|
+
assert nxt is not None
|
|
281
|
+
p.advance()
|
|
282
|
+
out.append(nxt)
|
|
283
|
+
continue
|
|
284
|
+
out.append(c)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"writer": "dark-slide",
|
|
3
|
+
"schema": "dark-slide deck",
|
|
4
|
+
"notes": "Implemented in all three legs (phase 6). src and background.image are image fields so that a LITERAL local path is refused: dark-slide's ImageResolver reads local paths.",
|
|
5
|
+
"fields": [
|
|
6
|
+
{ "path": "title", "encode": "text" },
|
|
7
|
+
{ "path": "slides.*.elements.*.content", "encode": "markup" },
|
|
8
|
+
{ "path": "slides.*.notes", "encode": "text" },
|
|
9
|
+
{ "path": "slides.*.elements.*.alt", "encode": "text" },
|
|
10
|
+
{ "path": "slides.*.elements.*.items.*", "encode": "text" },
|
|
11
|
+
{ "path": "slides.*.elements.*.columns.*.label", "encode": "text" },
|
|
12
|
+
{ "path": "slides.*.elements.*.rows.**", "encode": "text" },
|
|
13
|
+
{ "path": "slides.*.elements.*.src", "encode": "image", "mime": ["image/png", "image/jpeg"] },
|
|
14
|
+
{ "path": "slides.*.background.image", "encode": "image", "mime": ["image/png", "image/jpeg"] }
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"writer": "fine-print",
|
|
3
|
+
"schema": "fine-print 0.3",
|
|
4
|
+
"notes": "An ALLOWLIST: a string field not matched here refuses tokens. link, color, font, fonts, page geometry and every style field are deliberately absent. Band slots refuse a value containing {page}/{pages}: fine-print replaces them with str_replace and has no escape.",
|
|
5
|
+
"fields": [
|
|
6
|
+
{ "path": "title", "encode": "text" },
|
|
7
|
+
{ "path": "**.runs.*.text", "encode": "text" },
|
|
8
|
+
{ "path": "**.blocks.*.text", "encode": "text" },
|
|
9
|
+
{ "path": "**.blocks.*.alt", "encode": "text" },
|
|
10
|
+
{ "path": "**.blocks.*.src", "encode": "image", "mime": ["image/png", "image/jpeg"] },
|
|
11
|
+
{ "path": "page.header.left", "encode": "band" },
|
|
12
|
+
{ "path": "page.header.center", "encode": "band" },
|
|
13
|
+
{ "path": "page.header.right", "encode": "band" },
|
|
14
|
+
{ "path": "page.footer.left", "encode": "band" },
|
|
15
|
+
{ "path": "page.footer.center", "encode": "band" },
|
|
16
|
+
{ "path": "page.footer.right", "encode": "band" }
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"writer": "holy-sheet",
|
|
3
|
+
"schema": "holy-sheet workbook",
|
|
4
|
+
"notes": "Implemented in all three legs (phase 6). Every string result in a cell is written {value: s}, so user data can never become a formula. formula is deliberately absent.",
|
|
5
|
+
"fields": [
|
|
6
|
+
{ "path": "sheets.*.name", "encode": "text" },
|
|
7
|
+
{ "path": "sheets.*.rows.*.*", "encode": "cell" },
|
|
8
|
+
{ "path": "sheets.*.cells.*", "encode": "cell" },
|
|
9
|
+
{ "path": "sheets.*.cells.*.value", "encode": "text" }
|
|
10
|
+
]
|
|
11
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"writer": "last-word",
|
|
3
|
+
"schema": "last-word 0.x (Doc model shared with fine-print)",
|
|
4
|
+
"notes": "An ALLOWLIST: a string field not matched here refuses tokens. link, chart option, color, font, fonts, page geometry and every style field are deliberately absent. Band slots refuse a value containing {page}/{pages}: last-word writes them as PAGE and NUMPAGES fields with str_replace and has no escape.",
|
|
5
|
+
"fields": [
|
|
6
|
+
{ "path": "title", "encode": "text" },
|
|
7
|
+
{ "path": "**.runs.*.text", "encode": "text" },
|
|
8
|
+
{ "path": "**.blocks.*.text", "encode": "text" },
|
|
9
|
+
{ "path": "**.blocks.*.alt", "encode": "text" },
|
|
10
|
+
{ "path": "**.blocks.*.src", "encode": "image", "mime": ["image/png", "image/jpeg"] },
|
|
11
|
+
{ "path": "page.header.left", "encode": "band" },
|
|
12
|
+
{ "path": "page.header.center", "encode": "band" },
|
|
13
|
+
{ "path": "page.header.right", "encode": "band" },
|
|
14
|
+
{ "path": "page.footer.left", "encode": "band" },
|
|
15
|
+
{ "path": "page.footer.center", "encode": "band" },
|
|
16
|
+
{ "path": "page.footer.right", "encode": "band" }
|
|
17
|
+
]
|
|
18
|
+
}
|
fancy_template/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fancy-template
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Document templates for the Fancy document writers: a writer document with {{ tokens }} and JSON directives, plus a declaration of the data it needs. inspect() at save time, validate_data() before render, render() to plain writer JSON. No code execution, no includes, bounded everywhere. One spec, three runtimes, held to a shared conformance table.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Particle-Academy/fancy-template
|
|
6
|
+
Project-URL: Issues, https://github.com/Particle-Academy/fancy-template/issues
|
|
7
|
+
Project-URL: Source, https://github.com/Particle-Academy/fancy-template
|
|
8
|
+
Author: Particle Academy
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agentic,certificate,cross-language,document,docx,particle-academy,pdf,pptx,template,xlsx
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Text Processing :: Markup
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: fancy-expr<2.0,>=0.2.0
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# fancy-template (Python)
|
|
26
|
+
|
|
27
|
+
Document templates for the Fancy document writers: a writer document
|
|
28
|
+
(fine-print, last-word, dark-slide, holy-sheet JSON) with `{{ tokens }}` in some
|
|
29
|
+
string values and JSON directives on some array items, plus a declaration of the
|
|
30
|
+
data it needs. **One specification, three implementations** (Python, TypeScript,
|
|
31
|
+
PHP), all asserted against the same `template/*` table in `fancy-conformance`.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from fancy_template import inspect, validate_data, render
|
|
35
|
+
|
|
36
|
+
inspect(template)["issues"] # at SAVE time, no data
|
|
37
|
+
validate_data(template, data) # before rendering
|
|
38
|
+
doc = render(template, data) # plain writer JSON, or raises TemplateError
|
|
39
|
+
|
|
40
|
+
import holy_sheet
|
|
41
|
+
|
|
42
|
+
holy_sheet.to_bytes(doc) # the writer validates it again
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## What it guarantees
|
|
46
|
+
|
|
47
|
+
- **Missing data is loud.** An absent value raises `TemplateError` with a stable
|
|
48
|
+
`code`, a JSON Pointer `path` into the template and the `variable` path --
|
|
49
|
+
never the data value itself.
|
|
50
|
+
- **No code execution, no includes.** Tokens are paths plus a fixed filter list;
|
|
51
|
+
`$if` is a sandboxed fancy-expr expression; data is reduced to plain JSON
|
|
52
|
+
before anything reads it, and no method of a host object is ever called.
|
|
53
|
+
- **Profiles are allowlists.** Only listed fields take tokens. holy-sheet cells
|
|
54
|
+
write every string result as `{"value": s}`, so user data can never become a
|
|
55
|
+
formula; dark-slide escapes values for the element's markdown or html.
|
|
56
|
+
- **Bounded everywhere**, with a stable error code per bound.
|
|
57
|
+
|
|
58
|
+
## Where Python differs from its own instincts
|
|
59
|
+
|
|
60
|
+
- `True == 1` is true in Python; here a boolean is never a number, and
|
|
61
|
+
`{"fancyTemplate": True}` is refused.
|
|
62
|
+
- `str.isalpha`, `str.isdigit` and `re`'s `\d` are Unicode-aware; names, dates
|
|
63
|
+
and digits here are ASCII only, as in the other legs.
|
|
64
|
+
- The recursion limit sits below the 4096 levels the other legs accept as JSON,
|
|
65
|
+
so every walk of unknown depth is iterative.
|
|
66
|
+
|
|
67
|
+
The specification is `SPEC.md` in the repository root.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
fancy_template/__init__.py,sha256=R1ltUO7ZH3TCEZ_-MHZJnGFvK6T70TwnYRvx7S7_US0,4776
|
|
2
|
+
fancy_template/_dates.py,sha256=dlKm7PVppftAEWRKp2jzsjy78fZOWjcWP5iPpdDcVHM,5508
|
|
3
|
+
fancy_template/_declarations.py,sha256=aFOuvwd2JeDHbTb22Q9pWCOdSJtrp35Dv2cI0kX76cE,7353
|
|
4
|
+
fancy_template/_errors.py,sha256=1kHsoyHMFSElDxDG29kkWc9sjAnzEWLMRfJ2FsyvGno,1018
|
|
5
|
+
fancy_template/_filters.py,sha256=xEh5tX5H5Cn4ToqNGYfu-m8sCnts_FazUb-EmMLSao8,3923
|
|
6
|
+
fancy_template/_inspector.py,sha256=6_yp8W43cl0o3EXhsIZVu1Bc5vAUWmwYhDVR_To1G_M,14031
|
|
7
|
+
fancy_template/_json.py,sha256=SnQP4e6reXBg-AV3Z4oojsKm2iQzQ_RAImRS82LsIXw,6616
|
|
8
|
+
fancy_template/_limits.py,sha256=BOGIQE2pEw7ChoXE3xbI1ah_pqIQvz2q88tp0dlgGaw,1664
|
|
9
|
+
fancy_template/_numbers.py,sha256=rsSh8LOUmX-q7CGUANJbovHI3Ja1_wiV53ZC6wUeq_c,4687
|
|
10
|
+
fancy_template/_profile.py,sha256=pyhdFptrpdP3yvAC0NSfV9xF-nc9pQc1Y3jsn4gjZOc,3223
|
|
11
|
+
fancy_template/_renderer.py,sha256=6SU5m6ZtMb2NjYXNbvF6cIJvzguEMKIdXiZT5ipaJvg,11027
|
|
12
|
+
fancy_template/_scanner.py,sha256=-tzEUROCk0yV0tors7CWX43eKDtLmH6UGepXw2VCuL8,7979
|
|
13
|
+
fancy_template/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
fancy_template/profiles/dark-slide.json,sha256=9ve4c6BkILK4T_0spwSbEfJkeHmYO1gTTPHrSdWirsU,888
|
|
15
|
+
fancy_template/profiles/fine-print.json,sha256=AEbv8WEziX5wNI0thMhtaszFOt0bgPLKGrPoOYhBKg0,977
|
|
16
|
+
fancy_template/profiles/holy-sheet.json,sha256=ZUdvftUaVinQ6RVmBKVzykWVGVW3OnIeCvu2VIW9Bjg,483
|
|
17
|
+
fancy_template/profiles/last-word.json,sha256=pSRQH3YFXrOiH0lZ3cc9fFKUdu4lRUMhzEWCWon106Q,1049
|
|
18
|
+
fancy_template-0.2.0.dist-info/METADATA,sha256=MD4IWyld3XOObXg63rtkpZByfPwdjLZbmlhm6e6JEl4,3293
|
|
19
|
+
fancy_template-0.2.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
20
|
+
fancy_template-0.2.0.dist-info/licenses/LICENSE,sha256=nBlLZPUsduZ364Sj4w82nIJyfqWKvlBnDV3j-czSMnA,1073
|
|
21
|
+
fancy_template-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Particle Academy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|