@templatical/import-stripo 0.42.1 → 0.43.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/dist/index.d.ts +1 -1
- package/dist/index.js +782 -155
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { load } from "cheerio";
|
|
2
2
|
import { convertHtmlTemplate } from "@templatical/import-html";
|
|
3
|
-
import { createButtonBlock, createDefaultTemplateContent, createHtmlBlock, createMenuBlock, createSectionBlock, createSocialIconsBlock, createSpacerBlock, generateId } from "@templatical/types";
|
|
3
|
+
import { createButtonBlock, createDefaultTemplateContent, createDividerBlock, createHtmlBlock, createMenuBlock, createSectionBlock, createSocialIconsBlock, createSpacerBlock, generateId } from "@templatical/types";
|
|
4
4
|
//#region src/classes.ts
|
|
5
5
|
function classTokens($el) {
|
|
6
6
|
return ($el.attr("class") ?? "").trim().split(/\s+/).filter(Boolean);
|
|
@@ -39,10 +39,29 @@ function isHidden($el) {
|
|
|
39
39
|
}
|
|
40
40
|
//#endregion
|
|
41
41
|
//#region src/css.ts
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
const ZERO$3 = {
|
|
43
|
+
top: 0,
|
|
44
|
+
right: 0,
|
|
45
|
+
bottom: 0,
|
|
46
|
+
left: 0
|
|
47
|
+
};
|
|
48
|
+
/** Column shares of the body width. The third column takes the rounding remainder. */
|
|
49
|
+
const COLUMN_SHARES = {
|
|
50
|
+
"1": [100],
|
|
51
|
+
"2": [50, 50],
|
|
52
|
+
"3": [
|
|
53
|
+
33.33,
|
|
54
|
+
33.33,
|
|
55
|
+
33.34
|
|
56
|
+
]
|
|
57
|
+
};
|
|
58
|
+
const DIVIDER_WIDTH = /^(-?\d+(?:\.\d+)?)\s*(%|px)?$/i;
|
|
59
|
+
const SIDE_LETTER = {
|
|
60
|
+
t: "top",
|
|
61
|
+
r: "right",
|
|
62
|
+
b: "bottom",
|
|
63
|
+
l: "left"
|
|
64
|
+
};
|
|
46
65
|
function parseStyleAttribute(styleAttr) {
|
|
47
66
|
const result = {};
|
|
48
67
|
if (!styleAttr) return result;
|
|
@@ -55,13 +74,18 @@ function parseStyleAttribute(styleAttr) {
|
|
|
55
74
|
}
|
|
56
75
|
return result;
|
|
57
76
|
}
|
|
77
|
+
function channelHex(value) {
|
|
78
|
+
return Math.max(0, Math.min(255, Math.round(value))).toString(16).padStart(2, "0");
|
|
79
|
+
}
|
|
58
80
|
function parseColor(value) {
|
|
59
81
|
if (!value) return "";
|
|
60
82
|
const trimmed = value.trim().toLowerCase();
|
|
61
83
|
if (trimmed === "transparent" || trimmed === "inherit" || trimmed === "none") return "";
|
|
62
|
-
const hex = trimmed.startsWith("#") ? trimmed : /^[0-9a-f]{3}$|^[0-9a-f]{6}$/.test(trimmed) ? `#${trimmed}` :
|
|
84
|
+
const hex = trimmed.startsWith("#") ? trimmed : /^[0-9a-f]{3}$|^[0-9a-f]{6}$/.test(trimmed) ? `#${trimmed}` : "";
|
|
63
85
|
if (/^#[0-9a-f]{6}$/.test(hex)) return hex;
|
|
64
86
|
if (/^#[0-9a-f]{3}$/.test(hex)) return `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`;
|
|
87
|
+
const rgb = trimmed.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*[\d.]+\s*)?\)$/);
|
|
88
|
+
if (rgb) return `#${channelHex(Number(rgb[1]))}${channelHex(Number(rgb[2]))}${channelHex(Number(rgb[3]))}`;
|
|
65
89
|
return "";
|
|
66
90
|
}
|
|
67
91
|
function parsePxValue(value) {
|
|
@@ -75,6 +99,209 @@ function colorFromPaint(styleAttr, bgcolor) {
|
|
|
75
99
|
const fromShorthand = styles.background ? parseColor(styles.background.trim().split(/\s+/)[0]) : "";
|
|
76
100
|
return parseColor(styles["background-color"]) || fromShorthand || parseColor(bgcolor) || "";
|
|
77
101
|
}
|
|
102
|
+
function hasBackgroundImage(styleAttr) {
|
|
103
|
+
if (!styleAttr) return false;
|
|
104
|
+
const styles = parseStyleAttribute(styleAttr);
|
|
105
|
+
return /url\s*\(/i.test(styles["background-image"] ?? "") || /url\s*\(/i.test(styles.background ?? "");
|
|
106
|
+
}
|
|
107
|
+
function parsePaddingShorthand(value) {
|
|
108
|
+
if (!value) return { ...ZERO$3 };
|
|
109
|
+
const values = value.trim().split(/\s+/).filter(Boolean).map((part) => parsePxValue(part));
|
|
110
|
+
if (values.length === 0) return { ...ZERO$3 };
|
|
111
|
+
if (values.length === 1) return {
|
|
112
|
+
top: values[0],
|
|
113
|
+
right: values[0],
|
|
114
|
+
bottom: values[0],
|
|
115
|
+
left: values[0]
|
|
116
|
+
};
|
|
117
|
+
if (values.length === 2) return {
|
|
118
|
+
top: values[0],
|
|
119
|
+
right: values[1],
|
|
120
|
+
bottom: values[0],
|
|
121
|
+
left: values[1]
|
|
122
|
+
};
|
|
123
|
+
if (values.length === 3) return {
|
|
124
|
+
top: values[0],
|
|
125
|
+
right: values[1],
|
|
126
|
+
bottom: values[2],
|
|
127
|
+
left: values[1]
|
|
128
|
+
};
|
|
129
|
+
return {
|
|
130
|
+
top: values[0],
|
|
131
|
+
right: values[1],
|
|
132
|
+
bottom: values[2],
|
|
133
|
+
left: values[3]
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Longhand wins when it is present, including an explicit 0.
|
|
138
|
+
* A missing longhand falls back to the shorthand, then 0.
|
|
139
|
+
*/
|
|
140
|
+
function readPadding(styles) {
|
|
141
|
+
const shorthand = styles.padding !== void 0 ? parsePaddingShorthand(styles.padding) : ZERO$3;
|
|
142
|
+
const side = (name) => {
|
|
143
|
+
const key = `padding-${name}`;
|
|
144
|
+
if (styles[key] !== void 0) return parsePxValue(styles[key]);
|
|
145
|
+
return shorthand[name];
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
top: side("top"),
|
|
149
|
+
right: side("right"),
|
|
150
|
+
bottom: side("bottom"),
|
|
151
|
+
left: side("left")
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/** `es-p20` sets every side; `es-p10t` / `r` / `b` / `l` then override one side. */
|
|
155
|
+
function paddingFromClasses(classAttr) {
|
|
156
|
+
const pad = { ...ZERO$3 };
|
|
157
|
+
const tokens = (classAttr ?? "").split(/\s+/).filter(Boolean);
|
|
158
|
+
for (const token of tokens) {
|
|
159
|
+
const all = /^es-p(\d+)$/.exec(token);
|
|
160
|
+
if (!all) continue;
|
|
161
|
+
const n = Number(all[1]);
|
|
162
|
+
pad.top = n;
|
|
163
|
+
pad.right = n;
|
|
164
|
+
pad.bottom = n;
|
|
165
|
+
pad.left = n;
|
|
166
|
+
}
|
|
167
|
+
for (const token of tokens) {
|
|
168
|
+
const side = /^es-p(\d+)([trbl])$/.exec(token);
|
|
169
|
+
if (!side) continue;
|
|
170
|
+
pad[SIDE_LETTER[side[2]]] = Number(side[1]);
|
|
171
|
+
}
|
|
172
|
+
return pad;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Class padding first, then inline padding per side.
|
|
176
|
+
* A shorthand replaces every side; a longhand replaces only that side.
|
|
177
|
+
*/
|
|
178
|
+
function readBoxPadding(classAttr, styleAttr) {
|
|
179
|
+
const base = paddingFromClasses(classAttr);
|
|
180
|
+
const styles = parseStyleAttribute(styleAttr);
|
|
181
|
+
const hasShorthand = styles.padding !== void 0;
|
|
182
|
+
const shorthand = hasShorthand ? parsePaddingShorthand(styles.padding) : base;
|
|
183
|
+
const side = (name) => {
|
|
184
|
+
const key = `padding-${name}`;
|
|
185
|
+
if (styles[key] !== void 0) return parsePxValue(styles[key]);
|
|
186
|
+
if (hasShorthand) return shorthand[name];
|
|
187
|
+
return base[name];
|
|
188
|
+
};
|
|
189
|
+
return {
|
|
190
|
+
top: side("top"),
|
|
191
|
+
right: side("right"),
|
|
192
|
+
bottom: side("bottom"),
|
|
193
|
+
left: side("left")
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Body fill paints the section. A band that is neither the page nor the body
|
|
198
|
+
* becomes the section wrapper. A transparent body leaves the band on the section.
|
|
199
|
+
*/
|
|
200
|
+
function resolveSectionPaint(pageBackground, band, body) {
|
|
201
|
+
const page = pageBackground || "#ffffff";
|
|
202
|
+
if (body) {
|
|
203
|
+
if (band && band !== page && band !== body) return {
|
|
204
|
+
backgroundColor: body,
|
|
205
|
+
wrapperColor: band,
|
|
206
|
+
notes: ["The band colour was imported as a section wrapper."]
|
|
207
|
+
};
|
|
208
|
+
return {
|
|
209
|
+
backgroundColor: body,
|
|
210
|
+
notes: []
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
backgroundColor: band,
|
|
215
|
+
notes: []
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/** `es-*-body` width in px, or 600 when the document does not state one. */
|
|
219
|
+
function emailBodyWidth(widthAttr, styleAttr) {
|
|
220
|
+
const n = parsePxValue((widthAttr || parseStyleAttribute(styleAttr).width || "").trim());
|
|
221
|
+
return n > 0 ? n : 600;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Room each column has for a line, before the line's own side padding.
|
|
225
|
+
* The share is of the body width; the first column then loses the section's
|
|
226
|
+
* left padding and the last loses the right. One column loses both.
|
|
227
|
+
* A room that is not positive is unknown — the caller keeps the stated px.
|
|
228
|
+
*/
|
|
229
|
+
function columnRooms(bodyWidth, layout, padding, slotCount) {
|
|
230
|
+
const shares = COLUMN_SHARES[layout];
|
|
231
|
+
const count = layout === "1" ? 1 : slotCount;
|
|
232
|
+
const width = bodyWidth > 0 ? bodyWidth : 600;
|
|
233
|
+
return Array.from({ length: count }, (_, index) => {
|
|
234
|
+
const span = layout === "1" ? width : width * shares[index] / 100;
|
|
235
|
+
const left = layout === "1" || index === 0 ? padding.left : 0;
|
|
236
|
+
const right = layout === "1" || index === count - 1 ? padding.right : 0;
|
|
237
|
+
const room = span - left - right;
|
|
238
|
+
return room > 0 ? room : void 0;
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* No width, `auto` and `100%` span the column. Another percentage stays a
|
|
243
|
+
* share, clamped to 0–100 and rounded to two decimals. A px width stays px
|
|
244
|
+
* until it reaches `room` less the divider's own side padding.
|
|
245
|
+
*/
|
|
246
|
+
function readDividerWidth(raw, room, ownLeft, ownRight, notes) {
|
|
247
|
+
const trimmed = raw.trim();
|
|
248
|
+
if (trimmed === "" || trimmed.toLowerCase() === "auto" || trimmed === "100%") return "full";
|
|
249
|
+
const match = trimmed.match(DIVIDER_WIDTH);
|
|
250
|
+
if (!match) {
|
|
251
|
+
notes.push(`Divider width "${trimmed}" could not be read; imported as full width.`);
|
|
252
|
+
return "full";
|
|
253
|
+
}
|
|
254
|
+
const value = parseFloat(match[1]);
|
|
255
|
+
if ((match[2] ?? "").toLowerCase() === "%") {
|
|
256
|
+
const percent = Math.min(100, Math.max(0, value));
|
|
257
|
+
if (percent !== value) notes.push(`Divider width ${trimmed} was clamped to ${percent}%.`);
|
|
258
|
+
const share = Math.round((percent + Number.EPSILON) * 100) / 100;
|
|
259
|
+
return share === 100 ? "full" : `${share}%`;
|
|
260
|
+
}
|
|
261
|
+
const px = Math.max(0, Math.round(value));
|
|
262
|
+
if (value < 0) notes.push(`Divider width ${trimmed} was clamped to 0px.`);
|
|
263
|
+
if (room === void 0 || room <= 0) return px;
|
|
264
|
+
return px >= room - ownLeft - ownRight ? "full" : px;
|
|
265
|
+
}
|
|
266
|
+
const BORDER_STYLES = /* @__PURE__ */ new Set([
|
|
267
|
+
"none",
|
|
268
|
+
"hidden",
|
|
269
|
+
"dotted",
|
|
270
|
+
"dashed",
|
|
271
|
+
"solid",
|
|
272
|
+
"double",
|
|
273
|
+
"groove",
|
|
274
|
+
"ridge",
|
|
275
|
+
"inset",
|
|
276
|
+
"outset"
|
|
277
|
+
]);
|
|
278
|
+
/** A `border-bottom` / `border-top` shorthand. Color is empty when absent. */
|
|
279
|
+
function parseBorderShorthand(value) {
|
|
280
|
+
const parsed = {
|
|
281
|
+
width: 0,
|
|
282
|
+
style: "solid",
|
|
283
|
+
color: ""
|
|
284
|
+
};
|
|
285
|
+
if (!value) return parsed;
|
|
286
|
+
for (const token of value.trim().split(/\s+/)) {
|
|
287
|
+
const lower = token.toLowerCase();
|
|
288
|
+
if (BORDER_STYLES.has(lower)) parsed.style = lower;
|
|
289
|
+
else if (/^-?\d+(?:\.\d+)?(?:px)?$/i.test(lower)) parsed.width = parsePxValue(lower);
|
|
290
|
+
else {
|
|
291
|
+
const color = parseColor(lower);
|
|
292
|
+
if (color) parsed.color = color;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return parsed;
|
|
296
|
+
}
|
|
297
|
+
function visibleLineBorder(styles) {
|
|
298
|
+
for (const key of ["border-bottom", "border-top"]) {
|
|
299
|
+
if (styles[key] === void 0) continue;
|
|
300
|
+
const parsed = parseBorderShorthand(styles[key]);
|
|
301
|
+
if (parsed.width > 0 && parsed.style !== "none" && parsed.style !== "hidden") return parsed;
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
78
305
|
//#endregion
|
|
79
306
|
//#region src/fragment.ts
|
|
80
307
|
/** Paint after `createSectionBlock` so `styles` keeps its required padding. */
|
|
@@ -88,11 +315,21 @@ function flattenBlocks(blocks) {
|
|
|
88
315
|
else out.push(b);
|
|
89
316
|
return out;
|
|
90
317
|
}
|
|
91
|
-
|
|
318
|
+
function fragmentDocument(inner, css) {
|
|
319
|
+
const safe = css ? css.replace(/<\/style/gi, "<\\/style") : "";
|
|
320
|
+
return `<!DOCTYPE html><html>${safe ? `<head><style>${safe}</style></head>` : ""}<body>${inner}</body></html>`;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Run the generic HTML importer on a subtree and keep its blocks.
|
|
324
|
+
* Section entries belong to the row the HTML importer invented; Stripo's own
|
|
325
|
+
* section entries are recorded by the caller.
|
|
326
|
+
*/
|
|
92
327
|
function blocksFromHtml(inner, ctx) {
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
328
|
+
const result = convertHtmlTemplate(fragmentDocument(inner, ctx.css));
|
|
329
|
+
for (const entry of result.report.entries) {
|
|
330
|
+
if (entry.templaticalBlockType === "section") continue;
|
|
331
|
+
ctx.entries.push(entry);
|
|
332
|
+
}
|
|
96
333
|
ctx.warnings.push(...result.report.warnings);
|
|
97
334
|
return flattenBlocks(result.content.blocks);
|
|
98
335
|
}
|
|
@@ -152,6 +389,28 @@ function normalizePlatform(name, src, alt) {
|
|
|
152
389
|
}
|
|
153
390
|
//#endregion
|
|
154
391
|
//#region src/labelled.ts
|
|
392
|
+
const ZERO$2 = {
|
|
393
|
+
top: 0,
|
|
394
|
+
right: 0,
|
|
395
|
+
bottom: 0,
|
|
396
|
+
left: 0
|
|
397
|
+
};
|
|
398
|
+
/** Tables whose padding is the section's padding. A divider must not copy it. */
|
|
399
|
+
const STRUCTURE_TABLES = [
|
|
400
|
+
"es-header-body",
|
|
401
|
+
"es-content-body",
|
|
402
|
+
"es-footer-body",
|
|
403
|
+
"es-header",
|
|
404
|
+
"es-content",
|
|
405
|
+
"es-footer"
|
|
406
|
+
];
|
|
407
|
+
const MAPPED_LINE_STYLES = /* @__PURE__ */ new Set([
|
|
408
|
+
"double",
|
|
409
|
+
"groove",
|
|
410
|
+
"ridge",
|
|
411
|
+
"inset",
|
|
412
|
+
"outset"
|
|
413
|
+
]);
|
|
155
414
|
function firstAnchor($el) {
|
|
156
415
|
return $el.is("a") ? $el : $el.find("a").first();
|
|
157
416
|
}
|
|
@@ -175,13 +434,123 @@ function buttonFrom($el, sourceTag, ctx) {
|
|
|
175
434
|
...borderRadius > 0 ? { borderRadius } : {}
|
|
176
435
|
});
|
|
177
436
|
}
|
|
178
|
-
function
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
437
|
+
function declaresPadding($el) {
|
|
438
|
+
const styles = parseStyleAttribute($el.attr("style"));
|
|
439
|
+
return styles.padding !== void 0 || styles["padding-top"] !== void 0 || styles["padding-right"] !== void 0 || styles["padding-bottom"] !== void 0 || styles["padding-left"] !== void 0;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* The structure cell's padding becomes the section's padding. Reading it again
|
|
443
|
+
* as the divider's own padding shortens the line twice.
|
|
444
|
+
*/
|
|
445
|
+
function isStructureCell($cell) {
|
|
446
|
+
if (!$cell.is("td") && !$cell.is("th")) return false;
|
|
447
|
+
if (hasToken($cell, "esd-structure")) return true;
|
|
448
|
+
let $table = $cell.parent().parent();
|
|
449
|
+
if ($table.is("tbody") || $table.is("thead") || $table.is("tfoot")) $table = $table.parent();
|
|
450
|
+
return $table.is("table") && hasAnyToken($table, STRUCTURE_TABLES);
|
|
451
|
+
}
|
|
452
|
+
function spacerElement($root) {
|
|
453
|
+
if (hasToken($root, "es-spacer")) return $root;
|
|
454
|
+
const $inner = $root.find("[class~='es-spacer']").first();
|
|
455
|
+
return $inner.length ? $inner : $root;
|
|
456
|
+
}
|
|
457
|
+
function lineBorder($root, $spacer, $) {
|
|
458
|
+
const own = visibleLineBorder(parseStyleAttribute($root.attr("style")));
|
|
459
|
+
if (own) return own;
|
|
460
|
+
const onSpacer = visibleLineBorder(parseStyleAttribute($spacer.attr("style")));
|
|
461
|
+
if (onSpacer) return onSpacer;
|
|
462
|
+
let found = null;
|
|
463
|
+
$spacer.find("td, th").each((_, cell) => {
|
|
464
|
+
if (found) return;
|
|
465
|
+
found = visibleLineBorder(parseStyleAttribute($(cell).attr("style")));
|
|
466
|
+
});
|
|
467
|
+
return found;
|
|
468
|
+
}
|
|
469
|
+
function dividerPadding($root, $spacer) {
|
|
470
|
+
if (declaresPadding($root)) return readPadding(parseStyleAttribute($root.attr("style")));
|
|
471
|
+
if ($spacer[0] !== $root[0] && declaresPadding($spacer)) return readPadding(parseStyleAttribute($spacer.attr("style")));
|
|
472
|
+
const $parent = $spacer.parent();
|
|
473
|
+
if (($parent.is("td") || $parent.is("th")) && !isStructureCell($parent)) return readPadding(parseStyleAttribute($parent.attr("style")));
|
|
474
|
+
return { ...ZERO$2 };
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* No align and no margin is unset. A partial Stripo line with neither sits on
|
|
478
|
+
* the left; the caller supplies that default.
|
|
479
|
+
*/
|
|
480
|
+
function specifiedAlign($el) {
|
|
481
|
+
const styles = parseStyleAttribute($el.attr("style"));
|
|
482
|
+
const alignAttr = ($el.attr("align") ?? "").trim().toLowerCase();
|
|
483
|
+
const alignKnown = alignAttr === "left" || alignAttr === "center" || alignAttr === "right";
|
|
484
|
+
const hasMargin = styles.margin !== void 0 || styles["margin-left"] !== void 0 || styles["margin-right"] !== void 0;
|
|
485
|
+
if (!alignKnown && !hasMargin) return void 0;
|
|
486
|
+
let left = "auto";
|
|
487
|
+
let right = "auto";
|
|
488
|
+
if (alignAttr === "left") left = "0";
|
|
489
|
+
if (alignAttr === "right") right = "0";
|
|
490
|
+
const margin = (styles.margin ?? "").trim().split(/\s+/).filter(Boolean);
|
|
491
|
+
if (margin.length > 0) {
|
|
492
|
+
right = margin[1] ?? margin[0];
|
|
493
|
+
left = margin.length === 4 ? margin[3] : right;
|
|
494
|
+
}
|
|
495
|
+
if (styles["margin-left"] !== void 0) left = styles["margin-left"];
|
|
496
|
+
if (styles["margin-right"] !== void 0) right = styles["margin-right"];
|
|
497
|
+
const isAuto = (value) => value.trim().toLowerCase() === "auto";
|
|
498
|
+
if (isAuto(left) && isAuto(right)) return "center";
|
|
499
|
+
return isAuto(left) ? "right" : "left";
|
|
500
|
+
}
|
|
501
|
+
function placementOf($spacer, $root) {
|
|
502
|
+
const own = specifiedAlign($spacer);
|
|
503
|
+
if (own) return own;
|
|
504
|
+
for (const $candidate of [$spacer.parent(), $root.parent()]) {
|
|
505
|
+
if (!$candidate.is("td") && !$candidate.is("th")) continue;
|
|
506
|
+
const fromCell = specifiedAlign($candidate);
|
|
507
|
+
if (fromCell) return fromCell;
|
|
508
|
+
}
|
|
509
|
+
return "left";
|
|
510
|
+
}
|
|
511
|
+
function spacerHeight($el) {
|
|
512
|
+
const styles = parseStyleAttribute($el.attr("style"));
|
|
513
|
+
const fromStyle = parsePxValue(styles.height);
|
|
514
|
+
if (fromStyle > 0) return fromStyle;
|
|
515
|
+
const fromAttr = parsePxValue($el.attr("height"));
|
|
516
|
+
if (fromAttr > 0) return fromAttr;
|
|
517
|
+
const pad = readPadding(styles);
|
|
518
|
+
const vertical = pad.top + pad.bottom;
|
|
519
|
+
return vertical > 0 ? vertical : 24;
|
|
520
|
+
}
|
|
521
|
+
function dividerBlock(border, padding, width) {
|
|
522
|
+
const lineStyle = border.style === "dashed" || border.style === "dotted" ? border.style : "solid";
|
|
523
|
+
return createDividerBlock({
|
|
524
|
+
lineStyle,
|
|
525
|
+
color: border.color || "#e5e7eb",
|
|
526
|
+
thickness: border.width,
|
|
527
|
+
width,
|
|
528
|
+
styles: { padding }
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* A bordered spacer is a divider. `room` is the column width the line can
|
|
533
|
+
* span, after the section padding that column owns.
|
|
534
|
+
*/
|
|
535
|
+
function spacerFrom($el, sourceTag, ctx, $, room) {
|
|
536
|
+
const $spacer = spacerElement($el);
|
|
537
|
+
const border = lineBorder($el, $spacer, $);
|
|
538
|
+
if (!border) {
|
|
539
|
+
const height = spacerHeight($spacer);
|
|
540
|
+
pushEntry(ctx, sourceTag, "spacer", "converted");
|
|
541
|
+
return createSpacerBlock({ height });
|
|
542
|
+
}
|
|
543
|
+
const padding = dividerPadding($el, $spacer);
|
|
544
|
+
const raw = (parseStyleAttribute($spacer.attr("style")).width ?? $spacer.attr("width") ?? "").trim();
|
|
545
|
+
const notes = [];
|
|
546
|
+
const width = readDividerWidth(raw, room, padding.left, padding.right, notes);
|
|
547
|
+
if (MAPPED_LINE_STYLES.has(border.style)) notes.push(`Divider border style "${border.style}" was imported as solid.`);
|
|
548
|
+
if (width !== "full") {
|
|
549
|
+
const align = placementOf($spacer, $el);
|
|
550
|
+
if (align !== "center") notes.push(`The source aligns this divider ${align}; Templatical centres every divider.`);
|
|
551
|
+
}
|
|
552
|
+
pushEntry(ctx, sourceTag, "divider", notes.length > 0 ? "approximated" : "converted", notes.length > 0 ? notes.join(" ") : void 0);
|
|
553
|
+
return dividerBlock(border, padding, width);
|
|
185
554
|
}
|
|
186
555
|
function menuItems($table, $) {
|
|
187
556
|
const items = [];
|
|
@@ -237,157 +606,344 @@ function labelledInnerBlocks($el, $, ctx) {
|
|
|
237
606
|
}
|
|
238
607
|
//#endregion
|
|
239
608
|
//#region src/compiled.ts
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
*/
|
|
248
|
-
function stripeFill($stripe) {
|
|
249
|
-
const $body = $stripe.find("[class~='es-header-body'], [class~='es-content-body'], [class~='es-footer-body']").first();
|
|
250
|
-
if ($body.length) {
|
|
251
|
-
const fromBody = paintOf($body);
|
|
252
|
-
if (fromBody) return fromBody;
|
|
253
|
-
}
|
|
254
|
-
return paintOf($stripe);
|
|
255
|
-
}
|
|
609
|
+
const ZERO$1 = {
|
|
610
|
+
top: 0,
|
|
611
|
+
right: 0,
|
|
612
|
+
bottom: 0,
|
|
613
|
+
left: 0
|
|
614
|
+
};
|
|
615
|
+
const COLUMN_TOKENS = ["es-left", "es-right"];
|
|
256
616
|
const STRIPE_TOKENS = [
|
|
257
617
|
"es-header",
|
|
258
618
|
"es-content",
|
|
259
619
|
"es-footer"
|
|
260
620
|
];
|
|
621
|
+
const BODY_SELECTOR$1 = "[class~='es-header-body'], [class~='es-content-body'], [class~='es-footer-body']";
|
|
622
|
+
const BETWEEN_NOTE = "Content between columns was placed after them.";
|
|
623
|
+
const SEAM_NOTE = "This row's padding was split across more than one section.";
|
|
624
|
+
const IMAGE_NOTE$1 = "A background image was not imported.";
|
|
625
|
+
function paintOf($el) {
|
|
626
|
+
return colorFromPaint($el.attr("style"), $el.attr("bgcolor"));
|
|
627
|
+
}
|
|
261
628
|
function stripeToken($el) {
|
|
262
|
-
return STRIPE_TOKENS.find((
|
|
629
|
+
return STRIPE_TOKENS.find((token) => hasToken($el, token));
|
|
263
630
|
}
|
|
264
631
|
function topStripes($) {
|
|
265
632
|
const out = [];
|
|
266
633
|
$("[class]").each((_, node) => {
|
|
267
634
|
const $el = $(node);
|
|
268
635
|
if (!stripeToken($el)) return;
|
|
269
|
-
if ($el.parents().toArray().some((
|
|
636
|
+
if ($el.parents().toArray().some((parent) => hasAnyToken($(parent), STRIPE_TOKENS))) return;
|
|
270
637
|
out.push($el);
|
|
271
638
|
});
|
|
272
639
|
return out;
|
|
273
640
|
}
|
|
274
|
-
function
|
|
641
|
+
function directRows($table, $) {
|
|
642
|
+
const $direct = $table.children("tr");
|
|
643
|
+
const $nested = $table.children("tbody, thead, tfoot").children("tr");
|
|
644
|
+
return ($direct.length > 0 ? $direct : $nested).toArray().map((node) => $(node));
|
|
645
|
+
}
|
|
646
|
+
function directCell($table) {
|
|
647
|
+
const $direct = $table.children("tr").children("td, th");
|
|
648
|
+
const $nested = $table.children("tbody, thead, tfoot").children("tr").children("td, th");
|
|
649
|
+
return ($direct.length > 0 ? $direct : $nested).first();
|
|
650
|
+
}
|
|
651
|
+
/** Paint on the stripe table, else on its cell. A `td` stripe is its own paint. */
|
|
652
|
+
function bandColor$1($stripe) {
|
|
653
|
+
const own = paintOf($stripe);
|
|
654
|
+
if (own) return own;
|
|
655
|
+
if ($stripe.is("td") || $stripe.is("th")) return "";
|
|
656
|
+
const $cell = directCell($stripe);
|
|
657
|
+
return $cell.length > 0 ? paintOf($cell) : "";
|
|
658
|
+
}
|
|
659
|
+
function imageNote$1(...styles) {
|
|
660
|
+
return styles.some((style) => hasBackgroundImage(style)) ? IMAGE_NOTE$1 : void 0;
|
|
661
|
+
}
|
|
662
|
+
function hasPadding(padding) {
|
|
663
|
+
return padding.top !== 0 || padding.right !== 0 || padding.bottom !== 0 || padding.left !== 0;
|
|
664
|
+
}
|
|
665
|
+
function splitPadding(padding, count, index) {
|
|
666
|
+
if (count <= 1) return { ...padding };
|
|
667
|
+
return {
|
|
668
|
+
top: index === 0 ? padding.top : 0,
|
|
669
|
+
right: padding.right,
|
|
670
|
+
bottom: index === count - 1 ? padding.bottom : 0,
|
|
671
|
+
left: padding.left
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
function isColumn($el) {
|
|
675
|
+
return $el.is("table") && hasAnyToken($el, COLUMN_TOKENS);
|
|
676
|
+
}
|
|
677
|
+
function containsColumn($el, $) {
|
|
678
|
+
const stop = $el[0];
|
|
679
|
+
let found = false;
|
|
680
|
+
$el.find("table").each((_, node) => {
|
|
681
|
+
if (found) return;
|
|
682
|
+
const $n = $(node);
|
|
683
|
+
if (!hasAnyToken($n, COLUMN_TOKENS)) return;
|
|
684
|
+
let $parent = $n.parent();
|
|
685
|
+
while ($parent.length > 0 && $parent[0] !== stop) {
|
|
686
|
+
if (hasAnyToken($parent, COLUMN_TOKENS)) return;
|
|
687
|
+
$parent = $parent.parent();
|
|
688
|
+
}
|
|
689
|
+
found = true;
|
|
690
|
+
});
|
|
691
|
+
return found;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Columns in one cell are one section. Flow before them, between them and
|
|
695
|
+
* after them becomes its own one-column section, in that order.
|
|
696
|
+
*/
|
|
697
|
+
function collectPieces($root, $) {
|
|
698
|
+
const pieces = [];
|
|
699
|
+
const walk = ($parent) => {
|
|
700
|
+
const flow = [];
|
|
701
|
+
const flush = () => {
|
|
702
|
+
if (flow.length === 0) return;
|
|
703
|
+
pieces.push({
|
|
704
|
+
kind: "flow",
|
|
705
|
+
nodes: flow.splice(0, flow.length)
|
|
706
|
+
});
|
|
707
|
+
};
|
|
708
|
+
for (const node of $parent.contents().toArray()) {
|
|
709
|
+
if (node.type === "text") {
|
|
710
|
+
if (node.data.trim()) flow.push(node);
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
if (node.type !== "tag") continue;
|
|
714
|
+
const $el = $(node);
|
|
715
|
+
if (isHidden($el)) continue;
|
|
716
|
+
if (isColumn($el)) {
|
|
717
|
+
flush();
|
|
718
|
+
pieces.push({
|
|
719
|
+
kind: "column",
|
|
720
|
+
el: node
|
|
721
|
+
});
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
if (containsColumn($el, $)) {
|
|
725
|
+
flush();
|
|
726
|
+
walk($el);
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
flow.push(node);
|
|
730
|
+
}
|
|
731
|
+
flush();
|
|
732
|
+
};
|
|
733
|
+
walk($root);
|
|
734
|
+
return pieces;
|
|
735
|
+
}
|
|
736
|
+
function widgetKind($el) {
|
|
737
|
+
if (hasToken($el, "es-menu")) {
|
|
738
|
+
if (!$el.is("table") && $el.find("table").length > 0) return null;
|
|
739
|
+
return "menu";
|
|
740
|
+
}
|
|
741
|
+
if (hasToken($el, "es-social")) return "social";
|
|
742
|
+
if (hasToken($el, "es-button")) {
|
|
743
|
+
if ($el.is("a") || $el.find("a").length > 0) return "button";
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
if (hasToken($el, "es-spacer")) return "spacer";
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
function containsWidget($el, $) {
|
|
750
|
+
let found = false;
|
|
751
|
+
$el.find("[class]").each((_, node) => {
|
|
752
|
+
if (found) return;
|
|
753
|
+
if (widgetKind($(node))) found = true;
|
|
754
|
+
});
|
|
755
|
+
return found;
|
|
756
|
+
}
|
|
757
|
+
function widgetBlocks($el, kind, $, ctx, room) {
|
|
758
|
+
if (kind === "button") return [buttonFrom($el, "es-button", ctx)];
|
|
759
|
+
if (kind === "spacer") return [spacerFrom($el, "es-spacer", ctx, $, room)];
|
|
760
|
+
if (kind === "social") return [socialFrom($el, $, "es-social", ctx)];
|
|
761
|
+
const menu = menuFrom($el, $, "es-menu", ctx);
|
|
762
|
+
if (menu) return [menu];
|
|
763
|
+
return blocksFromHtml($.html($el) ?? "", ctx);
|
|
764
|
+
}
|
|
765
|
+
/** Widgets stay where they sit. A run of everything else goes through HTML import. */
|
|
766
|
+
function convertRegion(nodes, $, ctx, room) {
|
|
275
767
|
const out = [];
|
|
276
|
-
|
|
768
|
+
let pending = "";
|
|
769
|
+
const flush = () => {
|
|
770
|
+
if (!pending.trim()) {
|
|
771
|
+
pending = "";
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
out.push(...blocksFromHtml(pending, ctx));
|
|
775
|
+
pending = "";
|
|
776
|
+
};
|
|
777
|
+
const visit = (node) => {
|
|
778
|
+
if (node.type === "text") {
|
|
779
|
+
if (node.data.trim()) pending += node.data;
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
if (node.type !== "tag") return;
|
|
277
783
|
const $el = $(node);
|
|
784
|
+
if ($el.is("head, style, script")) return;
|
|
278
785
|
if (isHidden($el)) return;
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
786
|
+
const kind = widgetKind($el);
|
|
787
|
+
if (kind) {
|
|
788
|
+
flush();
|
|
789
|
+
out.push(...widgetBlocks($el, kind, $, ctx, room));
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (containsWidget($el, $)) {
|
|
793
|
+
for (const child of $el.contents().toArray()) visit(child);
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
pending += $.html(node) ?? "";
|
|
797
|
+
};
|
|
798
|
+
for (const node of nodes) visit(node);
|
|
799
|
+
flush();
|
|
288
800
|
return out;
|
|
289
801
|
}
|
|
290
|
-
function
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
802
|
+
function regionFromElement($el, $, ctx, room) {
|
|
803
|
+
return convertRegion($el.contents().toArray(), $, ctx, room);
|
|
804
|
+
}
|
|
805
|
+
function hasBlocks(children) {
|
|
806
|
+
return children.some((column) => column.length > 0);
|
|
807
|
+
}
|
|
808
|
+
function columnChildren(cols, $, ctx, bodyWidth, padding) {
|
|
809
|
+
const count = cols.length;
|
|
810
|
+
const layout = count >= 3 ? "3" : count === 2 ? "2" : "1";
|
|
811
|
+
const rooms = columnRooms(bodyWidth, count >= 4 ? "3" : layout, padding, count >= 4 ? 3 : layout === "1" ? 1 : count);
|
|
812
|
+
let children;
|
|
813
|
+
if (count >= 4) {
|
|
814
|
+
const rest = [];
|
|
815
|
+
for (const el of cols.slice(2)) rest.push(...regionFromElement($(el), $, ctx, rooms[2]));
|
|
816
|
+
children = [
|
|
817
|
+
regionFromElement($(cols[0]), $, ctx, rooms[0]),
|
|
818
|
+
regionFromElement($(cols[1]), $, ctx, rooms[1]),
|
|
819
|
+
rest
|
|
820
|
+
];
|
|
821
|
+
} else children = cols.map((el, index) => regionFromElement($(el), $, ctx, rooms[index]));
|
|
822
|
+
if (!hasBlocks(children)) return null;
|
|
823
|
+
if (count < 4) return {
|
|
824
|
+
children,
|
|
825
|
+
layout: count >= 3 ? "3" : layout
|
|
302
826
|
};
|
|
303
|
-
|
|
304
|
-
ctx.warnings.push(
|
|
827
|
+
const flattenNote = `Row with ${count} columns was flattened; extra columns merged into the third slot. Templatical supports up to 3 columns per section.`;
|
|
828
|
+
ctx.warnings.push(flattenNote);
|
|
305
829
|
return {
|
|
830
|
+
children,
|
|
306
831
|
layout: "3",
|
|
307
|
-
|
|
832
|
+
flattenNote
|
|
308
833
|
};
|
|
309
834
|
}
|
|
310
|
-
function
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
835
|
+
function flowNodes(pieces) {
|
|
836
|
+
return pieces.flatMap((piece) => piece.kind === "flow" ? piece.nodes : []);
|
|
837
|
+
}
|
|
838
|
+
function groupsFromPieces(pieces) {
|
|
839
|
+
const columns = pieces.filter((piece) => piece.kind === "column");
|
|
840
|
+
if (columns.length === 0) return pieces.length > 0 ? [{
|
|
841
|
+
mode: "flow",
|
|
842
|
+
pieces
|
|
843
|
+
}] : [];
|
|
844
|
+
let first = -1;
|
|
845
|
+
let last = -1;
|
|
846
|
+
pieces.forEach((piece, index) => {
|
|
847
|
+
if (piece.kind !== "column") return;
|
|
848
|
+
if (first === -1) first = index;
|
|
849
|
+
last = index;
|
|
324
850
|
});
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
851
|
+
const groups = [];
|
|
852
|
+
const leading = pieces.slice(0, first);
|
|
853
|
+
if (leading.length > 0) groups.push({
|
|
854
|
+
mode: "flow",
|
|
855
|
+
pieces: leading
|
|
328
856
|
});
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
take($n, buttonFrom($n, "es-button", ctx));
|
|
857
|
+
groups.push({
|
|
858
|
+
mode: "columns",
|
|
859
|
+
pieces: columns
|
|
333
860
|
});
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
861
|
+
const between = pieces.slice(first + 1, last).filter((piece) => piece.kind === "flow");
|
|
862
|
+
if (between.length > 0) groups.push({
|
|
863
|
+
mode: "flow",
|
|
864
|
+
pieces: between,
|
|
865
|
+
extra: BETWEEN_NOTE
|
|
337
866
|
});
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
used.add(i);
|
|
357
|
-
out.push(...replacements[i]);
|
|
358
|
-
continue;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
out.push(b);
|
|
867
|
+
const trailing = pieces.slice(last + 1);
|
|
868
|
+
if (trailing.length > 0) groups.push({
|
|
869
|
+
mode: "flow",
|
|
870
|
+
pieces: trailing
|
|
871
|
+
});
|
|
872
|
+
return groups;
|
|
873
|
+
}
|
|
874
|
+
function sectionsFromRow($row, $, ctx, token, paint, image, bodyWidth) {
|
|
875
|
+
if (isHidden($row)) return [];
|
|
876
|
+
const pieces = [];
|
|
877
|
+
let padding = { ...ZERO$1 };
|
|
878
|
+
let sawCell = false;
|
|
879
|
+
$row.children("td, th").each((_, cell) => {
|
|
880
|
+
const $cell = $(cell);
|
|
881
|
+
if (isHidden($cell)) return;
|
|
882
|
+
if (!sawCell) {
|
|
883
|
+
padding = readBoxPadding($cell.attr("class"), $cell.attr("style"));
|
|
884
|
+
sawCell = true;
|
|
362
885
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
886
|
+
pieces.push(...collectPieces($cell, $));
|
|
887
|
+
});
|
|
888
|
+
if (!sawCell || pieces.length === 0) return [];
|
|
889
|
+
const rendered = [];
|
|
890
|
+
for (const group of groupsFromPieces(pieces)) {
|
|
891
|
+
if (group.mode === "columns") {
|
|
892
|
+
const built = columnChildren(group.pieces.flatMap((piece) => piece.kind === "column" ? [piece.el] : []), $, ctx, bodyWidth, padding);
|
|
893
|
+
if (!built) continue;
|
|
894
|
+
rendered.push({
|
|
895
|
+
children: built.children,
|
|
896
|
+
layout: built.layout,
|
|
897
|
+
extra: built.flattenNote
|
|
898
|
+
});
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
const blocks = convertRegion(flowNodes(group.pieces), $, ctx, columnRooms(bodyWidth, "1", padding, 1)[0]);
|
|
902
|
+
if (blocks.length === 0) continue;
|
|
903
|
+
rendered.push({
|
|
904
|
+
children: [blocks],
|
|
905
|
+
layout: "1",
|
|
906
|
+
extra: group.extra
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
if (rendered.length === 0) return [];
|
|
910
|
+
const seam = rendered.length > 1 && hasPadding(padding) ? SEAM_NOTE : void 0;
|
|
911
|
+
return rendered.map((group, index) => {
|
|
912
|
+
const notes = [
|
|
913
|
+
...paint.notes,
|
|
914
|
+
group.extra,
|
|
915
|
+
image,
|
|
916
|
+
seam
|
|
917
|
+
].filter((note) => Boolean(note));
|
|
918
|
+
pushEntry(ctx, token, "section", notes.length > 0 ? "approximated" : "converted", notes.length > 0 ? notes.join(" ") : void 0);
|
|
919
|
+
return withBackground(createSectionBlock({
|
|
920
|
+
columns: group.layout,
|
|
921
|
+
children: group.children,
|
|
922
|
+
styles: { padding: splitPadding(padding, rendered.length, index) },
|
|
923
|
+
...paint.wrapperColor ? { wrapper: { backgroundColor: paint.wrapperColor } } : {}
|
|
924
|
+
}), paint.backgroundColor);
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
function sectionsFromStripe($stripe, $, ctx) {
|
|
928
|
+
if (isHidden($stripe)) return [];
|
|
929
|
+
const token = stripeToken($stripe) ?? "es-content";
|
|
930
|
+
const $body = $stripe.find(BODY_SELECTOR$1).first();
|
|
931
|
+
const rows = $body.length > 0 ? directRows($body, $) : directRows($stripe, $);
|
|
932
|
+
const paint = resolveSectionPaint(ctx.pageBackground ?? "#ffffff", bandColor$1($stripe), $body.length > 0 ? paintOf($body) : "");
|
|
933
|
+
const image = imageNote$1($stripe.attr("style"), directCell($stripe).attr("style"), $body.attr("style"));
|
|
934
|
+
const $width = $body.length > 0 ? $body : $stripe;
|
|
935
|
+
const bodyWidth = emailBodyWidth($width.attr("width"), $width.attr("style"));
|
|
936
|
+
const out = [];
|
|
937
|
+
for (const $row of rows) out.push(...sectionsFromRow($row, $, ctx, token, paint, image, bodyWidth));
|
|
366
938
|
return out;
|
|
367
939
|
}
|
|
368
940
|
function convertCompiled(html, ctx) {
|
|
369
941
|
const $ = load(html);
|
|
370
942
|
const stripes = topStripes($);
|
|
371
|
-
if (stripes.length === 0) return
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const { layout, flattenExtra } = columnLayout$1(cols.length || 1, ctx, token);
|
|
376
|
-
let children;
|
|
377
|
-
if (cols.length <= 1) children = [convertSubtree($stripe, $, ctx)];
|
|
378
|
-
else if (flattenExtra) {
|
|
379
|
-
const slots = cols.slice(0, 2).map(($c) => convertSubtree($c, $, ctx));
|
|
380
|
-
const rest = [];
|
|
381
|
-
for (const $c of cols.slice(2)) rest.push(...convertSubtree($c, $, ctx));
|
|
382
|
-
slots.push(rest);
|
|
383
|
-
children = slots;
|
|
384
|
-
} else children = cols.map(($c) => convertSubtree($c, $, ctx));
|
|
385
|
-
if (!flattenExtra) pushEntry(ctx, token, "section", "converted");
|
|
386
|
-
return withBackground(createSectionBlock({
|
|
387
|
-
columns: layout,
|
|
388
|
-
children
|
|
389
|
-
}), stripeFill($stripe));
|
|
390
|
-
});
|
|
943
|
+
if (stripes.length === 0) return convertRegion($.root().contents().toArray(), $, ctx);
|
|
944
|
+
const out = [];
|
|
945
|
+
for (const $stripe of stripes) out.push(...sectionsFromStripe($stripe, $, ctx));
|
|
946
|
+
return out;
|
|
391
947
|
}
|
|
392
948
|
//#endregion
|
|
393
949
|
//#region src/detect.ts
|
|
@@ -475,23 +1031,34 @@ function isStripoHtml(html) {
|
|
|
475
1031
|
}
|
|
476
1032
|
//#endregion
|
|
477
1033
|
//#region src/editor.ts
|
|
1034
|
+
const ZERO = {
|
|
1035
|
+
top: 0,
|
|
1036
|
+
right: 0,
|
|
1037
|
+
bottom: 0,
|
|
1038
|
+
left: 0
|
|
1039
|
+
};
|
|
1040
|
+
const BODY_SELECTOR = "[class~='es-header-body'], [class~='es-content-body'], [class~='es-footer-body']";
|
|
1041
|
+
const IMAGE_NOTE = "A background image was not imported.";
|
|
478
1042
|
function columnLayout(count, ctx) {
|
|
479
|
-
if (count <= 1) return "1";
|
|
480
|
-
if (count === 2) return "2";
|
|
481
|
-
if (count === 3) return "3";
|
|
482
|
-
|
|
483
|
-
ctx.warnings.push(
|
|
484
|
-
return
|
|
1043
|
+
if (count <= 1) return { layout: "1" };
|
|
1044
|
+
if (count === 2) return { layout: "2" };
|
|
1045
|
+
if (count === 3) return { layout: "3" };
|
|
1046
|
+
const flattenNote = `Row with ${count} columns was flattened to a single column. Templatical supports up to 3 columns per section.`;
|
|
1047
|
+
ctx.warnings.push(flattenNote);
|
|
1048
|
+
return {
|
|
1049
|
+
layout: "1",
|
|
1050
|
+
flattenNote
|
|
1051
|
+
};
|
|
485
1052
|
}
|
|
486
1053
|
function esdBlockKind($el) {
|
|
487
1054
|
const token = tokenStartingWith($el, "esd-block-");
|
|
488
1055
|
return token ? token.slice(10) : void 0;
|
|
489
1056
|
}
|
|
490
|
-
function convertEsdBlock($el, $, ctx) {
|
|
1057
|
+
function convertEsdBlock($el, $, ctx, room) {
|
|
491
1058
|
const kind = esdBlockKind($el);
|
|
492
1059
|
if (!kind) return labelledInnerBlocks($el, $, ctx);
|
|
493
1060
|
if (kind === "button") return [buttonFrom($el, "esd-block-button", ctx)];
|
|
494
|
-
if (kind === "spacer") return [spacerFrom($el, "esd-block-spacer", ctx)];
|
|
1061
|
+
if (kind === "spacer") return [spacerFrom($el, "esd-block-spacer", ctx, $, room)];
|
|
495
1062
|
if (kind === "social") return [socialFrom($el, $, "esd-block-social", ctx)];
|
|
496
1063
|
if (kind === "menu") {
|
|
497
1064
|
const menu = menuFrom($el, $, "esd-block-menu", ctx);
|
|
@@ -505,7 +1072,7 @@ function convertEsdBlock($el, $, ctx) {
|
|
|
505
1072
|
pushEntry(ctx, `esd-block-${kind}`, kind === "image" ? "image" : "paragraph", "converted");
|
|
506
1073
|
return labelledInnerBlocks($el, $, ctx);
|
|
507
1074
|
}
|
|
508
|
-
function blocksInContainer($frame, $, ctx) {
|
|
1075
|
+
function blocksInContainer($frame, $, ctx, room) {
|
|
509
1076
|
const out = [];
|
|
510
1077
|
const seen = /* @__PURE__ */ new Set();
|
|
511
1078
|
$frame.find("[class]").each((_, node) => {
|
|
@@ -514,11 +1081,33 @@ function blocksInContainer($frame, $, ctx) {
|
|
|
514
1081
|
if ($el.parents().toArray().some((p) => tokenStartingWith($(p), "esd-block-"))) return;
|
|
515
1082
|
if (seen.has(node)) return;
|
|
516
1083
|
seen.add(node);
|
|
517
|
-
out.push(...convertEsdBlock($el, $, ctx));
|
|
1084
|
+
out.push(...convertEsdBlock($el, $, ctx, room));
|
|
518
1085
|
});
|
|
519
1086
|
if (out.length === 0) return labelledInnerBlocks($frame, $, ctx);
|
|
520
1087
|
return out;
|
|
521
1088
|
}
|
|
1089
|
+
function enclosing($el, selector) {
|
|
1090
|
+
if ($el.is(selector)) return $el;
|
|
1091
|
+
const $parent = $el.parents(selector).first();
|
|
1092
|
+
return $parent.length > 0 ? $parent : null;
|
|
1093
|
+
}
|
|
1094
|
+
function bandColor($stripe) {
|
|
1095
|
+
const own = colorFromPaint($stripe.attr("style"), $stripe.attr("bgcolor"));
|
|
1096
|
+
if (own) return own;
|
|
1097
|
+
if ($stripe.is("td") || $stripe.is("th")) return "";
|
|
1098
|
+
const $direct = $stripe.children("tr").children("td, th");
|
|
1099
|
+
const $nested = $stripe.children("tbody, thead").children("tr").children("td, th");
|
|
1100
|
+
const $cell = ($direct.length > 0 ? $direct : $nested).first();
|
|
1101
|
+
return $cell.length > 0 ? colorFromPaint($cell.attr("style"), $cell.attr("bgcolor")) : "";
|
|
1102
|
+
}
|
|
1103
|
+
function paintFor($el, ctx) {
|
|
1104
|
+
const $stripe = enclosing($el, "[class~='esd-stripe']");
|
|
1105
|
+
const $body = enclosing($el, BODY_SELECTOR);
|
|
1106
|
+
return resolveSectionPaint(ctx.pageBackground ?? "#ffffff", $stripe ? bandColor($stripe) : "", $body ? colorFromPaint($body.attr("style"), $body.attr("bgcolor")) : "");
|
|
1107
|
+
}
|
|
1108
|
+
function imageNote(...styles) {
|
|
1109
|
+
return styles.some((style) => hasBackgroundImage(style)) ? IMAGE_NOTE : void 0;
|
|
1110
|
+
}
|
|
522
1111
|
function sectionFromStructure($structure, $, ctx) {
|
|
523
1112
|
const frames = $structure.find("[class~='esd-container-frame']").toArray().map((n) => $(n)).filter(($el) => {
|
|
524
1113
|
if (isHidden($el)) return false;
|
|
@@ -526,19 +1115,34 @@ function sectionFromStructure($structure, $, ctx) {
|
|
|
526
1115
|
return $(p).closest($structure).length > 0;
|
|
527
1116
|
}).length === 0;
|
|
528
1117
|
});
|
|
529
|
-
const
|
|
1118
|
+
const frameCount = frames.length;
|
|
1119
|
+
const { layout, flattenNote } = columnLayout(frameCount === 0 ? 1 : frameCount, ctx);
|
|
1120
|
+
const padding = readBoxPadding($structure.attr("class"), $structure.attr("style"));
|
|
1121
|
+
const $body = enclosing($structure, BODY_SELECTOR);
|
|
1122
|
+
const rooms = columnRooms(emailBodyWidth($body?.attr("width"), $body?.attr("style")), layout, padding, layout === "1" ? 1 : Math.min(frameCount, 3));
|
|
530
1123
|
let children;
|
|
531
1124
|
if (layout === "1") {
|
|
532
1125
|
const merged = [];
|
|
533
|
-
|
|
534
|
-
|
|
1126
|
+
const room = rooms[0];
|
|
1127
|
+
if (frames.length === 0) merged.push(...blocksInContainer($structure, $, ctx, room));
|
|
1128
|
+
else for (const $f of frames) merged.push(...blocksInContainer($f, $, ctx, room));
|
|
535
1129
|
children = [merged];
|
|
536
|
-
} else children = frames.map(($f) => blocksInContainer($f, $, ctx));
|
|
537
|
-
|
|
1130
|
+
} else children = frames.map(($f, index) => blocksInContainer($f, $, ctx, rooms[index]));
|
|
1131
|
+
const paint = paintFor($structure, ctx);
|
|
1132
|
+
const $stripe = enclosing($structure, "[class~='esd-stripe']");
|
|
1133
|
+
const notes = [
|
|
1134
|
+
...paint.notes,
|
|
1135
|
+
flattenNote,
|
|
1136
|
+
imageNote($stripe?.attr("style"), $body?.attr("style"))
|
|
1137
|
+
].filter((note) => Boolean(note));
|
|
1138
|
+
if (notes.length > 0) pushEntry(ctx, "esd-structure", "section", "approximated", notes.join(" "));
|
|
1139
|
+
else if (frameCount <= 3) pushEntry(ctx, "esd-structure", "section", "converted");
|
|
538
1140
|
return withBackground(createSectionBlock({
|
|
539
1141
|
columns: layout,
|
|
540
|
-
children
|
|
541
|
-
|
|
1142
|
+
children,
|
|
1143
|
+
styles: { padding },
|
|
1144
|
+
...paint.wrapperColor ? { wrapper: { backgroundColor: paint.wrapperColor } } : {}
|
|
1145
|
+
}), paint.backgroundColor);
|
|
542
1146
|
}
|
|
543
1147
|
function convertEditor(html, ctx) {
|
|
544
1148
|
const $ = load(html);
|
|
@@ -549,11 +1153,15 @@ function convertEditor(html, ctx) {
|
|
|
549
1153
|
return stripes.map(($stripe) => {
|
|
550
1154
|
const inner = $stripe.find("[class~='esd-structure']");
|
|
551
1155
|
if (inner.length > 0) return sectionFromStructure(inner.first(), $, ctx);
|
|
552
|
-
|
|
1156
|
+
const paint = paintFor($stripe, ctx);
|
|
1157
|
+
const notes = [...paint.notes, imageNote($stripe.attr("style"))].filter((note) => Boolean(note));
|
|
1158
|
+
pushEntry(ctx, "esd-stripe", "section", notes.length > 0 ? "approximated" : "converted", notes.length > 0 ? notes.join(" ") : void 0);
|
|
553
1159
|
return withBackground(createSectionBlock({
|
|
554
1160
|
columns: "1",
|
|
555
|
-
children: [blocksInContainer($stripe, $, ctx)]
|
|
556
|
-
|
|
1161
|
+
children: [blocksInContainer($stripe, $, ctx, columnRooms(600, "1", ZERO, 1)[0])],
|
|
1162
|
+
styles: { padding: { ...ZERO } },
|
|
1163
|
+
...paint.wrapperColor ? { wrapper: { backgroundColor: paint.wrapperColor } } : {}
|
|
1164
|
+
}), paint.backgroundColor);
|
|
557
1165
|
});
|
|
558
1166
|
}
|
|
559
1167
|
//#endregion
|
|
@@ -567,6 +1175,16 @@ function injectCss(html, css) {
|
|
|
567
1175
|
$("head").append($style);
|
|
568
1176
|
return $.html() ?? html;
|
|
569
1177
|
}
|
|
1178
|
+
/** First painted `es-wrapper-color` / `es-wrapper`, or white when neither is set. */
|
|
1179
|
+
function pageBackground($) {
|
|
1180
|
+
const nodes = $("[class~='es-wrapper-color'], [class~='es-wrapper']").toArray();
|
|
1181
|
+
for (const node of nodes) {
|
|
1182
|
+
const $el = $(node);
|
|
1183
|
+
const color = colorFromPaint($el.attr("style"), $el.attr("bgcolor"));
|
|
1184
|
+
if (color) return color;
|
|
1185
|
+
}
|
|
1186
|
+
return "#ffffff";
|
|
1187
|
+
}
|
|
570
1188
|
function summarize(ctx) {
|
|
571
1189
|
const summary = {
|
|
572
1190
|
total: ctx.entries.length,
|
|
@@ -609,14 +1227,23 @@ function convertStripoTemplate(html, options) {
|
|
|
609
1227
|
};
|
|
610
1228
|
const ctx = {
|
|
611
1229
|
entries: [],
|
|
612
|
-
warnings: []
|
|
1230
|
+
warnings: [],
|
|
1231
|
+
css: options?.css
|
|
613
1232
|
};
|
|
1233
|
+
const pageBg = pageBackground(load(prepared));
|
|
1234
|
+
ctx.pageBackground = pageBg;
|
|
614
1235
|
const blocks = kind === "editor" ? convertEditor(prepared, ctx) : convertCompiled(prepared, ctx);
|
|
1236
|
+
const defaults = createDefaultTemplateContent();
|
|
615
1237
|
return {
|
|
616
1238
|
content: {
|
|
617
|
-
...
|
|
1239
|
+
...defaults,
|
|
618
1240
|
...fallback.content,
|
|
619
|
-
blocks
|
|
1241
|
+
blocks,
|
|
1242
|
+
settings: {
|
|
1243
|
+
...defaults.settings,
|
|
1244
|
+
...fallback.content.settings,
|
|
1245
|
+
backgroundColor: pageBg
|
|
1246
|
+
}
|
|
620
1247
|
},
|
|
621
1248
|
report: summarize(ctx)
|
|
622
1249
|
};
|