@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.
- package/AGENTS.md +104 -37
- package/README.md +188 -52
- package/dist/assets-C5duwTya.d.ts +40 -0
- package/dist/brand-B-0bN_Ir.d.ts +88 -0
- package/dist/{render-aaJBnsXK.js → cascade-t6kxlgEI.js} +130 -160
- package/dist/channel-edit.d.ts +121 -0
- package/dist/channel-edit.js +609 -0
- package/dist/channel-specs-B6M_HlaV.js +29 -0
- package/dist/channel-specs-C2feJwuP.d.ts +13 -0
- package/dist/compatibility-DbISoFIq.js +589 -0
- package/dist/compiler.d.ts +19 -249
- package/dist/compiler.js +10 -1408
- package/dist/{css-COcFOT1y.d.ts → css-BVQKICKC.d.ts} +30 -4
- package/dist/diagnostic-codes.d.ts +403 -0
- package/dist/diagnostic-codes.js +399 -0
- package/dist/{diagnostics-BMYtBCQk.d.ts → diagnostics-B-Gym34R.d.ts} +8 -4
- package/dist/diagnostics.d.ts +31 -2
- package/dist/diagnostics.js +1 -19
- package/dist/{elements-BzjjnoCS.d.ts → elements-cqldpaJt.d.ts} +3 -3
- package/dist/email/assets.d.ts +2 -0
- package/dist/email/assets.js +63 -0
- package/dist/email/brand-footer.d.ts +34 -0
- package/dist/email/brand-footer.js +63 -0
- package/dist/email/brand.d.ts +2 -0
- package/dist/email/brand.js +148 -0
- package/dist/email/components.d.ts +21 -10
- package/dist/email/components.js +46 -11
- package/dist/email/diagnostics.d.ts +2 -2
- package/dist/email/diagnostics.js +2 -1
- package/dist/email/elements.d.ts +1 -1
- package/dist/email/html.d.ts +2 -42
- package/dist/email/html.js +56 -2
- package/dist/email/jsx-dev-runtime.d.ts +2 -2
- package/dist/email/jsx-runtime.d.ts +1 -1
- package/dist/email/render.d.ts +10 -13
- package/dist/email/render.js +163 -6
- package/dist/email/text.d.ts +1 -1
- package/dist/fmt.d.ts +32 -0
- package/dist/fmt.js +174 -0
- package/dist/html-BJepOwZ4.d.ts +56 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/ir-aaqNE987.d.ts +252 -0
- package/dist/ir.d.ts +2 -0
- package/dist/ir.js +7 -0
- package/dist/preview-Ekdg1dXG.d.ts +100 -0
- package/dist/preview.d.ts +2 -0
- package/dist/preview.js +160 -0
- package/dist/profile-BH-OtdDA.js +3670 -0
- package/dist/render-ir.d.ts +19 -0
- package/dist/render-ir.js +826 -0
- package/dist/sms-segments.d.ts +13 -2
- package/dist/sms.d.ts +16 -0
- package/dist/sms.js +12 -0
- package/dist/{email/source-edit.d.ts → source-edit.d.ts} +55 -6
- package/dist/source-edit.js +343 -0
- package/dist/source-locations-C8wCsiAy.d.ts +23 -0
- package/dist/source-locations.d.ts +2 -2
- package/dist/source-locations.js +3 -89
- package/dist/static.d.ts +57 -0
- package/dist/static.js +69 -0
- package/dist/tailwind-CMYpZf1v.js +614 -0
- package/dist/template-_QE_rQND.d.ts +65 -0
- package/dist/template.d.ts +2 -2
- package/dist/template.js +15 -10
- package/dist/theme-css-hFqcPSar.js +587 -0
- package/dist/theme-css.d.ts +57 -16
- package/dist/theme-css.js +2 -509
- package/dist/theme-imports-lvBw3AD1.js +109 -0
- package/dist/transform-Ct2OMkFN.d.ts +153 -0
- package/dist/unsubscribe-link-CoesnQDU.js +26 -0
- package/dist/whatsapp.d.ts +57 -0
- package/dist/whatsapp.js +22 -0
- package/generated/contract.json +1782 -0
- package/generated/reference.md +353 -0
- package/package.json +56 -154
- package/samva-brand.d.ts +14 -0
- package/skills/sml/SKILL.md +123 -0
- package/dist/ast-BrDtApwS.d.ts +0 -107
- package/dist/ast.d.ts +0 -2
- package/dist/ast.js +0 -124
- package/dist/candidate.d.ts +0 -29
- package/dist/candidate.js +0 -88
- package/dist/catalog-60eaYC7b.d.ts +0 -356
- package/dist/checks-DBweSIPN.js +0 -58954
- package/dist/checks.d.ts +0 -33
- package/dist/checks.js +0 -2
- package/dist/classes.d.ts +0 -27
- package/dist/classes.js +0 -184
- package/dist/compile-sms.d.ts +0 -28
- package/dist/compile-sms.js +0 -44
- package/dist/compile-whatsapp.d.ts +0 -56
- package/dist/compile-whatsapp.js +0 -186
- package/dist/compile.d.ts +0 -18
- package/dist/compile.js +0 -29
- package/dist/components.d.ts +0 -289
- package/dist/components.js +0 -106
- package/dist/conditionals.d.ts +0 -10
- package/dist/conditionals.js +0 -74
- package/dist/corpus-BjYMqOoY.d.ts +0 -106
- package/dist/diagnostics-OcQKQYSY.d.ts +0 -49
- package/dist/email/selection.d.ts +0 -91
- package/dist/email/selection.js +0 -91
- package/dist/email/source-edit.js +0 -185
- package/dist/email/stylesheets.d.ts +0 -15
- package/dist/email/stylesheets.js +0 -45
- package/dist/emit.d.ts +0 -51
- package/dist/emit.js +0 -151
- package/dist/footer.d.ts +0 -66
- package/dist/footer.js +0 -93
- package/dist/interpolate.d.ts +0 -24
- package/dist/interpolate.js +0 -43
- package/dist/jsx-dev-runtime.d.ts +0 -11
- package/dist/jsx-dev-runtime.js +0 -15
- package/dist/jsx-runtime-I-1nCtl2.d.ts +0 -57
- package/dist/jsx-runtime.d.ts +0 -2
- package/dist/jsx-runtime.js +0 -121
- package/dist/lexer.d.ts +0 -60
- package/dist/lexer.js +0 -281
- package/dist/model.d.ts +0 -24
- package/dist/model.js +0 -77
- package/dist/ops-2aiiYO7m.js +0 -630
- package/dist/ops.d.ts +0 -114
- package/dist/ops.js +0 -2
- package/dist/parser-t29cHLVL.d.ts +0 -396
- package/dist/parser.d.ts +0 -2
- package/dist/parser.js +0 -859
- package/dist/predicate-BI6uRRM-.js +0 -549
- package/dist/predicate-M-05Nd7R.d.ts +0 -56
- package/dist/predicate.d.ts +0 -2
- package/dist/predicate.js +0 -2
- package/dist/presets/category-mapping.d.ts +0 -13
- package/dist/presets/category-mapping.js +0 -49
- package/dist/presets/corpus.d.ts +0 -2
- package/dist/presets/corpus.js +0 -194
- package/dist/presets/public-gallery.d.ts +0 -266
- package/dist/presets/public-gallery.js +0 -916
- package/dist/presets/public-handoff.d.ts +0 -164
- package/dist/presets/public-handoff.js +0 -361
- package/dist/presets/public-preview-artifacts.d.ts +0 -43
- package/dist/presets/public-preview-artifacts.js +0 -104
- package/dist/presets/public-preview.d.ts +0 -61
- package/dist/presets/public-preview.js +0 -83
- package/dist/presets/showcase.d.ts +0 -2
- package/dist/presets/showcase.js +0 -627
- package/dist/presets/sources.d.ts +0 -38
- package/dist/presets/sources.js +0 -6083
- package/dist/presets.d.ts +0 -2
- package/dist/presets.js +0 -881
- package/dist/printer.d.ts +0 -6
- package/dist/printer.js +0 -122
- package/dist/registry.d.ts +0 -42
- package/dist/registry.js +0 -646
- package/dist/render-template.d.ts +0 -45
- package/dist/render-template.js +0 -76
- package/dist/showcase-DGZAWn4Z.d.ts +0 -456
- package/dist/sms-segments-y4FpPilJ.d.ts +0 -13
- package/dist/source-locations-DrCM82Ym.d.ts +0 -67
- package/dist/template-DWftqh4x.d.ts +0 -60
- package/dist/theme-6ApjjiBo.d.ts +0 -116
- package/dist/theme.d.ts +0 -2
- package/dist/theme.js +0 -118
- package/dist/tokens.d.ts +0 -6
- package/dist/tokens.js +0 -417
- package/dist/tsx-source-instrumentation.d.ts +0 -30
- package/dist/tsx-source-instrumentation.js +0 -115
- package/dist/tsx-tracer.d.ts +0 -112
- package/dist/tsx-tracer.js +0 -985
- package/dist/variables-J_L3qxRa.d.ts +0 -77
- package/dist/variables.d.ts +0 -2
- package/dist/variables.js +0 -141
- package/dist/version-BITr58kk.js +0 -96
- package/generated/sml-agent-contract.json +0 -709
- package/generated/sml-email-agent-contract.json +0 -615
- package/generated/sml-email-language-reference.md +0 -94
- 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
|
|
3
|
-
import { declarationValue,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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 ?
|
|
433
|
-
...
|
|
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
|
-
|
|
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=
|
|
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
|