@samva/markup 0.8.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 +90 -37
- package/README.md +188 -52
- package/dist/assets-C5duwTya.d.ts +40 -0
- package/dist/{brand-DcoVkh_S.d.ts → brand-B-0bN_Ir.d.ts} +2 -2
- package/dist/{render-vAv_4r9v.js → cascade-t6kxlgEI.js} +93 -280
- 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 +10 -257
- package/dist/compiler.js +10 -730
- package/dist/{css-CS2jLID6.d.ts → css-BVQKICKC.d.ts} +3 -3
- 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 -40
- package/dist/email/brand-footer.d.ts +2 -2
- package/dist/email/brand-footer.js +4 -3
- package/dist/email/brand.d.ts +1 -1
- package/dist/email/brand.js +1 -1
- 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 -2
- package/dist/email/html.js +45 -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 +8 -18
- package/dist/email/render.js +163 -3
- package/dist/email/text.d.ts +1 -1
- package/dist/fmt.d.ts +32 -0
- package/dist/fmt.js +174 -0
- package/dist/{html-Bl9BAdqc.d.ts → html-BJepOwZ4.d.ts} +19 -5
- 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.d.ts +1 -1
- package/dist/theme-imports-lvBw3AD1.js +109 -0
- package/dist/transform-Ct2OMkFN.d.ts +153 -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 +41 -151
- 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-CKuOooB8.d.ts +0 -354
- package/dist/checks-D73Xizbw.js +0 -59366
- package/dist/checks.d.ts +0 -33
- package/dist/checks.js +0 -2
- package/dist/classes.d.ts +0 -26
- 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-CPPfgAHN.d.ts +0 -106
- package/dist/diagnostics-OcQKQYSY.d.ts +0 -49
- package/dist/email/selection.d.ts +0 -2
- 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 -48
- package/dist/emit.d.ts +0 -51
- package/dist/emit.js +0 -151
- 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-BvwqHl7V.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/printer.d.ts +0 -6
- package/dist/printer.js +0 -122
- package/dist/registry-Dcp6QEK_.d.ts +0 -73
- package/dist/registry.d.ts +0 -2
- package/dist/registry.js +0 -646
- package/dist/render-template.d.ts +0 -45
- package/dist/render-template.js +0 -76
- package/dist/selection-CCArx9lU.d.ts +0 -91
- package/dist/showcase-egkusQBf.d.ts +0 -415
- package/dist/sms-segments-y4FpPilJ.d.ts +0 -13
- package/dist/source-locations-DrCM82Ym.d.ts +0 -67
- package/dist/starters/category-mapping.d.ts +0 -13
- package/dist/starters/category-mapping.js +0 -49
- package/dist/starters/corpus.d.ts +0 -2
- package/dist/starters/corpus.js +0 -194
- package/dist/starters/library.d.ts +0 -18
- package/dist/starters/library.js +0 -692
- package/dist/starters/public-gallery.d.ts +0 -266
- package/dist/starters/public-gallery.js +0 -916
- package/dist/starters/public-handoff.d.ts +0 -164
- package/dist/starters/public-handoff.js +0 -361
- package/dist/starters/public-preview-artifacts.d.ts +0 -43
- package/dist/starters/public-preview-artifacts.js +0 -104
- package/dist/starters/public-preview.d.ts +0 -61
- package/dist/starters/public-preview.js +0 -83
- package/dist/starters/showcase.d.ts +0 -2
- package/dist/starters/showcase.js +0 -335
- package/dist/starters/sources.d.ts +0 -45
- package/dist/starters/sources.js +0 -7762
- package/dist/starters/theme.d.ts +0 -45
- package/dist/starters/theme.js +0 -77
- package/dist/starters.d.ts +0 -2
- package/dist/starters.js +0 -874
- package/dist/tailwind-BYq7Lo6u.js +0 -727
- package/dist/template-DWftqh4x.d.ts +0 -60
- 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, escapeAttribute, kebab
|
|
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;
|
|
@@ -192,95 +277,6 @@ const renderHeadCss = (rules, headAtRules, variables) => {
|
|
|
192
277
|
return blocks.join("");
|
|
193
278
|
};
|
|
194
279
|
//#endregion
|
|
195
|
-
//#region src/email/font-stacks.ts
|
|
196
|
-
/** Generic families every email client understands as the last resort in a stack. */
|
|
197
|
-
const GENERIC_FAMILIES = /* @__PURE__ */ new Set([
|
|
198
|
-
"serif",
|
|
199
|
-
"sans-serif",
|
|
200
|
-
"monospace",
|
|
201
|
-
"cursive",
|
|
202
|
-
"fantasy"
|
|
203
|
-
]);
|
|
204
|
-
/** One family name as written, without its quotes and with runs of spaces collapsed. */
|
|
205
|
-
const unquote = (value) => {
|
|
206
|
-
const trimmed = value.trim();
|
|
207
|
-
const quote = trimmed[0];
|
|
208
|
-
return (quote === "\"" || quote === "'") && trimmed.endsWith(quote) && trimmed.length >= 2 ? trimmed.slice(1, -1) : trimmed.replace(/\s+/g, " ");
|
|
209
|
-
};
|
|
210
|
-
/** The families of a `font-family` value, unquoted, in stack order. */
|
|
211
|
-
const fontStackFamilies = (value) => {
|
|
212
|
-
const families = [];
|
|
213
|
-
let start = 0;
|
|
214
|
-
let quote;
|
|
215
|
-
for (let index = 0; index < value.length; index++) {
|
|
216
|
-
const character = value[index];
|
|
217
|
-
if (quote !== void 0) {
|
|
218
|
-
if (character === quote) quote = void 0;
|
|
219
|
-
} else if (character === "'" || character === "\"") quote = character;
|
|
220
|
-
else if (character === ",") {
|
|
221
|
-
families.push(unquote(value.slice(start, index)));
|
|
222
|
-
start = index + 1;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
families.push(unquote(value.slice(start)));
|
|
226
|
-
return families.filter((family) => family !== "");
|
|
227
|
-
};
|
|
228
|
-
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;
|
|
229
|
-
/** Whitespace-separated tokens at the top level, keeping parentheses and quotes whole. */
|
|
230
|
-
const shorthandTokens = (value) => {
|
|
231
|
-
const tokens = [];
|
|
232
|
-
let current = "";
|
|
233
|
-
let depth = 0;
|
|
234
|
-
let quote;
|
|
235
|
-
for (const character of value.trim()) {
|
|
236
|
-
if (quote !== void 0) {
|
|
237
|
-
if (character === quote) quote = void 0;
|
|
238
|
-
} else if (character === "'" || character === "\"") quote = character;
|
|
239
|
-
else if (character === "(") depth++;
|
|
240
|
-
else if (character === ")") depth--;
|
|
241
|
-
else if (/\s/.test(character) && depth === 0) {
|
|
242
|
-
if (current !== "") tokens.push(current);
|
|
243
|
-
current = "";
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
current += character;
|
|
247
|
-
}
|
|
248
|
-
if (current !== "") tokens.push(current);
|
|
249
|
-
return tokens;
|
|
250
|
-
};
|
|
251
|
-
/**
|
|
252
|
-
* The family list of a `font` shorthand: everything after the size (and its
|
|
253
|
-
* optional line height), or `undefined` for a system-font keyword that names
|
|
254
|
-
* no family.
|
|
255
|
-
*/
|
|
256
|
-
const fontShorthandFamilies = (value) => {
|
|
257
|
-
const tokens = shorthandTokens(value);
|
|
258
|
-
const size = tokens.findIndex((token) => FONT_SIZE.test(token.replace(/\/.*$/, "")));
|
|
259
|
-
if (size < 0) return void 0;
|
|
260
|
-
let next = size + 1;
|
|
261
|
-
const sizeToken = tokens[size];
|
|
262
|
-
if (sizeToken.endsWith("/")) next += 1;
|
|
263
|
-
else if (!sizeToken.includes("/") && tokens[next]?.startsWith("/")) next += tokens[next] === "/" ? 2 : 1;
|
|
264
|
-
const family = tokens.slice(next).join(" ");
|
|
265
|
-
return family === "" ? void 0 : family;
|
|
266
|
-
};
|
|
267
|
-
/**
|
|
268
|
-
* True when a stack names a declared project family without ending in a
|
|
269
|
-
* generic. `declared` holds lowercased family names. A quoted `"sans-serif"`
|
|
270
|
-
* names a font called sans-serif, not the generic, so it does not count.
|
|
271
|
-
*/
|
|
272
|
-
const lacksGenericFallback = (value, declared) => {
|
|
273
|
-
const stack = value.replace(/\s*!important\s*$/i, "");
|
|
274
|
-
if (!fontStackFamilies(stack).some((family) => declared.has(family.toLowerCase()))) return false;
|
|
275
|
-
const last = stack.slice(stack.lastIndexOf(",") + 1).trim();
|
|
276
|
-
return !GENERIC_FAMILIES.has(last.toLowerCase());
|
|
277
|
-
};
|
|
278
|
-
const fontFallbackDiagnostic = (value) => ({
|
|
279
|
-
code: "email-font-fallback",
|
|
280
|
-
severity: "error",
|
|
281
|
-
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.`
|
|
282
|
-
});
|
|
283
|
-
//#endregion
|
|
284
280
|
//#region src/email/selectors.ts
|
|
285
281
|
const classListOf = (element) => {
|
|
286
282
|
const value = element.props.class ?? element.props.className;
|
|
@@ -578,189 +574,6 @@ const applyStylesheet = (root, sheet, options = {}) => {
|
|
|
578
574
|
};
|
|
579
575
|
};
|
|
580
576
|
//#endregion
|
|
581
|
-
|
|
582
|
-
/** Classic Outlook lays a document out correctly only under a transitional doctype. */
|
|
583
|
-
const EMAIL_DOCTYPE = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">";
|
|
584
|
-
const settle = (diagnostics) => {
|
|
585
|
-
if (hasBlockingDiagnostic(diagnostics)) throw new EmailCompileError({ diagnostics });
|
|
586
|
-
return diagnostics;
|
|
587
|
-
};
|
|
588
|
-
/** Lower the derived tree to HTML and its plain-text alternative. */
|
|
589
|
-
const compileEmail = (node, options = {}) => {
|
|
590
|
-
const serialized = serializeEmailHtml(node, options);
|
|
591
|
-
const diagnostics = settle([...drainEmailDiagnostics(), ...serialized.diagnostics]);
|
|
592
|
-
return {
|
|
593
|
-
html: serialized.html,
|
|
594
|
-
text: deriveEmailText(node),
|
|
595
|
-
diagnostics,
|
|
596
|
-
positions: serialized.positions
|
|
597
|
-
};
|
|
598
|
-
};
|
|
599
|
-
/** The preview text already present in the tree, from `<Email preheader>`. */
|
|
600
|
-
const declaredPreheader = (node) => {
|
|
601
|
-
switch (node.type) {
|
|
602
|
-
case "Text":
|
|
603
|
-
case "Raw": return;
|
|
604
|
-
case "Fragment": return node.children.reduce((found, child) => found ?? declaredPreheader(child), void 0);
|
|
605
|
-
case "Element": {
|
|
606
|
-
const declared = node.props[PREHEADER_ATTRIBUTE];
|
|
607
|
-
if (typeof declared === "string") return declared;
|
|
608
|
-
return node.children.reduce((found, child) => found ?? declaredPreheader(child), void 0);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
};
|
|
612
|
-
/** Put the hidden preview block first inside `<body>`, or first overall when there is no shell. */
|
|
613
|
-
const withPreheader = (node, text) => {
|
|
614
|
-
const insert = (current) => {
|
|
615
|
-
if (current.type === "Fragment") {
|
|
616
|
-
for (const [index, child] of current.children.entries()) {
|
|
617
|
-
const replaced = insert(child);
|
|
618
|
-
if (replaced !== void 0) return EmailNode.Fragment({ children: current.children.with(index, replaced) });
|
|
619
|
-
}
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
if (current.type !== "Element") return void 0;
|
|
623
|
-
if (current.tag === "body") return EmailNode.Element({
|
|
624
|
-
...current,
|
|
625
|
-
children: [preheaderNode(text), ...current.children]
|
|
626
|
-
});
|
|
627
|
-
for (const [index, child] of current.children.entries()) {
|
|
628
|
-
const replaced = insert(child);
|
|
629
|
-
if (replaced !== void 0) return EmailNode.Element({
|
|
630
|
-
...current,
|
|
631
|
-
children: current.children.with(index, replaced)
|
|
632
|
-
});
|
|
633
|
-
}
|
|
634
|
-
};
|
|
635
|
-
return insert(node) ?? EmailNode.Fragment({ children: [preheaderNode(text), node] });
|
|
636
|
-
};
|
|
637
|
-
const headStyle = (css, origins) => EmailNode.Element({
|
|
638
|
-
tag: "style",
|
|
639
|
-
props: {},
|
|
640
|
-
children: [EmailNode.Text({ value: css })],
|
|
641
|
-
origins,
|
|
642
|
-
authored: false
|
|
643
|
-
});
|
|
644
|
-
/**
|
|
645
|
-
* Put the retained rules in a `<style>` at the end of `<head>`, and project
|
|
646
|
-
* `@font-face` rules in a sibling classic Outlook cannot see. Outlook for
|
|
647
|
-
* Windows registers a face it cannot load and then renders the family in Times
|
|
648
|
-
* New Roman, ignoring the stack; hidden, the family is merely missing and the
|
|
649
|
-
* stack's fallback applies.
|
|
650
|
-
*/
|
|
651
|
-
const withHeadCss = (node, css, fontFaceCss) => {
|
|
652
|
-
const insert = (current) => {
|
|
653
|
-
if (current.type === "Fragment") {
|
|
654
|
-
for (const [index, child] of current.children.entries()) {
|
|
655
|
-
const replaced = insert(child);
|
|
656
|
-
if (replaced !== void 0) return EmailNode.Fragment({ children: current.children.with(index, replaced) });
|
|
657
|
-
}
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
660
|
-
if (current.type !== "Element") return void 0;
|
|
661
|
-
if (current.tag === "head") return EmailNode.Element({
|
|
662
|
-
...current,
|
|
663
|
-
children: [
|
|
664
|
-
...current.children,
|
|
665
|
-
...css === "" ? [] : [headStyle(css, current.origins)],
|
|
666
|
-
...fontFaceCss === "" ? [] : [
|
|
667
|
-
EmailNode.Raw({
|
|
668
|
-
html: "<!--[if !mso]><!-->",
|
|
669
|
-
text: "",
|
|
670
|
-
origins: []
|
|
671
|
-
}),
|
|
672
|
-
headStyle(fontFaceCss, current.origins),
|
|
673
|
-
EmailNode.Raw({
|
|
674
|
-
html: "<!--<![endif]-->",
|
|
675
|
-
text: "",
|
|
676
|
-
origins: []
|
|
677
|
-
})
|
|
678
|
-
]
|
|
679
|
-
]
|
|
680
|
-
});
|
|
681
|
-
for (const [index, child] of current.children.entries()) {
|
|
682
|
-
const replaced = insert(child);
|
|
683
|
-
if (replaced !== void 0) return EmailNode.Element({
|
|
684
|
-
...current,
|
|
685
|
-
children: current.children.with(index, replaced)
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
};
|
|
689
|
-
return insert(node);
|
|
690
|
-
};
|
|
691
|
-
/** Render one validated input through the template's only content function. Host isolation is required for untrusted source. */
|
|
692
|
-
const renderEmail = (template, data, options = {}) => renderEmailWith(snapshotStylesheets(), template, data, options);
|
|
693
|
-
/**
|
|
694
|
-
* A render bound to the sheets registered so far. A compiled project's wrapper
|
|
695
|
-
* calls this once its own stylesheet imports have run, so a later project
|
|
696
|
-
* loaded into the same process cannot restyle an entry that is rendered again.
|
|
697
|
-
*/
|
|
698
|
-
const bindStylesheets = (stylesheet = snapshotStylesheets()) => (template, data, options = {}) => renderEmailWith(stylesheet, template, data, options);
|
|
699
|
-
const renderEmailWith = (stylesheet, template, data, options) => {
|
|
700
|
-
if (!template.schema.validate(data)) throw new TypeError(`Invalid template input: ${JSON.stringify(template.schema.validate.errors)}`);
|
|
701
|
-
return renderEmailContent(stylesheet, () => template.render(data), options);
|
|
702
|
-
};
|
|
703
|
-
/**
|
|
704
|
-
* Render authored content against a stylesheet: inline what a client honors,
|
|
705
|
-
* keep the rest for `<head>`, and lower to HTML and text. `renderEmail` runs a
|
|
706
|
-
* validated template through it; a host that composes content without a
|
|
707
|
-
* template definition calls it directly.
|
|
708
|
-
*/
|
|
709
|
-
const renderEmailContent = (stylesheet, render, options = {}) => {
|
|
710
|
-
drainEmailDiagnostics();
|
|
711
|
-
const content = render();
|
|
712
|
-
const invalid = [];
|
|
713
|
-
const fail = (code, message) => invalid.push({
|
|
714
|
-
code,
|
|
715
|
-
severity: "error",
|
|
716
|
-
message,
|
|
717
|
-
origins: []
|
|
718
|
-
});
|
|
719
|
-
if (typeof content.subject !== "string" || /[\r\n\u0000]/.test(content.subject)) fail("invalid-subject", "Invalid email subject: it must be one line of text.");
|
|
720
|
-
if (content.preheader !== void 0 && typeof content.preheader !== "string") fail("invalid-preheader", "Invalid email preheader: it must be a string.");
|
|
721
|
-
if (content.text !== void 0 && typeof content.text !== "string") fail("invalid-text", "Invalid plain text: it must be a string.");
|
|
722
|
-
const declared = declaredPreheader(content.body);
|
|
723
|
-
if (content.preheader !== void 0 && declared !== void 0) fail("duplicate-preheader", "Preheader is set on both the template content and <Email preheader>; keep one.");
|
|
724
|
-
settle(invalid);
|
|
725
|
-
const base = content.preheader === void 0 ? content.body : withPreheader(content.body, content.preheader);
|
|
726
|
-
const styled = stylesheet === void 0 ? {
|
|
727
|
-
tree: base,
|
|
728
|
-
headCss: "",
|
|
729
|
-
fontFaceCss: "",
|
|
730
|
-
diagnostics: []
|
|
731
|
-
} : applyStylesheet(base, stylesheet);
|
|
732
|
-
const style = [...styled.diagnostics];
|
|
733
|
-
let tree = styled.tree;
|
|
734
|
-
if (styled.headCss !== "" || styled.fontFaceCss !== "") {
|
|
735
|
-
const placed = withHeadCss(tree, styled.headCss, styled.fontFaceCss);
|
|
736
|
-
if (placed === void 0) style.push({
|
|
737
|
-
code: "missing-head",
|
|
738
|
-
severity: "error",
|
|
739
|
-
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.",
|
|
740
|
-
origins: []
|
|
741
|
-
});
|
|
742
|
-
else tree = placed;
|
|
743
|
-
}
|
|
744
|
-
settle(style);
|
|
745
|
-
const compiled = compileEmail(tree, options);
|
|
746
|
-
const prefix = compiled.html.startsWith("<html") ? EMAIL_DOCTYPE : "";
|
|
747
|
-
const html = `${prefix}${compiled.html}`;
|
|
748
|
-
const positions = prefix === "" ? compiled.positions : compiled.positions.map((position) => ({
|
|
749
|
-
...position,
|
|
750
|
-
start: position.start + prefix.length,
|
|
751
|
-
end: position.end + prefix.length
|
|
752
|
-
}));
|
|
753
|
-
return {
|
|
754
|
-
subject: content.subject,
|
|
755
|
-
preheader: content.preheader ?? declared,
|
|
756
|
-
html,
|
|
757
|
-
text: content.text ?? compiled.text,
|
|
758
|
-
tree,
|
|
759
|
-
diagnostics: [...style, ...compiled.diagnostics],
|
|
760
|
-
positions
|
|
761
|
-
};
|
|
762
|
-
};
|
|
763
|
-
//#endregion
|
|
764
|
-
export { renderEmailContent as a, matchesSelector as c, finalizeValue as d, foldCalc as f, renderEmail as i, unquote as l, resolveVariables as m, bindStylesheets as n, applyStylesheet as o, renderHeadCss as p, compileEmail as r, classListOf as s, EMAIL_DOCTYPE as t, downlevelMediaQuery 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 };
|
|
765
578
|
|
|
766
|
-
//# 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
|