@emailens/engine 0.12.0 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-UFSYNSDP.js → chunk-KKWC2C3R.js} +3 -3
- package/dist/chunk-KKWC2C3R.js.map +1 -0
- package/dist/{chunk-PW6EUG2G.js → chunk-OA6VNA5L.js} +16 -4
- package/dist/chunk-OA6VNA5L.js.map +1 -0
- package/dist/compile/index.cjs +18 -6
- package/dist/compile/index.cjs.map +1 -1
- package/dist/compile/index.d.cts +2 -21
- package/dist/compile/index.d.ts +2 -21
- package/dist/compile/index.js +4 -4
- package/dist/index.cjs +388 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +98 -5
- package/dist/index.d.ts +98 -5
- package/dist/index.js +387 -29
- package/dist/index.js.map +1 -1
- package/dist/maizzle-R5KU3HHZ.js +11 -0
- package/dist/{react-email-CVZZNVZ2.js → react-email-2DGTHWTN.js} +2 -2
- package/dist/{react-email-BzyXjPrH.d.ts → react-email-BTIw8QV8.d.ts} +9 -4
- package/dist/{react-email-DHkCZ7TG.d.cts → react-email-Dmn1QpXz.d.cts} +9 -4
- package/dist/server.d.cts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/{types-edFeUest.d.cts → types-DdFHwMtT.d.cts} +23 -88
- package/dist/{types-edFeUest.d.ts → types-DdFHwMtT.d.ts} +23 -88
- package/package.json +1 -1
- package/dist/chunk-PW6EUG2G.js.map +0 -1
- package/dist/chunk-UFSYNSDP.js.map +0 -1
- package/dist/maizzle-KKJF7XY7.js +0 -9
- /package/dist/{maizzle-KKJF7XY7.js.map → maizzle-R5KU3HHZ.js.map} +0 -0
- /package/dist/{react-email-CVZZNVZ2.js.map → react-email-2DGTHWTN.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from "./chunk-2NMEKWO5.js";
|
|
9
9
|
|
|
10
10
|
// src/clients.ts
|
|
11
|
-
var
|
|
11
|
+
var CLIENTS = [
|
|
12
12
|
{
|
|
13
13
|
id: "gmail-web",
|
|
14
14
|
name: "Gmail",
|
|
@@ -183,6 +183,7 @@ var EMAIL_CLIENTS = [
|
|
|
183
183
|
icon: "monitor"
|
|
184
184
|
}
|
|
185
185
|
];
|
|
186
|
+
var EMAIL_CLIENTS = [...CLIENTS];
|
|
186
187
|
function getClient(id) {
|
|
187
188
|
return EMAIL_CLIENTS.find((c) => c.id === id);
|
|
188
189
|
}
|
|
@@ -8622,7 +8623,9 @@ var GRACEFUL_FEATURES = /* @__PURE__ */ new Set([
|
|
|
8622
8623
|
"[aria-labelledby]",
|
|
8623
8624
|
"[aria-live]",
|
|
8624
8625
|
"[lang]",
|
|
8625
|
-
"[dir]"
|
|
8626
|
+
"[dir]",
|
|
8627
|
+
"[width]",
|
|
8628
|
+
"[height]"
|
|
8626
8629
|
]);
|
|
8627
8630
|
var OUTLOOK_WORD_UNSUPPORTED = /* @__PURE__ */ new Set([
|
|
8628
8631
|
"border-radius",
|
|
@@ -10939,9 +10942,74 @@ var MAIZZLE_SUGGESTION_DATABASE = {
|
|
|
10939
10942
|
"opacity::maizzle": "Use solid Tailwind color classes instead of opacity."
|
|
10940
10943
|
};
|
|
10941
10944
|
|
|
10945
|
+
// src/fix-snippets/framework-markup.ts
|
|
10946
|
+
var FRAMEWORK_MARKUP_SUGGESTIONS = {
|
|
10947
|
+
// ── The doctype ───────────────────────────────────────────────────────
|
|
10948
|
+
// Every client that "does not support" it simply rewrites it, and none of
|
|
10949
|
+
// the three frameworks lets you omit it. There is nothing to do.
|
|
10950
|
+
"doctype::mjml": "MJML writes the doctype itself, and the clients that replace it do so without changing how the email renders. Nothing to change in your MJML.",
|
|
10951
|
+
"doctype::maizzle": "Your Maizzle layout writes the doctype, and the clients that replace it do so without changing how the email renders. Nothing to change.",
|
|
10952
|
+
"doctype::jsx": "React Email's <Html> renders the doctype, and the clients that replace it do so without changing how the email renders. Nothing to change.",
|
|
10953
|
+
// ── lang and dir ──────────────────────────────────────────────────────
|
|
10954
|
+
// Dropped, these cost screen-reader language and bidi hints, not layout.
|
|
10955
|
+
"[lang]::mjml": "MJML sets lang on <html> from the lang attribute on <mjml>. Where a client drops it only assistive technology notices, so leave it: removing it helps nobody.",
|
|
10956
|
+
"[lang]::maizzle": "Your Maizzle layout sets lang on <html>. Where a client drops it only assistive technology notices, so leave it.",
|
|
10957
|
+
"[lang]::jsx": "React Email renders this from <Html lang=\u2026>. Where a client drops it only assistive technology notices, so leave it.",
|
|
10958
|
+
"[dir]::mjml": "MJML sets dir from the dir attribute on <mjml>. For a right-to-left email, also set align on mj-text and mj-section, which those clients do honour.",
|
|
10959
|
+
"[dir]::maizzle": "Your Maizzle layout sets dir. For a right-to-left email, also set align on the cells, which these clients do honour.",
|
|
10960
|
+
"[dir]::jsx": "React Email renders this from <Html dir=\u2026>. For a right-to-left email, also set textAlign on the components, which these clients do honour.",
|
|
10961
|
+
// ── role ──────────────────────────────────────────────────────────────
|
|
10962
|
+
// All three add it to their layout tables. Dropping it loses the
|
|
10963
|
+
// screen-reader hint and nothing visual.
|
|
10964
|
+
"[role]::mjml": 'MJML adds role="presentation" to the tables it generates. A client that strips it announces the layout table to screen readers, which is worth knowing but is not something you can change from your MJML.',
|
|
10965
|
+
"[role]::maizzle": "Maizzle's markup transformer adds role to layout tables. A client that strips it announces the table to screen readers; you can turn the transformer off, but the attribute is better present than absent.",
|
|
10966
|
+
"[role]::jsx": "React Email's layout components add role to their tables. A client that strips it announces the table to screen readers, which is not something to fix in your component.",
|
|
10967
|
+
// ── align ─────────────────────────────────────────────────────────────
|
|
10968
|
+
"[align]::mjml": "MJML writes align onto the cells it generates. Set it through the align attribute on mj-section, mj-column, mj-image or mj-text; for the clients that ignore it, add text-align through mj-style and a css-class.",
|
|
10969
|
+
"[align]::maizzle": "Maizzle writes align onto your table cells. For the clients that ignore it, add a text-align utility as well: those survive inlining.",
|
|
10970
|
+
"[align]::jsx": "React Email components render align. For the clients that ignore it, pass textAlign in the style prop too, which is inlined and survives.",
|
|
10971
|
+
// ── width and height ──────────────────────────────────────────────────
|
|
10972
|
+
"[width]::mjml": "MJML writes width from mj-image's or mj-section's width attribute. A client that drops it sizes the element by its content, so also set width through mj-style and a css-class.",
|
|
10973
|
+
"[width]::maizzle": "Maizzle writes width onto the table it generates. A client that drops it sizes by content, so keep a width utility on the element as well: inlined CSS survives where the attribute does not.",
|
|
10974
|
+
"[width]::jsx": "React Email renders width from the component's width prop. A client that drops it sizes by content, so set width in the style prop too.",
|
|
10975
|
+
"[height]::mjml": "MJML writes height from mj-image's height attribute. A client that drops it sizes by content, which for an image means reflow while it loads; set height through mj-style and a css-class as well.",
|
|
10976
|
+
"[height]::maizzle": "Maizzle writes height onto the element. A client that drops it sizes by content, so keep a height utility on it as well.",
|
|
10977
|
+
"[height]::jsx": "React Email renders height from the component's height prop. Set it in the style prop too, so the clients that drop the attribute still reserve the space.",
|
|
10978
|
+
// ── <body> ────────────────────────────────────────────────────────────
|
|
10979
|
+
"<body>::mjml": "MJML styles <body> from mj-body's background-color. Clients that replace <body> with a <div> lose it, so set background-color on mj-section too and the page still fills.",
|
|
10980
|
+
"<body>::maizzle": "Your layout styles <body>. Clients that replace it with a <div> lose those styles, so put the background on a full-width wrapper table as well.",
|
|
10981
|
+
"<body>::jsx": "React Email's <Body> carries your style prop. Clients that replace <body> with a <div> lose it, so put the background on a wrapping <Section> too."
|
|
10982
|
+
};
|
|
10983
|
+
var STYLE_SURVIVAL_NOTES = {
|
|
10984
|
+
"gmail-space-separated-color::mjml": "The block comes from mj-style; write the colour with commas there.",
|
|
10985
|
+
"gmail-space-separated-color::maizzle": "Maizzle rewrites colours it inlines, but a rule left in a <style> block passes through untouched. Fix it in your CSS source.",
|
|
10986
|
+
"gmail-space-separated-color::jsx": "The block comes from a <style> element in your component; write the colour with commas.",
|
|
10987
|
+
"gmail-space-separated-color-inline::mjml": "MJML builds inline styles from component attributes, so the colour came from one of them: write it with commas.",
|
|
10988
|
+
"gmail-space-separated-color-inline::maizzle": "This survived inlining, so it was written inline in your template rather than in a utility class. Write it with commas.",
|
|
10989
|
+
"gmail-space-separated-color-inline::jsx": "The colour is in a style prop object; write it with commas.",
|
|
10990
|
+
"outlook-double-brace::mjml": "The stylesheet is assembled from mj-style. Put a newline before the closing brace of the media query, or stop minifying that block.",
|
|
10991
|
+
"outlook-double-brace::maizzle": "Maizzle's minifier produces this. Leave a space between the braces in the source, or exclude the <style> block from minification.",
|
|
10992
|
+
"outlook-double-brace::jsx": "The stylesheet comes from a <style> element in your component. Put a newline before the media query's closing brace.",
|
|
10993
|
+
"yahoo-attribute-selector-semicolon::mjml": "The selector is in mj-style; split it into [style^=\u2026][style$=\u2026] there.",
|
|
10994
|
+
"yahoo-attribute-selector-semicolon::maizzle": "The selector is in your CSS source; split it into [style^=\u2026][style$=\u2026] there.",
|
|
10995
|
+
"yahoo-attribute-selector-semicolon::jsx": "The selector is in a <style> element in your component; split it into [style^=\u2026][style$=\u2026].",
|
|
10996
|
+
"outlook-web-chained-class::mjml": "css-class takes several names, and MJML passes them through as written. Give the element one class carrying both rules instead.",
|
|
10997
|
+
"outlook-web-chained-class::maizzle": "A compound like .a.b usually comes from @apply or an arbitrary variant. Flatten it into a single class in your component layer.",
|
|
10998
|
+
"outlook-web-chained-class::jsx": "Give the element one class carrying both rules, or move the declarations into the style prop, which is inlined and unaffected.",
|
|
10999
|
+
"outlook-first-class-only::mjml": "The classes come from css-class. Merge the conflicting ones into a single class in mj-style.",
|
|
11000
|
+
"outlook-first-class-only::maizzle": "Inlining sidesteps this entirely, and Maizzle inlines by default. If you turned it off, merge the conflicting utilities into one component class.",
|
|
11001
|
+
"outlook-first-class-only::jsx": "Merge the conflicting classes, or move the declarations into the style prop, which is inlined and unaffected by this.",
|
|
11002
|
+
"outlook-first-class-descendants::mjml": "The rule is in mj-style. Target the child directly with its own css-class rather than through a descendant selector.",
|
|
11003
|
+
"outlook-first-class-descendants::maizzle": "Inlining sidesteps this, and Maizzle inlines by default. Otherwise put the rule on the child element rather than on an ancestor's class.",
|
|
11004
|
+
"outlook-first-class-descendants::jsx": "Put the declarations on the child component, through its own class or its style prop, rather than through a descendant selector."
|
|
11005
|
+
};
|
|
11006
|
+
|
|
10942
11007
|
// src/fix-snippets/index.ts
|
|
10943
11008
|
var FIX_DATABASE = __spreadValues(__spreadValues(__spreadValues(__spreadValues({}, HTML_FIX_DATABASE), JSX_FIX_DATABASE), MJML_FIX_DATABASE), MAIZZLE_FIX_DATABASE);
|
|
10944
|
-
var SUGGESTION_DATABASE = __spreadValues(__spreadValues(__spreadValues(__spreadValues({}, HTML_SUGGESTION_DATABASE), JSX_SUGGESTION_DATABASE), MJML_SUGGESTION_DATABASE), MAIZZLE_SUGGESTION_DATABASE);
|
|
11009
|
+
var SUGGESTION_DATABASE = __spreadValues(__spreadValues(__spreadValues(__spreadValues(__spreadValues({}, HTML_SUGGESTION_DATABASE), JSX_SUGGESTION_DATABASE), MJML_SUGGESTION_DATABASE), MAIZZLE_SUGGESTION_DATABASE), FRAMEWORK_MARKUP_SUGGESTIONS);
|
|
11010
|
+
function getStyleSurvivalNote(rule, framework) {
|
|
11011
|
+
return framework ? STYLE_SURVIVAL_NOTES[`${rule}::${framework}`] : void 0;
|
|
11012
|
+
}
|
|
10945
11013
|
function getCodeFix(property, clientId, framework) {
|
|
10946
11014
|
const clientPrefix = getClientPrefix(clientId);
|
|
10947
11015
|
if (framework && clientPrefix) {
|
|
@@ -12042,6 +12110,8 @@ var GENERIC_LINK_TEXT = /* @__PURE__ */ new Set([
|
|
|
12042
12110
|
]);
|
|
12043
12111
|
var GMAIL_CLIP_THRESHOLD = 102 * 1024;
|
|
12044
12112
|
var GMAIL_CLIP_WARNING_THRESHOLD = 90 * 1024;
|
|
12113
|
+
var GMAIL_STYLE_LIMIT = 16 * 1024;
|
|
12114
|
+
var GMAIL_STYLE_WARNING_THRESHOLD = 14 * 1024;
|
|
12045
12115
|
var CLIENT_DISPLAY_LIMITS = [
|
|
12046
12116
|
{ client: "Gmail (Web)", subjectLimit: 70, preheaderLimit: 90 },
|
|
12047
12117
|
{ client: "Gmail (Mobile)", subjectLimit: 40, preheaderLimit: 45 },
|
|
@@ -12076,7 +12146,8 @@ var EMPTY_LINKS = {
|
|
|
12076
12146
|
var EMPTY_ACCESSIBILITY = { score: 100, issues: [] };
|
|
12077
12147
|
var EMPTY_IMAGES = { total: 0, totalDataUriBytes: 0, issues: [], images: [] };
|
|
12078
12148
|
var EMPTY_INBOX_PREVIEW = { subject: null, preheader: null, subjectLength: 0, preheaderLength: 0, truncation: [], issues: [] };
|
|
12079
|
-
var EMPTY_SIZE = { htmlBytes: 0, humanSize: "0 B", clipped: false, issues: [] };
|
|
12149
|
+
var EMPTY_SIZE = { htmlBytes: 0, styleBytes: 0, humanSize: "0 B", clipped: false, issues: [] };
|
|
12150
|
+
var EMPTY_STYLE_SURVIVAL = { issues: [] };
|
|
12080
12151
|
var EMPTY_TEMPLATE = { unresolvedCount: 0, issues: [] };
|
|
12081
12152
|
var EMPTY_OVERFLOW = { hasOverflow: false, issues: [] };
|
|
12082
12153
|
var EMPTY_VML = { hasVml: false, issues: [] };
|
|
@@ -14615,6 +14686,274 @@ function extractCode(response) {
|
|
|
14615
14686
|
return response.trim();
|
|
14616
14687
|
}
|
|
14617
14688
|
|
|
14689
|
+
// src/style-survival.ts
|
|
14690
|
+
import * as csstree6 from "css-tree";
|
|
14691
|
+
var WORD_ENGINE = ["outlook-windows-legacy"];
|
|
14692
|
+
var OUTLOOK_WEB = ["outlook-web", "outlook-windows"];
|
|
14693
|
+
var OUTLOOK_WEB_AND_MOBILE = [
|
|
14694
|
+
"outlook-web",
|
|
14695
|
+
"outlook-windows",
|
|
14696
|
+
"outlook-ios",
|
|
14697
|
+
"outlook-android"
|
|
14698
|
+
];
|
|
14699
|
+
var GMAIL = ["gmail-web", "gmail-android", "gmail-ios"];
|
|
14700
|
+
var YAHOO_AOL = [
|
|
14701
|
+
"yahoo-mail",
|
|
14702
|
+
"yahoo-mail-android",
|
|
14703
|
+
"yahoo-mail-ios",
|
|
14704
|
+
"aol"
|
|
14705
|
+
];
|
|
14706
|
+
var MAX_LOCS = 20;
|
|
14707
|
+
var MAX_COLOR_FUNCTION_LENGTH = 256;
|
|
14708
|
+
function hasSpaceSeparatedColor(css) {
|
|
14709
|
+
const fn = /\b(?:rgba?|hsla?)\(/gi;
|
|
14710
|
+
let m;
|
|
14711
|
+
while (m = fn.exec(css)) {
|
|
14712
|
+
const start = m.index + m[0].length;
|
|
14713
|
+
const limit = Math.min(css.length, start + MAX_COLOR_FUNCTION_LENGTH);
|
|
14714
|
+
let depth = 1;
|
|
14715
|
+
let comma = false;
|
|
14716
|
+
let i = start;
|
|
14717
|
+
for (; i < limit && depth > 0; i++) {
|
|
14718
|
+
const c = css[i];
|
|
14719
|
+
if (c === "(") depth++;
|
|
14720
|
+
else if (c === ")") depth--;
|
|
14721
|
+
else if (c === "," && depth === 1) comma = true;
|
|
14722
|
+
}
|
|
14723
|
+
if (depth > 0) continue;
|
|
14724
|
+
const args = css.slice(start, i - 1);
|
|
14725
|
+
if (!comma && /\S\s+\S/.test(args)) return true;
|
|
14726
|
+
}
|
|
14727
|
+
return false;
|
|
14728
|
+
}
|
|
14729
|
+
var ATTR_SELECTOR_SEMICOLON = /\[[^\]]*;[^\]]*\]/;
|
|
14730
|
+
var DOUBLE_BRACE = /\}\}/;
|
|
14731
|
+
function readSheet(ast) {
|
|
14732
|
+
const propsByClass = /* @__PURE__ */ new Map();
|
|
14733
|
+
const classesWithDescendants = /* @__PURE__ */ new Set();
|
|
14734
|
+
const chainedClassSelectors = [];
|
|
14735
|
+
csstree6.walk(ast, {
|
|
14736
|
+
visit: "Rule",
|
|
14737
|
+
enter(rule) {
|
|
14738
|
+
var _a;
|
|
14739
|
+
if (rule.type !== "Rule" || rule.prelude.type !== "SelectorList") return;
|
|
14740
|
+
const props = /* @__PURE__ */ new Set();
|
|
14741
|
+
csstree6.walk(rule.block, {
|
|
14742
|
+
visit: "Declaration",
|
|
14743
|
+
enter(d) {
|
|
14744
|
+
if (d.type === "Declaration") props.add(d.property.toLowerCase());
|
|
14745
|
+
}
|
|
14746
|
+
});
|
|
14747
|
+
for (const selector of rule.prelude.children) {
|
|
14748
|
+
if (selector.type !== "Selector") continue;
|
|
14749
|
+
const parts = selector.children.toArray();
|
|
14750
|
+
const classes = parts.filter((p) => p.type === "ClassSelector");
|
|
14751
|
+
if (!classes.length) continue;
|
|
14752
|
+
let run = 0;
|
|
14753
|
+
for (const part of parts) {
|
|
14754
|
+
if (part.type === "ClassSelector" || part.type === "IdSelector") {
|
|
14755
|
+
run++;
|
|
14756
|
+
if (run === 2) {
|
|
14757
|
+
chainedClassSelectors.push(csstree6.generate(selector));
|
|
14758
|
+
break;
|
|
14759
|
+
}
|
|
14760
|
+
} else if (part.type !== "PseudoClassSelector" && part.type !== "PseudoElementSelector") {
|
|
14761
|
+
run = 0;
|
|
14762
|
+
}
|
|
14763
|
+
}
|
|
14764
|
+
const hasCombinator = parts.some(
|
|
14765
|
+
(p) => p.type === "Combinator" || p.type === "WhiteSpace"
|
|
14766
|
+
);
|
|
14767
|
+
if (hasCombinator) {
|
|
14768
|
+
const head = parts[0];
|
|
14769
|
+
if ((head == null ? void 0 : head.type) === "ClassSelector") classesWithDescendants.add(head.name);
|
|
14770
|
+
continue;
|
|
14771
|
+
}
|
|
14772
|
+
if (parts.length === classes.length && classes.length === 1) {
|
|
14773
|
+
const name = classes[0].name;
|
|
14774
|
+
const seen = (_a = propsByClass.get(name)) != null ? _a : /* @__PURE__ */ new Set();
|
|
14775
|
+
for (const p of props) seen.add(p);
|
|
14776
|
+
propsByClass.set(name, seen);
|
|
14777
|
+
}
|
|
14778
|
+
}
|
|
14779
|
+
}
|
|
14780
|
+
});
|
|
14781
|
+
return { propsByClass, classesWithDescendants, chainedClassSelectors };
|
|
14782
|
+
}
|
|
14783
|
+
function checkStyleSurvivalFromDom($, source, framework) {
|
|
14784
|
+
const issues = [];
|
|
14785
|
+
const add2 = (rule, severity, clients, message, locs, detail) => {
|
|
14786
|
+
const frameworkNote = getStyleSurvivalNote(rule, framework);
|
|
14787
|
+
issues.push(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
|
|
14788
|
+
rule,
|
|
14789
|
+
severity,
|
|
14790
|
+
clients: [...clients],
|
|
14791
|
+
message
|
|
14792
|
+
}, frameworkNote ? { frameworkNote } : {}), detail ? { detail } : {}), locs.length ? { loc: locs[0], locs: locs.slice(0, MAX_LOCS) } : {}), locs.length > MAX_LOCS ? { locsTruncated: true } : {}));
|
|
14793
|
+
};
|
|
14794
|
+
const sheets = [];
|
|
14795
|
+
const hits = { spaceColor: 0, doubleBrace: 0, attrSemicolon: 0, chained: 0 };
|
|
14796
|
+
const spaceColorLocs = [];
|
|
14797
|
+
const doubleBraceLocs = [];
|
|
14798
|
+
const attrSemicolonLocs = [];
|
|
14799
|
+
const chainedLocs = [];
|
|
14800
|
+
let chainedExample = "";
|
|
14801
|
+
$("style").each((_, el) => {
|
|
14802
|
+
const cssText = $(el).text();
|
|
14803
|
+
if (!cssText.trim()) return;
|
|
14804
|
+
const anchor = cssBlockAnchor(el, cssText, source);
|
|
14805
|
+
const at = (index) => {
|
|
14806
|
+
const before = cssText.slice(0, index);
|
|
14807
|
+
const line = before.split("\n").length;
|
|
14808
|
+
const column = index - before.lastIndexOf("\n");
|
|
14809
|
+
return locInCssBlock(anchor, {
|
|
14810
|
+
start: { line, column, offset: index },
|
|
14811
|
+
end: { line, column: column + 1, offset: index + 1 }
|
|
14812
|
+
});
|
|
14813
|
+
};
|
|
14814
|
+
const push = (into, index) => {
|
|
14815
|
+
var _a;
|
|
14816
|
+
const loc = (_a = at(index)) != null ? _a : locOfElement(el);
|
|
14817
|
+
if (loc) into.push(loc);
|
|
14818
|
+
};
|
|
14819
|
+
if (hasSpaceSeparatedColor(cssText)) {
|
|
14820
|
+
hits.spaceColor++;
|
|
14821
|
+
push(spaceColorLocs, cssText.search(/\b(?:rgba?|hsla?)\(/i));
|
|
14822
|
+
}
|
|
14823
|
+
const brace = cssText.search(DOUBLE_BRACE);
|
|
14824
|
+
if (brace >= 0) {
|
|
14825
|
+
hits.doubleBrace++;
|
|
14826
|
+
push(doubleBraceLocs, brace);
|
|
14827
|
+
}
|
|
14828
|
+
const attr2 = cssText.search(ATTR_SELECTOR_SEMICOLON);
|
|
14829
|
+
if (attr2 >= 0) {
|
|
14830
|
+
hits.attrSemicolon++;
|
|
14831
|
+
push(attrSemicolonLocs, attr2);
|
|
14832
|
+
}
|
|
14833
|
+
try {
|
|
14834
|
+
const sheet = readSheet(csstree6.parse(cssText, { positions: true }));
|
|
14835
|
+
sheets.push(sheet);
|
|
14836
|
+
if (sheet.chainedClassSelectors.length) {
|
|
14837
|
+
hits.chained++;
|
|
14838
|
+
chainedExample || (chainedExample = sheet.chainedClassSelectors[0]);
|
|
14839
|
+
push(chainedLocs, 0);
|
|
14840
|
+
}
|
|
14841
|
+
} catch (e) {
|
|
14842
|
+
}
|
|
14843
|
+
});
|
|
14844
|
+
const inlineColorEls = $("[style]").toArray().filter((el) => {
|
|
14845
|
+
var _a;
|
|
14846
|
+
return hasSpaceSeparatedColor((_a = $(el).attr("style")) != null ? _a : "");
|
|
14847
|
+
});
|
|
14848
|
+
if (hits.spaceColor) {
|
|
14849
|
+
add2(
|
|
14850
|
+
"gmail-space-separated-color",
|
|
14851
|
+
"error",
|
|
14852
|
+
GMAIL,
|
|
14853
|
+
"Gmail removes an entire <style> block containing a space-separated colour like `rgb(0 0 0)`. Write it with commas, `rgb(0, 0, 0)`, and the block survives.",
|
|
14854
|
+
spaceColorLocs
|
|
14855
|
+
);
|
|
14856
|
+
}
|
|
14857
|
+
if (inlineColorEls.length) {
|
|
14858
|
+
add2(
|
|
14859
|
+
"gmail-space-separated-color-inline",
|
|
14860
|
+
"error",
|
|
14861
|
+
GMAIL,
|
|
14862
|
+
`Gmail drops every declaration in a style attribute that uses a space-separated colour like \`rgb(0 0 0)\`, not only the colour. ${inlineColorEls.length} element${inlineColorEls.length > 1 ? "s are" : " is"} affected; write the colour with commas.`,
|
|
14863
|
+
inlineColorEls.map((el) => locOfElement(el)).filter((l) => !!l)
|
|
14864
|
+
);
|
|
14865
|
+
}
|
|
14866
|
+
if (hits.doubleBrace) {
|
|
14867
|
+
add2(
|
|
14868
|
+
"outlook-double-brace",
|
|
14869
|
+
"error",
|
|
14870
|
+
OUTLOOK_WEB_AND_MOBILE,
|
|
14871
|
+
"Outlook.com and Outlook on iOS/Android discard every style rule after `}}`. Minified CSS closing a media query produces it. Put a space or newline between the two braces and the rest of the sheet survives.",
|
|
14872
|
+
doubleBraceLocs
|
|
14873
|
+
);
|
|
14874
|
+
}
|
|
14875
|
+
if (hits.attrSemicolon) {
|
|
14876
|
+
add2(
|
|
14877
|
+
"yahoo-attribute-selector-semicolon",
|
|
14878
|
+
"error",
|
|
14879
|
+
YAHOO_AOL,
|
|
14880
|
+
'Yahoo and AOL discard every style rule after an attribute selector whose value contains a semicolon, such as `div[style="margin: 16px;"]`. Split it into `[style^=\u2026][style$=\u2026]` and the rest of the sheet survives.',
|
|
14881
|
+
attrSemicolonLocs
|
|
14882
|
+
);
|
|
14883
|
+
}
|
|
14884
|
+
if (hits.chained) {
|
|
14885
|
+
add2(
|
|
14886
|
+
"outlook-web-chained-class",
|
|
14887
|
+
"warning",
|
|
14888
|
+
OUTLOOK_WEB,
|
|
14889
|
+
`Outlook.com rewrites class names to namespace them and only rewrites the first in a compound selector, so \`${chainedExample}\` stops matching. Give the element one class that carries both rules.`,
|
|
14890
|
+
chainedLocs
|
|
14891
|
+
);
|
|
14892
|
+
}
|
|
14893
|
+
const conflicting = [];
|
|
14894
|
+
const shadowed = [];
|
|
14895
|
+
let conflictCount = 0;
|
|
14896
|
+
let shadowCount = 0;
|
|
14897
|
+
let conflictExample = "";
|
|
14898
|
+
$("[class]").each((_, el) => {
|
|
14899
|
+
var _a, _b;
|
|
14900
|
+
const names = ((_a = $(el).attr("class")) != null ? _a : "").split(/\s+/).filter(Boolean);
|
|
14901
|
+
if (names.length < 2) return;
|
|
14902
|
+
const loc = locOfElement(el);
|
|
14903
|
+
const seen = /* @__PURE__ */ new Map();
|
|
14904
|
+
for (const sheet of sheets) {
|
|
14905
|
+
for (const name of names) {
|
|
14906
|
+
for (const prop of (_b = sheet.propsByClass.get(name)) != null ? _b : []) {
|
|
14907
|
+
const first = seen.get(prop);
|
|
14908
|
+
if (first && first !== name) {
|
|
14909
|
+
conflictExample || (conflictExample = `.${first} and .${name} both set ${prop}`);
|
|
14910
|
+
conflictCount++;
|
|
14911
|
+
if (loc) conflicting.push(loc);
|
|
14912
|
+
return;
|
|
14913
|
+
}
|
|
14914
|
+
if (!first) seen.set(prop, name);
|
|
14915
|
+
}
|
|
14916
|
+
}
|
|
14917
|
+
if (names.some((n) => sheet.classesWithDescendants.has(n))) {
|
|
14918
|
+
shadowCount++;
|
|
14919
|
+
if (loc) shadowed.push(loc);
|
|
14920
|
+
return;
|
|
14921
|
+
}
|
|
14922
|
+
}
|
|
14923
|
+
});
|
|
14924
|
+
if (conflictCount) {
|
|
14925
|
+
add2(
|
|
14926
|
+
"outlook-first-class-only",
|
|
14927
|
+
"warning",
|
|
14928
|
+
WORD_ENGINE,
|
|
14929
|
+
`Outlook on Windows applies only the first class on an element, so a second class setting the same property never takes effect (${conflictExample}). Merge them into one class.`,
|
|
14930
|
+
conflicting
|
|
14931
|
+
);
|
|
14932
|
+
}
|
|
14933
|
+
if (shadowCount) {
|
|
14934
|
+
add2(
|
|
14935
|
+
"outlook-first-class-descendants",
|
|
14936
|
+
"warning",
|
|
14937
|
+
WORD_ENGINE,
|
|
14938
|
+
"Outlook on Windows ignores a descendant rule like `.text td` once the element carries a second class. Move the rule onto the child, or give the element a single class.",
|
|
14939
|
+
shadowed
|
|
14940
|
+
);
|
|
14941
|
+
}
|
|
14942
|
+
return { issues };
|
|
14943
|
+
}
|
|
14944
|
+
function checkStyleSurvival(html, options) {
|
|
14945
|
+
return fromHtml(
|
|
14946
|
+
html,
|
|
14947
|
+
EMPTY_STYLE_SURVIVAL,
|
|
14948
|
+
($) => checkStyleSurvivalFromDom(
|
|
14949
|
+
$,
|
|
14950
|
+
(options == null ? void 0 : options.positions) ? html : void 0,
|
|
14951
|
+
options == null ? void 0 : options.framework
|
|
14952
|
+
),
|
|
14953
|
+
options
|
|
14954
|
+
);
|
|
14955
|
+
}
|
|
14956
|
+
|
|
14618
14957
|
// src/rules/feature-urls.ts
|
|
14619
14958
|
var FEATURE_URLS = {
|
|
14620
14959
|
"!important": "https://www.caniemail.com/features/css-important/",
|
|
@@ -15564,7 +15903,7 @@ function validateLinks(html, options) {
|
|
|
15564
15903
|
|
|
15565
15904
|
// src/accessibility-checker.ts
|
|
15566
15905
|
import * as cheerio5 from "cheerio";
|
|
15567
|
-
import * as
|
|
15906
|
+
import * as csstree7 from "css-tree";
|
|
15568
15907
|
var RULE_PENALTY_CAPS = {
|
|
15569
15908
|
"img-missing-alt": 3,
|
|
15570
15909
|
"link-generic-text": 3,
|
|
@@ -15808,7 +16147,7 @@ function specificityFromTokens(selector) {
|
|
|
15808
16147
|
}
|
|
15809
16148
|
function specificityOf(selector) {
|
|
15810
16149
|
try {
|
|
15811
|
-
const ast =
|
|
16150
|
+
const ast = csstree7.parse(selector, { context: "selector" });
|
|
15812
16151
|
return specificityOfSelector(ast);
|
|
15813
16152
|
} catch (e) {
|
|
15814
16153
|
return specificityFromTokens(selector);
|
|
@@ -15853,14 +16192,14 @@ function computeCascade($, ctx) {
|
|
|
15853
16192
|
});
|
|
15854
16193
|
});
|
|
15855
16194
|
const handleRule = (node) => {
|
|
15856
|
-
const selectorList =
|
|
16195
|
+
const selectorList = csstree7.generate(node.prelude).trim();
|
|
15857
16196
|
if (!selectorList) return;
|
|
15858
16197
|
const decls = [];
|
|
15859
16198
|
node.block.children.forEach((child) => {
|
|
15860
16199
|
if (child.type !== "Declaration") return;
|
|
15861
16200
|
const prop = child.property.toLowerCase();
|
|
15862
16201
|
if (!CASCADE_PROPS.has(prop)) return;
|
|
15863
|
-
decls.push({ prop, value:
|
|
16202
|
+
decls.push({ prop, value: csstree7.generate(child.value), important: !!child.important });
|
|
15864
16203
|
});
|
|
15865
16204
|
if (!decls.length) return;
|
|
15866
16205
|
for (const selector of splitTopLevel(selectorList).map((x) => x.trim()).filter(Boolean)) {
|
|
@@ -15890,7 +16229,7 @@ function computeCascade($, ctx) {
|
|
|
15890
16229
|
return;
|
|
15891
16230
|
}
|
|
15892
16231
|
if (node.type !== "Atrule" || node.name.toLowerCase() !== "media" || !node.block) return;
|
|
15893
|
-
const prelude = node.prelude ?
|
|
16232
|
+
const prelude = node.prelude ? csstree7.generate(node.prelude) : "";
|
|
15894
16233
|
if (!mediaApplies(prelude, ctx)) return;
|
|
15895
16234
|
visitBlock(node.block.children);
|
|
15896
16235
|
});
|
|
@@ -15898,7 +16237,7 @@ function computeCascade($, ctx) {
|
|
|
15898
16237
|
$("style").each((_, styleEl) => {
|
|
15899
16238
|
let ast;
|
|
15900
16239
|
try {
|
|
15901
|
-
ast =
|
|
16240
|
+
ast = csstree7.parse($(styleEl).text());
|
|
15902
16241
|
} catch (e) {
|
|
15903
16242
|
return;
|
|
15904
16243
|
}
|
|
@@ -16679,8 +17018,9 @@ function humanizeBytes(bytes) {
|
|
|
16679
17018
|
const mb = kb / 1024;
|
|
16680
17019
|
return `${mb.toFixed(2)} MB`;
|
|
16681
17020
|
}
|
|
16682
|
-
function checkSizeFromDom(
|
|
17021
|
+
function checkSizeFromDom($, html) {
|
|
16683
17022
|
const htmlBytes = new TextEncoder().encode(html).length;
|
|
17023
|
+
const styleBytes = $("style").toArray().reduce((n, el) => n + new TextEncoder().encode($(el).text()).length, 0);
|
|
16684
17024
|
const humanSize = humanizeBytes(htmlBytes);
|
|
16685
17025
|
const issues = [];
|
|
16686
17026
|
let clipped = false;
|
|
@@ -16700,7 +17040,22 @@ function checkSizeFromDom(_$, html) {
|
|
|
16700
17040
|
detail: `${htmlBytes} bytes is within ${GMAIL_CLIP_THRESHOLD - htmlBytes} bytes of the clip threshold.`
|
|
16701
17041
|
});
|
|
16702
17042
|
}
|
|
16703
|
-
|
|
17043
|
+
if (styleBytes > GMAIL_STYLE_LIMIT) {
|
|
17044
|
+
issues.push({
|
|
17045
|
+
rule: "gmail-style-truncated",
|
|
17046
|
+
severity: "error",
|
|
17047
|
+
message: `${humanizeBytes(styleBytes)} of CSS is past Gmail's 16 KB ceiling for <style>. Gmail keeps the rules before the limit and drops the rest, so the email renders with part of its CSS and no error anywhere.`,
|
|
17048
|
+
detail: `${styleBytes} bytes of CSS across all <style> elements exceeds the ${GMAIL_STYLE_LIMIT} byte limit.`
|
|
17049
|
+
});
|
|
17050
|
+
} else if (styleBytes > GMAIL_STYLE_WARNING_THRESHOLD) {
|
|
17051
|
+
issues.push({
|
|
17052
|
+
rule: "gmail-style-warning",
|
|
17053
|
+
severity: "warning",
|
|
17054
|
+
message: `${humanizeBytes(styleBytes)} of CSS, approaching Gmail's 16 KB <style> ceiling. Past it, later rules are dropped silently.`,
|
|
17055
|
+
detail: `${styleBytes} bytes is within ${GMAIL_STYLE_LIMIT - styleBytes} bytes of the limit.`
|
|
17056
|
+
});
|
|
17057
|
+
}
|
|
17058
|
+
return { htmlBytes, styleBytes, humanSize, clipped, issues };
|
|
16704
17059
|
}
|
|
16705
17060
|
function checkSize(html) {
|
|
16706
17061
|
return fromHtml(html, EMPTY_SIZE, checkSizeFromDom);
|
|
@@ -16814,7 +17169,7 @@ function checkTemplateVariables(html, options) {
|
|
|
16814
17169
|
}
|
|
16815
17170
|
|
|
16816
17171
|
// src/overflow-checker.ts
|
|
16817
|
-
import * as
|
|
17172
|
+
import * as csstree8 from "css-tree";
|
|
16818
17173
|
var RESPONSIVE_MIN_WIDTH = 320;
|
|
16819
17174
|
function fixedPxWidth($el) {
|
|
16820
17175
|
const style = $el.attr("style") || "";
|
|
@@ -16891,11 +17246,11 @@ function checkOverflowFromDom($, source) {
|
|
|
16891
17246
|
const anchor = cssBlockAnchor(el, cssText, source);
|
|
16892
17247
|
let ast;
|
|
16893
17248
|
try {
|
|
16894
|
-
ast =
|
|
17249
|
+
ast = csstree8.parse(cssText, { positions: true });
|
|
16895
17250
|
} catch (e) {
|
|
16896
17251
|
return;
|
|
16897
17252
|
}
|
|
16898
|
-
|
|
17253
|
+
csstree8.walk(ast, {
|
|
16899
17254
|
visit: "Rule",
|
|
16900
17255
|
enter(node) {
|
|
16901
17256
|
if (node.type !== "Rule") return;
|
|
@@ -16905,7 +17260,7 @@ function checkOverflowFromDom($, source) {
|
|
|
16905
17260
|
node.block.children.forEach((child) => {
|
|
16906
17261
|
if (child.type !== "Declaration") return;
|
|
16907
17262
|
const prop = child.property.toLowerCase();
|
|
16908
|
-
const val =
|
|
17263
|
+
const val = csstree8.generate(child.value);
|
|
16909
17264
|
if (prop === "width") {
|
|
16910
17265
|
const m = val.match(/^(\d+)px$/);
|
|
16911
17266
|
if (m) {
|
|
@@ -16918,7 +17273,7 @@ function checkOverflowFromDom($, source) {
|
|
|
16918
17273
|
}
|
|
16919
17274
|
});
|
|
16920
17275
|
if (widthPx !== null && widthPx > EMAIL_MAX_WIDTH && !fluid) {
|
|
16921
|
-
const selector =
|
|
17276
|
+
const selector = csstree8.generate(node.prelude).trim().slice(0, 40);
|
|
16922
17277
|
addWidthIssue(widthPx, selector || "rule", issues, seen, widthLoc);
|
|
16923
17278
|
}
|
|
16924
17279
|
}
|
|
@@ -17243,7 +17598,7 @@ function checkVml(html, options) {
|
|
|
17243
17598
|
}
|
|
17244
17599
|
|
|
17245
17600
|
// src/visual-checker.ts
|
|
17246
|
-
import * as
|
|
17601
|
+
import * as csstree9 from "css-tree";
|
|
17247
17602
|
var CSS_WIDE_KEYWORDS = /* @__PURE__ */ new Set(["inherit", "initial", "unset", "revert", "revert-layer"]);
|
|
17248
17603
|
var GRADIENT_RE = /(?:linear|radial|conic)-gradient\(/i;
|
|
17249
17604
|
function isSolidColor(value) {
|
|
@@ -17349,7 +17704,7 @@ function ruleToMap(node) {
|
|
|
17349
17704
|
const map = /* @__PURE__ */ new Map();
|
|
17350
17705
|
node.block.children.forEach((child) => {
|
|
17351
17706
|
if (child.type === "Declaration") {
|
|
17352
|
-
map.set(child.property.toLowerCase(),
|
|
17707
|
+
map.set(child.property.toLowerCase(), csstree9.generate(child.value));
|
|
17353
17708
|
}
|
|
17354
17709
|
});
|
|
17355
17710
|
return map;
|
|
@@ -17368,11 +17723,11 @@ function checkVisualFromDom($, source) {
|
|
|
17368
17723
|
const anchor = cssBlockAnchor(el, cssText, source);
|
|
17369
17724
|
let ast;
|
|
17370
17725
|
try {
|
|
17371
|
-
ast =
|
|
17726
|
+
ast = csstree9.parse(cssText, { positions: true });
|
|
17372
17727
|
} catch (e) {
|
|
17373
17728
|
return;
|
|
17374
17729
|
}
|
|
17375
|
-
|
|
17730
|
+
csstree9.walk(ast, {
|
|
17376
17731
|
visit: "Rule",
|
|
17377
17732
|
enter(node) {
|
|
17378
17733
|
if (node.type !== "Rule") return;
|
|
@@ -17398,7 +17753,7 @@ function checkVisual(html, options) {
|
|
|
17398
17753
|
}
|
|
17399
17754
|
|
|
17400
17755
|
// src/design-consistency.ts
|
|
17401
|
-
import * as
|
|
17756
|
+
import * as csstree10 from "css-tree";
|
|
17402
17757
|
var SAME_COLOUR_DISTANCE = 0.02;
|
|
17403
17758
|
var LIMITS = {
|
|
17404
17759
|
fontSize: 8,
|
|
@@ -17452,15 +17807,15 @@ function collect($) {
|
|
|
17452
17807
|
$("style").each((_, el) => {
|
|
17453
17808
|
let ast;
|
|
17454
17809
|
try {
|
|
17455
|
-
ast =
|
|
17810
|
+
ast = csstree10.parse($(el).text());
|
|
17456
17811
|
} catch (e) {
|
|
17457
17812
|
return;
|
|
17458
17813
|
}
|
|
17459
|
-
|
|
17814
|
+
csstree10.walk(ast, {
|
|
17460
17815
|
visit: "Declaration",
|
|
17461
17816
|
enter(node) {
|
|
17462
17817
|
if (node.type !== "Declaration") return;
|
|
17463
|
-
record(node.property,
|
|
17818
|
+
record(node.property, csstree10.generate(node.value));
|
|
17464
17819
|
}
|
|
17465
17820
|
});
|
|
17466
17821
|
});
|
|
@@ -17569,6 +17924,7 @@ var EMPTY_AUDIT = {
|
|
|
17569
17924
|
images: EMPTY_IMAGES,
|
|
17570
17925
|
inboxPreview: EMPTY_INBOX_PREVIEW,
|
|
17571
17926
|
size: EMPTY_SIZE,
|
|
17927
|
+
styleSurvival: EMPTY_STYLE_SURVIVAL,
|
|
17572
17928
|
templateVariables: EMPTY_TEMPLATE,
|
|
17573
17929
|
overflow: EMPTY_OVERFLOW,
|
|
17574
17930
|
visual: EMPTY_VISUAL,
|
|
@@ -17597,6 +17953,7 @@ function runAudit($, html, framework, options) {
|
|
|
17597
17953
|
const images = skip.has("images") ? EMPTY_IMAGES : analyzeImagesFromDom($);
|
|
17598
17954
|
const inboxPreview = skip.has("inboxPreview") ? EMPTY_INBOX_PREVIEW : extractInboxPreviewFromDom($);
|
|
17599
17955
|
const size = skip.has("size") ? EMPTY_SIZE : checkSizeFromDom($, html);
|
|
17956
|
+
const styleSurvival = skip.has("styleSurvival") ? EMPTY_STYLE_SURVIVAL : checkStyleSurvivalFromDom($, source, framework);
|
|
17600
17957
|
const templateVariables = skip.has("templateVariables") ? EMPTY_TEMPLATE : checkTemplateVariablesFromDom($, source);
|
|
17601
17958
|
const overflow = skip.has("overflow") ? EMPTY_OVERFLOW : checkOverflowFromDom($, source);
|
|
17602
17959
|
const visual = skip.has("visual") ? EMPTY_VISUAL : checkVisualFromDom($, source);
|
|
@@ -17607,7 +17964,7 @@ function runAudit($, html, framework, options) {
|
|
|
17607
17964
|
]);
|
|
17608
17965
|
const design = skip.has("design") ? EMPTY_DESIGN : checkDesignConsistencyFromDom($);
|
|
17609
17966
|
const mobileContrast = skip.has("mobileContrast") || skip.has("accessibility") ? [] : checkMobileContrastFromDom($, accessibility.issues);
|
|
17610
|
-
return { compatibility: { warnings, scores }, spam, links, accessibility, images, inboxPreview, size, templateVariables, overflow, visual, vml, darkContrast, mobileContrast, design };
|
|
17967
|
+
return { compatibility: { warnings, scores }, spam, links, accessibility, images, inboxPreview, size, styleSurvival, templateVariables, overflow, visual, vml, darkContrast, mobileContrast, design };
|
|
17611
17968
|
}
|
|
17612
17969
|
function auditEmail(html, options) {
|
|
17613
17970
|
return fromHtml(html, EMPTY_AUDIT, ($, h) => runAudit($, h, options == null ? void 0 : options.framework, options), options);
|
|
@@ -17651,7 +18008,7 @@ function toPlainText(html) {
|
|
|
17651
18008
|
if (trimmed) lines.push(trimmed);
|
|
17652
18009
|
currentLine = "";
|
|
17653
18010
|
}
|
|
17654
|
-
function
|
|
18011
|
+
function walk10(node) {
|
|
17655
18012
|
var _a;
|
|
17656
18013
|
if (node.type === "text") {
|
|
17657
18014
|
const text = node.data.replace(/\s+/g, " ");
|
|
@@ -17700,7 +18057,7 @@ function toPlainText(html) {
|
|
|
17700
18057
|
currentLine = "- ";
|
|
17701
18058
|
}
|
|
17702
18059
|
for (const child of el.children) {
|
|
17703
|
-
|
|
18060
|
+
walk10(child);
|
|
17704
18061
|
}
|
|
17705
18062
|
if (isBlock) flushLine();
|
|
17706
18063
|
}
|
|
@@ -17708,7 +18065,7 @@ function toPlainText(html) {
|
|
|
17708
18065
|
const root = body.length ? body[0] : $.root()[0];
|
|
17709
18066
|
if (root && "children" in root) {
|
|
17710
18067
|
for (const child of root.children) {
|
|
17711
|
-
|
|
18068
|
+
walk10(child);
|
|
17712
18069
|
}
|
|
17713
18070
|
}
|
|
17714
18071
|
flushLine();
|
|
@@ -17839,6 +18196,7 @@ export {
|
|
|
17839
18196
|
checkMobileContrast,
|
|
17840
18197
|
checkOverflow,
|
|
17841
18198
|
checkSize,
|
|
18199
|
+
checkStyleSurvival,
|
|
17842
18200
|
checkTemplateVariables,
|
|
17843
18201
|
checkVisual,
|
|
17844
18202
|
checkVml,
|