-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrun.py
More file actions
174 lines (150 loc) · 7.93 KB
/
Copy pathrun.py
File metadata and controls
174 lines (150 loc) · 7.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
"""
10 -- twig-cms
Ported using Jinja2 in place of PHP's Twig (see SUITE.md and this suite's
README for why: same template-engine family, same delimiters, same
`<raw>`/whitespace-control lessons -- only the driver code and the
template's file extension differ). See the header comment in
emails/newsletter.inky.jinja2 for the full CMS-integrator explanation of
Order A vs. Order B and why <raw> is load-bearing here (not just
defense-in-depth, as in 03-data-merge), plus real quirks found empirically
while building this example. This file builds both orders against the
same 3 recipients, asserts recipient 1 comes out the same document either
way (see the comment above that check for exactly what "the same" means
and why), and times both paths.
emails/ is this capstone's self-contained base_path -- the same base-root
convention as 09-transactional's EmailRenderer tree (one root,
layouts/themes/includes underneath, everything root-relative, no
traversal outside it -- see emails/layouts/main.html for the resolution
rule). Both inky.build() calls below pass emails/ as base_path.
"""
import os
import re
import sys
import time
sys.path.insert(0, os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..")))
import bootstrap # noqa: E402
import inky # noqa: E402
from jinja2 import Environment, FileSystemLoader # noqa: E402
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
dist = bootstrap.inky_example("10-twig-cms")
emails_dir = os.path.join(THIS_DIR, "emails")
# This is trusted, already-authored template content, not user input, so
# autoescape is off -- matching how inky's own `data` merge behaves (no
# HTML escaping). It also keeps the two orders comparable: the SAME
# Jinja2 environment renders in both orders, so whatever escaping policy
# is chosen applies identically either way; only the ORDER of Jinja2 vs.
# inky differs between them.
env = Environment(loader=FileSystemLoader(emails_dir), autoescape=False)
# Genuinely Jinja2-only: inky's own `data` merge (MiniJinja) has no
# mechanism for user-registered filters from Python. `|upper` in the
# template proves ordinary Jinja2 syntax works; this filter proves real
# Jinja2 extensibility that `data` alone cannot reach.
def loyalty_badge(tier: str) -> str:
return {
"gold": "Gold roaster",
"silver": "Silver roaster",
}.get(tier, "Roaster")
env.filters["loyalty_badge"] = loyalty_badge
# The template supplies the "$" as static text before the price variable.
products = [
{"name": "Colombia Huila, 12oz", "price": "17.00"},
{"name": "Guatemala Antigua, 12oz", "price": "18.50"},
{"name": "Decaf House Blend, 12oz", "price": "15.00"},
]
recipients = [
{"first_name": "Marcus", "tier": "gold"},
{"first_name": "Priya", "tier": "silver"},
{"first_name": "Devon", "tier": "bronze"},
]
def newsletter_context(recipient, products_):
return {
"subscriber": recipient,
"products": products_,
"shop_url": "https://northwindcoffee.example/shop",
}
TEMPLATE_NAME = "newsletter.inky.jinja2"
with open(os.path.join(emails_dir, TEMPLATE_NAME), encoding="utf-8") as f:
raw_source = f.read()
# `inline_css=False` in BOTH builds below is load-bearing, not cosmetic --
# see the comment above Order B's build call for why.
build_options = {"inline_css": False}
# --- Order A: Jinja2 first, then a full inky build, once PER RECIPIENT -----
start_a = time.perf_counter()
order_a_outputs = []
template = env.get_template(TEMPLATE_NAME)
for recipient in recipients:
jinja_html = template.render(**newsletter_context(recipient, products))
order_a_outputs.append(inky.build(jinja_html, emails_dir, **build_options).html)
duration_a = (time.perf_counter() - start_a) * 1000
# --- Order B: inky ONCE (the shell), then Jinja2 per recipient -------------
start_b = time.perf_counter()
# No `data` option: Jinja2's {{ }} and {% %} pass through untouched (same
# no-op behavior as 03-data-merge without `data`). The <raw>-wrapped loop
# is the part that would otherwise be corrupted by HTML5 table
# foster-parenting -- see the header comment in newsletter.inky.jinja2.
#
# `inline_css=False` here (and, to match, in Order A above too) works
# around a real inky-core limitation found while building this example:
# <raw> only protects its content from the FIRST HTML5 parse (component
# transform). CSS inlining runs a SEPARATE parse over that transform's
# output, and at shell-build time the reinjected loop is still literal
# {% for %}/{% endfor %} text sitting beside a <tr> inside <tbody> --
# which that second parse foster-parents out of the table, same failure
# mode as skipping <raw> entirely, just one stage later. Turning off
# per-tag inlining (framework_css stays on, so the compiled theme still
# ships as a <style> block) sidesteps the second parse and keeps both
# orders byte-comparable. inline_css=True remains fine for templates
# whose data is always fully merged before inky ever runs
# (09-transactional); it's specifically the survives-the-build,
# fill-in-later shape here that needs this.
shell = inky.build(raw_source, emails_dir, **build_options).html
shell_template = env.from_string(shell)
order_b_outputs = []
for recipient in recipients:
order_b_outputs.append(shell_template.render(**newsletter_context(recipient, products)))
duration_b = (time.perf_counter() - start_b) * 1000
for i, recipient in enumerate(recipients):
n = i + 1
with open(os.path.join(dist, f"order-a-{n}.html"), "w", encoding="utf-8") as f:
f.write(order_a_outputs[i])
with open(os.path.join(dist, f"order-b-{n}.html"), "w", encoding="utf-8") as f:
f.write(order_b_outputs[i])
# The correctness claim: recipient 1 must be the same document either way
# inky and Jinja2 are ordered. Investigated a real, reproducible
# divergence here while building this example: inky's pipeline-level
# cleanup passes (break_long_lines / collapse_closing_tags in inky-core's
# pipeline.rs) insert or fold newlines around every
# <table>/<tbody>/<tr>/<td>/<th> tag, unconditionally, on whatever
# document is in front of them at the moment they run. In Order A that's
# the FULLY-EXPANDED 3-row document (Jinja2 ran first), so all 3 rows get
# normalized together in one pass. In Order B it's the ONE-ROW shell
# (inky ran first, before Jinja2 had anything to expand) -- that single
# row gets normalized once, and then Jinja2's blind per-recipient text
# repetition duplicates it verbatim, with no further inky pass afterward
# to reconcile the seams between copies. The row boundaries can end up
# whitespace-differently-normalized between the two orders as a result --
# a genuine engine-level finding (see SUITE.md's "10-twig-cms:
# engine-level findings" subsection), not something papered over here.
#
# It's ALSO exactly the whitespace inky-core's own break_long_lines
# comment calls out as safe to disturb: "Whitespace between table elements
# ... is ignored by email clients." So the comparison below normalizes
# only that -- collapsing runs of whitespace strictly BETWEEN a closing
# '>' and the next '<' -- before comparing. Any real content or structural
# difference (attributes, text, tag order, row count) still fails this
# check; only inter-tag padding is treated as insignificant, on inky's own
# authority. dist/ still holds the RAW, un-normalized output from both
# orders so the actual whitespace diff can be inspected directly.
def collapse_insignificant_table_whitespace(html: str) -> str:
return re.sub(r">\s+<", "><", html)
normalized_a = collapse_insignificant_table_whitespace(order_a_outputs[0])
normalized_b = collapse_insignificant_table_whitespace(order_b_outputs[0])
identical = normalized_a == normalized_b
print(
"recipient 1 identical between orders (ignoring inter-tag whitespace): "
+ ("yes" if identical else "NO -- DIVERGENCE")
)
if not identical:
print("10-twig-cms: order-a-1.html and order-b-1.html diverged -- see dist/10-twig-cms/ for a diff", file=sys.stderr)
sys.exit(1)
print(f"orderA: {duration_a:.2f} ms, orderB: {duration_b:.2f} ms (shell built once)")