@samva/markup 0.1.2 → 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.
Files changed (92) hide show
  1. package/AGENTS.md +25 -5
  2. package/README.md +92 -26
  3. package/dist/{ast-DAhnAvQN.d.ts → ast-63I-VLQx.d.ts} +15 -2
  4. package/dist/ast.d.ts +2 -2
  5. package/dist/ast.js +14 -1
  6. package/dist/{checks-D-zFQt5R.js → checks-DXuERO4B.js} +36 -2
  7. package/dist/checks.d.ts +8 -1
  8. package/dist/checks.js +2 -2
  9. package/dist/compile-sms.d.ts +2 -2
  10. package/dist/compile-whatsapp.d.ts +2 -2
  11. package/dist/compile.d.ts +9 -84
  12. package/dist/compile.js +11 -920
  13. package/dist/compiler.d.ts +301 -0
  14. package/dist/compiler.js +1389 -0
  15. package/dist/components.d.ts +3 -3
  16. package/dist/corpus-Dz5irJ9x.d.ts +161 -0
  17. package/dist/diagnostics-Djd9ckLg.d.ts +90 -0
  18. package/dist/diagnostics.js +4 -1
  19. package/dist/elements-DtwKlFVH.d.ts +820 -0
  20. package/dist/email/components.d.ts +119 -0
  21. package/dist/email/components.js +207 -0
  22. package/dist/email/diagnostics.d.ts +2 -0
  23. package/dist/email/diagnostics.js +56 -0
  24. package/dist/email/elements.d.ts +2 -0
  25. package/dist/email/elements.js +299 -0
  26. package/dist/email/html.d.ts +42 -0
  27. package/dist/email/html.js +266 -0
  28. package/dist/email/jsx-dev-runtime.d.ts +26 -0
  29. package/dist/email/jsx-dev-runtime.js +12 -0
  30. package/dist/email/jsx-runtime.d.ts +2 -0
  31. package/dist/email/jsx-runtime.js +60 -0
  32. package/dist/email/render.d.ts +36 -0
  33. package/dist/email/render.js +6 -0
  34. package/dist/email/selection.d.ts +102 -0
  35. package/dist/email/selection.js +143 -0
  36. package/dist/email/source-edit.d.ts +67 -0
  37. package/dist/email/source-edit.js +185 -0
  38. package/dist/email/stylesheets.d.ts +2 -0
  39. package/dist/email/stylesheets.js +43 -0
  40. package/dist/email/text.d.ts +7 -0
  41. package/dist/email/text.js +92 -0
  42. package/dist/emit.d.ts +13 -12
  43. package/dist/emit.js +63 -63
  44. package/dist/input-schema-BIfzI63B.d.ts +23 -0
  45. package/dist/input-schema.d.ts +2 -0
  46. package/dist/input-schema.js +170 -0
  47. package/dist/jsx-dev-runtime.d.ts +4 -3
  48. package/dist/jsx-dev-runtime.js +9 -1
  49. package/dist/{jsx-runtime-eNIMY87F.d.ts → jsx-runtime-C5k_Zq0A.d.ts} +3 -3
  50. package/dist/jsx-runtime.d.ts +1 -1
  51. package/dist/jsx-runtime.js +20 -1
  52. package/dist/model.d.ts +1 -1
  53. package/dist/ops.d.ts +1 -1
  54. package/dist/ops.js +1 -1
  55. package/dist/{parser-DkQKIjDa.d.ts → parser-CGBS73Gz.d.ts} +2 -2
  56. package/dist/parser.d.ts +1 -1
  57. package/dist/presets/category-mapping.d.ts +1 -1
  58. package/dist/presets/corpus.d.ts +2 -0
  59. package/dist/presets/corpus.js +186 -0
  60. package/dist/presets/public-gallery.d.ts +272 -2
  61. package/dist/presets/public-gallery.js +305 -6
  62. package/dist/presets/public-handoff.d.ts +140 -2
  63. package/dist/presets/public-handoff.js +130 -33
  64. package/dist/presets/public-preview-artifacts.d.ts +2 -2
  65. package/dist/presets/public-preview-artifacts.js +20 -26
  66. package/dist/presets/public-preview.d.ts +56 -2
  67. package/dist/presets/public-preview.js +388 -1152
  68. package/dist/presets/sources.d.ts +17 -4
  69. package/dist/presets/sources.js +1212 -43
  70. package/dist/presets.d.ts +2 -2
  71. package/dist/printer.d.ts +1 -1
  72. package/dist/render-Cddsy_XF.js +585 -0
  73. package/dist/render-template.d.ts +5 -2
  74. package/dist/render-template.js +3 -1
  75. package/dist/source-locations-BCJQnIfU.d.ts +89 -0
  76. package/dist/source-locations.d.ts +2 -0
  77. package/dist/source-locations.js +125 -0
  78. package/dist/stylesheets-oDRAnYA3.d.ts +94 -0
  79. package/dist/template-Dy5yOPyp.d.ts +60 -0
  80. package/dist/template.d.ts +2 -0
  81. package/dist/template.js +37 -0
  82. package/dist/tsx-source-instrumentation.d.ts +23 -0
  83. package/dist/tsx-source-instrumentation.js +97 -0
  84. package/dist/tsx-tracer.d.ts +116 -0
  85. package/dist/tsx-tracer.js +985 -0
  86. package/generated/sml-agent-contract.json +574 -782
  87. package/generated/sml-email-agent-contract.json +502 -677
  88. package/generated/sml-email-language-reference.md +94 -160
  89. package/generated/sml-language-reference.md +195 -220
  90. package/package.json +111 -9
  91. package/dist/public-gallery-D0Zv0pZE.d.ts +0 -611
  92. package/dist/public-preview-u1Z9aaGq.d.ts +0 -1168
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
- const element = (tag, attrs, style, children, options) => ({
14
- kind: "element",
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
38
- const escapeHtmlAttribute = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll("\"", "&quot;");
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("&#8202;")]);
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>${"&#8202;".repeat(leftSpaceCount)}</i><![endif]-->`)]),
598
- element("span", [], innerSpanStyle, label),
599
- element("span", [], [], [raw(`<!--[if mso]><i style="mso-font-width:${rightFontWidth * 100}%" hidden>${"&#8202;".repeat(rightSpaceCount)}&#8203;</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 compileEmailSmlArtifact(document, options);
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
- * Drop `hidden="true"` elements (and their subtrees) before expansion, so a
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 compileSmlArtifact(parsed.success, options);
24
+ return compileSml(parsed.success);
934
25
  };
935
26
  //#endregion
936
- export { COMPILER_VERSION, compileSml, compileSmlArtifact, compileSource, compileSourceArtifact, serializeHtmlNode };
27
+ export { COMPILER_VERSION, compileSml, compileSource };
937
28
 
938
29
  //# sourceMappingURL=compile.js.map