@templatical/import-easy-email-pro 0.36.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/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +1330 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1330 @@
|
|
|
1
|
+
import { DEFAULT_TEMPLATE_DEFAULTS, createButtonBlock, createDefaultTemplateContent, createDividerBlock, createHtmlBlock, createImageBlock, createMenuBlock, createParagraphBlock, createSectionBlock, createSocialIconsBlock, createSpacerBlock, createTableBlock, createTitleBlock, createVideoBlock, findOpenTagEnd, generateId } from "@templatical/types";
|
|
2
|
+
//#region src/normalize.ts
|
|
3
|
+
const VAR_PREFIX = "$var(";
|
|
4
|
+
/** Spec §3.3 category map — unknown types skip the category layer. */
|
|
5
|
+
const CATEGORY_BY_TYPE = {
|
|
6
|
+
"standard-paragraph": "TEXT",
|
|
7
|
+
"standard-h1": "TEXT",
|
|
8
|
+
"standard-h2": "TEXT",
|
|
9
|
+
"standard-h3": "TEXT",
|
|
10
|
+
"standard-h4": "TEXT",
|
|
11
|
+
"standard-button": "BUTTON",
|
|
12
|
+
"standard-image": "IMAGE",
|
|
13
|
+
"standard-navbar": "NAVBAR",
|
|
14
|
+
"standard-navbar-link": "NAVBAR",
|
|
15
|
+
"standard-social": "SOCIAL",
|
|
16
|
+
"standard-social-element": "SOCIAL",
|
|
17
|
+
"standard-spacer": "SPACER",
|
|
18
|
+
"standard-divider": "DIVIDER",
|
|
19
|
+
"standard-hero": "HERO",
|
|
20
|
+
"standard-wrapper": "WRAPPER",
|
|
21
|
+
"standard-section": "SECTION",
|
|
22
|
+
"standard-column": "COLUMN",
|
|
23
|
+
"standard-group": "GROUP"
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* `null`, `""`, `"url()"`, `"transparent"`, and leftover `$var(` after
|
|
27
|
+
* substitution are unset. Presence-guards that treat these as set write
|
|
28
|
+
* illegal empties.
|
|
29
|
+
*/
|
|
30
|
+
function isUnset(value) {
|
|
31
|
+
if (value === null || value === void 0) return true;
|
|
32
|
+
if (typeof value !== "string") return false;
|
|
33
|
+
const trimmed = value.trim();
|
|
34
|
+
return trimmed === "" || trimmed === "url()" || trimmed.toLowerCase() === "transparent" || trimmed.includes(VAR_PREFIX);
|
|
35
|
+
}
|
|
36
|
+
/** Index `data.variables[]` by name for `$var` lookup. */
|
|
37
|
+
function buildVariableTable(variables) {
|
|
38
|
+
const out = {};
|
|
39
|
+
if (!variables) return out;
|
|
40
|
+
for (const entry of variables) {
|
|
41
|
+
if (typeof entry.name !== "string" || entry.name === "") continue;
|
|
42
|
+
if (typeof entry.value !== "string") continue;
|
|
43
|
+
out[entry.name] = entry.value;
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Replace `$var(name)` tokens via a linear scan (no regex — CodeQL
|
|
49
|
+
* `js/polynomial-redos`). Unknown names are left in place and flagged.
|
|
50
|
+
*/
|
|
51
|
+
function substituteVars(value, variables) {
|
|
52
|
+
let out = "";
|
|
53
|
+
let i = 0;
|
|
54
|
+
let resolved = 0;
|
|
55
|
+
let unresolved = 0;
|
|
56
|
+
while (i < value.length) {
|
|
57
|
+
const start = value.indexOf(VAR_PREFIX, i);
|
|
58
|
+
if (start === -1) {
|
|
59
|
+
out += value.slice(i);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
out += value.slice(i, start);
|
|
63
|
+
const nameStart = start + 5;
|
|
64
|
+
const nameEnd = findMatchingParen(value, nameStart);
|
|
65
|
+
if (nameEnd === -1) {
|
|
66
|
+
out += value.slice(start);
|
|
67
|
+
unresolved += 1;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
const name = value.slice(nameStart, nameEnd);
|
|
71
|
+
if (Object.prototype.hasOwnProperty.call(variables, name)) {
|
|
72
|
+
out += variables[name];
|
|
73
|
+
resolved += 1;
|
|
74
|
+
} else {
|
|
75
|
+
out += value.slice(start, nameEnd + 1);
|
|
76
|
+
unresolved += 1;
|
|
77
|
+
}
|
|
78
|
+
i = nameEnd + 1;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
value: out,
|
|
82
|
+
resolved,
|
|
83
|
+
unresolved
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** Walk from `openIdx` (char after `(`) to the matching `)`. */
|
|
87
|
+
function findMatchingParen(value, openIdx) {
|
|
88
|
+
let depth = 1;
|
|
89
|
+
for (let i = openIdx; i < value.length; i++) {
|
|
90
|
+
const ch = value[i];
|
|
91
|
+
if (ch === "(") depth += 1;
|
|
92
|
+
else if (ch === ")") {
|
|
93
|
+
depth -= 1;
|
|
94
|
+
if (depth === 0) return i;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return -1;
|
|
98
|
+
}
|
|
99
|
+
function contextFromPage(page) {
|
|
100
|
+
const data = isPlainObject$1(page.data) ? page.data : {};
|
|
101
|
+
return {
|
|
102
|
+
variables: buildVariableTable(Array.isArray(data.variables) ? data.variables : void 0),
|
|
103
|
+
blockAttributes: asNestedAttrMap(data.blockAttributes),
|
|
104
|
+
categoryAttributes: asNestedAttrMap(data.categoryAttributes),
|
|
105
|
+
globalAttributes: asAttrMap(data.globalAttributes),
|
|
106
|
+
page,
|
|
107
|
+
stats: {
|
|
108
|
+
resolved: 0,
|
|
109
|
+
unresolved: 0
|
|
110
|
+
},
|
|
111
|
+
attrCache: /* @__PURE__ */ new WeakMap()
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** Widget `data.input` overrides page variables for `$var` resolution. */
|
|
115
|
+
function withWidgetInput(ctx, input) {
|
|
116
|
+
if (!input) return ctx;
|
|
117
|
+
const variables = { ...ctx.variables };
|
|
118
|
+
for (const [key, value] of Object.entries(input)) if (typeof value === "string") variables[key] = value;
|
|
119
|
+
return {
|
|
120
|
+
...ctx,
|
|
121
|
+
variables,
|
|
122
|
+
attrCache: /* @__PURE__ */ new WeakMap()
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Cascade: element → blockAttributes[type] → categoryAttributes[cat] →
|
|
127
|
+
* globalAttributes. Missing keys fall through. String hits run through
|
|
128
|
+
* `substituteVars`. Keys stay kebab-case MJML.
|
|
129
|
+
*/
|
|
130
|
+
function readAttr(node, key, ctx) {
|
|
131
|
+
let cached = ctx.attrCache.get(node);
|
|
132
|
+
if (cached?.has(key)) return cached.get(key);
|
|
133
|
+
const raw = lookupAttr(node, key, ctx);
|
|
134
|
+
let value = raw;
|
|
135
|
+
if (typeof raw === "string") {
|
|
136
|
+
const result = substituteVars(raw, ctx.variables);
|
|
137
|
+
ctx.stats.resolved += result.resolved;
|
|
138
|
+
ctx.stats.unresolved += result.unresolved;
|
|
139
|
+
value = result.value;
|
|
140
|
+
}
|
|
141
|
+
if (!cached) {
|
|
142
|
+
cached = /* @__PURE__ */ new Map();
|
|
143
|
+
ctx.attrCache.set(node, cached);
|
|
144
|
+
}
|
|
145
|
+
cached.set(key, value);
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
function lookupAttr(node, key, ctx) {
|
|
149
|
+
const fromElement = node.attributes?.[key];
|
|
150
|
+
if (fromElement !== void 0) return fromElement;
|
|
151
|
+
if (typeof node.type === "string") {
|
|
152
|
+
const fromBlock = ctx.blockAttributes[node.type]?.[key];
|
|
153
|
+
if (fromBlock !== void 0) return fromBlock;
|
|
154
|
+
const category = CATEGORY_BY_TYPE[node.type];
|
|
155
|
+
if (category) {
|
|
156
|
+
const fromCategory = ctx.categoryAttributes[category]?.[key];
|
|
157
|
+
if (fromCategory !== void 0) return fromCategory;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return ctx.globalAttributes[key];
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Envelope unwrap. Never throws — the converter owns invalid-shape errors.
|
|
164
|
+
* Returns `{ page, subject }` when a page is visible; caller checks
|
|
165
|
+
* `page.type === "page"`.
|
|
166
|
+
*/
|
|
167
|
+
function unwrapDocument(root) {
|
|
168
|
+
if (!isPlainObject$1(root)) return { page: {} };
|
|
169
|
+
if (root.type === "page") return { page: root };
|
|
170
|
+
const doc = root;
|
|
171
|
+
const page = isPlainObject$1(doc.content) ? doc.content : root;
|
|
172
|
+
const subject = typeof doc.subject === "string" ? doc.subject : void 0;
|
|
173
|
+
return subject === void 0 ? { page } : {
|
|
174
|
+
page,
|
|
175
|
+
subject
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function isPlainObject$1(value) {
|
|
179
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
180
|
+
}
|
|
181
|
+
function asAttrMap(value) {
|
|
182
|
+
return isPlainObject$1(value) ? value : {};
|
|
183
|
+
}
|
|
184
|
+
function asNestedAttrMap(value) {
|
|
185
|
+
if (!isPlainObject$1(value)) return {};
|
|
186
|
+
const out = {};
|
|
187
|
+
for (const [key, entry] of Object.entries(value)) if (isPlainObject$1(entry)) out[key] = entry;
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
//#endregion
|
|
191
|
+
//#region src/attribute-parser.ts
|
|
192
|
+
/** Strip a trailing `px` (case-insensitive) and coerce to number. Unset/NaN → undefined. */
|
|
193
|
+
function parsePx(value) {
|
|
194
|
+
if (isUnset(value)) return void 0;
|
|
195
|
+
let raw = String(value).trim();
|
|
196
|
+
if (raw.toLowerCase().endsWith("px")) raw = raw.slice(0, -2).trimEnd();
|
|
197
|
+
const n = Number(raw);
|
|
198
|
+
return Number.isNaN(n) ? void 0 : n;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Strip a trailing `%` and coerce to number. Unset/NaN → `null` so
|
|
202
|
+
* `matchColumnLayout` can tell "missing width" from `0%`.
|
|
203
|
+
*/
|
|
204
|
+
function parsePercent(value) {
|
|
205
|
+
if (isUnset(value)) return null;
|
|
206
|
+
let raw = String(value).trim();
|
|
207
|
+
if (raw.endsWith("%")) raw = raw.slice(0, -1).trimEnd();
|
|
208
|
+
const n = Number(raw);
|
|
209
|
+
return Number.isNaN(n) ? null : n;
|
|
210
|
+
}
|
|
211
|
+
/** Return the colour string as-is when set. No named-colour expansion. */
|
|
212
|
+
function parseColor(value) {
|
|
213
|
+
if (isUnset(value)) return void 0;
|
|
214
|
+
if (typeof value !== "string") return void 0;
|
|
215
|
+
return value;
|
|
216
|
+
}
|
|
217
|
+
/** Read the four padding sides via a callback (typically wrapping `readAttr`). Missing → 0. */
|
|
218
|
+
function readPadding(read) {
|
|
219
|
+
const side = (key) => parsePx(read(key)) ?? 0;
|
|
220
|
+
return {
|
|
221
|
+
top: side("padding-top"),
|
|
222
|
+
right: side("padding-right"),
|
|
223
|
+
bottom: side("padding-bottom"),
|
|
224
|
+
left: side("padding-left")
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
//#endregion
|
|
228
|
+
//#region src/rich-text.ts
|
|
229
|
+
/** Escape `& < > "` for text content and attribute values. */
|
|
230
|
+
function escapeHtml(value) {
|
|
231
|
+
let out = "";
|
|
232
|
+
for (let i = 0; i < value.length; i++) {
|
|
233
|
+
const ch = value[i];
|
|
234
|
+
if (ch === "&") out += "&";
|
|
235
|
+
else if (ch === "<") out += "<";
|
|
236
|
+
else if (ch === ">") out += ">";
|
|
237
|
+
else if (ch === "\"") out += """;
|
|
238
|
+
else out += ch;
|
|
239
|
+
}
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
function isTextNode(node) {
|
|
243
|
+
return typeof node.text === "string";
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Nesting (outer → inner): link → color/bgColor span → underline → strike →
|
|
247
|
+
* em → strong. Matches the Task 4 brief / design §8.1 mark order.
|
|
248
|
+
*/
|
|
249
|
+
function serialiseTextNode(node) {
|
|
250
|
+
let html = escapeHtml(node.text);
|
|
251
|
+
if (node.bold) html = `<strong>${html}</strong>`;
|
|
252
|
+
if (node.italic) html = `<em>${html}</em>`;
|
|
253
|
+
if (node.strikethrough) html = `<s>${html}</s>`;
|
|
254
|
+
if (node.underline) html = `<u>${html}</u>`;
|
|
255
|
+
const styles = [];
|
|
256
|
+
if (typeof node.color === "string" && node.color !== "") styles.push(`color: ${escapeHtml(node.color)}`);
|
|
257
|
+
if (typeof node.bgColor === "string" && node.bgColor !== "") styles.push(`background-color: ${escapeHtml(node.bgColor)}`);
|
|
258
|
+
if (styles.length > 0) html = `<span style="${styles.join("; ")}">${html}</span>`;
|
|
259
|
+
if (node.link) html = `<a href="${escapeHtml(node.link.href ?? "")}"${node.link.blank === true ? " target=\"_blank\"" : ""}>${html}</a>`;
|
|
260
|
+
return html;
|
|
261
|
+
}
|
|
262
|
+
function serialiseNode(node) {
|
|
263
|
+
if (node.type === "line-break") return "<br>";
|
|
264
|
+
if (node.type === "html-block-node") {
|
|
265
|
+
const element = node;
|
|
266
|
+
const rawTag = element.data?.["tagName"];
|
|
267
|
+
const tagName = typeof rawTag === "string" && rawTag !== "" ? rawTag : "div";
|
|
268
|
+
return `<${tagName}>${serialiseChildren(element.children)}</${tagName}>`;
|
|
269
|
+
}
|
|
270
|
+
if (typeof node.type === "string" && node.type !== "") return "";
|
|
271
|
+
if (isTextNode(node)) return serialiseTextNode(node);
|
|
272
|
+
return "";
|
|
273
|
+
}
|
|
274
|
+
/** Slate children → HTML for TitleBlock / ParagraphBlock content. */
|
|
275
|
+
function serialiseChildren(children) {
|
|
276
|
+
if (!children || children.length === 0) return "";
|
|
277
|
+
let out = "";
|
|
278
|
+
for (const child of children) out += serialiseNode(child);
|
|
279
|
+
return out;
|
|
280
|
+
}
|
|
281
|
+
//#endregion
|
|
282
|
+
//#region src/block-mapper.ts
|
|
283
|
+
const LEAF_TYPES = /* @__PURE__ */ new Set([
|
|
284
|
+
"standard-paragraph",
|
|
285
|
+
"standard-h1",
|
|
286
|
+
"standard-h2",
|
|
287
|
+
"standard-h3",
|
|
288
|
+
"standard-h4",
|
|
289
|
+
"standard-button",
|
|
290
|
+
"standard-image",
|
|
291
|
+
"standard-divider",
|
|
292
|
+
"standard-spacer",
|
|
293
|
+
"standard-navbar",
|
|
294
|
+
"standard-social",
|
|
295
|
+
"standard-table2",
|
|
296
|
+
"marketing-countdown",
|
|
297
|
+
"placeholder",
|
|
298
|
+
"common-video"
|
|
299
|
+
]);
|
|
300
|
+
const SOCIAL_PLATFORMS = [
|
|
301
|
+
"facebook",
|
|
302
|
+
"instagram",
|
|
303
|
+
"twitter",
|
|
304
|
+
"linkedin",
|
|
305
|
+
"youtube",
|
|
306
|
+
"tiktok",
|
|
307
|
+
"pinterest",
|
|
308
|
+
"whatsapp",
|
|
309
|
+
"telegram",
|
|
310
|
+
"github"
|
|
311
|
+
];
|
|
312
|
+
const ENTITIES = {
|
|
313
|
+
"&": "&",
|
|
314
|
+
"<": "<",
|
|
315
|
+
">": ">",
|
|
316
|
+
""": "\"",
|
|
317
|
+
"'": "'",
|
|
318
|
+
"'": "'",
|
|
319
|
+
" ": " "
|
|
320
|
+
};
|
|
321
|
+
const COUNTDOWN_NOTE = "marketing-countdown GIF is the timer; Templatical countdown is Cloud-only";
|
|
322
|
+
function isLeafType(type) {
|
|
323
|
+
if (typeof type !== "string") return false;
|
|
324
|
+
if (LEAF_TYPES.has(type)) return true;
|
|
325
|
+
return headingLevelFromType(type) !== null;
|
|
326
|
+
}
|
|
327
|
+
function convertLeaf(node, map) {
|
|
328
|
+
const type = node.type;
|
|
329
|
+
if (type === "placeholder") return empty$1();
|
|
330
|
+
if (type === "standard-paragraph") return convertParagraph(node, map);
|
|
331
|
+
const heading = typeof type === "string" ? headingLevelFromType(type) : null;
|
|
332
|
+
if (heading) return convertHeading(node, map, heading);
|
|
333
|
+
switch (type) {
|
|
334
|
+
case "standard-button": return convertButton(node, map);
|
|
335
|
+
case "standard-image": return convertImage(node, map);
|
|
336
|
+
case "standard-divider": return convertDivider(node, map);
|
|
337
|
+
case "standard-spacer": return convertSpacer(node, map);
|
|
338
|
+
case "standard-navbar": return convertNavbar(node, map);
|
|
339
|
+
case "standard-social": return convertSocial(node, map);
|
|
340
|
+
case "standard-table2": return convertTable(node, map);
|
|
341
|
+
case "marketing-countdown": return convertCountdown(node, map);
|
|
342
|
+
case "common-video": return convertVideo(node, map);
|
|
343
|
+
default: return convertUnknown(node, map);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function convertParagraph(node, map) {
|
|
347
|
+
return finish(createParagraphBlock({
|
|
348
|
+
content: paragraphHtml(node, map),
|
|
349
|
+
...leafStyles(node, map)
|
|
350
|
+
}), node, report("standard-paragraph", "paragraph", "converted"));
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* ParagraphBlock has no color / fontSize / textAlign. Bake the cascaded
|
|
354
|
+
* values into a wrapping `<p style>` (single wrapper, Unlayer/BeeFree class).
|
|
355
|
+
*/
|
|
356
|
+
function paragraphHtml(node, map) {
|
|
357
|
+
const html = serialiseChildren(node.children);
|
|
358
|
+
const parts = [];
|
|
359
|
+
const color = parseColor(readAttr(node, "color", map.resolve));
|
|
360
|
+
if (color) parts.push(`color: ${escapeAttr(color)}`);
|
|
361
|
+
const fontSize = setString(readAttr(node, "font-size", map.resolve));
|
|
362
|
+
if (fontSize) parts.push(`font-size: ${escapeAttr(fontSize)}`);
|
|
363
|
+
const align = parseAlign(readAttr(node, "align", map.resolve));
|
|
364
|
+
if (align) parts.push(`text-align: ${align}`);
|
|
365
|
+
if (parts.length === 0) return html;
|
|
366
|
+
return `<p style="${parts.join("; ")}">${html}</p>`;
|
|
367
|
+
}
|
|
368
|
+
function escapeAttr(value) {
|
|
369
|
+
let out = "";
|
|
370
|
+
for (let i = 0; i < value.length; i++) {
|
|
371
|
+
const ch = value[i];
|
|
372
|
+
if (ch === "&") out += "&";
|
|
373
|
+
else if (ch === "<") out += "<";
|
|
374
|
+
else if (ch === ">") out += ">";
|
|
375
|
+
else if (ch === "\"") out += """;
|
|
376
|
+
else out += ch;
|
|
377
|
+
}
|
|
378
|
+
return out;
|
|
379
|
+
}
|
|
380
|
+
function convertHeading(node, map, heading) {
|
|
381
|
+
const color = parseColor(readAttr(node, "color", map.resolve));
|
|
382
|
+
const align = parseAlign(readAttr(node, "align", map.resolve));
|
|
383
|
+
const clamped = heading.source > 4;
|
|
384
|
+
return finish(createTitleBlock({
|
|
385
|
+
content: serialiseChildren(node.children),
|
|
386
|
+
level: heading.level,
|
|
387
|
+
...color ? { color } : {},
|
|
388
|
+
...align ? { textAlign: align } : {},
|
|
389
|
+
...leafStyles(node, map)
|
|
390
|
+
}), node, clamped ? report(node.type ?? "standard-h4", "title", "approximated", `Heading level h${heading.source} clamped to 4 — Templatical titles support h1-h4.`) : report(node.type ?? "standard-h1", "title", "converted"));
|
|
391
|
+
}
|
|
392
|
+
function convertButton(node, map) {
|
|
393
|
+
const fill = parseColor(readAttr(node, "background-color", map.resolve));
|
|
394
|
+
const textColor = parseColor(readAttr(node, "color", map.resolve));
|
|
395
|
+
const href = readAttr(node, "href", map.resolve);
|
|
396
|
+
const width = buttonWidth(readAttr(node, "width", map.resolve));
|
|
397
|
+
const align = parseAlign(readAttr(node, "align", map.resolve));
|
|
398
|
+
const outlined = isBorderEnabled(readAttr(node, "border-enabled", map.resolve)) && isUnset(readAttr(node, "background-color", map.resolve));
|
|
399
|
+
const block = createButtonBlock({
|
|
400
|
+
text: stripTags(serialiseChildren(node.children)),
|
|
401
|
+
url: setString(href) ?? "",
|
|
402
|
+
...fill ? { backgroundColor: fill } : {},
|
|
403
|
+
...textColor ? { textColor } : {},
|
|
404
|
+
...width !== void 0 ? { width } : {},
|
|
405
|
+
...align ? { align } : {},
|
|
406
|
+
...leafStyles(node, map)
|
|
407
|
+
});
|
|
408
|
+
if (outlined) block.backgroundColor = "#ffffff";
|
|
409
|
+
return finish(block, node, outlined ? report("standard-button", "button", "approximated", "outlined button has no Templatical equivalent") : report("standard-button", "button", "converted"));
|
|
410
|
+
}
|
|
411
|
+
function convertImage(node, map) {
|
|
412
|
+
const src = setString(readAttr(node, "src", map.resolve)) ?? "";
|
|
413
|
+
const alt = setString(readAttr(node, "alt", map.resolve)) ?? "";
|
|
414
|
+
const linkUrl = setString(readAttr(node, "href", map.resolve));
|
|
415
|
+
const radius = parsePx(readAttr(node, "border-radius", map.resolve));
|
|
416
|
+
const align = parseAlign(readAttr(node, "align", map.resolve));
|
|
417
|
+
const target = setString(readAttr(node, "target", map.resolve));
|
|
418
|
+
return finish(createImageBlock({
|
|
419
|
+
src,
|
|
420
|
+
alt,
|
|
421
|
+
...align ? { align } : {},
|
|
422
|
+
...radius !== void 0 && radius > 0 ? { borderRadius: radius } : {},
|
|
423
|
+
...linkUrl ? { linkUrl } : {},
|
|
424
|
+
...linkUrl && target === "_blank" ? { linkOpenInNewTab: true } : {},
|
|
425
|
+
...leafStyles(node, map)
|
|
426
|
+
}), node, report("standard-image", "image", "converted"));
|
|
427
|
+
}
|
|
428
|
+
function convertDivider(node, map) {
|
|
429
|
+
const color = parseColor(readAttr(node, "border-color", map.resolve));
|
|
430
|
+
const thickness = parsePx(readAttr(node, "border-width", map.resolve));
|
|
431
|
+
const lineStyle = asLineStyle(readAttr(node, "border-style", map.resolve));
|
|
432
|
+
return finish(createDividerBlock({
|
|
433
|
+
...color ? { color } : {},
|
|
434
|
+
...thickness !== void 0 ? { thickness } : {},
|
|
435
|
+
...lineStyle ? { lineStyle } : {},
|
|
436
|
+
...leafStyles(node, map)
|
|
437
|
+
}), node, report("standard-divider", "divider", "converted"));
|
|
438
|
+
}
|
|
439
|
+
function convertSpacer(node, map) {
|
|
440
|
+
const height = parsePx(readAttr(node, "height", map.resolve));
|
|
441
|
+
return finish(createSpacerBlock({
|
|
442
|
+
...height !== void 0 ? { height } : {},
|
|
443
|
+
...leafStyles(node, map)
|
|
444
|
+
}), node, report("standard-spacer", "spacer", "converted"));
|
|
445
|
+
}
|
|
446
|
+
function convertNavbar(node, map) {
|
|
447
|
+
const items = [];
|
|
448
|
+
for (const child of node.children ?? []) {
|
|
449
|
+
if (!isElement$2(child) || child.type !== "standard-navbar-link") continue;
|
|
450
|
+
const href = readAttr(child, "href", map.resolve);
|
|
451
|
+
const target = setString(readAttr(child, "target", map.resolve));
|
|
452
|
+
const color = parseColor(readAttr(child, "color", map.resolve));
|
|
453
|
+
const weight = setString(readAttr(child, "font-weight", map.resolve));
|
|
454
|
+
const deco = setString(readAttr(child, "text-decoration", map.resolve));
|
|
455
|
+
items.push({
|
|
456
|
+
id: generateId(),
|
|
457
|
+
text: stripTags(serialiseChildren(child.children)),
|
|
458
|
+
url: setString(href) ?? "",
|
|
459
|
+
openInNewTab: target === "_blank",
|
|
460
|
+
bold: weight === "bold" || weight === "700",
|
|
461
|
+
underline: deco !== void 0 && deco.includes("underline"),
|
|
462
|
+
...color ? { color } : {}
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
if (items.length === 0) return empty$1();
|
|
466
|
+
const align = parseAlign(readAttr(node, "align", map.resolve));
|
|
467
|
+
return finish(createMenuBlock({
|
|
468
|
+
items,
|
|
469
|
+
...align ? { textAlign: align } : {},
|
|
470
|
+
...leafStyles(node, map)
|
|
471
|
+
}), node, report("standard-navbar", "menu", "converted"));
|
|
472
|
+
}
|
|
473
|
+
function convertSocial(node, map) {
|
|
474
|
+
const icons = [];
|
|
475
|
+
let customSrc = false;
|
|
476
|
+
for (const child of node.children ?? []) {
|
|
477
|
+
if (!isElement$2(child) || child.type !== "standard-social-element") continue;
|
|
478
|
+
const href = readAttr(child, "href", map.resolve);
|
|
479
|
+
const src = readAttr(child, "src", map.resolve);
|
|
480
|
+
if (typeof src === "string" && !isUnset(src)) customSrc = true;
|
|
481
|
+
icons.push({
|
|
482
|
+
id: generateId(),
|
|
483
|
+
platform: inferPlatform(href, src),
|
|
484
|
+
url: setString(href) ?? ""
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
if (icons.length === 0) return empty$1();
|
|
488
|
+
const align = parseAlign(readAttr(node, "align", map.resolve));
|
|
489
|
+
return finish(createSocialIconsBlock({
|
|
490
|
+
icons,
|
|
491
|
+
...align ? { align } : {},
|
|
492
|
+
...leafStyles(node, map)
|
|
493
|
+
}), node, customSrc ? report("standard-social", "social", "approximated", "custom src was present; SocialIconsBlock has no custom PNG") : report("standard-social", "social", "converted"));
|
|
494
|
+
}
|
|
495
|
+
function convertTable(node, map) {
|
|
496
|
+
const rows = [];
|
|
497
|
+
let hasHeaderRow = false;
|
|
498
|
+
let first = true;
|
|
499
|
+
for (const row of collectRows(node)) {
|
|
500
|
+
const cells = [];
|
|
501
|
+
let rowHasTh = false;
|
|
502
|
+
for (const cell of row.children ?? []) {
|
|
503
|
+
if (!isElement$2(cell)) continue;
|
|
504
|
+
if (cell.type !== "td" && cell.type !== "th") continue;
|
|
505
|
+
if (cell.type === "th") rowHasTh = true;
|
|
506
|
+
cells.push({
|
|
507
|
+
id: generateId(),
|
|
508
|
+
content: serialiseChildren(cell.children)
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
if (cells.length === 0) continue;
|
|
512
|
+
if (first) {
|
|
513
|
+
hasHeaderRow = rowHasTh;
|
|
514
|
+
first = false;
|
|
515
|
+
}
|
|
516
|
+
rows.push({
|
|
517
|
+
id: generateId(),
|
|
518
|
+
cells
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
if (rows.length === 0) return empty$1();
|
|
522
|
+
const align = parseAlign(readAttr(node, "align", map.resolve));
|
|
523
|
+
const color = parseColor(readAttr(node, "color", map.resolve));
|
|
524
|
+
return finish(createTableBlock({
|
|
525
|
+
rows,
|
|
526
|
+
hasHeaderRow,
|
|
527
|
+
...align ? { textAlign: align } : {},
|
|
528
|
+
...color ? { color } : {},
|
|
529
|
+
...leafStyles(node, map)
|
|
530
|
+
}), node, report("standard-table2", "table", "converted"));
|
|
531
|
+
}
|
|
532
|
+
function convertCountdown(node, map) {
|
|
533
|
+
const blocks = [];
|
|
534
|
+
const entries = [];
|
|
535
|
+
for (const child of node.children ?? []) {
|
|
536
|
+
if (!isElement$2(child)) continue;
|
|
537
|
+
const remapped = remapOverlay(child);
|
|
538
|
+
if (!remapped) continue;
|
|
539
|
+
const inner = convertLeaf(remapped, map);
|
|
540
|
+
for (const block of inner.blocks) blocks.push(block);
|
|
541
|
+
for (const entry of inner.entries) entries.push({
|
|
542
|
+
sourceTag: "marketing-countdown",
|
|
543
|
+
templaticalBlockType: entry.templaticalBlockType,
|
|
544
|
+
status: "approximated",
|
|
545
|
+
note: entry.note ? `${entry.note} marketing-countdown` : COUNTDOWN_NOTE
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
const src = readAttr(node, "src", map.resolve);
|
|
549
|
+
if (typeof src === "string" && !isUnset(src)) {
|
|
550
|
+
const image = createImageBlock({
|
|
551
|
+
src,
|
|
552
|
+
...leafStyles(node, map)
|
|
553
|
+
});
|
|
554
|
+
const visibility = readVisibility$1(node);
|
|
555
|
+
if (visibility) image.visibility = visibility;
|
|
556
|
+
blocks.push(image);
|
|
557
|
+
entries.push({
|
|
558
|
+
sourceTag: "marketing-countdown",
|
|
559
|
+
templaticalBlockType: "image",
|
|
560
|
+
status: "approximated",
|
|
561
|
+
note: COUNTDOWN_NOTE
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
return {
|
|
565
|
+
blocks,
|
|
566
|
+
entries
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
function convertVideo(node, map) {
|
|
570
|
+
const url = firstUrl(node, map);
|
|
571
|
+
if (!url) return convertUnknown(node, map);
|
|
572
|
+
return finish(createVideoBlock({
|
|
573
|
+
url,
|
|
574
|
+
...leafStyles(node, map)
|
|
575
|
+
}), node, report("common-video", "video", "converted"));
|
|
576
|
+
}
|
|
577
|
+
function convertUnknown(node, map) {
|
|
578
|
+
const sourceTag = node.type ?? "unknown";
|
|
579
|
+
return finish(createHtmlBlock({
|
|
580
|
+
content: JSON.stringify(node),
|
|
581
|
+
...leafStyles(node, map)
|
|
582
|
+
}), node, report(sourceTag, "html", "html-fallback", `Unknown Easy Email Pro type "${sourceTag}"; preserved as HTML.`));
|
|
583
|
+
}
|
|
584
|
+
function remapOverlay(child) {
|
|
585
|
+
if (child.type === "text") return {
|
|
586
|
+
...child,
|
|
587
|
+
type: "standard-paragraph"
|
|
588
|
+
};
|
|
589
|
+
if (child.type === "standard-paragraph") return child;
|
|
590
|
+
if (typeof child.type === "string" && headingLevelFromType(child.type)) return child;
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
function collectRows(node) {
|
|
594
|
+
const rows = [];
|
|
595
|
+
for (const child of node.children ?? []) {
|
|
596
|
+
if (!isElement$2(child)) continue;
|
|
597
|
+
if (child.type === "tr") {
|
|
598
|
+
rows.push(child);
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
for (const inner of child.children ?? []) if (isElement$2(inner) && inner.type === "tr") rows.push(inner);
|
|
602
|
+
}
|
|
603
|
+
return rows;
|
|
604
|
+
}
|
|
605
|
+
function firstUrl(node, map) {
|
|
606
|
+
for (const key of [
|
|
607
|
+
"src",
|
|
608
|
+
"href",
|
|
609
|
+
"url"
|
|
610
|
+
]) {
|
|
611
|
+
const value = readAttr(node, key, map.resolve);
|
|
612
|
+
if (typeof value === "string" && !isUnset(value)) return value;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
function inferPlatform(href, src) {
|
|
616
|
+
const host = hostname(typeof href === "string" ? href : "");
|
|
617
|
+
for (const platform of SOCIAL_PLATFORMS) if (host.includes(platform)) return platform;
|
|
618
|
+
const path = pathOf(typeof src === "string" ? src : "");
|
|
619
|
+
for (const platform of SOCIAL_PLATFORMS) if (path.includes(platform)) return platform;
|
|
620
|
+
return "website";
|
|
621
|
+
}
|
|
622
|
+
function hostname(href) {
|
|
623
|
+
const lower = href.toLowerCase();
|
|
624
|
+
const scheme = lower.indexOf("://");
|
|
625
|
+
const rest = scheme === -1 ? lower : lower.slice(scheme + 3);
|
|
626
|
+
const slash = rest.indexOf("/");
|
|
627
|
+
const hostport = slash === -1 ? rest : rest.slice(0, slash);
|
|
628
|
+
const colon = hostport.indexOf(":");
|
|
629
|
+
return colon === -1 ? hostport : hostport.slice(0, colon);
|
|
630
|
+
}
|
|
631
|
+
function pathOf(src) {
|
|
632
|
+
const lower = src.toLowerCase();
|
|
633
|
+
const scheme = lower.indexOf("://");
|
|
634
|
+
const rest = scheme === -1 ? lower : lower.slice(scheme + 3);
|
|
635
|
+
const slash = rest.indexOf("/");
|
|
636
|
+
return slash === -1 ? rest : rest.slice(slash);
|
|
637
|
+
}
|
|
638
|
+
function headingLevelFromType(type) {
|
|
639
|
+
if (!type.startsWith("standard-h")) return null;
|
|
640
|
+
const raw = type.slice(10);
|
|
641
|
+
if (raw.length === 0) return null;
|
|
642
|
+
for (let i = 0; i < raw.length; i++) {
|
|
643
|
+
const code = raw.charCodeAt(i);
|
|
644
|
+
if (code < 48 || code > 57) return null;
|
|
645
|
+
}
|
|
646
|
+
const source = Number(raw);
|
|
647
|
+
if (!Number.isFinite(source) || source < 1) return null;
|
|
648
|
+
return {
|
|
649
|
+
level: source > 4 ? 4 : source,
|
|
650
|
+
source
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
function leafStyles(node, map) {
|
|
654
|
+
return { styles: { padding: readPadding((key) => readAttr(node, key, map.resolve)) } };
|
|
655
|
+
}
|
|
656
|
+
function finish(block, node, entry) {
|
|
657
|
+
const visibility = readVisibility$1(node);
|
|
658
|
+
if (visibility) block.visibility = visibility;
|
|
659
|
+
return {
|
|
660
|
+
blocks: [block],
|
|
661
|
+
entries: [entry]
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
function readVisibility$1(node) {
|
|
665
|
+
if (node.visible === "desktop") return {
|
|
666
|
+
desktop: true,
|
|
667
|
+
mobile: false
|
|
668
|
+
};
|
|
669
|
+
if (node.visible === "mobile") return {
|
|
670
|
+
desktop: false,
|
|
671
|
+
mobile: true
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
function report(sourceTag, templaticalBlockType, status, note) {
|
|
675
|
+
return {
|
|
676
|
+
sourceTag,
|
|
677
|
+
templaticalBlockType,
|
|
678
|
+
status,
|
|
679
|
+
...note ? { note } : {}
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
function empty$1() {
|
|
683
|
+
return {
|
|
684
|
+
blocks: [],
|
|
685
|
+
entries: []
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
function isElement$2(node) {
|
|
689
|
+
return typeof node.type === "string" && node.type !== "";
|
|
690
|
+
}
|
|
691
|
+
function isBorderEnabled(value) {
|
|
692
|
+
return value === true || value === "true";
|
|
693
|
+
}
|
|
694
|
+
function buttonWidth(value) {
|
|
695
|
+
if (isUnset(value)) return void 0;
|
|
696
|
+
if (typeof value === "string" && value.trim() === "100%") return "full";
|
|
697
|
+
return parsePx(value);
|
|
698
|
+
}
|
|
699
|
+
function parseAlign(value) {
|
|
700
|
+
if (typeof value !== "string") return void 0;
|
|
701
|
+
const align = value.trim().toLowerCase();
|
|
702
|
+
if (align === "left" || align === "center" || align === "right") return align;
|
|
703
|
+
}
|
|
704
|
+
function asLineStyle(value) {
|
|
705
|
+
if (typeof value !== "string") return void 0;
|
|
706
|
+
const style = value.trim().toLowerCase();
|
|
707
|
+
if (style === "solid" || style === "dashed" || style === "dotted") return style;
|
|
708
|
+
}
|
|
709
|
+
function setString(value) {
|
|
710
|
+
if (typeof value !== "string" || isUnset(value)) return void 0;
|
|
711
|
+
return value;
|
|
712
|
+
}
|
|
713
|
+
/** Button labels are plain text — strip tags the serialiser may have emitted. */
|
|
714
|
+
function stripTags(html) {
|
|
715
|
+
if (html === "") return "";
|
|
716
|
+
return decodeEntities(removeTags(html)).trim();
|
|
717
|
+
}
|
|
718
|
+
function removeTags(html) {
|
|
719
|
+
let out = "";
|
|
720
|
+
let i = 0;
|
|
721
|
+
while (i < html.length) {
|
|
722
|
+
const open = html.indexOf("<", i);
|
|
723
|
+
if (open === -1) {
|
|
724
|
+
out += html.slice(i);
|
|
725
|
+
break;
|
|
726
|
+
}
|
|
727
|
+
out += html.slice(i, open);
|
|
728
|
+
const tagEnd = findOpenTagEnd(html, open + 1);
|
|
729
|
+
if (tagEnd === -1) {
|
|
730
|
+
out += html.slice(open);
|
|
731
|
+
break;
|
|
732
|
+
}
|
|
733
|
+
i = tagEnd + 1;
|
|
734
|
+
}
|
|
735
|
+
return out;
|
|
736
|
+
}
|
|
737
|
+
function decodeEntities(text) {
|
|
738
|
+
let out = "";
|
|
739
|
+
let i = 0;
|
|
740
|
+
while (i < text.length) {
|
|
741
|
+
if (text[i] === "&") {
|
|
742
|
+
const semi = text.indexOf(";", i + 1);
|
|
743
|
+
if (semi !== -1 && semi - i <= 6) {
|
|
744
|
+
const mapped = ENTITIES[text.slice(i, semi + 1)];
|
|
745
|
+
if (mapped !== void 0) {
|
|
746
|
+
out += mapped;
|
|
747
|
+
i = semi + 1;
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
out += text[i];
|
|
753
|
+
i++;
|
|
754
|
+
}
|
|
755
|
+
return out;
|
|
756
|
+
}
|
|
757
|
+
//#endregion
|
|
758
|
+
//#region src/column-layout.ts
|
|
759
|
+
const LAYOUT_SHAPES = [
|
|
760
|
+
{
|
|
761
|
+
layout: "1",
|
|
762
|
+
percents: [100]
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
layout: "2",
|
|
766
|
+
percents: [50, 50]
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
layout: "1-2",
|
|
770
|
+
percents: [33.33, 66.67]
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
layout: "2-1",
|
|
774
|
+
percents: [66.67, 33.33]
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
layout: "3",
|
|
778
|
+
percents: [
|
|
779
|
+
33.33,
|
|
780
|
+
33.33,
|
|
781
|
+
33.34
|
|
782
|
+
]
|
|
783
|
+
}
|
|
784
|
+
];
|
|
785
|
+
const WIDTH_TOLERANCE = 2;
|
|
786
|
+
const COLUMN_COUNT = {
|
|
787
|
+
"1": 1,
|
|
788
|
+
"2": 2,
|
|
789
|
+
"3": 3,
|
|
790
|
+
"2-1": 2,
|
|
791
|
+
"1-2": 2
|
|
792
|
+
};
|
|
793
|
+
/**
|
|
794
|
+
* Resolve column widths to one of the five layouts `ColumnLayout` allows.
|
|
795
|
+
*
|
|
796
|
+
* `exact: false` means the caller reports `approximated` — Chamaileon permits
|
|
797
|
+
* any column count at any width and this union permits five shapes, so this is
|
|
798
|
+
* the importer's main irreducible loss.
|
|
799
|
+
*/
|
|
800
|
+
function matchColumnLayout(percents) {
|
|
801
|
+
const count = percents.length;
|
|
802
|
+
if (count === 0) return {
|
|
803
|
+
layout: "1",
|
|
804
|
+
exact: true
|
|
805
|
+
};
|
|
806
|
+
if (percents.every((p) => p === null)) {
|
|
807
|
+
if (count === 1) return {
|
|
808
|
+
layout: "1",
|
|
809
|
+
exact: true
|
|
810
|
+
};
|
|
811
|
+
if (count === 2) return {
|
|
812
|
+
layout: "2",
|
|
813
|
+
exact: true
|
|
814
|
+
};
|
|
815
|
+
if (count === 3) return {
|
|
816
|
+
layout: "3",
|
|
817
|
+
exact: true
|
|
818
|
+
};
|
|
819
|
+
return {
|
|
820
|
+
layout: "3",
|
|
821
|
+
exact: false
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
const known = percents.filter((p) => p !== null);
|
|
825
|
+
const unsetCount = count - known.length;
|
|
826
|
+
const remainder = unsetCount === 0 ? 0 : Math.max(0, 100 - known.reduce((a, b) => a + b, 0)) / unsetCount;
|
|
827
|
+
const resolved = percents.map((p) => p ?? remainder);
|
|
828
|
+
const sameCount = LAYOUT_SHAPES.filter((s) => s.percents.length === count);
|
|
829
|
+
for (const shape of sameCount) if (shape.percents.every((want, i) => Math.abs(want - resolved[i]) <= WIDTH_TOLERANCE)) return {
|
|
830
|
+
layout: shape.layout,
|
|
831
|
+
exact: true
|
|
832
|
+
};
|
|
833
|
+
const candidates = sameCount.length > 0 ? sameCount : LAYOUT_SHAPES.filter((s) => s.layout === "3");
|
|
834
|
+
let best = candidates[0];
|
|
835
|
+
let bestError = Infinity;
|
|
836
|
+
for (const shape of candidates) {
|
|
837
|
+
const error = shape.percents.reduce((sum, want, i) => sum + Math.abs(want - (resolved[i] ?? 0)), 0);
|
|
838
|
+
if (error < bestError) {
|
|
839
|
+
bestError = error;
|
|
840
|
+
best = shape;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return {
|
|
844
|
+
layout: best.layout,
|
|
845
|
+
exact: false
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
//#endregion
|
|
849
|
+
//#region src/section-builder.ts
|
|
850
|
+
function buildTopLevel(node, map) {
|
|
851
|
+
switch (node.type) {
|
|
852
|
+
case "standard-wrapper": return buildWrapper(node, map);
|
|
853
|
+
case "standard-hero": return buildHero(node, map);
|
|
854
|
+
case "standard-section": return buildSection(node, map);
|
|
855
|
+
default: return empty();
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
function buildSection(node, map) {
|
|
859
|
+
const plan = collectSlots(node);
|
|
860
|
+
const leftover = convertFlow(plan.leftovers, map);
|
|
861
|
+
const rawWidths = plan.columns.map((column) => readAttr(column, "width", map.resolve));
|
|
862
|
+
const { layout, exact } = matchColumnLayout(rawWidths.map((width) => parsePercent(width)));
|
|
863
|
+
const slots = COLUMN_COUNT[layout];
|
|
864
|
+
const children = Array.from({ length: slots }, () => []);
|
|
865
|
+
if (leftover.blocks.length > 0) children[0].push(...leftover.blocks);
|
|
866
|
+
const entries = leftover.entries;
|
|
867
|
+
plan.columns.forEach((column, index) => {
|
|
868
|
+
const slot = Math.min(index, slots - 1);
|
|
869
|
+
const inner = convertFlow(column.children ?? [], map);
|
|
870
|
+
children[slot].push(...inner.blocks);
|
|
871
|
+
for (const entry of inner.entries) entries.push(entry);
|
|
872
|
+
});
|
|
873
|
+
const reasons = [];
|
|
874
|
+
if (plan.mixed) reasons.push("Section mixed columns and groups; flattened into the column flow.");
|
|
875
|
+
if (!exact) {
|
|
876
|
+
if (plan.columns.length > slots) reasons.push(`${plan.columns.length}-column layout has no exact Templatical equivalent; folded onto "${layout}".`);
|
|
877
|
+
else {
|
|
878
|
+
const shown = rawWidths.map((width) => typeof width === "string" && !isUnset(width) ? width : "auto").join(", ");
|
|
879
|
+
reasons.push(`Column widths ${shown} have no exact Templatical layout; resolved to "${layout}".`);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
const section = createSectionBlock();
|
|
883
|
+
section.columns = layout;
|
|
884
|
+
section.children = children;
|
|
885
|
+
paintSection(section, node, map, { stackOnMobile: plan.grouped ? false : void 0 });
|
|
886
|
+
entries.unshift(sectionEntry(node, reasons.length > 0 ? "approximated" : "converted", reasons.length > 0 ? reasons.join(" ") : void 0));
|
|
887
|
+
return {
|
|
888
|
+
blocks: [section],
|
|
889
|
+
entries
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
function buildHero(node, map) {
|
|
893
|
+
const slot = [];
|
|
894
|
+
const entries = [];
|
|
895
|
+
const url = readAttr(node, "background-url", map.resolve);
|
|
896
|
+
if (typeof url === "string" && !isUnset(url)) {
|
|
897
|
+
slot.push(createImageBlock({ src: url }));
|
|
898
|
+
entries.push({
|
|
899
|
+
sourceTag: "standard-hero",
|
|
900
|
+
templaticalBlockType: "image",
|
|
901
|
+
status: "approximated",
|
|
902
|
+
note: "hero background-url stacked as a leading image (overlay is the loss)"
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
const inner = convertFlow(node.children ?? [], map);
|
|
906
|
+
slot.push(...inner.blocks);
|
|
907
|
+
for (const entry of inner.entries) entries.push(entry);
|
|
908
|
+
const section = createSectionBlock();
|
|
909
|
+
section.columns = "1";
|
|
910
|
+
section.children = [slot];
|
|
911
|
+
paintSection(section, node, map);
|
|
912
|
+
entries.unshift({
|
|
913
|
+
sourceTag: "standard-hero",
|
|
914
|
+
templaticalBlockType: "section",
|
|
915
|
+
status: "approximated",
|
|
916
|
+
note: "standard-hero overlay is stacked as a 1-column section; background-url becomes a leading image."
|
|
917
|
+
});
|
|
918
|
+
return {
|
|
919
|
+
blocks: [section],
|
|
920
|
+
entries
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
function buildWrapper(node, map) {
|
|
924
|
+
const blocks = [];
|
|
925
|
+
const entries = [];
|
|
926
|
+
for (const child of elements(node)) {
|
|
927
|
+
if (child.type === "placeholder") continue;
|
|
928
|
+
const inner = buildTopLevel(child, map);
|
|
929
|
+
for (const block of inner.blocks) blocks.push(block);
|
|
930
|
+
for (const entry of inner.entries) entries.push(entry);
|
|
931
|
+
}
|
|
932
|
+
const sections = blocks.filter((block) => block.type === "section");
|
|
933
|
+
if (sections.length === 0) return {
|
|
934
|
+
blocks: [],
|
|
935
|
+
entries: [{
|
|
936
|
+
sourceTag: "standard-wrapper",
|
|
937
|
+
templaticalBlockType: null,
|
|
938
|
+
status: "skipped",
|
|
939
|
+
note: "An empty standard-wrapper produces nothing."
|
|
940
|
+
}]
|
|
941
|
+
};
|
|
942
|
+
const wrapper = readWrapper(node, map);
|
|
943
|
+
for (const section of sections) section.wrapper = { ...wrapper };
|
|
944
|
+
if (sections.length > 1) {
|
|
945
|
+
const note = `standard-wrapper holding ${sections.length} sections was applied to each of them — Templatical has no multi-section band.`;
|
|
946
|
+
for (const entry of entries) {
|
|
947
|
+
if (entry.templaticalBlockType !== "section") continue;
|
|
948
|
+
if (entry.status !== "converted") continue;
|
|
949
|
+
entry.status = "approximated";
|
|
950
|
+
entry.note = note;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return {
|
|
954
|
+
blocks,
|
|
955
|
+
entries
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Direct columns become slots. A sole `standard-group` of columns becomes
|
|
960
|
+
* those slots with `stackOnMobile: false`. Mixed group + columns flatten
|
|
961
|
+
* into the slot list and are approximated.
|
|
962
|
+
*/
|
|
963
|
+
function collectSlots(section) {
|
|
964
|
+
const kids = structuralChildren(section);
|
|
965
|
+
if (kids.every((kid) => kid.type === "standard-column")) return {
|
|
966
|
+
columns: kids,
|
|
967
|
+
leftovers: [],
|
|
968
|
+
grouped: false,
|
|
969
|
+
mixed: false
|
|
970
|
+
};
|
|
971
|
+
if (kids.length === 1 && kids[0].type === "standard-group") {
|
|
972
|
+
const groupKids = structuralChildren(kids[0]);
|
|
973
|
+
if (groupKids.every((kid) => kid.type === "standard-column")) return {
|
|
974
|
+
columns: groupKids,
|
|
975
|
+
leftovers: [],
|
|
976
|
+
grouped: true,
|
|
977
|
+
mixed: false
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
const columns = [];
|
|
981
|
+
const leftovers = [];
|
|
982
|
+
let grouped = false;
|
|
983
|
+
for (const kid of kids) {
|
|
984
|
+
if (kid.type === "standard-column") {
|
|
985
|
+
columns.push(kid);
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
if (kid.type === "standard-group") {
|
|
989
|
+
grouped = true;
|
|
990
|
+
for (const inner of structuralChildren(kid)) if (inner.type === "standard-column") columns.push(inner);
|
|
991
|
+
else leftovers.push(inner);
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
leftovers.push(kid);
|
|
995
|
+
}
|
|
996
|
+
return {
|
|
997
|
+
columns,
|
|
998
|
+
leftovers,
|
|
999
|
+
grouped,
|
|
1000
|
+
mixed: true
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
function convertFlow(nodes, map) {
|
|
1004
|
+
const blocks = [];
|
|
1005
|
+
const entries = [];
|
|
1006
|
+
for (const child of nodes ?? []) {
|
|
1007
|
+
if (!isElement$1(child)) continue;
|
|
1008
|
+
if (child.type === "placeholder") continue;
|
|
1009
|
+
if (child.type === "standard-group") {
|
|
1010
|
+
const innerColumns = structuralChildren(child).filter((node) => node.type === "standard-column");
|
|
1011
|
+
entries.push({
|
|
1012
|
+
sourceTag: "standard-group",
|
|
1013
|
+
templaticalBlockType: null,
|
|
1014
|
+
status: "approximated",
|
|
1015
|
+
note: `nested group flattened (${innerColumns.length} columns)`
|
|
1016
|
+
});
|
|
1017
|
+
for (const column of innerColumns) {
|
|
1018
|
+
const inner = convertFlow(column.children, map);
|
|
1019
|
+
for (const block of inner.blocks) blocks.push(block);
|
|
1020
|
+
for (const entry of inner.entries) entries.push(entry);
|
|
1021
|
+
}
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
if (child.type === "standard-column") {
|
|
1025
|
+
const inner = convertFlow(child.children, map);
|
|
1026
|
+
for (const block of inner.blocks) blocks.push(block);
|
|
1027
|
+
for (const entry of inner.entries) entries.push(entry);
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
const converted = convertLeaf(child, map);
|
|
1031
|
+
for (const block of converted.blocks) blocks.push(block);
|
|
1032
|
+
for (const entry of converted.entries) entries.push(entry);
|
|
1033
|
+
}
|
|
1034
|
+
return {
|
|
1035
|
+
blocks,
|
|
1036
|
+
entries
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
/** `createSectionBlock()` first, then mutate fill — never a partial `styles`. */
|
|
1040
|
+
function paintSection(section, node, map, extras) {
|
|
1041
|
+
section.styles.padding = readPadding((key) => readAttr(node, key, map.resolve));
|
|
1042
|
+
const fill = sectionFill(node, map);
|
|
1043
|
+
if (fill) section.styles.backgroundColor = fill;
|
|
1044
|
+
const radius = parsePx(readAttr(node, "border-radius", map.resolve));
|
|
1045
|
+
if (radius !== void 0 && radius > 0) section.borderRadius = radius;
|
|
1046
|
+
if (extras?.stackOnMobile === false) section.stackOnMobile = false;
|
|
1047
|
+
const visibility = readVisibility(node);
|
|
1048
|
+
if (visibility) section.visibility = visibility;
|
|
1049
|
+
}
|
|
1050
|
+
function sectionFill(node, map) {
|
|
1051
|
+
const own = parseColor(readAttr(node, "background-color", map.resolve));
|
|
1052
|
+
if (own) return own;
|
|
1053
|
+
return parseColor(readAttr(map.resolve.page, "content-background-color", map.resolve));
|
|
1054
|
+
}
|
|
1055
|
+
function readWrapper(node, map) {
|
|
1056
|
+
const backgroundColor = parseColor(readAttr(node, "background-color", map.resolve));
|
|
1057
|
+
const padding = readPadding((key) => readAttr(node, key, map.resolve));
|
|
1058
|
+
const radius = parsePx(readAttr(node, "border-radius", map.resolve));
|
|
1059
|
+
return {
|
|
1060
|
+
...backgroundColor ? { backgroundColor } : {},
|
|
1061
|
+
padding,
|
|
1062
|
+
...radius !== void 0 && radius > 0 ? { borderRadius: radius } : {}
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
function sectionEntry(node, status, note) {
|
|
1066
|
+
return {
|
|
1067
|
+
sourceTag: node.type ?? "standard-section",
|
|
1068
|
+
templaticalBlockType: "section",
|
|
1069
|
+
status,
|
|
1070
|
+
...note ? { note } : {}
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
function readVisibility(node) {
|
|
1074
|
+
if (node.visible === "desktop") return {
|
|
1075
|
+
desktop: true,
|
|
1076
|
+
mobile: false
|
|
1077
|
+
};
|
|
1078
|
+
if (node.visible === "mobile") return {
|
|
1079
|
+
desktop: false,
|
|
1080
|
+
mobile: true
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
function structuralChildren(node) {
|
|
1084
|
+
return elements(node).filter((child) => child.type !== "placeholder");
|
|
1085
|
+
}
|
|
1086
|
+
function elements(node) {
|
|
1087
|
+
const out = [];
|
|
1088
|
+
for (const child of node.children ?? []) if (isElement$1(child)) out.push(child);
|
|
1089
|
+
return out;
|
|
1090
|
+
}
|
|
1091
|
+
function isElement$1(node) {
|
|
1092
|
+
return typeof node.type === "string" && node.type !== "";
|
|
1093
|
+
}
|
|
1094
|
+
function empty() {
|
|
1095
|
+
return {
|
|
1096
|
+
blocks: [],
|
|
1097
|
+
entries: []
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
//#endregion
|
|
1101
|
+
//#region src/converter.ts
|
|
1102
|
+
const INVALID_MESSAGE = "Invalid Easy Email Pro template: expected a page JSON object (EmailTemplate { subject, content } or the page element).";
|
|
1103
|
+
const OSS_MESSAGE = "Invalid Easy Email Pro template: this looks like open-source Easy Email JSON, not Easy Email Pro (expected standard-* children).";
|
|
1104
|
+
const EMPTY_WARNING = "No convertible content was found in the Easy Email Pro page. Check that page.children holds at least one standard-section.";
|
|
1105
|
+
const MOBILE_WARNING = "mobileAttributes were dropped; Templatical has no per-viewport padding.";
|
|
1106
|
+
function varWarning(resolved, unresolved) {
|
|
1107
|
+
return `Resolved ${resolved} $var() values, ${unresolved} left unresolved.`;
|
|
1108
|
+
}
|
|
1109
|
+
const OSS_CHILD_TYPES = /* @__PURE__ */ new Set([
|
|
1110
|
+
"section",
|
|
1111
|
+
"column",
|
|
1112
|
+
"text",
|
|
1113
|
+
"button",
|
|
1114
|
+
"image",
|
|
1115
|
+
"wrapper",
|
|
1116
|
+
"hero",
|
|
1117
|
+
"navbar",
|
|
1118
|
+
"social"
|
|
1119
|
+
]);
|
|
1120
|
+
const STRUCTURE_TYPES = /* @__PURE__ */ new Set([
|
|
1121
|
+
"standard-section",
|
|
1122
|
+
"standard-wrapper",
|
|
1123
|
+
"standard-hero"
|
|
1124
|
+
]);
|
|
1125
|
+
const WIDGET_TYPES = /* @__PURE__ */ new Set(["section_widget", "wrapper_widget"]);
|
|
1126
|
+
/**
|
|
1127
|
+
* `DEFAULT_TEMPLATE_DEFAULTS` is typed `Partial<TemplateSettings>` so a
|
|
1128
|
+
* consumer can override any subset, but its own literal always sets the six
|
|
1129
|
+
* required fields. Narrowing once here is what lets each fallback come out as
|
|
1130
|
+
* `number`/`string`/`boolean` rather than `| undefined`.
|
|
1131
|
+
*/
|
|
1132
|
+
const REQUIRED_DEFAULTS = DEFAULT_TEMPLATE_DEFAULTS;
|
|
1133
|
+
/**
|
|
1134
|
+
* Convert an Easy Email Pro page (or EmailTemplate envelope) into a
|
|
1135
|
+
* Templatical template. Accepts a bare `page` element, an
|
|
1136
|
+
* `EmailTemplate { subject, content }` envelope, or a JSON string of either.
|
|
1137
|
+
*/
|
|
1138
|
+
function convertEasyEmailProTemplate(doc) {
|
|
1139
|
+
const root = typeof doc === "string" ? safeParse(doc) : doc;
|
|
1140
|
+
if (typeof root !== "object" || root === null || Array.isArray(root)) throw new Error(INVALID_MESSAGE);
|
|
1141
|
+
const unwrapped = unwrapDocument(root);
|
|
1142
|
+
if (unwrapped.page.type !== "page") throw new Error(INVALID_MESSAGE);
|
|
1143
|
+
const page = unwrapped.page;
|
|
1144
|
+
const subject = unwrapped.subject;
|
|
1145
|
+
assertNotOss(page);
|
|
1146
|
+
const warnings = [];
|
|
1147
|
+
const entries = [];
|
|
1148
|
+
const blocks = [];
|
|
1149
|
+
const resolve = contextFromPage(page);
|
|
1150
|
+
const map = {
|
|
1151
|
+
resolve,
|
|
1152
|
+
warnings
|
|
1153
|
+
};
|
|
1154
|
+
const data = isPlainObject(page.data) ? page.data : {};
|
|
1155
|
+
if (typeof subject === "string" ? subject.trim() : "") warnings.push(`Document subject "${subject}" has no TemplateSettings field and was dropped.`);
|
|
1156
|
+
if (Array.isArray(data.fonts) && data.fonts.length > 0) warnings.push("Dropped fonts[] — Templatical templates have no document-level font table.");
|
|
1157
|
+
if (isNonEmptyHeadStyles(data.headStyles)) warnings.push("Dropped headStyles — Templatical templates have no document-level head-style table.");
|
|
1158
|
+
if (isPlainObject(data.classAttributes) && Object.keys(data.classAttributes).length > 0) warnings.push("Dropped classAttributes — class-based MJML attribute cascade is not applied.");
|
|
1159
|
+
for (const child of page.children ?? []) {
|
|
1160
|
+
if (!isElement(child)) continue;
|
|
1161
|
+
walkNode(child, map, entries, blocks);
|
|
1162
|
+
}
|
|
1163
|
+
const settings = readSettings(page, data, resolve);
|
|
1164
|
+
if (hasMobileAttributes(page)) warnings.push(MOBILE_WARNING);
|
|
1165
|
+
if (resolve.stats.resolved > 0 || resolve.stats.unresolved > 0) warnings.push(varWarning(resolve.stats.resolved, resolve.stats.unresolved));
|
|
1166
|
+
if (blocks.length === 0) warnings.push(EMPTY_WARNING);
|
|
1167
|
+
const defaults = createDefaultTemplateContent();
|
|
1168
|
+
return {
|
|
1169
|
+
content: {
|
|
1170
|
+
...defaults,
|
|
1171
|
+
blocks,
|
|
1172
|
+
settings: {
|
|
1173
|
+
...defaults.settings,
|
|
1174
|
+
...settings
|
|
1175
|
+
}
|
|
1176
|
+
},
|
|
1177
|
+
report: {
|
|
1178
|
+
entries,
|
|
1179
|
+
warnings,
|
|
1180
|
+
summary: {
|
|
1181
|
+
total: entries.length,
|
|
1182
|
+
converted: entries.filter((e) => e.status === "converted").length,
|
|
1183
|
+
approximated: entries.filter((e) => e.status === "approximated").length,
|
|
1184
|
+
htmlFallback: entries.filter((e) => e.status === "html-fallback").length,
|
|
1185
|
+
skipped: entries.filter((e) => e.status === "skipped").length
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
function readSettings(page, data, resolve) {
|
|
1191
|
+
const width = parsePx(readAttr(page, "width", resolve)) ?? REQUIRED_DEFAULTS.width;
|
|
1192
|
+
const backgroundColor = parseColor(readAttr(page, "background-color", resolve)) ?? REQUIRED_DEFAULTS.backgroundColor;
|
|
1193
|
+
const fontFamilyRaw = readAttr(page, "font-family", resolve);
|
|
1194
|
+
const fontFamily = typeof fontFamilyRaw === "string" && !isUnset(fontFamilyRaw) ? fontFamilyRaw : REQUIRED_DEFAULTS.fontFamily;
|
|
1195
|
+
const textColor = parseColor(readAttr(page, "color", resolve)) ?? REQUIRED_DEFAULTS.textColor;
|
|
1196
|
+
const linkColor = parseColor(readAttr(page, "link-color", resolve));
|
|
1197
|
+
const deco = readAttr(page, "link-text-decoration", resolve);
|
|
1198
|
+
const linkUnderline = deco === "underline" ? true : deco === "none" ? false : REQUIRED_DEFAULTS.linkUnderline;
|
|
1199
|
+
const preheaderRaw = typeof data.preheader === "string" ? data.preheader.trim() : "";
|
|
1200
|
+
return {
|
|
1201
|
+
width,
|
|
1202
|
+
backgroundColor,
|
|
1203
|
+
textColor,
|
|
1204
|
+
fontFamily,
|
|
1205
|
+
linkUnderline,
|
|
1206
|
+
locale: REQUIRED_DEFAULTS.locale,
|
|
1207
|
+
...linkColor ? { linkColor } : {},
|
|
1208
|
+
...preheaderRaw ? { preheaderText: preheaderRaw } : {}
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
function walkNode(node, map, entries, blocks) {
|
|
1212
|
+
if (node.type === "placeholder") return;
|
|
1213
|
+
const logic = node.logic;
|
|
1214
|
+
const logicOn = hasLogic(logic);
|
|
1215
|
+
if (logicOn && !hasTypedContent(node)) {
|
|
1216
|
+
entries.push({
|
|
1217
|
+
sourceTag: node.type ?? "logic",
|
|
1218
|
+
templaticalBlockType: null,
|
|
1219
|
+
status: "skipped",
|
|
1220
|
+
note: `Skipped empty logic (${logicExpression(logic)})`
|
|
1221
|
+
});
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
if (typeof node.type === "string" && WIDGET_TYPES.has(node.type)) {
|
|
1225
|
+
const from = entries.length;
|
|
1226
|
+
const data = isPlainObject(node.data) ? node.data : {};
|
|
1227
|
+
const input = isPlainObject(data.input) ? data.input : void 0;
|
|
1228
|
+
const inner = {
|
|
1229
|
+
resolve: withWidgetInput(map.resolve, input),
|
|
1230
|
+
warnings: map.warnings
|
|
1231
|
+
};
|
|
1232
|
+
for (const child of node.children ?? []) {
|
|
1233
|
+
if (!isElement(child)) continue;
|
|
1234
|
+
walkNode(child, inner, entries, blocks);
|
|
1235
|
+
}
|
|
1236
|
+
markApproximated(entries, from, `${node.type} flattened; $var resolved from data.input`, { sourceTag: node.type });
|
|
1237
|
+
if (logicOn) markApproximated(entries, from, `logic ${logicExpression(logic)}`);
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
const from = entries.length;
|
|
1241
|
+
if (typeof node.type === "string" && STRUCTURE_TYPES.has(node.type)) {
|
|
1242
|
+
const converted = buildTopLevel(node, map);
|
|
1243
|
+
for (const block of converted.blocks) blocks.push(block);
|
|
1244
|
+
for (const entry of converted.entries) entries.push(entry);
|
|
1245
|
+
} else {
|
|
1246
|
+
const converted = convertLeaf(node, map);
|
|
1247
|
+
for (const entry of converted.entries) entries.push(entry);
|
|
1248
|
+
if (converted.blocks.length > 0) {
|
|
1249
|
+
if (isLeafType(node.type)) blocks.push(wrapInSection(converted.blocks));
|
|
1250
|
+
else blocks.push(...converted.blocks);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
if (logicOn) markApproximated(entries, from, `logic ${logicExpression(logic)}`);
|
|
1254
|
+
}
|
|
1255
|
+
function wrapInSection(blocks) {
|
|
1256
|
+
const section = createSectionBlock();
|
|
1257
|
+
section.columns = "1";
|
|
1258
|
+
section.children = [blocks];
|
|
1259
|
+
return section;
|
|
1260
|
+
}
|
|
1261
|
+
function markApproximated(entries, from, extra, rewrite) {
|
|
1262
|
+
for (let i = from; i < entries.length; i++) {
|
|
1263
|
+
const entry = entries[i];
|
|
1264
|
+
if (rewrite) entry.sourceTag = rewrite.sourceTag;
|
|
1265
|
+
if (entry.status === "converted") entry.status = "approximated";
|
|
1266
|
+
entry.note = entry.note ? `${entry.note} ${extra}` : extra;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
function hasLogic(logic) {
|
|
1270
|
+
if (logic == null || typeof logic !== "object") return false;
|
|
1271
|
+
return logic.condition != null || logic.iteration != null;
|
|
1272
|
+
}
|
|
1273
|
+
function logicExpression(logic) {
|
|
1274
|
+
const parts = [];
|
|
1275
|
+
if (logic.condition != null) parts.push(typeof logic.condition === "string" ? logic.condition : JSON.stringify(logic.condition));
|
|
1276
|
+
if (logic.iteration != null) {
|
|
1277
|
+
const iteration = logic.iteration;
|
|
1278
|
+
if (typeof iteration === "string") parts.push(iteration);
|
|
1279
|
+
else if (isPlainObject(iteration) && typeof iteration.dataSource === "string") parts.push(iteration.dataSource);
|
|
1280
|
+
else parts.push(JSON.stringify(iteration));
|
|
1281
|
+
}
|
|
1282
|
+
return parts.join(" ");
|
|
1283
|
+
}
|
|
1284
|
+
function hasTypedContent(node) {
|
|
1285
|
+
for (const child of node.children ?? []) {
|
|
1286
|
+
if (!isElement(child)) continue;
|
|
1287
|
+
if (child.type === "placeholder") continue;
|
|
1288
|
+
return true;
|
|
1289
|
+
}
|
|
1290
|
+
return false;
|
|
1291
|
+
}
|
|
1292
|
+
function hasMobileAttributes(node) {
|
|
1293
|
+
if (!isPlainObject(node)) return false;
|
|
1294
|
+
const mobile = node.mobileAttributes;
|
|
1295
|
+
if (isPlainObject(mobile) && Object.keys(mobile).length > 0) return true;
|
|
1296
|
+
if (!Array.isArray(node.children)) return false;
|
|
1297
|
+
for (const child of node.children) if (hasMobileAttributes(child)) return true;
|
|
1298
|
+
return false;
|
|
1299
|
+
}
|
|
1300
|
+
function assertNotOss(page) {
|
|
1301
|
+
const children = page.children ?? [];
|
|
1302
|
+
for (const child of children) {
|
|
1303
|
+
if (!child || typeof child !== "object") continue;
|
|
1304
|
+
const type = child.type;
|
|
1305
|
+
if (typeof type !== "string") continue;
|
|
1306
|
+
if (OSS_CHILD_TYPES.has(type)) throw new Error(OSS_MESSAGE);
|
|
1307
|
+
break;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
function isElement(node) {
|
|
1311
|
+
return typeof node.type === "string" && node.type !== "";
|
|
1312
|
+
}
|
|
1313
|
+
function isNonEmptyHeadStyles(value) {
|
|
1314
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
1315
|
+
return isPlainObject(value) && Object.keys(value).length > 0;
|
|
1316
|
+
}
|
|
1317
|
+
function isPlainObject(value) {
|
|
1318
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1319
|
+
}
|
|
1320
|
+
function safeParse(raw) {
|
|
1321
|
+
try {
|
|
1322
|
+
return JSON.parse(raw);
|
|
1323
|
+
} catch {
|
|
1324
|
+
throw new Error(INVALID_MESSAGE);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
//#endregion
|
|
1328
|
+
export { convertEasyEmailProTemplate };
|
|
1329
|
+
|
|
1330
|
+
//# sourceMappingURL=index.js.map
|