@templatical/import-stripo 0.46.0 → 0.48.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.js +614 -43
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -62,10 +62,15 @@ const SIDE_LETTER = {
|
|
|
62
62
|
b: "bottom",
|
|
63
63
|
l: "left"
|
|
64
64
|
};
|
|
65
|
+
/**
|
|
66
|
+
* A `style` attribute as a key/value map. A semicolon inside quotes or
|
|
67
|
+
* parentheses — `url("a;b")`, `url(data:image/png;base64,…)` — belongs to
|
|
68
|
+
* its value.
|
|
69
|
+
*/
|
|
65
70
|
function parseStyleAttribute(styleAttr) {
|
|
66
71
|
const result = {};
|
|
67
72
|
if (!styleAttr) return result;
|
|
68
|
-
for (const decl of styleAttr
|
|
73
|
+
for (const decl of splitTopLevel(styleAttr, ";")) {
|
|
69
74
|
const idx = decl.indexOf(":");
|
|
70
75
|
if (idx === -1) continue;
|
|
71
76
|
const key = decl.slice(0, idx).trim().toLowerCase();
|
|
@@ -74,6 +79,35 @@ function parseStyleAttribute(styleAttr) {
|
|
|
74
79
|
}
|
|
75
80
|
return result;
|
|
76
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Split `value` at each `separator` that sits outside quotes and parentheses.
|
|
84
|
+
* A value whose quote or parenthesis never closes is split at every
|
|
85
|
+
* `separator` instead, so a malformed declaration costs only itself and the
|
|
86
|
+
* ones after it still read.
|
|
87
|
+
*/
|
|
88
|
+
function splitTopLevel(value, separator) {
|
|
89
|
+
const parts = [];
|
|
90
|
+
let depth = 0;
|
|
91
|
+
let quote = "";
|
|
92
|
+
let start = 0;
|
|
93
|
+
for (let i = 0; i < value.length; i++) {
|
|
94
|
+
const char = value[i];
|
|
95
|
+
if (quote) {
|
|
96
|
+
if (char === quote) quote = "";
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (char === "\"" || char === "'") quote = char;
|
|
100
|
+
else if (char === "(") depth++;
|
|
101
|
+
else if (char === ")") depth = Math.max(0, depth - 1);
|
|
102
|
+
else if (char === separator && depth === 0) {
|
|
103
|
+
parts.push(value.slice(start, i));
|
|
104
|
+
start = i + 1;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (quote || depth > 0) return value.split(separator);
|
|
108
|
+
parts.push(value.slice(start));
|
|
109
|
+
return parts;
|
|
110
|
+
}
|
|
77
111
|
function channelHex(value) {
|
|
78
112
|
return Math.max(0, Math.min(255, Math.round(value))).toString(16).padStart(2, "0");
|
|
79
113
|
}
|
|
@@ -99,11 +133,6 @@ function colorFromPaint(styleAttr, bgcolor) {
|
|
|
99
133
|
const fromShorthand = styles.background ? parseColor(styles.background.trim().split(/\s+/)[0]) : "";
|
|
100
134
|
return parseColor(styles["background-color"]) || fromShorthand || parseColor(bgcolor) || "";
|
|
101
135
|
}
|
|
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
136
|
function parsePaddingShorthand(value) {
|
|
108
137
|
if (!value) return { ...ZERO$3 };
|
|
109
138
|
const values = value.trim().split(/\s+/).filter(Boolean).map((part) => parsePxValue(part));
|
|
@@ -302,6 +331,433 @@ function visibleLineBorder(styles) {
|
|
|
302
331
|
}
|
|
303
332
|
return null;
|
|
304
333
|
}
|
|
334
|
+
const X_GRID = [
|
|
335
|
+
"left",
|
|
336
|
+
"center",
|
|
337
|
+
"right"
|
|
338
|
+
];
|
|
339
|
+
const Y_GRID = [
|
|
340
|
+
"top",
|
|
341
|
+
"center",
|
|
342
|
+
"bottom"
|
|
343
|
+
];
|
|
344
|
+
const PERCENT = /^(-?(?:\d+(?:\.\d+)?|\.\d+))%$/;
|
|
345
|
+
const LENGTH = /^-?(?:\d+(?:\.\d+)?|\.\d+)[a-z]*$/;
|
|
346
|
+
/**
|
|
347
|
+
* `background-size` as a keyword. A size with a side at 100%, or a px width
|
|
348
|
+
* spanning the box, fills it as `cover` does; any other length or percentage
|
|
349
|
+
* draws the image at a size of its own, which `auto` is closest to.
|
|
350
|
+
*/
|
|
351
|
+
function backgroundSizeKeyword(raw, boxWidth) {
|
|
352
|
+
const parts = raw.toLowerCase().split(/\s+/);
|
|
353
|
+
const [first] = parts;
|
|
354
|
+
if (parts.length === 1 && (first === "cover" || first === "contain")) return {
|
|
355
|
+
size: first,
|
|
356
|
+
exact: true
|
|
357
|
+
};
|
|
358
|
+
if (parts.every((part) => part === "auto")) return {
|
|
359
|
+
size: "auto",
|
|
360
|
+
exact: true
|
|
361
|
+
};
|
|
362
|
+
const px = /^(\d+(?:\.\d+)?|\.\d+)px$/.exec(first);
|
|
363
|
+
return {
|
|
364
|
+
size: parts.includes("100%") || px !== null && boxWidth !== void 0 && Number(px[1]) >= boxWidth ? "cover" : "auto",
|
|
365
|
+
exact: false
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* `background-position` split into x and y the way MJML and CSS read it: a
|
|
370
|
+
* lone `top` / `bottom` names y and centres x, any other lone value is x
|
|
371
|
+
* with y centred, and a pair is x then y unless it leads with `top` /
|
|
372
|
+
* `bottom` (or with `center` before `left` / `right`). Three- and four-value
|
|
373
|
+
* edge offsets keep their edge keywords only.
|
|
374
|
+
*/
|
|
375
|
+
function splitBackgroundPosition(raw) {
|
|
376
|
+
const parts = raw.toLowerCase().split(/\s+/);
|
|
377
|
+
if (parts.length === 1) {
|
|
378
|
+
const [only] = parts;
|
|
379
|
+
return only === "top" || only === "bottom" ? {
|
|
380
|
+
x: "center",
|
|
381
|
+
y: only,
|
|
382
|
+
exact: true
|
|
383
|
+
} : {
|
|
384
|
+
x: only,
|
|
385
|
+
y: "center",
|
|
386
|
+
exact: true
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
if (parts.length === 2) {
|
|
390
|
+
const [a, b] = parts;
|
|
391
|
+
return a === "top" || a === "bottom" || a === "center" && (b === "left" || b === "right") ? {
|
|
392
|
+
x: b,
|
|
393
|
+
y: a,
|
|
394
|
+
exact: true
|
|
395
|
+
} : {
|
|
396
|
+
x: a,
|
|
397
|
+
y: b,
|
|
398
|
+
exact: true
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
x: parts.find((part) => part === "left" || part === "right") ?? "center",
|
|
403
|
+
y: parts.find((part) => part === "top" || part === "bottom") ?? "center",
|
|
404
|
+
exact: false
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* One axis as a grid point: its keyword; the point nearest a percentage,
|
|
409
|
+
* ties going to the centre; or the start edge for a length, which is an
|
|
410
|
+
* offset from it. `null` when the token is unreadable.
|
|
411
|
+
*/
|
|
412
|
+
function backgroundAxisPoint(token, grid) {
|
|
413
|
+
const keyword = grid.find((point) => point === token);
|
|
414
|
+
if (keyword) return {
|
|
415
|
+
point: keyword,
|
|
416
|
+
exact: true
|
|
417
|
+
};
|
|
418
|
+
const percent = PERCENT.exec(token);
|
|
419
|
+
if (percent) {
|
|
420
|
+
const n = Number(percent[1]);
|
|
421
|
+
return {
|
|
422
|
+
point: n < 25 ? grid[0] : n > 75 ? grid[2] : grid[1],
|
|
423
|
+
exact: n === 0 || n === 50 || n === 100
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
if (LENGTH.test(token)) return {
|
|
427
|
+
point: grid[0],
|
|
428
|
+
exact: parseFloat(token) === 0
|
|
429
|
+
};
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* `background-repeat`: unrepeated only when every axis is `no-repeat`.
|
|
434
|
+
* Anything but plain `repeat` tiles too, inexactly.
|
|
435
|
+
*/
|
|
436
|
+
function backgroundRepeatFlag(raw) {
|
|
437
|
+
const parts = raw.toLowerCase().split(/\s+/);
|
|
438
|
+
if (parts.every((part) => part === "no-repeat")) return {
|
|
439
|
+
repeat: false,
|
|
440
|
+
exact: true
|
|
441
|
+
};
|
|
442
|
+
return {
|
|
443
|
+
repeat: true,
|
|
444
|
+
exact: parts.every((part) => part === "repeat")
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
/** CSS's initial values: what a browser paints for a longhand the source leaves out. */
|
|
448
|
+
const CSS_BACKGROUND_DEFAULTS = {
|
|
449
|
+
size: "auto",
|
|
450
|
+
x: "left",
|
|
451
|
+
y: "top",
|
|
452
|
+
repeat: true
|
|
453
|
+
};
|
|
454
|
+
const IMAGE_FUNCTION = /^(?:url|[a-z-]*gradient|image-set)\(/;
|
|
455
|
+
const REPEAT_TOKENS = /* @__PURE__ */ new Set([
|
|
456
|
+
"repeat",
|
|
457
|
+
"no-repeat",
|
|
458
|
+
"repeat-x",
|
|
459
|
+
"repeat-y",
|
|
460
|
+
"round",
|
|
461
|
+
"space"
|
|
462
|
+
]);
|
|
463
|
+
const POSITION_TOKENS = /* @__PURE__ */ new Set([
|
|
464
|
+
"left",
|
|
465
|
+
"right",
|
|
466
|
+
"top",
|
|
467
|
+
"bottom",
|
|
468
|
+
"center"
|
|
469
|
+
]);
|
|
470
|
+
const NON_COLOR_TOKENS = /* @__PURE__ */ new Set([
|
|
471
|
+
"none",
|
|
472
|
+
"scroll",
|
|
473
|
+
"fixed",
|
|
474
|
+
"local",
|
|
475
|
+
"border-box",
|
|
476
|
+
"padding-box",
|
|
477
|
+
"content-box",
|
|
478
|
+
"text",
|
|
479
|
+
"cover",
|
|
480
|
+
"contain",
|
|
481
|
+
"auto"
|
|
482
|
+
]);
|
|
483
|
+
const MEASURE = /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:%|[a-z]+)?$/i;
|
|
484
|
+
/**
|
|
485
|
+
* The URL inside the first CSS `url(…)`, unquoted and trimmed; "" when there
|
|
486
|
+
* is none or it is malformed. A forward scan rather than a regex, so a long
|
|
487
|
+
* run of unclosed `url(` can't make it backtrack.
|
|
488
|
+
*/
|
|
489
|
+
function cssUrl(value) {
|
|
490
|
+
const text = value ?? "";
|
|
491
|
+
const start = text.search(/url\(/i);
|
|
492
|
+
if (start === -1) return "";
|
|
493
|
+
let i = start + 4;
|
|
494
|
+
while (i < text.length && /\s/.test(text[i])) i++;
|
|
495
|
+
const quote = text[i];
|
|
496
|
+
if (quote === "\"" || quote === "'") {
|
|
497
|
+
const close = text.indexOf(quote, i + 1);
|
|
498
|
+
if (close === -1) return "";
|
|
499
|
+
let j = close + 1;
|
|
500
|
+
while (j < text.length && /\s/.test(text[j])) j++;
|
|
501
|
+
return text[j] === ")" ? text.slice(i + 1, close).trim() : "";
|
|
502
|
+
}
|
|
503
|
+
const close = text.indexOf(")", i);
|
|
504
|
+
if (close === -1) return "";
|
|
505
|
+
const inner = text.slice(i, close);
|
|
506
|
+
return /["']/.test(inner) ? "" : inner.trim();
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Whitespace-separated tokens, keeping a function's arguments together and
|
|
510
|
+
* making the `/` between position and size a token of its own.
|
|
511
|
+
*/
|
|
512
|
+
function cssTokens(value) {
|
|
513
|
+
const tokens = [];
|
|
514
|
+
let depth = 0;
|
|
515
|
+
let quote = "";
|
|
516
|
+
let current = "";
|
|
517
|
+
const push = () => {
|
|
518
|
+
if (current) tokens.push(current);
|
|
519
|
+
current = "";
|
|
520
|
+
};
|
|
521
|
+
for (const char of value) {
|
|
522
|
+
if (quote) {
|
|
523
|
+
current += char;
|
|
524
|
+
if (char === quote) quote = "";
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
if (char === "\"" || char === "'") {
|
|
528
|
+
quote = char;
|
|
529
|
+
current += char;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
if (char === "(") depth++;
|
|
533
|
+
if (char === ")") depth = Math.max(0, depth - 1);
|
|
534
|
+
if (depth === 0 && /\s/.test(char)) {
|
|
535
|
+
push();
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
if (depth === 0 && char === "/") {
|
|
539
|
+
push();
|
|
540
|
+
tokens.push("/");
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
current += char;
|
|
544
|
+
}
|
|
545
|
+
push();
|
|
546
|
+
return tokens;
|
|
547
|
+
}
|
|
548
|
+
function isColorToken(token) {
|
|
549
|
+
const lower = token.toLowerCase();
|
|
550
|
+
if (lower === "/" || IMAGE_FUNCTION.test(lower)) return false;
|
|
551
|
+
return !REPEAT_TOKENS.has(lower) && !POSITION_TOKENS.has(lower) && !NON_COLOR_TOKENS.has(lower) && !MEASURE.test(lower);
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* The parts of a `background` shorthand. The colour comes from the last
|
|
555
|
+
* layer, the only one CSS lets carry it; the image and its size, position
|
|
556
|
+
* and repeat come from the first layer with a `url()`.
|
|
557
|
+
*/
|
|
558
|
+
function parseBackgroundShorthand(value) {
|
|
559
|
+
const layers = splitTopLevel(value, ",").map((layer) => layer.trim()).filter(Boolean);
|
|
560
|
+
const color = cssTokens(layers[layers.length - 1] ?? "").find(isColorToken) ?? "";
|
|
561
|
+
const imageLayer = layers.find((layer) => cssUrl(layer) !== "");
|
|
562
|
+
if (!imageLayer) return {
|
|
563
|
+
color,
|
|
564
|
+
url: "",
|
|
565
|
+
size: "",
|
|
566
|
+
position: "",
|
|
567
|
+
repeat: "",
|
|
568
|
+
layers: layers.length
|
|
569
|
+
};
|
|
570
|
+
const tokens = cssTokens(imageLayer);
|
|
571
|
+
const slash = tokens.indexOf("/");
|
|
572
|
+
const before = slash === -1 ? tokens : tokens.slice(0, slash);
|
|
573
|
+
const after = slash === -1 ? [] : tokens.slice(slash + 1);
|
|
574
|
+
const size = [];
|
|
575
|
+
for (const token of after) {
|
|
576
|
+
const lower = token.toLowerCase();
|
|
577
|
+
const isSize = lower === "cover" || lower === "contain" || lower === "auto" || MEASURE.test(lower);
|
|
578
|
+
if (size.length === 2 || !isSize) break;
|
|
579
|
+
size.push(token);
|
|
580
|
+
}
|
|
581
|
+
const rest = [...before, ...after.slice(size.length)];
|
|
582
|
+
return {
|
|
583
|
+
color,
|
|
584
|
+
url: cssUrl(imageLayer),
|
|
585
|
+
size: size.join(" "),
|
|
586
|
+
position: before.filter((token) => POSITION_TOKENS.has(token.toLowerCase()) || MEASURE.test(token)).join(" "),
|
|
587
|
+
repeat: rest.filter((token) => REPEAT_TOKENS.has(token.toLowerCase())).join(" "),
|
|
588
|
+
layers: layers.length
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* A background image from a URL and its longhands. A longhand left out takes
|
|
593
|
+
* CSS's initial value; a value with no exact keyword takes the nearest one
|
|
594
|
+
* and adds a note naming it. `null` for a blank URL.
|
|
595
|
+
*/
|
|
596
|
+
function buildBackgroundImage(url, longhands, label, boxWidth) {
|
|
597
|
+
const trimmedUrl = url.trim();
|
|
598
|
+
if (!trimmedUrl) return null;
|
|
599
|
+
const notes = [];
|
|
600
|
+
const lossy = (name, raw, shown) => {
|
|
601
|
+
notes.push(`${label} ${name} "${raw}" has no exact Templatical equivalent; imported as "${shown}".`);
|
|
602
|
+
};
|
|
603
|
+
let size = CSS_BACKGROUND_DEFAULTS.size;
|
|
604
|
+
const rawSize = longhands.size?.trim();
|
|
605
|
+
if (rawSize) {
|
|
606
|
+
const parsed = backgroundSizeKeyword(rawSize, boxWidth);
|
|
607
|
+
size = parsed.size;
|
|
608
|
+
if (!parsed.exact) lossy("background-size", rawSize, size);
|
|
609
|
+
}
|
|
610
|
+
let x = CSS_BACKGROUND_DEFAULTS.x;
|
|
611
|
+
let y = CSS_BACKGROUND_DEFAULTS.y;
|
|
612
|
+
let lossyX = false;
|
|
613
|
+
let lossyY = false;
|
|
614
|
+
const rawPosition = longhands.position?.trim();
|
|
615
|
+
if (rawPosition) {
|
|
616
|
+
const split = splitBackgroundPosition(rawPosition);
|
|
617
|
+
const readX = backgroundAxisPoint(split.x, X_GRID);
|
|
618
|
+
const readY = backgroundAxisPoint(split.y, Y_GRID);
|
|
619
|
+
x = readX?.point ?? x;
|
|
620
|
+
y = readY?.point ?? y;
|
|
621
|
+
lossyX = !split.exact || !readX?.exact;
|
|
622
|
+
lossyY = !split.exact || !readY?.exact;
|
|
623
|
+
}
|
|
624
|
+
const rawX = longhands.positionX?.trim();
|
|
625
|
+
const overrideX = rawX ? backgroundAxisPoint(rawX.toLowerCase(), X_GRID) : null;
|
|
626
|
+
if (overrideX) {
|
|
627
|
+
x = overrideX.point;
|
|
628
|
+
lossyX = false;
|
|
629
|
+
}
|
|
630
|
+
const rawY = longhands.positionY?.trim();
|
|
631
|
+
const overrideY = rawY ? backgroundAxisPoint(rawY.toLowerCase(), Y_GRID) : null;
|
|
632
|
+
if (overrideY) {
|
|
633
|
+
y = overrideY.point;
|
|
634
|
+
lossyY = false;
|
|
635
|
+
}
|
|
636
|
+
if (rawPosition && (lossyX || lossyY)) lossy("background-position", rawPosition, `${x} ${y}`);
|
|
637
|
+
if (rawX && !overrideX?.exact) lossy("background-position-x", rawX, x);
|
|
638
|
+
if (rawY && !overrideY?.exact) lossy("background-position-y", rawY, y);
|
|
639
|
+
let repeat = CSS_BACKGROUND_DEFAULTS.repeat;
|
|
640
|
+
const rawRepeat = longhands.repeat?.trim();
|
|
641
|
+
if (rawRepeat) {
|
|
642
|
+
const parsed = backgroundRepeatFlag(rawRepeat);
|
|
643
|
+
repeat = parsed.repeat;
|
|
644
|
+
if (!parsed.exact) lossy("background-repeat", rawRepeat, repeat ? "repeat" : "no-repeat");
|
|
645
|
+
}
|
|
646
|
+
return {
|
|
647
|
+
image: {
|
|
648
|
+
url: trimmedUrl,
|
|
649
|
+
size,
|
|
650
|
+
position: {
|
|
651
|
+
x,
|
|
652
|
+
y
|
|
653
|
+
},
|
|
654
|
+
repeat
|
|
655
|
+
},
|
|
656
|
+
notes
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* A background image from CSS declarations: `background-image`, else the
|
|
661
|
+
* `background` shorthand's image, with the size, position and repeat
|
|
662
|
+
* longhands winning over the shorthand's. A style map keeps no declaration
|
|
663
|
+
* order, so a longhand is taken to follow the shorthand, as it usually does.
|
|
664
|
+
* Of several layers the first image is kept, and a note says so. `null`
|
|
665
|
+
* when the declarations paint no image.
|
|
666
|
+
*/
|
|
667
|
+
function readCssBackground(styles, label, boxWidth) {
|
|
668
|
+
const shorthand = styles.background !== void 0 ? parseBackgroundShorthand(styles.background) : null;
|
|
669
|
+
const imageLayers = styles["background-image"] !== void 0 ? splitTopLevel(styles["background-image"], ",").map((layer) => layer.trim()) : null;
|
|
670
|
+
const index = imageLayers ? imageLayers.findIndex((layer) => cssUrl(layer) !== "") : -1;
|
|
671
|
+
const url = imageLayers ? index === -1 ? "" : cssUrl(imageLayers[index]) : shorthand?.url ?? "";
|
|
672
|
+
if (!url) return null;
|
|
673
|
+
const layer = (value) => {
|
|
674
|
+
if (value === void 0) return void 0;
|
|
675
|
+
const parts = splitTopLevel(value, ",").map((part) => part.trim());
|
|
676
|
+
return parts[Math.max(index, 0)] ?? parts[0];
|
|
677
|
+
};
|
|
678
|
+
const read = buildBackgroundImage(url, {
|
|
679
|
+
size: layer(styles["background-size"]) ?? shorthand?.size,
|
|
680
|
+
position: layer(styles["background-position"]) ?? shorthand?.position,
|
|
681
|
+
positionX: styles["background-position-x"],
|
|
682
|
+
positionY: styles["background-position-y"],
|
|
683
|
+
repeat: layer(styles["background-repeat"]) ?? shorthand?.repeat
|
|
684
|
+
}, label, boxWidth);
|
|
685
|
+
const layers = imageLayers?.length ?? shorthand?.layers ?? 1;
|
|
686
|
+
if (read && layers > 1) read.notes.unshift(`${label} background has ${layers} layers; only the image "${url}" was imported.`);
|
|
687
|
+
return read;
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* The image an element paints: its inline CSS background, else its legacy
|
|
691
|
+
* `background` attribute. A `background-image` or `background` declaration
|
|
692
|
+
* overrides the attribute; a `background-color` doesn't. The image twin of
|
|
693
|
+
* `colorFromPaint`.
|
|
694
|
+
*/
|
|
695
|
+
function imageFromPaint(styleAttr, backgroundAttr, label, boxWidth) {
|
|
696
|
+
const styles = parseStyleAttribute(styleAttr);
|
|
697
|
+
if (styles["background-image"] !== void 0 || styles.background !== void 0) return readCssBackground(styles, label, boxWidth);
|
|
698
|
+
return buildBackgroundImage(backgroundAttr ?? "", {}, label, boxWidth);
|
|
699
|
+
}
|
|
700
|
+
function sameImage(a, b) {
|
|
701
|
+
return a.url === b.url && a.size === b.size && a.repeat === b.repeat && a.position.x === b.position.x && a.position.y === b.position.y;
|
|
702
|
+
}
|
|
703
|
+
function droppedColumnsNote(columns) {
|
|
704
|
+
return columns.length === 1 ? `The background image on column ${columns[0]} was dropped. A Templatical section has one background image.` : `Background images on columns ${columns.join(", ")} were dropped. A Templatical section has one background image.`;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* The image a section paints, nearest first: the structure's own, then the
|
|
708
|
+
* body's, then the containers', moved up when there is one or all share the
|
|
709
|
+
* same image, then the stripe's while the body leaves it showing. An image
|
|
710
|
+
* that showed in the section but can't be kept is named in a note. A body
|
|
711
|
+
* image beneath the structure's own is covered in this section, so it isn't
|
|
712
|
+
* named; it still paints the stripe's other sections. The image is a copy,
|
|
713
|
+
* so sections that share a source don't share an object.
|
|
714
|
+
*/
|
|
715
|
+
function resolveSectionImage(layers) {
|
|
716
|
+
const notes = [];
|
|
717
|
+
let picked = layers.structure ?? layers.body;
|
|
718
|
+
const withImage = layers.columns.flatMap((read, index) => read ? [index + 1] : []);
|
|
719
|
+
const first = layers.columns[0] ?? null;
|
|
720
|
+
const shared = first !== null && layers.columns.every((read) => read !== null && sameImage(read.image, first.image));
|
|
721
|
+
if (withImage.length > 0) {
|
|
722
|
+
if (!picked && shared) picked = {
|
|
723
|
+
read: first,
|
|
724
|
+
source: null
|
|
725
|
+
};
|
|
726
|
+
else notes.push(droppedColumnsNote(withImage));
|
|
727
|
+
}
|
|
728
|
+
if (layers.band) {
|
|
729
|
+
if (!picked && !layers.bodyPainted) {
|
|
730
|
+
picked = layers.band;
|
|
731
|
+
notes.push("The stripe's background image spans the full width in Stripo; the section paints it at the content width.");
|
|
732
|
+
} else notes.push("The stripe's background image outside the content width was dropped.");
|
|
733
|
+
}
|
|
734
|
+
if (!picked) return {
|
|
735
|
+
image: void 0,
|
|
736
|
+
notes,
|
|
737
|
+
source: null
|
|
738
|
+
};
|
|
739
|
+
return {
|
|
740
|
+
image: {
|
|
741
|
+
...picked.read.image,
|
|
742
|
+
position: { ...picked.read.image.position }
|
|
743
|
+
},
|
|
744
|
+
notes: [...picked.read.notes, ...notes],
|
|
745
|
+
source: picked.source
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* The paint, minus a band colour that would become the section's wrapper
|
|
750
|
+
* colour, when the section has a background image. The wrapper has no
|
|
751
|
+
* padding, so the colour shows nowhere except Outlook desktop, which paints
|
|
752
|
+
* it over the section's image.
|
|
753
|
+
*/
|
|
754
|
+
function withoutBandOverImage(paint, hasImage) {
|
|
755
|
+
if (!hasImage || !paint.wrapperColor) return paint;
|
|
756
|
+
return {
|
|
757
|
+
backgroundColor: paint.backgroundColor,
|
|
758
|
+
notes: [`The band colour ${paint.wrapperColor} was dropped: Outlook desktop paints a wrapper colour over the section's background image.`]
|
|
759
|
+
};
|
|
760
|
+
}
|
|
305
761
|
//#endregion
|
|
306
762
|
//#region src/fragment.ts
|
|
307
763
|
/** Paint after `createSectionBlock` so `styles` keeps its required padding. */
|
|
@@ -319,10 +775,15 @@ function fragmentDocument(inner, css) {
|
|
|
319
775
|
const safe = css ? css.replace(/<\/style/gi, "<\\/style") : "";
|
|
320
776
|
return `<!DOCTYPE html><html>${safe ? `<head><style>${safe}</style></head>` : ""}<body>${inner}</body></html>`;
|
|
321
777
|
}
|
|
778
|
+
/** The start of the HTML importer's page-image warning. */
|
|
779
|
+
const PAGE_IMAGE_WARNING = "The email background image ";
|
|
322
780
|
/**
|
|
323
781
|
* Run the generic HTML importer on a subtree and keep its blocks.
|
|
324
782
|
* Section entries belong to the row the HTML importer invented; Stripo's own
|
|
325
|
-
* section entries are recorded by the caller.
|
|
783
|
+
* section entries are recorded by the caller. A fragment's `<body>` is
|
|
784
|
+
* synthetic, so the page-image warning the HTML importer raises for a `body`
|
|
785
|
+
* background in the caller's CSS is dropped; Stripo reports the page image
|
|
786
|
+
* once, from `es-wrapper`.
|
|
326
787
|
*/
|
|
327
788
|
function blocksFromHtml(inner, ctx) {
|
|
328
789
|
const result = convertHtmlTemplate(fragmentDocument(inner, ctx.css));
|
|
@@ -330,16 +791,66 @@ function blocksFromHtml(inner, ctx) {
|
|
|
330
791
|
if (entry.templaticalBlockType === "section") continue;
|
|
331
792
|
ctx.entries.push(entry);
|
|
332
793
|
}
|
|
333
|
-
ctx.warnings.push(...result.report.warnings);
|
|
794
|
+
ctx.warnings.push(...result.report.warnings.filter((warning) => !warning.startsWith(PAGE_IMAGE_WARNING)));
|
|
334
795
|
return flattenBlocks(result.content.blocks);
|
|
335
796
|
}
|
|
336
797
|
function pushEntry(ctx, sourceTag, templaticalBlockType, status, note) {
|
|
337
|
-
|
|
798
|
+
const entry = {
|
|
338
799
|
sourceTag,
|
|
339
800
|
templaticalBlockType,
|
|
340
801
|
status,
|
|
341
802
|
...note ? { note } : {}
|
|
342
|
-
}
|
|
803
|
+
};
|
|
804
|
+
ctx.entries.push(entry);
|
|
805
|
+
return entry;
|
|
806
|
+
}
|
|
807
|
+
/** Record that a section's entry took the image `source` paints. */
|
|
808
|
+
function recordImageSpan(ctx, source, entry) {
|
|
809
|
+
if (!source) return;
|
|
810
|
+
ctx.imageSpans ??= /* @__PURE__ */ new Map();
|
|
811
|
+
const spanned = ctx.imageSpans.get(source) ?? [];
|
|
812
|
+
spanned.push(entry);
|
|
813
|
+
ctx.imageSpans.set(source, spanned);
|
|
814
|
+
}
|
|
815
|
+
/** Note on each entry whose section shares its image with another section. */
|
|
816
|
+
function noteImageSpans(ctx) {
|
|
817
|
+
for (const spanned of ctx.imageSpans?.values() ?? []) {
|
|
818
|
+
if (spanned.length < 2) continue;
|
|
819
|
+
const note = `A background image behind ${spanned.length} sections was applied to each of them — Templatical has no multi-section band.`;
|
|
820
|
+
for (const entry of spanned) {
|
|
821
|
+
entry.status = "approximated";
|
|
822
|
+
entry.note = entry.note ? `${entry.note} ${note}` : note;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
/** The image an element paints: its inline CSS background, else its `background` attribute. */
|
|
827
|
+
function imageOf($el, label, boxWidth) {
|
|
828
|
+
return imageFromPaint($el.attr("style"), $el.attr("background"), label, boxWidth);
|
|
829
|
+
}
|
|
830
|
+
/** An element's image with that element as its source; `null` when there is no element or it paints none. */
|
|
831
|
+
function imageLayer($el, label, boxWidth) {
|
|
832
|
+
if (!$el) return null;
|
|
833
|
+
const read = imageOf($el, label, boxWidth);
|
|
834
|
+
return read ? {
|
|
835
|
+
read,
|
|
836
|
+
source: $el[0]
|
|
837
|
+
} : null;
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* The stripe's image: on the stripe, else on `$cell`, the cell Stripo paints
|
|
841
|
+
* instead of a table stripe, as the band colour is read. A `td` / `th` stripe
|
|
842
|
+
* is its own paint. The stripe names the image either way, since it is the
|
|
843
|
+
* element that spans the sections.
|
|
844
|
+
*/
|
|
845
|
+
function bandImage($stripe, $cell, boxWidth) {
|
|
846
|
+
const own = imageLayer($stripe, "Stripe", boxWidth);
|
|
847
|
+
if (own) return own;
|
|
848
|
+
if ($stripe.is("td") || $stripe.is("th")) return null;
|
|
849
|
+
const cell = $cell.length > 0 ? imageOf($cell, "Stripe", boxWidth) : null;
|
|
850
|
+
return cell ? {
|
|
851
|
+
read: cell,
|
|
852
|
+
source: $stripe[0]
|
|
853
|
+
} : null;
|
|
343
854
|
}
|
|
344
855
|
//#endregion
|
|
345
856
|
//#region src/platform.ts
|
|
@@ -621,7 +1132,6 @@ const STRIPE_TOKENS = [
|
|
|
621
1132
|
const BODY_SELECTOR$1 = "[class~='es-header-body'], [class~='es-content-body'], [class~='es-footer-body']";
|
|
622
1133
|
const BETWEEN_NOTE = "Content between columns was placed after them.";
|
|
623
1134
|
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
1135
|
function paintOf($el) {
|
|
626
1136
|
return colorFromPaint($el.attr("style"), $el.attr("bgcolor"));
|
|
627
1137
|
}
|
|
@@ -656,8 +1166,12 @@ function bandColor$1($stripe) {
|
|
|
656
1166
|
const $cell = directCell($stripe);
|
|
657
1167
|
return $cell.length > 0 ? paintOf($cell) : "";
|
|
658
1168
|
}
|
|
659
|
-
|
|
660
|
-
|
|
1169
|
+
/** A column's image: on its `es-left` / `es-right` table, else on that table's cell. */
|
|
1170
|
+
function columnImage($column, boxWidth) {
|
|
1171
|
+
const own = imageOf($column, "Column", boxWidth);
|
|
1172
|
+
if (own) return own;
|
|
1173
|
+
const $cell = directCell($column);
|
|
1174
|
+
return $cell.length > 0 ? imageOf($cell, "Column", boxWidth) : null;
|
|
661
1175
|
}
|
|
662
1176
|
function hasPadding(padding) {
|
|
663
1177
|
return padding.top !== 0 || padding.right !== 0 || padding.bottom !== 0 || padding.left !== 0;
|
|
@@ -871,17 +1385,19 @@ function groupsFromPieces(pieces) {
|
|
|
871
1385
|
});
|
|
872
1386
|
return groups;
|
|
873
1387
|
}
|
|
874
|
-
function sectionsFromRow($row, $, ctx, token, paint,
|
|
1388
|
+
function sectionsFromRow($row, $, ctx, token, paint, images, bodyWidth) {
|
|
875
1389
|
if (isHidden($row)) return [];
|
|
876
1390
|
const pieces = [];
|
|
877
1391
|
let padding = { ...ZERO$1 };
|
|
878
1392
|
let sawCell = false;
|
|
1393
|
+
let $structure = null;
|
|
879
1394
|
$row.children("td, th").each((_, cell) => {
|
|
880
1395
|
const $cell = $(cell);
|
|
881
1396
|
if (isHidden($cell)) return;
|
|
882
1397
|
if (!sawCell) {
|
|
883
1398
|
padding = readBoxPadding($cell.attr("class"), $cell.attr("style"));
|
|
884
1399
|
sawCell = true;
|
|
1400
|
+
$structure = $cell;
|
|
885
1401
|
}
|
|
886
1402
|
pieces.push(...collectPieces($cell, $));
|
|
887
1403
|
});
|
|
@@ -889,12 +1405,14 @@ function sectionsFromRow($row, $, ctx, token, paint, image, bodyWidth) {
|
|
|
889
1405
|
const rendered = [];
|
|
890
1406
|
for (const group of groupsFromPieces(pieces)) {
|
|
891
1407
|
if (group.mode === "columns") {
|
|
892
|
-
const
|
|
1408
|
+
const cols = group.pieces.flatMap((piece) => piece.kind === "column" ? [piece.el] : []);
|
|
1409
|
+
const built = columnChildren(cols, $, ctx, bodyWidth, padding);
|
|
893
1410
|
if (!built) continue;
|
|
894
1411
|
rendered.push({
|
|
895
1412
|
children: built.children,
|
|
896
1413
|
layout: built.layout,
|
|
897
|
-
extra: built.flattenNote
|
|
1414
|
+
extra: built.flattenNote,
|
|
1415
|
+
columnImages: cols.map((el) => columnImage($(el), bodyWidth))
|
|
898
1416
|
});
|
|
899
1417
|
continue;
|
|
900
1418
|
}
|
|
@@ -903,25 +1421,37 @@ function sectionsFromRow($row, $, ctx, token, paint, image, bodyWidth) {
|
|
|
903
1421
|
rendered.push({
|
|
904
1422
|
children: [blocks],
|
|
905
1423
|
layout: "1",
|
|
906
|
-
extra: group.extra
|
|
1424
|
+
extra: group.extra,
|
|
1425
|
+
columnImages: []
|
|
907
1426
|
});
|
|
908
1427
|
}
|
|
909
1428
|
if (rendered.length === 0) return [];
|
|
910
1429
|
const seam = rendered.length > 1 && hasPadding(padding) ? SEAM_NOTE : void 0;
|
|
1430
|
+
const structure = imageLayer($structure, "Section", bodyWidth);
|
|
911
1431
|
return rendered.map((group, index) => {
|
|
1432
|
+
const image = resolveSectionImage({
|
|
1433
|
+
structure,
|
|
1434
|
+
body: images.body,
|
|
1435
|
+
band: images.band,
|
|
1436
|
+
columns: group.columnImages,
|
|
1437
|
+
bodyPainted: images.bodyPainted
|
|
1438
|
+
});
|
|
1439
|
+
const groupPaint = withoutBandOverImage(paint, image.image !== void 0);
|
|
912
1440
|
const notes = [
|
|
913
|
-
...
|
|
1441
|
+
...groupPaint.notes,
|
|
914
1442
|
group.extra,
|
|
915
|
-
image,
|
|
1443
|
+
...image.notes,
|
|
916
1444
|
seam
|
|
917
1445
|
].filter((note) => Boolean(note));
|
|
918
|
-
pushEntry(ctx, token, "section", notes.length > 0 ? "approximated" : "converted", notes.length > 0 ? notes.join(" ") : void 0);
|
|
1446
|
+
const entry = pushEntry(ctx, token, "section", notes.length > 0 ? "approximated" : "converted", notes.length > 0 ? notes.join(" ") : void 0);
|
|
1447
|
+
recordImageSpan(ctx, image.source, entry);
|
|
919
1448
|
return withBackground(createSectionBlock({
|
|
920
1449
|
columns: group.layout,
|
|
921
1450
|
children: group.children,
|
|
922
1451
|
styles: { padding: splitPadding(padding, rendered.length, index) },
|
|
923
|
-
...
|
|
924
|
-
|
|
1452
|
+
...groupPaint.wrapperColor ? { wrapper: { backgroundColor: groupPaint.wrapperColor } } : {},
|
|
1453
|
+
...image.image ? { backgroundImage: image.image } : {}
|
|
1454
|
+
}), groupPaint.backgroundColor);
|
|
925
1455
|
});
|
|
926
1456
|
}
|
|
927
1457
|
function sectionsFromStripe($stripe, $, ctx) {
|
|
@@ -929,12 +1459,16 @@ function sectionsFromStripe($stripe, $, ctx) {
|
|
|
929
1459
|
const token = stripeToken($stripe) ?? "es-content";
|
|
930
1460
|
const $body = $stripe.find(BODY_SELECTOR$1).first();
|
|
931
1461
|
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
1462
|
const $width = $body.length > 0 ? $body : $stripe;
|
|
935
1463
|
const bodyWidth = emailBodyWidth($width.attr("width"), $width.attr("style"));
|
|
1464
|
+
const paint = resolveSectionPaint(ctx.pageBackground ?? "#ffffff", bandColor$1($stripe), $body.length > 0 ? paintOf($body) : "");
|
|
1465
|
+
const images = {
|
|
1466
|
+
body: imageLayer($body, "Section", bodyWidth),
|
|
1467
|
+
band: bandImage($stripe, directCell($stripe), bodyWidth),
|
|
1468
|
+
bodyPainted: $body.length > 0 && paintOf($body) !== ""
|
|
1469
|
+
};
|
|
936
1470
|
const out = [];
|
|
937
|
-
for (const $row of rows) out.push(...sectionsFromRow($row, $, ctx, token, paint,
|
|
1471
|
+
for (const $row of rows) out.push(...sectionsFromRow($row, $, ctx, token, paint, images, bodyWidth));
|
|
938
1472
|
return out;
|
|
939
1473
|
}
|
|
940
1474
|
function convertCompiled(html, ctx) {
|
|
@@ -1038,7 +1572,6 @@ const ZERO = {
|
|
|
1038
1572
|
left: 0
|
|
1039
1573
|
};
|
|
1040
1574
|
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.";
|
|
1042
1575
|
function columnLayout(count, ctx) {
|
|
1043
1576
|
if (count <= 1) return { layout: "1" };
|
|
1044
1577
|
if (count === 2) return { layout: "2" };
|
|
@@ -1091,13 +1624,17 @@ function enclosing($el, selector) {
|
|
|
1091
1624
|
const $parent = $el.parents(selector).first();
|
|
1092
1625
|
return $parent.length > 0 ? $parent : null;
|
|
1093
1626
|
}
|
|
1627
|
+
/** A table stripe's first cell, which Stripo paints instead of the table. */
|
|
1628
|
+
function bandCell($stripe) {
|
|
1629
|
+
const $direct = $stripe.children("tr").children("td, th");
|
|
1630
|
+
const $nested = $stripe.children("tbody, thead").children("tr").children("td, th");
|
|
1631
|
+
return ($direct.length > 0 ? $direct : $nested).first();
|
|
1632
|
+
}
|
|
1094
1633
|
function bandColor($stripe) {
|
|
1095
1634
|
const own = colorFromPaint($stripe.attr("style"), $stripe.attr("bgcolor"));
|
|
1096
1635
|
if (own) return own;
|
|
1097
1636
|
if ($stripe.is("td") || $stripe.is("th")) return "";
|
|
1098
|
-
const $
|
|
1099
|
-
const $nested = $stripe.children("tbody, thead").children("tr").children("td, th");
|
|
1100
|
-
const $cell = ($direct.length > 0 ? $direct : $nested).first();
|
|
1637
|
+
const $cell = bandCell($stripe);
|
|
1101
1638
|
return $cell.length > 0 ? colorFromPaint($cell.attr("style"), $cell.attr("bgcolor")) : "";
|
|
1102
1639
|
}
|
|
1103
1640
|
function paintFor($el, ctx) {
|
|
@@ -1105,9 +1642,6 @@ function paintFor($el, ctx) {
|
|
|
1105
1642
|
const $body = enclosing($el, BODY_SELECTOR);
|
|
1106
1643
|
return resolveSectionPaint(ctx.pageBackground ?? "#ffffff", $stripe ? bandColor($stripe) : "", $body ? colorFromPaint($body.attr("style"), $body.attr("bgcolor")) : "");
|
|
1107
1644
|
}
|
|
1108
|
-
function imageNote(...styles) {
|
|
1109
|
-
return styles.some((style) => hasBackgroundImage(style)) ? IMAGE_NOTE : void 0;
|
|
1110
|
-
}
|
|
1111
1645
|
function sectionFromStructure($structure, $, ctx) {
|
|
1112
1646
|
const frames = $structure.find("[class~='esd-container-frame']").toArray().map((n) => $(n)).filter(($el) => {
|
|
1113
1647
|
if (isHidden($el)) return false;
|
|
@@ -1119,7 +1653,8 @@ function sectionFromStructure($structure, $, ctx) {
|
|
|
1119
1653
|
const { layout, flattenNote } = columnLayout(frameCount === 0 ? 1 : frameCount, ctx);
|
|
1120
1654
|
const padding = readBoxPadding($structure.attr("class"), $structure.attr("style"));
|
|
1121
1655
|
const $body = enclosing($structure, BODY_SELECTOR);
|
|
1122
|
-
const
|
|
1656
|
+
const bodyWidth = emailBodyWidth($body?.attr("width"), $body?.attr("style"));
|
|
1657
|
+
const rooms = columnRooms(bodyWidth, layout, padding, layout === "1" ? 1 : Math.min(frameCount, 3));
|
|
1123
1658
|
let children;
|
|
1124
1659
|
if (layout === "1") {
|
|
1125
1660
|
const merged = [];
|
|
@@ -1128,20 +1663,33 @@ function sectionFromStructure($structure, $, ctx) {
|
|
|
1128
1663
|
else for (const $f of frames) merged.push(...blocksInContainer($f, $, ctx, room));
|
|
1129
1664
|
children = [merged];
|
|
1130
1665
|
} else children = frames.map(($f, index) => blocksInContainer($f, $, ctx, rooms[index]));
|
|
1131
|
-
const paint = paintFor($structure, ctx);
|
|
1132
1666
|
const $stripe = enclosing($structure, "[class~='esd-stripe']");
|
|
1667
|
+
const image = resolveSectionImage({
|
|
1668
|
+
structure: imageLayer($structure, "Section", bodyWidth),
|
|
1669
|
+
body: imageLayer($body, "Section", bodyWidth),
|
|
1670
|
+
band: $stripe ? bandImage($stripe, bandCell($stripe), bodyWidth) : null,
|
|
1671
|
+
columns: frames.map(($f) => imageOf($f, "Column", bodyWidth)),
|
|
1672
|
+
bodyPainted: $body ? colorFromPaint($body.attr("style"), $body.attr("bgcolor")) !== "" : false
|
|
1673
|
+
});
|
|
1674
|
+
const paint = withoutBandOverImage(paintFor($structure, ctx), image.image !== void 0);
|
|
1133
1675
|
const notes = [
|
|
1134
1676
|
...paint.notes,
|
|
1135
1677
|
flattenNote,
|
|
1136
|
-
|
|
1678
|
+
...image.notes
|
|
1137
1679
|
].filter((note) => Boolean(note));
|
|
1138
|
-
if (notes.length > 0)
|
|
1139
|
-
|
|
1680
|
+
if (notes.length > 0) {
|
|
1681
|
+
const entry = pushEntry(ctx, "esd-structure", "section", "approximated", notes.join(" "));
|
|
1682
|
+
recordImageSpan(ctx, image.source, entry);
|
|
1683
|
+
} else if (frameCount <= 3) {
|
|
1684
|
+
const entry = pushEntry(ctx, "esd-structure", "section", "converted");
|
|
1685
|
+
recordImageSpan(ctx, image.source, entry);
|
|
1686
|
+
}
|
|
1140
1687
|
return withBackground(createSectionBlock({
|
|
1141
1688
|
columns: layout,
|
|
1142
1689
|
children,
|
|
1143
1690
|
styles: { padding },
|
|
1144
|
-
...paint.wrapperColor ? { wrapper: { backgroundColor: paint.wrapperColor } } : {}
|
|
1691
|
+
...paint.wrapperColor ? { wrapper: { backgroundColor: paint.wrapperColor } } : {},
|
|
1692
|
+
...image.image ? { backgroundImage: image.image } : {}
|
|
1145
1693
|
}), paint.backgroundColor);
|
|
1146
1694
|
}
|
|
1147
1695
|
function convertEditor(html, ctx) {
|
|
@@ -1153,14 +1701,23 @@ function convertEditor(html, ctx) {
|
|
|
1153
1701
|
return stripes.map(($stripe) => {
|
|
1154
1702
|
const inner = $stripe.find("[class~='esd-structure']");
|
|
1155
1703
|
if (inner.length > 0) return sectionFromStructure(inner.first(), $, ctx);
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
-
|
|
1704
|
+
const image = resolveSectionImage({
|
|
1705
|
+
structure: null,
|
|
1706
|
+
body: null,
|
|
1707
|
+
band: bandImage($stripe, bandCell($stripe), 600),
|
|
1708
|
+
columns: [],
|
|
1709
|
+
bodyPainted: false
|
|
1710
|
+
});
|
|
1711
|
+
const paint = withoutBandOverImage(paintFor($stripe, ctx), image.image !== void 0);
|
|
1712
|
+
const notes = [...paint.notes, ...image.notes].filter((note) => Boolean(note));
|
|
1713
|
+
const entry = pushEntry(ctx, "esd-stripe", "section", notes.length > 0 ? "approximated" : "converted", notes.length > 0 ? notes.join(" ") : void 0);
|
|
1714
|
+
recordImageSpan(ctx, image.source, entry);
|
|
1159
1715
|
return withBackground(createSectionBlock({
|
|
1160
1716
|
columns: "1",
|
|
1161
1717
|
children: [blocksInContainer($stripe, $, ctx, columnRooms(600, "1", ZERO, 1)[0])],
|
|
1162
1718
|
styles: { padding: { ...ZERO } },
|
|
1163
|
-
...paint.wrapperColor ? { wrapper: { backgroundColor: paint.wrapperColor } } : {}
|
|
1719
|
+
...paint.wrapperColor ? { wrapper: { backgroundColor: paint.wrapperColor } } : {},
|
|
1720
|
+
...image.image ? { backgroundImage: image.image } : {}
|
|
1164
1721
|
}), paint.backgroundColor);
|
|
1165
1722
|
});
|
|
1166
1723
|
}
|
|
@@ -1185,6 +1742,16 @@ function pageBackground($) {
|
|
|
1185
1742
|
}
|
|
1186
1743
|
return "#ffffff";
|
|
1187
1744
|
}
|
|
1745
|
+
/** The page's background image, which has no Templatical equivalent: the first `es-wrapper-color` / `es-wrapper` that paints one. */
|
|
1746
|
+
function pageImageWarning($) {
|
|
1747
|
+
const nodes = $("[class~='es-wrapper-color'], [class~='es-wrapper']").toArray();
|
|
1748
|
+
for (const node of nodes) {
|
|
1749
|
+
const $el = $(node);
|
|
1750
|
+
const read = imageFromPaint($el.attr("style"), $el.attr("background"), "Page", void 0);
|
|
1751
|
+
if (read) return `The email background image "${read.image.url}" was dropped; Templatical has no email-level background image.`;
|
|
1752
|
+
}
|
|
1753
|
+
return null;
|
|
1754
|
+
}
|
|
1188
1755
|
function summarize(ctx) {
|
|
1189
1756
|
const summary = {
|
|
1190
1757
|
total: ctx.entries.length,
|
|
@@ -1230,9 +1797,13 @@ function convertStripoTemplate(html, options) {
|
|
|
1230
1797
|
warnings: [],
|
|
1231
1798
|
css: options?.css
|
|
1232
1799
|
};
|
|
1233
|
-
const
|
|
1800
|
+
const $page = load(prepared);
|
|
1801
|
+
const pageBg = pageBackground($page);
|
|
1802
|
+
const pageImage = pageImageWarning($page);
|
|
1803
|
+
if (pageImage) ctx.warnings.push(pageImage);
|
|
1234
1804
|
ctx.pageBackground = pageBg;
|
|
1235
1805
|
const blocks = kind === "editor" ? convertEditor(prepared, ctx) : convertCompiled(prepared, ctx);
|
|
1806
|
+
noteImageSpans(ctx);
|
|
1236
1807
|
const defaults = createDefaultTemplateContent();
|
|
1237
1808
|
return {
|
|
1238
1809
|
content: {
|