@hyper-elements/react 0.3.0 → 0.3.1
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/GUIDE.md +47 -28
- package/README.md +24 -5
- package/REFERENCE.md +487 -485
- package/dist/builder.cjs +795 -206
- package/dist/builder.cjs.map +1 -1
- package/dist/builder.js +795 -206
- package/dist/builder.js.map +1 -1
- package/dist/image.cjs +1 -1
- package/dist/image.cjs.map +1 -1
- package/dist/image.js +1 -1
- package/dist/image.js.map +1 -1
- package/dist/inspect.cjs +3601 -0
- package/dist/inspect.cjs.map +1 -0
- package/dist/inspect.d.cts +472 -0
- package/dist/inspect.d.ts +472 -0
- package/dist/inspect.js +3572 -0
- package/dist/inspect.js.map +1 -0
- package/dist/pdf.cjs +54 -9
- package/dist/pdf.cjs.map +1 -1
- package/dist/pdf.js +54 -9
- package/dist/pdf.js.map +1 -1
- package/dist/primitives.cjs +738 -142
- package/dist/primitives.cjs.map +1 -1
- package/dist/primitives.d.cts +558 -158
- package/dist/primitives.d.ts +558 -158
- package/dist/primitives.js +736 -143
- package/dist/primitives.js.map +1 -1
- package/package.json +10 -1
package/dist/primitives.js
CHANGED
|
@@ -9,21 +9,26 @@ var Diagnostics = class {
|
|
|
9
9
|
constructor() {
|
|
10
10
|
this.list = [];
|
|
11
11
|
}
|
|
12
|
-
add(code, severity, message, path, nodeId) {
|
|
13
|
-
|
|
12
|
+
add(code, severity, message, path, nodeId, extra = {}) {
|
|
13
|
+
const d = { code, severity, message, path };
|
|
14
|
+
if (nodeId !== void 0) d.nodeId = nodeId;
|
|
15
|
+
if (extra.clients) d.clients = [...extra.clients];
|
|
16
|
+
if (extra.fix) d.fix = extra.fix;
|
|
17
|
+
if (extra.see) d.see = extra.see;
|
|
18
|
+
this.list.push(d);
|
|
14
19
|
}
|
|
15
20
|
error(code, message, path, nodeId) {
|
|
16
21
|
this.add(code, "error", message, path, nodeId);
|
|
17
22
|
}
|
|
18
|
-
warning(code, message, path, nodeId) {
|
|
19
|
-
this.add(code, "warning", message, path, nodeId);
|
|
23
|
+
warning(code, message, path, nodeId, extra) {
|
|
24
|
+
this.add(code, "warning", message, path, nodeId, extra);
|
|
20
25
|
}
|
|
21
26
|
get hasErrors() {
|
|
22
27
|
return this.list.some((d) => d.severity === "error");
|
|
23
28
|
}
|
|
24
29
|
};
|
|
25
30
|
function formatDiagnostics(diagnostics) {
|
|
26
|
-
return diagnostics.map((d) => `${d.severity} ${d.code} at ${d.path}: ${d.message}`).join("\n");
|
|
31
|
+
return diagnostics.map((d) => `${d.severity} ${d.code} at ${d.path}: ${d.message}${d.clients ? ` [${d.clients.join(", ")}]` : ""}${d.fix ? ` Fix: ${d.fix}` : ""}${d.see ? ` See: ${d.see}` : ""}`).join("\n");
|
|
27
32
|
}
|
|
28
33
|
var ElementsError = class extends Error {
|
|
29
34
|
constructor(diagnostics) {
|
|
@@ -34,6 +39,64 @@ ${formatDiagnostics(errors)}`);
|
|
|
34
39
|
this.diagnostics = diagnostics;
|
|
35
40
|
}
|
|
36
41
|
};
|
|
42
|
+
var captured = (against, wordsWithin4px, lineBreaks) => ({ elements: "0.3.0", against, captured: "2026-11-08", wordsWithin4px, lineBreaks });
|
|
43
|
+
var tables = (name) => ({ name, widths: {}, dark: false, approximate: true, measured: null });
|
|
44
|
+
var EMAIL_CLIENTS = {
|
|
45
|
+
outlook: { name: "Classic Outlook", widths: { desktop: 820 }, dark: false, approximate: true, measured: captured("Outlook 16.0.20527 for Windows, 96 DPI", 0.85, 0.983) },
|
|
46
|
+
"outlook-com": { name: "Outlook.com and new Outlook", widths: { desktop: 820 }, dark: false, approximate: false, measured: captured("Outlook on the web", 1, 1) },
|
|
47
|
+
"outlook-mac": tables("Outlook for Mac"),
|
|
48
|
+
"outlook-app": tables("Outlook app (iOS and Android)"),
|
|
49
|
+
gmail: { name: "Gmail", widths: { desktop: 820 }, dark: false, approximate: false, measured: captured("Gmail on the web", 1, 1) },
|
|
50
|
+
"gmail-app": { name: "Gmail app", widths: { phone: 375 }, dark: false, approximate: true, measured: null },
|
|
51
|
+
"gmail-app-other": { name: "Gmail app, another account", widths: { phone: 375 }, dark: false, approximate: true, measured: null },
|
|
52
|
+
"apple-mail": { name: "Apple Mail", widths: { desktop: 820 }, dark: true, approximate: false, measured: captured("Mail 16 on macOS 26", 0.999, 0.999) },
|
|
53
|
+
yahoo: tables("Yahoo Mail"),
|
|
54
|
+
aol: tables("AOL Mail"),
|
|
55
|
+
"samsung-email": tables("Samsung Email"),
|
|
56
|
+
thunderbird: tables("Thunderbird"),
|
|
57
|
+
protonmail: tables("Proton Mail"),
|
|
58
|
+
fastmail: tables("Fastmail"),
|
|
59
|
+
hey: tables("HEY"),
|
|
60
|
+
gmx: tables("GMX"),
|
|
61
|
+
"web-de": tables("WEB.DE"),
|
|
62
|
+
orange: tables("Orange"),
|
|
63
|
+
sfr: tables("SFR"),
|
|
64
|
+
laposte: tables("La Poste"),
|
|
65
|
+
"free-fr": tables("Free"),
|
|
66
|
+
"t-online-de": tables("T-Online"),
|
|
67
|
+
"mail-ru": tables("Mail.ru"),
|
|
68
|
+
ionos: tables("IONOS (1&1)"),
|
|
69
|
+
"wp-pl": tables("WP Poczta"),
|
|
70
|
+
rainloop: tables("RainLoop")
|
|
71
|
+
};
|
|
72
|
+
var DEFAULT_EMAIL_CLIENTS = ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "apple-mail", "yahoo", "aol", "samsung-email"];
|
|
73
|
+
var caniemail = (feature) => `https://www.caniemail.com/features/${feature}/`;
|
|
74
|
+
function checkEmailClients(clients) {
|
|
75
|
+
if (clients === void 0) return;
|
|
76
|
+
const unknown = Array.isArray(clients) ? clients.filter((c) => !Object.prototype.hasOwnProperty.call(EMAIL_CLIENTS, c)) : [clients];
|
|
77
|
+
if (unknown.length) throw new TypeError(`[Elements] emailClients takes a list of ${Object.keys(EMAIL_CLIENTS).join(", ")}, not ${unknown.map((c) => JSON.stringify(c)).join(", ")}`);
|
|
78
|
+
}
|
|
79
|
+
function forEmailClients(diagnostics, selection = DEFAULT_EMAIL_CLIENTS) {
|
|
80
|
+
const out = [];
|
|
81
|
+
const seen = /* @__PURE__ */ new Map();
|
|
82
|
+
for (const d of diagnostics) {
|
|
83
|
+
if (!d.clients) {
|
|
84
|
+
out.push(d);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const clients = d.clients.filter((c) => selection.includes(c));
|
|
88
|
+
if (!clients.length) continue;
|
|
89
|
+
const key = JSON.stringify([d.code, d.path, d.severity, d.message]);
|
|
90
|
+
const same42 = seen.get(key);
|
|
91
|
+
if (same42) same42.clients = [.../* @__PURE__ */ new Set([...same42.clients, ...clients])];
|
|
92
|
+
else {
|
|
93
|
+
const kept = { ...d, clients };
|
|
94
|
+
seen.set(key, kept);
|
|
95
|
+
out.push(kept);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
37
100
|
var ok = (value) => ({ ok: true, value });
|
|
38
101
|
var fail = (message) => ({ ok: false, message });
|
|
39
102
|
var show = (input) => {
|
|
@@ -619,6 +682,26 @@ function attributes() {
|
|
|
619
682
|
jsonSchema: () => ({ type: "object", additionalProperties: { type: "string" } })
|
|
620
683
|
});
|
|
621
684
|
}
|
|
685
|
+
var MIN_PHONE_WIDTH = 240;
|
|
686
|
+
function breakpoints() {
|
|
687
|
+
const accepts = `{ mobile }: the phone width in px (from ${MIN_PHONE_WIDTH})`;
|
|
688
|
+
return kind({
|
|
689
|
+
name: "breakpoints",
|
|
690
|
+
editor: "number",
|
|
691
|
+
accepts,
|
|
692
|
+
parse(input) {
|
|
693
|
+
if (!isObject(input)) return fail(`expected ${accepts}, not ${show(input)}`);
|
|
694
|
+
const unknown = Object.keys(input).find((key) => key !== "mobile");
|
|
695
|
+
if (unknown !== void 0) return fail(`${show(unknown)} isn't a breakpoint a Page has: it has mobile`);
|
|
696
|
+
const { mobile } = input;
|
|
697
|
+
if (mobile === void 0) return ok({});
|
|
698
|
+
if (typeof mobile !== "number" || !Number.isInteger(mobile) || mobile < MIN_PHONE_WIDTH) return fail(`mobile is the widest window its phone styles apply to, an integer from ${MIN_PHONE_WIDTH}, not ${show(mobile)}`);
|
|
699
|
+
return ok({ mobile });
|
|
700
|
+
},
|
|
701
|
+
empty: (value) => value.mobile === void 0,
|
|
702
|
+
jsonSchema: () => ({ type: "object", properties: { mobile: { type: "integer", minimum: MIN_PHONE_WIDTH } }, additionalProperties: false })
|
|
703
|
+
});
|
|
704
|
+
}
|
|
622
705
|
function linkParts(value) {
|
|
623
706
|
return typeof value === "string" ? { href: value } : value;
|
|
624
707
|
}
|
|
@@ -632,6 +715,54 @@ function defineComponent(definition) {
|
|
|
632
715
|
function prop(kind2, options) {
|
|
633
716
|
return { kind: kind2, ...options };
|
|
634
717
|
}
|
|
718
|
+
var MOBILE = {
|
|
719
|
+
name: "mobile",
|
|
720
|
+
on: "on phones",
|
|
721
|
+
for: "for phones",
|
|
722
|
+
roots: (root) => !!root?.responsive,
|
|
723
|
+
notAllowed: { code: "mobile/not-supported", message: "Phone styles are for an Email or a Page, which have a phone width" },
|
|
724
|
+
sheet: { media: "phone", selector: "&" },
|
|
725
|
+
allows: (spec) => !!spec.mobile,
|
|
726
|
+
support: { web: { level: "full", source: "writer" }, email: { level: "full", source: "writer" } },
|
|
727
|
+
styles: {
|
|
728
|
+
// The email's phone text styles are rules that override a size; "normal" isn't one.
|
|
729
|
+
lineHeight: { email: { level: "none", values: ["normal"], shows: "the line height it has on desktop", source: "writer" } }
|
|
730
|
+
},
|
|
731
|
+
outlookApplies: false,
|
|
732
|
+
hiddenKey: "mobile"
|
|
733
|
+
};
|
|
734
|
+
var HOVER = {
|
|
735
|
+
name: "hover",
|
|
736
|
+
on: "on hover",
|
|
737
|
+
for: "for hover",
|
|
738
|
+
// Keyboard focus gets the same styles as the pointer: a link or button reached by Tab shows its hover colors.
|
|
739
|
+
// A box around the link (an email Button's cell, its background) takes them while the link in it has focus.
|
|
740
|
+
sheet: { media: "all", selector: "&:hover,&:focus-visible", within: "&:hover,&:focus-within", level: "hover" },
|
|
741
|
+
allows: (_, key, definition) => !!definition.hover?.includes(key),
|
|
742
|
+
support: {
|
|
743
|
+
web: { level: "full", source: "css" },
|
|
744
|
+
// On type selectors only in Outlook.com and Outlook for Mac (Elements' are classes); the phone apps' results don't
|
|
745
|
+
// matter, as nothing hovers there.
|
|
746
|
+
email: {
|
|
747
|
+
level: "partial",
|
|
748
|
+
shows: "no change (where it shows, keyboard focus gets it too)",
|
|
749
|
+
source: "caniemail",
|
|
750
|
+
feature: "css-pseudo-class-hover",
|
|
751
|
+
clients: ["outlook", "outlook-com", "outlook-mac", "gmx", "web-de", "sfr", "laposte", "mail-ru"]
|
|
752
|
+
}
|
|
753
|
+
},
|
|
754
|
+
paged: (node, where) => `A hover isn't possible in ${where}: nothing points at it. The ${node.type} shows its own colors`,
|
|
755
|
+
outlookApplies: false
|
|
756
|
+
};
|
|
757
|
+
var CONDITIONS = [MOBILE, HOVER];
|
|
758
|
+
var CONDITION_NAMES = new Set(CONDITIONS.map((condition) => condition.name));
|
|
759
|
+
function blockOf(node, condition) {
|
|
760
|
+
const block = node[condition.name];
|
|
761
|
+
return block && Object.keys(block).length ? block : void 0;
|
|
762
|
+
}
|
|
763
|
+
function shownAnywhere(node) {
|
|
764
|
+
return !(node.hidden?.mobile && node.hidden?.desktop);
|
|
765
|
+
}
|
|
635
766
|
var writer = { level: "full", source: "writer" };
|
|
636
767
|
var css = { level: "full", source: "css" };
|
|
637
768
|
var textStyle = {
|
|
@@ -640,7 +771,12 @@ var textStyle = {
|
|
|
640
771
|
fontFamily: { kind: fontFamily(), label: "Font", inherited: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
|
|
641
772
|
// Word rounds sizes to half points (13px is 10pt): a fraction of a px.
|
|
642
773
|
fontSize: { kind: length(["px"], { min: 1 }), label: "Font size", inherited: true, mobile: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
|
|
643
|
-
fontWeight: {
|
|
774
|
+
fontWeight: {
|
|
775
|
+
kind: fontWeight(),
|
|
776
|
+
label: "Font weight",
|
|
777
|
+
inherited: true,
|
|
778
|
+
support: { email: { level: "outlook", values: [100, 200, 300, 500], shows: "normal weight: Word draws only normal and bold (600 and over)", source: "caniemail", feature: "css-font-weight" }, web: css }
|
|
779
|
+
},
|
|
644
780
|
fontStyle: { kind: enumOf(["normal", "italic"]), label: "Font style", inherited: true, support: { email: { level: "full", source: "caniemail" }, web: css } },
|
|
645
781
|
// Written in px with mso-line-height-rule: exactly, which Word keeps (it drops unitless ones).
|
|
646
782
|
lineHeight: { kind: lineHeight(), label: "Line height", inherited: true, mobile: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
|
|
@@ -652,8 +788,21 @@ var textStyle = {
|
|
|
652
788
|
support: { email: { level: "outlook", shows: "normal spacing: Word keeps letter spacing but doesn't draw it", source: "outlook-captures" }, web: css }
|
|
653
789
|
},
|
|
654
790
|
textAlign: { kind: enumOf(["left", "center", "right", "justify"]), label: "Alignment", inherited: true, mobile: true, support: { email: { level: "full", source: "caniemail" }, web: css } },
|
|
655
|
-
textTransform: {
|
|
656
|
-
|
|
791
|
+
textTransform: {
|
|
792
|
+
kind: enumOf(["none", "uppercase", "lowercase", "capitalize"]),
|
|
793
|
+
label: "Letter case",
|
|
794
|
+
inherited: true,
|
|
795
|
+
support: { email: { level: "outlook", values: ["lowercase"], shows: "the text as written: Word doesn't lowercase it", source: "caniemail", feature: "css-text-transform" }, web: css }
|
|
796
|
+
},
|
|
797
|
+
textDecoration: {
|
|
798
|
+
kind: enumOf(["none", "underline", "line-through"]),
|
|
799
|
+
label: "Decoration",
|
|
800
|
+
support: {
|
|
801
|
+
// Removed, so "none" leaves a link the client's own underline: said only where a line is asked for.
|
|
802
|
+
email: { level: "partial", values: ["underline", "line-through"], shows: "no line: the client removes it", source: "caniemail", feature: "css-text-decoration", clients: ["gmail-app-other", "protonmail", "orange", "sfr"] },
|
|
803
|
+
web: css
|
|
804
|
+
}
|
|
805
|
+
}
|
|
657
806
|
};
|
|
658
807
|
var inheritedTextStyle = (({ textDecoration: _, ...rest }) => rest)(textStyle);
|
|
659
808
|
var boxStyle = {
|
|
@@ -666,7 +815,17 @@ var boxStyle = {
|
|
|
666
815
|
padding: { kind: insets(), label: "Padding", mobile: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
|
|
667
816
|
backgroundColor: { kind: color(), label: "Background color", support: { email: writer, web: css } },
|
|
668
817
|
border: { kind: border(), label: "Border", mobile: true, support: { email: { level: "full", source: "caniemail" }, web: css } },
|
|
669
|
-
borderRadius: {
|
|
818
|
+
borderRadius: {
|
|
819
|
+
kind: radius(),
|
|
820
|
+
label: "Corner radius",
|
|
821
|
+
support: {
|
|
822
|
+
email: [
|
|
823
|
+
{ level: "outlook", shows: "square corners", source: "caniemail", feature: "css-border-radius" },
|
|
824
|
+
{ level: "partial", shows: "square corners", source: "caniemail", feature: "css-border-radius", clients: ["gmx", "web-de", "orange"] }
|
|
825
|
+
],
|
|
826
|
+
web: css
|
|
827
|
+
}
|
|
828
|
+
},
|
|
670
829
|
overflow: {
|
|
671
830
|
kind: enumOf(["visible", "hidden"]),
|
|
672
831
|
label: "Overflow",
|
|
@@ -874,6 +1033,15 @@ var Grid = defineComponent({
|
|
|
874
1033
|
},
|
|
875
1034
|
children: { categories: ["layout", "block"] },
|
|
876
1035
|
sizing: "stretch",
|
|
1036
|
+
// Stacked on phones, every two of its children are its phone gap apart: there are no rows for a row gap.
|
|
1037
|
+
conditionSupport: {
|
|
1038
|
+
mobile: {
|
|
1039
|
+
rowGap: {
|
|
1040
|
+
web: { level: "none", when: (_, node) => node.props.stackOnMobile !== false, shows: "its phone gap between every two children", source: "writer" },
|
|
1041
|
+
email: { level: "none", when: (_, node) => node.props.stackOnMobile !== false, shows: "its phone gap between every two children", source: "writer" }
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
},
|
|
877
1045
|
targets: { email: {}, web: {} },
|
|
878
1046
|
editor: {
|
|
879
1047
|
groups: [
|
|
@@ -886,7 +1054,7 @@ var Grid = defineComponent({
|
|
|
886
1054
|
var Layer = defineComponent({
|
|
887
1055
|
type: "Layer",
|
|
888
1056
|
label: "Layer",
|
|
889
|
-
description: "Its children over each other, in one box as tall as the tallest: an image
|
|
1057
|
+
description: "Its children over each other, in one box as tall as the tallest: words on an image (a background image, its first child). In an email, an Image with one child over it.",
|
|
890
1058
|
categories: ["layout", "block"],
|
|
891
1059
|
props: {},
|
|
892
1060
|
style: {
|
|
@@ -918,13 +1086,27 @@ var Layer = defineComponent({
|
|
|
918
1086
|
}
|
|
919
1087
|
});
|
|
920
1088
|
var WRITTEN3 = { email: { level: "full", source: "writer" }, web: { level: "full", source: "writer" } };
|
|
1089
|
+
var NO_WEB_FONTS = ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "yahoo", "aol", "protonmail", "fastmail", "gmx", "web-de", "orange", "sfr", "laposte", "mail-ru", "ionos"];
|
|
921
1090
|
var ROOT_PROPS = {
|
|
922
1091
|
title: prop(text(), { support: WRITTEN3, label: "Title", description: "The document's title (shown in a browser tab, and by some email clients)." }),
|
|
923
|
-
lang: prop(text({ maxLength: 35 }), {
|
|
924
|
-
|
|
1092
|
+
lang: prop(text({ maxLength: 35 }), {
|
|
1093
|
+
support: {
|
|
1094
|
+
...WRITTEN3,
|
|
1095
|
+
email: { level: "partial", shows: "no language: the client removes it, and a screen reader reads the email in its own", source: "caniemail", feature: "html-lang", clients: ["hey", "gmx", "web-de", "orange", "ionos"] }
|
|
1096
|
+
},
|
|
1097
|
+
label: "Language",
|
|
1098
|
+
description: 'Its language, as a BCP 47 tag ("en", "ar").'
|
|
1099
|
+
}),
|
|
1100
|
+
dir: prop(enumOf(["ltr", "rtl", "auto"]), {
|
|
1101
|
+
support: {
|
|
1102
|
+
...WRITTEN3,
|
|
1103
|
+
email: { level: "partial", values: ["rtl", "auto"], shows: "left to right: the client removes the direction", source: "caniemail", feature: "html-dir", clients: ["gmx", "web-de"] }
|
|
1104
|
+
},
|
|
1105
|
+
label: "Text direction"
|
|
1106
|
+
}),
|
|
925
1107
|
fonts: prop(fontFaces(), {
|
|
926
1108
|
support: {
|
|
927
|
-
email: { level: "partial", shows: "
|
|
1109
|
+
email: { level: "partial", shows: "no web font loads: the next font in each stack shows", source: "caniemail", feature: "css-at-font-face", clients: NO_WEB_FONTS },
|
|
928
1110
|
web: { level: "full", source: "writer" }
|
|
929
1111
|
},
|
|
930
1112
|
label: "Web fonts",
|
|
@@ -1064,7 +1246,10 @@ var PageNumber = defineComponent({
|
|
|
1064
1246
|
style: {},
|
|
1065
1247
|
children: {},
|
|
1066
1248
|
sizing: "none",
|
|
1067
|
-
targets: { web: {} }
|
|
1249
|
+
targets: { web: {} },
|
|
1250
|
+
// A Page's or a Canvas's text has no page to number.
|
|
1251
|
+
roots: ["Document"],
|
|
1252
|
+
notAllowed: { code: "document/page-number", message: (root) => `A PageNumber is a page's number in a Document; this ${root} has no pages` }
|
|
1068
1253
|
});
|
|
1069
1254
|
function documentPages(root) {
|
|
1070
1255
|
const pages = [];
|
|
@@ -1200,6 +1385,11 @@ var Page = defineComponent({
|
|
|
1200
1385
|
props: {
|
|
1201
1386
|
width: prop(length(["px"], { min: 200 }), { support: WRITTEN6, label: "Content width", description: "The widest the content gets.", default: 1200 }),
|
|
1202
1387
|
description: prop(text(), { support: WRITTEN6, label: "Description", description: "The page's meta description." }),
|
|
1388
|
+
breakpoints: prop(breakpoints(), {
|
|
1389
|
+
support: { web: { level: "full", source: "writer" } },
|
|
1390
|
+
label: "Breakpoints",
|
|
1391
|
+
description: "Where its phone styles start: mobile, the widest window they apply to, in px (480 where it's not set)."
|
|
1392
|
+
}),
|
|
1203
1393
|
...ROOT_PROPS
|
|
1204
1394
|
},
|
|
1205
1395
|
style: ROOT_STYLE,
|
|
@@ -1207,6 +1397,12 @@ var Page = defineComponent({
|
|
|
1207
1397
|
children: { categories: ["section", "layout", "block"] },
|
|
1208
1398
|
sizing: "none",
|
|
1209
1399
|
responsive: true,
|
|
1400
|
+
// The page writer writes the root's phone gap, not its phone text styles.
|
|
1401
|
+
conditionSupport: {
|
|
1402
|
+
mobile: Object.fromEntries(
|
|
1403
|
+
["fontSize", "lineHeight", "letterSpacing", "textAlign"].map((key) => [key, { web: { level: "none", shows: "its desktop value: set phone text styles on its Sections or Columns", source: "writer" } }])
|
|
1404
|
+
)
|
|
1405
|
+
},
|
|
1210
1406
|
targets: { web: {} },
|
|
1211
1407
|
editor: {
|
|
1212
1408
|
groups: [
|
|
@@ -1450,6 +1646,7 @@ var Image = defineComponent({
|
|
|
1450
1646
|
},
|
|
1451
1647
|
children: {},
|
|
1452
1648
|
sizing: "shrink",
|
|
1649
|
+
conditionSupport: { mobile: { border: { email: { level: "none", shows: "its desktop border", source: "writer" } } } },
|
|
1453
1650
|
targets: { email: {}, web: {} },
|
|
1454
1651
|
editor: { groups: [{ title: "Image", paths: ["props.src", "props.alt", "style.width"] }, { title: "Box", paths: ["style.border", "style.borderRadius"] }] }
|
|
1455
1652
|
});
|
|
@@ -1580,6 +1777,7 @@ var TableCell = defineComponent({
|
|
|
1580
1777
|
},
|
|
1581
1778
|
children: { text: true, categories: ["inline"], types: ["Image"] },
|
|
1582
1779
|
sizing: "none",
|
|
1780
|
+
conditionSupport: { mobile: { border: { email: { level: "none", shows: "its desktop border", source: "writer" } } } },
|
|
1583
1781
|
targets: { email: {}, web: {} },
|
|
1584
1782
|
editor: { groups: [{ title: "Cell", paths: ["props.colSpan", "style.width", "style.verticalAlign"] }, TEXT_EDITOR, { title: "Box", paths: ["style.padding", "style.backgroundColor", "style.border"] }] }
|
|
1585
1783
|
});
|
|
@@ -1697,17 +1895,27 @@ function checkDocument(input, options) {
|
|
|
1697
1895
|
const extra = Object.keys(input).filter((key) => key !== "root");
|
|
1698
1896
|
for (const key of extra) d.error("document/unknown-key", `"${key}" isn't part of a document (root)`, key);
|
|
1699
1897
|
const rootDefinition = isObject2(input.root) && typeof input.root.type === "string" ? registry.get(input.root.type) : void 0;
|
|
1700
|
-
const context = { d, registry, used: /* @__PURE__ */ new Set(),
|
|
1898
|
+
const context = { d, registry, used: /* @__PURE__ */ new Set(), root: rootDefinition };
|
|
1701
1899
|
const root = input.root === void 0 ? null : readNode(input.root, "root", null, context);
|
|
1702
1900
|
if (input.root === void 0) d.error("document/root", 'A document has a root: { type: "Email" }, "Page", \u2026', "root");
|
|
1703
1901
|
return { document: root ? { root } : null, diagnostics: d.list };
|
|
1704
1902
|
}
|
|
1705
1903
|
function checkFragment(input, options) {
|
|
1706
1904
|
const d = new Diagnostics();
|
|
1707
|
-
const context = { d, registry: options.registry, idPrefix: options.idPrefix, used: new Set(options.taken),
|
|
1905
|
+
const context = { d, registry: options.registry, idPrefix: options.idPrefix, used: new Set(options.taken), root: options.root };
|
|
1708
1906
|
const node = readNode(input, options.path, options.parent, context);
|
|
1709
1907
|
return { node, diagnostics: d.list };
|
|
1710
1908
|
}
|
|
1909
|
+
function rootList(roots) {
|
|
1910
|
+
const named = roots.map((root) => a(root).replace(/^A/, "a"));
|
|
1911
|
+
return named.length > 1 ? `${named.slice(0, -1).join(", ")} or ${named[named.length - 1]}` : named[0] ?? "no root";
|
|
1912
|
+
}
|
|
1913
|
+
function rootTakes(roots, root) {
|
|
1914
|
+
return !roots || !root || roots.includes(root.type);
|
|
1915
|
+
}
|
|
1916
|
+
function sharesOutput(definition, root) {
|
|
1917
|
+
return Object.keys(definition.targets).some((target) => target in root.targets);
|
|
1918
|
+
}
|
|
1711
1919
|
function allowedIn(parent, child) {
|
|
1712
1920
|
const rule2 = parent.children;
|
|
1713
1921
|
return !!rule2.types?.includes(child.type) || child.categories.some((category) => rule2.categories?.includes(category));
|
|
@@ -1733,7 +1941,29 @@ function editDistance(from, to) {
|
|
|
1733
1941
|
}
|
|
1734
1942
|
return row[to.length];
|
|
1735
1943
|
}
|
|
1944
|
+
var OTHER_BREAKPOINTS = /* @__PURE__ */ new Set(["base", "initial", "xs", "sm", "md", "lg", "xl", "2xl", "tablet", "desktop", "wide", "small", "medium", "large"]);
|
|
1945
|
+
function conditionHint(definition, key) {
|
|
1946
|
+
const name = key.trim().replace(/^(&?:+|_+|\$|@)/, "").replace(/Style$/, "").toLowerCase();
|
|
1947
|
+
const own = CUSTOM_TYPE.test(definition.type);
|
|
1948
|
+
const hoverStyles = definition.hover ?? [];
|
|
1949
|
+
const toHover = own ? `a template's own component takes only its place in a Row in its style: set hover styles in what it renders` : hoverStyles.length ? `hover styles go in its style's hover block, style={{ hover: { \u2026 } }} (it takes ${hoverStyles.join(", ")})` : `${a(definition.type).replace(/^A/, "a")} takes no hover styles (a Button's and a Link's colors do)`;
|
|
1950
|
+
const toPhone = own ? `a template's own component takes only its place in a Row in its style: set phone styles in what it renders` : `phone styles go in its style's mobile block, style={{ mobile: { \u2026 } }}`;
|
|
1951
|
+
if (name === "hover") return toHover;
|
|
1952
|
+
if (name === "mobile" || name === "phone" || name === "phones" || key.trim().startsWith("@media")) return toPhone;
|
|
1953
|
+
if (name === "focus" || name === "focus-visible" || name === "focusvisible" || name === "focus-within") {
|
|
1954
|
+
return hoverStyles.length && !own ? `keyboard focus gets its hover styles: set them in style={{ hover: { \u2026 } }}` : `there are no focus styles; keyboard focus gets a Button's and a Link's hover styles`;
|
|
1955
|
+
}
|
|
1956
|
+
if (OTHER_BREAKPOINTS.has(name)) {
|
|
1957
|
+
return `Elements has one breakpoint, phones: desktop styles go in style itself, phone styles in its mobile block, style={{ mobile: { \u2026 } }} (a Page sets where phones start with breakpoints={{ mobile }})`;
|
|
1958
|
+
}
|
|
1959
|
+
if (name === "active" || name === "pressed" || name === "press" || name === "visited" || name === "dark" || name === "light" || name === "print") {
|
|
1960
|
+
return `there are no ${name} styles: Elements has phone styles (mobile) and hover styles (hover) in style`;
|
|
1961
|
+
}
|
|
1962
|
+
return void 0;
|
|
1963
|
+
}
|
|
1736
1964
|
function unknownProp(definition, key) {
|
|
1965
|
+
const condition = conditionHint(definition, key);
|
|
1966
|
+
if (condition) return `${a(definition.type)} has no prop "${key}": ${condition}`;
|
|
1737
1967
|
if (Object.prototype.hasOwnProperty.call(definition.style, key)) return `${a(definition.type)} has no prop "${key}": it's a style, style={{ ${key}: \u2026 }}`;
|
|
1738
1968
|
const known = Object.keys(definition.props);
|
|
1739
1969
|
const meant = nearest(key, known);
|
|
@@ -1773,6 +2003,15 @@ function readNode(input, path, parent, context) {
|
|
|
1773
2003
|
d.error("children/not-allowed", placementMessage(parent, definition), path);
|
|
1774
2004
|
return null;
|
|
1775
2005
|
}
|
|
2006
|
+
const root = context.root;
|
|
2007
|
+
if (root && !rootTakes(definition.roots, root) && sharesOutput(definition, root)) {
|
|
2008
|
+
const { code, message } = definition.notAllowed ?? {
|
|
2009
|
+
code: "component/not-supported",
|
|
2010
|
+
message: (under) => `${a(definition.type)} goes in ${rootList(definition.roots ?? [])}, not in ${a(under).replace(/^A/, "a")}`
|
|
2011
|
+
};
|
|
2012
|
+
d.error(code, message(root.type), path);
|
|
2013
|
+
return null;
|
|
2014
|
+
}
|
|
1776
2015
|
let id;
|
|
1777
2016
|
if (input.id !== void 0) {
|
|
1778
2017
|
if (typeof input.id !== "string" || !ID.test(input.id)) {
|
|
@@ -1792,21 +2031,49 @@ function readNode(input, path, parent, context) {
|
|
|
1792
2031
|
if (!NODE_KEYS.includes(key)) d.error("node/unknown-key", `"${key}" isn't part of a component (${NODE_KEYS.join(", ")})`, `${path}.${key}`, id);
|
|
1793
2032
|
}
|
|
1794
2033
|
const node = id === void 0 ? { type: definition.type } : { id, type: definition.type };
|
|
1795
|
-
const props = readProps(input.props, definition, `${path}.props`, id, d);
|
|
2034
|
+
const props = readProps(input.props, definition, `${path}.props`, id, d, context.root);
|
|
1796
2035
|
if (props) node.props = props;
|
|
1797
|
-
const
|
|
1798
|
-
|
|
1799
|
-
if (
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
2036
|
+
const lifted = {};
|
|
2037
|
+
let styleInput = input.style;
|
|
2038
|
+
if (isObject2(styleInput) && !CUSTOM_TYPE.test(definition.type)) {
|
|
2039
|
+
const rest = {};
|
|
2040
|
+
for (const [key, value] of Object.entries(styleInput)) {
|
|
2041
|
+
if (CONDITION_NAMES.has(key)) lifted[key] = value;
|
|
2042
|
+
else rest[key] = value;
|
|
1804
2043
|
}
|
|
2044
|
+
styleInput = rest;
|
|
1805
2045
|
}
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
2046
|
+
const style = readStyles(styleInput, definition, definition.style, `${path}.style`, id, d, "style", () => true, void 0, context.root);
|
|
2047
|
+
if (style) node.style = style;
|
|
2048
|
+
for (const condition of CONDITIONS) {
|
|
2049
|
+
const inStyle = lifted[condition.name];
|
|
2050
|
+
const given = input[condition.name];
|
|
2051
|
+
if (inStyle !== void 0 && given !== void 0) {
|
|
2052
|
+
d.error(
|
|
2053
|
+
"style/twice",
|
|
2054
|
+
`${condition.name} is given twice: as ${condition.name}={\u2026} and in style. Give it once, in style: style={{ ${condition.name}: { \u2026 } }}`,
|
|
2055
|
+
`${path}.style.${condition.name}`,
|
|
2056
|
+
id
|
|
2057
|
+
);
|
|
2058
|
+
continue;
|
|
2059
|
+
}
|
|
2060
|
+
let value = inStyle ?? given;
|
|
2061
|
+
if (value === void 0) continue;
|
|
2062
|
+
const at = inStyle !== void 0 ? `${path}.style.${condition.name}` : `${path}.${condition.name}`;
|
|
2063
|
+
if (condition.roots && !condition.roots(context.root)) {
|
|
2064
|
+
if (condition.notAllowed) d.error(condition.notAllowed.code, condition.notAllowed.message, at, id);
|
|
2065
|
+
continue;
|
|
2066
|
+
}
|
|
2067
|
+
if (isObject2(value) && Object.keys(value).some((key) => CONDITION_NAMES.has(key))) {
|
|
2068
|
+
const own = {};
|
|
2069
|
+
for (const [key, inner] of Object.entries(value)) {
|
|
2070
|
+
if (CONDITION_NAMES.has(key)) d.error("style/nested", `${key} can't be set inside ${condition.name}: a block holds styles, not another block`, `${at}.${key}`, id);
|
|
2071
|
+
else own[key] = inner;
|
|
2072
|
+
}
|
|
2073
|
+
value = own;
|
|
2074
|
+
}
|
|
2075
|
+
const read2 = readStyles(value, definition, definition.style, at, id, d, condition.name, (spec, key) => condition.allows(spec, key, definition), condition.for, context.root);
|
|
2076
|
+
if (read2) node[condition.name] = read2;
|
|
1810
2077
|
}
|
|
1811
2078
|
if (input.hidden !== void 0) {
|
|
1812
2079
|
const hidden = readHidden(input.hidden, `${path}.hidden`, id, context);
|
|
@@ -1816,7 +2083,7 @@ function readNode(input, path, parent, context) {
|
|
|
1816
2083
|
if (children.length) node.children = children;
|
|
1817
2084
|
return node;
|
|
1818
2085
|
}
|
|
1819
|
-
function readProps(input, definition, path, id, d) {
|
|
2086
|
+
function readProps(input, definition, path, id, d, root) {
|
|
1820
2087
|
const out = {};
|
|
1821
2088
|
if (input !== void 0 && !isObject2(input)) {
|
|
1822
2089
|
d.error("props/invalid", "props is an object", path, id);
|
|
@@ -1829,9 +2096,14 @@ function readProps(input, definition, path, id, d) {
|
|
|
1829
2096
|
d.error("prop/unknown", unknownProp(definition, key), `${path}.${key}`, id);
|
|
1830
2097
|
continue;
|
|
1831
2098
|
}
|
|
2099
|
+
if (!rootTakes(spec.roots, root)) {
|
|
2100
|
+
d.error("prop/not-supported", `${a(definition.type)}'s "${key}" is for ${rootList(spec.roots)}, not ${a(root.type).replace(/^A/, "a")}`, `${path}.${key}`, id);
|
|
2101
|
+
continue;
|
|
2102
|
+
}
|
|
1832
2103
|
const parsed = spec.kind.parse(value);
|
|
1833
|
-
if (parsed.ok)
|
|
1834
|
-
|
|
2104
|
+
if (parsed.ok) {
|
|
2105
|
+
if (!spec.kind.empty?.(parsed.value)) out[key] = parsed.value;
|
|
2106
|
+
} else d.error("prop/invalid", `${key}: ${parsed.message}`, `${path}.${key}`, id);
|
|
1835
2107
|
}
|
|
1836
2108
|
for (const [key, spec] of Object.entries(definition.props)) {
|
|
1837
2109
|
if (spec.required && !(key in out) && !(isObject2(input) && input[key] !== void 0)) {
|
|
@@ -1848,6 +2120,8 @@ var UNSUPPORTED_CSS = {
|
|
|
1848
2120
|
marginRight: "use padding",
|
|
1849
2121
|
display: "the component sets it",
|
|
1850
2122
|
position: "use a Layer for content placed over other content",
|
|
2123
|
+
backgroundImage: "put a Layer around it, its first child an Image (the Layer's height crops it to cover), the rest over it",
|
|
2124
|
+
background: "backgroundColor for a color; for an image, a Layer: its first child an Image, the rest over it",
|
|
1851
2125
|
float: "use a Row to put things side by side",
|
|
1852
2126
|
// Names Elements used before it took CSS's.
|
|
1853
2127
|
align: "it's alignItems now",
|
|
@@ -1861,10 +2135,12 @@ function styleHint(definition, specs, key) {
|
|
|
1861
2135
|
if (Object.prototype.hasOwnProperty.call(definition.props, key)) return `: it's a prop, ${key}={\u2026}`;
|
|
1862
2136
|
if (key === "padding" && definition.categories.includes("root")) return ": a root has no padding; put it on a Column (a card) or a Section (a band) inside it";
|
|
1863
2137
|
if (UNSUPPORTED_CSS[key]) return `: ${UNSUPPORTED_CSS[key]}`;
|
|
2138
|
+
const condition = conditionHint(definition, key);
|
|
2139
|
+
if (condition) return `: ${condition}`;
|
|
1864
2140
|
const meant = nearest(key, Object.keys(specs));
|
|
1865
2141
|
return meant ? `: did you mean "${meant}"?` : "";
|
|
1866
2142
|
}
|
|
1867
|
-
function readStyles(input, definition, specs, path, id, d, what, allowed) {
|
|
2143
|
+
function readStyles(input, definition, specs, path, id, d, what, allowed, forLabel, root) {
|
|
1868
2144
|
if (input === void 0) return void 0;
|
|
1869
2145
|
if (!isObject2(input)) {
|
|
1870
2146
|
d.error(`${what}/invalid`, `${what} is an object of style properties`, path, id);
|
|
@@ -1878,8 +2154,12 @@ function readStyles(input, definition, specs, path, id, d, what, allowed) {
|
|
|
1878
2154
|
d.error("style/unknown", seeing(`"${key}" isn't a style ${a(definition.type).replace(/^A/, "a")} takes${styleHint(definition, specs, key)}`, definition.type), `${path}.${key}`, id);
|
|
1879
2155
|
continue;
|
|
1880
2156
|
}
|
|
2157
|
+
if (!rootTakes(spec.roots, root)) {
|
|
2158
|
+
d.error("style/not-supported", `${a(definition.type)}'s "${key}" is for ${rootList(spec.roots)}, not ${a(root.type).replace(/^A/, "a")}`, `${path}.${key}`, id);
|
|
2159
|
+
continue;
|
|
2160
|
+
}
|
|
1881
2161
|
if (!allowed(spec, key)) {
|
|
1882
|
-
d.error(`${what}/not-allowed`,
|
|
2162
|
+
d.error(`${what}/not-allowed`, `"${key}" can't be set ${forLabel ?? "here"}`, `${path}.${key}`, id);
|
|
1883
2163
|
continue;
|
|
1884
2164
|
}
|
|
1885
2165
|
const parsed = spec.kind.parse(value);
|
|
@@ -1890,7 +2170,7 @@ function readStyles(input, definition, specs, path, id, d, what, allowed) {
|
|
|
1890
2170
|
}
|
|
1891
2171
|
function readHidden(input, path, id, context) {
|
|
1892
2172
|
const { d } = context;
|
|
1893
|
-
if (!context.responsive) {
|
|
2173
|
+
if (!context.root?.responsive) {
|
|
1894
2174
|
d.error("hidden/not-supported", "Hiding on phones or desktops is for an Email or a Page", path, id);
|
|
1895
2175
|
return void 0;
|
|
1896
2176
|
}
|
|
@@ -1901,7 +2181,7 @@ function readHidden(input, path, id, context) {
|
|
|
1901
2181
|
const out = {};
|
|
1902
2182
|
if (input.mobile === true) out.mobile = true;
|
|
1903
2183
|
if (input.desktop === true) out.desktop = true;
|
|
1904
|
-
if (
|
|
2184
|
+
if (!shownAnywhere({ hidden: out })) d.warning("hidden/everywhere", "Hidden on phones and on desktops, it never shows", path, id);
|
|
1905
2185
|
return Object.keys(out).length ? out : void 0;
|
|
1906
2186
|
}
|
|
1907
2187
|
function readChildren(input, definition, path, id, context) {
|
|
@@ -1934,6 +2214,22 @@ function readChildren(input, definition, path, id, context) {
|
|
|
1934
2214
|
});
|
|
1935
2215
|
return out;
|
|
1936
2216
|
}
|
|
2217
|
+
var same = (a2, b) => JSON.stringify(a2) === JSON.stringify(b);
|
|
2218
|
+
function reportDeclared(d, node, at, key, value, declared, target, outlook = true) {
|
|
2219
|
+
if (!declared) return;
|
|
2220
|
+
for (const rule2 of [].concat(declared)) {
|
|
2221
|
+
if (rule2.level === "full") continue;
|
|
2222
|
+
if (rule2.level === "outlook" && !outlook) continue;
|
|
2223
|
+
if (rule2.values && !rule2.values.some((v) => same(v, value))) continue;
|
|
2224
|
+
if (rule2.when && !rule2.when(value, node)) continue;
|
|
2225
|
+
const asked = typeof value === "string" ? `${key}: "${value}"` : key;
|
|
2226
|
+
const clients = target === "email" ? rule2.clients ?? (rule2.level === "outlook" ? ["outlook"] : void 0) : void 0;
|
|
2227
|
+
const output = clients ? "some email clients" : target === "web" ? "a page" : target;
|
|
2228
|
+
const [code, severity, message] = rule2.level === "none" ? [`${target}/unsupported`, "warning", `${asked} isn't possible in ${output}: ${rule2.shows}`] : rule2.level === "partial" ? [`${target}/degraded`, "info", `${asked} on this ${node.type} in ${output}: ${rule2.shows}`] : ["email/outlook-degraded", "info", `${asked} on this ${node.type} in classic Outlook: ${rule2.shows}`];
|
|
2229
|
+
const [said, as] = rule2.source === "assumed" ? ["info", `${message} (not checked)`] : [severity, message];
|
|
2230
|
+
d.add(code, said, as, at, node.id, { clients, fix: rule2.fix, see: rule2.feature && caniemail(rule2.feature) });
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
1937
2233
|
var MAX_DEPTH = 32;
|
|
1938
2234
|
function templatePathsOf(document, paths) {
|
|
1939
2235
|
const places = /* @__PURE__ */ new Map();
|
|
@@ -1950,7 +2246,7 @@ function templatePathsOf(document, paths) {
|
|
|
1950
2246
|
return path;
|
|
1951
2247
|
};
|
|
1952
2248
|
}
|
|
1953
|
-
function expandDocument(document, options) {
|
|
2249
|
+
function expandDocument(document, options, target) {
|
|
1954
2250
|
const registry = registryOf(options);
|
|
1955
2251
|
const d = new Diagnostics();
|
|
1956
2252
|
const paths = /* @__PURE__ */ new WeakMap();
|
|
@@ -1962,7 +2258,20 @@ function expandDocument(document, options) {
|
|
|
1962
2258
|
collect(document.root);
|
|
1963
2259
|
const rootDefinition = registry.get(document.root.type);
|
|
1964
2260
|
const responsive = !!rootDefinition?.responsive;
|
|
2261
|
+
const unshown = (node, path) => {
|
|
2262
|
+
const declared = target && registry.get(node.type)?.conditionSupport;
|
|
2263
|
+
if (!declared) return;
|
|
2264
|
+
for (const condition of CONDITIONS) {
|
|
2265
|
+
const block = blockOf(node, condition);
|
|
2266
|
+
const byStyle = declared[condition.name];
|
|
2267
|
+
if (!block || !byStyle || condition.hiddenKey && node.hidden?.[condition.hiddenKey]) continue;
|
|
2268
|
+
for (const [key, value] of Object.entries(block)) {
|
|
2269
|
+
reportDeclared(d, { type: node.type, id: node.id, props: node.props ?? {} }, `${path}.${condition.name}.${key}`, key, value, byStyle[key]?.[target], target);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
};
|
|
1965
2273
|
const visit = (given, path, depth, self = path) => {
|
|
2274
|
+
unshown(given, path);
|
|
1966
2275
|
const arrange = registry.get(given.type)?.arrange;
|
|
1967
2276
|
if (!arrange) return walk(given, path, self, depth);
|
|
1968
2277
|
const origins = /* @__PURE__ */ new Map();
|
|
@@ -1989,27 +2298,34 @@ function expandDocument(document, options) {
|
|
|
1989
2298
|
return void 0;
|
|
1990
2299
|
}
|
|
1991
2300
|
let raw2;
|
|
2301
|
+
const reported = [];
|
|
1992
2302
|
try {
|
|
1993
2303
|
const style = { ...definition.defaultStyle, ...node.style };
|
|
1994
|
-
raw2 = definition.expand(propsWithDefaults(definition, node.props), { register: (custom) => registry.add(custom), style, responsive });
|
|
2304
|
+
raw2 = definition.expand(propsWithDefaults(definition, node.props), { register: (custom) => registry.add(custom), style, responsive, report: (diagnostic) => reported.push(diagnostic) });
|
|
1995
2305
|
} catch (error) {
|
|
1996
2306
|
d.error("custom/render", `${node.type} failed to render: ${error instanceof Error ? error.message : String(error)}`, path, node.id);
|
|
1997
2307
|
return void 0;
|
|
1998
2308
|
}
|
|
2309
|
+
const at = `${path}(${node.type})`;
|
|
2310
|
+
for (const diagnostic of reported) d.list.push({ ...diagnostic, path: diagnostic.path === "root" || diagnostic.path.startsWith("root.") ? at + diagnostic.path.slice(4) : at });
|
|
1999
2311
|
if (Array.isArray(raw2) || typeof raw2 !== "object" || raw2 === null) {
|
|
2000
2312
|
d.error("custom/render", `${node.type} renders one component (a Column, a Section, \u2026), not ${Array.isArray(raw2) ? "several" : JSON.stringify(raw2)}`, path, node.id);
|
|
2001
2313
|
return void 0;
|
|
2002
2314
|
}
|
|
2003
|
-
const { node: expanded, diagnostics } = checkFragment(raw2, { parent,
|
|
2315
|
+
const { node: expanded, diagnostics } = checkFragment(raw2, { parent, root: rootDefinition, idPrefix: node.id ?? null, taken, registry, path: `${path}(${node.type})` });
|
|
2004
2316
|
for (const diagnostic of diagnostics) d.list.push(diagnostic);
|
|
2005
2317
|
if (!expanded) return void 0;
|
|
2006
2318
|
collectIds(expanded);
|
|
2007
2319
|
if (node.style?.flex !== void 0) expanded.style = { ...expanded.style, flex: node.style.flex };
|
|
2008
2320
|
if (node.hidden) expanded.hidden = { ...expanded.hidden, ...node.hidden };
|
|
2009
2321
|
if (node.id && !expanded.id && !CUSTOM_TYPE.test(node.type)) expanded.id = node.id;
|
|
2010
|
-
for (const
|
|
2011
|
-
|
|
2012
|
-
|
|
2322
|
+
for (const condition of CONDITIONS) {
|
|
2323
|
+
const rendered = registry.get(expanded.type);
|
|
2324
|
+
for (const [key, value] of Object.entries(node[condition.name] ?? {})) {
|
|
2325
|
+
const spec = rendered?.style[key];
|
|
2326
|
+
if (rendered && spec && condition.allows(spec, key, rendered)) expanded[condition.name] = { [key]: value, ...expanded[condition.name] };
|
|
2327
|
+
else d.warning(`${condition.name}/not-shown`, `${node.type}'s ${key} ${condition.on} isn't shown: what it renders (a ${expanded.type}) has no ${key} ${condition.for}`, `${path}.${condition.name}.${key}`, node.id);
|
|
2328
|
+
}
|
|
2013
2329
|
}
|
|
2014
2330
|
const inner = registry.get(expanded.type);
|
|
2015
2331
|
if (isCustom(inner)) return expandOne(expanded, inner, parent, `${path}(${node.type})`, depth + 1, self);
|
|
@@ -2462,11 +2778,11 @@ var TEXT_CSS = [
|
|
|
2462
2778
|
["textAlign", "text-align", (v) => v],
|
|
2463
2779
|
["textTransform", "text-transform", (v) => v]
|
|
2464
2780
|
];
|
|
2465
|
-
var
|
|
2781
|
+
var same2 = (a2, b) => a2 === b || JSON.stringify(a2) === JSON.stringify(b);
|
|
2466
2782
|
function textDeclarations(text2, parent, always = []) {
|
|
2467
2783
|
const out = [];
|
|
2468
2784
|
for (const [key, property, write3] of TEXT_CSS) {
|
|
2469
|
-
if (!parent || always.includes(key) || !
|
|
2785
|
+
if (!parent || always.includes(key) || !same2(text2[key], parent[key])) out.push([property, write3(text2[key])]);
|
|
2470
2786
|
}
|
|
2471
2787
|
return out;
|
|
2472
2788
|
}
|
|
@@ -2542,7 +2858,9 @@ function classesOf(name, value) {
|
|
|
2542
2858
|
if (name === "class") return (Array.isArray(value) ? value : [value]).filter((v) => typeof v === "string" && v !== "");
|
|
2543
2859
|
const text2 = styleText(value, true);
|
|
2544
2860
|
if (!text2) return [];
|
|
2545
|
-
|
|
2861
|
+
if (name === "phone") return [rule(MOBILE.sheet.media, `${MOBILE.sheet.selector}{${text2}}`)];
|
|
2862
|
+
const selector = name === "hoverWithin" ? HOVER.sheet.within ?? HOVER.sheet.selector : HOVER.sheet.selector;
|
|
2863
|
+
return [rule(HOVER.sheet.media, `${selector}{${text2}}`, HOVER.sheet.level)];
|
|
2546
2864
|
}
|
|
2547
2865
|
function attributes2(props) {
|
|
2548
2866
|
const out = [];
|
|
@@ -2550,7 +2868,7 @@ function attributes2(props) {
|
|
|
2550
2868
|
let slot = -1;
|
|
2551
2869
|
for (const [name, value] of Object.entries(props)) {
|
|
2552
2870
|
if (name === "children" || name === "style" || value === void 0 || value === null || value === false) continue;
|
|
2553
|
-
if (name === "class" || name === "phone" || name === "hover") {
|
|
2871
|
+
if (name === "class" || name === "phone" || name === "hover" || name === "hoverWithin") {
|
|
2554
2872
|
const found = classesOf(name, value);
|
|
2555
2873
|
if (found.length && slot < 0) slot = out.push("") - 1;
|
|
2556
2874
|
classes.push(...found);
|
|
@@ -2566,8 +2884,8 @@ function withId(element, id) {
|
|
|
2566
2884
|
if (!id) return element;
|
|
2567
2885
|
const tag = /^<[a-zA-Z0-9]+/.exec(element.markup)?.[0];
|
|
2568
2886
|
if (!tag) throw new Error("[Elements] An id goes on an element");
|
|
2569
|
-
const
|
|
2570
|
-
return new Html2(
|
|
2887
|
+
const insert2 = (markup) => `${tag} id="${escapeAttr(id)}"${markup.slice(tag.length)}`;
|
|
2888
|
+
return new Html2(insert2(element.markup), { ...element.options, start: element.options.start && insert2(element.options.start) });
|
|
2571
2889
|
}
|
|
2572
2890
|
function h(type, props, ...children) {
|
|
2573
2891
|
const all = props ?? {};
|
|
@@ -2594,6 +2912,98 @@ function Merged({ children }) {
|
|
|
2594
2912
|
}
|
|
2595
2913
|
var open = (element) => new Html2(element.options.start ?? element.markup);
|
|
2596
2914
|
var close = (tag) => new Html2(`</${tag}>`);
|
|
2915
|
+
var BLOCK_MAP_VERSION = 1;
|
|
2916
|
+
var MARK_PAIRS = 16;
|
|
2917
|
+
var BlockMarks = class {
|
|
2918
|
+
constructor(pair) {
|
|
2919
|
+
this.nodes = [];
|
|
2920
|
+
this.open = String.fromCharCode(64976 + pair * 2);
|
|
2921
|
+
this.close = String.fromCharCode(64977 + pair * 2);
|
|
2922
|
+
}
|
|
2923
|
+
};
|
|
2924
|
+
var current = null;
|
|
2925
|
+
function withBlockMarks(marks, write3) {
|
|
2926
|
+
const before = current;
|
|
2927
|
+
current = marks;
|
|
2928
|
+
try {
|
|
2929
|
+
return write3();
|
|
2930
|
+
} finally {
|
|
2931
|
+
current = before;
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
function markBlock(html, node) {
|
|
2935
|
+
if (!current) return html;
|
|
2936
|
+
const mark = `${current.open}${current.nodes.length}${current.close}`;
|
|
2937
|
+
const marked = insert(html, mark, false) ?? insert(html, mark, true);
|
|
2938
|
+
if (!marked) return html;
|
|
2939
|
+
current.nodes.push(node);
|
|
2940
|
+
return marked;
|
|
2941
|
+
}
|
|
2942
|
+
var OUTLOOK_OPEN = "<!--[if mso]>";
|
|
2943
|
+
var OUTLOOK_CLOSE = "<![endif]-->";
|
|
2944
|
+
var TAG2 = /<[a-zA-Z][a-zA-Z0-9]*/y;
|
|
2945
|
+
function tagEnd(markup, outlook) {
|
|
2946
|
+
for (let i = markup.indexOf("<"); i >= 0; i = markup.indexOf("<", i + 1)) {
|
|
2947
|
+
if (markup.startsWith(OUTLOOK_OPEN, i)) {
|
|
2948
|
+
if (outlook) {
|
|
2949
|
+
i += OUTLOOK_OPEN.length - 1;
|
|
2950
|
+
continue;
|
|
2951
|
+
}
|
|
2952
|
+
const end = markup.indexOf(OUTLOOK_CLOSE, i);
|
|
2953
|
+
if (end < 0) return -1;
|
|
2954
|
+
i = end + OUTLOOK_CLOSE.length - 1;
|
|
2955
|
+
continue;
|
|
2956
|
+
}
|
|
2957
|
+
if (markup.startsWith("<!--", i)) {
|
|
2958
|
+
const end = markup.indexOf("-->", i + 4);
|
|
2959
|
+
if (end < 0) return -1;
|
|
2960
|
+
i = end + 2;
|
|
2961
|
+
continue;
|
|
2962
|
+
}
|
|
2963
|
+
TAG2.lastIndex = i;
|
|
2964
|
+
const tag = TAG2.exec(markup);
|
|
2965
|
+
if (tag) return i + tag[0].length;
|
|
2966
|
+
}
|
|
2967
|
+
return -1;
|
|
2968
|
+
}
|
|
2969
|
+
function insert(html, mark, outlook) {
|
|
2970
|
+
const { list, kind: kind2, start } = html.options;
|
|
2971
|
+
if (list) {
|
|
2972
|
+
for (let i = 0; i < list.length; i++) {
|
|
2973
|
+
const marked = insert(list[i], mark, outlook);
|
|
2974
|
+
if (marked) return new Html2(html.markup, { ...html.options, list: list.map((piece, j) => j === i ? marked : piece) });
|
|
2975
|
+
}
|
|
2976
|
+
return null;
|
|
2977
|
+
}
|
|
2978
|
+
if (kind2 === "outlook" && !outlook) return null;
|
|
2979
|
+
const at = tagEnd(html.markup, outlook);
|
|
2980
|
+
if (at < 0) return null;
|
|
2981
|
+
const put = (text2) => text2.slice(0, at) + mark + text2.slice(at);
|
|
2982
|
+
return new Html2(put(html.markup), { ...html.options, start: start !== void 0 && at <= start.length ? put(start) : start });
|
|
2983
|
+
}
|
|
2984
|
+
function readMarks(marks, written) {
|
|
2985
|
+
const found = [];
|
|
2986
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2987
|
+
const pieces2 = [];
|
|
2988
|
+
let length3 = 0;
|
|
2989
|
+
let last = 0;
|
|
2990
|
+
for (const match of written.matchAll(new RegExp(`${marks.open}(\\d+)${marks.close}`, "g"))) {
|
|
2991
|
+
const index = Number(match[1]);
|
|
2992
|
+
const node = marks.nodes[index];
|
|
2993
|
+
if (!node || seen.has(index)) return null;
|
|
2994
|
+
seen.add(index);
|
|
2995
|
+
const before = written.slice(last, match.index);
|
|
2996
|
+
pieces2.push(before);
|
|
2997
|
+
length3 += before.length;
|
|
2998
|
+
last = match.index + match[0].length;
|
|
2999
|
+
if (!/<[a-zA-Z][a-zA-Z0-9]*$/.test(written.slice(Math.max(0, match.index - 32), match.index).replace(new RegExp(`${marks.open}\\d+${marks.close}`, "g"), ""))) return null;
|
|
3000
|
+
found.push({ node, at: length3 });
|
|
3001
|
+
}
|
|
3002
|
+
pieces2.push(written.slice(last));
|
|
3003
|
+
const text2 = pieces2.join("");
|
|
3004
|
+
if (text2.includes(marks.open) || text2.includes(marks.close)) return null;
|
|
3005
|
+
return { text: text2, found };
|
|
3006
|
+
}
|
|
2597
3007
|
function utf8Bytes(text2) {
|
|
2598
3008
|
let bytes = 0;
|
|
2599
3009
|
for (let i = 0; i < text2.length; i++) {
|
|
@@ -2680,7 +3090,7 @@ function fontFaces2(root, { swap }) {
|
|
|
2680
3090
|
return [...faces.values()];
|
|
2681
3091
|
}
|
|
2682
3092
|
var isNode2 = (child) => typeof child !== "string";
|
|
2683
|
-
var shown = (node) =>
|
|
3093
|
+
var shown = (node) => shownAnywhere(node);
|
|
2684
3094
|
function link2(props) {
|
|
2685
3095
|
const { href, target } = linkParts(props.href);
|
|
2686
3096
|
return {
|
|
@@ -2915,7 +3325,7 @@ var Button2 = ({ node, style, mobile, fill, phoneText: phoneText2, text: text2 }
|
|
|
2915
3325
|
{
|
|
2916
3326
|
align: TEXT_ALIGN[node.text.textAlign],
|
|
2917
3327
|
bgcolor: bgcolor(style.backgroundColor),
|
|
2918
|
-
|
|
3328
|
+
hoverWithin: { backgroundColor: node.hover?.backgroundColor },
|
|
2919
3329
|
phone: { border: mobile.border, backgroundColor: mobile.backgroundColor },
|
|
2920
3330
|
style: { backgroundColor: style.backgroundColor, border: style.border, borderRadius: style.borderRadius, msoPaddingAlt: insets2(style.padding) }
|
|
2921
3331
|
},
|
|
@@ -2926,8 +3336,11 @@ var Button2 = ({ node, style, mobile, fill, phoneText: phoneText2, text: text2 }
|
|
|
2926
3336
|
phone: [phoneText2, { padding: mobile.padding }],
|
|
2927
3337
|
hover: { color: node.hover?.color },
|
|
2928
3338
|
style: [
|
|
2929
|
-
// A button with a width fills it.
|
|
2930
|
-
|
|
3339
|
+
// A button with a width fills it. Its label's words stay whole (the email's word-break: break-word
|
|
3340
|
+
// is for long links in text): a button narrower than its padding otherwise sized itself to one letter
|
|
3341
|
+
// and drew its label a letter a line, in Gmail and browsers; it grows to its longest word instead, as
|
|
3342
|
+
// classic Outlook's and Apple Mail's do (captures of Elements 0.3.0, e3-fresh/g-0054).
|
|
3343
|
+
{ display: width.width !== void 0 ? "block" : "inline-block", padding: style.padding, wordBreak: "normal" },
|
|
2931
3344
|
fullText(node),
|
|
2932
3345
|
{ textDecoration: style.textDecoration ?? "none", borderRadius: style.borderRadius }
|
|
2933
3346
|
]
|
|
@@ -3391,27 +3804,42 @@ function propsOf(node, place, options) {
|
|
|
3391
3804
|
options
|
|
3392
3805
|
};
|
|
3393
3806
|
}
|
|
3807
|
+
var UNMAPPED = /* @__PURE__ */ new Set(["LineBreak", "MergeTag"]);
|
|
3394
3808
|
function write(node, place, options) {
|
|
3395
3809
|
if (place.inline) {
|
|
3396
3810
|
if (!shown(node)) return raw("");
|
|
3397
3811
|
const component22 = INLINES[node.type];
|
|
3398
3812
|
if (!component22) throw new Error(`[Elements] The email exporter can't render a ${node.type} inside text`);
|
|
3399
|
-
|
|
3813
|
+
const html = component22(propsOf(node, place, options));
|
|
3814
|
+
return UNMAPPED.has(node.type) ? html : markBlock(html, node);
|
|
3400
3815
|
}
|
|
3401
3816
|
const component2 = BLOCKS[node.type];
|
|
3402
3817
|
if (!component2) throw new Error(`[Elements] The email exporter can't render a ${node.type} here (in a ${place.parent.type})`);
|
|
3403
|
-
return component2(propsOf(node, place, options));
|
|
3818
|
+
return markBlock(component2(propsOf(node, place, options)), node);
|
|
3404
3819
|
}
|
|
3405
3820
|
var EMAIL_PHONE_MARGIN = 20;
|
|
3406
3821
|
var GMAIL_CLIP_BYTES = 102400;
|
|
3407
3822
|
var GMAIL_STYLE_BYTES = 16384;
|
|
3408
3823
|
function renderEmail(resolved, options = {}) {
|
|
3409
3824
|
const root = resolved.root;
|
|
3410
|
-
const sheet = new Stylesheet(options.classPrefix);
|
|
3411
3825
|
const d = new Diagnostics();
|
|
3412
3826
|
const fonts = fontFaces2(root, { swap: false });
|
|
3413
3827
|
const width = root.width ?? 600;
|
|
3414
|
-
const
|
|
3828
|
+
const write3 = (marks) => {
|
|
3829
|
+
const sheet2 = new Stylesheet(options.classPrefix);
|
|
3830
|
+
const markup = withStylesheet(sheet2, () => withBlockMarks(marks, () => String(markBlock(Email2(propsOf(root, { parent: root, fill: false, mobile: {}, inline: false, rtl: false }, options)), root))));
|
|
3831
|
+
return { sheet: sheet2, markup };
|
|
3832
|
+
};
|
|
3833
|
+
let written = options.blockMap ? null : write3(null);
|
|
3834
|
+
let marked = null;
|
|
3835
|
+
for (let pair = 0; !written; pair++) {
|
|
3836
|
+
if (pair === MARK_PAIRS) throw new Error("[Elements] blockMap can't mark this email: its text holds every pair of the noncharacters U+FDD0 to U+FDEF");
|
|
3837
|
+
const marks = new BlockMarks(pair);
|
|
3838
|
+
const attempt = write3(marks);
|
|
3839
|
+
marked = readMarks(marks, attempt.markup);
|
|
3840
|
+
if (marked) written = { sheet: attempt.sheet, markup: marked.text };
|
|
3841
|
+
}
|
|
3842
|
+
const { sheet, markup: email } = written;
|
|
3415
3843
|
const background = root.style.backgroundColor;
|
|
3416
3844
|
const lang = root.props.lang;
|
|
3417
3845
|
const dir = root.props.dir;
|
|
@@ -3435,7 +3863,8 @@ function renderEmail(resolved, options = {}) {
|
|
|
3435
3863
|
`<style>${stylesheet}</style>`
|
|
3436
3864
|
].join("");
|
|
3437
3865
|
const preheader = preview ? `<div style="display:none;max-height:0;overflow:hidden;mso-hide:all" aria-hidden="true" data-skip-in-text="true">${escapeText(preview)}${"͏‌ ".repeat(30)}</div>` : "";
|
|
3438
|
-
const
|
|
3866
|
+
const opening = `<div${attrs({ role: "article", "aria-roledescription": "email", "aria-label": title, lang, dir })}${styleAttr([["background-color", background], ["overflow-wrap", "break-word"], ["word-break", "break-word"]])}>`;
|
|
3867
|
+
const body = `${preheader}${opening}${email}</div>`;
|
|
3439
3868
|
const bodyDeclarations = [
|
|
3440
3869
|
["margin", "0"],
|
|
3441
3870
|
["padding", "0"],
|
|
@@ -3443,20 +3872,38 @@ function renderEmail(resolved, options = {}) {
|
|
|
3443
3872
|
["background-color", background]
|
|
3444
3873
|
];
|
|
3445
3874
|
const bodyStyle = css2(bodyDeclarations);
|
|
3446
|
-
const
|
|
3875
|
+
const shell = `<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html${attrs({ lang, dir, xmlns: "http://www.w3.org/1999/xhtml", "xmlns:v": "urn:schemas-microsoft-com:vml", "xmlns:o": "urn:schemas-microsoft-com:office:office" })}><head>${head}</head><body${styleAttr(bodyDeclarations)}>`;
|
|
3876
|
+
const html = `${shell}${body}</body></html>`;
|
|
3447
3877
|
const bytes = utf8Bytes(html);
|
|
3878
|
+
const gmails = ["gmail", "gmail-app", "gmail-app-other"];
|
|
3448
3879
|
if (bytes > GMAIL_CLIP_BYTES) {
|
|
3449
|
-
d.warning("email/size", `The email is ${Math.round(bytes / 1024)} KB; Gmail clips a message after ${GMAIL_CLIP_BYTES / 1024} KB ("[Message clipped]")`, "root"
|
|
3880
|
+
d.warning("email/size", `The email is ${Math.round(bytes / 1024)} KB; Gmail clips a message after ${GMAIL_CLIP_BYTES / 1024} KB ("[Message clipped]")`, "root", void 0, {
|
|
3881
|
+
clients: gmails,
|
|
3882
|
+
fix: "shorten the email, or move long sections to a page: what follows the clip is behind a link"
|
|
3883
|
+
});
|
|
3450
3884
|
}
|
|
3451
3885
|
const styleBytes = utf8Bytes(stylesheet);
|
|
3452
3886
|
if (styleBytes > GMAIL_STYLE_BYTES) {
|
|
3453
|
-
d.warning("email/style-size", `The stylesheet is ${Math.round(styleBytes / 1024)} KB; Gmail reads ${GMAIL_STYLE_BYTES / 1024} KB of a <style>`, "root"
|
|
3887
|
+
d.warning("email/style-size", `The stylesheet is ${Math.round(styleBytes / 1024)} KB; Gmail reads ${GMAIL_STYLE_BYTES / 1024} KB of a <style>`, "root", void 0, {
|
|
3888
|
+
clients: gmails,
|
|
3889
|
+
fix: "use fewer phone and hover styles: each one adds a rule"
|
|
3890
|
+
});
|
|
3454
3891
|
}
|
|
3455
|
-
return { html, head, body, bodyStyle, diagnostics: d.list };
|
|
3892
|
+
return { html, head, body, bodyStyle, diagnostics: d.list, ...marked ? { blocks: blockMap(resolved, marked.found, email, utf8Bytes(shell + preheader + opening)) } : {} };
|
|
3893
|
+
}
|
|
3894
|
+
function blockMap(resolved, found, email, start) {
|
|
3895
|
+
const templatePath = resolved.templatePath ?? ((path) => path);
|
|
3896
|
+
let offset = start;
|
|
3897
|
+
let from = 0;
|
|
3898
|
+
const entries = found.map(({ node, at }) => {
|
|
3899
|
+
offset += utf8Bytes(email.slice(from, at));
|
|
3900
|
+
from = at;
|
|
3901
|
+
return { path: templatePath(node.path), ...node.id ? { nodeId: node.id } : {}, offset };
|
|
3902
|
+
});
|
|
3903
|
+
return { version: BLOCK_MAP_VERSION, unit: "utf8", entries };
|
|
3456
3904
|
}
|
|
3457
3905
|
var isNode3 = (child) => typeof child !== "string";
|
|
3458
|
-
var
|
|
3459
|
-
var same2 = (a2, b) => JSON.stringify(a2) === JSON.stringify(b);
|
|
3906
|
+
var same3 = (a2, b) => JSON.stringify(a2) === JSON.stringify(b);
|
|
3460
3907
|
function support(root, target) {
|
|
3461
3908
|
const d = new Diagnostics();
|
|
3462
3909
|
const visit = (node) => {
|
|
@@ -3464,41 +3911,81 @@ function support(root, target) {
|
|
|
3464
3911
|
const byProp = definition.defaultStyleBy ? definition.defaultStyleBy.values[String(node.props[definition.defaultStyleBy.prop])] : void 0;
|
|
3465
3912
|
for (const [key, value] of Object.entries(node.style)) {
|
|
3466
3913
|
const fallback2 = byProp?.[key] ?? definition.defaultStyle?.[key];
|
|
3467
|
-
|
|
3914
|
+
if (same3(value, fallback2)) continue;
|
|
3915
|
+
reportDeclared(d, node, `${node.path}.style.${key}`, key, value, definition.style[key]?.support?.[target], target);
|
|
3468
3916
|
}
|
|
3469
|
-
for (const [key, value] of Object.entries(node.props))
|
|
3470
|
-
|
|
3917
|
+
for (const [key, value] of Object.entries(node.props)) {
|
|
3918
|
+
if (same3(value, definition.props[key]?.default)) continue;
|
|
3919
|
+
reportDeclared(d, node, `${node.path}.props.${key}`, key, value, definition.props[key]?.support?.[target], target);
|
|
3920
|
+
}
|
|
3921
|
+
for (const condition of CONDITIONS) {
|
|
3922
|
+
const block = blockOf(node, condition);
|
|
3923
|
+
if (!block || condition.hiddenKey && node.hidden?.[condition.hiddenKey]) continue;
|
|
3924
|
+
const own = condition.support[target];
|
|
3925
|
+
reportDeclared(d, node, `${node.path}.${condition.name}`, condition.name, block, own, target);
|
|
3926
|
+
if (!own || own.level === "none") continue;
|
|
3927
|
+
for (const [key, value] of Object.entries(block)) {
|
|
3928
|
+
const rules = condition.styles?.[key]?.[target] ?? definition.style[key]?.support?.[target];
|
|
3929
|
+
reportDeclared(d, node, `${node.path}.${condition.name}.${key}`, key, value, rules, target, condition.outlookApplies);
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
for (const child of node.children) if (isNode3(child) && shownAnywhere(child)) visit(child);
|
|
3471
3933
|
};
|
|
3472
3934
|
visit(root);
|
|
3473
3935
|
return d.list;
|
|
3474
3936
|
}
|
|
3475
|
-
function
|
|
3476
|
-
const rules = declared?.[target];
|
|
3477
|
-
if (!rules || same2(value, fallback2)) return;
|
|
3478
|
-
for (const rule2 of [].concat(rules)) {
|
|
3479
|
-
if (rule2.level === "full") continue;
|
|
3480
|
-
if (rule2.values && !rule2.values.some((v) => same2(v, value))) continue;
|
|
3481
|
-
if (rule2.when && !rule2.when(value, node)) continue;
|
|
3482
|
-
const at = `${node.path}.${where}.${key}`;
|
|
3483
|
-
const asked = typeof value === "string" ? `${key}: "${value}"` : key;
|
|
3484
|
-
const output = target === "web" ? "a page" : target;
|
|
3485
|
-
if (rule2.level === "none") d.warning(`${target}/unsupported`, `${asked} isn't possible in ${output}: ${rule2.shows}`, at, node.id);
|
|
3486
|
-
else if (rule2.level === "partial") d.add(`${target}/degraded`, "info", `${asked} on this ${node.type} in ${output}: ${rule2.shows}`, at, node.id);
|
|
3487
|
-
else d.add("email/outlook-degraded", "info", `${asked} on this ${node.type} in classic Outlook: ${rule2.shows}`, at, node.id);
|
|
3488
|
-
}
|
|
3489
|
-
}
|
|
3490
|
-
function emailSupport(root) {
|
|
3937
|
+
function emailSupport(root, templatePath = (path) => path) {
|
|
3491
3938
|
const d = new Diagnostics();
|
|
3492
3939
|
d.list.push(...support(root, "email"));
|
|
3493
3940
|
const webFonts = new Set((root.props.fonts ?? []).map((face) => face.family.toLowerCase()));
|
|
3941
|
+
const phone = { styled: false, phoneHidden: false, desktopHidden: false };
|
|
3494
3942
|
const visit = (node) => {
|
|
3495
3943
|
structure(node, d);
|
|
3496
3944
|
fallback(node, webFonts, d);
|
|
3945
|
+
if (node.type === "Html") htmlBlock(node, d);
|
|
3946
|
+
if (node.type === "Image") imageFormat(node, "src", d);
|
|
3947
|
+
if (node.type === "Video" && node.props.thumbnail !== void 0) imageFormat(node, "thumbnail", d);
|
|
3948
|
+
if (node.mobile && Object.keys(node.mobile).length) phone.styled = true;
|
|
3949
|
+
if (node.hidden?.mobile && !builtIn(templatePath(node.path))) phone.phoneHidden = true;
|
|
3950
|
+
if (node.hidden?.desktop && !builtIn(templatePath(node.path))) phone.desktopHidden = true;
|
|
3497
3951
|
for (const child of node.children) if (isNode2(child) && shown(child)) visit(child);
|
|
3498
3952
|
};
|
|
3499
3953
|
visit(root);
|
|
3954
|
+
phoneStyles(root, phone, d);
|
|
3500
3955
|
return d.list;
|
|
3501
3956
|
}
|
|
3957
|
+
function builtIn(place) {
|
|
3958
|
+
const made = [...place.matchAll(/\((\w+)\)/g)].pop()?.[1];
|
|
3959
|
+
return !!made && Object.prototype.hasOwnProperty.call(COMPONENTS, made);
|
|
3960
|
+
}
|
|
3961
|
+
var NO_PHONE_STYLES = ["gmail-app-other", "sfr", "laposte", "gmx", "web-de", "rainloop"];
|
|
3962
|
+
function phoneStyles(root, phone, d) {
|
|
3963
|
+
const parts = [phone.desktopHidden && "what's hidden on desktop shows nowhere", phone.phoneHidden && "what's hidden on phones shows", phone.styled && "it keeps its desktop styles"].filter(Boolean);
|
|
3964
|
+
if (!parts.length) return;
|
|
3965
|
+
d.add("email/phone-styles", phone.desktopHidden ? "warning" : "info", `On phones, the email's phone styles don't apply: ${parts.join(", ")} (Rows still stack)`, root.path, root.id, {
|
|
3966
|
+
clients: NO_PHONE_STYLES,
|
|
3967
|
+
fix: phone.desktopHidden ? "show what a reader needs on desktop too, and hide on phones only what they can do without" : void 0,
|
|
3968
|
+
see: caniemail("css-at-media")
|
|
3969
|
+
});
|
|
3970
|
+
}
|
|
3971
|
+
var IMAGE_FORMATS = {
|
|
3972
|
+
webp: { clients: ["outlook"], feature: "image-webp", name: "a WebP" },
|
|
3973
|
+
avif: { clients: ["outlook", "fastmail", "gmx", "web-de", "orange", "sfr", "mail-ru", "ionos"], feature: "image-avif", name: "an AVIF" },
|
|
3974
|
+
svg: { clients: ["mail-ru"], feature: "image-svg", name: "an SVG" }
|
|
3975
|
+
};
|
|
3976
|
+
var FORMAT_IN = [/^data:image\/([a-z]+)/i, /[?&](?:fm|format|f)=(webp|avif|svg|png|jpe?g|gif)\b/i, /\/f_(webp|avif|svg|png|jpe?g|gif)[,/]/i, /\.([a-z]+)(?:[?#]|$)/i];
|
|
3977
|
+
function imageFormat(node, key, d) {
|
|
3978
|
+
const src = assetParts(node.props[key]).src;
|
|
3979
|
+
const format = FORMAT_IN.map((pattern) => pattern.exec(src)?.[1]).find(Boolean)?.toLowerCase();
|
|
3980
|
+
const known = format && Object.prototype.hasOwnProperty.call(IMAGE_FORMATS, format) ? IMAGE_FORMATS[format] : void 0;
|
|
3981
|
+
if (!known) return;
|
|
3982
|
+
const what = key === "src" ? "This Image is" : "This Video's thumbnail is";
|
|
3983
|
+
d.warning("email/image-format", `${what} ${known.name} file, which these email clients don't show`, `${node.path}.props.${key}`, node.id, {
|
|
3984
|
+
clients: known.clients,
|
|
3985
|
+
fix: "use a PNG, JPEG or GIF file",
|
|
3986
|
+
see: caniemail(known.feature)
|
|
3987
|
+
});
|
|
3988
|
+
}
|
|
3502
3989
|
function fallback(node, webFonts, d) {
|
|
3503
3990
|
const stack = node.style.fontFamily;
|
|
3504
3991
|
if (typeof stack !== "string" || !webFonts.size) return;
|
|
@@ -3507,9 +3994,10 @@ function fallback(node, webFonts, d) {
|
|
|
3507
3994
|
if (!last || !webFonts.has(last.toLowerCase())) return;
|
|
3508
3995
|
d.warning(
|
|
3509
3996
|
"email/font-fallback",
|
|
3510
|
-
`This font stack ends with the web font "${last}": where it doesn't load
|
|
3997
|
+
`This font stack ends with the web font "${last}": where it doesn't load, the email client picks its own font`,
|
|
3511
3998
|
`${node.path}.style.fontFamily`,
|
|
3512
|
-
node.id
|
|
3999
|
+
node.id,
|
|
4000
|
+
{ clients: NO_WEB_FONTS, fix: `end it with a font the client has: "${stack}, Arial, sans-serif"`, see: caniemail("css-at-font-face") }
|
|
3513
4001
|
);
|
|
3514
4002
|
}
|
|
3515
4003
|
function structure(node, d) {
|
|
@@ -3522,7 +4010,8 @@ function structure(node, d) {
|
|
|
3522
4010
|
"info",
|
|
3523
4011
|
`What it holds looks about ${past}px wide on one line, wider than its ${node.contentWidth}px (an estimate: text isn't measured yet): classic Outlook can't wrap it onto another line as other clients do, and draws it wider`,
|
|
3524
4012
|
path,
|
|
3525
|
-
id
|
|
4013
|
+
id,
|
|
4014
|
+
{ clients: ["outlook"], fix: "put less in it, or let it stack on phones (stackOnMobile)" }
|
|
3526
4015
|
);
|
|
3527
4016
|
}
|
|
3528
4017
|
const lines2 = wrapsWithGap(node);
|
|
@@ -3558,15 +4047,95 @@ function structure(node, d) {
|
|
|
3558
4047
|
d.warning("email/image-width", "This Image has no width: Outlook shows it at the size of its file, however wide that is. Give it a width, or the file's width in src", `${path}.props.src`, id);
|
|
3559
4048
|
}
|
|
3560
4049
|
}
|
|
4050
|
+
var HTML_CSS = [
|
|
4051
|
+
{ pattern: /!\s*(?!important\b)[iI][mM][pP][oO][rR][tT][aA][nN][tT]\b/, clients: ["gmail"], removes: "!important not written in lowercase: Gmail removes the whole style attribute, or the whole <style>, that holds it", fix: "write !important in lowercase" },
|
|
4052
|
+
{ pattern: /margin[a-z-]*\s*:([^;"'}]*\s)?-\d/i, clients: ["gmail", "outlook-com"], removes: "a negative margin: the client removes the whole margin declaration", fix: "move it with padding on what holds it instead" },
|
|
4053
|
+
{
|
|
4054
|
+
pattern: /(^|[\s;{"'])position\s*:\s*absolute\b/i,
|
|
4055
|
+
clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "yahoo", "aol", "hey", "sfr", "laposte", "mail-ru", "gmx", "web-de"],
|
|
4056
|
+
removes: "position: absolute: the client removes it",
|
|
4057
|
+
fix: "lay it out without position (a Layer puts content over an image)",
|
|
4058
|
+
feature: "css-position"
|
|
4059
|
+
},
|
|
4060
|
+
{
|
|
4061
|
+
// Outlook.com removes every position (captures); the others not named keep relative.
|
|
4062
|
+
pattern: /(^|[\s;{"'])position\s*:\s*relative\b/i,
|
|
4063
|
+
clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "mail-ru", "gmx", "web-de"],
|
|
4064
|
+
removes: "position: relative: the client removes it, and what it moves stays where it is",
|
|
4065
|
+
fix: "move it with padding instead",
|
|
4066
|
+
feature: "css-position",
|
|
4067
|
+
use: "relative"
|
|
4068
|
+
},
|
|
4069
|
+
{
|
|
4070
|
+
pattern: /var\(\s*--|(^|[\s;{"'])--[\w-]+\s*:/i,
|
|
4071
|
+
clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "yahoo", "aol", "orange", "mail-ru", "fastmail", "gmx", "web-de"],
|
|
4072
|
+
removes: "CSS variables: the client removes var() and their declarations",
|
|
4073
|
+
fix: "write the values themselves",
|
|
4074
|
+
feature: "css-variables"
|
|
4075
|
+
},
|
|
4076
|
+
{
|
|
4077
|
+
pattern: /gradient\(/i,
|
|
4078
|
+
clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail-app", "gmail-app-other", "yahoo", "aol", "orange", "gmx", "web-de"],
|
|
4079
|
+
removes: "a gradient: the client removes it",
|
|
4080
|
+
fix: "give it a solid background color too, or use an image",
|
|
4081
|
+
feature: "css-linear-gradient"
|
|
4082
|
+
},
|
|
4083
|
+
{ pattern: /\b(oklch|oklab|lch|lab|hwb|color-mix|color)\(/i, clients: ["gmail", "outlook-com"], removes: "a newer color syntax: Outlook.com removes the declaration, and Gmail every inline style on its element", fix: "write the color as hex or rgb()" },
|
|
4084
|
+
{ pattern: /rgba?\(\s*[\d.]+%?\s+[\d.]/i, clients: ["gmail"], removes: "rgb() with spaces between its numbers: Gmail removes the whole style attribute, or the whole <style>", fix: "separate them with commas: rgb(8, 121, 161)" },
|
|
4085
|
+
{
|
|
4086
|
+
pattern: /<style[\s>]/i,
|
|
4087
|
+
clients: ["gmail", "gmail-app", "gmail-app-other", "protonmail", "gmx", "web-de", "sfr", "laposte", "rainloop"],
|
|
4088
|
+
removes: "a <style> in the email's body: the client removes it, and what it styles shows without it",
|
|
4089
|
+
fix: "write the styles inline, on the elements they're for",
|
|
4090
|
+
feature: "html-style"
|
|
4091
|
+
},
|
|
4092
|
+
{ pattern: /box-shadow\s*:/i, clients: ["outlook", "gmail", "gmail-app", "yahoo", "aol", "orange", "gmx", "web-de"], removes: "box-shadow: the client removes it", fix: "draw it with a border, or leave it out", feature: "css-box-shadow" },
|
|
4093
|
+
{ pattern: /text-shadow\s*:/i, clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other"], removes: "text-shadow: the client removes it", fix: "leave it out, and keep the text readable without it", feature: "css-text-shadow" },
|
|
4094
|
+
{ pattern: /display\s*:\s*flex\b/i, clients: ["outlook", "gmail-app-other", "orange", "gmx", "web-de"], removes: "display: flex: the client lays it out as blocks", fix: "lay it out with a Row", feature: "css-display-flex" },
|
|
4095
|
+
{
|
|
4096
|
+
pattern: /display\s*:\s*inline-flex\b/i,
|
|
4097
|
+
clients: ["outlook", "gmail-app-other", "yahoo", "aol", "orange", "gmx", "web-de"],
|
|
4098
|
+
removes: "display: inline-flex: the client lays it out without flex",
|
|
4099
|
+
fix: "lay it out with a Row",
|
|
4100
|
+
feature: "css-display-flex",
|
|
4101
|
+
use: "inline"
|
|
4102
|
+
},
|
|
4103
|
+
{ pattern: /display\s*:\s*(inline-)?grid\b/i, clients: ["outlook", "gmail-app-other", "orange", "gmx", "web-de"], removes: "display: grid: the client lays it out as blocks", fix: "lay it out with a Row or a Grid", feature: "css-display-grid" },
|
|
4104
|
+
{ pattern: /(^|[\s;{"'])opacity\s*:/i, clients: ["outlook", "aol", "orange", "mail-ru", "gmx", "web-de"], removes: "opacity: the client shows it opaque", fix: "use a lighter color instead", feature: "css-opacity" },
|
|
4105
|
+
{
|
|
4106
|
+
pattern: /object-fit\s*:/i,
|
|
4107
|
+
clients: ["outlook", "gmail-app-other", "yahoo", "aol", "orange", "gmx", "web-de"],
|
|
4108
|
+
removes: "object-fit: the client stretches the image to its box",
|
|
4109
|
+
fix: "crop the image file to the size it's shown at",
|
|
4110
|
+
feature: "css-object-fit"
|
|
4111
|
+
},
|
|
4112
|
+
{
|
|
4113
|
+
pattern: /(^|[\s;{"'])aspect-ratio\s*:/i,
|
|
4114
|
+
clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "yahoo", "aol", "samsung-email", "thunderbird", "fastmail", "gmx", "web-de", "orange"],
|
|
4115
|
+
removes: "aspect-ratio: the client removes it",
|
|
4116
|
+
fix: "give it a width and a height",
|
|
4117
|
+
feature: "css-aspect-ratio"
|
|
4118
|
+
}
|
|
4119
|
+
];
|
|
4120
|
+
var OUTLOOK_ONLY2 = /<!--\[if\s+(?:(?:gte?|lte?)\s+)?mso[^\]]*\]>[\s\S]*?<!\[endif\]-->/gi;
|
|
4121
|
+
function htmlBlock(node, d) {
|
|
4122
|
+
if (typeof node.props.html !== "string") return;
|
|
4123
|
+
const markup = node.props.html.replace(OUTLOOK_ONLY2, "");
|
|
4124
|
+
for (const check of HTML_CSS) {
|
|
4125
|
+
if (check.pattern.test(markup)) {
|
|
4126
|
+
d.add("email/html-css", "info", `This Html block's markup may have ${check.removes}`, `${node.path}.props.html`, node.id, { clients: check.clients, fix: check.fix, see: check.feature && caniemail(check.feature) });
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
3561
4130
|
var isNode4 = (child) => typeof child !== "string";
|
|
3562
4131
|
var OUTPUT = { pdf: "a PDF", image: "an image" };
|
|
3563
4132
|
function pagedSupport(root, output) {
|
|
3564
4133
|
const d = new Diagnostics();
|
|
3565
4134
|
const where = OUTPUT[output];
|
|
3566
4135
|
const visit = (node) => {
|
|
3567
|
-
if (node
|
|
3568
|
-
|
|
3569
|
-
d.warning(`${output}/unsupported`,
|
|
4136
|
+
if (!shownAnywhere(node)) return;
|
|
4137
|
+
for (const condition of CONDITIONS) {
|
|
4138
|
+
if (condition.paged && blockOf(node, condition)) d.warning(`${output}/unsupported`, condition.paged(node, where), `${node.path}.${condition.name}`, node.id);
|
|
3570
4139
|
}
|
|
3571
4140
|
if (node.type === "Video") {
|
|
3572
4141
|
const shows = output === "pdf" ? "its thumbnail (where its file is given) or its title, linked to it" : "its title";
|
|
@@ -3719,15 +4288,15 @@ function classesFor(node, parent) {
|
|
|
3719
4288
|
const classes = [];
|
|
3720
4289
|
if (node.mobile) {
|
|
3721
4290
|
const { width, ...rest } = node.mobile;
|
|
3722
|
-
if (Object.keys(rest).length) classes.push(rule(
|
|
4291
|
+
if (Object.keys(rest).length) classes.push(rule(MOBILE.sheet.media, `${MOBILE.sheet.selector}{${css2(declarationsOf(rest), true)}}`));
|
|
3723
4292
|
const line2 = node.type === "Divider" && node.style.padding !== void 0 ? ">hr" : "";
|
|
3724
|
-
if (width !== void 0) classes.push(rule(
|
|
4293
|
+
if (width !== void 0) classes.push(rule(MOBILE.sheet.media, `${MOBILE.sheet.selector}${line2}{${css2(declarationsOf({ width }), true)}}`));
|
|
3725
4294
|
const own = node.style.width === void 0 && (node.style.flex === "none" || node.style.flex === 0);
|
|
3726
4295
|
const basis = own ? void 0 : length2(phoneWidthInRow(node, parent));
|
|
3727
4296
|
if (basis !== void 0) classes.push(rule("phone", `&{flex:0 1 ${basis}!important}`));
|
|
3728
4297
|
}
|
|
3729
4298
|
classes.push(...hidingClasses(node.hidden));
|
|
3730
|
-
if (node.hover) classes.push(rule(
|
|
4299
|
+
if (node.hover) classes.push(rule(HOVER.sheet.media, `${HOVER.sheet.selector}{${css2(declarationsOf(node.hover), true)}}`, HOVER.sheet.level));
|
|
3731
4300
|
if (node.type === "Row" && node.props.stackOnMobile !== false) {
|
|
3732
4301
|
classes.push(
|
|
3733
4302
|
rule("phone", "&{flex-direction:column!important;align-items:stretch!important}&>*{flex:0 0 auto!important;width:auto!important;max-width:100%!important;align-self:stretch!important}", "layout")
|
|
@@ -3935,17 +4504,17 @@ var Layer3 = ({ node, style, parent, placement, classes, children }) => /* @__PU
|
|
|
3935
4504
|
},
|
|
3936
4505
|
children()
|
|
3937
4506
|
);
|
|
3938
|
-
var
|
|
4507
|
+
var current2 = { page: 1, total: 1 };
|
|
3939
4508
|
function numbering(numbers, write3) {
|
|
3940
|
-
const before =
|
|
3941
|
-
|
|
4509
|
+
const before = current2;
|
|
4510
|
+
current2 = numbers;
|
|
3942
4511
|
try {
|
|
3943
4512
|
return write3();
|
|
3944
4513
|
} finally {
|
|
3945
|
-
|
|
4514
|
+
current2 = before;
|
|
3946
4515
|
}
|
|
3947
4516
|
}
|
|
3948
|
-
var PageNumber2 = ({ node }) => /* @__PURE__ */ h(Fragment, null, String(node.props.total ?
|
|
4517
|
+
var PageNumber2 = ({ node }) => /* @__PURE__ */ h(Fragment, null, String(node.props.total ? current2.total : current2.page));
|
|
3949
4518
|
function documentCss(root) {
|
|
3950
4519
|
const box2 = pageBox(root.props);
|
|
3951
4520
|
return `@page{size:${box2.css};margin:0}body{-webkit-print-color-adjust:exact;print-color-adjust:exact}@media screen{body{background:#e5e7eb;padding:24px 0}.elements-page{box-shadow:0 1px 4px rgba(0,0,0,.2)}.elements-page+.elements-page{margin-top:24px!important}}`;
|
|
@@ -4119,8 +4688,23 @@ var Video3 = ({ node, style, placement, classes }) => /* @__PURE__ */ h(
|
|
|
4119
4688
|
))
|
|
4120
4689
|
);
|
|
4121
4690
|
var PAGE_PHONE_WIDTH = 480;
|
|
4122
|
-
|
|
4123
|
-
|
|
4691
|
+
function pagePhoneWidth(root) {
|
|
4692
|
+
const mobile = root.props.breakpoints?.mobile;
|
|
4693
|
+
return typeof mobile === "number" ? mobile : PAGE_PHONE_WIDTH;
|
|
4694
|
+
}
|
|
4695
|
+
function pageBreakpoints(root) {
|
|
4696
|
+
const mobile = root.props.breakpoints?.mobile;
|
|
4697
|
+
const width = typeof root.props.width === "number" ? root.props.width : void 0;
|
|
4698
|
+
if (root.type !== "Page" || typeof mobile !== "number" || width === void 0 || mobile < width) return [];
|
|
4699
|
+
return [
|
|
4700
|
+
{
|
|
4701
|
+
code: "page/phone-width",
|
|
4702
|
+
severity: "warning",
|
|
4703
|
+
message: `Its phone styles apply to every window up to ${mobile}px, wider than its ${width}px content: on those, the content is at its full width with its phone styles. Set breakpoints.mobile below ${width}`,
|
|
4704
|
+
path: "root.props.breakpoints"
|
|
4705
|
+
}
|
|
4706
|
+
];
|
|
4707
|
+
}
|
|
4124
4708
|
var COMPONENTS2 = {
|
|
4125
4709
|
Section: Section3,
|
|
4126
4710
|
Row: Row3,
|
|
@@ -4181,7 +4765,8 @@ function renderPage(resolved, options = {}) {
|
|
|
4181
4765
|
const writer2 = document ? Document2 : root.type === "Canvas" ? Canvas2 : Page2;
|
|
4182
4766
|
const body = withStylesheet(sheet, () => String(writer2(propsOf2(root, root, [], options))));
|
|
4183
4767
|
const fixed = !root.definition.responsive;
|
|
4184
|
-
const
|
|
4768
|
+
const phoneWidth2 = pagePhoneWidth(root);
|
|
4769
|
+
const stylesheet = [...fonts, sheet.css(fixed ? null : `@media (max-width: ${phoneWidth2}px)`, fixed ? null : `@media (min-width: ${phoneWidth2 + 1}px)`), document ? documentCss(root) : ""].join("");
|
|
4185
4770
|
const head = [
|
|
4186
4771
|
'<meta charset="utf-8">',
|
|
4187
4772
|
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
@@ -4201,7 +4786,7 @@ function renderText(resolved, options = {}) {
|
|
|
4201
4786
|
const tag = options.mergeTag ?? ((name) => `{{${name}}}`);
|
|
4202
4787
|
const inline = (node, upper = false) => node.children.map((child) => {
|
|
4203
4788
|
if (typeof child === "string") return upper ? child.toUpperCase() : child;
|
|
4204
|
-
if (child
|
|
4789
|
+
if (!shownAnywhere(child)) return "";
|
|
4205
4790
|
switch (child.type) {
|
|
4206
4791
|
case "LineBreak":
|
|
4207
4792
|
return "\n";
|
|
@@ -4225,7 +4810,7 @@ function renderText(resolved, options = {}) {
|
|
|
4225
4810
|
return alt ? `[${upper ? alt.toUpperCase() : alt}]` : "";
|
|
4226
4811
|
};
|
|
4227
4812
|
const blocks2 = (node) => node.children.flatMap((child) => {
|
|
4228
|
-
if (typeof child === "string" || child
|
|
4813
|
+
if (typeof child === "string" || !shownAnywhere(child)) return [];
|
|
4229
4814
|
switch (child.type) {
|
|
4230
4815
|
case "Heading":
|
|
4231
4816
|
return [tidy(inline(child, Number(child.props.level) <= 2))];
|
|
@@ -4263,8 +4848,8 @@ function renderText(resolved, options = {}) {
|
|
|
4263
4848
|
return blocks2(child);
|
|
4264
4849
|
}
|
|
4265
4850
|
});
|
|
4266
|
-
const
|
|
4267
|
-
const list = (node, indent) => node.children.filter((child) => typeof child !== "string" &&
|
|
4851
|
+
const shown22 = (node) => shownAnywhere(node);
|
|
4852
|
+
const list = (node, indent) => node.children.filter((child) => typeof child !== "string" && shown22(child)).map((item, index) => {
|
|
4268
4853
|
const marker2 = node.props.type === "number" ? `${Number(node.props.start) + index}. ` : "- ";
|
|
4269
4854
|
const lines2 = [];
|
|
4270
4855
|
let run = [];
|
|
@@ -4280,13 +4865,13 @@ function renderText(resolved, options = {}) {
|
|
|
4280
4865
|
}
|
|
4281
4866
|
flush();
|
|
4282
4867
|
if (!lines2.length) lines2.push(`${indent}${marker2}`.trimEnd());
|
|
4283
|
-
if (
|
|
4868
|
+
if (shown22(child)) lines2.push(list(child, `${indent} `));
|
|
4284
4869
|
}
|
|
4285
4870
|
flush();
|
|
4286
4871
|
return (lines2.length ? lines2 : [`${indent}${marker2}`.trimEnd()]).join("\n");
|
|
4287
4872
|
}).join("\n");
|
|
4288
|
-
const table = (node) => node.children.filter((row) => typeof row !== "string" &&
|
|
4289
|
-
(row) => row.children.filter((cell) => typeof cell !== "string" &&
|
|
4873
|
+
const table = (node) => node.children.filter((row) => typeof row !== "string" && shown22(row)).map(
|
|
4874
|
+
(row) => row.children.filter((cell) => typeof cell !== "string" && shown22(cell)).map((cell) => tidy(inline(cell))).join(" | ")
|
|
4290
4875
|
).join("\n");
|
|
4291
4876
|
return blocks2(resolved.root).filter((block) => block.length > 0).join("\n\n");
|
|
4292
4877
|
}
|
|
@@ -4329,45 +4914,39 @@ var OUTPUTS = {
|
|
|
4329
4914
|
Email: { target: "email", ready: true }
|
|
4330
4915
|
};
|
|
4331
4916
|
function prepareTemplate(input, options = {}) {
|
|
4917
|
+
checkEmailClients(options.emailClients);
|
|
4332
4918
|
const registry = registryOf(options);
|
|
4333
4919
|
const checked = checkDocument(input, registry);
|
|
4334
4920
|
const diagnostics = [...checked.diagnostics];
|
|
4335
4921
|
const failed = () => diagnostics.some((d) => d.severity === "error");
|
|
4336
4922
|
if (!checked.document || failed()) return { resolved: null, target: null, diagnostics };
|
|
4337
4923
|
const root = checked.document.root.type;
|
|
4924
|
+
const done = (resolved2, target) => {
|
|
4925
|
+
if (root !== "Email" && options.emailClients) diagnostics.push({ code: "email/clients-ignored", severity: "info", message: `emailClients is for an Email; a ${root} isn't checked for email clients`, path: "root" });
|
|
4926
|
+
return { resolved: resolved2, target, diagnostics: root === "Email" ? forEmailClients(diagnostics, options.emailClients) : diagnostics };
|
|
4927
|
+
};
|
|
4338
4928
|
const output = OUTPUTS[root];
|
|
4339
4929
|
if (!output?.ready) {
|
|
4340
4930
|
diagnostics.push({ code: "output/unavailable", severity: "error", message: output?.later ?? `A ${root} can't be rendered yet`, path: "root" });
|
|
4341
|
-
return
|
|
4931
|
+
return done(null, null);
|
|
4342
4932
|
}
|
|
4343
|
-
const expanded = expandDocument(checked.document, registry);
|
|
4933
|
+
const expanded = expandDocument(checked.document, registry, output.target);
|
|
4344
4934
|
diagnostics.push(...expanded.diagnostics);
|
|
4345
|
-
if (failed()) return
|
|
4935
|
+
if (failed()) return done(null, output.target);
|
|
4346
4936
|
diagnostics.push(...checkTargetExpanded(expanded.document, output.target, registry, expanded.paths));
|
|
4347
|
-
if (failed()) return
|
|
4937
|
+
if (failed()) return done(null, output.target);
|
|
4348
4938
|
const resolved = resolveExpanded(expanded.document, registry, expanded.paths);
|
|
4349
4939
|
diagnostics.push(...resolved.diagnostics);
|
|
4350
|
-
const found = output.target === "email" ? emailSupport(resolved.root) : support(resolved.root, output.target);
|
|
4940
|
+
const found = output.target === "email" ? emailSupport(resolved.root, resolved.templatePath) : support(resolved.root, output.target);
|
|
4351
4941
|
if (root === "Document" || root === "Canvas") found.push(...overflow(resolved.root));
|
|
4352
|
-
|
|
4942
|
+
found.push(...pageBreakpoints(resolved.root));
|
|
4353
4943
|
diagnostics.push(...inTemplate(resolved, found));
|
|
4354
|
-
return
|
|
4944
|
+
return done(failed() ? null : resolved, output.target);
|
|
4355
4945
|
}
|
|
4356
4946
|
function inTemplate(resolved, diagnostics) {
|
|
4357
4947
|
const { templatePath } = resolved;
|
|
4358
4948
|
return templatePath ? diagnostics.map((diagnostic) => ({ ...diagnostic, path: templatePath(diagnostic.path) })) : diagnostics;
|
|
4359
4949
|
}
|
|
4360
|
-
function pageNumbersOutside(root) {
|
|
4361
|
-
const out = [];
|
|
4362
|
-
const visit = (node) => {
|
|
4363
|
-
if (node.type === "PageNumber") {
|
|
4364
|
-
out.push({ code: "document/page-number", severity: "error", message: `A PageNumber is a page's number in a Document; this ${root.type} has no pages`, path: node.path, ...node.id ? { nodeId: node.id } : {} });
|
|
4365
|
-
}
|
|
4366
|
-
for (const child of node.children) if (typeof child !== "string") visit(child);
|
|
4367
|
-
};
|
|
4368
|
-
visit(root);
|
|
4369
|
-
return out;
|
|
4370
|
-
}
|
|
4371
4950
|
function validateTemplate(input, options = {}) {
|
|
4372
4951
|
const { diagnostics, resolved } = prepareTemplate(input, options);
|
|
4373
4952
|
const root = resolved?.root;
|
|
@@ -4376,12 +4955,22 @@ function validateTemplate(input, options = {}) {
|
|
|
4376
4955
|
return { valid: !all.some((d) => d.severity === "error"), diagnostics: all };
|
|
4377
4956
|
}
|
|
4378
4957
|
function renderTemplate(input, options = {}) {
|
|
4958
|
+
if (options.blockMap && options.pretty) throw new ElementsError([{ code: "render/block-map-pretty", severity: "error", message: "blockMap gives offsets in the compact HTML; leave pretty off", path: "root" }]);
|
|
4379
4959
|
const { resolved, target, diagnostics } = prepareTemplate(input, options);
|
|
4380
4960
|
if (!resolved) throw new ElementsError(diagnostics);
|
|
4381
4961
|
const text2 = renderText(resolved, options);
|
|
4382
4962
|
const written = target === "email" ? renderEmail(resolved, options) : { ...renderPage(resolved, options), diagnostics: [] };
|
|
4383
|
-
const
|
|
4384
|
-
|
|
4963
|
+
const shown22 = (html) => options.pretty ? prettyHtml(html) : html;
|
|
4964
|
+
const blocks2 = "blocks" in written ? written.blocks : void 0;
|
|
4965
|
+
return {
|
|
4966
|
+
html: shown22(written.html),
|
|
4967
|
+
head: shown22(written.head),
|
|
4968
|
+
body: shown22(written.body),
|
|
4969
|
+
bodyStyle: written.bodyStyle,
|
|
4970
|
+
text: text2,
|
|
4971
|
+
diagnostics: [...diagnostics, ...inTemplate(resolved, forEmailClients(written.diagnostics, options.emailClients))],
|
|
4972
|
+
...blocks2 ? { blocks: blocks2 } : {}
|
|
4973
|
+
};
|
|
4385
4974
|
}
|
|
4386
4975
|
|
|
4387
4976
|
// src/utils/collect.ts
|
|
@@ -4412,7 +5001,7 @@ function isAsyncComponent(type) {
|
|
|
4412
5001
|
}
|
|
4413
5002
|
var SHARED2 = Symbol.for("@hyper-elements/react/render-builds");
|
|
4414
5003
|
var using = globalThis[SHARED2] ?? (globalThis[SHARED2] = []);
|
|
4415
|
-
var
|
|
5004
|
+
var same4 = (a2, b) => a2.renderToString === b.renderToString;
|
|
4416
5005
|
function dispatcher() {
|
|
4417
5006
|
const internals = React2;
|
|
4418
5007
|
return internals.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE?.H ?? internals.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?.ReactCurrentDispatcher?.current ?? null;
|
|
@@ -4458,7 +5047,7 @@ function secondBuild(outer) {
|
|
|
4458
5047
|
for (const requirer of requirers) {
|
|
4459
5048
|
try {
|
|
4460
5049
|
const build = requirer()?.(id);
|
|
4461
|
-
if (build?.renderToStaticMarkup && !
|
|
5050
|
+
if (build?.renderToStaticMarkup && !same4(build, defaultBuild) && ![null, outer].includes(dispatcherOf(build))) {
|
|
4462
5051
|
second = build;
|
|
4463
5052
|
break search;
|
|
4464
5053
|
}
|
|
@@ -4472,11 +5061,11 @@ function secondBuild(outer) {
|
|
|
4472
5061
|
var buildOf = (outer) => outer === null ? void 0 : [defaultBuild, second].find((b) => !!b && dispatcherOf(b) === outer);
|
|
4473
5062
|
function withBuild(render2) {
|
|
4474
5063
|
const outer = dispatcher();
|
|
4475
|
-
let build =
|
|
5064
|
+
let build = current3();
|
|
4476
5065
|
const theirs = using.length === 0 || collecting() !== void 0 ? buildOf(outer) : void 0;
|
|
4477
5066
|
const busy = [];
|
|
4478
5067
|
if (theirs) {
|
|
4479
|
-
const free = (b) => !
|
|
5068
|
+
const free = (b) => !same4(b, theirs) && !using.some((u) => same4(u, b));
|
|
4480
5069
|
const other = free(defaultBuild) ? defaultBuild : secondBuild(outer);
|
|
4481
5070
|
if (other && free(other)) {
|
|
4482
5071
|
busy.push(theirs);
|
|
@@ -4501,10 +5090,10 @@ function rendersHere(build) {
|
|
|
4501
5090
|
return false;
|
|
4502
5091
|
}
|
|
4503
5092
|
}
|
|
4504
|
-
function
|
|
5093
|
+
function current3() {
|
|
4505
5094
|
return using[using.length - 1] ?? defaultBuild;
|
|
4506
5095
|
}
|
|
4507
|
-
var renderToString = (element, options) =>
|
|
5096
|
+
var renderToString = (element, options) => current3().renderToString(element, options);
|
|
4508
5097
|
|
|
4509
5098
|
// src/utils/render-scope.ts
|
|
4510
5099
|
function withRenderScope(render2) {
|
|
@@ -4558,7 +5147,7 @@ function withoutAsync(children, collection) {
|
|
|
4558
5147
|
}
|
|
4559
5148
|
var ENTITIES2 = { "&": "&", "<": "<", ">": ">", """: '"', "'": "'" };
|
|
4560
5149
|
var unescape = (text2) => text2.replace(/&|<|>|"|'/g, (entity) => ENTITIES2[entity]);
|
|
4561
|
-
var NODE_KEYS2 = /* @__PURE__ */ new Set(["id", "style", "
|
|
5150
|
+
var NODE_KEYS2 = /* @__PURE__ */ new Set(["id", "style", "hidden"]);
|
|
4562
5151
|
function toJsonNode({ type, props }, diagnostics) {
|
|
4563
5152
|
const node = { type, children: [] };
|
|
4564
5153
|
const semantic = {};
|
|
@@ -4683,10 +5272,10 @@ function collectTemplate(element) {
|
|
|
4683
5272
|
path: "jsx"
|
|
4684
5273
|
});
|
|
4685
5274
|
}
|
|
4686
|
-
return { document: null, diagnostics, components };
|
|
5275
|
+
return { document: null, diagnostics, components, collected: null };
|
|
4687
5276
|
}
|
|
4688
5277
|
const result = checkDocument({ root: top[0] }, new Registry(components));
|
|
4689
|
-
return { document: result.document, diagnostics: [...diagnostics, ...result.diagnostics], components };
|
|
5278
|
+
return { document: result.document, diagnostics: [...diagnostics, ...result.diagnostics], components, collected: top[0] };
|
|
4690
5279
|
}
|
|
4691
5280
|
|
|
4692
5281
|
// src/primitives/components.tsx
|
|
@@ -4776,7 +5365,8 @@ function component(spec) {
|
|
|
4776
5365
|
expand(props, context) {
|
|
4777
5366
|
const { nodes, components, diagnostics } = collectNodes(spec.render(props));
|
|
4778
5367
|
for (const used of components) context.register(used);
|
|
4779
|
-
if (diagnostics.
|
|
5368
|
+
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) throw new Error(formatDiagnostics(diagnostics));
|
|
5369
|
+
for (const diagnostic of diagnostics) context.report(diagnostic);
|
|
4780
5370
|
return nodes.length === 1 ? nodes[0] : nodes;
|
|
4781
5371
|
}
|
|
4782
5372
|
});
|
|
@@ -4795,12 +5385,12 @@ function component(spec) {
|
|
|
4795
5385
|
// src/primitives/outline.ts
|
|
4796
5386
|
var LONGEST = 80;
|
|
4797
5387
|
var clip = (text2) => text2.length > LONGEST ? `${text2.slice(0, LONGEST - 1)}\u2026` : text2;
|
|
4798
|
-
var
|
|
4799
|
-
var blocks = (node) => node.children.filter((child) => typeof child !== "string" &&
|
|
5388
|
+
var shown2 = (node) => shownAnywhere(node);
|
|
5389
|
+
var blocks = (node) => node.children.filter((child) => typeof child !== "string" && shown2(child));
|
|
4800
5390
|
function words(node) {
|
|
4801
5391
|
return node.children.map((child) => {
|
|
4802
5392
|
if (typeof child === "string") return child;
|
|
4803
|
-
if (!
|
|
5393
|
+
if (!shown2(child) || child.type === "List") return "";
|
|
4804
5394
|
if (child.type === "MergeTag") return `{{${child.props.name}}}`;
|
|
4805
5395
|
if (child.type === "LineBreak") return " / ";
|
|
4806
5396
|
if (child.type === "Image") return `[${child.props.alt ?? ""}]`;
|
|
@@ -4851,14 +5441,16 @@ function template(element, call) {
|
|
|
4851
5441
|
return collectTemplate(element);
|
|
4852
5442
|
}
|
|
4853
5443
|
async function render(element, options = {}) {
|
|
5444
|
+
checkEmailClients(options.emailClients);
|
|
4854
5445
|
const { document, diagnostics, components } = template(element, "render");
|
|
4855
5446
|
if (!document || diagnostics.some((d) => d.severity === "error")) throw new ElementsError(diagnostics);
|
|
4856
5447
|
const rendered = renderTemplate(document, { ...options, components });
|
|
4857
5448
|
return { ...rendered, diagnostics: [...diagnostics, ...rendered.diagnostics] };
|
|
4858
5449
|
}
|
|
4859
|
-
function validate(element) {
|
|
5450
|
+
function validate(element, options = {}) {
|
|
5451
|
+
checkEmailClients(options.emailClients);
|
|
4860
5452
|
const { document, diagnostics, components } = template(element, "validate");
|
|
4861
|
-
const all = [...diagnostics, ...document && !diagnostics.some((d) => d.severity === "error") ? validateTemplate(document, { components }).diagnostics : []];
|
|
5453
|
+
const all = [...diagnostics, ...document && !diagnostics.some((d) => d.severity === "error") ? validateTemplate(document, { components, emailClients: options.emailClients }).diagnostics : []];
|
|
4862
5454
|
return { valid: !all.some((d) => d.severity === "error"), diagnostics: all };
|
|
4863
5455
|
}
|
|
4864
5456
|
function outline(element) {
|
|
@@ -4869,15 +5461,16 @@ function outline(element) {
|
|
|
4869
5461
|
if (!root) throw new ElementsError([...diagnostics, ...prepared.diagnostics]);
|
|
4870
5462
|
return outlineOf(root);
|
|
4871
5463
|
}
|
|
4872
|
-
function prepare(element) {
|
|
5464
|
+
function prepare(element, options = {}) {
|
|
5465
|
+
checkEmailClients(options.emailClients);
|
|
4873
5466
|
const { document, diagnostics, components } = template(element, "prepare");
|
|
4874
5467
|
if (!document || diagnostics.some((d) => d.severity === "error")) return { root: null, diagnostics, layout: LAYOUT_VERSION };
|
|
4875
|
-
const prepared = prepareTemplate(document, { components });
|
|
5468
|
+
const prepared = prepareTemplate(document, { components, emailClients: options.emailClients });
|
|
4876
5469
|
const root = prepared.resolved?.root ?? null;
|
|
4877
5470
|
const paged = root?.type === "Document" ? pagedSupport(root, "pdf") : root?.type === "Canvas" ? pagedSupport(root, "image") : [];
|
|
4878
5471
|
return { root, diagnostics: [...diagnostics, ...prepared.diagnostics, ...paged], layout: LAYOUT_VERSION };
|
|
4879
5472
|
}
|
|
4880
5473
|
|
|
4881
|
-
export { Button4 as Button, Canvas3 as Canvas, Circle2 as Circle, Column4 as Column, Divider4 as Divider, Document3 as Document, ElementsError, Ellipse2 as Ellipse, Email3 as Email, Grid2 as Grid, Heading4 as Heading, Html3 as Html, Image4 as Image, Layer4 as Layer, Line2 as Line, LineBreak4 as LineBreak, Link4 as Link, List4 as List, ListItem4 as ListItem, Menu2 as Menu, MergeTag4 as MergeTag, Page3 as Page, PageBreak2 as PageBreak, PageFooter2 as PageFooter, PageHeader2 as PageHeader, PageNumber3 as PageNumber, Path2 as Path, Polygon2 as Polygon, Rect2 as Rect, Row4 as Row, Section4 as Section, Social2 as Social, Span4 as Span, Svg3 as Svg, Table5 as Table, TableCell4 as TableCell, TableRow4 as TableRow, Text4 as Text, Video4 as Video, component, formatDiagnostics, outline, prepare, prop2 as prop, render, validate };
|
|
5474
|
+
export { BLOCK_MAP_VERSION, Button4 as Button, Canvas3 as Canvas, Circle2 as Circle, Column4 as Column, DEFAULT_EMAIL_CLIENTS, Divider4 as Divider, Document3 as Document, EMAIL_CLIENTS, ElementsError, Ellipse2 as Ellipse, Email3 as Email, Grid2 as Grid, Heading4 as Heading, Html3 as Html, Image4 as Image, Layer4 as Layer, Line2 as Line, LineBreak4 as LineBreak, Link4 as Link, List4 as List, ListItem4 as ListItem, Menu2 as Menu, MergeTag4 as MergeTag, Page3 as Page, PageBreak2 as PageBreak, PageFooter2 as PageFooter, PageHeader2 as PageHeader, PageNumber3 as PageNumber, Path2 as Path, Polygon2 as Polygon, Rect2 as Rect, Row4 as Row, Section4 as Section, Social2 as Social, Span4 as Span, Svg3 as Svg, Table5 as Table, TableCell4 as TableCell, TableRow4 as TableRow, Text4 as Text, Video4 as Video, component, formatDiagnostics, outline, prepare, prop2 as prop, render, validate };
|
|
4882
5475
|
//# sourceMappingURL=primitives.js.map
|
|
4883
5476
|
//# sourceMappingURL=primitives.js.map
|