@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.cjs
CHANGED
|
@@ -34,21 +34,26 @@ var Diagnostics = class {
|
|
|
34
34
|
constructor() {
|
|
35
35
|
this.list = [];
|
|
36
36
|
}
|
|
37
|
-
add(code, severity, message, path, nodeId) {
|
|
38
|
-
|
|
37
|
+
add(code, severity, message, path, nodeId, extra = {}) {
|
|
38
|
+
const d = { code, severity, message, path };
|
|
39
|
+
if (nodeId !== void 0) d.nodeId = nodeId;
|
|
40
|
+
if (extra.clients) d.clients = [...extra.clients];
|
|
41
|
+
if (extra.fix) d.fix = extra.fix;
|
|
42
|
+
if (extra.see) d.see = extra.see;
|
|
43
|
+
this.list.push(d);
|
|
39
44
|
}
|
|
40
45
|
error(code, message, path, nodeId) {
|
|
41
46
|
this.add(code, "error", message, path, nodeId);
|
|
42
47
|
}
|
|
43
|
-
warning(code, message, path, nodeId) {
|
|
44
|
-
this.add(code, "warning", message, path, nodeId);
|
|
48
|
+
warning(code, message, path, nodeId, extra) {
|
|
49
|
+
this.add(code, "warning", message, path, nodeId, extra);
|
|
45
50
|
}
|
|
46
51
|
get hasErrors() {
|
|
47
52
|
return this.list.some((d) => d.severity === "error");
|
|
48
53
|
}
|
|
49
54
|
};
|
|
50
55
|
function formatDiagnostics(diagnostics) {
|
|
51
|
-
return diagnostics.map((d) => `${d.severity} ${d.code} at ${d.path}: ${d.message}`).join("\n");
|
|
56
|
+
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");
|
|
52
57
|
}
|
|
53
58
|
var ElementsError = class extends Error {
|
|
54
59
|
constructor(diagnostics) {
|
|
@@ -59,6 +64,64 @@ ${formatDiagnostics(errors)}`);
|
|
|
59
64
|
this.diagnostics = diagnostics;
|
|
60
65
|
}
|
|
61
66
|
};
|
|
67
|
+
var captured = (against, wordsWithin4px, lineBreaks) => ({ elements: "0.3.0", against, captured: "2026-11-08", wordsWithin4px, lineBreaks });
|
|
68
|
+
var tables = (name) => ({ name, widths: {}, dark: false, approximate: true, measured: null });
|
|
69
|
+
var EMAIL_CLIENTS = {
|
|
70
|
+
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) },
|
|
71
|
+
"outlook-com": { name: "Outlook.com and new Outlook", widths: { desktop: 820 }, dark: false, approximate: false, measured: captured("Outlook on the web", 1, 1) },
|
|
72
|
+
"outlook-mac": tables("Outlook for Mac"),
|
|
73
|
+
"outlook-app": tables("Outlook app (iOS and Android)"),
|
|
74
|
+
gmail: { name: "Gmail", widths: { desktop: 820 }, dark: false, approximate: false, measured: captured("Gmail on the web", 1, 1) },
|
|
75
|
+
"gmail-app": { name: "Gmail app", widths: { phone: 375 }, dark: false, approximate: true, measured: null },
|
|
76
|
+
"gmail-app-other": { name: "Gmail app, another account", widths: { phone: 375 }, dark: false, approximate: true, measured: null },
|
|
77
|
+
"apple-mail": { name: "Apple Mail", widths: { desktop: 820 }, dark: true, approximate: false, measured: captured("Mail 16 on macOS 26", 0.999, 0.999) },
|
|
78
|
+
yahoo: tables("Yahoo Mail"),
|
|
79
|
+
aol: tables("AOL Mail"),
|
|
80
|
+
"samsung-email": tables("Samsung Email"),
|
|
81
|
+
thunderbird: tables("Thunderbird"),
|
|
82
|
+
protonmail: tables("Proton Mail"),
|
|
83
|
+
fastmail: tables("Fastmail"),
|
|
84
|
+
hey: tables("HEY"),
|
|
85
|
+
gmx: tables("GMX"),
|
|
86
|
+
"web-de": tables("WEB.DE"),
|
|
87
|
+
orange: tables("Orange"),
|
|
88
|
+
sfr: tables("SFR"),
|
|
89
|
+
laposte: tables("La Poste"),
|
|
90
|
+
"free-fr": tables("Free"),
|
|
91
|
+
"t-online-de": tables("T-Online"),
|
|
92
|
+
"mail-ru": tables("Mail.ru"),
|
|
93
|
+
ionos: tables("IONOS (1&1)"),
|
|
94
|
+
"wp-pl": tables("WP Poczta"),
|
|
95
|
+
rainloop: tables("RainLoop")
|
|
96
|
+
};
|
|
97
|
+
var DEFAULT_EMAIL_CLIENTS = ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "apple-mail", "yahoo", "aol", "samsung-email"];
|
|
98
|
+
var caniemail = (feature) => `https://www.caniemail.com/features/${feature}/`;
|
|
99
|
+
function checkEmailClients(clients) {
|
|
100
|
+
if (clients === void 0) return;
|
|
101
|
+
const unknown = Array.isArray(clients) ? clients.filter((c) => !Object.prototype.hasOwnProperty.call(EMAIL_CLIENTS, c)) : [clients];
|
|
102
|
+
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(", ")}`);
|
|
103
|
+
}
|
|
104
|
+
function forEmailClients(diagnostics, selection = DEFAULT_EMAIL_CLIENTS) {
|
|
105
|
+
const out = [];
|
|
106
|
+
const seen = /* @__PURE__ */ new Map();
|
|
107
|
+
for (const d of diagnostics) {
|
|
108
|
+
if (!d.clients) {
|
|
109
|
+
out.push(d);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const clients = d.clients.filter((c) => selection.includes(c));
|
|
113
|
+
if (!clients.length) continue;
|
|
114
|
+
const key = JSON.stringify([d.code, d.path, d.severity, d.message]);
|
|
115
|
+
const same42 = seen.get(key);
|
|
116
|
+
if (same42) same42.clients = [.../* @__PURE__ */ new Set([...same42.clients, ...clients])];
|
|
117
|
+
else {
|
|
118
|
+
const kept = { ...d, clients };
|
|
119
|
+
seen.set(key, kept);
|
|
120
|
+
out.push(kept);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
62
125
|
var ok = (value) => ({ ok: true, value });
|
|
63
126
|
var fail = (message) => ({ ok: false, message });
|
|
64
127
|
var show = (input) => {
|
|
@@ -644,6 +707,26 @@ function attributes() {
|
|
|
644
707
|
jsonSchema: () => ({ type: "object", additionalProperties: { type: "string" } })
|
|
645
708
|
});
|
|
646
709
|
}
|
|
710
|
+
var MIN_PHONE_WIDTH = 240;
|
|
711
|
+
function breakpoints() {
|
|
712
|
+
const accepts = `{ mobile }: the phone width in px (from ${MIN_PHONE_WIDTH})`;
|
|
713
|
+
return kind({
|
|
714
|
+
name: "breakpoints",
|
|
715
|
+
editor: "number",
|
|
716
|
+
accepts,
|
|
717
|
+
parse(input) {
|
|
718
|
+
if (!isObject(input)) return fail(`expected ${accepts}, not ${show(input)}`);
|
|
719
|
+
const unknown = Object.keys(input).find((key) => key !== "mobile");
|
|
720
|
+
if (unknown !== void 0) return fail(`${show(unknown)} isn't a breakpoint a Page has: it has mobile`);
|
|
721
|
+
const { mobile } = input;
|
|
722
|
+
if (mobile === void 0) return ok({});
|
|
723
|
+
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)}`);
|
|
724
|
+
return ok({ mobile });
|
|
725
|
+
},
|
|
726
|
+
empty: (value) => value.mobile === void 0,
|
|
727
|
+
jsonSchema: () => ({ type: "object", properties: { mobile: { type: "integer", minimum: MIN_PHONE_WIDTH } }, additionalProperties: false })
|
|
728
|
+
});
|
|
729
|
+
}
|
|
647
730
|
function linkParts(value) {
|
|
648
731
|
return typeof value === "string" ? { href: value } : value;
|
|
649
732
|
}
|
|
@@ -657,6 +740,54 @@ function defineComponent(definition) {
|
|
|
657
740
|
function prop(kind2, options) {
|
|
658
741
|
return { kind: kind2, ...options };
|
|
659
742
|
}
|
|
743
|
+
var MOBILE = {
|
|
744
|
+
name: "mobile",
|
|
745
|
+
on: "on phones",
|
|
746
|
+
for: "for phones",
|
|
747
|
+
roots: (root) => !!root?.responsive,
|
|
748
|
+
notAllowed: { code: "mobile/not-supported", message: "Phone styles are for an Email or a Page, which have a phone width" },
|
|
749
|
+
sheet: { media: "phone", selector: "&" },
|
|
750
|
+
allows: (spec) => !!spec.mobile,
|
|
751
|
+
support: { web: { level: "full", source: "writer" }, email: { level: "full", source: "writer" } },
|
|
752
|
+
styles: {
|
|
753
|
+
// The email's phone text styles are rules that override a size; "normal" isn't one.
|
|
754
|
+
lineHeight: { email: { level: "none", values: ["normal"], shows: "the line height it has on desktop", source: "writer" } }
|
|
755
|
+
},
|
|
756
|
+
outlookApplies: false,
|
|
757
|
+
hiddenKey: "mobile"
|
|
758
|
+
};
|
|
759
|
+
var HOVER = {
|
|
760
|
+
name: "hover",
|
|
761
|
+
on: "on hover",
|
|
762
|
+
for: "for hover",
|
|
763
|
+
// Keyboard focus gets the same styles as the pointer: a link or button reached by Tab shows its hover colors.
|
|
764
|
+
// A box around the link (an email Button's cell, its background) takes them while the link in it has focus.
|
|
765
|
+
sheet: { media: "all", selector: "&:hover,&:focus-visible", within: "&:hover,&:focus-within", level: "hover" },
|
|
766
|
+
allows: (_, key, definition) => !!definition.hover?.includes(key),
|
|
767
|
+
support: {
|
|
768
|
+
web: { level: "full", source: "css" },
|
|
769
|
+
// On type selectors only in Outlook.com and Outlook for Mac (Elements' are classes); the phone apps' results don't
|
|
770
|
+
// matter, as nothing hovers there.
|
|
771
|
+
email: {
|
|
772
|
+
level: "partial",
|
|
773
|
+
shows: "no change (where it shows, keyboard focus gets it too)",
|
|
774
|
+
source: "caniemail",
|
|
775
|
+
feature: "css-pseudo-class-hover",
|
|
776
|
+
clients: ["outlook", "outlook-com", "outlook-mac", "gmx", "web-de", "sfr", "laposte", "mail-ru"]
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
paged: (node, where) => `A hover isn't possible in ${where}: nothing points at it. The ${node.type} shows its own colors`,
|
|
780
|
+
outlookApplies: false
|
|
781
|
+
};
|
|
782
|
+
var CONDITIONS = [MOBILE, HOVER];
|
|
783
|
+
var CONDITION_NAMES = new Set(CONDITIONS.map((condition) => condition.name));
|
|
784
|
+
function blockOf(node, condition) {
|
|
785
|
+
const block = node[condition.name];
|
|
786
|
+
return block && Object.keys(block).length ? block : void 0;
|
|
787
|
+
}
|
|
788
|
+
function shownAnywhere(node) {
|
|
789
|
+
return !(node.hidden?.mobile && node.hidden?.desktop);
|
|
790
|
+
}
|
|
660
791
|
var writer = { level: "full", source: "writer" };
|
|
661
792
|
var css = { level: "full", source: "css" };
|
|
662
793
|
var textStyle = {
|
|
@@ -665,7 +796,12 @@ var textStyle = {
|
|
|
665
796
|
fontFamily: { kind: fontFamily(), label: "Font", inherited: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
|
|
666
797
|
// Word rounds sizes to half points (13px is 10pt): a fraction of a px.
|
|
667
798
|
fontSize: { kind: length(["px"], { min: 1 }), label: "Font size", inherited: true, mobile: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
|
|
668
|
-
fontWeight: {
|
|
799
|
+
fontWeight: {
|
|
800
|
+
kind: fontWeight(),
|
|
801
|
+
label: "Font weight",
|
|
802
|
+
inherited: true,
|
|
803
|
+
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 }
|
|
804
|
+
},
|
|
669
805
|
fontStyle: { kind: enumOf(["normal", "italic"]), label: "Font style", inherited: true, support: { email: { level: "full", source: "caniemail" }, web: css } },
|
|
670
806
|
// Written in px with mso-line-height-rule: exactly, which Word keeps (it drops unitless ones).
|
|
671
807
|
lineHeight: { kind: lineHeight(), label: "Line height", inherited: true, mobile: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
|
|
@@ -677,8 +813,21 @@ var textStyle = {
|
|
|
677
813
|
support: { email: { level: "outlook", shows: "normal spacing: Word keeps letter spacing but doesn't draw it", source: "outlook-captures" }, web: css }
|
|
678
814
|
},
|
|
679
815
|
textAlign: { kind: enumOf(["left", "center", "right", "justify"]), label: "Alignment", inherited: true, mobile: true, support: { email: { level: "full", source: "caniemail" }, web: css } },
|
|
680
|
-
textTransform: {
|
|
681
|
-
|
|
816
|
+
textTransform: {
|
|
817
|
+
kind: enumOf(["none", "uppercase", "lowercase", "capitalize"]),
|
|
818
|
+
label: "Letter case",
|
|
819
|
+
inherited: true,
|
|
820
|
+
support: { email: { level: "outlook", values: ["lowercase"], shows: "the text as written: Word doesn't lowercase it", source: "caniemail", feature: "css-text-transform" }, web: css }
|
|
821
|
+
},
|
|
822
|
+
textDecoration: {
|
|
823
|
+
kind: enumOf(["none", "underline", "line-through"]),
|
|
824
|
+
label: "Decoration",
|
|
825
|
+
support: {
|
|
826
|
+
// Removed, so "none" leaves a link the client's own underline: said only where a line is asked for.
|
|
827
|
+
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"] },
|
|
828
|
+
web: css
|
|
829
|
+
}
|
|
830
|
+
}
|
|
682
831
|
};
|
|
683
832
|
var inheritedTextStyle = (({ textDecoration: _, ...rest }) => rest)(textStyle);
|
|
684
833
|
var boxStyle = {
|
|
@@ -691,7 +840,17 @@ var boxStyle = {
|
|
|
691
840
|
padding: { kind: insets(), label: "Padding", mobile: true, support: { email: { level: "full", source: "outlook-captures" }, web: css } },
|
|
692
841
|
backgroundColor: { kind: color(), label: "Background color", support: { email: writer, web: css } },
|
|
693
842
|
border: { kind: border(), label: "Border", mobile: true, support: { email: { level: "full", source: "caniemail" }, web: css } },
|
|
694
|
-
borderRadius: {
|
|
843
|
+
borderRadius: {
|
|
844
|
+
kind: radius(),
|
|
845
|
+
label: "Corner radius",
|
|
846
|
+
support: {
|
|
847
|
+
email: [
|
|
848
|
+
{ level: "outlook", shows: "square corners", source: "caniemail", feature: "css-border-radius" },
|
|
849
|
+
{ level: "partial", shows: "square corners", source: "caniemail", feature: "css-border-radius", clients: ["gmx", "web-de", "orange"] }
|
|
850
|
+
],
|
|
851
|
+
web: css
|
|
852
|
+
}
|
|
853
|
+
},
|
|
695
854
|
overflow: {
|
|
696
855
|
kind: enumOf(["visible", "hidden"]),
|
|
697
856
|
label: "Overflow",
|
|
@@ -899,6 +1058,15 @@ var Grid = defineComponent({
|
|
|
899
1058
|
},
|
|
900
1059
|
children: { categories: ["layout", "block"] },
|
|
901
1060
|
sizing: "stretch",
|
|
1061
|
+
// Stacked on phones, every two of its children are its phone gap apart: there are no rows for a row gap.
|
|
1062
|
+
conditionSupport: {
|
|
1063
|
+
mobile: {
|
|
1064
|
+
rowGap: {
|
|
1065
|
+
web: { level: "none", when: (_, node) => node.props.stackOnMobile !== false, shows: "its phone gap between every two children", source: "writer" },
|
|
1066
|
+
email: { level: "none", when: (_, node) => node.props.stackOnMobile !== false, shows: "its phone gap between every two children", source: "writer" }
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
},
|
|
902
1070
|
targets: { email: {}, web: {} },
|
|
903
1071
|
editor: {
|
|
904
1072
|
groups: [
|
|
@@ -911,7 +1079,7 @@ var Grid = defineComponent({
|
|
|
911
1079
|
var Layer = defineComponent({
|
|
912
1080
|
type: "Layer",
|
|
913
1081
|
label: "Layer",
|
|
914
|
-
description: "Its children over each other, in one box as tall as the tallest: an image
|
|
1082
|
+
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.",
|
|
915
1083
|
categories: ["layout", "block"],
|
|
916
1084
|
props: {},
|
|
917
1085
|
style: {
|
|
@@ -943,13 +1111,27 @@ var Layer = defineComponent({
|
|
|
943
1111
|
}
|
|
944
1112
|
});
|
|
945
1113
|
var WRITTEN3 = { email: { level: "full", source: "writer" }, web: { level: "full", source: "writer" } };
|
|
1114
|
+
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"];
|
|
946
1115
|
var ROOT_PROPS = {
|
|
947
1116
|
title: prop(text(), { support: WRITTEN3, label: "Title", description: "The document's title (shown in a browser tab, and by some email clients)." }),
|
|
948
|
-
lang: prop(text({ maxLength: 35 }), {
|
|
949
|
-
|
|
1117
|
+
lang: prop(text({ maxLength: 35 }), {
|
|
1118
|
+
support: {
|
|
1119
|
+
...WRITTEN3,
|
|
1120
|
+
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"] }
|
|
1121
|
+
},
|
|
1122
|
+
label: "Language",
|
|
1123
|
+
description: 'Its language, as a BCP 47 tag ("en", "ar").'
|
|
1124
|
+
}),
|
|
1125
|
+
dir: prop(enumOf(["ltr", "rtl", "auto"]), {
|
|
1126
|
+
support: {
|
|
1127
|
+
...WRITTEN3,
|
|
1128
|
+
email: { level: "partial", values: ["rtl", "auto"], shows: "left to right: the client removes the direction", source: "caniemail", feature: "html-dir", clients: ["gmx", "web-de"] }
|
|
1129
|
+
},
|
|
1130
|
+
label: "Text direction"
|
|
1131
|
+
}),
|
|
950
1132
|
fonts: prop(fontFaces(), {
|
|
951
1133
|
support: {
|
|
952
|
-
email: { level: "partial", shows: "
|
|
1134
|
+
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 },
|
|
953
1135
|
web: { level: "full", source: "writer" }
|
|
954
1136
|
},
|
|
955
1137
|
label: "Web fonts",
|
|
@@ -1089,7 +1271,10 @@ var PageNumber = defineComponent({
|
|
|
1089
1271
|
style: {},
|
|
1090
1272
|
children: {},
|
|
1091
1273
|
sizing: "none",
|
|
1092
|
-
targets: { web: {} }
|
|
1274
|
+
targets: { web: {} },
|
|
1275
|
+
// A Page's or a Canvas's text has no page to number.
|
|
1276
|
+
roots: ["Document"],
|
|
1277
|
+
notAllowed: { code: "document/page-number", message: (root) => `A PageNumber is a page's number in a Document; this ${root} has no pages` }
|
|
1093
1278
|
});
|
|
1094
1279
|
function documentPages(root) {
|
|
1095
1280
|
const pages = [];
|
|
@@ -1225,6 +1410,11 @@ var Page = defineComponent({
|
|
|
1225
1410
|
props: {
|
|
1226
1411
|
width: prop(length(["px"], { min: 200 }), { support: WRITTEN6, label: "Content width", description: "The widest the content gets.", default: 1200 }),
|
|
1227
1412
|
description: prop(text(), { support: WRITTEN6, label: "Description", description: "The page's meta description." }),
|
|
1413
|
+
breakpoints: prop(breakpoints(), {
|
|
1414
|
+
support: { web: { level: "full", source: "writer" } },
|
|
1415
|
+
label: "Breakpoints",
|
|
1416
|
+
description: "Where its phone styles start: mobile, the widest window they apply to, in px (480 where it's not set)."
|
|
1417
|
+
}),
|
|
1228
1418
|
...ROOT_PROPS
|
|
1229
1419
|
},
|
|
1230
1420
|
style: ROOT_STYLE,
|
|
@@ -1232,6 +1422,12 @@ var Page = defineComponent({
|
|
|
1232
1422
|
children: { categories: ["section", "layout", "block"] },
|
|
1233
1423
|
sizing: "none",
|
|
1234
1424
|
responsive: true,
|
|
1425
|
+
// The page writer writes the root's phone gap, not its phone text styles.
|
|
1426
|
+
conditionSupport: {
|
|
1427
|
+
mobile: Object.fromEntries(
|
|
1428
|
+
["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" } }])
|
|
1429
|
+
)
|
|
1430
|
+
},
|
|
1235
1431
|
targets: { web: {} },
|
|
1236
1432
|
editor: {
|
|
1237
1433
|
groups: [
|
|
@@ -1475,6 +1671,7 @@ var Image = defineComponent({
|
|
|
1475
1671
|
},
|
|
1476
1672
|
children: {},
|
|
1477
1673
|
sizing: "shrink",
|
|
1674
|
+
conditionSupport: { mobile: { border: { email: { level: "none", shows: "its desktop border", source: "writer" } } } },
|
|
1478
1675
|
targets: { email: {}, web: {} },
|
|
1479
1676
|
editor: { groups: [{ title: "Image", paths: ["props.src", "props.alt", "style.width"] }, { title: "Box", paths: ["style.border", "style.borderRadius"] }] }
|
|
1480
1677
|
});
|
|
@@ -1605,6 +1802,7 @@ var TableCell = defineComponent({
|
|
|
1605
1802
|
},
|
|
1606
1803
|
children: { text: true, categories: ["inline"], types: ["Image"] },
|
|
1607
1804
|
sizing: "none",
|
|
1805
|
+
conditionSupport: { mobile: { border: { email: { level: "none", shows: "its desktop border", source: "writer" } } } },
|
|
1608
1806
|
targets: { email: {}, web: {} },
|
|
1609
1807
|
editor: { groups: [{ title: "Cell", paths: ["props.colSpan", "style.width", "style.verticalAlign"] }, TEXT_EDITOR, { title: "Box", paths: ["style.padding", "style.backgroundColor", "style.border"] }] }
|
|
1610
1808
|
});
|
|
@@ -1722,17 +1920,27 @@ function checkDocument(input, options) {
|
|
|
1722
1920
|
const extra = Object.keys(input).filter((key) => key !== "root");
|
|
1723
1921
|
for (const key of extra) d.error("document/unknown-key", `"${key}" isn't part of a document (root)`, key);
|
|
1724
1922
|
const rootDefinition = isObject2(input.root) && typeof input.root.type === "string" ? registry.get(input.root.type) : void 0;
|
|
1725
|
-
const context = { d, registry, used: /* @__PURE__ */ new Set(),
|
|
1923
|
+
const context = { d, registry, used: /* @__PURE__ */ new Set(), root: rootDefinition };
|
|
1726
1924
|
const root = input.root === void 0 ? null : readNode(input.root, "root", null, context);
|
|
1727
1925
|
if (input.root === void 0) d.error("document/root", 'A document has a root: { type: "Email" }, "Page", \u2026', "root");
|
|
1728
1926
|
return { document: root ? { root } : null, diagnostics: d.list };
|
|
1729
1927
|
}
|
|
1730
1928
|
function checkFragment(input, options) {
|
|
1731
1929
|
const d = new Diagnostics();
|
|
1732
|
-
const context = { d, registry: options.registry, idPrefix: options.idPrefix, used: new Set(options.taken),
|
|
1930
|
+
const context = { d, registry: options.registry, idPrefix: options.idPrefix, used: new Set(options.taken), root: options.root };
|
|
1733
1931
|
const node = readNode(input, options.path, options.parent, context);
|
|
1734
1932
|
return { node, diagnostics: d.list };
|
|
1735
1933
|
}
|
|
1934
|
+
function rootList(roots) {
|
|
1935
|
+
const named = roots.map((root) => a(root).replace(/^A/, "a"));
|
|
1936
|
+
return named.length > 1 ? `${named.slice(0, -1).join(", ")} or ${named[named.length - 1]}` : named[0] ?? "no root";
|
|
1937
|
+
}
|
|
1938
|
+
function rootTakes(roots, root) {
|
|
1939
|
+
return !roots || !root || roots.includes(root.type);
|
|
1940
|
+
}
|
|
1941
|
+
function sharesOutput(definition, root) {
|
|
1942
|
+
return Object.keys(definition.targets).some((target) => target in root.targets);
|
|
1943
|
+
}
|
|
1736
1944
|
function allowedIn(parent, child) {
|
|
1737
1945
|
const rule2 = parent.children;
|
|
1738
1946
|
return !!rule2.types?.includes(child.type) || child.categories.some((category) => rule2.categories?.includes(category));
|
|
@@ -1758,7 +1966,29 @@ function editDistance(from, to) {
|
|
|
1758
1966
|
}
|
|
1759
1967
|
return row[to.length];
|
|
1760
1968
|
}
|
|
1969
|
+
var OTHER_BREAKPOINTS = /* @__PURE__ */ new Set(["base", "initial", "xs", "sm", "md", "lg", "xl", "2xl", "tablet", "desktop", "wide", "small", "medium", "large"]);
|
|
1970
|
+
function conditionHint(definition, key) {
|
|
1971
|
+
const name = key.trim().replace(/^(&?:+|_+|\$|@)/, "").replace(/Style$/, "").toLowerCase();
|
|
1972
|
+
const own = CUSTOM_TYPE.test(definition.type);
|
|
1973
|
+
const hoverStyles = definition.hover ?? [];
|
|
1974
|
+
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)`;
|
|
1975
|
+
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 } }}`;
|
|
1976
|
+
if (name === "hover") return toHover;
|
|
1977
|
+
if (name === "mobile" || name === "phone" || name === "phones" || key.trim().startsWith("@media")) return toPhone;
|
|
1978
|
+
if (name === "focus" || name === "focus-visible" || name === "focusvisible" || name === "focus-within") {
|
|
1979
|
+
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`;
|
|
1980
|
+
}
|
|
1981
|
+
if (OTHER_BREAKPOINTS.has(name)) {
|
|
1982
|
+
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 }})`;
|
|
1983
|
+
}
|
|
1984
|
+
if (name === "active" || name === "pressed" || name === "press" || name === "visited" || name === "dark" || name === "light" || name === "print") {
|
|
1985
|
+
return `there are no ${name} styles: Elements has phone styles (mobile) and hover styles (hover) in style`;
|
|
1986
|
+
}
|
|
1987
|
+
return void 0;
|
|
1988
|
+
}
|
|
1761
1989
|
function unknownProp(definition, key) {
|
|
1990
|
+
const condition = conditionHint(definition, key);
|
|
1991
|
+
if (condition) return `${a(definition.type)} has no prop "${key}": ${condition}`;
|
|
1762
1992
|
if (Object.prototype.hasOwnProperty.call(definition.style, key)) return `${a(definition.type)} has no prop "${key}": it's a style, style={{ ${key}: \u2026 }}`;
|
|
1763
1993
|
const known = Object.keys(definition.props);
|
|
1764
1994
|
const meant = nearest(key, known);
|
|
@@ -1798,6 +2028,15 @@ function readNode(input, path, parent, context) {
|
|
|
1798
2028
|
d.error("children/not-allowed", placementMessage(parent, definition), path);
|
|
1799
2029
|
return null;
|
|
1800
2030
|
}
|
|
2031
|
+
const root = context.root;
|
|
2032
|
+
if (root && !rootTakes(definition.roots, root) && sharesOutput(definition, root)) {
|
|
2033
|
+
const { code, message } = definition.notAllowed ?? {
|
|
2034
|
+
code: "component/not-supported",
|
|
2035
|
+
message: (under) => `${a(definition.type)} goes in ${rootList(definition.roots ?? [])}, not in ${a(under).replace(/^A/, "a")}`
|
|
2036
|
+
};
|
|
2037
|
+
d.error(code, message(root.type), path);
|
|
2038
|
+
return null;
|
|
2039
|
+
}
|
|
1801
2040
|
let id;
|
|
1802
2041
|
if (input.id !== void 0) {
|
|
1803
2042
|
if (typeof input.id !== "string" || !ID.test(input.id)) {
|
|
@@ -1817,21 +2056,49 @@ function readNode(input, path, parent, context) {
|
|
|
1817
2056
|
if (!NODE_KEYS.includes(key)) d.error("node/unknown-key", `"${key}" isn't part of a component (${NODE_KEYS.join(", ")})`, `${path}.${key}`, id);
|
|
1818
2057
|
}
|
|
1819
2058
|
const node = id === void 0 ? { type: definition.type } : { id, type: definition.type };
|
|
1820
|
-
const props = readProps(input.props, definition, `${path}.props`, id, d);
|
|
2059
|
+
const props = readProps(input.props, definition, `${path}.props`, id, d, context.root);
|
|
1821
2060
|
if (props) node.props = props;
|
|
1822
|
-
const
|
|
1823
|
-
|
|
1824
|
-
if (
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
2061
|
+
const lifted = {};
|
|
2062
|
+
let styleInput = input.style;
|
|
2063
|
+
if (isObject2(styleInput) && !CUSTOM_TYPE.test(definition.type)) {
|
|
2064
|
+
const rest = {};
|
|
2065
|
+
for (const [key, value] of Object.entries(styleInput)) {
|
|
2066
|
+
if (CONDITION_NAMES.has(key)) lifted[key] = value;
|
|
2067
|
+
else rest[key] = value;
|
|
1829
2068
|
}
|
|
2069
|
+
styleInput = rest;
|
|
1830
2070
|
}
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
2071
|
+
const style = readStyles(styleInput, definition, definition.style, `${path}.style`, id, d, "style", () => true, void 0, context.root);
|
|
2072
|
+
if (style) node.style = style;
|
|
2073
|
+
for (const condition of CONDITIONS) {
|
|
2074
|
+
const inStyle = lifted[condition.name];
|
|
2075
|
+
const given = input[condition.name];
|
|
2076
|
+
if (inStyle !== void 0 && given !== void 0) {
|
|
2077
|
+
d.error(
|
|
2078
|
+
"style/twice",
|
|
2079
|
+
`${condition.name} is given twice: as ${condition.name}={\u2026} and in style. Give it once, in style: style={{ ${condition.name}: { \u2026 } }}`,
|
|
2080
|
+
`${path}.style.${condition.name}`,
|
|
2081
|
+
id
|
|
2082
|
+
);
|
|
2083
|
+
continue;
|
|
2084
|
+
}
|
|
2085
|
+
let value = inStyle ?? given;
|
|
2086
|
+
if (value === void 0) continue;
|
|
2087
|
+
const at = inStyle !== void 0 ? `${path}.style.${condition.name}` : `${path}.${condition.name}`;
|
|
2088
|
+
if (condition.roots && !condition.roots(context.root)) {
|
|
2089
|
+
if (condition.notAllowed) d.error(condition.notAllowed.code, condition.notAllowed.message, at, id);
|
|
2090
|
+
continue;
|
|
2091
|
+
}
|
|
2092
|
+
if (isObject2(value) && Object.keys(value).some((key) => CONDITION_NAMES.has(key))) {
|
|
2093
|
+
const own = {};
|
|
2094
|
+
for (const [key, inner] of Object.entries(value)) {
|
|
2095
|
+
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);
|
|
2096
|
+
else own[key] = inner;
|
|
2097
|
+
}
|
|
2098
|
+
value = own;
|
|
2099
|
+
}
|
|
2100
|
+
const read2 = readStyles(value, definition, definition.style, at, id, d, condition.name, (spec, key) => condition.allows(spec, key, definition), condition.for, context.root);
|
|
2101
|
+
if (read2) node[condition.name] = read2;
|
|
1835
2102
|
}
|
|
1836
2103
|
if (input.hidden !== void 0) {
|
|
1837
2104
|
const hidden = readHidden(input.hidden, `${path}.hidden`, id, context);
|
|
@@ -1841,7 +2108,7 @@ function readNode(input, path, parent, context) {
|
|
|
1841
2108
|
if (children.length) node.children = children;
|
|
1842
2109
|
return node;
|
|
1843
2110
|
}
|
|
1844
|
-
function readProps(input, definition, path, id, d) {
|
|
2111
|
+
function readProps(input, definition, path, id, d, root) {
|
|
1845
2112
|
const out = {};
|
|
1846
2113
|
if (input !== void 0 && !isObject2(input)) {
|
|
1847
2114
|
d.error("props/invalid", "props is an object", path, id);
|
|
@@ -1854,9 +2121,14 @@ function readProps(input, definition, path, id, d) {
|
|
|
1854
2121
|
d.error("prop/unknown", unknownProp(definition, key), `${path}.${key}`, id);
|
|
1855
2122
|
continue;
|
|
1856
2123
|
}
|
|
2124
|
+
if (!rootTakes(spec.roots, root)) {
|
|
2125
|
+
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);
|
|
2126
|
+
continue;
|
|
2127
|
+
}
|
|
1857
2128
|
const parsed = spec.kind.parse(value);
|
|
1858
|
-
if (parsed.ok)
|
|
1859
|
-
|
|
2129
|
+
if (parsed.ok) {
|
|
2130
|
+
if (!spec.kind.empty?.(parsed.value)) out[key] = parsed.value;
|
|
2131
|
+
} else d.error("prop/invalid", `${key}: ${parsed.message}`, `${path}.${key}`, id);
|
|
1860
2132
|
}
|
|
1861
2133
|
for (const [key, spec] of Object.entries(definition.props)) {
|
|
1862
2134
|
if (spec.required && !(key in out) && !(isObject2(input) && input[key] !== void 0)) {
|
|
@@ -1873,6 +2145,8 @@ var UNSUPPORTED_CSS = {
|
|
|
1873
2145
|
marginRight: "use padding",
|
|
1874
2146
|
display: "the component sets it",
|
|
1875
2147
|
position: "use a Layer for content placed over other content",
|
|
2148
|
+
backgroundImage: "put a Layer around it, its first child an Image (the Layer's height crops it to cover), the rest over it",
|
|
2149
|
+
background: "backgroundColor for a color; for an image, a Layer: its first child an Image, the rest over it",
|
|
1876
2150
|
float: "use a Row to put things side by side",
|
|
1877
2151
|
// Names Elements used before it took CSS's.
|
|
1878
2152
|
align: "it's alignItems now",
|
|
@@ -1886,10 +2160,12 @@ function styleHint(definition, specs, key) {
|
|
|
1886
2160
|
if (Object.prototype.hasOwnProperty.call(definition.props, key)) return `: it's a prop, ${key}={\u2026}`;
|
|
1887
2161
|
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";
|
|
1888
2162
|
if (UNSUPPORTED_CSS[key]) return `: ${UNSUPPORTED_CSS[key]}`;
|
|
2163
|
+
const condition = conditionHint(definition, key);
|
|
2164
|
+
if (condition) return `: ${condition}`;
|
|
1889
2165
|
const meant = nearest(key, Object.keys(specs));
|
|
1890
2166
|
return meant ? `: did you mean "${meant}"?` : "";
|
|
1891
2167
|
}
|
|
1892
|
-
function readStyles(input, definition, specs, path, id, d, what, allowed) {
|
|
2168
|
+
function readStyles(input, definition, specs, path, id, d, what, allowed, forLabel, root) {
|
|
1893
2169
|
if (input === void 0) return void 0;
|
|
1894
2170
|
if (!isObject2(input)) {
|
|
1895
2171
|
d.error(`${what}/invalid`, `${what} is an object of style properties`, path, id);
|
|
@@ -1903,8 +2179,12 @@ function readStyles(input, definition, specs, path, id, d, what, allowed) {
|
|
|
1903
2179
|
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);
|
|
1904
2180
|
continue;
|
|
1905
2181
|
}
|
|
2182
|
+
if (!rootTakes(spec.roots, root)) {
|
|
2183
|
+
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);
|
|
2184
|
+
continue;
|
|
2185
|
+
}
|
|
1906
2186
|
if (!allowed(spec, key)) {
|
|
1907
|
-
d.error(`${what}/not-allowed`,
|
|
2187
|
+
d.error(`${what}/not-allowed`, `"${key}" can't be set ${forLabel ?? "here"}`, `${path}.${key}`, id);
|
|
1908
2188
|
continue;
|
|
1909
2189
|
}
|
|
1910
2190
|
const parsed = spec.kind.parse(value);
|
|
@@ -1915,7 +2195,7 @@ function readStyles(input, definition, specs, path, id, d, what, allowed) {
|
|
|
1915
2195
|
}
|
|
1916
2196
|
function readHidden(input, path, id, context) {
|
|
1917
2197
|
const { d } = context;
|
|
1918
|
-
if (!context.responsive) {
|
|
2198
|
+
if (!context.root?.responsive) {
|
|
1919
2199
|
d.error("hidden/not-supported", "Hiding on phones or desktops is for an Email or a Page", path, id);
|
|
1920
2200
|
return void 0;
|
|
1921
2201
|
}
|
|
@@ -1926,7 +2206,7 @@ function readHidden(input, path, id, context) {
|
|
|
1926
2206
|
const out = {};
|
|
1927
2207
|
if (input.mobile === true) out.mobile = true;
|
|
1928
2208
|
if (input.desktop === true) out.desktop = true;
|
|
1929
|
-
if (
|
|
2209
|
+
if (!shownAnywhere({ hidden: out })) d.warning("hidden/everywhere", "Hidden on phones and on desktops, it never shows", path, id);
|
|
1930
2210
|
return Object.keys(out).length ? out : void 0;
|
|
1931
2211
|
}
|
|
1932
2212
|
function readChildren(input, definition, path, id, context) {
|
|
@@ -1959,6 +2239,22 @@ function readChildren(input, definition, path, id, context) {
|
|
|
1959
2239
|
});
|
|
1960
2240
|
return out;
|
|
1961
2241
|
}
|
|
2242
|
+
var same = (a2, b) => JSON.stringify(a2) === JSON.stringify(b);
|
|
2243
|
+
function reportDeclared(d, node, at, key, value, declared, target, outlook = true) {
|
|
2244
|
+
if (!declared) return;
|
|
2245
|
+
for (const rule2 of [].concat(declared)) {
|
|
2246
|
+
if (rule2.level === "full") continue;
|
|
2247
|
+
if (rule2.level === "outlook" && !outlook) continue;
|
|
2248
|
+
if (rule2.values && !rule2.values.some((v) => same(v, value))) continue;
|
|
2249
|
+
if (rule2.when && !rule2.when(value, node)) continue;
|
|
2250
|
+
const asked = typeof value === "string" ? `${key}: "${value}"` : key;
|
|
2251
|
+
const clients = target === "email" ? rule2.clients ?? (rule2.level === "outlook" ? ["outlook"] : void 0) : void 0;
|
|
2252
|
+
const output = clients ? "some email clients" : target === "web" ? "a page" : target;
|
|
2253
|
+
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}`];
|
|
2254
|
+
const [said, as] = rule2.source === "assumed" ? ["info", `${message} (not checked)`] : [severity, message];
|
|
2255
|
+
d.add(code, said, as, at, node.id, { clients, fix: rule2.fix, see: rule2.feature && caniemail(rule2.feature) });
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
1962
2258
|
var MAX_DEPTH = 32;
|
|
1963
2259
|
function templatePathsOf(document, paths) {
|
|
1964
2260
|
const places = /* @__PURE__ */ new Map();
|
|
@@ -1975,7 +2271,7 @@ function templatePathsOf(document, paths) {
|
|
|
1975
2271
|
return path;
|
|
1976
2272
|
};
|
|
1977
2273
|
}
|
|
1978
|
-
function expandDocument(document, options) {
|
|
2274
|
+
function expandDocument(document, options, target) {
|
|
1979
2275
|
const registry = registryOf(options);
|
|
1980
2276
|
const d = new Diagnostics();
|
|
1981
2277
|
const paths = /* @__PURE__ */ new WeakMap();
|
|
@@ -1987,7 +2283,20 @@ function expandDocument(document, options) {
|
|
|
1987
2283
|
collect(document.root);
|
|
1988
2284
|
const rootDefinition = registry.get(document.root.type);
|
|
1989
2285
|
const responsive = !!rootDefinition?.responsive;
|
|
2286
|
+
const unshown = (node, path) => {
|
|
2287
|
+
const declared = target && registry.get(node.type)?.conditionSupport;
|
|
2288
|
+
if (!declared) return;
|
|
2289
|
+
for (const condition of CONDITIONS) {
|
|
2290
|
+
const block = blockOf(node, condition);
|
|
2291
|
+
const byStyle = declared[condition.name];
|
|
2292
|
+
if (!block || !byStyle || condition.hiddenKey && node.hidden?.[condition.hiddenKey]) continue;
|
|
2293
|
+
for (const [key, value] of Object.entries(block)) {
|
|
2294
|
+
reportDeclared(d, { type: node.type, id: node.id, props: node.props ?? {} }, `${path}.${condition.name}.${key}`, key, value, byStyle[key]?.[target], target);
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
};
|
|
1990
2298
|
const visit = (given, path, depth, self = path) => {
|
|
2299
|
+
unshown(given, path);
|
|
1991
2300
|
const arrange = registry.get(given.type)?.arrange;
|
|
1992
2301
|
if (!arrange) return walk(given, path, self, depth);
|
|
1993
2302
|
const origins = /* @__PURE__ */ new Map();
|
|
@@ -2014,27 +2323,34 @@ function expandDocument(document, options) {
|
|
|
2014
2323
|
return void 0;
|
|
2015
2324
|
}
|
|
2016
2325
|
let raw2;
|
|
2326
|
+
const reported = [];
|
|
2017
2327
|
try {
|
|
2018
2328
|
const style = { ...definition.defaultStyle, ...node.style };
|
|
2019
|
-
raw2 = definition.expand(propsWithDefaults(definition, node.props), { register: (custom) => registry.add(custom), style, responsive });
|
|
2329
|
+
raw2 = definition.expand(propsWithDefaults(definition, node.props), { register: (custom) => registry.add(custom), style, responsive, report: (diagnostic) => reported.push(diagnostic) });
|
|
2020
2330
|
} catch (error) {
|
|
2021
2331
|
d.error("custom/render", `${node.type} failed to render: ${error instanceof Error ? error.message : String(error)}`, path, node.id);
|
|
2022
2332
|
return void 0;
|
|
2023
2333
|
}
|
|
2334
|
+
const at = `${path}(${node.type})`;
|
|
2335
|
+
for (const diagnostic of reported) d.list.push({ ...diagnostic, path: diagnostic.path === "root" || diagnostic.path.startsWith("root.") ? at + diagnostic.path.slice(4) : at });
|
|
2024
2336
|
if (Array.isArray(raw2) || typeof raw2 !== "object" || raw2 === null) {
|
|
2025
2337
|
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);
|
|
2026
2338
|
return void 0;
|
|
2027
2339
|
}
|
|
2028
|
-
const { node: expanded, diagnostics } = checkFragment(raw2, { parent,
|
|
2340
|
+
const { node: expanded, diagnostics } = checkFragment(raw2, { parent, root: rootDefinition, idPrefix: node.id ?? null, taken, registry, path: `${path}(${node.type})` });
|
|
2029
2341
|
for (const diagnostic of diagnostics) d.list.push(diagnostic);
|
|
2030
2342
|
if (!expanded) return void 0;
|
|
2031
2343
|
collectIds(expanded);
|
|
2032
2344
|
if (node.style?.flex !== void 0) expanded.style = { ...expanded.style, flex: node.style.flex };
|
|
2033
2345
|
if (node.hidden) expanded.hidden = { ...expanded.hidden, ...node.hidden };
|
|
2034
2346
|
if (node.id && !expanded.id && !CUSTOM_TYPE.test(node.type)) expanded.id = node.id;
|
|
2035
|
-
for (const
|
|
2036
|
-
|
|
2037
|
-
|
|
2347
|
+
for (const condition of CONDITIONS) {
|
|
2348
|
+
const rendered = registry.get(expanded.type);
|
|
2349
|
+
for (const [key, value] of Object.entries(node[condition.name] ?? {})) {
|
|
2350
|
+
const spec = rendered?.style[key];
|
|
2351
|
+
if (rendered && spec && condition.allows(spec, key, rendered)) expanded[condition.name] = { [key]: value, ...expanded[condition.name] };
|
|
2352
|
+
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);
|
|
2353
|
+
}
|
|
2038
2354
|
}
|
|
2039
2355
|
const inner = registry.get(expanded.type);
|
|
2040
2356
|
if (isCustom(inner)) return expandOne(expanded, inner, parent, `${path}(${node.type})`, depth + 1, self);
|
|
@@ -2487,11 +2803,11 @@ var TEXT_CSS = [
|
|
|
2487
2803
|
["textAlign", "text-align", (v) => v],
|
|
2488
2804
|
["textTransform", "text-transform", (v) => v]
|
|
2489
2805
|
];
|
|
2490
|
-
var
|
|
2806
|
+
var same2 = (a2, b) => a2 === b || JSON.stringify(a2) === JSON.stringify(b);
|
|
2491
2807
|
function textDeclarations(text2, parent, always = []) {
|
|
2492
2808
|
const out = [];
|
|
2493
2809
|
for (const [key, property, write3] of TEXT_CSS) {
|
|
2494
|
-
if (!parent || always.includes(key) || !
|
|
2810
|
+
if (!parent || always.includes(key) || !same2(text2[key], parent[key])) out.push([property, write3(text2[key])]);
|
|
2495
2811
|
}
|
|
2496
2812
|
return out;
|
|
2497
2813
|
}
|
|
@@ -2567,7 +2883,9 @@ function classesOf(name, value) {
|
|
|
2567
2883
|
if (name === "class") return (Array.isArray(value) ? value : [value]).filter((v) => typeof v === "string" && v !== "");
|
|
2568
2884
|
const text2 = styleText(value, true);
|
|
2569
2885
|
if (!text2) return [];
|
|
2570
|
-
|
|
2886
|
+
if (name === "phone") return [rule(MOBILE.sheet.media, `${MOBILE.sheet.selector}{${text2}}`)];
|
|
2887
|
+
const selector = name === "hoverWithin" ? HOVER.sheet.within ?? HOVER.sheet.selector : HOVER.sheet.selector;
|
|
2888
|
+
return [rule(HOVER.sheet.media, `${selector}{${text2}}`, HOVER.sheet.level)];
|
|
2571
2889
|
}
|
|
2572
2890
|
function attributes2(props) {
|
|
2573
2891
|
const out = [];
|
|
@@ -2575,7 +2893,7 @@ function attributes2(props) {
|
|
|
2575
2893
|
let slot = -1;
|
|
2576
2894
|
for (const [name, value] of Object.entries(props)) {
|
|
2577
2895
|
if (name === "children" || name === "style" || value === void 0 || value === null || value === false) continue;
|
|
2578
|
-
if (name === "class" || name === "phone" || name === "hover") {
|
|
2896
|
+
if (name === "class" || name === "phone" || name === "hover" || name === "hoverWithin") {
|
|
2579
2897
|
const found = classesOf(name, value);
|
|
2580
2898
|
if (found.length && slot < 0) slot = out.push("") - 1;
|
|
2581
2899
|
classes.push(...found);
|
|
@@ -2591,8 +2909,8 @@ function withId(element, id) {
|
|
|
2591
2909
|
if (!id) return element;
|
|
2592
2910
|
const tag = /^<[a-zA-Z0-9]+/.exec(element.markup)?.[0];
|
|
2593
2911
|
if (!tag) throw new Error("[Elements] An id goes on an element");
|
|
2594
|
-
const
|
|
2595
|
-
return new Html2(
|
|
2912
|
+
const insert2 = (markup) => `${tag} id="${escapeAttr(id)}"${markup.slice(tag.length)}`;
|
|
2913
|
+
return new Html2(insert2(element.markup), { ...element.options, start: element.options.start && insert2(element.options.start) });
|
|
2596
2914
|
}
|
|
2597
2915
|
function h(type, props, ...children) {
|
|
2598
2916
|
const all = props ?? {};
|
|
@@ -2619,6 +2937,98 @@ function Merged({ children }) {
|
|
|
2619
2937
|
}
|
|
2620
2938
|
var open = (element) => new Html2(element.options.start ?? element.markup);
|
|
2621
2939
|
var close = (tag) => new Html2(`</${tag}>`);
|
|
2940
|
+
var BLOCK_MAP_VERSION = 1;
|
|
2941
|
+
var MARK_PAIRS = 16;
|
|
2942
|
+
var BlockMarks = class {
|
|
2943
|
+
constructor(pair) {
|
|
2944
|
+
this.nodes = [];
|
|
2945
|
+
this.open = String.fromCharCode(64976 + pair * 2);
|
|
2946
|
+
this.close = String.fromCharCode(64977 + pair * 2);
|
|
2947
|
+
}
|
|
2948
|
+
};
|
|
2949
|
+
var current = null;
|
|
2950
|
+
function withBlockMarks(marks, write3) {
|
|
2951
|
+
const before = current;
|
|
2952
|
+
current = marks;
|
|
2953
|
+
try {
|
|
2954
|
+
return write3();
|
|
2955
|
+
} finally {
|
|
2956
|
+
current = before;
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
function markBlock(html, node) {
|
|
2960
|
+
if (!current) return html;
|
|
2961
|
+
const mark = `${current.open}${current.nodes.length}${current.close}`;
|
|
2962
|
+
const marked = insert(html, mark, false) ?? insert(html, mark, true);
|
|
2963
|
+
if (!marked) return html;
|
|
2964
|
+
current.nodes.push(node);
|
|
2965
|
+
return marked;
|
|
2966
|
+
}
|
|
2967
|
+
var OUTLOOK_OPEN = "<!--[if mso]>";
|
|
2968
|
+
var OUTLOOK_CLOSE = "<![endif]-->";
|
|
2969
|
+
var TAG2 = /<[a-zA-Z][a-zA-Z0-9]*/y;
|
|
2970
|
+
function tagEnd(markup, outlook) {
|
|
2971
|
+
for (let i = markup.indexOf("<"); i >= 0; i = markup.indexOf("<", i + 1)) {
|
|
2972
|
+
if (markup.startsWith(OUTLOOK_OPEN, i)) {
|
|
2973
|
+
if (outlook) {
|
|
2974
|
+
i += OUTLOOK_OPEN.length - 1;
|
|
2975
|
+
continue;
|
|
2976
|
+
}
|
|
2977
|
+
const end = markup.indexOf(OUTLOOK_CLOSE, i);
|
|
2978
|
+
if (end < 0) return -1;
|
|
2979
|
+
i = end + OUTLOOK_CLOSE.length - 1;
|
|
2980
|
+
continue;
|
|
2981
|
+
}
|
|
2982
|
+
if (markup.startsWith("<!--", i)) {
|
|
2983
|
+
const end = markup.indexOf("-->", i + 4);
|
|
2984
|
+
if (end < 0) return -1;
|
|
2985
|
+
i = end + 2;
|
|
2986
|
+
continue;
|
|
2987
|
+
}
|
|
2988
|
+
TAG2.lastIndex = i;
|
|
2989
|
+
const tag = TAG2.exec(markup);
|
|
2990
|
+
if (tag) return i + tag[0].length;
|
|
2991
|
+
}
|
|
2992
|
+
return -1;
|
|
2993
|
+
}
|
|
2994
|
+
function insert(html, mark, outlook) {
|
|
2995
|
+
const { list, kind: kind2, start } = html.options;
|
|
2996
|
+
if (list) {
|
|
2997
|
+
for (let i = 0; i < list.length; i++) {
|
|
2998
|
+
const marked = insert(list[i], mark, outlook);
|
|
2999
|
+
if (marked) return new Html2(html.markup, { ...html.options, list: list.map((piece, j) => j === i ? marked : piece) });
|
|
3000
|
+
}
|
|
3001
|
+
return null;
|
|
3002
|
+
}
|
|
3003
|
+
if (kind2 === "outlook" && !outlook) return null;
|
|
3004
|
+
const at = tagEnd(html.markup, outlook);
|
|
3005
|
+
if (at < 0) return null;
|
|
3006
|
+
const put = (text2) => text2.slice(0, at) + mark + text2.slice(at);
|
|
3007
|
+
return new Html2(put(html.markup), { ...html.options, start: start !== void 0 && at <= start.length ? put(start) : start });
|
|
3008
|
+
}
|
|
3009
|
+
function readMarks(marks, written) {
|
|
3010
|
+
const found = [];
|
|
3011
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3012
|
+
const pieces2 = [];
|
|
3013
|
+
let length3 = 0;
|
|
3014
|
+
let last = 0;
|
|
3015
|
+
for (const match of written.matchAll(new RegExp(`${marks.open}(\\d+)${marks.close}`, "g"))) {
|
|
3016
|
+
const index = Number(match[1]);
|
|
3017
|
+
const node = marks.nodes[index];
|
|
3018
|
+
if (!node || seen.has(index)) return null;
|
|
3019
|
+
seen.add(index);
|
|
3020
|
+
const before = written.slice(last, match.index);
|
|
3021
|
+
pieces2.push(before);
|
|
3022
|
+
length3 += before.length;
|
|
3023
|
+
last = match.index + match[0].length;
|
|
3024
|
+
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;
|
|
3025
|
+
found.push({ node, at: length3 });
|
|
3026
|
+
}
|
|
3027
|
+
pieces2.push(written.slice(last));
|
|
3028
|
+
const text2 = pieces2.join("");
|
|
3029
|
+
if (text2.includes(marks.open) || text2.includes(marks.close)) return null;
|
|
3030
|
+
return { text: text2, found };
|
|
3031
|
+
}
|
|
2622
3032
|
function utf8Bytes(text2) {
|
|
2623
3033
|
let bytes = 0;
|
|
2624
3034
|
for (let i = 0; i < text2.length; i++) {
|
|
@@ -2705,7 +3115,7 @@ function fontFaces2(root, { swap }) {
|
|
|
2705
3115
|
return [...faces.values()];
|
|
2706
3116
|
}
|
|
2707
3117
|
var isNode2 = (child) => typeof child !== "string";
|
|
2708
|
-
var shown = (node) =>
|
|
3118
|
+
var shown = (node) => shownAnywhere(node);
|
|
2709
3119
|
function link2(props) {
|
|
2710
3120
|
const { href, target } = linkParts(props.href);
|
|
2711
3121
|
return {
|
|
@@ -2940,7 +3350,7 @@ var Button2 = ({ node, style, mobile, fill, phoneText: phoneText2, text: text2 }
|
|
|
2940
3350
|
{
|
|
2941
3351
|
align: TEXT_ALIGN[node.text.textAlign],
|
|
2942
3352
|
bgcolor: bgcolor(style.backgroundColor),
|
|
2943
|
-
|
|
3353
|
+
hoverWithin: { backgroundColor: node.hover?.backgroundColor },
|
|
2944
3354
|
phone: { border: mobile.border, backgroundColor: mobile.backgroundColor },
|
|
2945
3355
|
style: { backgroundColor: style.backgroundColor, border: style.border, borderRadius: style.borderRadius, msoPaddingAlt: insets2(style.padding) }
|
|
2946
3356
|
},
|
|
@@ -2951,8 +3361,11 @@ var Button2 = ({ node, style, mobile, fill, phoneText: phoneText2, text: text2 }
|
|
|
2951
3361
|
phone: [phoneText2, { padding: mobile.padding }],
|
|
2952
3362
|
hover: { color: node.hover?.color },
|
|
2953
3363
|
style: [
|
|
2954
|
-
// A button with a width fills it.
|
|
2955
|
-
|
|
3364
|
+
// A button with a width fills it. Its label's words stay whole (the email's word-break: break-word
|
|
3365
|
+
// is for long links in text): a button narrower than its padding otherwise sized itself to one letter
|
|
3366
|
+
// and drew its label a letter a line, in Gmail and browsers; it grows to its longest word instead, as
|
|
3367
|
+
// classic Outlook's and Apple Mail's do (captures of Elements 0.3.0, e3-fresh/g-0054).
|
|
3368
|
+
{ display: width.width !== void 0 ? "block" : "inline-block", padding: style.padding, wordBreak: "normal" },
|
|
2956
3369
|
fullText(node),
|
|
2957
3370
|
{ textDecoration: style.textDecoration ?? "none", borderRadius: style.borderRadius }
|
|
2958
3371
|
]
|
|
@@ -3416,27 +3829,42 @@ function propsOf(node, place, options) {
|
|
|
3416
3829
|
options
|
|
3417
3830
|
};
|
|
3418
3831
|
}
|
|
3832
|
+
var UNMAPPED = /* @__PURE__ */ new Set(["LineBreak", "MergeTag"]);
|
|
3419
3833
|
function write(node, place, options) {
|
|
3420
3834
|
if (place.inline) {
|
|
3421
3835
|
if (!shown(node)) return raw("");
|
|
3422
3836
|
const component22 = INLINES[node.type];
|
|
3423
3837
|
if (!component22) throw new Error(`[Elements] The email exporter can't render a ${node.type} inside text`);
|
|
3424
|
-
|
|
3838
|
+
const html = component22(propsOf(node, place, options));
|
|
3839
|
+
return UNMAPPED.has(node.type) ? html : markBlock(html, node);
|
|
3425
3840
|
}
|
|
3426
3841
|
const component2 = BLOCKS[node.type];
|
|
3427
3842
|
if (!component2) throw new Error(`[Elements] The email exporter can't render a ${node.type} here (in a ${place.parent.type})`);
|
|
3428
|
-
return component2(propsOf(node, place, options));
|
|
3843
|
+
return markBlock(component2(propsOf(node, place, options)), node);
|
|
3429
3844
|
}
|
|
3430
3845
|
var EMAIL_PHONE_MARGIN = 20;
|
|
3431
3846
|
var GMAIL_CLIP_BYTES = 102400;
|
|
3432
3847
|
var GMAIL_STYLE_BYTES = 16384;
|
|
3433
3848
|
function renderEmail(resolved, options = {}) {
|
|
3434
3849
|
const root = resolved.root;
|
|
3435
|
-
const sheet = new Stylesheet(options.classPrefix);
|
|
3436
3850
|
const d = new Diagnostics();
|
|
3437
3851
|
const fonts = fontFaces2(root, { swap: false });
|
|
3438
3852
|
const width = root.width ?? 600;
|
|
3439
|
-
const
|
|
3853
|
+
const write3 = (marks) => {
|
|
3854
|
+
const sheet2 = new Stylesheet(options.classPrefix);
|
|
3855
|
+
const markup = withStylesheet(sheet2, () => withBlockMarks(marks, () => String(markBlock(Email2(propsOf(root, { parent: root, fill: false, mobile: {}, inline: false, rtl: false }, options)), root))));
|
|
3856
|
+
return { sheet: sheet2, markup };
|
|
3857
|
+
};
|
|
3858
|
+
let written = options.blockMap ? null : write3(null);
|
|
3859
|
+
let marked = null;
|
|
3860
|
+
for (let pair = 0; !written; pair++) {
|
|
3861
|
+
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");
|
|
3862
|
+
const marks = new BlockMarks(pair);
|
|
3863
|
+
const attempt = write3(marks);
|
|
3864
|
+
marked = readMarks(marks, attempt.markup);
|
|
3865
|
+
if (marked) written = { sheet: attempt.sheet, markup: marked.text };
|
|
3866
|
+
}
|
|
3867
|
+
const { sheet, markup: email } = written;
|
|
3440
3868
|
const background = root.style.backgroundColor;
|
|
3441
3869
|
const lang = root.props.lang;
|
|
3442
3870
|
const dir = root.props.dir;
|
|
@@ -3460,7 +3888,8 @@ function renderEmail(resolved, options = {}) {
|
|
|
3460
3888
|
`<style>${stylesheet}</style>`
|
|
3461
3889
|
].join("");
|
|
3462
3890
|
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>` : "";
|
|
3463
|
-
const
|
|
3891
|
+
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"]])}>`;
|
|
3892
|
+
const body = `${preheader}${opening}${email}</div>`;
|
|
3464
3893
|
const bodyDeclarations = [
|
|
3465
3894
|
["margin", "0"],
|
|
3466
3895
|
["padding", "0"],
|
|
@@ -3468,20 +3897,38 @@ function renderEmail(resolved, options = {}) {
|
|
|
3468
3897
|
["background-color", background]
|
|
3469
3898
|
];
|
|
3470
3899
|
const bodyStyle = css2(bodyDeclarations);
|
|
3471
|
-
const
|
|
3900
|
+
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)}>`;
|
|
3901
|
+
const html = `${shell}${body}</body></html>`;
|
|
3472
3902
|
const bytes = utf8Bytes(html);
|
|
3903
|
+
const gmails = ["gmail", "gmail-app", "gmail-app-other"];
|
|
3473
3904
|
if (bytes > GMAIL_CLIP_BYTES) {
|
|
3474
|
-
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"
|
|
3905
|
+
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, {
|
|
3906
|
+
clients: gmails,
|
|
3907
|
+
fix: "shorten the email, or move long sections to a page: what follows the clip is behind a link"
|
|
3908
|
+
});
|
|
3475
3909
|
}
|
|
3476
3910
|
const styleBytes = utf8Bytes(stylesheet);
|
|
3477
3911
|
if (styleBytes > GMAIL_STYLE_BYTES) {
|
|
3478
|
-
d.warning("email/style-size", `The stylesheet is ${Math.round(styleBytes / 1024)} KB; Gmail reads ${GMAIL_STYLE_BYTES / 1024} KB of a <style>`, "root"
|
|
3912
|
+
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, {
|
|
3913
|
+
clients: gmails,
|
|
3914
|
+
fix: "use fewer phone and hover styles: each one adds a rule"
|
|
3915
|
+
});
|
|
3479
3916
|
}
|
|
3480
|
-
return { html, head, body, bodyStyle, diagnostics: d.list };
|
|
3917
|
+
return { html, head, body, bodyStyle, diagnostics: d.list, ...marked ? { blocks: blockMap(resolved, marked.found, email, utf8Bytes(shell + preheader + opening)) } : {} };
|
|
3918
|
+
}
|
|
3919
|
+
function blockMap(resolved, found, email, start) {
|
|
3920
|
+
const templatePath = resolved.templatePath ?? ((path) => path);
|
|
3921
|
+
let offset = start;
|
|
3922
|
+
let from = 0;
|
|
3923
|
+
const entries = found.map(({ node, at }) => {
|
|
3924
|
+
offset += utf8Bytes(email.slice(from, at));
|
|
3925
|
+
from = at;
|
|
3926
|
+
return { path: templatePath(node.path), ...node.id ? { nodeId: node.id } : {}, offset };
|
|
3927
|
+
});
|
|
3928
|
+
return { version: BLOCK_MAP_VERSION, unit: "utf8", entries };
|
|
3481
3929
|
}
|
|
3482
3930
|
var isNode3 = (child) => typeof child !== "string";
|
|
3483
|
-
var
|
|
3484
|
-
var same2 = (a2, b) => JSON.stringify(a2) === JSON.stringify(b);
|
|
3931
|
+
var same3 = (a2, b) => JSON.stringify(a2) === JSON.stringify(b);
|
|
3485
3932
|
function support(root, target) {
|
|
3486
3933
|
const d = new Diagnostics();
|
|
3487
3934
|
const visit = (node) => {
|
|
@@ -3489,41 +3936,81 @@ function support(root, target) {
|
|
|
3489
3936
|
const byProp = definition.defaultStyleBy ? definition.defaultStyleBy.values[String(node.props[definition.defaultStyleBy.prop])] : void 0;
|
|
3490
3937
|
for (const [key, value] of Object.entries(node.style)) {
|
|
3491
3938
|
const fallback2 = byProp?.[key] ?? definition.defaultStyle?.[key];
|
|
3492
|
-
|
|
3939
|
+
if (same3(value, fallback2)) continue;
|
|
3940
|
+
reportDeclared(d, node, `${node.path}.style.${key}`, key, value, definition.style[key]?.support?.[target], target);
|
|
3493
3941
|
}
|
|
3494
|
-
for (const [key, value] of Object.entries(node.props))
|
|
3495
|
-
|
|
3942
|
+
for (const [key, value] of Object.entries(node.props)) {
|
|
3943
|
+
if (same3(value, definition.props[key]?.default)) continue;
|
|
3944
|
+
reportDeclared(d, node, `${node.path}.props.${key}`, key, value, definition.props[key]?.support?.[target], target);
|
|
3945
|
+
}
|
|
3946
|
+
for (const condition of CONDITIONS) {
|
|
3947
|
+
const block = blockOf(node, condition);
|
|
3948
|
+
if (!block || condition.hiddenKey && node.hidden?.[condition.hiddenKey]) continue;
|
|
3949
|
+
const own = condition.support[target];
|
|
3950
|
+
reportDeclared(d, node, `${node.path}.${condition.name}`, condition.name, block, own, target);
|
|
3951
|
+
if (!own || own.level === "none") continue;
|
|
3952
|
+
for (const [key, value] of Object.entries(block)) {
|
|
3953
|
+
const rules = condition.styles?.[key]?.[target] ?? definition.style[key]?.support?.[target];
|
|
3954
|
+
reportDeclared(d, node, `${node.path}.${condition.name}.${key}`, key, value, rules, target, condition.outlookApplies);
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
for (const child of node.children) if (isNode3(child) && shownAnywhere(child)) visit(child);
|
|
3496
3958
|
};
|
|
3497
3959
|
visit(root);
|
|
3498
3960
|
return d.list;
|
|
3499
3961
|
}
|
|
3500
|
-
function
|
|
3501
|
-
const rules = declared?.[target];
|
|
3502
|
-
if (!rules || same2(value, fallback2)) return;
|
|
3503
|
-
for (const rule2 of [].concat(rules)) {
|
|
3504
|
-
if (rule2.level === "full") continue;
|
|
3505
|
-
if (rule2.values && !rule2.values.some((v) => same2(v, value))) continue;
|
|
3506
|
-
if (rule2.when && !rule2.when(value, node)) continue;
|
|
3507
|
-
const at = `${node.path}.${where}.${key}`;
|
|
3508
|
-
const asked = typeof value === "string" ? `${key}: "${value}"` : key;
|
|
3509
|
-
const output = target === "web" ? "a page" : target;
|
|
3510
|
-
if (rule2.level === "none") d.warning(`${target}/unsupported`, `${asked} isn't possible in ${output}: ${rule2.shows}`, at, node.id);
|
|
3511
|
-
else if (rule2.level === "partial") d.add(`${target}/degraded`, "info", `${asked} on this ${node.type} in ${output}: ${rule2.shows}`, at, node.id);
|
|
3512
|
-
else d.add("email/outlook-degraded", "info", `${asked} on this ${node.type} in classic Outlook: ${rule2.shows}`, at, node.id);
|
|
3513
|
-
}
|
|
3514
|
-
}
|
|
3515
|
-
function emailSupport(root) {
|
|
3962
|
+
function emailSupport(root, templatePath = (path) => path) {
|
|
3516
3963
|
const d = new Diagnostics();
|
|
3517
3964
|
d.list.push(...support(root, "email"));
|
|
3518
3965
|
const webFonts = new Set((root.props.fonts ?? []).map((face) => face.family.toLowerCase()));
|
|
3966
|
+
const phone = { styled: false, phoneHidden: false, desktopHidden: false };
|
|
3519
3967
|
const visit = (node) => {
|
|
3520
3968
|
structure(node, d);
|
|
3521
3969
|
fallback(node, webFonts, d);
|
|
3970
|
+
if (node.type === "Html") htmlBlock(node, d);
|
|
3971
|
+
if (node.type === "Image") imageFormat(node, "src", d);
|
|
3972
|
+
if (node.type === "Video" && node.props.thumbnail !== void 0) imageFormat(node, "thumbnail", d);
|
|
3973
|
+
if (node.mobile && Object.keys(node.mobile).length) phone.styled = true;
|
|
3974
|
+
if (node.hidden?.mobile && !builtIn(templatePath(node.path))) phone.phoneHidden = true;
|
|
3975
|
+
if (node.hidden?.desktop && !builtIn(templatePath(node.path))) phone.desktopHidden = true;
|
|
3522
3976
|
for (const child of node.children) if (isNode2(child) && shown(child)) visit(child);
|
|
3523
3977
|
};
|
|
3524
3978
|
visit(root);
|
|
3979
|
+
phoneStyles(root, phone, d);
|
|
3525
3980
|
return d.list;
|
|
3526
3981
|
}
|
|
3982
|
+
function builtIn(place) {
|
|
3983
|
+
const made = [...place.matchAll(/\((\w+)\)/g)].pop()?.[1];
|
|
3984
|
+
return !!made && Object.prototype.hasOwnProperty.call(COMPONENTS, made);
|
|
3985
|
+
}
|
|
3986
|
+
var NO_PHONE_STYLES = ["gmail-app-other", "sfr", "laposte", "gmx", "web-de", "rainloop"];
|
|
3987
|
+
function phoneStyles(root, phone, d) {
|
|
3988
|
+
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);
|
|
3989
|
+
if (!parts.length) return;
|
|
3990
|
+
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, {
|
|
3991
|
+
clients: NO_PHONE_STYLES,
|
|
3992
|
+
fix: phone.desktopHidden ? "show what a reader needs on desktop too, and hide on phones only what they can do without" : void 0,
|
|
3993
|
+
see: caniemail("css-at-media")
|
|
3994
|
+
});
|
|
3995
|
+
}
|
|
3996
|
+
var IMAGE_FORMATS = {
|
|
3997
|
+
webp: { clients: ["outlook"], feature: "image-webp", name: "a WebP" },
|
|
3998
|
+
avif: { clients: ["outlook", "fastmail", "gmx", "web-de", "orange", "sfr", "mail-ru", "ionos"], feature: "image-avif", name: "an AVIF" },
|
|
3999
|
+
svg: { clients: ["mail-ru"], feature: "image-svg", name: "an SVG" }
|
|
4000
|
+
};
|
|
4001
|
+
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];
|
|
4002
|
+
function imageFormat(node, key, d) {
|
|
4003
|
+
const src = assetParts(node.props[key]).src;
|
|
4004
|
+
const format = FORMAT_IN.map((pattern) => pattern.exec(src)?.[1]).find(Boolean)?.toLowerCase();
|
|
4005
|
+
const known = format && Object.prototype.hasOwnProperty.call(IMAGE_FORMATS, format) ? IMAGE_FORMATS[format] : void 0;
|
|
4006
|
+
if (!known) return;
|
|
4007
|
+
const what = key === "src" ? "This Image is" : "This Video's thumbnail is";
|
|
4008
|
+
d.warning("email/image-format", `${what} ${known.name} file, which these email clients don't show`, `${node.path}.props.${key}`, node.id, {
|
|
4009
|
+
clients: known.clients,
|
|
4010
|
+
fix: "use a PNG, JPEG or GIF file",
|
|
4011
|
+
see: caniemail(known.feature)
|
|
4012
|
+
});
|
|
4013
|
+
}
|
|
3527
4014
|
function fallback(node, webFonts, d) {
|
|
3528
4015
|
const stack = node.style.fontFamily;
|
|
3529
4016
|
if (typeof stack !== "string" || !webFonts.size) return;
|
|
@@ -3532,9 +4019,10 @@ function fallback(node, webFonts, d) {
|
|
|
3532
4019
|
if (!last || !webFonts.has(last.toLowerCase())) return;
|
|
3533
4020
|
d.warning(
|
|
3534
4021
|
"email/font-fallback",
|
|
3535
|
-
`This font stack ends with the web font "${last}": where it doesn't load
|
|
4022
|
+
`This font stack ends with the web font "${last}": where it doesn't load, the email client picks its own font`,
|
|
3536
4023
|
`${node.path}.style.fontFamily`,
|
|
3537
|
-
node.id
|
|
4024
|
+
node.id,
|
|
4025
|
+
{ clients: NO_WEB_FONTS, fix: `end it with a font the client has: "${stack}, Arial, sans-serif"`, see: caniemail("css-at-font-face") }
|
|
3538
4026
|
);
|
|
3539
4027
|
}
|
|
3540
4028
|
function structure(node, d) {
|
|
@@ -3547,7 +4035,8 @@ function structure(node, d) {
|
|
|
3547
4035
|
"info",
|
|
3548
4036
|
`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`,
|
|
3549
4037
|
path,
|
|
3550
|
-
id
|
|
4038
|
+
id,
|
|
4039
|
+
{ clients: ["outlook"], fix: "put less in it, or let it stack on phones (stackOnMobile)" }
|
|
3551
4040
|
);
|
|
3552
4041
|
}
|
|
3553
4042
|
const lines2 = wrapsWithGap(node);
|
|
@@ -3583,15 +4072,95 @@ function structure(node, d) {
|
|
|
3583
4072
|
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);
|
|
3584
4073
|
}
|
|
3585
4074
|
}
|
|
4075
|
+
var HTML_CSS = [
|
|
4076
|
+
{ 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" },
|
|
4077
|
+
{ 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" },
|
|
4078
|
+
{
|
|
4079
|
+
pattern: /(^|[\s;{"'])position\s*:\s*absolute\b/i,
|
|
4080
|
+
clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "yahoo", "aol", "hey", "sfr", "laposte", "mail-ru", "gmx", "web-de"],
|
|
4081
|
+
removes: "position: absolute: the client removes it",
|
|
4082
|
+
fix: "lay it out without position (a Layer puts content over an image)",
|
|
4083
|
+
feature: "css-position"
|
|
4084
|
+
},
|
|
4085
|
+
{
|
|
4086
|
+
// Outlook.com removes every position (captures); the others not named keep relative.
|
|
4087
|
+
pattern: /(^|[\s;{"'])position\s*:\s*relative\b/i,
|
|
4088
|
+
clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "mail-ru", "gmx", "web-de"],
|
|
4089
|
+
removes: "position: relative: the client removes it, and what it moves stays where it is",
|
|
4090
|
+
fix: "move it with padding instead",
|
|
4091
|
+
feature: "css-position",
|
|
4092
|
+
use: "relative"
|
|
4093
|
+
},
|
|
4094
|
+
{
|
|
4095
|
+
pattern: /var\(\s*--|(^|[\s;{"'])--[\w-]+\s*:/i,
|
|
4096
|
+
clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "yahoo", "aol", "orange", "mail-ru", "fastmail", "gmx", "web-de"],
|
|
4097
|
+
removes: "CSS variables: the client removes var() and their declarations",
|
|
4098
|
+
fix: "write the values themselves",
|
|
4099
|
+
feature: "css-variables"
|
|
4100
|
+
},
|
|
4101
|
+
{
|
|
4102
|
+
pattern: /gradient\(/i,
|
|
4103
|
+
clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail-app", "gmail-app-other", "yahoo", "aol", "orange", "gmx", "web-de"],
|
|
4104
|
+
removes: "a gradient: the client removes it",
|
|
4105
|
+
fix: "give it a solid background color too, or use an image",
|
|
4106
|
+
feature: "css-linear-gradient"
|
|
4107
|
+
},
|
|
4108
|
+
{ 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()" },
|
|
4109
|
+
{ 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)" },
|
|
4110
|
+
{
|
|
4111
|
+
pattern: /<style[\s>]/i,
|
|
4112
|
+
clients: ["gmail", "gmail-app", "gmail-app-other", "protonmail", "gmx", "web-de", "sfr", "laposte", "rainloop"],
|
|
4113
|
+
removes: "a <style> in the email's body: the client removes it, and what it styles shows without it",
|
|
4114
|
+
fix: "write the styles inline, on the elements they're for",
|
|
4115
|
+
feature: "html-style"
|
|
4116
|
+
},
|
|
4117
|
+
{ 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" },
|
|
4118
|
+
{ 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" },
|
|
4119
|
+
{ 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" },
|
|
4120
|
+
{
|
|
4121
|
+
pattern: /display\s*:\s*inline-flex\b/i,
|
|
4122
|
+
clients: ["outlook", "gmail-app-other", "yahoo", "aol", "orange", "gmx", "web-de"],
|
|
4123
|
+
removes: "display: inline-flex: the client lays it out without flex",
|
|
4124
|
+
fix: "lay it out with a Row",
|
|
4125
|
+
feature: "css-display-flex",
|
|
4126
|
+
use: "inline"
|
|
4127
|
+
},
|
|
4128
|
+
{ 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" },
|
|
4129
|
+
{ 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" },
|
|
4130
|
+
{
|
|
4131
|
+
pattern: /object-fit\s*:/i,
|
|
4132
|
+
clients: ["outlook", "gmail-app-other", "yahoo", "aol", "orange", "gmx", "web-de"],
|
|
4133
|
+
removes: "object-fit: the client stretches the image to its box",
|
|
4134
|
+
fix: "crop the image file to the size it's shown at",
|
|
4135
|
+
feature: "css-object-fit"
|
|
4136
|
+
},
|
|
4137
|
+
{
|
|
4138
|
+
pattern: /(^|[\s;{"'])aspect-ratio\s*:/i,
|
|
4139
|
+
clients: ["outlook", "outlook-com", "outlook-mac", "outlook-app", "gmail", "gmail-app", "gmail-app-other", "yahoo", "aol", "samsung-email", "thunderbird", "fastmail", "gmx", "web-de", "orange"],
|
|
4140
|
+
removes: "aspect-ratio: the client removes it",
|
|
4141
|
+
fix: "give it a width and a height",
|
|
4142
|
+
feature: "css-aspect-ratio"
|
|
4143
|
+
}
|
|
4144
|
+
];
|
|
4145
|
+
var OUTLOOK_ONLY2 = /<!--\[if\s+(?:(?:gte?|lte?)\s+)?mso[^\]]*\]>[\s\S]*?<!\[endif\]-->/gi;
|
|
4146
|
+
function htmlBlock(node, d) {
|
|
4147
|
+
if (typeof node.props.html !== "string") return;
|
|
4148
|
+
const markup = node.props.html.replace(OUTLOOK_ONLY2, "");
|
|
4149
|
+
for (const check of HTML_CSS) {
|
|
4150
|
+
if (check.pattern.test(markup)) {
|
|
4151
|
+
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) });
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
3586
4155
|
var isNode4 = (child) => typeof child !== "string";
|
|
3587
4156
|
var OUTPUT = { pdf: "a PDF", image: "an image" };
|
|
3588
4157
|
function pagedSupport(root, output) {
|
|
3589
4158
|
const d = new Diagnostics();
|
|
3590
4159
|
const where = OUTPUT[output];
|
|
3591
4160
|
const visit = (node) => {
|
|
3592
|
-
if (node
|
|
3593
|
-
|
|
3594
|
-
d.warning(`${output}/unsupported`,
|
|
4161
|
+
if (!shownAnywhere(node)) return;
|
|
4162
|
+
for (const condition of CONDITIONS) {
|
|
4163
|
+
if (condition.paged && blockOf(node, condition)) d.warning(`${output}/unsupported`, condition.paged(node, where), `${node.path}.${condition.name}`, node.id);
|
|
3595
4164
|
}
|
|
3596
4165
|
if (node.type === "Video") {
|
|
3597
4166
|
const shows = output === "pdf" ? "its thumbnail (where its file is given) or its title, linked to it" : "its title";
|
|
@@ -3744,15 +4313,15 @@ function classesFor(node, parent) {
|
|
|
3744
4313
|
const classes = [];
|
|
3745
4314
|
if (node.mobile) {
|
|
3746
4315
|
const { width, ...rest } = node.mobile;
|
|
3747
|
-
if (Object.keys(rest).length) classes.push(rule(
|
|
4316
|
+
if (Object.keys(rest).length) classes.push(rule(MOBILE.sheet.media, `${MOBILE.sheet.selector}{${css2(declarationsOf(rest), true)}}`));
|
|
3748
4317
|
const line2 = node.type === "Divider" && node.style.padding !== void 0 ? ">hr" : "";
|
|
3749
|
-
if (width !== void 0) classes.push(rule(
|
|
4318
|
+
if (width !== void 0) classes.push(rule(MOBILE.sheet.media, `${MOBILE.sheet.selector}${line2}{${css2(declarationsOf({ width }), true)}}`));
|
|
3750
4319
|
const own = node.style.width === void 0 && (node.style.flex === "none" || node.style.flex === 0);
|
|
3751
4320
|
const basis = own ? void 0 : length2(phoneWidthInRow(node, parent));
|
|
3752
4321
|
if (basis !== void 0) classes.push(rule("phone", `&{flex:0 1 ${basis}!important}`));
|
|
3753
4322
|
}
|
|
3754
4323
|
classes.push(...hidingClasses(node.hidden));
|
|
3755
|
-
if (node.hover) classes.push(rule(
|
|
4324
|
+
if (node.hover) classes.push(rule(HOVER.sheet.media, `${HOVER.sheet.selector}{${css2(declarationsOf(node.hover), true)}}`, HOVER.sheet.level));
|
|
3756
4325
|
if (node.type === "Row" && node.props.stackOnMobile !== false) {
|
|
3757
4326
|
classes.push(
|
|
3758
4327
|
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")
|
|
@@ -3960,17 +4529,17 @@ var Layer3 = ({ node, style, parent, placement, classes, children }) => /* @__PU
|
|
|
3960
4529
|
},
|
|
3961
4530
|
children()
|
|
3962
4531
|
);
|
|
3963
|
-
var
|
|
4532
|
+
var current2 = { page: 1, total: 1 };
|
|
3964
4533
|
function numbering(numbers, write3) {
|
|
3965
|
-
const before =
|
|
3966
|
-
|
|
4534
|
+
const before = current2;
|
|
4535
|
+
current2 = numbers;
|
|
3967
4536
|
try {
|
|
3968
4537
|
return write3();
|
|
3969
4538
|
} finally {
|
|
3970
|
-
|
|
4539
|
+
current2 = before;
|
|
3971
4540
|
}
|
|
3972
4541
|
}
|
|
3973
|
-
var PageNumber2 = ({ node }) => /* @__PURE__ */ h(Fragment, null, String(node.props.total ?
|
|
4542
|
+
var PageNumber2 = ({ node }) => /* @__PURE__ */ h(Fragment, null, String(node.props.total ? current2.total : current2.page));
|
|
3974
4543
|
function documentCss(root) {
|
|
3975
4544
|
const box2 = pageBox(root.props);
|
|
3976
4545
|
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}}`;
|
|
@@ -4144,8 +4713,23 @@ var Video3 = ({ node, style, placement, classes }) => /* @__PURE__ */ h(
|
|
|
4144
4713
|
))
|
|
4145
4714
|
);
|
|
4146
4715
|
var PAGE_PHONE_WIDTH = 480;
|
|
4147
|
-
|
|
4148
|
-
|
|
4716
|
+
function pagePhoneWidth(root) {
|
|
4717
|
+
const mobile = root.props.breakpoints?.mobile;
|
|
4718
|
+
return typeof mobile === "number" ? mobile : PAGE_PHONE_WIDTH;
|
|
4719
|
+
}
|
|
4720
|
+
function pageBreakpoints(root) {
|
|
4721
|
+
const mobile = root.props.breakpoints?.mobile;
|
|
4722
|
+
const width = typeof root.props.width === "number" ? root.props.width : void 0;
|
|
4723
|
+
if (root.type !== "Page" || typeof mobile !== "number" || width === void 0 || mobile < width) return [];
|
|
4724
|
+
return [
|
|
4725
|
+
{
|
|
4726
|
+
code: "page/phone-width",
|
|
4727
|
+
severity: "warning",
|
|
4728
|
+
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}`,
|
|
4729
|
+
path: "root.props.breakpoints"
|
|
4730
|
+
}
|
|
4731
|
+
];
|
|
4732
|
+
}
|
|
4149
4733
|
var COMPONENTS2 = {
|
|
4150
4734
|
Section: Section3,
|
|
4151
4735
|
Row: Row3,
|
|
@@ -4206,7 +4790,8 @@ function renderPage(resolved, options = {}) {
|
|
|
4206
4790
|
const writer2 = document ? Document2 : root.type === "Canvas" ? Canvas2 : Page2;
|
|
4207
4791
|
const body = withStylesheet(sheet, () => String(writer2(propsOf2(root, root, [], options))));
|
|
4208
4792
|
const fixed = !root.definition.responsive;
|
|
4209
|
-
const
|
|
4793
|
+
const phoneWidth2 = pagePhoneWidth(root);
|
|
4794
|
+
const stylesheet = [...fonts, sheet.css(fixed ? null : `@media (max-width: ${phoneWidth2}px)`, fixed ? null : `@media (min-width: ${phoneWidth2 + 1}px)`), document ? documentCss(root) : ""].join("");
|
|
4210
4795
|
const head = [
|
|
4211
4796
|
'<meta charset="utf-8">',
|
|
4212
4797
|
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
@@ -4226,7 +4811,7 @@ function renderText(resolved, options = {}) {
|
|
|
4226
4811
|
const tag = options.mergeTag ?? ((name) => `{{${name}}}`);
|
|
4227
4812
|
const inline = (node, upper = false) => node.children.map((child) => {
|
|
4228
4813
|
if (typeof child === "string") return upper ? child.toUpperCase() : child;
|
|
4229
|
-
if (child
|
|
4814
|
+
if (!shownAnywhere(child)) return "";
|
|
4230
4815
|
switch (child.type) {
|
|
4231
4816
|
case "LineBreak":
|
|
4232
4817
|
return "\n";
|
|
@@ -4250,7 +4835,7 @@ function renderText(resolved, options = {}) {
|
|
|
4250
4835
|
return alt ? `[${upper ? alt.toUpperCase() : alt}]` : "";
|
|
4251
4836
|
};
|
|
4252
4837
|
const blocks2 = (node) => node.children.flatMap((child) => {
|
|
4253
|
-
if (typeof child === "string" || child
|
|
4838
|
+
if (typeof child === "string" || !shownAnywhere(child)) return [];
|
|
4254
4839
|
switch (child.type) {
|
|
4255
4840
|
case "Heading":
|
|
4256
4841
|
return [tidy(inline(child, Number(child.props.level) <= 2))];
|
|
@@ -4288,8 +4873,8 @@ function renderText(resolved, options = {}) {
|
|
|
4288
4873
|
return blocks2(child);
|
|
4289
4874
|
}
|
|
4290
4875
|
});
|
|
4291
|
-
const
|
|
4292
|
-
const list = (node, indent) => node.children.filter((child) => typeof child !== "string" &&
|
|
4876
|
+
const shown22 = (node) => shownAnywhere(node);
|
|
4877
|
+
const list = (node, indent) => node.children.filter((child) => typeof child !== "string" && shown22(child)).map((item, index) => {
|
|
4293
4878
|
const marker2 = node.props.type === "number" ? `${Number(node.props.start) + index}. ` : "- ";
|
|
4294
4879
|
const lines2 = [];
|
|
4295
4880
|
let run = [];
|
|
@@ -4305,13 +4890,13 @@ function renderText(resolved, options = {}) {
|
|
|
4305
4890
|
}
|
|
4306
4891
|
flush();
|
|
4307
4892
|
if (!lines2.length) lines2.push(`${indent}${marker2}`.trimEnd());
|
|
4308
|
-
if (
|
|
4893
|
+
if (shown22(child)) lines2.push(list(child, `${indent} `));
|
|
4309
4894
|
}
|
|
4310
4895
|
flush();
|
|
4311
4896
|
return (lines2.length ? lines2 : [`${indent}${marker2}`.trimEnd()]).join("\n");
|
|
4312
4897
|
}).join("\n");
|
|
4313
|
-
const table = (node) => node.children.filter((row) => typeof row !== "string" &&
|
|
4314
|
-
(row) => row.children.filter((cell) => typeof cell !== "string" &&
|
|
4898
|
+
const table = (node) => node.children.filter((row) => typeof row !== "string" && shown22(row)).map(
|
|
4899
|
+
(row) => row.children.filter((cell) => typeof cell !== "string" && shown22(cell)).map((cell) => tidy(inline(cell))).join(" | ")
|
|
4315
4900
|
).join("\n");
|
|
4316
4901
|
return blocks2(resolved.root).filter((block) => block.length > 0).join("\n\n");
|
|
4317
4902
|
}
|
|
@@ -4354,45 +4939,39 @@ var OUTPUTS = {
|
|
|
4354
4939
|
Email: { target: "email", ready: true }
|
|
4355
4940
|
};
|
|
4356
4941
|
function prepareTemplate(input, options = {}) {
|
|
4942
|
+
checkEmailClients(options.emailClients);
|
|
4357
4943
|
const registry = registryOf(options);
|
|
4358
4944
|
const checked = checkDocument(input, registry);
|
|
4359
4945
|
const diagnostics = [...checked.diagnostics];
|
|
4360
4946
|
const failed = () => diagnostics.some((d) => d.severity === "error");
|
|
4361
4947
|
if (!checked.document || failed()) return { resolved: null, target: null, diagnostics };
|
|
4362
4948
|
const root = checked.document.root.type;
|
|
4949
|
+
const done = (resolved2, target) => {
|
|
4950
|
+
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" });
|
|
4951
|
+
return { resolved: resolved2, target, diagnostics: root === "Email" ? forEmailClients(diagnostics, options.emailClients) : diagnostics };
|
|
4952
|
+
};
|
|
4363
4953
|
const output = OUTPUTS[root];
|
|
4364
4954
|
if (!output?.ready) {
|
|
4365
4955
|
diagnostics.push({ code: "output/unavailable", severity: "error", message: output?.later ?? `A ${root} can't be rendered yet`, path: "root" });
|
|
4366
|
-
return
|
|
4956
|
+
return done(null, null);
|
|
4367
4957
|
}
|
|
4368
|
-
const expanded = expandDocument(checked.document, registry);
|
|
4958
|
+
const expanded = expandDocument(checked.document, registry, output.target);
|
|
4369
4959
|
diagnostics.push(...expanded.diagnostics);
|
|
4370
|
-
if (failed()) return
|
|
4960
|
+
if (failed()) return done(null, output.target);
|
|
4371
4961
|
diagnostics.push(...checkTargetExpanded(expanded.document, output.target, registry, expanded.paths));
|
|
4372
|
-
if (failed()) return
|
|
4962
|
+
if (failed()) return done(null, output.target);
|
|
4373
4963
|
const resolved = resolveExpanded(expanded.document, registry, expanded.paths);
|
|
4374
4964
|
diagnostics.push(...resolved.diagnostics);
|
|
4375
|
-
const found = output.target === "email" ? emailSupport(resolved.root) : support(resolved.root, output.target);
|
|
4965
|
+
const found = output.target === "email" ? emailSupport(resolved.root, resolved.templatePath) : support(resolved.root, output.target);
|
|
4376
4966
|
if (root === "Document" || root === "Canvas") found.push(...overflow(resolved.root));
|
|
4377
|
-
|
|
4967
|
+
found.push(...pageBreakpoints(resolved.root));
|
|
4378
4968
|
diagnostics.push(...inTemplate(resolved, found));
|
|
4379
|
-
return
|
|
4969
|
+
return done(failed() ? null : resolved, output.target);
|
|
4380
4970
|
}
|
|
4381
4971
|
function inTemplate(resolved, diagnostics) {
|
|
4382
4972
|
const { templatePath } = resolved;
|
|
4383
4973
|
return templatePath ? diagnostics.map((diagnostic) => ({ ...diagnostic, path: templatePath(diagnostic.path) })) : diagnostics;
|
|
4384
4974
|
}
|
|
4385
|
-
function pageNumbersOutside(root) {
|
|
4386
|
-
const out = [];
|
|
4387
|
-
const visit = (node) => {
|
|
4388
|
-
if (node.type === "PageNumber") {
|
|
4389
|
-
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 } : {} });
|
|
4390
|
-
}
|
|
4391
|
-
for (const child of node.children) if (typeof child !== "string") visit(child);
|
|
4392
|
-
};
|
|
4393
|
-
visit(root);
|
|
4394
|
-
return out;
|
|
4395
|
-
}
|
|
4396
4975
|
function validateTemplate(input, options = {}) {
|
|
4397
4976
|
const { diagnostics, resolved } = prepareTemplate(input, options);
|
|
4398
4977
|
const root = resolved?.root;
|
|
@@ -4401,12 +4980,22 @@ function validateTemplate(input, options = {}) {
|
|
|
4401
4980
|
return { valid: !all.some((d) => d.severity === "error"), diagnostics: all };
|
|
4402
4981
|
}
|
|
4403
4982
|
function renderTemplate(input, options = {}) {
|
|
4983
|
+
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" }]);
|
|
4404
4984
|
const { resolved, target, diagnostics } = prepareTemplate(input, options);
|
|
4405
4985
|
if (!resolved) throw new ElementsError(diagnostics);
|
|
4406
4986
|
const text2 = renderText(resolved, options);
|
|
4407
4987
|
const written = target === "email" ? renderEmail(resolved, options) : { ...renderPage(resolved, options), diagnostics: [] };
|
|
4408
|
-
const
|
|
4409
|
-
|
|
4988
|
+
const shown22 = (html) => options.pretty ? prettyHtml(html) : html;
|
|
4989
|
+
const blocks2 = "blocks" in written ? written.blocks : void 0;
|
|
4990
|
+
return {
|
|
4991
|
+
html: shown22(written.html),
|
|
4992
|
+
head: shown22(written.head),
|
|
4993
|
+
body: shown22(written.body),
|
|
4994
|
+
bodyStyle: written.bodyStyle,
|
|
4995
|
+
text: text2,
|
|
4996
|
+
diagnostics: [...diagnostics, ...inTemplate(resolved, forEmailClients(written.diagnostics, options.emailClients))],
|
|
4997
|
+
...blocks2 ? { blocks: blocks2 } : {}
|
|
4998
|
+
};
|
|
4410
4999
|
}
|
|
4411
5000
|
|
|
4412
5001
|
// src/utils/collect.ts
|
|
@@ -4437,7 +5026,7 @@ function isAsyncComponent(type) {
|
|
|
4437
5026
|
}
|
|
4438
5027
|
var SHARED2 = Symbol.for("@hyper-elements/react/render-builds");
|
|
4439
5028
|
var using = globalThis[SHARED2] ?? (globalThis[SHARED2] = []);
|
|
4440
|
-
var
|
|
5029
|
+
var same4 = (a2, b) => a2.renderToString === b.renderToString;
|
|
4441
5030
|
function dispatcher() {
|
|
4442
5031
|
const internals = React2__default.default;
|
|
4443
5032
|
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;
|
|
@@ -4484,7 +5073,7 @@ function secondBuild(outer) {
|
|
|
4484
5073
|
for (const requirer of requirers) {
|
|
4485
5074
|
try {
|
|
4486
5075
|
const build = requirer()?.(id);
|
|
4487
|
-
if (build?.renderToStaticMarkup && !
|
|
5076
|
+
if (build?.renderToStaticMarkup && !same4(build, defaultBuild__namespace) && ![null, outer].includes(dispatcherOf(build))) {
|
|
4488
5077
|
second = build;
|
|
4489
5078
|
break search;
|
|
4490
5079
|
}
|
|
@@ -4498,11 +5087,11 @@ function secondBuild(outer) {
|
|
|
4498
5087
|
var buildOf = (outer) => outer === null ? void 0 : [defaultBuild__namespace, second].find((b) => !!b && dispatcherOf(b) === outer);
|
|
4499
5088
|
function withBuild(render2) {
|
|
4500
5089
|
const outer = dispatcher();
|
|
4501
|
-
let build =
|
|
5090
|
+
let build = current3();
|
|
4502
5091
|
const theirs = using.length === 0 || collecting() !== void 0 ? buildOf(outer) : void 0;
|
|
4503
5092
|
const busy = [];
|
|
4504
5093
|
if (theirs) {
|
|
4505
|
-
const free = (b) => !
|
|
5094
|
+
const free = (b) => !same4(b, theirs) && !using.some((u) => same4(u, b));
|
|
4506
5095
|
const other = free(defaultBuild__namespace) ? defaultBuild__namespace : secondBuild(outer);
|
|
4507
5096
|
if (other && free(other)) {
|
|
4508
5097
|
busy.push(theirs);
|
|
@@ -4527,10 +5116,10 @@ function rendersHere(build) {
|
|
|
4527
5116
|
return false;
|
|
4528
5117
|
}
|
|
4529
5118
|
}
|
|
4530
|
-
function
|
|
5119
|
+
function current3() {
|
|
4531
5120
|
return using[using.length - 1] ?? defaultBuild__namespace;
|
|
4532
5121
|
}
|
|
4533
|
-
var renderToString = (element, options) =>
|
|
5122
|
+
var renderToString = (element, options) => current3().renderToString(element, options);
|
|
4534
5123
|
|
|
4535
5124
|
// src/utils/render-scope.ts
|
|
4536
5125
|
function withRenderScope(render2) {
|
|
@@ -4584,7 +5173,7 @@ function withoutAsync(children, collection) {
|
|
|
4584
5173
|
}
|
|
4585
5174
|
var ENTITIES2 = { "&": "&", "<": "<", ">": ">", """: '"', "'": "'" };
|
|
4586
5175
|
var unescape = (text2) => text2.replace(/&|<|>|"|'/g, (entity) => ENTITIES2[entity]);
|
|
4587
|
-
var NODE_KEYS2 = /* @__PURE__ */ new Set(["id", "style", "
|
|
5176
|
+
var NODE_KEYS2 = /* @__PURE__ */ new Set(["id", "style", "hidden"]);
|
|
4588
5177
|
function toJsonNode({ type, props }, diagnostics) {
|
|
4589
5178
|
const node = { type, children: [] };
|
|
4590
5179
|
const semantic = {};
|
|
@@ -4709,10 +5298,10 @@ function collectTemplate(element) {
|
|
|
4709
5298
|
path: "jsx"
|
|
4710
5299
|
});
|
|
4711
5300
|
}
|
|
4712
|
-
return { document: null, diagnostics, components };
|
|
5301
|
+
return { document: null, diagnostics, components, collected: null };
|
|
4713
5302
|
}
|
|
4714
5303
|
const result = checkDocument({ root: top[0] }, new Registry(components));
|
|
4715
|
-
return { document: result.document, diagnostics: [...diagnostics, ...result.diagnostics], components };
|
|
5304
|
+
return { document: result.document, diagnostics: [...diagnostics, ...result.diagnostics], components, collected: top[0] };
|
|
4716
5305
|
}
|
|
4717
5306
|
|
|
4718
5307
|
// src/primitives/components.tsx
|
|
@@ -4802,7 +5391,8 @@ function component(spec) {
|
|
|
4802
5391
|
expand(props, context) {
|
|
4803
5392
|
const { nodes, components, diagnostics } = collectNodes(spec.render(props));
|
|
4804
5393
|
for (const used of components) context.register(used);
|
|
4805
|
-
if (diagnostics.
|
|
5394
|
+
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) throw new Error(formatDiagnostics(diagnostics));
|
|
5395
|
+
for (const diagnostic of diagnostics) context.report(diagnostic);
|
|
4806
5396
|
return nodes.length === 1 ? nodes[0] : nodes;
|
|
4807
5397
|
}
|
|
4808
5398
|
});
|
|
@@ -4821,12 +5411,12 @@ function component(spec) {
|
|
|
4821
5411
|
// src/primitives/outline.ts
|
|
4822
5412
|
var LONGEST = 80;
|
|
4823
5413
|
var clip = (text2) => text2.length > LONGEST ? `${text2.slice(0, LONGEST - 1)}\u2026` : text2;
|
|
4824
|
-
var
|
|
4825
|
-
var blocks = (node) => node.children.filter((child) => typeof child !== "string" &&
|
|
5414
|
+
var shown2 = (node) => shownAnywhere(node);
|
|
5415
|
+
var blocks = (node) => node.children.filter((child) => typeof child !== "string" && shown2(child));
|
|
4826
5416
|
function words(node) {
|
|
4827
5417
|
return node.children.map((child) => {
|
|
4828
5418
|
if (typeof child === "string") return child;
|
|
4829
|
-
if (!
|
|
5419
|
+
if (!shown2(child) || child.type === "List") return "";
|
|
4830
5420
|
if (child.type === "MergeTag") return `{{${child.props.name}}}`;
|
|
4831
5421
|
if (child.type === "LineBreak") return " / ";
|
|
4832
5422
|
if (child.type === "Image") return `[${child.props.alt ?? ""}]`;
|
|
@@ -4877,14 +5467,16 @@ function template(element, call) {
|
|
|
4877
5467
|
return collectTemplate(element);
|
|
4878
5468
|
}
|
|
4879
5469
|
async function render(element, options = {}) {
|
|
5470
|
+
checkEmailClients(options.emailClients);
|
|
4880
5471
|
const { document, diagnostics, components } = template(element, "render");
|
|
4881
5472
|
if (!document || diagnostics.some((d) => d.severity === "error")) throw new ElementsError(diagnostics);
|
|
4882
5473
|
const rendered = renderTemplate(document, { ...options, components });
|
|
4883
5474
|
return { ...rendered, diagnostics: [...diagnostics, ...rendered.diagnostics] };
|
|
4884
5475
|
}
|
|
4885
|
-
function validate(element) {
|
|
5476
|
+
function validate(element, options = {}) {
|
|
5477
|
+
checkEmailClients(options.emailClients);
|
|
4886
5478
|
const { document, diagnostics, components } = template(element, "validate");
|
|
4887
|
-
const all = [...diagnostics, ...document && !diagnostics.some((d) => d.severity === "error") ? validateTemplate(document, { components }).diagnostics : []];
|
|
5479
|
+
const all = [...diagnostics, ...document && !diagnostics.some((d) => d.severity === "error") ? validateTemplate(document, { components, emailClients: options.emailClients }).diagnostics : []];
|
|
4888
5480
|
return { valid: !all.some((d) => d.severity === "error"), diagnostics: all };
|
|
4889
5481
|
}
|
|
4890
5482
|
function outline(element) {
|
|
@@ -4895,21 +5487,25 @@ function outline(element) {
|
|
|
4895
5487
|
if (!root) throw new ElementsError([...diagnostics, ...prepared.diagnostics]);
|
|
4896
5488
|
return outlineOf(root);
|
|
4897
5489
|
}
|
|
4898
|
-
function prepare(element) {
|
|
5490
|
+
function prepare(element, options = {}) {
|
|
5491
|
+
checkEmailClients(options.emailClients);
|
|
4899
5492
|
const { document, diagnostics, components } = template(element, "prepare");
|
|
4900
5493
|
if (!document || diagnostics.some((d) => d.severity === "error")) return { root: null, diagnostics, layout: LAYOUT_VERSION };
|
|
4901
|
-
const prepared = prepareTemplate(document, { components });
|
|
5494
|
+
const prepared = prepareTemplate(document, { components, emailClients: options.emailClients });
|
|
4902
5495
|
const root = prepared.resolved?.root ?? null;
|
|
4903
5496
|
const paged = root?.type === "Document" ? pagedSupport(root, "pdf") : root?.type === "Canvas" ? pagedSupport(root, "image") : [];
|
|
4904
5497
|
return { root, diagnostics: [...diagnostics, ...prepared.diagnostics, ...paged], layout: LAYOUT_VERSION };
|
|
4905
5498
|
}
|
|
4906
5499
|
|
|
5500
|
+
exports.BLOCK_MAP_VERSION = BLOCK_MAP_VERSION;
|
|
4907
5501
|
exports.Button = Button4;
|
|
4908
5502
|
exports.Canvas = Canvas3;
|
|
4909
5503
|
exports.Circle = Circle2;
|
|
4910
5504
|
exports.Column = Column4;
|
|
5505
|
+
exports.DEFAULT_EMAIL_CLIENTS = DEFAULT_EMAIL_CLIENTS;
|
|
4911
5506
|
exports.Divider = Divider4;
|
|
4912
5507
|
exports.Document = Document3;
|
|
5508
|
+
exports.EMAIL_CLIENTS = EMAIL_CLIENTS;
|
|
4913
5509
|
exports.ElementsError = ElementsError;
|
|
4914
5510
|
exports.Ellipse = Ellipse2;
|
|
4915
5511
|
exports.Email = Email3;
|