fancy-template 0.3.0__tar.gz → 0.4.0__tar.gz
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-0.3.0 → fancy_template-0.4.0}/PKG-INFO +1 -1
- {fancy_template-0.3.0 → fancy_template-0.4.0}/SPEC.md +8 -5
- fancy_template-0.4.0/profiles/fine-print.json +50 -0
- {fancy_template-0.3.0 → fancy_template-0.4.0}/profiles/last-word.json +2 -1
- {fancy_template-0.3.0 → fancy_template-0.4.0}/pyproject.toml +1 -1
- {fancy_template-0.3.0 → fancy_template-0.4.0}/src/fancy_template/__init__.py +1 -1
- {fancy_template-0.3.0 → fancy_template-0.4.0}/src/fancy_template/_renderer.py +9 -1
- fancy_template-0.4.0/tests/test_band_value.py +92 -0
- {fancy_template-0.3.0 → fancy_template-0.4.0}/tests/test_conformance.py +1 -1
- fancy_template-0.3.0/profiles/fine-print.json +0 -18
- {fancy_template-0.3.0 → fancy_template-0.4.0}/.gitignore +0 -0
- {fancy_template-0.3.0 → fancy_template-0.4.0}/LICENSE +0 -0
- {fancy_template-0.3.0 → fancy_template-0.4.0}/README.md +0 -0
- {fancy_template-0.3.0 → fancy_template-0.4.0}/profiles/dark-slide.json +0 -0
- {fancy_template-0.3.0 → fancy_template-0.4.0}/profiles/holy-sheet.json +0 -0
- {fancy_template-0.3.0 → fancy_template-0.4.0}/src/fancy_template/_dates.py +0 -0
- {fancy_template-0.3.0 → fancy_template-0.4.0}/src/fancy_template/_declarations.py +0 -0
- {fancy_template-0.3.0 → fancy_template-0.4.0}/src/fancy_template/_errors.py +0 -0
- {fancy_template-0.3.0 → fancy_template-0.4.0}/src/fancy_template/_files.py +0 -0
- {fancy_template-0.3.0 → fancy_template-0.4.0}/src/fancy_template/_filters.py +0 -0
- {fancy_template-0.3.0 → fancy_template-0.4.0}/src/fancy_template/_inspector.py +0 -0
- {fancy_template-0.3.0 → fancy_template-0.4.0}/src/fancy_template/_json.py +0 -0
- {fancy_template-0.3.0 → fancy_template-0.4.0}/src/fancy_template/_limits.py +0 -0
- {fancy_template-0.3.0 → fancy_template-0.4.0}/src/fancy_template/_numbers.py +0 -0
- {fancy_template-0.3.0 → fancy_template-0.4.0}/src/fancy_template/_profile.py +0 -0
- {fancy_template-0.3.0 → fancy_template-0.4.0}/src/fancy_template/_scanner.py +0 -0
- {fancy_template-0.3.0 → fancy_template-0.4.0}/src/fancy_template/py.typed +0 -0
- {fancy_template-0.3.0 → fancy_template-0.4.0}/tests/template_runner.py +0 -0
- {fancy_template-0.3.0 → fancy_template-0.4.0}/tests/test_files.py +0 -0
- {fancy_template-0.3.0 → fancy_template-0.4.0}/tests/test_security.py +0 -0
- {fancy_template-0.3.0 → fancy_template-0.4.0}/tests/test_spec.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fancy-template
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
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
5
|
Project-URL: Homepage, https://github.com/Particle-Academy/fancy-template
|
|
6
6
|
Project-URL: Issues, https://github.com/Particle-Academy/fancy-template/issues
|
|
@@ -331,7 +331,7 @@ index, `**` for zero or more. **Anything not listed refuses tokens**
|
|
|
331
331
|
| encoding | result |
|
|
332
332
|
|---|---|
|
|
333
333
|
| `text` | the string, every token stringified |
|
|
334
|
-
| `band` | as `text`, but a VALUE containing `{page}`
|
|
334
|
+
| `band` | as `text`, but a VALUE containing a writer's band field is `VALUE_PAGE_FIELD`: `{page}` `{pages}` `{section}` `{sectionpages}` `{author}` `{title}` `{filename}`, and `{date}` / `{time}`, each with an optional `:FORMAT` of one or more characters that are not a brace, a double quote or a backslash. Other braces are ordinary text |
|
|
335
335
|
| `markup` | as `text`, each value escaped for the element's `format`: `markdown` backslash-escapes every ASCII punctuation character (CommonMark), `html` escapes `& < > " '` as entities, `plain` or absent leaves it |
|
|
336
336
|
| `markdown` | as `markup` with format fixed to markdown |
|
|
337
337
|
| `cell` | a whole token keeps a number, boolean or date as-is; every other result is written `{ "value": s }`, so user data can never become a formula. "Date" means: the token's path is DECLARED `date` and the result (after filters) is a valid `date` string (section 3) -- a date-shaped value of a `string` variable is wrapped, and `{{ d \| date:"yyyy" }}` is text. A valid date cannot begin with `=`. A literal (token-free) cell is the author's and is left as written |
|
|
@@ -339,10 +339,13 @@ index, `**` for zero or more. **Anything not listed refuses tokens**
|
|
|
339
339
|
|
|
340
340
|
Wrong use of an image field is `TOKEN_IMAGE_FIELD`.
|
|
341
341
|
|
|
342
|
-
|
|
343
|
-
last-word has an escape for it: both
|
|
344
|
-
|
|
345
|
-
|
|
342
|
+
**A band field is refused rather than escaped** because neither fine-print nor
|
|
343
|
+
last-word has an escape for it: both turn the literal text into a live value in a
|
|
344
|
+
band, in a one-line slot and in a blocks band's run text alike, so a blocks band's
|
|
345
|
+
run text is `band` too. The list is one for both writers, including the ones a
|
|
346
|
+
writer writes literally today (fine-print's `{date}`), so a value that refuses in
|
|
347
|
+
one refuses in the other. A refusal is loud; any lookalike substitution would print
|
|
348
|
+
a different character. When a writer gains an escape, the band encoding uses it.
|
|
346
349
|
|
|
347
350
|
**The markdown escape is CommonMark's, and the profile does not force `plain`.**
|
|
348
351
|
dark-slide's PPTX writer (`MarkdownInline`, in all three of its languages) does
|
|
@@ -0,0 +1,50 @@
|
|
|
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. First match wins, so a band's own patterns come before the generic text ones. Every band slot and a blocks band's run text refuse a value holding a field token ({page}, {section}, {date}, ...): fine-print resolves them there and has no escape.",
|
|
5
|
+
"fields": [
|
|
6
|
+
{ "path": "title", "encode": "text" },
|
|
7
|
+
{ "path": "page.header.left", "encode": "band" },
|
|
8
|
+
{ "path": "page.header.center", "encode": "band" },
|
|
9
|
+
{ "path": "page.header.right", "encode": "band" },
|
|
10
|
+
{ "path": "page.footer.left", "encode": "band" },
|
|
11
|
+
{ "path": "page.footer.center", "encode": "band" },
|
|
12
|
+
{ "path": "page.footer.right", "encode": "band" },
|
|
13
|
+
{ "path": "page.firstHeader.left", "encode": "band" },
|
|
14
|
+
{ "path": "page.firstHeader.center", "encode": "band" },
|
|
15
|
+
{ "path": "page.firstHeader.right", "encode": "band" },
|
|
16
|
+
{ "path": "page.firstFooter.left", "encode": "band" },
|
|
17
|
+
{ "path": "page.firstFooter.center", "encode": "band" },
|
|
18
|
+
{ "path": "page.firstFooter.right", "encode": "band" },
|
|
19
|
+
{ "path": "page.evenHeader.left", "encode": "band" },
|
|
20
|
+
{ "path": "page.evenHeader.center", "encode": "band" },
|
|
21
|
+
{ "path": "page.evenHeader.right", "encode": "band" },
|
|
22
|
+
{ "path": "page.evenFooter.left", "encode": "band" },
|
|
23
|
+
{ "path": "page.evenFooter.center", "encode": "band" },
|
|
24
|
+
{ "path": "page.evenFooter.right", "encode": "band" },
|
|
25
|
+
{ "path": "blocks.*.page.header.left", "encode": "band" },
|
|
26
|
+
{ "path": "blocks.*.page.header.center", "encode": "band" },
|
|
27
|
+
{ "path": "blocks.*.page.header.right", "encode": "band" },
|
|
28
|
+
{ "path": "blocks.*.page.footer.left", "encode": "band" },
|
|
29
|
+
{ "path": "blocks.*.page.footer.center", "encode": "band" },
|
|
30
|
+
{ "path": "blocks.*.page.footer.right", "encode": "band" },
|
|
31
|
+
{ "path": "blocks.*.page.firstHeader.left", "encode": "band" },
|
|
32
|
+
{ "path": "blocks.*.page.firstHeader.center", "encode": "band" },
|
|
33
|
+
{ "path": "blocks.*.page.firstHeader.right", "encode": "band" },
|
|
34
|
+
{ "path": "blocks.*.page.firstFooter.left", "encode": "band" },
|
|
35
|
+
{ "path": "blocks.*.page.firstFooter.center", "encode": "band" },
|
|
36
|
+
{ "path": "blocks.*.page.firstFooter.right", "encode": "band" },
|
|
37
|
+
{ "path": "blocks.*.page.evenHeader.left", "encode": "band" },
|
|
38
|
+
{ "path": "blocks.*.page.evenHeader.center", "encode": "band" },
|
|
39
|
+
{ "path": "blocks.*.page.evenHeader.right", "encode": "band" },
|
|
40
|
+
{ "path": "blocks.*.page.evenFooter.left", "encode": "band" },
|
|
41
|
+
{ "path": "blocks.*.page.evenFooter.center", "encode": "band" },
|
|
42
|
+
{ "path": "blocks.*.page.evenFooter.right", "encode": "band" },
|
|
43
|
+
{ "path": "page.*.blocks.**.runs.*.text", "encode": "band" },
|
|
44
|
+
{ "path": "blocks.*.page.*.blocks.**.runs.*.text", "encode": "band" },
|
|
45
|
+
{ "path": "**.runs.*.text", "encode": "text" },
|
|
46
|
+
{ "path": "**.blocks.*.text", "encode": "text" },
|
|
47
|
+
{ "path": "**.blocks.*.alt", "encode": "text" },
|
|
48
|
+
{ "path": "**.blocks.*.src", "encode": "image", "mime": ["image/png", "image/jpeg"] }
|
|
49
|
+
]
|
|
50
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"writer": "last-word",
|
|
3
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
|
|
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. First match wins, so a band's own patterns come before the generic text ones. Band slots and a blocks band's run text refuse a value holding a field token ({page}, {section}, {date}, ...): last-word writes them as live Word fields and has no escape.",
|
|
5
5
|
"fields": [
|
|
6
6
|
{ "path": "title", "encode": "text" },
|
|
7
|
+
{ "path": "page.*.blocks.**.runs.*.text", "encode": "band" },
|
|
7
8
|
{ "path": "**.runs.*.text", "encode": "text" },
|
|
8
9
|
{ "path": "**.blocks.*.text", "encode": "text" },
|
|
9
10
|
{ "path": "**.blocks.*.alt", "encode": "text" },
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fancy-template"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "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."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
# 3.11, matching every other Fancy Python package (3.10 reaches end of life on
|
|
@@ -9,6 +9,7 @@ is refused before it is built rather than after.
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import re
|
|
12
13
|
from collections.abc import Callable, Iterator
|
|
13
14
|
from typing import Any
|
|
14
15
|
|
|
@@ -23,6 +24,13 @@ from ._limits import Limits
|
|
|
23
24
|
from ._profile import Profile
|
|
24
25
|
from ._scanner import LOOP_HEADS, is_digits, path_only, tokenize
|
|
25
26
|
|
|
27
|
+
# A writer's band field, the grammar last-word's Schema::FIELD_TOKEN and
|
|
28
|
+
# fine-print's share. Run over a VALUE, never over template text; a value holding
|
|
29
|
+
# one is refused (SPEC.md section 9).
|
|
30
|
+
BAND_FIELD = re.compile(
|
|
31
|
+
r'\{(?:(?:page|pages|section|sectionpages|author|title|filename)|(?:date|time)(?::[^{}"\\]+)?)\}'
|
|
32
|
+
)
|
|
33
|
+
|
|
26
34
|
|
|
27
35
|
def escape_markdown(s: str) -> str:
|
|
28
36
|
"""CommonMark: a backslash before every ASCII punctuation character (SPEC.md section 9)."""
|
|
@@ -232,7 +240,7 @@ class Renderer:
|
|
|
232
240
|
except TemplateError as e:
|
|
233
241
|
raise TemplateError(e.code, path, variable) from None
|
|
234
242
|
|
|
235
|
-
if field["encode"] == "band" and (
|
|
243
|
+
if field["encode"] == "band" and BAND_FIELD.search(piece):
|
|
236
244
|
raise TemplateError("VALUE_PAGE_FIELD", path, variable)
|
|
237
245
|
out += escape(piece)
|
|
238
246
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""User data must not become a live field: a value holding a band token is refused,
|
|
2
|
+
never rewritten. The PHP and TypeScript legs hold the same rows."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from fancy_template import TemplateError, render
|
|
11
|
+
|
|
12
|
+
BANDS = ["header", "footer", "firstHeader", "firstFooter", "evenHeader", "evenFooter"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def tpl(writer: str, document: dict[str, Any]) -> dict[str, Any]:
|
|
16
|
+
return {"fancyTemplate": 1, "writer": writer, "variables": {"v": {"type": "string"}}, "document": document}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def code(template: dict[str, Any], value: str) -> str | None:
|
|
20
|
+
try:
|
|
21
|
+
render(template, {"v": value})
|
|
22
|
+
except TemplateError as e:
|
|
23
|
+
return e.code
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.mark.parametrize(
|
|
28
|
+
"value",
|
|
29
|
+
[
|
|
30
|
+
"{page}",
|
|
31
|
+
"{pages}",
|
|
32
|
+
"{section}",
|
|
33
|
+
"{sectionpages}",
|
|
34
|
+
"{author}",
|
|
35
|
+
"{title}",
|
|
36
|
+
"{filename}",
|
|
37
|
+
"{date}",
|
|
38
|
+
"{time}",
|
|
39
|
+
"see {section} here",
|
|
40
|
+
"{date:d MMMM yyyy}",
|
|
41
|
+
"{time:HH:mm}",
|
|
42
|
+
],
|
|
43
|
+
)
|
|
44
|
+
@pytest.mark.parametrize("writer", ["fine-print", "last-word"])
|
|
45
|
+
def test_refuses_a_band_value_holding_any_field_token(writer: str, value: str) -> None:
|
|
46
|
+
assert code(tpl(writer, {"blocks": [], "page": {"footer": {"left": "{{ v }}"}}}), value) == "VALUE_PAGE_FIELD"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.mark.parametrize("value", ["{Page}", "{pagex}", "x{ page }", "{date:}", "{date:a{b}", "no braces"])
|
|
50
|
+
def test_keeps_a_value_that_is_only_brace_shaped(value: str) -> None:
|
|
51
|
+
out = render(tpl("fine-print", {"blocks": [], "page": {"footer": {"left": "{{ v }}"}}}), {"v": value})
|
|
52
|
+
assert out["page"]["footer"]["left"] == value
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.mark.parametrize("key", BANDS)
|
|
56
|
+
@pytest.mark.parametrize("slot", ["left", "center", "right"])
|
|
57
|
+
def test_every_fine_print_band_slot(key: str, slot: str) -> None:
|
|
58
|
+
template = tpl("fine-print", {"blocks": [], "page": {key: {slot: "{{ v }}"}}})
|
|
59
|
+
assert code(template, "Ada") is None
|
|
60
|
+
assert code(template, "{section}") == "VALUE_PAGE_FIELD"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@pytest.mark.parametrize("key", BANDS)
|
|
64
|
+
def test_a_band_on_a_fine_print_break(key: str) -> None:
|
|
65
|
+
template = tpl(
|
|
66
|
+
"fine-print",
|
|
67
|
+
{"blocks": [{"type": "pageBreak", "page": {"orientation": "landscape", key: {"left": "{{ v }}"}}}]},
|
|
68
|
+
)
|
|
69
|
+
assert code(template, "Ada") is None
|
|
70
|
+
assert code(template, "{pages}") == "VALUE_PAGE_FIELD"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@pytest.mark.parametrize("writer", ["fine-print", "last-word"])
|
|
74
|
+
def test_a_blocks_band_and_a_table_cell_in_it(writer: str) -> None:
|
|
75
|
+
para = {"type": "paragraph", "runs": [{"text": "{{ v }}"}]}
|
|
76
|
+
cell = {"type": "table", "rows": [{"cells": [{"blocks": [para]}]}]}
|
|
77
|
+
for block in (para, cell):
|
|
78
|
+
assert (
|
|
79
|
+
code(tpl(writer, {"blocks": [], "page": {"footer": {"blocks": [block]}}}), "see {page}")
|
|
80
|
+
== "VALUE_PAGE_FIELD"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_a_body_run_keeps_a_token_shaped_value_literal() -> None:
|
|
85
|
+
assert code(tpl("fine-print", {"blocks": [{"type": "paragraph", "runs": [{"text": "{{ v }}"}]}]}), "{page}") is None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_page_geometry_on_a_break_stays_closed_to_tokens() -> None:
|
|
89
|
+
assert (
|
|
90
|
+
code(tpl("fine-print", {"blocks": [{"type": "pageBreak", "page": {"orientation": "{{ v }}"}}]}), "landscape")
|
|
91
|
+
is not None
|
|
92
|
+
)
|
|
@@ -24,7 +24,7 @@ from template_runner import run_template_case
|
|
|
24
24
|
# version (package.json's range + the committed lockfile; composer.json's exact
|
|
25
25
|
# require-dev). Move all of them together, and only after re-running the tables in
|
|
26
26
|
# every runtime; the tests below fail otherwise.
|
|
27
|
-
PINNED_SUITE_VERSION = "0.
|
|
27
|
+
PINNED_SUITE_VERSION = "0.44.0"
|
|
28
28
|
|
|
29
29
|
SUITES = (
|
|
30
30
|
"template/tokenize",
|
|
@@ -1,18 +0,0 @@
|
|
|
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
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|