@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/compile.js
CHANGED
|
@@ -1,938 +1,29 @@
|
|
|
1
1
|
import { SmlCompileError } from "./diagnostics.js";
|
|
2
|
-
import { isHiddenNode, numberAttribute, stringAttribute } from "./ast.js";
|
|
3
|
-
import { GRADIENT_DIRECTION_PROPERTY, GRADIENT_FROM_PROPERTY, GRADIENT_TO_PROPERTY } from "./registry.js";
|
|
4
|
-
import { resolveClasses } from "./classes.js";
|
|
5
2
|
import { parseSml } from "./parser.js";
|
|
6
3
|
import { t as COMPILER_VERSION } from "./version-BITr58kk.js";
|
|
7
4
|
import { compileSmsRoot } from "./compile-sms.js";
|
|
8
5
|
import { compileWhatsAppRoot } from "./compile-whatsapp.js";
|
|
9
|
-
import { escapeCssString, resolveTokenTable } from "./theme.js";
|
|
10
|
-
import { baseTokens } from "./tokens.js";
|
|
11
6
|
import { Result } from "effect";
|
|
12
7
|
//#region src/compile.ts
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
tag,
|
|
16
|
-
attrs,
|
|
17
|
-
style,
|
|
18
|
-
children,
|
|
19
|
-
void: options?.void === true ? true : void 0
|
|
20
|
-
});
|
|
21
|
-
const text = (value) => ({
|
|
22
|
-
kind: "text",
|
|
23
|
-
value
|
|
24
|
-
});
|
|
25
|
-
const raw = (html) => ({
|
|
26
|
-
kind: "raw",
|
|
27
|
-
html
|
|
28
|
-
});
|
|
29
|
-
const INLINE_HTML_TAGS = /* @__PURE__ */ new Set([
|
|
30
|
-
"a",
|
|
31
|
-
"span",
|
|
32
|
-
"strong",
|
|
33
|
-
"em",
|
|
34
|
-
"i",
|
|
35
|
-
"title"
|
|
36
|
-
]);
|
|
37
|
-
const escapeHtmlText = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
38
|
-
const escapeHtmlAttribute = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll("\"", """);
|
|
39
|
-
const serializeStyle = (style) => style.map((declaration) => `${declaration.property}:${declaration.value}`).join(";");
|
|
40
|
-
const serializeOpenTag = (node, selfClose) => {
|
|
41
|
-
let result = `<${node.tag}`;
|
|
42
|
-
for (const [name, value] of node.attrs) result += value === "" ? ` ${name}=""` : ` ${name}="${escapeHtmlAttribute(value)}"`;
|
|
43
|
-
if (node.style.length > 0) result += ` style="${escapeHtmlAttribute(serializeStyle(node.style))}"`;
|
|
44
|
-
return result + (selfClose ? "/>" : ">");
|
|
45
|
-
};
|
|
46
|
-
const isInlineNode = (node) => node.kind === "fragment" ? false : node.kind !== "element" || INLINE_HTML_TAGS.has(node.tag);
|
|
47
|
-
const serializeHtmlNode = (node) => {
|
|
48
|
-
if (node.kind === "text") return escapeHtmlText(node.value);
|
|
49
|
-
if (node.kind === "raw") return node.html;
|
|
50
|
-
if (node.kind === "fragment") return node.children.map(serializeHtmlNode).join("\n");
|
|
51
|
-
if (node.void === true || node.children.length === 0) {
|
|
52
|
-
if (node.void === true) return serializeOpenTag(node, true);
|
|
53
|
-
return `${serializeOpenTag(node, false)}</${node.tag}>`;
|
|
54
|
-
}
|
|
55
|
-
const inlineContent = node.children.every(isInlineNode);
|
|
56
|
-
const children = node.children.map(serializeHtmlNode);
|
|
57
|
-
if (inlineContent) return `${serializeOpenTag(node, false)}${children.join("")}</${node.tag}>`;
|
|
58
|
-
return `${serializeOpenTag(node, false)}\n${children.join("\n")}\n</${node.tag}>`;
|
|
59
|
-
};
|
|
60
|
-
const GRADIENT_PROPERTIES = /* @__PURE__ */ new Set([
|
|
61
|
-
GRADIENT_DIRECTION_PROPERTY,
|
|
62
|
-
GRADIENT_FROM_PROPERTY,
|
|
63
|
-
GRADIENT_TO_PROPERTY
|
|
64
|
-
]);
|
|
65
|
-
const isGradientClass = (resolved) => resolved.declarations.some((declaration) => GRADIENT_PROPERTIES.has(declaration.property));
|
|
66
|
-
const combineGradient = (state, baseClasses) => {
|
|
67
|
-
const gradientClasses = baseClasses.filter(isGradientClass);
|
|
68
|
-
if (gradientClasses.length === 0) return baseClasses.flatMap((resolved) => [...resolved.declarations]);
|
|
69
|
-
const slots = /* @__PURE__ */ new Map();
|
|
70
|
-
for (const resolved of gradientClasses) for (const declaration of resolved.declarations) slots.set(declaration.property, declaration.value);
|
|
71
|
-
const direction = slots.get(GRADIENT_DIRECTION_PROPERTY);
|
|
72
|
-
const from = slots.get(GRADIENT_FROM_PROPERTY);
|
|
73
|
-
const to = slots.get(GRADIENT_TO_PROPERTY);
|
|
74
|
-
const anchor = gradientClasses[0];
|
|
75
|
-
const missing = [];
|
|
76
|
-
if (direction === void 0) missing.push("a direction (bg-gradient-to-b/t/l/r)");
|
|
77
|
-
if (from === void 0) missing.push("a start color (from-{color})");
|
|
78
|
-
if (to === void 0) missing.push("an end color (to-{color})");
|
|
79
|
-
if (missing.length > 0) {
|
|
80
|
-
state.errors.push({
|
|
81
|
-
code: "incomplete-gradient",
|
|
82
|
-
message: `incomplete gradient near "${anchor.raw}" — add ${missing.join(" and ")}`,
|
|
83
|
-
span: anchor.span
|
|
84
|
-
});
|
|
85
|
-
return baseClasses.filter((resolved) => !isGradientClass(resolved)).flatMap((resolved) => [...resolved.declarations]);
|
|
86
|
-
}
|
|
87
|
-
const combined = [{
|
|
88
|
-
property: "background-color",
|
|
89
|
-
value: from
|
|
90
|
-
}];
|
|
91
|
-
if (state.base === "modern") combined.push({
|
|
92
|
-
property: "background-image",
|
|
93
|
-
value: `linear-gradient(${direction},${from},${to})`
|
|
94
|
-
});
|
|
95
|
-
const out = [];
|
|
96
|
-
for (const resolved of baseClasses) {
|
|
97
|
-
if (isGradientClass(resolved)) {
|
|
98
|
-
if (resolved === anchor) out.push(...combined);
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
out.push(...resolved.declarations);
|
|
102
|
-
}
|
|
103
|
-
return out;
|
|
104
|
-
};
|
|
105
|
-
const resolveNodeStyles = (state, node, defaults = {
|
|
106
|
-
inline: [],
|
|
107
|
-
dark: []
|
|
108
|
-
}) => {
|
|
109
|
-
const classAttribute = node.attributes.find((attribute) => attribute.name === "class");
|
|
110
|
-
if (classAttribute === void 0 || classAttribute.value.kind !== "string") {
|
|
111
|
-
if (defaults.dark.length === 0) return {
|
|
112
|
-
inline: [...defaults.inline],
|
|
113
|
-
className: null
|
|
114
|
-
};
|
|
115
|
-
const className = `sml-${node.id}`;
|
|
116
|
-
state.darkRules.push({
|
|
117
|
-
selector: `.${className}`,
|
|
118
|
-
declarations: defaults.dark
|
|
119
|
-
});
|
|
120
|
-
return {
|
|
121
|
-
inline: [...defaults.inline],
|
|
122
|
-
className
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
const resolution = resolveClasses(classAttribute.value.value, state.theme, classAttribute.valueSpan.start);
|
|
126
|
-
state.errors.push(...resolution.errors);
|
|
127
|
-
state.warnings.push(...resolution.warnings);
|
|
128
|
-
const byVariant = (variant) => resolution.classes.filter((resolved) => resolved.variant === variant);
|
|
129
|
-
const base = byVariant("base");
|
|
130
|
-
const inline = [...defaults.inline, ...combineGradient(state, base)];
|
|
131
|
-
const smDeclarations = byVariant("sm").flatMap((resolved) => [...resolved.declarations]);
|
|
132
|
-
const darkDeclarations = [
|
|
133
|
-
...defaults.dark,
|
|
134
|
-
...base.flatMap((resolved) => [...resolved.darkDeclarations ?? []]),
|
|
135
|
-
...byVariant("dark").flatMap((resolved) => [...resolved.declarations])
|
|
136
|
-
];
|
|
137
|
-
if (smDeclarations.length === 0 && darkDeclarations.length === 0) return {
|
|
138
|
-
inline,
|
|
139
|
-
className: null
|
|
140
|
-
};
|
|
141
|
-
const className = `sml-${node.id}`;
|
|
142
|
-
if (smDeclarations.length > 0) state.smRules.push({
|
|
143
|
-
selector: `.${className}`,
|
|
144
|
-
declarations: smDeclarations
|
|
145
|
-
});
|
|
146
|
-
if (darkDeclarations.length > 0) state.darkRules.push({
|
|
147
|
-
selector: `.${className}`,
|
|
148
|
-
declarations: darkDeclarations
|
|
149
|
-
});
|
|
150
|
-
return {
|
|
151
|
-
inline,
|
|
152
|
-
className
|
|
153
|
-
};
|
|
154
|
-
};
|
|
155
|
-
/**
|
|
156
|
-
* Inline anchors are deliberately a narrower surface than block primitives:
|
|
157
|
-
* classes resolve through the exact same safe registry, but source IDs stay on
|
|
158
|
-
* their block owner so the canvas does not expose links as independent nodes.
|
|
159
|
-
*
|
|
160
|
-
* A link always has a concrete color and decoration. That prevents each email
|
|
161
|
-
* client from choosing its browser-default blue/underline treatment; authored
|
|
162
|
-
* classes follow these defaults and therefore win by normal CSS last-wins.
|
|
163
|
-
*/
|
|
164
|
-
const resolveInlineLinkStyles = (state, node) => resolveNodeStyles(state, node, {
|
|
165
|
-
inline: [{
|
|
166
|
-
property: "color",
|
|
167
|
-
value: state.theme.colors.brand
|
|
168
|
-
}, {
|
|
169
|
-
property: "text-decoration",
|
|
170
|
-
value: "underline"
|
|
171
|
-
}],
|
|
172
|
-
dark: state.theme.colorsDark.brand === void 0 ? [] : [{
|
|
173
|
-
property: "color",
|
|
174
|
-
value: state.theme.colorsDark.brand
|
|
175
|
-
}]
|
|
176
|
-
});
|
|
177
|
-
const nodeAttrs = (state, node, styles, extra = [], options) => {
|
|
178
|
-
const attrs = state.stripInstrumentation ? [...extra] : [["data-sml-id", node.id], ...extra];
|
|
179
|
-
const classNames = [styles.className, options?.extraClassName].filter((className) => className !== null && className !== void 0);
|
|
180
|
-
if (classNames.length > 0) attrs.push(["class", classNames.join(" ")]);
|
|
181
|
-
return attrs;
|
|
182
|
-
};
|
|
183
|
-
const isPaddingProperty = (property) => property === "padding" || property.startsWith("padding-");
|
|
184
|
-
const splitPadding = (declarations) => {
|
|
185
|
-
const padding = [];
|
|
186
|
-
const rest = [];
|
|
187
|
-
for (const declaration of declarations) (isPaddingProperty(declaration.property) ? padding : rest).push(declaration);
|
|
188
|
-
return {
|
|
189
|
-
padding,
|
|
190
|
-
rest
|
|
191
|
-
};
|
|
192
|
-
};
|
|
193
|
-
const BACKGROUND_POSITION_DEFAULT = "center";
|
|
194
|
-
const BACKGROUND_SIZE_DEFAULT = "cover";
|
|
195
|
-
const backgroundImageUrl = (node) => {
|
|
196
|
-
const url = stringAttribute(node, "backgroundImage");
|
|
197
|
-
return url === void 0 || url === "" ? void 0 : url;
|
|
198
|
-
};
|
|
199
|
-
const backgroundImageStyle = (node) => {
|
|
200
|
-
const url = backgroundImageUrl(node);
|
|
201
|
-
if (url === void 0) return [];
|
|
202
|
-
return [
|
|
203
|
-
{
|
|
204
|
-
property: "background-image",
|
|
205
|
-
value: `url('${escapeCssString(url)}')`
|
|
206
|
-
},
|
|
207
|
-
{
|
|
208
|
-
property: "background-position",
|
|
209
|
-
value: stringAttribute(node, "backgroundPosition") ?? BACKGROUND_POSITION_DEFAULT
|
|
210
|
-
},
|
|
211
|
-
{
|
|
212
|
-
property: "background-size",
|
|
213
|
-
value: stringAttribute(node, "backgroundSize") ?? BACKGROUND_SIZE_DEFAULT
|
|
214
|
-
},
|
|
215
|
-
{
|
|
216
|
-
property: "background-repeat",
|
|
217
|
-
value: "no-repeat"
|
|
218
|
-
}
|
|
219
|
-
];
|
|
220
|
-
};
|
|
221
|
-
const MAX_FONT_WIDTH = 5;
|
|
222
|
-
const computeFontWidthAndSpaceCount = (expectedWidth) => {
|
|
223
|
-
if (expectedWidth === 0) return [0, 0];
|
|
224
|
-
let spaceCount = 0;
|
|
225
|
-
const requiredFontWidth = () => spaceCount > 0 ? expectedWidth / spaceCount / 2 : Number.POSITIVE_INFINITY;
|
|
226
|
-
while (requiredFontWidth() > MAX_FONT_WIDTH) spaceCount++;
|
|
227
|
-
return [requiredFontWidth(), spaceCount];
|
|
228
|
-
};
|
|
229
|
-
const pxToPt = (px) => px * 3 / 4;
|
|
230
|
-
const paddingPx = (declarations) => {
|
|
231
|
-
const padding = {
|
|
232
|
-
top: 0,
|
|
233
|
-
right: 0,
|
|
234
|
-
bottom: 0,
|
|
235
|
-
left: 0
|
|
236
|
-
};
|
|
237
|
-
const parse = (value) => {
|
|
238
|
-
const match = /^([\d.]+)px$/.exec(value);
|
|
239
|
-
return match === null ? 0 : Number.parseFloat(match[1]);
|
|
240
|
-
};
|
|
241
|
-
for (const declaration of declarations) switch (declaration.property) {
|
|
242
|
-
case "padding":
|
|
243
|
-
padding.top = padding.right = padding.bottom = padding.left = parse(declaration.value);
|
|
244
|
-
break;
|
|
245
|
-
case "padding-top":
|
|
246
|
-
padding.top = parse(declaration.value);
|
|
247
|
-
break;
|
|
248
|
-
case "padding-right":
|
|
249
|
-
padding.right = parse(declaration.value);
|
|
250
|
-
break;
|
|
251
|
-
case "padding-bottom":
|
|
252
|
-
padding.bottom = parse(declaration.value);
|
|
253
|
-
break;
|
|
254
|
-
case "padding-left": padding.left = parse(declaration.value);
|
|
255
|
-
}
|
|
256
|
-
return padding;
|
|
257
|
-
};
|
|
258
|
-
const PREVIEW_MAX_LENGTH = 200;
|
|
259
|
-
const PREVIEW_WHITESPACE = "\xA0";
|
|
260
|
-
const TABLE_RESET = [
|
|
261
|
-
["border", "0"],
|
|
262
|
-
["cellpadding", "0"],
|
|
263
|
-
["cellspacing", "0"],
|
|
264
|
-
["role", "presentation"]
|
|
265
|
-
];
|
|
266
|
-
const expandInline = (state, nodes) => nodes.map((node) => {
|
|
267
|
-
if (node.kind === "text") return text(node.value);
|
|
268
|
-
if (node.kind === "interpolation") return text(node.syntax === "curly" ? `{{${node.name}}}` : `\${${node.name}}`);
|
|
269
|
-
if (node.tag === "a") {
|
|
270
|
-
const styles = resolveInlineLinkStyles(state, node);
|
|
271
|
-
const attrs = [["href", stringAttribute(node, "href") ?? ""], ["target", "_blank"]];
|
|
272
|
-
if (styles.className !== null) attrs.push(["class", styles.className]);
|
|
273
|
-
return element("a", attrs, styles.inline, expandInline(state, node.children));
|
|
274
|
-
}
|
|
275
|
-
return element(node.tag, [], [], expandInline(state, node.children));
|
|
276
|
-
});
|
|
277
|
-
const expandBlock = (state, node) => {
|
|
278
|
-
const styles = resolveNodeStyles(state, node);
|
|
279
|
-
switch (node.tag) {
|
|
280
|
-
case "Container":
|
|
281
|
-
case "Section": {
|
|
282
|
-
const { padding, rest } = splitPadding(styles.inline);
|
|
283
|
-
const bgStyle = backgroundImageStyle(node);
|
|
284
|
-
const bgUrl = backgroundImageUrl(node);
|
|
285
|
-
const children = node.children.flatMap((child) => child.kind === "element" ? [expandBlock(state, child)] : []);
|
|
286
|
-
if (state.base === "modern") {
|
|
287
|
-
const style = node.tag === "Container" ? [
|
|
288
|
-
{
|
|
289
|
-
property: "max-width",
|
|
290
|
-
value: `${state.emailWidth}px`
|
|
291
|
-
},
|
|
292
|
-
{
|
|
293
|
-
property: "margin-left",
|
|
294
|
-
value: "auto"
|
|
295
|
-
},
|
|
296
|
-
{
|
|
297
|
-
property: "margin-right",
|
|
298
|
-
value: "auto"
|
|
299
|
-
}
|
|
300
|
-
] : [];
|
|
301
|
-
return element("div", nodeAttrs(state, node, styles), [
|
|
302
|
-
...style,
|
|
303
|
-
...rest,
|
|
304
|
-
...bgStyle,
|
|
305
|
-
...padding
|
|
306
|
-
], children);
|
|
307
|
-
}
|
|
308
|
-
const tableStyle = node.tag === "Container" ? [{
|
|
309
|
-
property: "max-width",
|
|
310
|
-
value: `${state.emailWidth}px`
|
|
311
|
-
}, ...rest] : rest;
|
|
312
|
-
const tdAttrs = bgUrl === void 0 ? [] : [["background", bgUrl]];
|
|
313
|
-
return element("table", nodeAttrs(state, node, styles, [
|
|
314
|
-
["align", "center"],
|
|
315
|
-
["width", "100%"],
|
|
316
|
-
...TABLE_RESET
|
|
317
|
-
]), tableStyle, [element("tbody", [], [], [element("tr", [], [{
|
|
318
|
-
property: "width",
|
|
319
|
-
value: "100%"
|
|
320
|
-
}], [element("td", tdAttrs, [...bgStyle, ...padding], children)])])]);
|
|
321
|
-
}
|
|
322
|
-
case "Row": {
|
|
323
|
-
const columns = node.children.flatMap((child) => child.kind === "element" ? [expandBlock(state, child)] : []);
|
|
324
|
-
const { padding, rest } = splitPadding(styles.inline);
|
|
325
|
-
const rowTable = (tableStyle) => element("table", nodeAttrs(state, node, styles, [
|
|
326
|
-
["align", "center"],
|
|
327
|
-
["width", "100%"],
|
|
328
|
-
...TABLE_RESET
|
|
329
|
-
]), tableStyle, [element("tbody", [], [{
|
|
330
|
-
property: "width",
|
|
331
|
-
value: "100%"
|
|
332
|
-
}], [element("tr", [], [{
|
|
333
|
-
property: "width",
|
|
334
|
-
value: "100%"
|
|
335
|
-
}], columns)])]);
|
|
336
|
-
if (state.base === "modern" || padding.length === 0) return rowTable(styles.inline);
|
|
337
|
-
return element("table", [
|
|
338
|
-
["align", "center"],
|
|
339
|
-
["width", "100%"],
|
|
340
|
-
...TABLE_RESET
|
|
341
|
-
], [], [element("tbody", [], [], [element("tr", [], [{
|
|
342
|
-
property: "width",
|
|
343
|
-
value: "100%"
|
|
344
|
-
}], [element("td", [], padding, [rowTable(rest)])])])]);
|
|
345
|
-
}
|
|
346
|
-
case "Column": {
|
|
347
|
-
const width = stringAttribute(node, "width");
|
|
348
|
-
const valign = stringAttribute(node, "valign");
|
|
349
|
-
const children = node.children.flatMap((child) => child.kind === "element" ? [expandBlock(state, child)] : []);
|
|
350
|
-
return element("td", nodeAttrs(state, node, styles, [...width === void 0 ? [] : [["width", width]], ...valign === void 0 ? [] : [["valign", valign]]]), [...valign === void 0 ? [] : [{
|
|
351
|
-
property: "vertical-align",
|
|
352
|
-
value: valign
|
|
353
|
-
}], ...styles.inline], children);
|
|
354
|
-
}
|
|
355
|
-
case "Heading": {
|
|
356
|
-
const level = numberAttribute(node, "level") ?? 1;
|
|
357
|
-
return element(`h${level}`, nodeAttrs(state, node, styles), styles.inline, expandInline(state, node.children));
|
|
358
|
-
}
|
|
359
|
-
case "Text": return element("p", nodeAttrs(state, node, styles), [...[
|
|
360
|
-
{
|
|
361
|
-
property: "font-size",
|
|
362
|
-
value: "14px"
|
|
363
|
-
},
|
|
364
|
-
{
|
|
365
|
-
property: "line-height",
|
|
366
|
-
value: "24px"
|
|
367
|
-
},
|
|
368
|
-
{
|
|
369
|
-
property: "margin-top",
|
|
370
|
-
value: "16px"
|
|
371
|
-
},
|
|
372
|
-
{
|
|
373
|
-
property: "margin-bottom",
|
|
374
|
-
value: "16px"
|
|
375
|
-
}
|
|
376
|
-
], ...styles.inline], expandInline(state, node.children));
|
|
377
|
-
case "Button": return expandButton(state, node, styles);
|
|
378
|
-
case "List": {
|
|
379
|
-
const tag = stringAttribute(node, "type") === "ordered" ? "ol" : "ul";
|
|
380
|
-
const defaults = [
|
|
381
|
-
{
|
|
382
|
-
property: "margin-top",
|
|
383
|
-
value: "16px"
|
|
384
|
-
},
|
|
385
|
-
{
|
|
386
|
-
property: "margin-bottom",
|
|
387
|
-
value: "16px"
|
|
388
|
-
},
|
|
389
|
-
{
|
|
390
|
-
property: "padding-left",
|
|
391
|
-
value: "24px"
|
|
392
|
-
}
|
|
393
|
-
];
|
|
394
|
-
const items = node.children.flatMap((child) => child.kind === "element" ? [expandBlock(state, child)] : []);
|
|
395
|
-
return element(tag, nodeAttrs(state, node, styles), [...defaults, ...styles.inline], items);
|
|
396
|
-
}
|
|
397
|
-
case "ListItem": return element("li", nodeAttrs(state, node, styles), [...[
|
|
398
|
-
{
|
|
399
|
-
property: "font-size",
|
|
400
|
-
value: "14px"
|
|
401
|
-
},
|
|
402
|
-
{
|
|
403
|
-
property: "line-height",
|
|
404
|
-
value: "24px"
|
|
405
|
-
},
|
|
406
|
-
{
|
|
407
|
-
property: "margin-top",
|
|
408
|
-
value: "0px"
|
|
409
|
-
},
|
|
410
|
-
{
|
|
411
|
-
property: "margin-bottom",
|
|
412
|
-
value: "8px"
|
|
413
|
-
}
|
|
414
|
-
], ...styles.inline], expandInline(state, node.children));
|
|
415
|
-
case "If": {
|
|
416
|
-
const test = stringAttribute(node, "test") ?? "";
|
|
417
|
-
const elements = node.children.filter((child) => child.kind === "element");
|
|
418
|
-
const elseNode = elements.find((child) => child.tag === "Else");
|
|
419
|
-
const children = [raw(`<!--sml:if:${node.id} ${test}-->`)];
|
|
420
|
-
for (const child of elements) if (child.tag !== "Else") children.push(expandBlock(state, child));
|
|
421
|
-
if (elseNode !== void 0) {
|
|
422
|
-
children.push(raw(`<!--sml:else:${node.id}-->`));
|
|
423
|
-
for (const child of elseNode.children) if (child.kind === "element") children.push(expandBlock(state, child));
|
|
424
|
-
}
|
|
425
|
-
children.push(raw(`<!--sml:endif:${node.id}-->`));
|
|
426
|
-
return {
|
|
427
|
-
kind: "fragment",
|
|
428
|
-
children
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
case "Image": {
|
|
432
|
-
const width = numberAttribute(node, "width");
|
|
433
|
-
const height = numberAttribute(node, "height");
|
|
434
|
-
const src = stringAttribute(node, "src") ?? "";
|
|
435
|
-
const alt = stringAttribute(node, "alt") ?? "";
|
|
436
|
-
const darkSource = node.children.find((child) => child.kind === "element" && child.tag === "Source" && stringAttribute(child, "scheme") === "dark");
|
|
437
|
-
const darkImageSource = darkSource === void 0 ? void 0 : stringAttribute(darkSource, "src");
|
|
438
|
-
const attrs = nodeAttrs(state, node, styles, [
|
|
439
|
-
["alt", alt],
|
|
440
|
-
["src", src],
|
|
441
|
-
...width === void 0 ? [] : [["width", String(width)]],
|
|
442
|
-
...height === void 0 ? [] : [["height", String(height)]]
|
|
443
|
-
]);
|
|
444
|
-
const reset = [
|
|
445
|
-
{
|
|
446
|
-
property: "display",
|
|
447
|
-
value: "block"
|
|
448
|
-
},
|
|
449
|
-
{
|
|
450
|
-
property: "outline",
|
|
451
|
-
value: "none"
|
|
452
|
-
},
|
|
453
|
-
{
|
|
454
|
-
property: "border",
|
|
455
|
-
value: "none"
|
|
456
|
-
},
|
|
457
|
-
{
|
|
458
|
-
property: "text-decoration",
|
|
459
|
-
value: "none"
|
|
460
|
-
}
|
|
461
|
-
];
|
|
462
|
-
if (darkImageSource !== void 0) {
|
|
463
|
-
const lightClassName = `sml-${node.id}-theme-light`;
|
|
464
|
-
const darkClassName = `sml-${node.id}-theme-dark`;
|
|
465
|
-
state.darkRules.push({
|
|
466
|
-
selector: state.stripInstrumentation ? `.${lightClassName}` : `[data-sml-id="${node.id}"][data-sml-theme="light"]`,
|
|
467
|
-
declarations: [{
|
|
468
|
-
property: "display",
|
|
469
|
-
value: "none"
|
|
470
|
-
}]
|
|
471
|
-
}, {
|
|
472
|
-
selector: state.stripInstrumentation ? `.${darkClassName}` : `[data-sml-id="${node.id}"][data-sml-theme="dark"]`,
|
|
473
|
-
declarations: [{
|
|
474
|
-
property: "display",
|
|
475
|
-
value: "block"
|
|
476
|
-
}]
|
|
477
|
-
});
|
|
478
|
-
const themedAttrs = (theme, imageSrc) => {
|
|
479
|
-
const themeClassName = theme === "light" ? lightClassName : darkClassName;
|
|
480
|
-
return nodeAttrs(state, node, styles, [
|
|
481
|
-
...state.stripInstrumentation ? [] : [["data-sml-theme", theme]],
|
|
482
|
-
["alt", alt],
|
|
483
|
-
["src", imageSrc],
|
|
484
|
-
...width === void 0 ? [] : [["width", String(width)]],
|
|
485
|
-
...height === void 0 ? [] : [["height", String(height)]]
|
|
486
|
-
], state.stripInstrumentation ? { extraClassName: themeClassName } : void 0);
|
|
487
|
-
};
|
|
488
|
-
return {
|
|
489
|
-
kind: "fragment",
|
|
490
|
-
children: [element("img", themedAttrs("light", src), [...reset, ...styles.inline], [], { void: true }), element("img", themedAttrs("dark", darkImageSource), [
|
|
491
|
-
{
|
|
492
|
-
property: "display",
|
|
493
|
-
value: "none"
|
|
494
|
-
},
|
|
495
|
-
...reset.slice(1),
|
|
496
|
-
...styles.inline
|
|
497
|
-
], [], { void: true })]
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
return element("img", attrs, [...reset, ...styles.inline], [], { void: true });
|
|
501
|
-
}
|
|
502
|
-
case "Divider": return element("hr", nodeAttrs(state, node, styles), [...[
|
|
503
|
-
{
|
|
504
|
-
property: "width",
|
|
505
|
-
value: "100%"
|
|
506
|
-
},
|
|
507
|
-
{
|
|
508
|
-
property: "border",
|
|
509
|
-
value: "none"
|
|
510
|
-
},
|
|
511
|
-
{
|
|
512
|
-
property: "border-top",
|
|
513
|
-
value: "1px solid #eaeaea"
|
|
514
|
-
}
|
|
515
|
-
], ...styles.inline], [], { void: true });
|
|
516
|
-
case "Spacer": {
|
|
517
|
-
const height = numberAttribute(node, "height") ?? 0;
|
|
518
|
-
return element("div", nodeAttrs(state, node, styles), [
|
|
519
|
-
{
|
|
520
|
-
property: "height",
|
|
521
|
-
value: `${height}px`
|
|
522
|
-
},
|
|
523
|
-
{
|
|
524
|
-
property: "line-height",
|
|
525
|
-
value: `${height}px`
|
|
526
|
-
},
|
|
527
|
-
{
|
|
528
|
-
property: "font-size",
|
|
529
|
-
value: "1px"
|
|
530
|
-
}
|
|
531
|
-
], [raw(" ")]);
|
|
532
|
-
}
|
|
533
|
-
default:
|
|
534
|
-
state.errors.push({
|
|
535
|
-
code: "internal-unexpected-node",
|
|
536
|
-
message: `unexpected <${node.tag}> during expansion`,
|
|
537
|
-
span: node.span
|
|
538
|
-
});
|
|
539
|
-
return raw("");
|
|
540
|
-
}
|
|
541
|
-
};
|
|
542
|
-
const expandButton = (state, node, styles) => {
|
|
543
|
-
const href = stringAttribute(node, "href") ?? "";
|
|
544
|
-
const label = expandInline(state, node.children);
|
|
545
|
-
const baseStyle = [
|
|
546
|
-
{
|
|
547
|
-
property: "line-height",
|
|
548
|
-
value: "100%"
|
|
549
|
-
},
|
|
550
|
-
{
|
|
551
|
-
property: "text-decoration",
|
|
552
|
-
value: "none"
|
|
553
|
-
},
|
|
554
|
-
{
|
|
555
|
-
property: "display",
|
|
556
|
-
value: "inline-block"
|
|
557
|
-
},
|
|
558
|
-
{
|
|
559
|
-
property: "max-width",
|
|
560
|
-
value: "100%"
|
|
561
|
-
},
|
|
562
|
-
{
|
|
563
|
-
property: "mso-padding-alt",
|
|
564
|
-
value: "0px"
|
|
565
|
-
}
|
|
566
|
-
];
|
|
567
|
-
const attrs = nodeAttrs(state, node, styles, [["href", href], ["target", "_blank"]]);
|
|
568
|
-
if (state.base === "modern") return element("a", attrs, [...baseStyle, ...styles.inline], label);
|
|
569
|
-
const padding = paddingPx(styles.inline);
|
|
570
|
-
const verticalPadding = padding.top + padding.bottom;
|
|
571
|
-
const textRaise = pxToPt(verticalPadding);
|
|
572
|
-
const [leftFontWidth, leftSpaceCount] = computeFontWidthAndSpaceCount(padding.left);
|
|
573
|
-
const [rightFontWidth, rightSpaceCount] = computeFontWidthAndSpaceCount(padding.right);
|
|
574
|
-
const innerSpanStyle = [
|
|
575
|
-
{
|
|
576
|
-
property: "max-width",
|
|
577
|
-
value: "100%"
|
|
578
|
-
},
|
|
579
|
-
{
|
|
580
|
-
property: "display",
|
|
581
|
-
value: "inline-block"
|
|
582
|
-
},
|
|
583
|
-
{
|
|
584
|
-
property: "line-height",
|
|
585
|
-
value: "120%"
|
|
586
|
-
},
|
|
587
|
-
{
|
|
588
|
-
property: "mso-padding-alt",
|
|
589
|
-
value: "0px"
|
|
590
|
-
},
|
|
591
|
-
...padding.bottom > 0 ? [{
|
|
592
|
-
property: "mso-text-raise",
|
|
593
|
-
value: `${pxToPt(padding.bottom)}`
|
|
594
|
-
}] : []
|
|
595
|
-
];
|
|
596
|
-
return element("a", attrs, [...baseStyle, ...styles.inline], [
|
|
597
|
-
element("span", [], [], [raw(`<!--[if mso]><i style="mso-font-width:${leftFontWidth * 100}%;mso-text-raise:${textRaise}" hidden>${" ".repeat(leftSpaceCount)}</i><![endif]-->`)]),
|
|
598
|
-
element("span", [], innerSpanStyle, label),
|
|
599
|
-
element("span", [], [], [raw(`<!--[if mso]><i style="mso-font-width:${rightFontWidth * 100}%" hidden>${" ".repeat(rightSpaceCount)}​</i><![endif]-->`)])
|
|
600
|
-
]);
|
|
601
|
-
};
|
|
602
|
-
const expandPreview = (state, node) => {
|
|
603
|
-
let previewText = scalarElementText(node);
|
|
604
|
-
let literalLength = 0;
|
|
605
|
-
for (const child of node.children) if (child.kind === "text") literalLength += child.value.length;
|
|
606
|
-
previewText = previewText.substring(0, PREVIEW_MAX_LENGTH);
|
|
607
|
-
const guaranteedLength = Math.min(literalLength, PREVIEW_MAX_LENGTH);
|
|
608
|
-
const children = [text(previewText)];
|
|
609
|
-
if (guaranteedLength < PREVIEW_MAX_LENGTH) children.push(element("div", [], [], [text(PREVIEW_WHITESPACE.repeat(PREVIEW_MAX_LENGTH - guaranteedLength))]));
|
|
610
|
-
return element("div", [...state.stripInstrumentation ? [] : [["data-sml-id", node.id]], ["data-skip-in-text", "true"]], [
|
|
611
|
-
{
|
|
612
|
-
property: "display",
|
|
613
|
-
value: "none"
|
|
614
|
-
},
|
|
615
|
-
{
|
|
616
|
-
property: "overflow",
|
|
617
|
-
value: "hidden"
|
|
618
|
-
},
|
|
619
|
-
{
|
|
620
|
-
property: "line-height",
|
|
621
|
-
value: "1px"
|
|
622
|
-
},
|
|
623
|
-
{
|
|
624
|
-
property: "opacity",
|
|
625
|
-
value: "0"
|
|
626
|
-
},
|
|
627
|
-
{
|
|
628
|
-
property: "max-height",
|
|
629
|
-
value: "0"
|
|
630
|
-
},
|
|
631
|
-
{
|
|
632
|
-
property: "max-width",
|
|
633
|
-
value: "0"
|
|
634
|
-
}
|
|
635
|
-
], children);
|
|
636
|
-
};
|
|
637
|
-
const serializeRules = (rules, indent, important) => rules.map((rule) => `${indent}${rule.selector}{${rule.declarations.map((d) => `${d.property}:${d.value}${important ? " !important" : ""}`).join(";")}}`).join("\n");
|
|
638
|
-
const isBackgroundProperty = (property) => property.startsWith("background");
|
|
639
|
-
const buildHeadStyle = (state) => {
|
|
640
|
-
const parts = [":root{color-scheme:light dark;supported-color-schemes:light dark}"];
|
|
641
|
-
if (state.smRules.length > 0) parts.push(`@media (min-width:640px){\n${serializeRules(state.smRules, " ", true)}\n}`);
|
|
642
|
-
if (state.darkRules.length > 0) {
|
|
643
|
-
parts.push(`@media (prefers-color-scheme:dark){\n${serializeRules(state.darkRules, " ", true)}\n}`);
|
|
644
|
-
const ogscRules = state.darkRules.flatMap((rule) => {
|
|
645
|
-
const declarations = rule.declarations.filter((d) => !isBackgroundProperty(d.property));
|
|
646
|
-
return declarations.length > 0 ? [{
|
|
647
|
-
selector: `[data-ogsc] ${rule.selector}`,
|
|
648
|
-
declarations
|
|
649
|
-
}] : [];
|
|
650
|
-
});
|
|
651
|
-
const ogsbRules = state.darkRules.flatMap((rule) => {
|
|
652
|
-
const declarations = rule.declarations.filter((d) => isBackgroundProperty(d.property));
|
|
653
|
-
return declarations.length > 0 ? [{
|
|
654
|
-
selector: `[data-ogsb] ${rule.selector}`,
|
|
655
|
-
declarations
|
|
656
|
-
}] : [];
|
|
657
|
-
});
|
|
658
|
-
if (ogscRules.length > 0) parts.push(serializeRules(ogscRules, "", true));
|
|
659
|
-
if (ogsbRules.length > 0) parts.push(serializeRules(ogsbRules, "", true));
|
|
660
|
-
}
|
|
661
|
-
return parts.join("\n");
|
|
662
|
-
};
|
|
663
|
-
/**
|
|
664
|
-
* The @font-face block for self-hosted webfont tokens, emitted into a SEPARATE
|
|
665
|
-
* <style> that is hidden from Outlook for Windows.
|
|
666
|
-
*
|
|
667
|
-
* Outlook-Windows must not SEE these rules: it would register an @font-face whose
|
|
668
|
-
* family matches a used font, fail to load the distant src, and render the text in
|
|
669
|
-
* Times New Roman — actively ignoring the inline font-family stack. Hiding the
|
|
670
|
-
* block behind a downlevel-hidden conditional comment makes Outlook treat the font
|
|
671
|
-
* as merely missing, so it falls through the inline stack to its generic instead.
|
|
672
|
-
* (The head's OTHER <style> stays visible — it carries the Outlook.com [data-ogsc]
|
|
673
|
-
* dark rules — so only this sibling is wrapped.) Empty when no token self-hosts.
|
|
674
|
-
*/
|
|
675
|
-
const UNICODE_RANGE = /^U\+[0-9A-Fa-f?]{1,6}(-[0-9A-Fa-f]{1,6})?$/;
|
|
676
|
-
const isUnicodeRange = (value) => value.split(",").every((token) => UNICODE_RANGE.test(token.trim()) && token.trim() !== "");
|
|
677
|
-
const buildFontFaceCss = (state) => {
|
|
678
|
-
return Object.entries(state.theme.fontFaces).flatMap(([name, set]) => {
|
|
679
|
-
const family = escapeCssString(set.family);
|
|
680
|
-
const stack = state.theme.fontFamilies[name];
|
|
681
|
-
if (stack === void 0 || !stack.startsWith(`'${family}',`)) return [];
|
|
682
|
-
return set.faces.map((face) => {
|
|
683
|
-
const range = face.unicodeRange && isUnicodeRange(face.unicodeRange) ? `unicode-range:${face.unicodeRange};` : "";
|
|
684
|
-
return `@font-face{font-family:'${family}';font-style:${face.style};font-weight:${face.weight};font-display:swap;src:url('${escapeCssString(face.src)}') format('woff2');${range}}`;
|
|
685
|
-
});
|
|
686
|
-
}).join("");
|
|
687
|
-
};
|
|
688
|
-
const buildFontFaceStyle = (css) => css === "" ? "" : `<!--[if !mso]><!--><style>${css}</style><!--<![endif]-->`;
|
|
689
|
-
const inlinePlainText = (nodes) => {
|
|
690
|
-
let result = "";
|
|
691
|
-
for (const node of nodes) if (node.kind === "text") result += node.value;
|
|
692
|
-
else if (node.kind === "interpolation") result += node.syntax === "curly" ? `{{${node.name}}}` : `\${${node.name}}`;
|
|
693
|
-
else if (node.kind === "element" && node.tag === "a") {
|
|
694
|
-
const label = inlinePlainText(node.children);
|
|
695
|
-
const href = stringAttribute(node, "href") ?? "";
|
|
696
|
-
result += href === "" ? label : `${label} (${href})`;
|
|
697
|
-
} else if (node.kind === "element") result += inlinePlainText(node.children);
|
|
698
|
-
return result;
|
|
699
|
-
};
|
|
700
|
-
const blockPlainText = (node, blocks) => {
|
|
701
|
-
switch (node.tag) {
|
|
702
|
-
case "Subject":
|
|
703
|
-
case "Preview": return;
|
|
704
|
-
case "Heading":
|
|
705
|
-
case "Text": {
|
|
706
|
-
const value = inlinePlainText(node.children).trim();
|
|
707
|
-
if (value.length > 0) blocks.push(value);
|
|
708
|
-
return;
|
|
709
|
-
}
|
|
710
|
-
case "Button": {
|
|
711
|
-
const label = inlinePlainText(node.children).trim();
|
|
712
|
-
const href = stringAttribute(node, "href") ?? "";
|
|
713
|
-
blocks.push(href === "" ? label : `${label} (${href})`);
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
case "Image": {
|
|
717
|
-
const alt = stringAttribute(node, "alt");
|
|
718
|
-
if (alt !== void 0 && alt.length > 0) blocks.push(alt);
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
case "Divider":
|
|
722
|
-
case "Spacer": return;
|
|
723
|
-
case "List": {
|
|
724
|
-
const ordered = stringAttribute(node, "type") === "ordered";
|
|
725
|
-
const lines = [];
|
|
726
|
-
let position = 0;
|
|
727
|
-
for (const child of node.children) {
|
|
728
|
-
if (child.kind !== "element" || child.tag !== "ListItem") continue;
|
|
729
|
-
position += 1;
|
|
730
|
-
const value = inlinePlainText(child.children).trim();
|
|
731
|
-
if (value.length === 0) continue;
|
|
732
|
-
lines.push(ordered ? `${position}. ${value}` : `- ${value}`);
|
|
733
|
-
}
|
|
734
|
-
if (lines.length > 0) blocks.push(lines.join("\n"));
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
737
|
-
case "If": {
|
|
738
|
-
const test = stringAttribute(node, "test") ?? "";
|
|
739
|
-
const elements = node.children.filter((child) => child.kind === "element");
|
|
740
|
-
const elseNode = elements.find((child) => child.tag === "Else");
|
|
741
|
-
blocks.push(`<!--sml:if:${node.id} ${test}-->`);
|
|
742
|
-
for (const child of elements) if (child.tag !== "Else") blockPlainText(child, blocks);
|
|
743
|
-
if (elseNode !== void 0) {
|
|
744
|
-
blocks.push(`<!--sml:else:${node.id}-->`);
|
|
745
|
-
for (const child of elseNode.children) if (child.kind === "element") blockPlainText(child, blocks);
|
|
746
|
-
}
|
|
747
|
-
blocks.push(`<!--sml:endif:${node.id}-->`);
|
|
748
|
-
return;
|
|
749
|
-
}
|
|
750
|
-
default: for (const child of node.children) if (child.kind === "element") blockPlainText(child, blocks);
|
|
751
|
-
}
|
|
752
|
-
};
|
|
753
|
-
const XHTML_DOCTYPE = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">";
|
|
754
|
-
/** Compile a parsed document; the root tag routes to its channel backend. */
|
|
755
|
-
const compileSml = (document, options) => {
|
|
756
|
-
switch (document.root.tag) {
|
|
757
|
-
case "Sms": return Result.succeed(compileSmsRoot(document.root));
|
|
758
|
-
case "WhatsApp": return compileWhatsAppRoot(document.root);
|
|
759
|
-
default: return compileEmailSml(document, options);
|
|
760
|
-
}
|
|
761
|
-
};
|
|
762
|
-
const compileSmlArtifact = (document, options) => {
|
|
8
|
+
/** Compile a structured document; email definitions execute through the email runtime. */
|
|
9
|
+
const compileSml = (document) => {
|
|
763
10
|
switch (document.root.tag) {
|
|
764
11
|
case "Sms": return Result.succeed(compileSmsRoot(document.root));
|
|
765
12
|
case "WhatsApp": return compileWhatsAppRoot(document.root);
|
|
766
|
-
default: return
|
|
13
|
+
default: return Result.fail(new SmlCompileError({ diagnostics: [{
|
|
14
|
+
code: "email-requires-object-template",
|
|
15
|
+
message: "Email templates must default-export defineEmail with an input schema and fixtures.",
|
|
16
|
+
span: document.root.span
|
|
17
|
+
}] }));
|
|
767
18
|
}
|
|
768
19
|
};
|
|
769
|
-
/**
|
|
770
|
-
|
|
771
|
-
* hidden node leaves the artifact exactly as if it were never authored — no
|
|
772
|
-
* stray whitespace, and the surviving nodes keep their positional ids (hiding
|
|
773
|
-
* one node never renumbers the rest). The root itself is never filtered.
|
|
774
|
-
*/
|
|
775
|
-
const stripHiddenNodes = (node) => ({
|
|
776
|
-
...node,
|
|
777
|
-
children: node.children.flatMap((child) => {
|
|
778
|
-
if (child.kind !== "element") return [child];
|
|
779
|
-
return isHiddenNode(child) ? [] : [stripHiddenNodes(child)];
|
|
780
|
-
})
|
|
781
|
-
});
|
|
782
|
-
const addNodeId = (map, id, elementNode) => {
|
|
783
|
-
const current = map[id];
|
|
784
|
-
if (current === void 0) map[id] = [elementNode];
|
|
785
|
-
else current.push(elementNode);
|
|
786
|
-
};
|
|
787
|
-
const collectNodeIdMap = (node, map) => {
|
|
788
|
-
if (node.kind === "fragment") {
|
|
789
|
-
for (const child of node.children) collectNodeIdMap(child, map);
|
|
790
|
-
return;
|
|
791
|
-
}
|
|
792
|
-
if (node.kind !== "element") return;
|
|
793
|
-
const id = node.attrs.find(([name]) => name === "data-sml-id")?.[1];
|
|
794
|
-
if (id !== void 0) addNodeId(map, id, node);
|
|
795
|
-
for (const child of node.children) collectNodeIdMap(child, map);
|
|
796
|
-
};
|
|
797
|
-
const createNodeIdMap = (tree) => {
|
|
798
|
-
const map = {};
|
|
799
|
-
collectNodeIdMap(tree, map);
|
|
800
|
-
return map;
|
|
801
|
-
};
|
|
802
|
-
const compiledEmailFromArtifact = (artifact) => ({
|
|
803
|
-
channel: artifact.channel,
|
|
804
|
-
html: artifact.html,
|
|
805
|
-
text: artifact.text,
|
|
806
|
-
subjectText: artifact.subjectText,
|
|
807
|
-
warnings: artifact.warnings,
|
|
808
|
-
base: artifact.base,
|
|
809
|
-
compilerVersion: artifact.compilerVersion
|
|
810
|
-
});
|
|
811
|
-
const scalarElementText = (node) => {
|
|
812
|
-
let result = "";
|
|
813
|
-
for (const child of node.children) if (child.kind === "text") result += child.value;
|
|
814
|
-
else if (child.kind === "interpolation") result += child.syntax === "curly" ? `{{${child.name}}}` : `\${${child.name}}`;
|
|
815
|
-
return result;
|
|
816
|
-
};
|
|
817
|
-
const compileEmailSml = (inputDocument, options) => {
|
|
818
|
-
const artifact = compileEmailSmlArtifact(inputDocument, options);
|
|
819
|
-
if (Result.isFailure(artifact)) return Result.fail(artifact.failure);
|
|
820
|
-
return Result.succeed(compiledEmailFromArtifact(artifact.success));
|
|
821
|
-
};
|
|
822
|
-
const compileEmailSmlArtifact = (inputDocument, options) => {
|
|
823
|
-
const document = {
|
|
824
|
-
...inputDocument,
|
|
825
|
-
root: stripHiddenNodes(inputDocument.root)
|
|
826
|
-
};
|
|
827
|
-
const base = options?.base ?? "outlook-safe";
|
|
828
|
-
const emailWidth = numberAttribute(document.root, "width") ?? 600;
|
|
829
|
-
const state = {
|
|
830
|
-
base,
|
|
831
|
-
emailWidth,
|
|
832
|
-
theme: resolveTokenTable({
|
|
833
|
-
base: options?.theme?.base ?? baseTokens,
|
|
834
|
-
overrides: options?.theme?.overrides
|
|
835
|
-
}),
|
|
836
|
-
stripInstrumentation: options?.stripInstrumentation ?? false,
|
|
837
|
-
errors: [],
|
|
838
|
-
warnings: [],
|
|
839
|
-
smRules: [],
|
|
840
|
-
darkRules: []
|
|
841
|
-
};
|
|
842
|
-
const root = document.root;
|
|
843
|
-
const rootStyles = resolveNodeStyles(state, root);
|
|
844
|
-
const lang = stringAttribute(root, "lang") ?? "en";
|
|
845
|
-
const dir = stringAttribute(root, "dir") ?? "ltr";
|
|
846
|
-
const preview = root.children.find((child) => child.kind === "element" && child.tag === "Preview");
|
|
847
|
-
const subject = root.children.find((child) => child.kind === "element" && child.tag === "Subject");
|
|
848
|
-
const blocks = root.children.flatMap((child) => child.kind === "element" && child.tag !== "Preview" && child.tag !== "Subject" ? [expandBlock(state, child)] : []);
|
|
849
|
-
if (state.errors.length > 0) return Result.fail(new SmlCompileError({ diagnostics: state.errors }));
|
|
850
|
-
const bodyBackground = rootStyles.inline.filter((d) => isBackgroundProperty(d.property));
|
|
851
|
-
const bodyChildren = [];
|
|
852
|
-
if (preview !== void 0) bodyChildren.push(expandPreview(state, preview));
|
|
853
|
-
bodyChildren.push(element("table", [
|
|
854
|
-
["align", "center"],
|
|
855
|
-
["width", "100%"],
|
|
856
|
-
...TABLE_RESET
|
|
857
|
-
], [], [element("tbody", [], [], [element("tr", [], [], [element("td", [
|
|
858
|
-
["dir", dir],
|
|
859
|
-
["lang", lang],
|
|
860
|
-
...rootStyles.className !== null ? [["class", rootStyles.className]] : []
|
|
861
|
-
], [...rootStyles.inline], blocks)])])]));
|
|
862
|
-
const bodyAttrs = [
|
|
863
|
-
...state.stripInstrumentation ? [] : [["data-sml-id", root.id]],
|
|
864
|
-
["dir", dir],
|
|
865
|
-
["lang", lang]
|
|
866
|
-
];
|
|
867
|
-
if (rootStyles.className !== null) bodyAttrs.push(["class", rootStyles.className]);
|
|
868
|
-
const body = element("body", bodyAttrs, [...bodyBackground, {
|
|
869
|
-
property: "margin",
|
|
870
|
-
value: "0"
|
|
871
|
-
}], bodyChildren);
|
|
872
|
-
const headStyle = buildHeadStyle(state);
|
|
873
|
-
const fontFaceCss = buildFontFaceCss(state);
|
|
874
|
-
const fontFaceStyle = buildFontFaceStyle(fontFaceCss);
|
|
875
|
-
const headAssets = {
|
|
876
|
-
styles: [{
|
|
877
|
-
kind: "style",
|
|
878
|
-
css: headStyle,
|
|
879
|
-
html: `<style>${headStyle}</style>`,
|
|
880
|
-
outlookHidden: false
|
|
881
|
-
}, ...fontFaceCss === "" ? [] : [{
|
|
882
|
-
kind: "style",
|
|
883
|
-
css: fontFaceCss,
|
|
884
|
-
html: fontFaceStyle,
|
|
885
|
-
outlookHidden: true
|
|
886
|
-
}]],
|
|
887
|
-
links: []
|
|
888
|
-
};
|
|
889
|
-
const head = element("head", [], [], [
|
|
890
|
-
element("meta", [["content", "text/html; charset=UTF-8"], ["http-equiv", "Content-Type"]], [], [], { void: true }),
|
|
891
|
-
element("meta", [["name", "viewport"], ["content", "width=device-width"]], [], [], { void: true }),
|
|
892
|
-
element("meta", [["name", "x-apple-disable-message-reformatting"]], [], [], { void: true }),
|
|
893
|
-
element("meta", [["name", "color-scheme"], ["content", "light dark"]], [], [], { void: true }),
|
|
894
|
-
element("meta", [["name", "supported-color-schemes"], ["content", "light dark"]], [], [], { void: true }),
|
|
895
|
-
element("style", [], [], [raw(headStyle)]),
|
|
896
|
-
...fontFaceStyle !== "" ? [raw(fontFaceStyle)] : []
|
|
897
|
-
]);
|
|
898
|
-
const htmlAttrs = base === "outlook-safe" ? [
|
|
899
|
-
["dir", dir],
|
|
900
|
-
["lang", lang],
|
|
901
|
-
["xmlns", "http://www.w3.org/1999/xhtml"],
|
|
902
|
-
["xmlns:o", "urn:schemas-microsoft-com:office:office"],
|
|
903
|
-
["xmlns:v", "urn:schemas-microsoft-com:vml"]
|
|
904
|
-
] : [["dir", dir], ["lang", lang]];
|
|
905
|
-
const doctype = base === "outlook-safe" ? XHTML_DOCTYPE : "<!DOCTYPE html>";
|
|
906
|
-
const tree = element("html", htmlAttrs, [], [head, body]);
|
|
907
|
-
const html = `${doctype}\n${serializeHtmlNode(tree)}\n`;
|
|
908
|
-
const textBlocks = [];
|
|
909
|
-
for (const child of root.children) if (child.kind === "element") blockPlainText(child, textBlocks);
|
|
910
|
-
return Result.succeed({
|
|
911
|
-
channel: "email",
|
|
912
|
-
width: emailWidth,
|
|
913
|
-
html,
|
|
914
|
-
text: textBlocks.join("\n\n"),
|
|
915
|
-
subjectText: subject === void 0 ? null : scalarElementText(subject),
|
|
916
|
-
warnings: state.warnings,
|
|
917
|
-
base,
|
|
918
|
-
compilerVersion: COMPILER_VERSION,
|
|
919
|
-
tree,
|
|
920
|
-
doctype,
|
|
921
|
-
headAssets,
|
|
922
|
-
nodeIdMap: createNodeIdMap(tree)
|
|
923
|
-
});
|
|
924
|
-
};
|
|
925
|
-
const compileSource = (source, options) => {
|
|
926
|
-
const parsed = parseSml(source);
|
|
927
|
-
if (Result.isFailure(parsed)) return Result.fail(parsed.failure);
|
|
928
|
-
return compileSml(parsed.success, options);
|
|
929
|
-
};
|
|
930
|
-
const compileSourceArtifact = (source, options) => {
|
|
20
|
+
/** Parse and compile a structured SMS or WhatsApp document. */
|
|
21
|
+
const compileSource = (source) => {
|
|
931
22
|
const parsed = parseSml(source);
|
|
932
23
|
if (Result.isFailure(parsed)) return Result.fail(parsed.failure);
|
|
933
|
-
return
|
|
24
|
+
return compileSml(parsed.success);
|
|
934
25
|
};
|
|
935
26
|
//#endregion
|
|
936
|
-
export { COMPILER_VERSION, compileSml,
|
|
27
|
+
export { COMPILER_VERSION, compileSml, compileSource };
|
|
937
28
|
|
|
938
29
|
//# sourceMappingURL=compile.js.map
|