@samva/markup 0.1.1 → 0.2.0
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.
- package/AGENTS.md +25 -5
- package/README.md +92 -26
- package/dist/{ast-DAhnAvQN.d.ts → ast-63I-VLQx.d.ts} +15 -2
- package/dist/ast.d.ts +2 -2
- package/dist/ast.js +15 -2
- package/dist/{checks-CW0nFj5n.js → checks-DXuERO4B.js} +37 -3
- package/dist/checks.d.ts +8 -1
- package/dist/checks.js +2 -2
- package/dist/compile-sms.d.ts +2 -2
- package/dist/compile-whatsapp.d.ts +2 -2
- package/dist/compile.d.ts +9 -84
- package/dist/compile.js +11 -920
- package/dist/compiler.d.ts +301 -0
- package/dist/compiler.js +1389 -0
- package/dist/components.d.ts +4 -4
- package/dist/conditionals.js +1 -1
- package/dist/corpus-Dz5irJ9x.d.ts +161 -0
- package/dist/diagnostics-Djd9ckLg.d.ts +90 -0
- package/dist/diagnostics.js +6 -3
- package/dist/elements-DtwKlFVH.d.ts +820 -0
- package/dist/email/components.d.ts +119 -0
- package/dist/email/components.js +207 -0
- package/dist/email/diagnostics.d.ts +2 -0
- package/dist/email/diagnostics.js +56 -0
- package/dist/email/elements.d.ts +2 -0
- package/dist/email/elements.js +299 -0
- package/dist/email/html.d.ts +42 -0
- package/dist/email/html.js +266 -0
- package/dist/email/jsx-dev-runtime.d.ts +26 -0
- package/dist/email/jsx-dev-runtime.js +12 -0
- package/dist/email/jsx-runtime.d.ts +2 -0
- package/dist/email/jsx-runtime.js +60 -0
- package/dist/email/render.d.ts +36 -0
- package/dist/email/render.js +6 -0
- package/dist/email/selection.d.ts +102 -0
- package/dist/email/selection.js +143 -0
- package/dist/email/source-edit.d.ts +67 -0
- package/dist/email/source-edit.js +185 -0
- package/dist/email/stylesheets.d.ts +2 -0
- package/dist/email/stylesheets.js +43 -0
- package/dist/email/text.d.ts +7 -0
- package/dist/email/text.js +92 -0
- package/dist/emit.d.ts +13 -12
- package/dist/emit.js +63 -63
- package/dist/input-schema-BIfzI63B.d.ts +23 -0
- package/dist/input-schema.d.ts +2 -0
- package/dist/input-schema.js +170 -0
- package/dist/jsx-dev-runtime.d.ts +4 -3
- package/dist/jsx-dev-runtime.js +9 -1
- package/dist/{jsx-runtime-fSxcvlx4.d.ts → jsx-runtime-C5k_Zq0A.d.ts} +4 -4
- package/dist/jsx-runtime.d.ts +1 -1
- package/dist/jsx-runtime.js +20 -1
- package/dist/model.d.ts +1 -1
- package/dist/ops.d.ts +1 -1
- package/dist/ops.js +2 -2
- package/dist/{parser-yzjUOGhc.d.ts → parser-CGBS73Gz.d.ts} +12 -3
- package/dist/parser.d.ts +2 -2
- package/dist/parser.js +48 -9
- package/dist/predicate-CvkzT-a2.js +539 -0
- package/dist/{predicate-DTtVtLK7.d.ts → predicate-G2SeSvoN.d.ts} +9 -2
- package/dist/predicate.d.ts +2 -2
- package/dist/predicate.js +2 -417
- package/dist/presets/corpus.d.ts +2 -0
- package/dist/presets/corpus.js +186 -0
- package/dist/presets/public-gallery.d.ts +241 -459
- package/dist/presets/public-gallery.js +305 -5
- package/dist/presets/public-handoff.d.ts +75 -3
- package/dist/presets/public-handoff.js +220 -37
- package/dist/presets/public-preview-artifacts.js +20 -26
- package/dist/presets/public-preview.d.ts +40 -1152
- package/dist/presets/public-preview.js +388 -1152
- package/dist/presets/sources.d.ts +17 -4
- package/dist/presets/sources.js +1212 -43
- package/dist/presets.d.ts +2 -2
- package/dist/printer.d.ts +1 -1
- package/dist/render-Cddsy_XF.js +585 -0
- package/dist/render-template.d.ts +6 -3
- package/dist/render-template.js +4 -2
- package/dist/source-locations-BCJQnIfU.d.ts +89 -0
- package/dist/source-locations.d.ts +2 -0
- package/dist/source-locations.js +125 -0
- package/dist/stylesheets-oDRAnYA3.d.ts +94 -0
- package/dist/template-Dy5yOPyp.d.ts +60 -0
- package/dist/template.d.ts +2 -0
- package/dist/template.js +37 -0
- package/dist/tsx-source-instrumentation.d.ts +23 -0
- package/dist/tsx-source-instrumentation.js +97 -0
- package/dist/tsx-tracer.d.ts +116 -0
- package/dist/tsx-tracer.js +985 -0
- package/dist/{variables-3NGLpNTB.d.ts → variables-if1VrV34.d.ts} +2 -2
- package/dist/variables.d.ts +1 -1
- package/dist/variables.js +1 -1
- package/generated/sml-agent-contract.json +574 -782
- package/generated/sml-email-agent-contract.json +502 -677
- package/generated/sml-email-language-reference.md +94 -160
- package/generated/sml-language-reference.md +195 -220
- package/package.json +111 -8
package/dist/presets.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { d as Channel } from "./ast-
|
|
1
|
+
import { d as Channel } from "./ast-63I-VLQx.js";
|
|
2
2
|
import { o as TokenOverrides } from "./theme-6ApjjiBo.js";
|
|
3
3
|
//#region presets/catalog.d.ts
|
|
4
4
|
type PresetCategory = "transactional" | "digest" | "marketing" | "lifecycle";
|
|
5
5
|
interface PresetMeta {
|
|
6
|
-
/** Stable id and source filename stem (`${id}.
|
|
6
|
+
/** Stable id and source filename stem (`${id}.tsx`). */
|
|
7
7
|
readonly id: string;
|
|
8
8
|
/** The source's root-tag channel; the gallery filters by it. */
|
|
9
9
|
readonly channel: Channel;
|
package/dist/printer.d.ts
CHANGED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
import { EmailNode, drainEmailDiagnostics } from "./email/jsx-runtime.js";
|
|
2
|
+
import { declarationValue, kebab, serializeEmailHtml } from "./email/html.js";
|
|
3
|
+
import { PREHEADER_ATTRIBUTE, preheaderNode } from "./email/components.js";
|
|
4
|
+
import { EmailCompileError, hasBlockingDiagnostic } from "./email/diagnostics.js";
|
|
5
|
+
import { collectStylesheets, hasStylesheets } from "./email/stylesheets.js";
|
|
6
|
+
import { deriveEmailText } from "./email/text.js";
|
|
7
|
+
import { Predicate } from "effect";
|
|
8
|
+
//#region src/email/css-values.ts
|
|
9
|
+
/** Expand `var(--x, fallback)` against the collected custom properties. */
|
|
10
|
+
const resolveVariables = (value, variables, depth = 0) => {
|
|
11
|
+
if (depth > 8 || !value.includes("var(")) return value;
|
|
12
|
+
let result = "";
|
|
13
|
+
let rest = value;
|
|
14
|
+
while (rest.length > 0) {
|
|
15
|
+
const start = rest.indexOf("var(");
|
|
16
|
+
if (start < 0) {
|
|
17
|
+
result += rest;
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
result += rest.slice(0, start);
|
|
21
|
+
let level = 0;
|
|
22
|
+
let end = -1;
|
|
23
|
+
for (let index = start + 3; index < rest.length; index++) if (rest[index] === "(") level += 1;
|
|
24
|
+
else if (rest[index] === ")") {
|
|
25
|
+
level -= 1;
|
|
26
|
+
if (level === 0) {
|
|
27
|
+
end = index;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (end < 0) {
|
|
32
|
+
result += rest.slice(start);
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
const body = rest.slice(start + 4, end);
|
|
36
|
+
const comma = body.indexOf(",");
|
|
37
|
+
const name = (comma < 0 ? body : body.slice(0, comma)).trim();
|
|
38
|
+
const fallback = comma < 0 ? void 0 : body.slice(comma + 1).trim();
|
|
39
|
+
const replacement = variables[name] ?? fallback;
|
|
40
|
+
result += replacement === void 0 ? rest.slice(start, end + 1) : resolveVariables(replacement, variables, depth + 1);
|
|
41
|
+
rest = rest.slice(end + 1);
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Fold a constant `calc()` to a plain value. Tailwind writes every spacing
|
|
47
|
+
* utility as `calc(var(--spacing) * n)`, and classic Outlook does not implement
|
|
48
|
+
* `calc()` at all, so an unfolded value is a silently missing style. An
|
|
49
|
+
* expression that cannot be reduced to one term is left exactly as authored.
|
|
50
|
+
*/
|
|
51
|
+
const foldCalc = (value) => {
|
|
52
|
+
if (!value.includes("calc(")) return value;
|
|
53
|
+
let result = "";
|
|
54
|
+
let rest = value;
|
|
55
|
+
while (rest.length > 0) {
|
|
56
|
+
const start = rest.indexOf("calc(");
|
|
57
|
+
if (start < 0) {
|
|
58
|
+
result += rest;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
result += rest.slice(0, start);
|
|
62
|
+
let level = 0;
|
|
63
|
+
let end = -1;
|
|
64
|
+
for (let index = start + 4; index < rest.length; index++) if (rest[index] === "(") level += 1;
|
|
65
|
+
else if (rest[index] === ")") {
|
|
66
|
+
level -= 1;
|
|
67
|
+
if (level === 0) {
|
|
68
|
+
end = index;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (end < 0) {
|
|
73
|
+
result += rest.slice(start);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
const folded = evaluate(rest.slice(start + 5, end));
|
|
77
|
+
result += folded === void 0 ? rest.slice(start, end + 1) : format(folded);
|
|
78
|
+
rest = rest.slice(end + 1);
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
};
|
|
82
|
+
const format = (value) => `${Math.round(value.n * 1e6) / 1e6}${value.n === 0 && value.unit !== "%" ? "" : value.unit}`;
|
|
83
|
+
const evaluate = (expression) => {
|
|
84
|
+
const tokens = expression.match(/\(|\)|[+-]\s|[*/]|[^\s()*/]+/g);
|
|
85
|
+
if (tokens === null) return void 0;
|
|
86
|
+
let position = 0;
|
|
87
|
+
const peek = () => tokens[position]?.trim();
|
|
88
|
+
const sum = () => {
|
|
89
|
+
let left = product();
|
|
90
|
+
while (left !== void 0 && (peek() === "+" || peek() === "-")) {
|
|
91
|
+
const operator = peek();
|
|
92
|
+
position += 1;
|
|
93
|
+
const right = product();
|
|
94
|
+
if (right === void 0 || left.unit !== right.unit) return void 0;
|
|
95
|
+
left = {
|
|
96
|
+
n: operator === "+" ? left.n + right.n : left.n - right.n,
|
|
97
|
+
unit: left.unit
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return left;
|
|
101
|
+
};
|
|
102
|
+
const product = () => {
|
|
103
|
+
let left = term();
|
|
104
|
+
while (left !== void 0 && (peek() === "*" || peek() === "/")) {
|
|
105
|
+
const operator = peek();
|
|
106
|
+
position += 1;
|
|
107
|
+
const right = term();
|
|
108
|
+
if (right === void 0) return void 0;
|
|
109
|
+
if (operator === "*") {
|
|
110
|
+
if (left.unit !== "" && right.unit !== "") return void 0;
|
|
111
|
+
left = {
|
|
112
|
+
n: left.n * right.n,
|
|
113
|
+
unit: left.unit === "" ? right.unit : left.unit
|
|
114
|
+
};
|
|
115
|
+
} else {
|
|
116
|
+
if (right.unit !== "" || right.n === 0) return void 0;
|
|
117
|
+
left = {
|
|
118
|
+
n: left.n / right.n,
|
|
119
|
+
unit: left.unit
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return left;
|
|
124
|
+
};
|
|
125
|
+
const term = () => {
|
|
126
|
+
const token = tokens[position];
|
|
127
|
+
if (token === void 0) return void 0;
|
|
128
|
+
if (token === "(") {
|
|
129
|
+
position += 1;
|
|
130
|
+
const inner = sum();
|
|
131
|
+
if (peek() !== ")") return void 0;
|
|
132
|
+
position += 1;
|
|
133
|
+
return inner;
|
|
134
|
+
}
|
|
135
|
+
const match = /^(-?(?:\d+\.?\d*|\.\d+))([a-z%]*)$/i.exec(token.trim());
|
|
136
|
+
if (match === null) return void 0;
|
|
137
|
+
position += 1;
|
|
138
|
+
return {
|
|
139
|
+
n: Number.parseFloat(match[1]),
|
|
140
|
+
unit: (match[2] ?? "").toLowerCase()
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
const value = sum();
|
|
144
|
+
return position === tokens.length ? value : void 0;
|
|
145
|
+
};
|
|
146
|
+
/** Resolve custom properties, then fold what the arithmetic allows. */
|
|
147
|
+
const finalizeValue = (value, variables) => foldCalc(resolveVariables(value, variables));
|
|
148
|
+
const RANGE_FEATURE = /\(\s*([a-z-]+)\s*(<=|>=|<|>)\s*([^)]+?)\s*\)/gi;
|
|
149
|
+
/** Media Queries Level 4 range syntax down to the `min-`/`max-` prefixes clients support. */
|
|
150
|
+
const downlevelMediaQuery = (condition) => condition.replace(RANGE_FEATURE, (whole, feature, comparison, bound) => {
|
|
151
|
+
return `(${comparison.startsWith(">") ? "min-" : "max-"}${feature}:${bound})`;
|
|
152
|
+
});
|
|
153
|
+
/** Render the rules that could not be inlined as one `<style>` body. */
|
|
154
|
+
const renderHeadCss = (rules, headAtRules, variables) => {
|
|
155
|
+
const blocks = [...headAtRules];
|
|
156
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
157
|
+
for (const rule of rules) {
|
|
158
|
+
const conditions = rule.conditions.map(downlevelMediaQuery);
|
|
159
|
+
const key = conditions.join("\0");
|
|
160
|
+
const bucket = grouped.get(key);
|
|
161
|
+
if (bucket === void 0) grouped.set(key, {
|
|
162
|
+
conditions,
|
|
163
|
+
rules: [rule]
|
|
164
|
+
});
|
|
165
|
+
else bucket.rules.push(rule);
|
|
166
|
+
}
|
|
167
|
+
for (const { conditions, rules: bucket } of grouped.values()) {
|
|
168
|
+
const body = bucket.map((rule) => {
|
|
169
|
+
const declarations = rule.declarations.filter((declaration) => !declaration.property.startsWith("--")).map((declaration) => `${declaration.property}:${finalizeValue(declaration.value, variables)}!important`).join(";");
|
|
170
|
+
return declarations === "" ? "" : `${rule.selector}{${declarations}}`;
|
|
171
|
+
}).filter((block) => block !== "").join("");
|
|
172
|
+
if (body === "") continue;
|
|
173
|
+
blocks.push(conditions.reduceRight((inner, condition) => `${condition}{${inner}}`, body));
|
|
174
|
+
}
|
|
175
|
+
return blocks.join("");
|
|
176
|
+
};
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region src/email/selectors.ts
|
|
179
|
+
const classListOf = (element) => {
|
|
180
|
+
const value = element.props.class ?? element.props.className;
|
|
181
|
+
return typeof value === "string" ? value.split(/\s+/).filter((name) => name !== "") : [];
|
|
182
|
+
};
|
|
183
|
+
const attributeOf = (element, name) => {
|
|
184
|
+
const value = element.props[name];
|
|
185
|
+
if (value === void 0 || value === null || value === false) return void 0;
|
|
186
|
+
if (value === true) return "";
|
|
187
|
+
return typeof value === "string" || typeof value === "number" ? String(value) : void 0;
|
|
188
|
+
};
|
|
189
|
+
const matchesAttribute = (element, test) => {
|
|
190
|
+
const actual = test.name === "class" ? classListOf(element).join(" ") : attributeOf(element, test.name);
|
|
191
|
+
if (actual === void 0) return false;
|
|
192
|
+
if (test.operator === void 0 || test.value === void 0) return true;
|
|
193
|
+
switch (test.operator) {
|
|
194
|
+
case "=": return actual === test.value;
|
|
195
|
+
case "~=": return actual.split(/\s+/).includes(test.value);
|
|
196
|
+
case "^=": return actual.startsWith(test.value);
|
|
197
|
+
case "$=": return actual.endsWith(test.value);
|
|
198
|
+
case "*=": return actual.includes(test.value);
|
|
199
|
+
case "|=": return actual === test.value || actual.startsWith(`${test.value}-`);
|
|
200
|
+
default: return false;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
const matchesCompound = (element, compound) => {
|
|
204
|
+
if (compound.opaque) return false;
|
|
205
|
+
if (compound.tag !== void 0 && compound.tag !== element.tag) return false;
|
|
206
|
+
if (compound.id !== void 0 && attributeOf(element, "id") !== compound.id) return false;
|
|
207
|
+
if (compound.classes.length > 0) {
|
|
208
|
+
const classes = classListOf(element);
|
|
209
|
+
if (!compound.classes.every((name) => classes.includes(name))) return false;
|
|
210
|
+
}
|
|
211
|
+
return compound.attributes.every((test) => matchesAttribute(element, test));
|
|
212
|
+
};
|
|
213
|
+
const matchesFrom = (selector, index, ancestry) => {
|
|
214
|
+
if (ancestry === void 0) return false;
|
|
215
|
+
const part = selector.parts[index];
|
|
216
|
+
if (part === void 0) return false;
|
|
217
|
+
if (!matchesCompound(ancestry.element, part.compound)) return false;
|
|
218
|
+
const left = selector.parts[index - 1];
|
|
219
|
+
if (left === void 0) return true;
|
|
220
|
+
switch (left.after ?? "descendant") {
|
|
221
|
+
case "child": return matchesFrom(selector, index - 1, ancestry.parent);
|
|
222
|
+
case "descendant":
|
|
223
|
+
for (let parent = ancestry.parent; parent !== void 0; parent = parent.parent) if (matchesFrom(selector, index - 1, parent)) return true;
|
|
224
|
+
return false;
|
|
225
|
+
case "adjacent": {
|
|
226
|
+
const sibling = ancestry.previous.at(-1);
|
|
227
|
+
return sibling === void 0 ? false : matchesFrom(selector, index - 1, {
|
|
228
|
+
element: sibling,
|
|
229
|
+
parent: ancestry.parent,
|
|
230
|
+
previous: ancestry.previous.slice(0, -1)
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
case "sibling":
|
|
234
|
+
for (let position = ancestry.previous.length - 1; position >= 0; position--) if (matchesFrom(selector, index - 1, {
|
|
235
|
+
element: ancestry.previous[position],
|
|
236
|
+
parent: ancestry.parent,
|
|
237
|
+
previous: ancestry.previous.slice(0, position)
|
|
238
|
+
})) return true;
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
/** Match a compiled selector against one element in its tree position. */
|
|
243
|
+
const matchesSelector = (selector, ancestry) => selector === void 0 || selector.parts.length === 0 ? false : matchesFrom(selector, selector.parts.length - 1, ancestry);
|
|
244
|
+
//#endregion
|
|
245
|
+
//#region src/email/cascade.ts
|
|
246
|
+
/**
|
|
247
|
+
* Cascade rank as the tuple CSS actually sorts by: importance first, then
|
|
248
|
+
* whether the declaration is attached to the element itself, then layer, then
|
|
249
|
+
* specificity, then order of appearance down to the declaration. Normal
|
|
250
|
+
* declarations sort by layer ascending (unlayered last, so it wins); important
|
|
251
|
+
* declarations reverse the layer order, which is why the layer term is negated
|
|
252
|
+
* for them. Element-attached declarations are compared before layers and
|
|
253
|
+
* specificity, so an author's important inline value beats an important
|
|
254
|
+
* stylesheet declaration exactly as it does in a browser. Two declarations never
|
|
255
|
+
* tie: rule order is unique across every registered sheet, and the position
|
|
256
|
+
* inside a rule separates the declarations that share it.
|
|
257
|
+
*/
|
|
258
|
+
const rank = (important, attached, layer, specificity, order, position) => [
|
|
259
|
+
important ? 1 : 0,
|
|
260
|
+
attached ? 1 : 0,
|
|
261
|
+
important ? -layer : layer,
|
|
262
|
+
specificity[0],
|
|
263
|
+
specificity[1],
|
|
264
|
+
specificity[2],
|
|
265
|
+
order,
|
|
266
|
+
position
|
|
267
|
+
];
|
|
268
|
+
const compareRank = (left, right) => {
|
|
269
|
+
for (let index = 0; index < Math.max(left.length, right.length); index++) {
|
|
270
|
+
const a = left[index] ?? 0;
|
|
271
|
+
const b = right[index] ?? 0;
|
|
272
|
+
if (a !== b) return a - b;
|
|
273
|
+
}
|
|
274
|
+
return 0;
|
|
275
|
+
};
|
|
276
|
+
const outranks = (left, right) => compareRank(left, right) >= 0;
|
|
277
|
+
/** An authored `style` object becomes declarations with the same normalization the serializer uses. */
|
|
278
|
+
const inlineDeclarations = (style) => {
|
|
279
|
+
if (!Predicate.isObject(style) || Array.isArray(style)) return [];
|
|
280
|
+
const output = [];
|
|
281
|
+
for (const [property, value] of Object.entries(style)) {
|
|
282
|
+
if (value === void 0 || value === null) continue;
|
|
283
|
+
if (typeof value !== "string" && typeof value !== "number") continue;
|
|
284
|
+
const text = declarationValue(property, value);
|
|
285
|
+
const important = /!important\s*$/i.test(text);
|
|
286
|
+
output.push({
|
|
287
|
+
property: kebab(property),
|
|
288
|
+
value: important ? text.replace(/\s*!important\s*$/i, "") : text,
|
|
289
|
+
important
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
return output;
|
|
293
|
+
};
|
|
294
|
+
const horizontalWriting = {
|
|
295
|
+
direction: "ltr",
|
|
296
|
+
writingMode: "horizontal-tb"
|
|
297
|
+
};
|
|
298
|
+
const paddingValues = (value) => {
|
|
299
|
+
const values = [];
|
|
300
|
+
let depth = 0;
|
|
301
|
+
let start = 0;
|
|
302
|
+
for (let index = 0; index < value.length; index++) {
|
|
303
|
+
const character = value[index];
|
|
304
|
+
if (character === "(") depth++;
|
|
305
|
+
else if (character === ")") depth--;
|
|
306
|
+
else if (/\s/.test(character) && depth === 0) {
|
|
307
|
+
if (index > start) values.push(value.slice(start, index));
|
|
308
|
+
start = index + 1;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (start < value.length) values.push(value.slice(start));
|
|
312
|
+
return values;
|
|
313
|
+
};
|
|
314
|
+
const physicalPadding = (property, value, context) => {
|
|
315
|
+
const original = [[property, value]];
|
|
316
|
+
const match = /^padding-(inline|block)(?:-(start|end))?$/.exec(property);
|
|
317
|
+
if (match === null || context.writingMode !== "horizontal-tb" || !["ltr", "rtl"].includes(context.direction) || /^(inherit|revert|revert-layer)$/.test(value) || value.includes("var(")) return original;
|
|
318
|
+
const values = paddingValues(value.trim());
|
|
319
|
+
if (values.length < 1 || values.length > (match[2] === void 0 ? 2 : 1)) return original;
|
|
320
|
+
const sides = match[1] === "block" ? ["top", "bottom"] : context.direction === "rtl" ? ["right", "left"] : ["left", "right"];
|
|
321
|
+
if (match[2] !== void 0) return [[`padding-${sides[match[2] === "start" ? 0 : 1]}`, values[0]]];
|
|
322
|
+
return [[`padding-${sides[0]}`, values[0]], [`padding-${sides[1]}`, values[1] ?? values[0]]];
|
|
323
|
+
};
|
|
324
|
+
/**
|
|
325
|
+
* Resolve every stylesheet against the tree: inline what a client will honor on
|
|
326
|
+
* the element, and keep the rest for `<head>`.
|
|
327
|
+
*/
|
|
328
|
+
const applyStylesheet = (root, sheet, options = {}) => {
|
|
329
|
+
const diagnostics = [...sheet.diagnostics];
|
|
330
|
+
const inlinable = [];
|
|
331
|
+
const retained = [];
|
|
332
|
+
const styledClasses = /* @__PURE__ */ new Set();
|
|
333
|
+
for (const rule of sheet.rules) {
|
|
334
|
+
(rule.inlinable ? inlinable : retained).push(rule);
|
|
335
|
+
for (const name of rule.classes) styledClasses.add(name);
|
|
336
|
+
}
|
|
337
|
+
const conditionalWritingRules = retained.filter((rule) => rule.declarations.some((declaration) => declaration.property === "direction" || declaration.property === "writing-mode" || declaration.property === "all"));
|
|
338
|
+
const unmatched = /* @__PURE__ */ new Map();
|
|
339
|
+
/**
|
|
340
|
+
* Children are visited with their own preceding-sibling list so `+`/`~` work.
|
|
341
|
+
* A fragment is not an element in the serialized document, so its children
|
|
342
|
+
* join that same list rather than starting a new one.
|
|
343
|
+
*/
|
|
344
|
+
const visitChildren = (children, ancestry, variables, writing) => {
|
|
345
|
+
const previous = [];
|
|
346
|
+
const visit = (nodes) => nodes.map((child) => {
|
|
347
|
+
if (child._tag === "Element") {
|
|
348
|
+
const result = applyTo(child, {
|
|
349
|
+
element: child,
|
|
350
|
+
parent: ancestry,
|
|
351
|
+
previous: [...previous]
|
|
352
|
+
}, variables, writing);
|
|
353
|
+
previous.push(child);
|
|
354
|
+
return result;
|
|
355
|
+
}
|
|
356
|
+
if (child._tag === "Fragment") return EmailNode.Fragment({ children: visit(child.children) });
|
|
357
|
+
return child;
|
|
358
|
+
});
|
|
359
|
+
return visit(children);
|
|
360
|
+
};
|
|
361
|
+
/**
|
|
362
|
+
* `inherited` is the custom-property scope this element sees from its
|
|
363
|
+
* ancestors, rooted in the sheet's unconditional properties. Custom properties
|
|
364
|
+
* cascade like any other declaration and then inherit, so a `--brand` declared
|
|
365
|
+
* on `.card` has to reach `var(--brand)` on `.card` and everything under it.
|
|
366
|
+
*/
|
|
367
|
+
const applyTo = (element, ancestry, inherited, inheritedWriting) => {
|
|
368
|
+
const conditionalWriting = conditionalWritingRules.some((rule) => {
|
|
369
|
+
if (rule.matcher === void 0 || rule.matcher.parts.length === 0 || rule.matcher.parts.some((part) => part.compound.opaque)) return true;
|
|
370
|
+
for (let context = ancestry; context !== void 0; context = context.parent) if (matchesSelector(rule.matcher, context)) return true;
|
|
371
|
+
return false;
|
|
372
|
+
});
|
|
373
|
+
const winners = /* @__PURE__ */ new Map();
|
|
374
|
+
const claim = (declaration, key) => {
|
|
375
|
+
const current = winners.get(declaration.property);
|
|
376
|
+
if (current === void 0 || outranks(key, current.key)) winners.set(declaration.property, {
|
|
377
|
+
value: declaration.value,
|
|
378
|
+
key
|
|
379
|
+
});
|
|
380
|
+
};
|
|
381
|
+
for (const rule of inlinable) {
|
|
382
|
+
if (!matchesSelector(rule.matcher, ancestry)) continue;
|
|
383
|
+
for (const [position, declaration] of rule.declarations.entries()) claim(declaration, rank(declaration.important, false, rule.layer, rule.specificity, rule.order, position));
|
|
384
|
+
}
|
|
385
|
+
for (const [position, declaration] of inlineDeclarations(element.props.style).entries()) claim(declaration, rank(declaration.important, true, 0, [
|
|
386
|
+
0,
|
|
387
|
+
0,
|
|
388
|
+
0
|
|
389
|
+
], 0, position));
|
|
390
|
+
for (const name of classListOf(element)) if (!styledClasses.has(name) && options.knownClasses?.has(name) !== true) unmatched.set(name, [...unmatched.get(name) ?? [], ...element.origins]);
|
|
391
|
+
const declared = {};
|
|
392
|
+
for (const [property, entry] of winners) if (property.startsWith("--")) declared[property] = entry.value;
|
|
393
|
+
const variables = Object.keys(declared).length === 0 ? inherited : {
|
|
394
|
+
...inherited,
|
|
395
|
+
...declared
|
|
396
|
+
};
|
|
397
|
+
const inheritedValue = (property, parent, initial, fallback = parent) => {
|
|
398
|
+
const value = winners.get(property)?.value;
|
|
399
|
+
if (value === void 0) return fallback;
|
|
400
|
+
const resolved = finalizeValue(value, variables);
|
|
401
|
+
return resolved === "inherit" || resolved === "unset" ? parent : resolved === "initial" ? initial : resolved;
|
|
402
|
+
};
|
|
403
|
+
const writing = {
|
|
404
|
+
direction: inheritedValue("direction", inheritedWriting.direction, "ltr", typeof element.props.dir === "string" ? element.props.dir : element.tag === "bdi" ? "auto" : inheritedWriting.direction),
|
|
405
|
+
writingMode: winners.has("all") ? "unknown" : inheritedValue("writing-mode", inheritedWriting.writingMode, "horizontal-tb")
|
|
406
|
+
};
|
|
407
|
+
const emitted = [...winners].filter(([property]) => !property.startsWith("--")).sort(([, left], [, right]) => compareRank(left.key, right.key)).map(([property, entry]) => [property, finalizeValue(entry.value, variables)]);
|
|
408
|
+
const physical = /* @__PURE__ */ new Map();
|
|
409
|
+
for (const [property, value] of emitted) for (const [name, lowered] of conditionalWriting ? [[property, value]] : physicalPadding(property, value, writing)) {
|
|
410
|
+
physical.delete(name);
|
|
411
|
+
physical.set(name, lowered);
|
|
412
|
+
}
|
|
413
|
+
return EmailNode.Element({
|
|
414
|
+
...element,
|
|
415
|
+
props: emitted.length === 0 ? element.props : {
|
|
416
|
+
...element.props,
|
|
417
|
+
style: Object.fromEntries(physical)
|
|
418
|
+
},
|
|
419
|
+
children: visitChildren(element.children, ancestry, variables, writing)
|
|
420
|
+
});
|
|
421
|
+
};
|
|
422
|
+
const tree = root._tag === "Element" ? applyTo(root, {
|
|
423
|
+
element: root,
|
|
424
|
+
parent: void 0,
|
|
425
|
+
previous: []
|
|
426
|
+
}, sheet.variables, horizontalWriting) : root._tag === "Fragment" ? EmailNode.Fragment({ children: visitChildren(root.children, void 0, sheet.variables, horizontalWriting) }) : root;
|
|
427
|
+
for (const [name, origins] of unmatched) diagnostics.push({
|
|
428
|
+
code: "unmatched-class",
|
|
429
|
+
severity: "warning",
|
|
430
|
+
message: `Class ${JSON.stringify(name)} produced no styles. If it is composed at run time, the compiler cannot discover it; list it in the template's declared classes or write the style inline.`,
|
|
431
|
+
origins: origins.slice(0, 1)
|
|
432
|
+
});
|
|
433
|
+
return {
|
|
434
|
+
tree,
|
|
435
|
+
headCss: renderHeadCss(retained, sheet.headAtRules, sheet.variables),
|
|
436
|
+
diagnostics
|
|
437
|
+
};
|
|
438
|
+
};
|
|
439
|
+
//#endregion
|
|
440
|
+
//#region src/email/render.ts
|
|
441
|
+
/** Classic Outlook lays a document out correctly only under a transitional doctype. */
|
|
442
|
+
const EMAIL_DOCTYPE = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">";
|
|
443
|
+
const settle = (diagnostics) => {
|
|
444
|
+
if (hasBlockingDiagnostic(diagnostics)) throw new EmailCompileError({ diagnostics });
|
|
445
|
+
return diagnostics;
|
|
446
|
+
};
|
|
447
|
+
/** Lower the derived tree to HTML and its plain-text alternative. */
|
|
448
|
+
const compileEmail = (node, options = {}) => {
|
|
449
|
+
const serialized = serializeEmailHtml(node, options);
|
|
450
|
+
const diagnostics = settle([...drainEmailDiagnostics(), ...serialized.diagnostics]);
|
|
451
|
+
return {
|
|
452
|
+
html: serialized.html,
|
|
453
|
+
text: deriveEmailText(node),
|
|
454
|
+
diagnostics,
|
|
455
|
+
positions: serialized.positions
|
|
456
|
+
};
|
|
457
|
+
};
|
|
458
|
+
/** The preview text already present in the tree, from `<Email preheader>`. */
|
|
459
|
+
const declaredPreheader = (node) => {
|
|
460
|
+
switch (node._tag) {
|
|
461
|
+
case "Text":
|
|
462
|
+
case "Raw": return;
|
|
463
|
+
case "Fragment": return node.children.reduce((found, child) => found ?? declaredPreheader(child), void 0);
|
|
464
|
+
case "Element": {
|
|
465
|
+
const declared = node.props[PREHEADER_ATTRIBUTE];
|
|
466
|
+
if (typeof declared === "string") return declared;
|
|
467
|
+
return node.children.reduce((found, child) => found ?? declaredPreheader(child), void 0);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
/** Put the hidden preview block first inside `<body>`, or first overall when there is no shell. */
|
|
472
|
+
const withPreheader = (node, text) => {
|
|
473
|
+
const insert = (current) => {
|
|
474
|
+
if (current._tag === "Fragment") {
|
|
475
|
+
for (const [index, child] of current.children.entries()) {
|
|
476
|
+
const replaced = insert(child);
|
|
477
|
+
if (replaced !== void 0) return EmailNode.Fragment({ children: current.children.with(index, replaced) });
|
|
478
|
+
}
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (current._tag !== "Element") return void 0;
|
|
482
|
+
if (current.tag === "body") return EmailNode.Element({
|
|
483
|
+
...current,
|
|
484
|
+
children: [preheaderNode(text), ...current.children]
|
|
485
|
+
});
|
|
486
|
+
for (const [index, child] of current.children.entries()) {
|
|
487
|
+
const replaced = insert(child);
|
|
488
|
+
if (replaced !== void 0) return EmailNode.Element({
|
|
489
|
+
...current,
|
|
490
|
+
children: current.children.with(index, replaced)
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
return insert(node) ?? EmailNode.Fragment({ children: [preheaderNode(text), node] });
|
|
495
|
+
};
|
|
496
|
+
/** Put the retained rules in a `<style>` at the end of `<head>`. */
|
|
497
|
+
const withHeadCss = (node, css) => {
|
|
498
|
+
const insert = (current) => {
|
|
499
|
+
if (current._tag === "Fragment") {
|
|
500
|
+
for (const [index, child] of current.children.entries()) {
|
|
501
|
+
const replaced = insert(child);
|
|
502
|
+
if (replaced !== void 0) return EmailNode.Fragment({ children: current.children.with(index, replaced) });
|
|
503
|
+
}
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (current._tag !== "Element") return void 0;
|
|
507
|
+
if (current.tag === "head") return EmailNode.Element({
|
|
508
|
+
...current,
|
|
509
|
+
children: [...current.children, EmailNode.Element({
|
|
510
|
+
tag: "style",
|
|
511
|
+
props: {},
|
|
512
|
+
children: [EmailNode.Text({ value: css })],
|
|
513
|
+
origins: current.origins,
|
|
514
|
+
authored: false
|
|
515
|
+
})]
|
|
516
|
+
});
|
|
517
|
+
for (const [index, child] of current.children.entries()) {
|
|
518
|
+
const replaced = insert(child);
|
|
519
|
+
if (replaced !== void 0) return EmailNode.Element({
|
|
520
|
+
...current,
|
|
521
|
+
children: current.children.with(index, replaced)
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
return insert(node);
|
|
526
|
+
};
|
|
527
|
+
/** Render one validated input through the template's only content function. Host isolation is required for untrusted source. */
|
|
528
|
+
const renderEmail = (template, data, options = {}) => {
|
|
529
|
+
if (!template.schema.validate(data)) throw new TypeError(`Invalid template input: ${JSON.stringify(template.schema.validate.errors)}`);
|
|
530
|
+
drainEmailDiagnostics();
|
|
531
|
+
const content = template.render(data);
|
|
532
|
+
const invalid = [];
|
|
533
|
+
const fail = (code, message) => invalid.push({
|
|
534
|
+
code,
|
|
535
|
+
severity: "error",
|
|
536
|
+
message,
|
|
537
|
+
origins: []
|
|
538
|
+
});
|
|
539
|
+
if (typeof content.subject !== "string" || /[\r\n\u0000]/.test(content.subject)) fail("invalid-subject", "Invalid email subject: it must be one line of text.");
|
|
540
|
+
if (content.preheader !== void 0 && typeof content.preheader !== "string") fail("invalid-preheader", "Invalid email preheader: it must be a string.");
|
|
541
|
+
if (content.text !== void 0 && typeof content.text !== "string") fail("invalid-text", "Invalid plain text: it must be a string.");
|
|
542
|
+
const declared = declaredPreheader(content.body);
|
|
543
|
+
if (content.preheader !== void 0 && declared !== void 0) fail("duplicate-preheader", "Preheader is set on both the template content and <Email preheader>; keep one.");
|
|
544
|
+
settle(invalid);
|
|
545
|
+
const base = content.preheader === void 0 ? content.body : withPreheader(content.body, content.preheader);
|
|
546
|
+
const styled = hasStylesheets() ? applyStylesheet(base, collectStylesheets()) : {
|
|
547
|
+
tree: base,
|
|
548
|
+
headCss: "",
|
|
549
|
+
diagnostics: []
|
|
550
|
+
};
|
|
551
|
+
const style = [...styled.diagnostics];
|
|
552
|
+
let tree = styled.tree;
|
|
553
|
+
if (styled.headCss !== "") {
|
|
554
|
+
const placed = withHeadCss(tree, styled.headCss);
|
|
555
|
+
if (placed === void 0) style.push({
|
|
556
|
+
code: "missing-head",
|
|
557
|
+
severity: "error",
|
|
558
|
+
message: "Styles that cannot be inlined — media queries, dark-scheme rules and pseudo-selectors — need a <head> to live in. Wrap the message in <Email>, or write those declarations as inline styles.",
|
|
559
|
+
origins: []
|
|
560
|
+
});
|
|
561
|
+
else tree = placed;
|
|
562
|
+
}
|
|
563
|
+
settle(style);
|
|
564
|
+
const compiled = compileEmail(tree, options);
|
|
565
|
+
const prefix = compiled.html.startsWith("<html") ? EMAIL_DOCTYPE : "";
|
|
566
|
+
const html = `${prefix}${compiled.html}`;
|
|
567
|
+
const positions = prefix === "" ? compiled.positions : compiled.positions.map((position) => ({
|
|
568
|
+
...position,
|
|
569
|
+
start: position.start + prefix.length,
|
|
570
|
+
end: position.end + prefix.length
|
|
571
|
+
}));
|
|
572
|
+
return {
|
|
573
|
+
subject: content.subject,
|
|
574
|
+
preheader: content.preheader ?? declared,
|
|
575
|
+
html,
|
|
576
|
+
text: content.text ?? compiled.text,
|
|
577
|
+
tree,
|
|
578
|
+
diagnostics: [...style, ...compiled.diagnostics],
|
|
579
|
+
positions
|
|
580
|
+
};
|
|
581
|
+
};
|
|
582
|
+
//#endregion
|
|
583
|
+
export { classListOf as a, finalizeValue as c, resolveVariables as d, applyStylesheet as i, foldCalc as l, compileEmail as n, matchesSelector as o, renderEmail as r, downlevelMediaQuery as s, EMAIL_DOCTYPE as t, renderHeadCss as u };
|
|
584
|
+
|
|
585
|
+
//# sourceMappingURL=render-Cddsy_XF.js.map
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { o as SmlParseError } from "./diagnostics-oveE-laV.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { a as
|
|
2
|
+
import { w as SmlDocument } from "./ast-63I-VLQx.js";
|
|
3
|
+
import { c as UnboundTemplateSourceLocations } from "./source-locations-BCJQnIfU.js";
|
|
4
|
+
import { a as VariableKind, l as Variables } from "./variables-if1VrV34.js";
|
|
5
|
+
import { a as JsxResult } from "./jsx-runtime-C5k_Zq0A.js";
|
|
5
6
|
import { Result } from "effect";
|
|
6
7
|
//#region src/render-template.d.ts
|
|
7
8
|
/**
|
|
@@ -27,6 +28,8 @@ interface RenderedTemplate {
|
|
|
27
28
|
readonly document: SmlDocument;
|
|
28
29
|
readonly source: string;
|
|
29
30
|
readonly variables: ReadonlyArray<TemplateVariable>;
|
|
31
|
+
/** Ephemeral parsed-node-id → authored JSX source origins, when available. */
|
|
32
|
+
readonly sourceLocations: UnboundTemplateSourceLocations;
|
|
30
33
|
}
|
|
31
34
|
/**
|
|
32
35
|
* Runs a template function and returns its canonical document, source, and
|
package/dist/render-template.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mapJsxSourceLocations } from "./source-locations.js";
|
|
2
|
+
import { i as parsePredicate, n as collectPredicateVariables } from "./predicate-CvkzT-a2.js";
|
|
2
3
|
import { isRootTag } from "./ast.js";
|
|
3
4
|
import { createVariablesProxy } from "./variables.js";
|
|
4
5
|
import { parseSml } from "./parser.js";
|
|
@@ -27,7 +28,8 @@ const renderTemplate = (template) => {
|
|
|
27
28
|
return Result.succeed({
|
|
28
29
|
document,
|
|
29
30
|
source,
|
|
30
|
-
variables: collectVariableSchema(document, recorder)
|
|
31
|
+
variables: collectVariableSchema(document, recorder),
|
|
32
|
+
sourceLocations: mapJsxSourceLocations(root, document.root)
|
|
31
33
|
});
|
|
32
34
|
};
|
|
33
35
|
/**
|