@samva/markup 0.7.0 → 0.9.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 (176) hide show
  1. package/AGENTS.md +104 -37
  2. package/README.md +188 -52
  3. package/dist/assets-C5duwTya.d.ts +40 -0
  4. package/dist/brand-B-0bN_Ir.d.ts +88 -0
  5. package/dist/{render-aaJBnsXK.js → cascade-t6kxlgEI.js} +130 -160
  6. package/dist/channel-edit.d.ts +121 -0
  7. package/dist/channel-edit.js +609 -0
  8. package/dist/channel-specs-B6M_HlaV.js +29 -0
  9. package/dist/channel-specs-C2feJwuP.d.ts +13 -0
  10. package/dist/compatibility-DbISoFIq.js +589 -0
  11. package/dist/compiler.d.ts +19 -249
  12. package/dist/compiler.js +10 -1408
  13. package/dist/{css-COcFOT1y.d.ts → css-BVQKICKC.d.ts} +30 -4
  14. package/dist/diagnostic-codes.d.ts +403 -0
  15. package/dist/diagnostic-codes.js +399 -0
  16. package/dist/{diagnostics-BMYtBCQk.d.ts → diagnostics-B-Gym34R.d.ts} +8 -4
  17. package/dist/diagnostics.d.ts +31 -2
  18. package/dist/diagnostics.js +1 -19
  19. package/dist/{elements-BzjjnoCS.d.ts → elements-cqldpaJt.d.ts} +3 -3
  20. package/dist/email/assets.d.ts +2 -0
  21. package/dist/email/assets.js +63 -0
  22. package/dist/email/brand-footer.d.ts +34 -0
  23. package/dist/email/brand-footer.js +63 -0
  24. package/dist/email/brand.d.ts +2 -0
  25. package/dist/email/brand.js +148 -0
  26. package/dist/email/components.d.ts +21 -10
  27. package/dist/email/components.js +46 -11
  28. package/dist/email/diagnostics.d.ts +2 -2
  29. package/dist/email/diagnostics.js +2 -1
  30. package/dist/email/elements.d.ts +1 -1
  31. package/dist/email/html.d.ts +2 -42
  32. package/dist/email/html.js +56 -2
  33. package/dist/email/jsx-dev-runtime.d.ts +2 -2
  34. package/dist/email/jsx-runtime.d.ts +1 -1
  35. package/dist/email/render.d.ts +10 -13
  36. package/dist/email/render.js +163 -6
  37. package/dist/email/text.d.ts +1 -1
  38. package/dist/fmt.d.ts +32 -0
  39. package/dist/fmt.js +174 -0
  40. package/dist/html-BJepOwZ4.d.ts +56 -0
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.js +2 -0
  43. package/dist/ir-aaqNE987.d.ts +252 -0
  44. package/dist/ir.d.ts +2 -0
  45. package/dist/ir.js +7 -0
  46. package/dist/preview-Ekdg1dXG.d.ts +100 -0
  47. package/dist/preview.d.ts +2 -0
  48. package/dist/preview.js +160 -0
  49. package/dist/profile-BH-OtdDA.js +3670 -0
  50. package/dist/render-ir.d.ts +19 -0
  51. package/dist/render-ir.js +826 -0
  52. package/dist/sms-segments.d.ts +13 -2
  53. package/dist/sms.d.ts +16 -0
  54. package/dist/sms.js +12 -0
  55. package/dist/{email/source-edit.d.ts → source-edit.d.ts} +55 -6
  56. package/dist/source-edit.js +343 -0
  57. package/dist/source-locations-C8wCsiAy.d.ts +23 -0
  58. package/dist/source-locations.d.ts +2 -2
  59. package/dist/source-locations.js +3 -89
  60. package/dist/static.d.ts +57 -0
  61. package/dist/static.js +69 -0
  62. package/dist/tailwind-CMYpZf1v.js +614 -0
  63. package/dist/template-_QE_rQND.d.ts +65 -0
  64. package/dist/template.d.ts +2 -2
  65. package/dist/template.js +15 -10
  66. package/dist/theme-css-hFqcPSar.js +587 -0
  67. package/dist/theme-css.d.ts +57 -16
  68. package/dist/theme-css.js +2 -509
  69. package/dist/theme-imports-lvBw3AD1.js +109 -0
  70. package/dist/transform-Ct2OMkFN.d.ts +153 -0
  71. package/dist/unsubscribe-link-CoesnQDU.js +26 -0
  72. package/dist/whatsapp.d.ts +57 -0
  73. package/dist/whatsapp.js +22 -0
  74. package/generated/contract.json +1782 -0
  75. package/generated/reference.md +353 -0
  76. package/package.json +56 -154
  77. package/samva-brand.d.ts +14 -0
  78. package/skills/sml/SKILL.md +123 -0
  79. package/dist/ast-BrDtApwS.d.ts +0 -107
  80. package/dist/ast.d.ts +0 -2
  81. package/dist/ast.js +0 -124
  82. package/dist/candidate.d.ts +0 -29
  83. package/dist/candidate.js +0 -88
  84. package/dist/catalog-60eaYC7b.d.ts +0 -356
  85. package/dist/checks-DBweSIPN.js +0 -58954
  86. package/dist/checks.d.ts +0 -33
  87. package/dist/checks.js +0 -2
  88. package/dist/classes.d.ts +0 -27
  89. package/dist/classes.js +0 -184
  90. package/dist/compile-sms.d.ts +0 -28
  91. package/dist/compile-sms.js +0 -44
  92. package/dist/compile-whatsapp.d.ts +0 -56
  93. package/dist/compile-whatsapp.js +0 -186
  94. package/dist/compile.d.ts +0 -18
  95. package/dist/compile.js +0 -29
  96. package/dist/components.d.ts +0 -289
  97. package/dist/components.js +0 -106
  98. package/dist/conditionals.d.ts +0 -10
  99. package/dist/conditionals.js +0 -74
  100. package/dist/corpus-BjYMqOoY.d.ts +0 -106
  101. package/dist/diagnostics-OcQKQYSY.d.ts +0 -49
  102. package/dist/email/selection.d.ts +0 -91
  103. package/dist/email/selection.js +0 -91
  104. package/dist/email/source-edit.js +0 -185
  105. package/dist/email/stylesheets.d.ts +0 -15
  106. package/dist/email/stylesheets.js +0 -45
  107. package/dist/emit.d.ts +0 -51
  108. package/dist/emit.js +0 -151
  109. package/dist/footer.d.ts +0 -66
  110. package/dist/footer.js +0 -93
  111. package/dist/interpolate.d.ts +0 -24
  112. package/dist/interpolate.js +0 -43
  113. package/dist/jsx-dev-runtime.d.ts +0 -11
  114. package/dist/jsx-dev-runtime.js +0 -15
  115. package/dist/jsx-runtime-I-1nCtl2.d.ts +0 -57
  116. package/dist/jsx-runtime.d.ts +0 -2
  117. package/dist/jsx-runtime.js +0 -121
  118. package/dist/lexer.d.ts +0 -60
  119. package/dist/lexer.js +0 -281
  120. package/dist/model.d.ts +0 -24
  121. package/dist/model.js +0 -77
  122. package/dist/ops-2aiiYO7m.js +0 -630
  123. package/dist/ops.d.ts +0 -114
  124. package/dist/ops.js +0 -2
  125. package/dist/parser-t29cHLVL.d.ts +0 -396
  126. package/dist/parser.d.ts +0 -2
  127. package/dist/parser.js +0 -859
  128. package/dist/predicate-BI6uRRM-.js +0 -549
  129. package/dist/predicate-M-05Nd7R.d.ts +0 -56
  130. package/dist/predicate.d.ts +0 -2
  131. package/dist/predicate.js +0 -2
  132. package/dist/presets/category-mapping.d.ts +0 -13
  133. package/dist/presets/category-mapping.js +0 -49
  134. package/dist/presets/corpus.d.ts +0 -2
  135. package/dist/presets/corpus.js +0 -194
  136. package/dist/presets/public-gallery.d.ts +0 -266
  137. package/dist/presets/public-gallery.js +0 -916
  138. package/dist/presets/public-handoff.d.ts +0 -164
  139. package/dist/presets/public-handoff.js +0 -361
  140. package/dist/presets/public-preview-artifacts.d.ts +0 -43
  141. package/dist/presets/public-preview-artifacts.js +0 -104
  142. package/dist/presets/public-preview.d.ts +0 -61
  143. package/dist/presets/public-preview.js +0 -83
  144. package/dist/presets/showcase.d.ts +0 -2
  145. package/dist/presets/showcase.js +0 -627
  146. package/dist/presets/sources.d.ts +0 -38
  147. package/dist/presets/sources.js +0 -6083
  148. package/dist/presets.d.ts +0 -2
  149. package/dist/presets.js +0 -881
  150. package/dist/printer.d.ts +0 -6
  151. package/dist/printer.js +0 -122
  152. package/dist/registry.d.ts +0 -42
  153. package/dist/registry.js +0 -646
  154. package/dist/render-template.d.ts +0 -45
  155. package/dist/render-template.js +0 -76
  156. package/dist/showcase-DGZAWn4Z.d.ts +0 -456
  157. package/dist/sms-segments-y4FpPilJ.d.ts +0 -13
  158. package/dist/source-locations-DrCM82Ym.d.ts +0 -67
  159. package/dist/template-DWftqh4x.d.ts +0 -60
  160. package/dist/theme-6ApjjiBo.d.ts +0 -116
  161. package/dist/theme.d.ts +0 -2
  162. package/dist/theme.js +0 -118
  163. package/dist/tokens.d.ts +0 -6
  164. package/dist/tokens.js +0 -417
  165. package/dist/tsx-source-instrumentation.d.ts +0 -30
  166. package/dist/tsx-source-instrumentation.js +0 -115
  167. package/dist/tsx-tracer.d.ts +0 -112
  168. package/dist/tsx-tracer.js +0 -985
  169. package/dist/variables-J_L3qxRa.d.ts +0 -77
  170. package/dist/variables.d.ts +0 -2
  171. package/dist/variables.js +0 -141
  172. package/dist/version-BITr58kk.js +0 -96
  173. package/generated/sml-agent-contract.json +0 -709
  174. package/generated/sml-email-agent-contract.json +0 -615
  175. package/generated/sml-email-language-reference.md +0 -94
  176. package/generated/sml-language-reference.md +0 -195
@@ -1,10 +1,95 @@
1
1
  import { n as isObject } from "./guards-eV1qLbf2.js";
2
- import { EmailNode, drainEmailDiagnostics } from "./email/jsx-runtime.js";
3
- import { declarationValue, kebab, serializeEmailHtml } from "./email/html.js";
4
- import { PREHEADER_ATTRIBUTE, preheaderNode } from "./email/components.js";
5
- import { EmailCompileError, hasBlockingDiagnostic } from "./email/diagnostics.js";
6
- import { snapshotStylesheets } from "./email/stylesheets.js";
7
- import { deriveEmailText } from "./email/text.js";
2
+ import { EmailNode } from "./email/jsx-runtime.js";
3
+ import { declarationValue, escapeAttribute, kebab } from "./email/html.js";
4
+ //#region src/email/font-stacks.ts
5
+ /** Generic families every email client understands as the last resort in a stack. */
6
+ const GENERIC_FAMILIES = /* @__PURE__ */ new Set([
7
+ "serif",
8
+ "sans-serif",
9
+ "monospace",
10
+ "cursive",
11
+ "fantasy"
12
+ ]);
13
+ /** One family name as written, without its quotes and with runs of spaces collapsed. */
14
+ const unquote = (value) => {
15
+ const trimmed = value.trim();
16
+ const quote = trimmed[0];
17
+ return (quote === "\"" || quote === "'") && trimmed.endsWith(quote) && trimmed.length >= 2 ? trimmed.slice(1, -1) : trimmed.replace(/\s+/g, " ");
18
+ };
19
+ /** The families of a `font-family` value, unquoted, in stack order. */
20
+ const fontStackFamilies = (value) => {
21
+ const families = [];
22
+ let start = 0;
23
+ let quote;
24
+ for (let index = 0; index < value.length; index++) {
25
+ const character = value[index];
26
+ if (quote !== void 0) {
27
+ if (character === quote) quote = void 0;
28
+ } else if (character === "'" || character === "\"") quote = character;
29
+ else if (character === ",") {
30
+ families.push(unquote(value.slice(start, index)));
31
+ start = index + 1;
32
+ }
33
+ }
34
+ families.push(unquote(value.slice(start)));
35
+ return families.filter((family) => family !== "");
36
+ };
37
+ const FONT_SIZE = /^(?:xx-small|x-small|small|medium|large|x-large|xx-large|xxx-large|larger|smaller|[+-]?(?:\d+\.?\d*|\.\d+)(?:[a-z]+|%)|(?:calc|clamp|min|max|var)\(.*\))$/i;
38
+ /** Whitespace-separated tokens at the top level, keeping parentheses and quotes whole. */
39
+ const shorthandTokens = (value) => {
40
+ const tokens = [];
41
+ let current = "";
42
+ let depth = 0;
43
+ let quote;
44
+ for (const character of value.trim()) {
45
+ if (quote !== void 0) {
46
+ if (character === quote) quote = void 0;
47
+ } else if (character === "'" || character === "\"") quote = character;
48
+ else if (character === "(") depth++;
49
+ else if (character === ")") depth--;
50
+ else if (/\s/.test(character) && depth === 0) {
51
+ if (current !== "") tokens.push(current);
52
+ current = "";
53
+ continue;
54
+ }
55
+ current += character;
56
+ }
57
+ if (current !== "") tokens.push(current);
58
+ return tokens;
59
+ };
60
+ /**
61
+ * The family list of a `font` shorthand: everything after the size (and its
62
+ * optional line height), or `undefined` for a system-font keyword that names
63
+ * no family.
64
+ */
65
+ const fontShorthandFamilies = (value) => {
66
+ const tokens = shorthandTokens(value);
67
+ const size = tokens.findIndex((token) => FONT_SIZE.test(token.replace(/\/.*$/, "")));
68
+ if (size < 0) return void 0;
69
+ let next = size + 1;
70
+ const sizeToken = tokens[size];
71
+ if (sizeToken.endsWith("/")) next += 1;
72
+ else if (!sizeToken.includes("/") && tokens[next]?.startsWith("/")) next += tokens[next] === "/" ? 2 : 1;
73
+ const family = tokens.slice(next).join(" ");
74
+ return family === "" ? void 0 : family;
75
+ };
76
+ /**
77
+ * True when a stack names a declared project family without ending in a
78
+ * generic. `declared` holds lowercased family names. A quoted `"sans-serif"`
79
+ * names a font called sans-serif, not the generic, so it does not count.
80
+ */
81
+ const lacksGenericFallback = (value, declared) => {
82
+ const stack = value.replace(/\s*!important\s*$/i, "");
83
+ if (!fontStackFamilies(stack).some((family) => declared.has(family.toLowerCase()))) return false;
84
+ const last = stack.slice(stack.lastIndexOf(",") + 1).trim();
85
+ return !GENERIC_FAMILIES.has(last.toLowerCase());
86
+ };
87
+ const fontFallbackDiagnostic = (value) => ({
88
+ code: "email-font-fallback",
89
+ severity: "error",
90
+ message: `font-family ${JSON.stringify(value)} uses a project font without a generic fallback. Clients that drop webfonts (Gmail, classic Outlook) render this stack as written, so end it with sans-serif, serif or monospace.`
91
+ });
92
+ //#endregion
8
93
  //#region src/email/css-values.ts
9
94
  const MAX_RESOLVED_VALUE_LENGTH = 65536;
10
95
  const MAX_RESOLUTION_WORK = 262144;
@@ -260,6 +345,17 @@ const matchesFrom = (selector, index, ancestry) => {
260
345
  const matchesSelector = (selector, ancestry) => selector === void 0 || selector.parts.length === 0 ? false : matchesFrom(selector, selector.parts.length - 1, ancestry);
261
346
  //#endregion
262
347
  //#region src/email/cascade.ts
348
+ const VARIABLE_REFERENCE = /var\(\s*--[\w-]+\s*(?:,[^()]*)?\)/g;
349
+ /**
350
+ * Compatibility markup a primitive emits (a Button's VML) and a presentational
351
+ * `bgcolor` attribute are not CSS, so no client resolves `var()` in them. Their
352
+ * theme variables resolve here, in the scope of the element they sit in, as a
353
+ * declaration on that element would.
354
+ */
355
+ const resolveMarkupVariables = (markup, variables) => markup.replace(VARIABLE_REFERENCE, (reference) => {
356
+ const value = finalizeValue(reference, variables);
357
+ return value === reference ? reference : escapeAttribute(value);
358
+ });
263
359
  /**
264
360
  * Cascade rank as the tuple CSS actually sorts by: importance first, then
265
361
  * whether the declaration is attached to the element itself, then layer, then
@@ -353,6 +449,14 @@ const applyStylesheet = (root, sheet, options = {}) => {
353
449
  }
354
450
  const conditionalWritingRules = retained.filter((rule) => rule.declarations.some((declaration) => declaration.property === "direction" || declaration.property === "writing-mode" || declaration.property === "all"));
355
451
  const unmatched = /* @__PURE__ */ new Map();
452
+ const declaredFamilies = new Set(sheet.fontFaces.map((face) => face.family.toLowerCase()));
453
+ const fallbackFindings = /* @__PURE__ */ new Map();
454
+ const checkFallback = (property, value, origins) => {
455
+ if (declaredFamilies.size === 0) return;
456
+ const stack = property === "font-family" ? value : property === "font" ? fontShorthandFamilies(value) : void 0;
457
+ if (stack === void 0 || fallbackFindings.has(stack)) return;
458
+ if (lacksGenericFallback(stack, declaredFamilies)) fallbackFindings.set(stack, origins);
459
+ };
356
460
  /**
357
461
  * Children are visited with their own preceding-sibling list so `+`/`~` work.
358
462
  * A fragment is not an element in the serialized document, so its children
@@ -371,6 +475,10 @@ const applyStylesheet = (root, sheet, options = {}) => {
371
475
  return result;
372
476
  }
373
477
  if (child.type === "Fragment") return EmailNode.Fragment({ children: visit(child.children) });
478
+ if (child.type === "Raw" && child.html.includes("var(")) return EmailNode.Raw({
479
+ ...child,
480
+ html: resolveMarkupVariables(child.html, variables)
481
+ });
374
482
  return child;
375
483
  });
376
484
  return visit(children);
@@ -422,15 +530,21 @@ const applyStylesheet = (root, sheet, options = {}) => {
422
530
  writingMode: winners.has("all") ? "unknown" : inheritedValue("writing-mode", inheritedWriting.writingMode, "horizontal-tb")
423
531
  };
424
532
  const emitted = [...winners].filter(([property]) => !property.startsWith("--")).sort(([, left], [, right]) => compareRank(left.key, right.key)).map(([property, entry]) => [property, finalizeValue(entry.value, variables)]);
533
+ for (const [property, value] of emitted) checkFallback(property, value, element.origins.slice(0, 1));
425
534
  const physical = /* @__PURE__ */ new Map();
426
535
  for (const [property, value] of emitted) for (const [name, lowered] of conditionalWriting ? [[property, value]] : physicalPadding(property, value, writing)) {
427
536
  physical.delete(name);
428
537
  physical.set(name, lowered);
429
538
  }
539
+ const bgcolor = element.props.bgcolor;
540
+ const props = typeof bgcolor === "string" && bgcolor.includes("var(") ? {
541
+ ...element.props,
542
+ bgcolor: finalizeValue(bgcolor, variables)
543
+ } : element.props;
430
544
  return EmailNode.Element({
431
545
  ...element,
432
- props: emitted.length === 0 ? element.props : {
433
- ...element.props,
546
+ props: emitted.length === 0 ? props : {
547
+ ...props,
434
548
  style: Object.fromEntries(physical)
435
549
  },
436
550
  children: visitChildren(element.children, ancestry, variables, writing)
@@ -441,6 +555,11 @@ const applyStylesheet = (root, sheet, options = {}) => {
441
555
  parent: void 0,
442
556
  previous: []
443
557
  }, sheet.variables, horizontalWriting) : root.type === "Fragment" ? EmailNode.Fragment({ children: visitChildren(root.children, void 0, sheet.variables, horizontalWriting) }) : root;
558
+ for (const rule of retained) for (const declaration of rule.declarations) checkFallback(declaration.property, finalizeValue(declaration.value, sheet.variables), []);
559
+ for (const [value, origins] of fallbackFindings) diagnostics.push({
560
+ ...fontFallbackDiagnostic(value),
561
+ origins
562
+ });
444
563
  for (const [name, origins] of unmatched) diagnostics.push({
445
564
  code: "unmatched-class",
446
565
  severity: "warning",
@@ -450,160 +569,11 @@ const applyStylesheet = (root, sheet, options = {}) => {
450
569
  return {
451
570
  tree,
452
571
  headCss: renderHeadCss(retained, sheet.headAtRules, sheet.variables),
572
+ fontFaceCss: sheet.fontFaces.map((face) => face.css).join(""),
453
573
  diagnostics
454
574
  };
455
575
  };
456
576
  //#endregion
457
- //#region src/email/render.ts
458
- /** Classic Outlook lays a document out correctly only under a transitional doctype. */
459
- const EMAIL_DOCTYPE = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">";
460
- const settle = (diagnostics) => {
461
- if (hasBlockingDiagnostic(diagnostics)) throw new EmailCompileError({ diagnostics });
462
- return diagnostics;
463
- };
464
- /** Lower the derived tree to HTML and its plain-text alternative. */
465
- const compileEmail = (node, options = {}) => {
466
- const serialized = serializeEmailHtml(node, options);
467
- const diagnostics = settle([...drainEmailDiagnostics(), ...serialized.diagnostics]);
468
- return {
469
- html: serialized.html,
470
- text: deriveEmailText(node),
471
- diagnostics,
472
- positions: serialized.positions
473
- };
474
- };
475
- /** The preview text already present in the tree, from `<Email preheader>`. */
476
- const declaredPreheader = (node) => {
477
- switch (node.type) {
478
- case "Text":
479
- case "Raw": return;
480
- case "Fragment": return node.children.reduce((found, child) => found ?? declaredPreheader(child), void 0);
481
- case "Element": {
482
- const declared = node.props[PREHEADER_ATTRIBUTE];
483
- if (typeof declared === "string") return declared;
484
- return node.children.reduce((found, child) => found ?? declaredPreheader(child), void 0);
485
- }
486
- }
487
- };
488
- /** Put the hidden preview block first inside `<body>`, or first overall when there is no shell. */
489
- const withPreheader = (node, text) => {
490
- const insert = (current) => {
491
- if (current.type === "Fragment") {
492
- for (const [index, child] of current.children.entries()) {
493
- const replaced = insert(child);
494
- if (replaced !== void 0) return EmailNode.Fragment({ children: current.children.with(index, replaced) });
495
- }
496
- return;
497
- }
498
- if (current.type !== "Element") return void 0;
499
- if (current.tag === "body") return EmailNode.Element({
500
- ...current,
501
- children: [preheaderNode(text), ...current.children]
502
- });
503
- for (const [index, child] of current.children.entries()) {
504
- const replaced = insert(child);
505
- if (replaced !== void 0) return EmailNode.Element({
506
- ...current,
507
- children: current.children.with(index, replaced)
508
- });
509
- }
510
- };
511
- return insert(node) ?? EmailNode.Fragment({ children: [preheaderNode(text), node] });
512
- };
513
- /** Put the retained rules in a `<style>` at the end of `<head>`. */
514
- const withHeadCss = (node, css) => {
515
- const insert = (current) => {
516
- if (current.type === "Fragment") {
517
- for (const [index, child] of current.children.entries()) {
518
- const replaced = insert(child);
519
- if (replaced !== void 0) return EmailNode.Fragment({ children: current.children.with(index, replaced) });
520
- }
521
- return;
522
- }
523
- if (current.type !== "Element") return void 0;
524
- if (current.tag === "head") return EmailNode.Element({
525
- ...current,
526
- children: [...current.children, EmailNode.Element({
527
- tag: "style",
528
- props: {},
529
- children: [EmailNode.Text({ value: css })],
530
- origins: current.origins,
531
- authored: false
532
- })]
533
- });
534
- for (const [index, child] of current.children.entries()) {
535
- const replaced = insert(child);
536
- if (replaced !== void 0) return EmailNode.Element({
537
- ...current,
538
- children: current.children.with(index, replaced)
539
- });
540
- }
541
- };
542
- return insert(node);
543
- };
544
- /** Render one validated input through the template's only content function. Host isolation is required for untrusted source. */
545
- const renderEmail = (template, data, options = {}) => renderEmailWith(snapshotStylesheets(), template, data, options);
546
- /**
547
- * A render bound to the sheets registered so far. A compiled project's wrapper
548
- * calls this once its own stylesheet imports have run, so a later project
549
- * loaded into the same process cannot restyle an entry that is rendered again.
550
- */
551
- const bindStylesheets = (stylesheet = snapshotStylesheets()) => (template, data, options = {}) => renderEmailWith(stylesheet, template, data, options);
552
- const renderEmailWith = (stylesheet, template, data, options) => {
553
- if (!template.schema.validate(data)) throw new TypeError(`Invalid template input: ${JSON.stringify(template.schema.validate.errors)}`);
554
- drainEmailDiagnostics();
555
- const content = template.render(data);
556
- const invalid = [];
557
- const fail = (code, message) => invalid.push({
558
- code,
559
- severity: "error",
560
- message,
561
- origins: []
562
- });
563
- if (typeof content.subject !== "string" || /[\r\n\u0000]/.test(content.subject)) fail("invalid-subject", "Invalid email subject: it must be one line of text.");
564
- if (content.preheader !== void 0 && typeof content.preheader !== "string") fail("invalid-preheader", "Invalid email preheader: it must be a string.");
565
- if (content.text !== void 0 && typeof content.text !== "string") fail("invalid-text", "Invalid plain text: it must be a string.");
566
- const declared = declaredPreheader(content.body);
567
- if (content.preheader !== void 0 && declared !== void 0) fail("duplicate-preheader", "Preheader is set on both the template content and <Email preheader>; keep one.");
568
- settle(invalid);
569
- const base = content.preheader === void 0 ? content.body : withPreheader(content.body, content.preheader);
570
- const styled = stylesheet === void 0 ? {
571
- tree: base,
572
- headCss: "",
573
- diagnostics: []
574
- } : applyStylesheet(base, stylesheet);
575
- const style = [...styled.diagnostics];
576
- let tree = styled.tree;
577
- if (styled.headCss !== "") {
578
- const placed = withHeadCss(tree, styled.headCss);
579
- if (placed === void 0) style.push({
580
- code: "missing-head",
581
- severity: "error",
582
- 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.",
583
- origins: []
584
- });
585
- else tree = placed;
586
- }
587
- settle(style);
588
- const compiled = compileEmail(tree, options);
589
- const prefix = compiled.html.startsWith("<html") ? EMAIL_DOCTYPE : "";
590
- const html = `${prefix}${compiled.html}`;
591
- const positions = prefix === "" ? compiled.positions : compiled.positions.map((position) => ({
592
- ...position,
593
- start: position.start + prefix.length,
594
- end: position.end + prefix.length
595
- }));
596
- return {
597
- subject: content.subject,
598
- preheader: content.preheader ?? declared,
599
- html,
600
- text: content.text ?? compiled.text,
601
- tree,
602
- diagnostics: [...style, ...compiled.diagnostics],
603
- positions
604
- };
605
- };
606
- //#endregion
607
- export { applyStylesheet as a, downlevelMediaQuery as c, renderHeadCss as d, resolveVariables as f, renderEmail as i, finalizeValue as l, bindStylesheets as n, classListOf as o, compileEmail as r, matchesSelector as s, EMAIL_DOCTYPE as t, foldCalc as u };
577
+ export { finalizeValue as a, resolveVariables as c, downlevelMediaQuery as i, unquote as l, classListOf as n, foldCalc as o, matchesSelector as r, renderHeadCss as s, applyStylesheet as t };
608
578
 
609
- //# sourceMappingURL=render-aaJBnsXK.js.map
579
+ //# sourceMappingURL=cascade-t6kxlgEI.js.map
@@ -0,0 +1,121 @@
1
+ import { n as WhatsAppCategory, r as WhatsAppHeaderType, t as SmsCategory } from "./channel-specs-C2feJwuP.js";
2
+ import { SourceReplacement } from "./source-edit.js";
3
+ //#region src/channel-edit.d.ts
4
+ /** A run of body text between two locked segments. It is empty when nothing sits between them. */
5
+ interface TextSegment {
6
+ readonly kind: "text";
7
+ readonly text: string;
8
+ /** The source range the text is written back over. */
9
+ readonly start: number;
10
+ readonly end: number;
11
+ }
12
+ /** An expression, conditional or element the form does not edit, kept as authored. */
13
+ interface LockedSegment {
14
+ readonly kind: "locked";
15
+ /** The authored source, shortened for display. */
16
+ readonly summary: string;
17
+ readonly start: number;
18
+ readonly end: number;
19
+ }
20
+ type ChannelSegment = TextSegment | LockedSegment;
21
+ /** How an attribute appears on an element. Only a literal is editable. */
22
+ type AttributeState = {
23
+ readonly kind: "absent";
24
+ } | {
25
+ readonly kind: "literal";
26
+ readonly value: string;
27
+ } | {
28
+ readonly kind: "expression";
29
+ readonly source: string;
30
+ };
31
+ interface SmsChannelForm {
32
+ readonly category: AttributeState;
33
+ readonly segments: readonly ChannelSegment[];
34
+ /** The input fields the body reads, as dotted paths. */
35
+ readonly variables: readonly string[];
36
+ }
37
+ type WhatsAppButtonKind = "quick-reply" | "url" | "phone" | "copy-code";
38
+ type WhatsAppButtonRow = {
39
+ readonly kind: "editable";
40
+ readonly type: WhatsAppButtonKind;
41
+ /** The label, or the copied code for a copy-code button. */
42
+ readonly text: string;
43
+ readonly url: AttributeState;
44
+ readonly phone: AttributeState;
45
+ } | {
46
+ /** A conditional, or a button whose label reads the input. */
47
+ readonly kind: "locked";
48
+ readonly summary: string;
49
+ };
50
+ interface WhatsAppChannelForm {
51
+ readonly name: AttributeState;
52
+ readonly language: AttributeState;
53
+ readonly category: AttributeState;
54
+ /** Null when the template has no header. */
55
+ readonly header: readonly ChannelSegment[] | null;
56
+ readonly headerType: AttributeState;
57
+ readonly body: readonly ChannelSegment[];
58
+ /** Null when the template has no footer. */
59
+ readonly footer: readonly ChannelSegment[] | null;
60
+ readonly buttons: readonly WhatsAppButtonRow[];
61
+ readonly variables: readonly string[];
62
+ }
63
+ type ChannelEditResult = {
64
+ readonly ok: true;
65
+ readonly edits: readonly SourceReplacement[];
66
+ } | {
67
+ readonly ok: false;
68
+ readonly reason: string;
69
+ };
70
+ type SmsEdit = {
71
+ readonly kind: "category";
72
+ readonly value: SmsCategory | undefined;
73
+ } | {
74
+ readonly kind: "text";
75
+ readonly segment: number;
76
+ readonly text: string;
77
+ };
78
+ type WhatsAppPart = "header" | "body" | "footer";
79
+ type WhatsAppEdit = {
80
+ readonly kind: "attribute";
81
+ readonly name: "name" | "language" | "category";
82
+ readonly value: string | undefined;
83
+ } | {
84
+ readonly kind: "headerType";
85
+ readonly value: WhatsAppHeaderType;
86
+ } | {
87
+ readonly kind: "text";
88
+ readonly part: WhatsAppPart;
89
+ readonly segment: number;
90
+ readonly text: string;
91
+ } | {
92
+ readonly kind: "buttonText";
93
+ readonly button: number;
94
+ readonly text: string;
95
+ } | {
96
+ readonly kind: "buttonAttribute";
97
+ readonly button: number;
98
+ readonly name: "url" | "phone";
99
+ readonly value: string;
100
+ } | {
101
+ readonly kind: "buttonType";
102
+ readonly button: number;
103
+ readonly type: WhatsAppButtonKind;
104
+ } | {
105
+ readonly kind: "buttonAdd";
106
+ readonly type: WhatsAppButtonKind;
107
+ } | {
108
+ readonly kind: "buttonRemove";
109
+ readonly button: number;
110
+ };
111
+ /** The form model of the template's SMS body, or null when it is not a `<Sms>` written in place. */
112
+ declare const readSmsChannel: (source: string) => SmsChannelForm | null;
113
+ /** The source replacements that apply one form change to the SMS body. */
114
+ declare const editSmsChannel: (source: string, edit: SmsEdit) => ChannelEditResult;
115
+ /** The form model of the template's WhatsApp body, or null when it is not a `<WhatsApp>` written in place. */
116
+ declare const readWhatsAppChannel: (source: string) => WhatsAppChannelForm | null;
117
+ /** The source replacements that apply one form change to the WhatsApp body. */
118
+ declare const editWhatsAppChannel: (source: string, edit: WhatsAppEdit) => ChannelEditResult;
119
+ //#endregion
120
+ export { AttributeState, ChannelEditResult, ChannelSegment, LockedSegment, type SmsCategory, SmsChannelForm, SmsEdit, TextSegment, WhatsAppButtonKind, WhatsAppButtonRow, type WhatsAppCategory, WhatsAppChannelForm, WhatsAppEdit, type WhatsAppHeaderType, WhatsAppPart, editSmsChannel, editWhatsAppChannel, readSmsChannel, readWhatsAppChannel };
121
+ //# sourceMappingURL=channel-edit.d.ts.map