@samva/markup 0.18.1 → 0.19.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 (54) hide show
  1. package/AGENTS.md +11 -4
  2. package/CHANGELOG.md +36 -0
  3. package/README.md +89 -5
  4. package/dist/assets-BSpgZOSR.js +61 -0
  5. package/dist/assets-k7_v3IOJ.d.ts +39 -0
  6. package/dist/{assets-CS5Xa_mr.d.ts → brand-Bxb6Sm7y.d.ts} +34 -41
  7. package/dist/brand.d.ts +2 -2
  8. package/dist/brand.js +3 -3
  9. package/dist/compatibility.d.ts +15 -5
  10. package/dist/compatibility.js +48 -5
  11. package/dist/compile-C1GWLQ5C.js +490 -0
  12. package/dist/compile-CIeuGmP-.d.ts +98 -0
  13. package/dist/{compiled-template-Bc4Dq9zy.d.ts → compiled-template-CPDHvFlt.d.ts} +157 -6
  14. package/dist/compiler.d.ts +4 -89
  15. package/dist/compiler.js +8 -486
  16. package/dist/components-DNBCz20_.js +3267 -0
  17. package/dist/{css-color-DLM12Osu.js → dark-scheme-BGDCCaUq.js} +12 -1
  18. package/dist/{diagnostic-codes-DZKb0Xcg.js → diagnostic-codes-BCwfUgnF.js} +33 -36
  19. package/dist/diagnostic-model-DJtqbETd.js +36 -0
  20. package/dist/{diagnostics-D4vSLQrO.d.ts → diagnostics-C3TSx6SU.d.ts} +22 -2
  21. package/dist/diagnostics.d.ts +33 -1
  22. package/dist/diagnostics.js +2 -1
  23. package/dist/edit.d.ts +15 -4
  24. package/dist/edit.js +67 -4
  25. package/dist/email/jsx-dev-runtime.d.ts +2 -2
  26. package/dist/email/jsx-runtime.d.ts +1 -1
  27. package/dist/email.d.ts +6 -2
  28. package/dist/email.js +6 -2
  29. package/dist/index.d.ts +1 -1
  30. package/dist/{jsx-runtime-DL8NVF1f.d.ts → jsx-runtime-HHfrVZEf.d.ts} +1 -2
  31. package/dist/markdown.d.ts +69 -0
  32. package/dist/markdown.js +95 -0
  33. package/dist/parse-Bex7QFqj.js +921 -0
  34. package/dist/{preview-rNoJBEIY.d.ts → preview-DRLWgmfg.d.ts} +3 -3
  35. package/dist/profile-DFk4gs7o.js +56 -0
  36. package/dist/{profile-JTvgCD6X.js → project-B6D64SZS.js} +497 -234
  37. package/dist/render.d.ts +8 -8
  38. package/dist/render.js +1 -2
  39. package/dist/{css-B_aVKrDo.js → selectors-CKGuc9MO.js} +158 -4
  40. package/dist/sms.d.ts +1 -1
  41. package/dist/{template-DmNcoH4Q.d.ts → template-4Dse7_OC.d.ts} +1 -1
  42. package/dist/{theme-css-P9F5S7Ic.js → theme-css-BujDuuEA.js} +1 -2
  43. package/dist/whatsapp.d.ts +1 -1
  44. package/generated/contract.json +41 -1
  45. package/generated/reference.md +97 -89
  46. package/package.json +15 -4
  47. package/dist/brand-CIiEqkU7.js +0 -193
  48. package/dist/brand-plugin-Dj9SMpfS.d.ts +0 -33
  49. package/dist/components-C5850-Qg.js +0 -409
  50. package/dist/dark-scheme-CbCwGDWH.js +0 -12
  51. package/dist/html-DJUK2YJF.js +0 -483
  52. package/dist/render-template-DwPDm3Hq.js +0 -1226
  53. package/dist/selectors-CYWCoxZr.js +0 -157
  54. package/dist/source-locations-C8wCsiAy.d.ts +0 -22
@@ -1,483 +0,0 @@
1
- import { n as isObject } from "./guards-eV1qLbf2.js";
2
- import { a as EMAIL_ELEMENTS, d as reservedBrandAttributes, f as samvaBrandPlugin, o as EMAIL_GLOBAL_ATTRIBUTES, r as renderEmail, s as emailElement } from "./render-template-DwPDm3Hq.js";
3
- //#region src/internal/unsubscribe-link.ts
4
- /**
5
- * Marks the one link the serializer lets carry the brand plugin's unsubscribe
6
- * placeholder as its href. Only `BrandFooter` sets it; nothing public exports
7
- * it, so no author-written link can claim the exemption.
8
- */
9
- const UNSUBSCRIBE_LINK = Symbol("samva.unsubscribe-link");
10
- /**
11
- * Marks the elements `BrandFooter` writes the brand plugin's footer markers
12
- * on. The serializer emits those reserved attributes only for an element
13
- * carrying it, so an author cannot claim a footer the send path would then
14
- * not append.
15
- */
16
- const BRAND_FOOTER_MARKER = Symbol("samva.brand-footer-marker");
17
- //#endregion
18
- //#region src/preview.ts
19
- /**
20
- * The attribute a preview stamps so a click in a rendered document resolves to exactly one
21
- * rendered element. A delivered message never carries it.
22
- */
23
- const INSTANCE_PATH_ATTRIBUTE = "data-samva-instance";
24
- const locationKey = (origin) => `${origin.fileName}:${origin.lineNumber}:${origin.columnNumber}`;
25
- const originKey = (selection) => `${selection.tag}\u0000${selection.origins.map(locationKey).join("\0")}`;
26
- const parentOf = (instancePath) => {
27
- const cut = instancePath.lastIndexOf(".");
28
- return cut === -1 ? void 0 : instancePath.slice(0, cut);
29
- };
30
- /**
31
- * Turn one render's positions into selections, in document order. `occurrence` and `occurrences`
32
- * count the rendered elements that share an authored origin and tag.
33
- */
34
- const emailSelections = (positions) => {
35
- const counts = /* @__PURE__ */ new Map();
36
- for (const position of positions) {
37
- const key = originKey(position);
38
- counts.set(key, (counts.get(key) ?? 0) + 1);
39
- }
40
- const seen = /* @__PURE__ */ new Map();
41
- return [...positions].toSorted((left, right) => left.start === right.start ? right.end - left.end : left.start - right.start).map((position) => {
42
- const key = originKey(position);
43
- const occurrence = (seen.get(key) ?? 0) + 1;
44
- seen.set(key, occurrence);
45
- const parentPath = parentOf(position.instancePath);
46
- return {
47
- instancePath: position.instancePath,
48
- tag: position.tag,
49
- origins: position.origins,
50
- authored: position.authored,
51
- occurrence,
52
- occurrences: counts.get(key) ?? 1,
53
- ...parentPath === void 0 ? {} : { parentPath },
54
- start: position.start,
55
- end: position.end
56
- };
57
- });
58
- };
59
- /**
60
- * Render one fixture with an instance path stamped on every element, and the selection each path
61
- * resolves to. The selections come from the spans the compiler recorded, so a click on the canvas
62
- * lands on the exact node of the source that produced it.
63
- */
64
- const previewEmail = (template, input, options = {}) => {
65
- const rendered = renderEmail(template, input, {
66
- ...options,
67
- positions: true
68
- });
69
- const sources = template.sources ?? [];
70
- const ordered = (rendered.positions ?? []).map((position) => {
71
- const src = position.src;
72
- const origins = src === void 0 ? [] : [{
73
- fileName: sources[src[2] ?? 0] ?? "",
74
- lineNumber: src[0],
75
- columnNumber: src[1]
76
- }];
77
- return {
78
- start: position.start,
79
- end: position.end,
80
- tag: position.tag,
81
- src: src === void 0 ? void 0 : src.join(":"),
82
- origins,
83
- path: "",
84
- parent: void 0
85
- };
86
- }).toSorted((left, right) => left.start === right.start ? right.end - left.end : left.start - right.start);
87
- const open = [];
88
- const childCount = /* @__PURE__ */ new Map();
89
- for (const element of ordered) {
90
- while (open.length > 0 && open[open.length - 1].end <= element.start) open.pop();
91
- const parent = open[open.length - 1];
92
- const index = childCount.get(parent) ?? 0;
93
- childCount.set(parent, index + 1);
94
- element.parent = parent;
95
- element.path = parent === void 0 ? String(index) : `${parent.path}.${index}`;
96
- open.push(element);
97
- }
98
- const inserts = ordered.map((element) => ({
99
- at: element.start + 1 + element.tag.length,
100
- text: ` ${INSTANCE_PATH_ATTRIBUTE}="${element.path}"`
101
- }));
102
- inserts.sort((left, right) => left.at - right.at);
103
- let html = "";
104
- let cursor = 0;
105
- for (const insert of inserts) {
106
- html += rendered.html.slice(cursor, insert.at) + insert.text;
107
- cursor = insert.at;
108
- }
109
- html += rendered.html.slice(cursor);
110
- const shift = (offset) => {
111
- let moved = 0;
112
- for (const insert of inserts) {
113
- if (insert.at > offset) break;
114
- moved += insert.text.length;
115
- }
116
- return offset + moved;
117
- };
118
- const positions = ordered.map((element) => ({
119
- start: shift(element.start),
120
- end: shift(element.end),
121
- origins: element.origins,
122
- instancePath: element.path,
123
- tag: element.tag,
124
- authored: element.src !== void 0 && (element.parent === void 0 || element.parent.src !== element.src)
125
- }));
126
- const selections = emailSelections(positions);
127
- return {
128
- subject: rendered.subject,
129
- preheader: rendered.preheader,
130
- html,
131
- text: rendered.text,
132
- selections
133
- };
134
- };
135
- /** Look selections up by the instance path a rendered document carries. */
136
- const emailSelectionIndex = (selections) => new Map(selections.map((selection) => [selection.instancePath, selection]));
137
- /**
138
- * The authoring element a selection edits. For an authored element that is the element itself; for
139
- * a generated wrapper it is the primitive that asked for it. Undefined only where the compiler
140
- * generated markup with no authoring behind it, which nothing can edit in place.
141
- */
142
- const emailAuthoringTarget = (selection) => selection.origins[0];
143
- /** Root-first ancestry of a selection, itself last. */
144
- const emailSelectionAncestry = (index, instancePath) => {
145
- const chain = [];
146
- let current = index.get(instancePath);
147
- while (current !== void 0) {
148
- chain.unshift(current);
149
- current = current.parentPath === void 0 ? void 0 : index.get(current.parentPath);
150
- }
151
- return chain;
152
- };
153
- /**
154
- * Every rendered instance produced by the same authored element, in document order. One entry
155
- * means it rendered once; several mean it repeated.
156
- */
157
- const emailSelectionSiblings = (selections, selection) => {
158
- const key = originKey(selection);
159
- return selections.filter((candidate) => originKey(candidate) === key);
160
- };
161
- /**
162
- * Every rendered instance of the same authored element. A partial edited at its definition changes
163
- * all of these, which is the scope a control has to name before it applies.
164
- */
165
- const emailDefinitionInstances = (selections, selection) => {
166
- const authoring = selection.origins[0];
167
- const key = `${selection.tag}\u0000${authoring === void 0 ? "" : locationKey(authoring)}`;
168
- return selections.filter((candidate) => {
169
- const origin = candidate.origins[0];
170
- return `${candidate.tag}\u0000${origin === void 0 ? "" : locationKey(origin)}` === key;
171
- });
172
- };
173
- //#endregion
174
- //#region src/email/html.ts
175
- /** Escape a value for element content. */
176
- const escapeText = (value) => value.replace(/[&<>]/g, (character) => ({
177
- "&": "&amp;",
178
- "<": "&lt;",
179
- ">": "&gt;"
180
- })[character] ?? character);
181
- /** Escape a value for a double-quoted attribute. */
182
- const escapeAttribute = (value) => value.replace(/[&<>"']/g, (character) => ({
183
- "&": "&amp;",
184
- "<": "&lt;",
185
- ">": "&gt;",
186
- "\"": "&quot;",
187
- "'": "&#39;"
188
- })[character] ?? character);
189
- const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
190
- const SAFE_URL_SCHEME = /^(https:\/\/|mailto:|tel:|cid:|#)/i;
191
- const RELATIVE_URL = /^(\.{1,2}\/|\/(?!\/))/;
192
- /**
193
- * Only references an email client will follow safely; no `javascript:`, no
194
- * `data:`. A relative reference is legal only for an export: a file on disk can
195
- * resolve one against its own directory, and a delivered message cannot resolve
196
- * anything but an absolute URL.
197
- */
198
- const isSafeUrl = (value, relative) => !/^[\u0000-\u0020]/.test(value) && !CONTROL_CHARACTERS.test(value) && !value.includes("\\") && (SAFE_URL_SCHEME.test(value) || relative && RELATIVE_URL.test(value));
199
- const CSS_PROPERTY = /^(--[a-zA-Z0-9-]+|[a-zA-Z][a-zA-Z0-9-]*)$/;
200
- const CSS_UNSAFE = /[;{}<>\\]|expression\s*\(|@import|\/\*/i;
201
- const CSS_URL = /url\(\s*(['"]?)([^'")]*)\1\s*\)/gi;
202
- /** camelCase to kebab-case; already-kebab names pass through unchanged. */
203
- const kebab = (property) => property.startsWith("--") ? property : property.replace(/[A-Z]/g, (l) => `-${l.toLowerCase()}`);
204
- const UNITLESS = /* @__PURE__ */ new Set([
205
- "opacity",
206
- "zIndex",
207
- "fontWeight",
208
- "lineHeight",
209
- "order",
210
- "flexGrow",
211
- "flexShrink",
212
- "columnCount",
213
- "orphans",
214
- "widows",
215
- "zoom",
216
- "fillOpacity",
217
- "strokeOpacity"
218
- ]);
219
- /** A bare number is pixels for every property that takes a length. */
220
- const declarationValue = (property, value) => typeof value === "number" && value !== 0 && !UNITLESS.has(property) && !property.startsWith("--") ? `${value}px` : String(value);
221
- const report = (sink, code, message, origins, severity = "error") => {
222
- sink.diagnostics.push({
223
- code,
224
- severity,
225
- message,
226
- origins
227
- });
228
- };
229
- const declarations = (sink, style, origins) => {
230
- const parts = [];
231
- for (const [property, value] of Object.entries(style)) {
232
- if (value === void 0 || value === null) continue;
233
- if (!CSS_PROPERTY.test(property)) {
234
- report(sink, "unsupported-style-property", `Style property ${JSON.stringify(property)} is not a CSS property name; write it in camelCase (backgroundColor) or as a custom property (--brand).`, origins);
235
- continue;
236
- }
237
- if (typeof value !== "string" && typeof value !== "number") {
238
- report(sink, "invalid-style-value", `Style ${property} must be a string or number, not ${typeof value}.`, origins);
239
- continue;
240
- }
241
- const text = declarationValue(property, value);
242
- if (CSS_UNSAFE.test(text) || CONTROL_CHARACTERS.test(text)) {
243
- report(sink, "unsafe-style-value", `Style ${property} contains CSS that could break out of the declaration; remove ${JSON.stringify(text)}.`, origins);
244
- continue;
245
- }
246
- let unsafeUrl;
247
- for (const match of text.matchAll(CSS_URL)) if (!isSafeUrl(match[2] ?? "", sink.relativeUrls)) unsafeUrl = match[2] ?? "";
248
- if (unsafeUrl !== void 0) {
249
- report(sink, "unsafe-url", `Style ${property} points at ${JSON.stringify(unsafeUrl)}; email URLs must use https:, mailto:, tel:, cid: or a fragment${sink.relativeUrls ? ", or a relative path" : ""}.`, origins);
250
- continue;
251
- }
252
- parts.push(`${kebab(property)}:${text}`);
253
- }
254
- return parts.join(";");
255
- };
256
- const attributeValue = (sink, tag, name, spec, value, origins) => {
257
- if (value === void 0 || value === null || value === false) return void 0;
258
- if (value === true) return "";
259
- if (typeof value !== "string" && typeof value !== "number") {
260
- report(sink, "invalid-attribute-value", `<${tag} ${name}> must be a string or number, not ${typeof value}.`, origins);
261
- return;
262
- }
263
- const text = String(value);
264
- if (CONTROL_CHARACTERS.test(text)) {
265
- report(sink, "invalid-attribute-value", `<${tag} ${name}> contains control characters.`, origins);
266
- return;
267
- }
268
- if (spec.values !== void 0 && !spec.values.includes(text)) {
269
- report(sink, "invalid-attribute-value", `<${tag} ${name}> accepts ${spec.values.join(", ")}; received ${JSON.stringify(text)}.`, origins);
270
- return;
271
- }
272
- if (spec.type === "number" && !/^-?\d+$/.test(text)) {
273
- report(sink, "invalid-attribute-value", `<${tag} ${name}> must be an integer.`, origins);
274
- return;
275
- }
276
- if (spec.type === "measure" && !/^\d+(\.\d+)?%?$/.test(text)) {
277
- report(sink, "invalid-attribute-value", `<${tag} ${name}> must be a pixel count or a percentage.`, origins);
278
- return;
279
- }
280
- if (spec.type === "url" && !isSafeUrl(text, sink.relativeUrls)) {
281
- report(sink, "unsafe-url", sink.relativeUrls ? `<${tag} ${name}> must use https:, mailto:, tel:, cid:, a fragment, or a relative path; received ${JSON.stringify(text)}.` : `<${tag} ${name}> must use https:, mailto:, tel:, cid: or a fragment; received ${JSON.stringify(text)}. A relative path resolves only in an export, never in a delivered message.`, origins);
282
- return;
283
- }
284
- return text;
285
- };
286
- const attributes = (sink, tag, spec, props, origins) => {
287
- const emitted = [];
288
- const present = /* @__PURE__ */ new Set();
289
- let classes;
290
- const unsubscribeLink = tag === "a" && props[UNSUBSCRIBE_LINK] === true;
291
- const { unsubscribeUrlPlaceholder, noTrackAttribute } = sink.brandPlugin;
292
- for (const [key, value] of Object.entries(props)) {
293
- if (key === "key") continue;
294
- const name = key === "className" ? "class" : key;
295
- if (unsubscribeLink && name === "href" && value === unsubscribeUrlPlaceholder) {
296
- present.add(name);
297
- emitted.push(` href="${escapeAttribute(unsubscribeUrlPlaceholder)}" ${noTrackAttribute}`);
298
- continue;
299
- }
300
- if (name === "class") {
301
- if (value === void 0 || value === null) continue;
302
- if (typeof value !== "string") {
303
- report(sink, "invalid-attribute-value", `<${tag} class> must be a string.`, origins);
304
- continue;
305
- }
306
- if (classes !== void 0) {
307
- report(sink, "duplicate-attribute", `<${tag}> sets both class and className; keep one.`, origins);
308
- continue;
309
- }
310
- classes = value;
311
- emitted.push(` class="${escapeAttribute(value)}"`);
312
- continue;
313
- }
314
- if (name === "style") {
315
- if (value === void 0 || value === null) continue;
316
- if (!isObject(value) || Array.isArray(value)) {
317
- report(sink, "invalid-style", `<${tag} style> must be a property object, for example style={{ backgroundColor: "#fff" }}.`, origins);
318
- continue;
319
- }
320
- const css = declarations(sink, value, origins);
321
- if (css !== "") emitted.push(` style="${escapeAttribute(css)}"`);
322
- continue;
323
- }
324
- if (/^on/i.test(name)) {
325
- report(sink, "event-handler", `<${tag} ${name}> is an event handler; email clients strip scripting and Samva refuses it.`, origins);
326
- continue;
327
- }
328
- if (sink.reservedAttributes.has(name) && props[BRAND_FOOTER_MARKER] !== true) {
329
- report(sink, "reserved-attribute", `<${tag} ${name}> is written only by BrandFooter from ${sink.brandPlugin.specifier}; render BrandFooter instead of marking an element as the footer.`, origins);
330
- continue;
331
- }
332
- if (/^(data|aria)-[a-z0-9-]+$/.test(name)) {
333
- const text = attributeValue(sink, tag, name, { type: "string" }, value, origins);
334
- if (text !== void 0) emitted.push(` ${name}="${escapeAttribute(text)}"`);
335
- continue;
336
- }
337
- const globalAttributes = EMAIL_GLOBAL_ATTRIBUTES;
338
- const attribute = Object.hasOwn(globalAttributes, name) ? globalAttributes[name] : Object.hasOwn(spec.attributes, name) ? spec.attributes[name] : void 0;
339
- if (attribute === void 0) {
340
- const supported = [
341
- ...Object.keys(EMAIL_GLOBAL_ATTRIBUTES),
342
- ...Object.keys(spec.attributes),
343
- "class",
344
- "style"
345
- ].sort().join(", ");
346
- report(sink, "unsupported-attribute", `<${tag}> does not support the ${name} attribute. Supported: ${supported}, plus data-* and aria-*.`, origins);
347
- continue;
348
- }
349
- present.add(name);
350
- const text = attributeValue(sink, tag, name, attribute, value, origins);
351
- if (text !== void 0) emitted.push(` ${name}="${escapeAttribute(text)}"`);
352
- }
353
- for (const [name, attribute] of Object.entries(spec.attributes)) if (attribute.required === true && !present.has(name)) report(sink, "missing-attribute", `<${tag}> requires the ${name} attribute.`, origins);
354
- return emitted.join("");
355
- };
356
- /** Fragments are transparent to a content model; raw compatibility markup is not content. */
357
- const contentChildren = (children) => children.flatMap((child) => child.type === "Fragment" ? contentChildren(child.children) : [child]);
358
- const checkContentModel = (sink, tag, spec, children, origins) => {
359
- if (spec.content === "void") {
360
- if (contentChildren(children).length > 0) report(sink, "void-element-children", `<${tag}> cannot have children.`, origins);
361
- return;
362
- }
363
- if (spec.content === "text" || spec.content === "raw") {
364
- for (const child of contentChildren(children)) if (child.type !== "Text") report(sink, "invalid-content", `<${tag}> accepts text only.`, child.type === "Element" ? child.origins : origins);
365
- return;
366
- }
367
- if (spec.childElements === void 0) return;
368
- for (const child of contentChildren(children)) {
369
- if (child.type === "Raw") continue;
370
- if (child.type === "Text") {
371
- if (child.value.trim() !== "") report(sink, "invalid-content", `<${tag}> cannot contain text directly; put it inside <${spec.childElements[0]}>.`, origins);
372
- continue;
373
- }
374
- if (child.type === "Element" && !spec.childElements.includes(child.tag)) report(sink, "invalid-content", `<${tag}> accepts only ${spec.childElements.map((name) => `<${name}>`).join(", ")}; found <${child.tag}>.`, child.origins);
375
- }
376
- };
377
- /** Every write goes through here so `sink.written` stays the emitted offset. */
378
- const emit = (sink, text) => {
379
- sink.written += text.length;
380
- return text;
381
- };
382
- const serialize = (sink, node, parentPath, counter) => {
383
- switch (node.type) {
384
- case "Text": return emit(sink, escapeText(node.value));
385
- case "Raw": return emit(sink, node.html);
386
- case "Fragment": return node.children.map((child) => serialize(sink, child, parentPath, counter)).join("");
387
- case "Element": {
388
- const start = sink.written;
389
- const index = counter.next++;
390
- const path = parentPath === "" ? String(index) : `${parentPath}.${index}`;
391
- const own = { next: 0 };
392
- const stamp = sink.instancePaths ? ` ${INSTANCE_PATH_ATTRIBUTE}="${path}"` : "";
393
- const spec = emailElement(node.tag);
394
- if (spec === void 0) {
395
- report(sink, "unsupported-element", `<${node.tag}> is not part of the supported email vocabulary. Supported elements: ${Object.keys(EMAIL_ELEMENTS).join(", ")}.`, node.origins);
396
- return "";
397
- }
398
- checkContentModel(sink, node.tag, spec, node.children, node.origins);
399
- if (spec.content === "raw") {
400
- const css = node.children.map((child) => child.type === "Text" ? child.value : "").join("");
401
- if (/<\/|<!--/i.test(css)) {
402
- report(sink, "unsafe-raw-text", `<${node.tag}> content cannot contain markup.`, node.origins);
403
- return "";
404
- }
405
- const rawAttrs = attributes(sink, node.tag, spec, node.props, node.origins);
406
- const raw = css === "" ? "" : `<${node.tag}${rawAttrs}${stamp}>${css}</${node.tag}>`;
407
- return record(sink, start, node, path, emit(sink, raw));
408
- }
409
- const attrs = attributes(sink, node.tag, spec, node.props, node.origins);
410
- if (spec.content === "void") return record(sink, start, node, path, emit(sink, `<${node.tag}${attrs}${stamp} />`));
411
- const open = emit(sink, `<${node.tag}${attrs}${stamp}>`);
412
- const children = node.children.map((child) => serialize(sink, child, path, own)).join("");
413
- const close = emit(sink, `</${node.tag}>`);
414
- return record(sink, start, node, path, `${open}${children}${close}`);
415
- }
416
- }
417
- };
418
- /** Record an element's emitted range once its content is written. */
419
- const record = (sink, start, node, instancePath, text) => {
420
- sink.positions.push({
421
- start,
422
- end: sink.written,
423
- origins: node.origins,
424
- instancePath,
425
- tag: node.tag,
426
- authored: node.authored
427
- });
428
- return text;
429
- };
430
- const serializationSink = (options) => {
431
- const brandPlugin = options.brandPlugin ?? samvaBrandPlugin;
432
- return {
433
- diagnostics: [],
434
- positions: [],
435
- written: 0,
436
- relativeUrls: options.relativeUrls === true,
437
- instancePaths: options.instancePaths === true,
438
- brandPlugin,
439
- reservedAttributes: reservedBrandAttributes(brandPlugin)
440
- };
441
- };
442
- /** Lower the derived tree to email HTML, reporting every unsupported construct with its origin. */
443
- const serializeEmailHtml = (node, options = {}) => {
444
- const sink = serializationSink(options);
445
- return {
446
- html: serialize(sink, node, "", { next: 0 }),
447
- diagnostics: sink.diagnostics,
448
- positions: sink.positions
449
- };
450
- };
451
- /** An authored style object as a declaration list, checked by the same rules as a serialized tree. */
452
- const serializeStyle = (style, origins) => {
453
- const sink = serializationSink({});
454
- return {
455
- css: declarations(sink, style, origins),
456
- diagnostics: sink.diagnostics
457
- };
458
- };
459
- /**
460
- * Check one static attribute value against its element's spec, as serialization does. Returns
461
- * the text that would be emitted, or `undefined` when the attribute is dropped or invalid.
462
- */
463
- const checkAttribute = (tag, name, value, origins) => {
464
- const sink = serializationSink({});
465
- const element = emailElement(tag);
466
- const globals = EMAIL_GLOBAL_ATTRIBUTES;
467
- const attributeSpec = () => {
468
- if (/^(data|aria)-[a-z0-9-]+$/.test(name)) return { type: "string" };
469
- if (Object.hasOwn(globals, name)) return globals[name];
470
- if (element !== void 0 && Object.hasOwn(element.attributes, name)) return element.attributes[name];
471
- };
472
- const spec = attributeSpec();
473
- if (spec === void 0) return {
474
- text: void 0,
475
- diagnostics: sink.diagnostics
476
- };
477
- return {
478
- text: attributeValue(sink, tag, name, spec, value, origins),
479
- diagnostics: sink.diagnostics
480
- };
481
- };
482
- //#endregion
483
- export { UNSUBSCRIBE_LINK as _, kebab as a, INSTANCE_PATH_ATTRIBUTE as c, emailSelectionAncestry as d, emailSelectionIndex as f, BRAND_FOOTER_MARKER as g, previewEmail as h, escapeText as i, emailAuthoringTarget as l, emailSelections as m, declarationValue as n, serializeEmailHtml as o, emailSelectionSiblings as p, escapeAttribute as r, serializeStyle as s, checkAttribute as t, emailDefinitionInstances as u };