@samva/markup 0.18.0 → 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.
- package/AGENTS.md +11 -4
- package/CHANGELOG.md +36 -0
- package/README.md +89 -5
- package/dist/assets-BSpgZOSR.js +61 -0
- package/dist/assets-k7_v3IOJ.d.ts +39 -0
- package/dist/{assets-CS5Xa_mr.d.ts → brand-Bxb6Sm7y.d.ts} +34 -41
- package/dist/brand.d.ts +2 -2
- package/dist/brand.js +3 -3
- package/dist/compatibility.d.ts +15 -5
- package/dist/compatibility.js +48 -5
- package/dist/compile-C1GWLQ5C.js +490 -0
- package/dist/compile-CIeuGmP-.d.ts +98 -0
- package/dist/{compiled-template-Bc4Dq9zy.d.ts → compiled-template-CPDHvFlt.d.ts} +157 -6
- package/dist/compiler.d.ts +4 -89
- package/dist/compiler.js +8 -486
- package/dist/components-DNBCz20_.js +3267 -0
- package/dist/{css-color-DLM12Osu.js → dark-scheme-BGDCCaUq.js} +12 -1
- package/dist/{diagnostic-codes-DZKb0Xcg.js → diagnostic-codes-BCwfUgnF.js} +33 -36
- package/dist/diagnostic-model-DJtqbETd.js +36 -0
- package/dist/{diagnostics-D4vSLQrO.d.ts → diagnostics-C3TSx6SU.d.ts} +22 -2
- package/dist/diagnostics.d.ts +33 -1
- package/dist/diagnostics.js +2 -1
- package/dist/edit.d.ts +15 -4
- package/dist/edit.js +67 -4
- package/dist/email/jsx-dev-runtime.d.ts +2 -2
- package/dist/email/jsx-runtime.d.ts +1 -1
- package/dist/email.d.ts +6 -2
- package/dist/email.js +6 -2
- package/dist/index.d.ts +1 -1
- package/dist/{jsx-runtime-DL8NVF1f.d.ts → jsx-runtime-HHfrVZEf.d.ts} +1 -2
- package/dist/markdown.d.ts +69 -0
- package/dist/markdown.js +95 -0
- package/dist/parse-Bex7QFqj.js +921 -0
- package/dist/{preview-rNoJBEIY.d.ts → preview-DRLWgmfg.d.ts} +3 -3
- package/dist/profile-DFk4gs7o.js +56 -0
- package/dist/{profile-JTvgCD6X.js → project-B6D64SZS.js} +497 -234
- package/dist/render.d.ts +8 -8
- package/dist/render.js +1 -2
- package/dist/{css-B_aVKrDo.js → selectors-CKGuc9MO.js} +158 -4
- package/dist/sms.d.ts +1 -1
- package/dist/{template-DmNcoH4Q.d.ts → template-4Dse7_OC.d.ts} +1 -1
- package/dist/{theme-css-P9F5S7Ic.js → theme-css-BujDuuEA.js} +1 -2
- package/dist/whatsapp.d.ts +1 -1
- package/generated/contract.json +41 -1
- package/generated/reference.md +97 -89
- package/package.json +15 -4
- package/dist/brand-CIiEqkU7.js +0 -193
- package/dist/brand-plugin-Dj9SMpfS.d.ts +0 -33
- package/dist/components-C5850-Qg.js +0 -409
- package/dist/dark-scheme-CbCwGDWH.js +0 -12
- package/dist/html-DJUK2YJF.js +0 -483
- package/dist/render-template-DwPDm3Hq.js +0 -1226
- package/dist/selectors-CYWCoxZr.js +0 -157
- package/dist/source-locations-C8wCsiAy.d.ts +0 -22
|
@@ -1,1226 +0,0 @@
|
|
|
1
|
-
import { callFormatter, createFmt } from "./fmt.js";
|
|
2
|
-
import { TaggedError } from "better-result";
|
|
3
|
-
//#region src/compiled-template.ts
|
|
4
|
-
/** The version this package writes. A publication pins the version it was built with. */
|
|
5
|
-
const COMPILED_TEMPLATE_VERSION = 1;
|
|
6
|
-
//#endregion
|
|
7
|
-
//#region src/email/samva-brand-plugin.ts
|
|
8
|
-
/**
|
|
9
|
-
* Samva's brand plugin, the compiler's default. A template imports its
|
|
10
|
-
* organization's brand from `samva:brand`, and Samva's send path fills
|
|
11
|
-
* `%samva:unsubscribe-url%` per delivery and turns `data-samva-no-track` into
|
|
12
|
-
* its email provider's own no-track marker.
|
|
13
|
-
*/
|
|
14
|
-
const samvaBrandPlugin = {
|
|
15
|
-
specifier: "samva:brand",
|
|
16
|
-
footerAttribute: "data-samva-footer",
|
|
17
|
-
unsubscribeRowAttribute: "data-samva-unsubscribe",
|
|
18
|
-
unsubscribeUrlPlaceholder: "%samva:unsubscribe-url%",
|
|
19
|
-
noTrackAttribute: "data-samva-no-track"
|
|
20
|
-
};
|
|
21
|
-
//#endregion
|
|
22
|
-
//#region src/email/brand-plugin.ts
|
|
23
|
-
const SLUG = "[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?";
|
|
24
|
-
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
25
|
-
/** `<specifier>` or `<specifier>/<slug>`, as a regular expression source with no groups. */
|
|
26
|
-
const brandSpecifierPattern = (plugin) => `${escapeRegExp(plugin.specifier)}(?:\\/${SLUG})?`;
|
|
27
|
-
/** The brand a specifier names: `undefined` slug means the default brand; `undefined` when it names none. */
|
|
28
|
-
const parseBrandSpecifier = (specifier, options = {}) => {
|
|
29
|
-
const plugin = options.brandPlugin ?? samvaBrandPlugin;
|
|
30
|
-
const match = new RegExp(`^${escapeRegExp(plugin.specifier)}(?:\\/(${SLUG}))?$`).exec(specifier);
|
|
31
|
-
return match === null ? void 0 : { slug: match[1] };
|
|
32
|
-
};
|
|
33
|
-
/** The attributes only `BrandFooter` writes; an authored element carrying one is refused. */
|
|
34
|
-
const reservedBrandAttributes = (plugin) => /* @__PURE__ */ new Set([
|
|
35
|
-
plugin.footerAttribute,
|
|
36
|
-
plugin.unsubscribeRowAttribute,
|
|
37
|
-
plugin.noTrackAttribute
|
|
38
|
-
]);
|
|
39
|
-
//#endregion
|
|
40
|
-
//#region src/email/elements.ts
|
|
41
|
-
/** Attributes every supported element accepts. `class`/`className`/`style` are handled separately. */
|
|
42
|
-
const EMAIL_GLOBAL_ATTRIBUTES = {
|
|
43
|
-
id: { type: "string" },
|
|
44
|
-
title: { type: "string" },
|
|
45
|
-
lang: { type: "string" },
|
|
46
|
-
dir: {
|
|
47
|
-
type: "string",
|
|
48
|
-
values: [
|
|
49
|
-
"ltr",
|
|
50
|
-
"rtl",
|
|
51
|
-
"auto"
|
|
52
|
-
]
|
|
53
|
-
},
|
|
54
|
-
role: { type: "string" }
|
|
55
|
-
};
|
|
56
|
-
const alignment = {
|
|
57
|
-
type: "string",
|
|
58
|
-
values: [
|
|
59
|
-
"left",
|
|
60
|
-
"center",
|
|
61
|
-
"right"
|
|
62
|
-
]
|
|
63
|
-
};
|
|
64
|
-
const verticalAlignment = {
|
|
65
|
-
type: "string",
|
|
66
|
-
values: [
|
|
67
|
-
"top",
|
|
68
|
-
"middle",
|
|
69
|
-
"bottom"
|
|
70
|
-
]
|
|
71
|
-
};
|
|
72
|
-
const flowAttributes = { align: alignment };
|
|
73
|
-
const cellAttributes = {
|
|
74
|
-
align: alignment,
|
|
75
|
-
valign: verticalAlignment,
|
|
76
|
-
width: { type: "measure" },
|
|
77
|
-
height: { type: "measure" },
|
|
78
|
-
bgcolor: { type: "string" },
|
|
79
|
-
colspan: { type: "number" },
|
|
80
|
-
rowspan: { type: "number" }
|
|
81
|
-
};
|
|
82
|
-
const block = (attributes) => ({
|
|
83
|
-
content: "flow",
|
|
84
|
-
layout: "block",
|
|
85
|
-
attributes
|
|
86
|
-
});
|
|
87
|
-
const inline = () => ({
|
|
88
|
-
content: "flow",
|
|
89
|
-
layout: "inline",
|
|
90
|
-
attributes: {}
|
|
91
|
-
});
|
|
92
|
-
/** The complete supported vocabulary. Anything outside it is a compile error with a source location. */
|
|
93
|
-
const EMAIL_ELEMENTS = {
|
|
94
|
-
html: {
|
|
95
|
-
content: "flow",
|
|
96
|
-
layout: "document",
|
|
97
|
-
attributes: {
|
|
98
|
-
xmlns: { type: "string" },
|
|
99
|
-
"xmlns:o": { type: "string" },
|
|
100
|
-
"xmlns:v": { type: "string" },
|
|
101
|
-
"xmlns:w": { type: "string" }
|
|
102
|
-
},
|
|
103
|
-
childElements: ["head", "body"]
|
|
104
|
-
},
|
|
105
|
-
head: {
|
|
106
|
-
content: "flow",
|
|
107
|
-
layout: "metadata",
|
|
108
|
-
attributes: {},
|
|
109
|
-
childElements: [
|
|
110
|
-
"title",
|
|
111
|
-
"meta",
|
|
112
|
-
"style"
|
|
113
|
-
]
|
|
114
|
-
},
|
|
115
|
-
title: {
|
|
116
|
-
content: "text",
|
|
117
|
-
layout: "metadata",
|
|
118
|
-
attributes: {}
|
|
119
|
-
},
|
|
120
|
-
style: {
|
|
121
|
-
content: "raw",
|
|
122
|
-
layout: "metadata",
|
|
123
|
-
attributes: { type: { type: "string" } }
|
|
124
|
-
},
|
|
125
|
-
meta: {
|
|
126
|
-
content: "void",
|
|
127
|
-
layout: "metadata",
|
|
128
|
-
attributes: {
|
|
129
|
-
name: { type: "string" },
|
|
130
|
-
content: { type: "string" },
|
|
131
|
-
charset: { type: "string" },
|
|
132
|
-
"http-equiv": { type: "string" }
|
|
133
|
-
}
|
|
134
|
-
},
|
|
135
|
-
body: {
|
|
136
|
-
content: "flow",
|
|
137
|
-
layout: "block",
|
|
138
|
-
attributes: { bgcolor: { type: "string" } }
|
|
139
|
-
},
|
|
140
|
-
div: block(flowAttributes),
|
|
141
|
-
section: block(flowAttributes),
|
|
142
|
-
article: block(flowAttributes),
|
|
143
|
-
header: block(flowAttributes),
|
|
144
|
-
footer: block(flowAttributes),
|
|
145
|
-
main: block(flowAttributes),
|
|
146
|
-
center: block({}),
|
|
147
|
-
p: block(flowAttributes),
|
|
148
|
-
blockquote: block(flowAttributes),
|
|
149
|
-
pre: {
|
|
150
|
-
content: "flow",
|
|
151
|
-
layout: "preformatted",
|
|
152
|
-
attributes: {}
|
|
153
|
-
},
|
|
154
|
-
h1: block(flowAttributes),
|
|
155
|
-
h2: block(flowAttributes),
|
|
156
|
-
h3: block(flowAttributes),
|
|
157
|
-
h4: block(flowAttributes),
|
|
158
|
-
h5: block(flowAttributes),
|
|
159
|
-
h6: block(flowAttributes),
|
|
160
|
-
span: inline(),
|
|
161
|
-
strong: inline(),
|
|
162
|
-
b: inline(),
|
|
163
|
-
em: inline(),
|
|
164
|
-
i: inline(),
|
|
165
|
-
u: inline(),
|
|
166
|
-
s: inline(),
|
|
167
|
-
small: inline(),
|
|
168
|
-
code: inline(),
|
|
169
|
-
a: {
|
|
170
|
-
content: "flow",
|
|
171
|
-
layout: "link",
|
|
172
|
-
attributes: {
|
|
173
|
-
href: {
|
|
174
|
-
type: "url",
|
|
175
|
-
required: true
|
|
176
|
-
},
|
|
177
|
-
target: { type: "string" },
|
|
178
|
-
rel: { type: "string" },
|
|
179
|
-
name: { type: "string" }
|
|
180
|
-
}
|
|
181
|
-
},
|
|
182
|
-
img: {
|
|
183
|
-
content: "void",
|
|
184
|
-
layout: "image",
|
|
185
|
-
attributes: {
|
|
186
|
-
src: {
|
|
187
|
-
type: "url",
|
|
188
|
-
required: true
|
|
189
|
-
},
|
|
190
|
-
alt: {
|
|
191
|
-
type: "string",
|
|
192
|
-
required: true
|
|
193
|
-
},
|
|
194
|
-
width: { type: "measure" },
|
|
195
|
-
height: { type: "measure" },
|
|
196
|
-
border: { type: "measure" },
|
|
197
|
-
align: alignment
|
|
198
|
-
}
|
|
199
|
-
},
|
|
200
|
-
table: {
|
|
201
|
-
content: "flow",
|
|
202
|
-
layout: "table",
|
|
203
|
-
attributes: {
|
|
204
|
-
width: { type: "measure" },
|
|
205
|
-
height: { type: "measure" },
|
|
206
|
-
border: { type: "measure" },
|
|
207
|
-
cellpadding: { type: "measure" },
|
|
208
|
-
cellspacing: { type: "measure" },
|
|
209
|
-
align: alignment,
|
|
210
|
-
bgcolor: { type: "string" }
|
|
211
|
-
},
|
|
212
|
-
childElements: [
|
|
213
|
-
"caption",
|
|
214
|
-
"colgroup",
|
|
215
|
-
"thead",
|
|
216
|
-
"tbody",
|
|
217
|
-
"tfoot",
|
|
218
|
-
"tr"
|
|
219
|
-
]
|
|
220
|
-
},
|
|
221
|
-
caption: {
|
|
222
|
-
content: "flow",
|
|
223
|
-
layout: "block",
|
|
224
|
-
attributes: flowAttributes
|
|
225
|
-
},
|
|
226
|
-
colgroup: {
|
|
227
|
-
content: "flow",
|
|
228
|
-
layout: "table-section",
|
|
229
|
-
attributes: {
|
|
230
|
-
span: { type: "number" },
|
|
231
|
-
width: { type: "measure" }
|
|
232
|
-
},
|
|
233
|
-
childElements: ["col"]
|
|
234
|
-
},
|
|
235
|
-
col: {
|
|
236
|
-
content: "void",
|
|
237
|
-
layout: "table-section",
|
|
238
|
-
attributes: {
|
|
239
|
-
span: { type: "number" },
|
|
240
|
-
width: { type: "measure" }
|
|
241
|
-
}
|
|
242
|
-
},
|
|
243
|
-
thead: {
|
|
244
|
-
content: "flow",
|
|
245
|
-
layout: "table-section",
|
|
246
|
-
attributes: {
|
|
247
|
-
align: alignment,
|
|
248
|
-
valign: verticalAlignment,
|
|
249
|
-
bgcolor: { type: "string" }
|
|
250
|
-
},
|
|
251
|
-
childElements: ["tr"]
|
|
252
|
-
},
|
|
253
|
-
tbody: {
|
|
254
|
-
content: "flow",
|
|
255
|
-
layout: "table-section",
|
|
256
|
-
attributes: {
|
|
257
|
-
align: alignment,
|
|
258
|
-
valign: verticalAlignment,
|
|
259
|
-
bgcolor: { type: "string" }
|
|
260
|
-
},
|
|
261
|
-
childElements: ["tr"]
|
|
262
|
-
},
|
|
263
|
-
tfoot: {
|
|
264
|
-
content: "flow",
|
|
265
|
-
layout: "table-section",
|
|
266
|
-
attributes: {
|
|
267
|
-
align: alignment,
|
|
268
|
-
valign: verticalAlignment,
|
|
269
|
-
bgcolor: { type: "string" }
|
|
270
|
-
},
|
|
271
|
-
childElements: ["tr"]
|
|
272
|
-
},
|
|
273
|
-
tr: {
|
|
274
|
-
content: "flow",
|
|
275
|
-
layout: "row",
|
|
276
|
-
attributes: {
|
|
277
|
-
align: alignment,
|
|
278
|
-
valign: verticalAlignment,
|
|
279
|
-
bgcolor: { type: "string" },
|
|
280
|
-
height: { type: "measure" }
|
|
281
|
-
},
|
|
282
|
-
childElements: ["td", "th"]
|
|
283
|
-
},
|
|
284
|
-
td: {
|
|
285
|
-
content: "flow",
|
|
286
|
-
layout: "cell",
|
|
287
|
-
attributes: cellAttributes
|
|
288
|
-
},
|
|
289
|
-
th: {
|
|
290
|
-
content: "flow",
|
|
291
|
-
layout: "cell",
|
|
292
|
-
attributes: cellAttributes
|
|
293
|
-
},
|
|
294
|
-
ul: {
|
|
295
|
-
content: "flow",
|
|
296
|
-
layout: "list",
|
|
297
|
-
attributes: {},
|
|
298
|
-
childElements: ["li"]
|
|
299
|
-
},
|
|
300
|
-
ol: {
|
|
301
|
-
content: "flow",
|
|
302
|
-
layout: "list",
|
|
303
|
-
attributes: {
|
|
304
|
-
start: { type: "number" },
|
|
305
|
-
type: { type: "string" }
|
|
306
|
-
},
|
|
307
|
-
childElements: ["li"]
|
|
308
|
-
},
|
|
309
|
-
li: {
|
|
310
|
-
content: "flow",
|
|
311
|
-
layout: "list-item",
|
|
312
|
-
attributes: {}
|
|
313
|
-
},
|
|
314
|
-
br: {
|
|
315
|
-
content: "void",
|
|
316
|
-
layout: "break",
|
|
317
|
-
attributes: {}
|
|
318
|
-
},
|
|
319
|
-
hr: {
|
|
320
|
-
content: "void",
|
|
321
|
-
layout: "rule",
|
|
322
|
-
attributes: {
|
|
323
|
-
width: { type: "measure" },
|
|
324
|
-
size: { type: "number" },
|
|
325
|
-
align: alignment
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
};
|
|
329
|
-
/** Attributes that must carry a URL; these are checked against the safe-scheme rule. */
|
|
330
|
-
const isUrlAttribute = (tag, attribute) => {
|
|
331
|
-
return EMAIL_ELEMENTS[tag]?.attributes[attribute]?.type === "url";
|
|
332
|
-
};
|
|
333
|
-
/** Look up one supported element, or `undefined` when the tag is outside the vocabulary. */
|
|
334
|
-
const emailElement = (tag) => Object.hasOwn(EMAIL_ELEMENTS, tag) ? EMAIL_ELEMENTS[tag] : void 0;
|
|
335
|
-
//#endregion
|
|
336
|
-
//#region src/sms-segments.ts
|
|
337
|
-
const GSM7_EXTENDED = /* @__PURE__ */ new Set([
|
|
338
|
-
"[",
|
|
339
|
-
"]",
|
|
340
|
-
"{",
|
|
341
|
-
"}",
|
|
342
|
-
"\\",
|
|
343
|
-
"~",
|
|
344
|
-
"|",
|
|
345
|
-
"€",
|
|
346
|
-
"^",
|
|
347
|
-
"\f"
|
|
348
|
-
]);
|
|
349
|
-
const GSM7_BASIC_PATTERN = /^[@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !"#¤%&'()*+,\-./:;<=>?¡0-9ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà]*$/;
|
|
350
|
-
const containsNonGsmCharacters = (text) => {
|
|
351
|
-
for (const char of text) {
|
|
352
|
-
if (GSM7_EXTENDED.has(char)) continue;
|
|
353
|
-
if (!GSM7_BASIC_PATTERN.test(char)) return true;
|
|
354
|
-
}
|
|
355
|
-
return false;
|
|
356
|
-
};
|
|
357
|
-
const smsSegmentInfo = (text) => {
|
|
358
|
-
if (containsNonGsmCharacters(text)) {
|
|
359
|
-
const codePoints = [...text].length;
|
|
360
|
-
const segmentCount = codePoints <= 70 ? 1 : Math.ceil(codePoints / 67);
|
|
361
|
-
return {
|
|
362
|
-
encoding: "ucs2",
|
|
363
|
-
characterCount: text.length,
|
|
364
|
-
segmentCount
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
let encodedLength = 0;
|
|
368
|
-
for (const char of text) encodedLength += GSM7_EXTENDED.has(char) ? 2 : 1;
|
|
369
|
-
const segmentCount = encodedLength <= 160 ? 1 : Math.ceil(encodedLength / 153);
|
|
370
|
-
return {
|
|
371
|
-
encoding: "gsm7",
|
|
372
|
-
characterCount: text.length,
|
|
373
|
-
segmentCount
|
|
374
|
-
};
|
|
375
|
-
};
|
|
376
|
-
//#endregion
|
|
377
|
-
//#region src/render-template.ts
|
|
378
|
-
var RenderError = class extends TaggedError("RenderError") {
|
|
379
|
-
constructor(args) {
|
|
380
|
-
super({
|
|
381
|
-
diagnostics: args.diagnostics,
|
|
382
|
-
message: `Template render rejected: ${args.diagnostics.map((item) => `${item.code}: ${item.message}`).join("; ")}`
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
};
|
|
386
|
-
const fail = (code, message) => {
|
|
387
|
-
throw new RenderError({ diagnostics: [{
|
|
388
|
-
code,
|
|
389
|
-
message
|
|
390
|
-
}] });
|
|
391
|
-
};
|
|
392
|
-
const MAX_ITERATIONS = 1e4;
|
|
393
|
-
const MAX_DEPTH = 64;
|
|
394
|
-
/** Classic Outlook lays a document out correctly only under a transitional doctype. */
|
|
395
|
-
const EMAIL_DOCTYPE = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">";
|
|
396
|
-
const PREHEADER_TARGET_LENGTH = 200;
|
|
397
|
-
const PREHEADER_PADDING = "\xA0";
|
|
398
|
-
const escapeText = (value) => value.replace(/[&<>]/g, (character) => ({
|
|
399
|
-
"&": "&",
|
|
400
|
-
"<": "<",
|
|
401
|
-
">": ">"
|
|
402
|
-
})[character] ?? character);
|
|
403
|
-
const escapeAttribute = (value) => value.replace(/[&<>"']/g, (character) => ({
|
|
404
|
-
"&": "&",
|
|
405
|
-
"<": "<",
|
|
406
|
-
">": ">",
|
|
407
|
-
"\"": """,
|
|
408
|
-
"'": "'"
|
|
409
|
-
})[character] ?? character);
|
|
410
|
-
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
|
|
411
|
-
const SAFE_URL_SCHEME = /^(https:\/\/|mailto:|tel:|cid:|#)/i;
|
|
412
|
-
/** A delivered message can resolve only an absolute URL, so relative references are refused. */
|
|
413
|
-
const isSafeUrl = (value) => !/^[\u0000- ]/.test(value) && !CONTROL_CHARACTERS.test(value) && !value.includes("\\") && SAFE_URL_SCHEME.test(value);
|
|
414
|
-
const CSS_PROPERTY = /^(--[a-zA-Z0-9-]+|[a-zA-Z][a-zA-Z0-9-]*)$/;
|
|
415
|
-
const CSS_UNSAFE = /[;{}<>\\]|expression\s*\(|@import|\/\*/i;
|
|
416
|
-
const CSS_URL = /url\(\s*(['"]?)([^'")]*)\1\s*\)/gi;
|
|
417
|
-
const kebab = (property) => property.startsWith("--") ? property : property.replace(/[A-Z]/g, (l) => `-${l.toLowerCase()}`);
|
|
418
|
-
const UNITLESS = /* @__PURE__ */ new Set([
|
|
419
|
-
"opacity",
|
|
420
|
-
"zIndex",
|
|
421
|
-
"fontWeight",
|
|
422
|
-
"lineHeight",
|
|
423
|
-
"order",
|
|
424
|
-
"flexGrow",
|
|
425
|
-
"flexShrink",
|
|
426
|
-
"columnCount",
|
|
427
|
-
"orphans",
|
|
428
|
-
"widows",
|
|
429
|
-
"zoom",
|
|
430
|
-
"fillOpacity",
|
|
431
|
-
"strokeOpacity"
|
|
432
|
-
]);
|
|
433
|
-
const declarationValue = (property, value) => typeof value === "number" && value !== 0 && !UNITLESS.has(property) && !property.startsWith("--") ? `${value}px` : String(value);
|
|
434
|
-
const report = (ctx, code, message) => {
|
|
435
|
-
ctx.diagnostics.push({
|
|
436
|
-
code,
|
|
437
|
-
message
|
|
438
|
-
});
|
|
439
|
-
};
|
|
440
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
441
|
-
const has = (value, key) => Object.hasOwn(value, key);
|
|
442
|
-
const FORBIDDEN_SEGMENTS = /* @__PURE__ */ new Set([
|
|
443
|
-
"__proto__",
|
|
444
|
-
"constructor",
|
|
445
|
-
"prototype"
|
|
446
|
-
]);
|
|
447
|
-
const ARRAY_INDEX = /^(0|[1-9]\d*)$/;
|
|
448
|
-
const step = (value, segment) => {
|
|
449
|
-
if (FORBIDDEN_SEGMENTS.has(segment)) return void 0;
|
|
450
|
-
if (Array.isArray(value)) return ARRAY_INDEX.test(segment) && Object.hasOwn(value, segment) ? value[Number(segment)] : void 0;
|
|
451
|
-
if (isRecord(value) && Object.hasOwn(value, segment)) return value[segment];
|
|
452
|
-
};
|
|
453
|
-
const resolvePath = (ctx, scope, path) => {
|
|
454
|
-
if (typeof path !== "string" || path === "") return void 0;
|
|
455
|
-
const [first = "", ...rest] = path.split(".");
|
|
456
|
-
if (FORBIDDEN_SEGMENTS.has(first)) return void 0;
|
|
457
|
-
let current = first.startsWith("$") ? scope.get(first) : step(ctx.input, first);
|
|
458
|
-
for (const segment of rest) {
|
|
459
|
-
if (current === void 0 || current === null) return void 0;
|
|
460
|
-
current = step(current, segment);
|
|
461
|
-
}
|
|
462
|
-
return current;
|
|
463
|
-
};
|
|
464
|
-
const invalid = (message) => fail("invalid-value", message);
|
|
465
|
-
/** The text a value contributes: strings as is, finite numbers, nothing for null and booleans. */
|
|
466
|
-
const textOf = (value) => {
|
|
467
|
-
if (typeof value === "string") return value;
|
|
468
|
-
if (typeof value === "number") return Number.isFinite(value) ? String(value) : invalid("A non-finite number cannot be text.");
|
|
469
|
-
if (value === null || value === void 0 || typeof value === "boolean") return "";
|
|
470
|
-
return invalid("An object or array cannot be rendered as text.");
|
|
471
|
-
};
|
|
472
|
-
const finiteNumber = (value, what) => typeof value === "number" && Number.isFinite(value) ? value : invalid(`${what} must be a finite number.`);
|
|
473
|
-
const arithmetic = (op, left, right) => {
|
|
474
|
-
if ((op === "/" || op === "%") && right === 0) return invalid("Division by zero.");
|
|
475
|
-
let result;
|
|
476
|
-
switch (op) {
|
|
477
|
-
case "+":
|
|
478
|
-
result = left + right;
|
|
479
|
-
break;
|
|
480
|
-
case "-":
|
|
481
|
-
result = left - right;
|
|
482
|
-
break;
|
|
483
|
-
case "*":
|
|
484
|
-
result = left * right;
|
|
485
|
-
break;
|
|
486
|
-
case "/":
|
|
487
|
-
result = left / right;
|
|
488
|
-
break;
|
|
489
|
-
case "%":
|
|
490
|
-
result = left % right;
|
|
491
|
-
break;
|
|
492
|
-
default: return invalid(`Unknown arithmetic operator ${JSON.stringify(op)}.`);
|
|
493
|
-
}
|
|
494
|
-
return Number.isFinite(result) ? result : invalid("Arithmetic produced a non-finite number.");
|
|
495
|
-
};
|
|
496
|
-
const formatterContext = (ctx) => {
|
|
497
|
-
ctx.fmt ??= createFmt({
|
|
498
|
-
locale: ctx.locale,
|
|
499
|
-
timeZone: ctx.timeZone
|
|
500
|
-
});
|
|
501
|
-
return ctx.fmt;
|
|
502
|
-
};
|
|
503
|
-
const applyFormat = (ctx, name, args) => {
|
|
504
|
-
if (typeof name !== "string") return invalid("A format node needs a formatter name.");
|
|
505
|
-
try {
|
|
506
|
-
return callFormatter(formatterContext(ctx), name, args);
|
|
507
|
-
} catch (error) {
|
|
508
|
-
return fail("format-error", error instanceof Error ? error.message : String(error));
|
|
509
|
-
}
|
|
510
|
-
};
|
|
511
|
-
const nullish = (value) => value === void 0 ? null : value;
|
|
512
|
-
const compare = (op, left, right) => {
|
|
513
|
-
switch (op) {
|
|
514
|
-
case "eq": return nullish(left) === nullish(right);
|
|
515
|
-
case "ne": return nullish(left) !== nullish(right);
|
|
516
|
-
case "lt":
|
|
517
|
-
case "gt":
|
|
518
|
-
case "le":
|
|
519
|
-
case "ge": {
|
|
520
|
-
if (!(typeof left === "number" && typeof right === "number" || typeof left === "string" && typeof right === "string")) return false;
|
|
521
|
-
const a = left;
|
|
522
|
-
const b = right;
|
|
523
|
-
if (op === "lt") return a < b;
|
|
524
|
-
if (op === "gt") return a > b;
|
|
525
|
-
return op === "le" ? a <= b : a >= b;
|
|
526
|
-
}
|
|
527
|
-
default: return invalid(`Unknown comparison ${JSON.stringify(op)}.`);
|
|
528
|
-
}
|
|
529
|
-
};
|
|
530
|
-
const checkDepth = (depth) => {
|
|
531
|
-
if (depth > MAX_DEPTH) fail("render-limit", `Templates nest at most ${MAX_DEPTH} levels deep.`);
|
|
532
|
-
};
|
|
533
|
-
const evalArgs = (ctx, scope, args, depth) => Array.isArray(args) ? args.map((arg) => evalValue(ctx, scope, arg, depth + 1)) : invalid("Arguments must be an array.");
|
|
534
|
-
const evalValue = (ctx, scope, value, depth) => {
|
|
535
|
-
checkDepth(depth);
|
|
536
|
-
if (value === null || typeof value !== "object") return value;
|
|
537
|
-
if (Array.isArray(value)) return invalid("Unknown value node.");
|
|
538
|
-
const node = value;
|
|
539
|
-
if (has(node, "bind")) return resolvePath(ctx, scope, node.bind);
|
|
540
|
-
if (has(node, "lit")) return node.lit;
|
|
541
|
-
if (has(node, "concat")) return evalArgs(ctx, scope, node.concat, depth).map(textOf).join("");
|
|
542
|
-
if (has(node, "format")) return applyFormat(ctx, node.format, evalArgs(ctx, scope, node.args, depth));
|
|
543
|
-
if (has(node, "if")) return evalPredicate(ctx, scope, node.if, depth + 1) ? evalValue(ctx, scope, node.then, depth + 1) : evalValue(ctx, scope, node.else, depth + 1);
|
|
544
|
-
if (has(node, "op")) {
|
|
545
|
-
const [left, right] = evalArgs(ctx, scope, node.args, depth);
|
|
546
|
-
return arithmetic(node.op, finiteNumber(left, "An arithmetic operand"), finiteNumber(right, "An arithmetic operand"));
|
|
547
|
-
}
|
|
548
|
-
if (has(node, "count")) {
|
|
549
|
-
const items = evalValue(ctx, scope, node.count, depth + 1);
|
|
550
|
-
if (!Array.isArray(items)) return invalid("count applies to an array.");
|
|
551
|
-
return selectItems(ctx, scope, items, String(node.as), node.where, depth).length;
|
|
552
|
-
}
|
|
553
|
-
if (has(node, "length")) {
|
|
554
|
-
const measured = evalValue(ctx, scope, node.length, depth + 1);
|
|
555
|
-
return typeof measured === "string" || Array.isArray(measured) ? measured.length : invalid("length applies to a string or an array.");
|
|
556
|
-
}
|
|
557
|
-
return invalid("Unknown value node.");
|
|
558
|
-
};
|
|
559
|
-
const evalPredicate = (ctx, scope, predicate, depth) => {
|
|
560
|
-
checkDepth(depth);
|
|
561
|
-
if (typeof predicate !== "object" || predicate === null || Array.isArray(predicate)) return invalid("Unknown predicate.");
|
|
562
|
-
const node = predicate;
|
|
563
|
-
if (has(node, "present")) return Boolean(resolvePath(ctx, scope, node.present));
|
|
564
|
-
if (has(node, "truthy")) return Boolean(evalValue(ctx, scope, node.truthy, depth + 1));
|
|
565
|
-
if (has(node, "not")) return !evalPredicate(ctx, scope, node.not, depth + 1);
|
|
566
|
-
if (has(node, "and")) return asArray(node.and).every((item) => evalPredicate(ctx, scope, item, depth + 1));
|
|
567
|
-
if (has(node, "or")) return asArray(node.or).some((item) => evalPredicate(ctx, scope, item, depth + 1));
|
|
568
|
-
if (has(node, "cmp")) {
|
|
569
|
-
const [left, right] = evalArgs(ctx, scope, node.args, depth);
|
|
570
|
-
return compare(node.cmp, left, right);
|
|
571
|
-
}
|
|
572
|
-
return invalid("Unknown predicate.");
|
|
573
|
-
};
|
|
574
|
-
const asArray = (value) => Array.isArray(value) ? value : invalid("Expected an array.");
|
|
575
|
-
const declarations = (ctx, scope, entries, depth) => {
|
|
576
|
-
const parts = [];
|
|
577
|
-
for (const entry of entries) {
|
|
578
|
-
if (!Array.isArray(entry) || entry.length !== 2) {
|
|
579
|
-
report(ctx, "invalid-style", "A style declaration is a [property, value] pair.");
|
|
580
|
-
continue;
|
|
581
|
-
}
|
|
582
|
-
const [property, raw] = entry;
|
|
583
|
-
const value = evalValue(ctx, scope, raw, depth + 1);
|
|
584
|
-
if (value === void 0 || value === null) continue;
|
|
585
|
-
if (typeof property !== "string" || !CSS_PROPERTY.test(property)) {
|
|
586
|
-
report(ctx, "unsupported-style-property", `Style property ${JSON.stringify(property)} is not a CSS property name.`);
|
|
587
|
-
continue;
|
|
588
|
-
}
|
|
589
|
-
if (typeof value !== "string" && !(typeof value === "number" && Number.isFinite(value))) {
|
|
590
|
-
report(ctx, "invalid-style-value", `Style ${property} must be a string or finite number.`);
|
|
591
|
-
continue;
|
|
592
|
-
}
|
|
593
|
-
const text = declarationValue(property, value);
|
|
594
|
-
if (CSS_UNSAFE.test(text) || CONTROL_CHARACTERS.test(text)) {
|
|
595
|
-
report(ctx, "unsafe-style-value", `Style ${property} contains CSS that could break out of the declaration.`);
|
|
596
|
-
continue;
|
|
597
|
-
}
|
|
598
|
-
let unsafeUrl;
|
|
599
|
-
for (const match of text.matchAll(CSS_URL)) if (!isSafeUrl(match[2] ?? "")) unsafeUrl = match[2] ?? "";
|
|
600
|
-
if (unsafeUrl !== void 0) {
|
|
601
|
-
report(ctx, "unsafe-url", `Style ${property} points at ${JSON.stringify(unsafeUrl)}; email URLs must use https:, mailto:, tel:, cid: or a fragment.`);
|
|
602
|
-
continue;
|
|
603
|
-
}
|
|
604
|
-
parts.push(`${kebab(property)}:${text}`);
|
|
605
|
-
}
|
|
606
|
-
return parts.join(";");
|
|
607
|
-
};
|
|
608
|
-
const styleText = (ctx, scope, style, depth) => {
|
|
609
|
-
checkDepth(depth);
|
|
610
|
-
if (typeof style === "string") {
|
|
611
|
-
if (CONTROL_CHARACTERS.test(style)) {
|
|
612
|
-
report(ctx, "unsafe-style-value", "A style contains control characters.");
|
|
613
|
-
return "";
|
|
614
|
-
}
|
|
615
|
-
return style;
|
|
616
|
-
}
|
|
617
|
-
if (Array.isArray(style)) return declarations(ctx, scope, style, depth);
|
|
618
|
-
if (isRecord(style) && has(style, "if")) return evalPredicate(ctx, scope, style.if, depth + 1) ? styleText(ctx, scope, style.then, depth + 1) : styleText(ctx, scope, style.else, depth + 1);
|
|
619
|
-
report(ctx, "invalid-style", "An element style is a string, a declaration list or a conditional.");
|
|
620
|
-
return "";
|
|
621
|
-
};
|
|
622
|
-
const attributeText = (ctx, tag, name, spec, value) => {
|
|
623
|
-
if (value === void 0 || value === null || value === false) return void 0;
|
|
624
|
-
if (value === true) return "";
|
|
625
|
-
if (typeof value !== "string" && !(typeof value === "number" && Number.isFinite(value))) {
|
|
626
|
-
report(ctx, "invalid-attribute-value", `<${tag} ${name}> must be a string or finite number, not ${typeof value}.`);
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
|
-
const text = String(value);
|
|
630
|
-
if (CONTROL_CHARACTERS.test(text)) {
|
|
631
|
-
report(ctx, "invalid-attribute-value", `<${tag} ${name}> contains control characters.`);
|
|
632
|
-
return;
|
|
633
|
-
}
|
|
634
|
-
if (spec.values !== void 0 && !spec.values.includes(text)) {
|
|
635
|
-
report(ctx, "invalid-attribute-value", `<${tag} ${name}> accepts ${spec.values.join(", ")}; received ${JSON.stringify(text)}.`);
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
if (spec.type === "number" && !/^-?\d+$/.test(text)) {
|
|
639
|
-
report(ctx, "invalid-attribute-value", `<${tag} ${name}> must be an integer.`);
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
if (spec.type === "measure" && !/^\d+(\.\d+)?%?$/.test(text)) {
|
|
643
|
-
report(ctx, "invalid-attribute-value", `<${tag} ${name}> must be a pixel count or a percentage.`);
|
|
644
|
-
return;
|
|
645
|
-
}
|
|
646
|
-
if (spec.type === "url" && !isSafeUrl(text)) {
|
|
647
|
-
report(ctx, "unsafe-url", `<${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.`);
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
return text;
|
|
651
|
-
};
|
|
652
|
-
const buildAttributes = (ctx, scope, tag, spec, node, props, depth) => {
|
|
653
|
-
const emitted = [];
|
|
654
|
-
const present = /* @__PURE__ */ new Set();
|
|
655
|
-
let classes;
|
|
656
|
-
const source = node.attrs ?? {};
|
|
657
|
-
if (!isRecord(source)) {
|
|
658
|
-
report(ctx, "invalid-attribute-value", `<${tag}> attrs must be an object.`);
|
|
659
|
-
return "";
|
|
660
|
-
}
|
|
661
|
-
for (const [key, raw] of Object.entries(source)) {
|
|
662
|
-
if (key === "key") continue;
|
|
663
|
-
const name = key === "className" ? "class" : key;
|
|
664
|
-
const value = evalValue(ctx, scope, raw, depth + 1);
|
|
665
|
-
props.set(name, value);
|
|
666
|
-
if (tag === "a" && name === "href" && raw === ctx.brandPlugin.unsubscribeUrlPlaceholder) {
|
|
667
|
-
present.add(name);
|
|
668
|
-
emitted.push(` href="${escapeAttribute(ctx.brandPlugin.unsubscribeUrlPlaceholder)}" ${ctx.brandPlugin.noTrackAttribute}`);
|
|
669
|
-
continue;
|
|
670
|
-
}
|
|
671
|
-
if (name === "class") {
|
|
672
|
-
if (value === void 0 || value === null) continue;
|
|
673
|
-
if (typeof value !== "string") {
|
|
674
|
-
report(ctx, "invalid-attribute-value", `<${tag} class> must be a string.`);
|
|
675
|
-
continue;
|
|
676
|
-
}
|
|
677
|
-
if (CONTROL_CHARACTERS.test(value)) {
|
|
678
|
-
report(ctx, "invalid-attribute-value", `<${tag} class> contains control characters.`);
|
|
679
|
-
continue;
|
|
680
|
-
}
|
|
681
|
-
if (classes !== void 0) {
|
|
682
|
-
report(ctx, "duplicate-attribute", `<${tag}> sets both class and className; keep one.`);
|
|
683
|
-
continue;
|
|
684
|
-
}
|
|
685
|
-
classes = value;
|
|
686
|
-
emitted.push(` class="${escapeAttribute(value)}"`);
|
|
687
|
-
continue;
|
|
688
|
-
}
|
|
689
|
-
if (name === "style") {
|
|
690
|
-
report(ctx, "invalid-style", `<${tag}> writes its style on the element, not as an attribute.`);
|
|
691
|
-
continue;
|
|
692
|
-
}
|
|
693
|
-
if (/^on/i.test(name)) {
|
|
694
|
-
report(ctx, "event-handler", `<${tag} ${name}> is an event handler; email clients strip scripting and Samva refuses it.`);
|
|
695
|
-
continue;
|
|
696
|
-
}
|
|
697
|
-
if (ctx.reservedAttributes.has(name) && node.reserved !== true) {
|
|
698
|
-
report(ctx, "reserved-attribute", `<${tag} ${name}> is reserved for the brand footer.`);
|
|
699
|
-
continue;
|
|
700
|
-
}
|
|
701
|
-
if (/^(data|aria)-[a-z0-9-]+$/.test(name)) {
|
|
702
|
-
const text = attributeText(ctx, tag, name, { type: "string" }, value);
|
|
703
|
-
if (text !== void 0) emitted.push(` ${name}="${escapeAttribute(text)}"`);
|
|
704
|
-
continue;
|
|
705
|
-
}
|
|
706
|
-
const globals = EMAIL_GLOBAL_ATTRIBUTES;
|
|
707
|
-
const attribute = Object.hasOwn(globals, name) ? globals[name] : Object.hasOwn(spec.attributes, name) ? spec.attributes[name] : void 0;
|
|
708
|
-
if (attribute === void 0) {
|
|
709
|
-
report(ctx, "unsupported-attribute", `<${tag}> does not support the ${name} attribute.`);
|
|
710
|
-
continue;
|
|
711
|
-
}
|
|
712
|
-
present.add(name);
|
|
713
|
-
const text = attributeText(ctx, tag, name, attribute, value);
|
|
714
|
-
if (text !== void 0) emitted.push(` ${name}="${escapeAttribute(text)}"`);
|
|
715
|
-
}
|
|
716
|
-
for (const [name, attribute] of Object.entries(spec.attributes)) if (attribute.required === true && !present.has(name)) report(ctx, "missing-attribute", `<${tag}> requires the ${name} attribute.`);
|
|
717
|
-
if (node.style !== void 0) {
|
|
718
|
-
const css = styleText(ctx, scope, node.style, depth + 1);
|
|
719
|
-
if (css !== "") emitted.push(` style="${escapeAttribute(css)}"`);
|
|
720
|
-
}
|
|
721
|
-
return emitted.join("");
|
|
722
|
-
};
|
|
723
|
-
const checkContentModel = (ctx, tag, spec, children) => {
|
|
724
|
-
if (spec.content === "void") {
|
|
725
|
-
if (children.length > 0) report(ctx, "void-element-children", `<${tag}> cannot have children.`);
|
|
726
|
-
return;
|
|
727
|
-
}
|
|
728
|
-
if (spec.content === "text" || spec.content === "raw") {
|
|
729
|
-
for (const child of children) if (child.t !== "text") report(ctx, "invalid-content", `<${tag}> accepts text only.`);
|
|
730
|
-
return;
|
|
731
|
-
}
|
|
732
|
-
if (spec.childElements === void 0) return;
|
|
733
|
-
for (const child of children) {
|
|
734
|
-
if (child.t === "raw") continue;
|
|
735
|
-
if (child.t === "text") {
|
|
736
|
-
if (child.v.trim() !== "") report(ctx, "invalid-content", `<${tag}> cannot contain text directly; put it inside <${spec.childElements[0]}>.`);
|
|
737
|
-
continue;
|
|
738
|
-
}
|
|
739
|
-
if (!spec.childElements.includes(child.tag)) report(ctx, "invalid-content", `<${tag}> accepts only ${spec.childElements.map((name) => `<${name}>`).join(", ")}; found <${child.tag}>.`);
|
|
740
|
-
}
|
|
741
|
-
};
|
|
742
|
-
const pushText = (out, value) => {
|
|
743
|
-
if (value === null || value === void 0 || typeof value === "boolean") return;
|
|
744
|
-
out.push({
|
|
745
|
-
t: "text",
|
|
746
|
-
v: textOf(value)
|
|
747
|
-
});
|
|
748
|
-
};
|
|
749
|
-
const evalElement = (ctx, scope, node, depth, out) => {
|
|
750
|
-
const tag = node.el;
|
|
751
|
-
if (ctx.plain) {
|
|
752
|
-
report(ctx, "unsupported-element", "A plain-text body cannot contain elements.");
|
|
753
|
-
return;
|
|
754
|
-
}
|
|
755
|
-
const spec = typeof tag === "string" ? emailElement(tag) : void 0;
|
|
756
|
-
if (spec === void 0) {
|
|
757
|
-
report(ctx, "unsupported-element", `<${String(tag)}> is not part of the supported email vocabulary.`);
|
|
758
|
-
return;
|
|
759
|
-
}
|
|
760
|
-
const children = [];
|
|
761
|
-
for (const child of node.children ?? []) evalNode(ctx, scope, child, depth + 1, children);
|
|
762
|
-
checkContentModel(ctx, tag, spec, children);
|
|
763
|
-
if (spec.content === "raw") {
|
|
764
|
-
const css = children.map((child) => child.t === "text" ? child.v : "").join("");
|
|
765
|
-
if (/<\/|<!--/i.test(css)) {
|
|
766
|
-
report(ctx, "unsafe-raw-text", `<${tag}> content cannot contain markup.`);
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
const props = /* @__PURE__ */ new Map();
|
|
771
|
-
const attrs = buildAttributes(ctx, scope, tag, spec, node, props, depth);
|
|
772
|
-
out.push({
|
|
773
|
-
t: "el",
|
|
774
|
-
tag,
|
|
775
|
-
content: spec.content,
|
|
776
|
-
layout: spec.layout,
|
|
777
|
-
attrs,
|
|
778
|
-
props,
|
|
779
|
-
children,
|
|
780
|
-
src: node.src
|
|
781
|
-
});
|
|
782
|
-
};
|
|
783
|
-
const evalRaw = (ctx, scope, node, depth, out) => {
|
|
784
|
-
if (ctx.plain) {
|
|
785
|
-
out.push({
|
|
786
|
-
t: "text",
|
|
787
|
-
v: node.text === void 0 ? "" : textOf(evalValue(ctx, scope, node.text, depth + 1))
|
|
788
|
-
});
|
|
789
|
-
return;
|
|
790
|
-
}
|
|
791
|
-
const html = asArray(node.raw).map((part) => rawPart(ctx, scope, part, depth + 1)).join("");
|
|
792
|
-
const text = node.text === void 0 ? "" : textOf(evalValue(ctx, scope, node.text, depth + 1));
|
|
793
|
-
out.push({
|
|
794
|
-
t: "raw",
|
|
795
|
-
html,
|
|
796
|
-
text
|
|
797
|
-
});
|
|
798
|
-
};
|
|
799
|
-
const rawPart = (ctx, scope, part, depth) => {
|
|
800
|
-
if (typeof part === "string") return part;
|
|
801
|
-
const record = part;
|
|
802
|
-
if (isRecord(record) && has(record, "attr")) return escapeAttribute(textOf(evalValue(ctx, scope, record.attr, depth + 1)));
|
|
803
|
-
if (isRecord(record) && has(record, "text")) return escapeText(textOf(evalValue(ctx, scope, record.text, depth + 1)));
|
|
804
|
-
return invalid("A raw part is a string, {attr} or {text}.");
|
|
805
|
-
};
|
|
806
|
-
const loopKey = (name) => name.startsWith("$") ? name : `$${name}`;
|
|
807
|
-
/** The items that satisfy a `where` clause, each bound as the loop variable while it is tested. */
|
|
808
|
-
const selectItems = (ctx, scope, items, as, where, depth) => {
|
|
809
|
-
if (where === void 0) return items;
|
|
810
|
-
const key = loopKey(as);
|
|
811
|
-
return items.filter((item) => {
|
|
812
|
-
if (++ctx.iterations > MAX_ITERATIONS) fail("render-limit", `A render iterates at most ${MAX_ITERATIONS} times.`);
|
|
813
|
-
const inner = new Map(scope);
|
|
814
|
-
inner.set(key, item);
|
|
815
|
-
return evalPredicate(ctx, inner, where, depth + 1);
|
|
816
|
-
});
|
|
817
|
-
};
|
|
818
|
-
const evalEach = (ctx, scope, node, depth, out) => {
|
|
819
|
-
const source = evalValue(ctx, scope, node.each, depth + 1);
|
|
820
|
-
if (!Array.isArray(source)) invalid("each needs an array.");
|
|
821
|
-
const selected = selectItems(ctx, scope, source, String(node.as), node.where, depth);
|
|
822
|
-
let items = selected;
|
|
823
|
-
if (node.chunk !== void 0) {
|
|
824
|
-
if (!Number.isInteger(node.chunk) || node.chunk < 1) invalid("chunk is a positive integer.");
|
|
825
|
-
const slices = [];
|
|
826
|
-
for (let at = 0; at < selected.length; at += node.chunk) slices.push(selected.slice(at, at + node.chunk));
|
|
827
|
-
items = slices;
|
|
828
|
-
}
|
|
829
|
-
const itemKey = loopKey(String(node.as));
|
|
830
|
-
const indexKey = node.index === void 0 ? void 0 : loopKey(String(node.index));
|
|
831
|
-
for (const [index, item] of items.entries()) {
|
|
832
|
-
if (++ctx.iterations > MAX_ITERATIONS) fail("render-limit", `A render iterates at most ${MAX_ITERATIONS} times.`);
|
|
833
|
-
const inner = new Map(scope);
|
|
834
|
-
inner.set(itemKey, item);
|
|
835
|
-
if (indexKey !== void 0) inner.set(indexKey, index);
|
|
836
|
-
for (const child of asArray(node.children)) evalNode(ctx, inner, child, depth + 1, out);
|
|
837
|
-
}
|
|
838
|
-
};
|
|
839
|
-
const evalNode = (ctx, scope, node, depth, out) => {
|
|
840
|
-
checkDepth(depth);
|
|
841
|
-
if (node === null || typeof node !== "object") {
|
|
842
|
-
pushText(out, node);
|
|
843
|
-
return;
|
|
844
|
-
}
|
|
845
|
-
if (!isRecord(node)) {
|
|
846
|
-
invalid("An array cannot be rendered as a node.");
|
|
847
|
-
return;
|
|
848
|
-
}
|
|
849
|
-
if (has(node, "el")) return evalElement(ctx, scope, node, depth, out);
|
|
850
|
-
if (has(node, "each")) return evalEach(ctx, scope, node, depth, out);
|
|
851
|
-
if (has(node, "raw")) return evalRaw(ctx, scope, node, depth, out);
|
|
852
|
-
if (has(node, "preheader")) {
|
|
853
|
-
if (!ctx.markerUsed && ctx.preheader !== void 0 && !ctx.plain) {
|
|
854
|
-
ctx.markerUsed = true;
|
|
855
|
-
out.push(preheaderBlock(ctx, ctx.preheader));
|
|
856
|
-
}
|
|
857
|
-
return;
|
|
858
|
-
}
|
|
859
|
-
if (has(node, "if") && Array.isArray(node.then)) {
|
|
860
|
-
const branch = evalPredicate(ctx, scope, node.if, depth + 1) ? node.then : node.else ?? [];
|
|
861
|
-
for (const child of branch) evalNode(ctx, scope, child, depth + 1, out);
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
864
|
-
pushText(out, evalValue(ctx, scope, node, depth + 1));
|
|
865
|
-
};
|
|
866
|
-
const PREHEADER_STYLE = [
|
|
867
|
-
["display", "none"],
|
|
868
|
-
["overflow", "hidden"],
|
|
869
|
-
["lineHeight", "1px"],
|
|
870
|
-
["opacity", 0],
|
|
871
|
-
["maxHeight", 0],
|
|
872
|
-
["maxWidth", 0]
|
|
873
|
-
];
|
|
874
|
-
/** The hidden inbox-preview block: the text, then padding so the client cannot borrow body copy. */
|
|
875
|
-
const preheaderBlock = (ctx, text) => {
|
|
876
|
-
const children = [text];
|
|
877
|
-
if (text.length < PREHEADER_TARGET_LENGTH) children.push({
|
|
878
|
-
el: "div",
|
|
879
|
-
attrs: { "data-samva-preheader-padding": true },
|
|
880
|
-
children: [PREHEADER_PADDING.repeat(Math.ceil((PREHEADER_TARGET_LENGTH - text.length) / 7))]
|
|
881
|
-
});
|
|
882
|
-
const out = [];
|
|
883
|
-
evalElement(ctx, /* @__PURE__ */ new Map(), {
|
|
884
|
-
el: "div",
|
|
885
|
-
attrs: { "data-samva-preheader": text },
|
|
886
|
-
style: PREHEADER_STYLE,
|
|
887
|
-
children
|
|
888
|
-
}, 0, out);
|
|
889
|
-
return out[0] ?? {
|
|
890
|
-
t: "text",
|
|
891
|
-
v: ""
|
|
892
|
-
};
|
|
893
|
-
};
|
|
894
|
-
/** Put the block first inside `<body>`, or return undefined when there is no body. */
|
|
895
|
-
const insertPreheader = (nodes, block) => {
|
|
896
|
-
for (const [index, node] of nodes.entries()) {
|
|
897
|
-
if (node.t !== "el") continue;
|
|
898
|
-
if (node.tag === "body") return nodes.with(index, {
|
|
899
|
-
...node,
|
|
900
|
-
children: [block, ...node.children]
|
|
901
|
-
});
|
|
902
|
-
const replaced = insertPreheader(node.children, block);
|
|
903
|
-
if (replaced !== void 0) return nodes.with(index, {
|
|
904
|
-
...node,
|
|
905
|
-
children: replaced
|
|
906
|
-
});
|
|
907
|
-
}
|
|
908
|
-
};
|
|
909
|
-
const serialize = (node) => {
|
|
910
|
-
switch (node.t) {
|
|
911
|
-
case "text": return escapeText(node.v);
|
|
912
|
-
case "raw": return node.html;
|
|
913
|
-
case "el":
|
|
914
|
-
if (node.content === "raw") {
|
|
915
|
-
const css = node.children.map((child) => child.t === "text" ? child.v : "").join("");
|
|
916
|
-
return css === "" ? "" : `<${node.tag}${node.attrs}>${css}</${node.tag}>`;
|
|
917
|
-
}
|
|
918
|
-
if (node.content === "void") return `<${node.tag}${node.attrs} />`;
|
|
919
|
-
return `<${node.tag}${node.attrs}>${node.children.map(serialize).join("")}</${node.tag}>`;
|
|
920
|
-
}
|
|
921
|
-
};
|
|
922
|
-
/** Serialize while recording where each element lands, for the same document `serialize` writes. */
|
|
923
|
-
const serializeWithPositions = (nodes, offset) => {
|
|
924
|
-
const positions = [];
|
|
925
|
-
let written = offset;
|
|
926
|
-
const write = (node) => {
|
|
927
|
-
switch (node.t) {
|
|
928
|
-
case "text": {
|
|
929
|
-
const out = escapeText(node.v);
|
|
930
|
-
written += out.length;
|
|
931
|
-
return out;
|
|
932
|
-
}
|
|
933
|
-
case "raw":
|
|
934
|
-
written += node.html.length;
|
|
935
|
-
return node.html;
|
|
936
|
-
case "el": {
|
|
937
|
-
const start = written;
|
|
938
|
-
let out;
|
|
939
|
-
if (node.content === "raw") {
|
|
940
|
-
const css = node.children.map((child) => child.t === "text" ? child.v : "").join("");
|
|
941
|
-
out = css === "" ? "" : `<${node.tag}${node.attrs}>${css}</${node.tag}>`;
|
|
942
|
-
written += out.length;
|
|
943
|
-
} else if (node.content === "void") {
|
|
944
|
-
out = `<${node.tag}${node.attrs} />`;
|
|
945
|
-
written += out.length;
|
|
946
|
-
} else {
|
|
947
|
-
const open = `<${node.tag}${node.attrs}>`;
|
|
948
|
-
written += open.length;
|
|
949
|
-
const inner = node.children.map(write).join("");
|
|
950
|
-
const close = `</${node.tag}>`;
|
|
951
|
-
written += close.length;
|
|
952
|
-
out = `${open}${inner}${close}`;
|
|
953
|
-
}
|
|
954
|
-
positions.push({
|
|
955
|
-
start,
|
|
956
|
-
end: written,
|
|
957
|
-
tag: node.tag,
|
|
958
|
-
...node.src === void 0 ? {} : { src: node.src }
|
|
959
|
-
});
|
|
960
|
-
return out;
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
};
|
|
964
|
-
return {
|
|
965
|
-
html: nodes.map(write).join(""),
|
|
966
|
-
positions
|
|
967
|
-
};
|
|
968
|
-
};
|
|
969
|
-
const PARAGRAPH = "\n\n";
|
|
970
|
-
const LINE = "\n";
|
|
971
|
-
const INVISIBLE = /||||||/g;
|
|
972
|
-
const TABLE_SECTIONS = /* @__PURE__ */ new Set([
|
|
973
|
-
"thead",
|
|
974
|
-
"tbody",
|
|
975
|
-
"tfoot"
|
|
976
|
-
]);
|
|
977
|
-
const isLayoutTable = (props) => props.get("role") === "presentation" || props.get("role") === "none";
|
|
978
|
-
const wrap = (inner, separator) => {
|
|
979
|
-
const trimmed = inner.replace(/^\s+/, "").replace(/\s+$/, "");
|
|
980
|
-
return trimmed === "" ? "" : `${separator}${trimmed}${separator}`;
|
|
981
|
-
};
|
|
982
|
-
const collect = (nodes, keep, descend) => nodes.flatMap((child) => {
|
|
983
|
-
if (child.t !== "el") return [];
|
|
984
|
-
if (keep(child.tag)) return [child];
|
|
985
|
-
return descend(child.tag) ? collect(child.children, keep, descend) : [];
|
|
986
|
-
});
|
|
987
|
-
const childText = (node, context) => node.children.map((child) => derive(child, context)).join("");
|
|
988
|
-
const isDecorativeCell = (cell, context) => {
|
|
989
|
-
const images = collect(cell.children, (tag) => tag === "img", () => true);
|
|
990
|
-
return images.length > 0 && images.every((image) => image.props.get("alt") === "") && childText(cell, context).trim() === "";
|
|
991
|
-
};
|
|
992
|
-
const deriveList = (node, context) => {
|
|
993
|
-
const indent = " ".repeat(context.listDepth);
|
|
994
|
-
const start = Number(node.props.get("start") ?? 1);
|
|
995
|
-
const first = Number.isFinite(start) ? start : 1;
|
|
996
|
-
const nested = {
|
|
997
|
-
...context,
|
|
998
|
-
listDepth: context.listDepth + 1
|
|
999
|
-
};
|
|
1000
|
-
const lines = collect(node.children, (tag) => tag === "li", () => false).map((item, index) => {
|
|
1001
|
-
const marker = node.tag === "ol" ? `${first + index}.` : "-";
|
|
1002
|
-
const continuation = `${indent}${" ".repeat(marker.length + 1)}`;
|
|
1003
|
-
const [head = "", ...rest] = derive(item, nested).replace(/^\s+/, "").replace(/\s+$/, "").replace(/\n{2,}/g, LINE).split("\n");
|
|
1004
|
-
return [`${indent}${marker} ${head}`, ...rest.map((line) => line.trim() === "" ? "" : /^\s/.test(line) ? line : `${continuation}${line}`)].join(LINE);
|
|
1005
|
-
});
|
|
1006
|
-
return wrap(lines.join(LINE), PARAGRAPH);
|
|
1007
|
-
};
|
|
1008
|
-
const deriveTable = (node, context) => {
|
|
1009
|
-
if (isLayoutTable(node.props)) return childText(node, context);
|
|
1010
|
-
const rows = collect(node.children, (tag) => tag === "tr", (tag) => TABLE_SECTIONS.has(tag)).map((row) => collect(row.children, (tag) => tag === "td" || tag === "th", () => false).filter((cell) => !isDecorativeCell(cell, context)).map((cell) => childText(cell, context).replace(/\s+/g, " ").trim()).join(" | "));
|
|
1011
|
-
return wrap(rows.join(LINE), PARAGRAPH);
|
|
1012
|
-
};
|
|
1013
|
-
const deriveElement = (node, context) => {
|
|
1014
|
-
switch (node.layout) {
|
|
1015
|
-
case "metadata": return "";
|
|
1016
|
-
case "document":
|
|
1017
|
-
case "inline":
|
|
1018
|
-
case "list-item": return childText(node, context);
|
|
1019
|
-
case "break": return LINE;
|
|
1020
|
-
case "rule": return `${PARAGRAPH}---${PARAGRAPH}`;
|
|
1021
|
-
case "image": {
|
|
1022
|
-
const alt = node.props.get("alt");
|
|
1023
|
-
const trimmed = typeof alt === "string" ? alt.trim() : "";
|
|
1024
|
-
return trimmed === "" ? "" : `[${trimmed}]`;
|
|
1025
|
-
}
|
|
1026
|
-
case "link": {
|
|
1027
|
-
const label = childText(node, context).replace(/\s+/g, " ").trim();
|
|
1028
|
-
const target = node.props.get("href");
|
|
1029
|
-
const href = typeof target === "string" ? target : "";
|
|
1030
|
-
if (href === "" || href === label) return label;
|
|
1031
|
-
return label === "" ? href : `${label} (${href})`;
|
|
1032
|
-
}
|
|
1033
|
-
case "list": return deriveList(node, context);
|
|
1034
|
-
case "table": return deriveTable(node, context);
|
|
1035
|
-
case "row": return wrap(collect(node.children, (tag) => tag === "td" || tag === "th", () => false).map((cell) => childText(cell, context).replace(/^\s+|\s+$/g, "")).filter((cell) => cell !== "").join(LINE), LINE);
|
|
1036
|
-
case "table-section":
|
|
1037
|
-
case "cell": return wrap(childText(node, context), LINE);
|
|
1038
|
-
case "preformatted": return wrap(childText(node, {
|
|
1039
|
-
...context,
|
|
1040
|
-
preformatted: true
|
|
1041
|
-
}), PARAGRAPH);
|
|
1042
|
-
case "block": return wrap(childText(node, context), PARAGRAPH);
|
|
1043
|
-
}
|
|
1044
|
-
};
|
|
1045
|
-
const derive = (node, context) => {
|
|
1046
|
-
switch (node.t) {
|
|
1047
|
-
case "text": return context.preformatted ? node.v : node.v.replace(INVISIBLE, "").replace(/[^\S\n]+/g, " ");
|
|
1048
|
-
case "raw": return node.text;
|
|
1049
|
-
case "el": return deriveElement(node, context);
|
|
1050
|
-
}
|
|
1051
|
-
};
|
|
1052
|
-
const normalize = (value) => value.replace(/\r\n?/g, "\n").split("\n").map((line) => line.replace(/[^\S\n]+$/, "")).join("\n").replace(/\n{3,}/g, PARAGRAPH).trim();
|
|
1053
|
-
const createContext = (template, input, options, plain) => {
|
|
1054
|
-
if (template.sml !== 1) fail("unsupported-version", `The template was compiled as version ${String(template.sml)}; this renderer reads version 1.`);
|
|
1055
|
-
const brandPlugin = options.brandPlugin ?? samvaBrandPlugin;
|
|
1056
|
-
return {
|
|
1057
|
-
input,
|
|
1058
|
-
locale: options.locale ?? template.locale,
|
|
1059
|
-
timeZone: options.timeZone,
|
|
1060
|
-
plain,
|
|
1061
|
-
brandPlugin,
|
|
1062
|
-
reservedAttributes: reservedBrandAttributes(brandPlugin),
|
|
1063
|
-
diagnostics: [],
|
|
1064
|
-
iterations: 0,
|
|
1065
|
-
fmt: void 0,
|
|
1066
|
-
preheader: void 0,
|
|
1067
|
-
markerUsed: false
|
|
1068
|
-
};
|
|
1069
|
-
};
|
|
1070
|
-
const settle = (ctx) => {
|
|
1071
|
-
if (ctx.diagnostics.length > 0) throw new RenderError({ diagnostics: ctx.diagnostics });
|
|
1072
|
-
};
|
|
1073
|
-
/**
|
|
1074
|
-
* Render the email channel against one validated input. `positions` also returns where each
|
|
1075
|
-
* element landed in the HTML, which compatibility findings and editor selections map back to source.
|
|
1076
|
-
*/
|
|
1077
|
-
const renderEmail = (template, input, options = {}) => {
|
|
1078
|
-
const ctx = createContext(template, input, options, false);
|
|
1079
|
-
const email = template.email;
|
|
1080
|
-
if (email === void 0) return fail("missing-channel", "The template has no email channel.");
|
|
1081
|
-
const empty = /* @__PURE__ */ new Map();
|
|
1082
|
-
const subject = textOf(evalValue(ctx, empty, email.subject, 0));
|
|
1083
|
-
if (/[\r\n\u0000]/.test(subject)) fail("invalid-subject", "Invalid email subject: it must be one line of text.");
|
|
1084
|
-
if (email.preheader !== void 0) {
|
|
1085
|
-
const value = evalValue(ctx, empty, email.preheader, 0);
|
|
1086
|
-
if (value !== void 0 && value !== null) ctx.preheader = textOf(value);
|
|
1087
|
-
}
|
|
1088
|
-
const preheader = ctx.preheader;
|
|
1089
|
-
let tree = [];
|
|
1090
|
-
const nodes = [];
|
|
1091
|
-
evalNode(ctx, empty, email.body, 0, nodes);
|
|
1092
|
-
tree = nodes;
|
|
1093
|
-
if (preheader !== void 0 && !ctx.markerUsed) {
|
|
1094
|
-
const block = preheaderBlock(ctx, preheader);
|
|
1095
|
-
tree = insertPreheader(tree, block) ?? [block, ...tree];
|
|
1096
|
-
}
|
|
1097
|
-
settle(ctx);
|
|
1098
|
-
const text = normalize(tree.map((node) => derive(node, {
|
|
1099
|
-
preformatted: false,
|
|
1100
|
-
listDepth: 0
|
|
1101
|
-
})).join(""));
|
|
1102
|
-
if (options.positions === true) {
|
|
1103
|
-
const prefix = serializeWithPositions(tree, 0).html.startsWith("<html") ? EMAIL_DOCTYPE : "";
|
|
1104
|
-
const shifted = serializeWithPositions(tree, prefix.length);
|
|
1105
|
-
return {
|
|
1106
|
-
subject,
|
|
1107
|
-
preheader,
|
|
1108
|
-
html: `${prefix}${shifted.html}`,
|
|
1109
|
-
text,
|
|
1110
|
-
positions: shifted.positions
|
|
1111
|
-
};
|
|
1112
|
-
}
|
|
1113
|
-
const serialized = tree.map(serialize).join("");
|
|
1114
|
-
return {
|
|
1115
|
-
subject,
|
|
1116
|
-
preheader,
|
|
1117
|
-
html: serialized.startsWith("<html") ? `${EMAIL_DOCTYPE}${serialized}` : serialized,
|
|
1118
|
-
text
|
|
1119
|
-
};
|
|
1120
|
-
};
|
|
1121
|
-
const plainText = (ctx, nodes) => {
|
|
1122
|
-
const out = [];
|
|
1123
|
-
for (const node of asArray(nodes)) evalNode(ctx, /* @__PURE__ */ new Map(), node, 0, out);
|
|
1124
|
-
return out.map((node) => node.t === "text" ? node.v : node.t === "raw" ? node.text : "").join("");
|
|
1125
|
-
};
|
|
1126
|
-
const renderSms = (template, input, options) => {
|
|
1127
|
-
const ctx = createContext(template, input, options, true);
|
|
1128
|
-
const sms = template.sms;
|
|
1129
|
-
if (sms === void 0) return fail("missing-channel", "The template has no SMS channel.");
|
|
1130
|
-
const text = plainText(ctx, sms.body);
|
|
1131
|
-
settle(ctx);
|
|
1132
|
-
return {
|
|
1133
|
-
text,
|
|
1134
|
-
segments: smsSegmentInfo(text)
|
|
1135
|
-
};
|
|
1136
|
-
};
|
|
1137
|
-
const resolveButtons = (ctx, nodes, out, depth) => {
|
|
1138
|
-
checkDepth(depth);
|
|
1139
|
-
const scope = /* @__PURE__ */ new Map();
|
|
1140
|
-
const value = (part) => textOf(evalValue(ctx, scope, part, 0));
|
|
1141
|
-
for (const node of asArray(nodes)) {
|
|
1142
|
-
if ("if" in node) {
|
|
1143
|
-
const branch = evalPredicate(ctx, scope, node.if, depth + 1) ? node.then : node.else ?? [];
|
|
1144
|
-
resolveButtons(ctx, branch, out, depth + 1);
|
|
1145
|
-
continue;
|
|
1146
|
-
}
|
|
1147
|
-
switch (node.type) {
|
|
1148
|
-
case "quick-reply":
|
|
1149
|
-
out.push({
|
|
1150
|
-
type: "quick-reply",
|
|
1151
|
-
text: value(node.text)
|
|
1152
|
-
});
|
|
1153
|
-
break;
|
|
1154
|
-
case "url": {
|
|
1155
|
-
const url = value(node.url);
|
|
1156
|
-
if (!isSafeUrl(url)) invalid("A button URL must be an absolute https URL.");
|
|
1157
|
-
out.push({
|
|
1158
|
-
type: "url",
|
|
1159
|
-
text: value(node.text),
|
|
1160
|
-
url
|
|
1161
|
-
});
|
|
1162
|
-
break;
|
|
1163
|
-
}
|
|
1164
|
-
case "phone":
|
|
1165
|
-
out.push({
|
|
1166
|
-
type: "phone",
|
|
1167
|
-
text: value(node.text),
|
|
1168
|
-
phone: value(node.phone)
|
|
1169
|
-
});
|
|
1170
|
-
break;
|
|
1171
|
-
case "copy-code":
|
|
1172
|
-
out.push({
|
|
1173
|
-
type: "copy-code",
|
|
1174
|
-
code: value(node.code)
|
|
1175
|
-
});
|
|
1176
|
-
break;
|
|
1177
|
-
case "flow":
|
|
1178
|
-
out.push({
|
|
1179
|
-
type: "flow",
|
|
1180
|
-
text: value(node.text),
|
|
1181
|
-
flow: node.flow,
|
|
1182
|
-
action: node.action,
|
|
1183
|
-
...node.screen === void 0 ? {} : { screen: node.screen }
|
|
1184
|
-
});
|
|
1185
|
-
break;
|
|
1186
|
-
default: invalid("Unknown WhatsApp button.");
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
};
|
|
1190
|
-
/** The WhatsApp channel with every text rendered and conditional buttons resolved. */
|
|
1191
|
-
const renderWhatsApp = (template, input, options) => {
|
|
1192
|
-
const ctx = createContext(template, input, options, true);
|
|
1193
|
-
const whatsapp = template.whatsapp;
|
|
1194
|
-
if (whatsapp === void 0) return fail("missing-channel", "The template has no WhatsApp channel.");
|
|
1195
|
-
const header = whatsapp.header === void 0 ? void 0 : {
|
|
1196
|
-
type: whatsapp.header.type,
|
|
1197
|
-
text: plainText(ctx, whatsapp.header.content)
|
|
1198
|
-
};
|
|
1199
|
-
if (header !== void 0 && header.type !== "text" && !isSafeUrl(header.text)) invalid("A media header URL must be an absolute https URL.");
|
|
1200
|
-
const body = plainText(ctx, whatsapp.body);
|
|
1201
|
-
const footer = whatsapp.footer === void 0 ? void 0 : plainText(ctx, whatsapp.footer);
|
|
1202
|
-
const buttons = [];
|
|
1203
|
-
resolveButtons(ctx, whatsapp.buttons ?? [], buttons, 0);
|
|
1204
|
-
settle(ctx);
|
|
1205
|
-
return {
|
|
1206
|
-
language: whatsapp.language,
|
|
1207
|
-
category: whatsapp.category,
|
|
1208
|
-
...header === void 0 ? {} : { header },
|
|
1209
|
-
body,
|
|
1210
|
-
...footer === void 0 ? {} : { footer },
|
|
1211
|
-
buttons
|
|
1212
|
-
};
|
|
1213
|
-
};
|
|
1214
|
-
const CHANNEL_RENDERERS = {
|
|
1215
|
-
email: (template, input, options) => renderEmail(template, input, options),
|
|
1216
|
-
sms: renderSms,
|
|
1217
|
-
whatsapp: renderWhatsApp
|
|
1218
|
-
};
|
|
1219
|
-
/**
|
|
1220
|
-
* Render one channel of a compiled template against one validated input. It throws only
|
|
1221
|
-
* `RenderError`: a template compiled by another version, a channel the template does not declare,
|
|
1222
|
-
* or input or markup the renderer refuses.
|
|
1223
|
-
*/
|
|
1224
|
-
const render = (template, input, options) => CHANNEL_RENDERERS[options.channel](template, input, options);
|
|
1225
|
-
//#endregion
|
|
1226
|
-
export { EMAIL_ELEMENTS as a, isUrlAttribute as c, reservedBrandAttributes as d, samvaBrandPlugin as f, smsSegmentInfo as i, brandSpecifierPattern as l, render as n, EMAIL_GLOBAL_ATTRIBUTES as o, COMPILED_TEMPLATE_VERSION as p, renderEmail as r, emailElement as s, RenderError as t, parseBrandSpecifier as u };
|