@emailens/engine 0.10.0 → 0.10.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/README.md +258 -247
- package/dist/compile/index.d.cts +2 -2
- package/dist/compile/index.d.ts +2 -2
- package/dist/index.cjs +1348 -518
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +64 -14
- package/dist/index.d.ts +64 -14
- package/dist/index.js +1344 -518
- package/dist/index.js.map +1 -1
- package/dist/{react-email-B1Rd5itD.d.cts → react-email-DTzVpGgB.d.cts} +1 -1
- package/dist/{react-email-D4XyNztP.d.ts → react-email-DwBWB2kr.d.ts} +1 -1
- package/dist/server.d.cts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/{types-DEAn3IiX.d.cts → types-BLR3-Fzo.d.cts} +58 -1
- package/dist/{types-DEAn3IiX.d.ts → types-BLR3-Fzo.d.ts} +58 -1
- package/package.json +124 -123
package/dist/index.cjs
CHANGED
|
@@ -64,18 +64,22 @@ __export(index_exports, {
|
|
|
64
64
|
COMPOUND_VALUE_FEATURES: () => COMPOUND_VALUE_FEATURES,
|
|
65
65
|
CSS_FUNCTION_FEATURES: () => CSS_FUNCTION_FEATURES,
|
|
66
66
|
CSS_SUPPORT: () => CSS_SUPPORT,
|
|
67
|
+
CSS_SUPPORT_NOTES: () => CSS_SUPPORT_NOTES,
|
|
67
68
|
CompileError: () => CompileError,
|
|
68
69
|
EMAIL_CLIENTS: () => EMAIL_CLIENTS,
|
|
69
70
|
EMPTY_DELIVERABILITY: () => EMPTY_DELIVERABILITY,
|
|
70
71
|
GENERIC_LINK_TEXT: () => GENERIC_LINK_TEXT,
|
|
71
72
|
HTML_ELEMENT_FEATURES: () => HTML_ELEMENT_FEATURES,
|
|
72
73
|
MAX_HTML_SIZE: () => MAX_HTML_SIZE,
|
|
74
|
+
MAX_WARNING_LOCATIONS: () => MAX_WARNING_LOCATIONS,
|
|
73
75
|
STRUCTURAL_FIX_PROPERTIES: () => STRUCTURAL_FIX_PROPERTIES,
|
|
76
|
+
VALUE_CAVEAT_PROPS: () => VALUE_CAVEAT_PROPS,
|
|
74
77
|
alphaBlend: () => alphaBlend,
|
|
75
78
|
analyzeEmail: () => analyzeEmail,
|
|
76
79
|
analyzeImages: () => analyzeImages,
|
|
77
80
|
analyzeSpam: () => analyzeSpam,
|
|
78
81
|
auditEmail: () => auditEmail,
|
|
82
|
+
caveatApplies: () => caveatApplies,
|
|
79
83
|
checkAccessibility: () => checkAccessibility,
|
|
80
84
|
checkOverflow: () => checkOverflow,
|
|
81
85
|
checkSize: () => checkSize,
|
|
@@ -158,7 +162,11 @@ var EMAIL_CLIENTS = [
|
|
|
158
162
|
engine: "Microsoft Word",
|
|
159
163
|
darkModeSupport: true,
|
|
160
164
|
icon: "monitor",
|
|
161
|
-
|
|
165
|
+
// End of support ~Q2 2029 (Microsoft: "supported until at least 2029").
|
|
166
|
+
// The April 2026 date some sources cite is the opt-out phase, when classic
|
|
167
|
+
// stops being the Windows default, not end of support. October 2026 was
|
|
168
|
+
// wrong: the nearest real Oct date is Oct 2025, for legacy Outlook for Mac.
|
|
169
|
+
deprecated: "2029-06"
|
|
162
170
|
},
|
|
163
171
|
{
|
|
164
172
|
id: "outlook-ios",
|
|
@@ -8991,6 +8999,16 @@ function getStyleValue(style, property) {
|
|
|
8991
8999
|
}
|
|
8992
9000
|
return null;
|
|
8993
9001
|
}
|
|
9002
|
+
function getStyleValues(style, property) {
|
|
9003
|
+
const values = [];
|
|
9004
|
+
for (const part of splitStyleDeclarations(style)) {
|
|
9005
|
+
const colonIndex = part.indexOf(":");
|
|
9006
|
+
if (colonIndex === -1) continue;
|
|
9007
|
+
if (part.slice(0, colonIndex).trim().toLowerCase() !== property) continue;
|
|
9008
|
+
values.push(part.slice(colonIndex + 1).trim());
|
|
9009
|
+
}
|
|
9010
|
+
return values;
|
|
9011
|
+
}
|
|
8994
9012
|
function parseInlineStyle(style) {
|
|
8995
9013
|
const map = /* @__PURE__ */ new Map();
|
|
8996
9014
|
const declarations = splitStyleDeclarations(style);
|
|
@@ -9741,6 +9759,7 @@ function downlevelCSS(html) {
|
|
|
9741
9759
|
|
|
9742
9760
|
// src/constants.ts
|
|
9743
9761
|
var MAX_HTML_SIZE = 2 * 1024 * 1024;
|
|
9762
|
+
var MAX_WARNING_LOCATIONS = 100;
|
|
9744
9763
|
var GENERIC_LINK_TEXT = /* @__PURE__ */ new Set([
|
|
9745
9764
|
"click here",
|
|
9746
9765
|
"here",
|
|
@@ -10477,360 +10496,256 @@ function transformForAllClients(html, framework) {
|
|
|
10477
10496
|
}
|
|
10478
10497
|
|
|
10479
10498
|
// src/analyze.ts
|
|
10480
|
-
var
|
|
10481
|
-
|
|
10482
|
-
|
|
10483
|
-
|
|
10484
|
-
"
|
|
10485
|
-
"
|
|
10486
|
-
"
|
|
10487
|
-
"
|
|
10488
|
-
"
|
|
10489
|
-
"
|
|
10490
|
-
"
|
|
10491
|
-
"
|
|
10492
|
-
"
|
|
10493
|
-
"
|
|
10494
|
-
"
|
|
10495
|
-
|
|
10496
|
-
|
|
10497
|
-
"
|
|
10498
|
-
}
|
|
10499
|
-
var
|
|
10500
|
-
|
|
10501
|
-
|
|
10502
|
-
|
|
10503
|
-
|
|
10504
|
-
|
|
10505
|
-
|
|
10506
|
-
|
|
10507
|
-
|
|
10508
|
-
|
|
10509
|
-
|
|
10510
|
-
|
|
10511
|
-
|
|
10512
|
-
|
|
10513
|
-
|
|
10514
|
-
|
|
10515
|
-
|
|
10516
|
-
|
|
10517
|
-
|
|
10518
|
-
|
|
10519
|
-
"<svg>": (n) => `${n} does not support inline SVG.`,
|
|
10520
|
-
"<video>": (n) => `${n} does not support <video> elements.`,
|
|
10521
|
-
"<form>": (n) => `${n} strips form elements.`
|
|
10522
|
-
};
|
|
10523
|
-
var COMPOUND_DETECTORS = [
|
|
10524
|
-
{ key: "display:flex", property: "display", valueIncludes: "flex" },
|
|
10525
|
-
{ key: "display:grid", property: "display", valueIncludes: "grid" },
|
|
10526
|
-
{ key: "display:none", property: "display", valueIncludes: "none" }
|
|
10527
|
-
];
|
|
10528
|
-
var CSS_FUNCTION_DETECTORS = CSS_FUNCTION_FEATURES.map((fn) => ({
|
|
10529
|
-
key: fn,
|
|
10530
|
-
pattern: `${fn}(`
|
|
10531
|
-
// require opening paren — matches "min(" but not "Minion"
|
|
10532
|
-
}));
|
|
10533
|
-
function analyzeEmailFromDom($, framework) {
|
|
10534
|
-
const warnings = [];
|
|
10535
|
-
const seenWarnings = /* @__PURE__ */ new Set();
|
|
10536
|
-
function addWarning(w) {
|
|
10537
|
-
const key = `${w.client}:${w.property}:${w.severity}:${w.selector || ""}`;
|
|
10538
|
-
if (!seenWarnings.has(key)) {
|
|
10539
|
-
seenWarnings.add(key);
|
|
10540
|
-
warnings.push(w);
|
|
10499
|
+
var csstree5 = __toESM(require("css-tree"), 1);
|
|
10500
|
+
|
|
10501
|
+
// src/rules/value-caveats.ts
|
|
10502
|
+
var VALUE_CAVEAT_PROPS = /* @__PURE__ */ new Set([
|
|
10503
|
+
"background",
|
|
10504
|
+
"border-radius",
|
|
10505
|
+
"display",
|
|
10506
|
+
"font-size",
|
|
10507
|
+
"font-weight",
|
|
10508
|
+
"letter-spacing",
|
|
10509
|
+
"margin",
|
|
10510
|
+
"overflow",
|
|
10511
|
+
"position",
|
|
10512
|
+
"text-align",
|
|
10513
|
+
"transition"
|
|
10514
|
+
]);
|
|
10515
|
+
function normalize(value) {
|
|
10516
|
+
return value.replace(/\/\*[\s\S]*?\*\//g, " ").toLowerCase().replace(/!\s*important/g, " ").replace(/;+\s*$/, "").replace(/\s+/g, " ").trim();
|
|
10517
|
+
}
|
|
10518
|
+
var preparedValues = /* @__PURE__ */ new WeakMap();
|
|
10519
|
+
function prepare(values) {
|
|
10520
|
+
let out = preparedValues.get(values);
|
|
10521
|
+
if (!out) {
|
|
10522
|
+
out = [...new Set(values.map(normalize))];
|
|
10523
|
+
preparedValues.set(values, out);
|
|
10524
|
+
}
|
|
10525
|
+
return out;
|
|
10526
|
+
}
|
|
10527
|
+
function topLevelSplit(value, sep) {
|
|
10528
|
+
const parts = [];
|
|
10529
|
+
let depth = 0;
|
|
10530
|
+
let start = 0;
|
|
10531
|
+
for (let i = 0; i < value.length; i++) {
|
|
10532
|
+
const c = value[i];
|
|
10533
|
+
if (c === "(") depth++;
|
|
10534
|
+
else if (c === ")") depth = Math.max(0, depth - 1);
|
|
10535
|
+
else if (c === sep && depth === 0) {
|
|
10536
|
+
parts.push(value.slice(start, i));
|
|
10537
|
+
start = i + 1;
|
|
10541
10538
|
}
|
|
10542
10539
|
}
|
|
10543
|
-
|
|
10544
|
-
|
|
10545
|
-
|
|
10546
|
-
|
|
10547
|
-
|
|
10548
|
-
|
|
10549
|
-
|
|
10550
|
-
|
|
10551
|
-
|
|
10552
|
-
|
|
10553
|
-
|
|
10540
|
+
parts.push(value.slice(start));
|
|
10541
|
+
return parts.map((p) => p.trim()).filter(Boolean);
|
|
10542
|
+
}
|
|
10543
|
+
function tokens(value) {
|
|
10544
|
+
return topLevelSplit(value, " ");
|
|
10545
|
+
}
|
|
10546
|
+
function hasUnit(value, units) {
|
|
10547
|
+
return new RegExp(`\\d(?:${units.join("|")})\\b`).test(value);
|
|
10548
|
+
}
|
|
10549
|
+
function bareNumbers(value) {
|
|
10550
|
+
return tokens(value).filter((t) => /^[+-]?\d+(?:\.\d+)?(?:e[+-]?\d+)?$/.test(t)).map(Number);
|
|
10551
|
+
}
|
|
10552
|
+
function hasNegative(value) {
|
|
10553
|
+
return /(?:^|[\s,(])-\.?\d/.test(value);
|
|
10554
|
+
}
|
|
10555
|
+
function dashed(v) {
|
|
10556
|
+
return v.replace(/\s+/g, "-");
|
|
10557
|
+
}
|
|
10558
|
+
function unprefixed(token) {
|
|
10559
|
+
return token.replace(/^-(?:webkit|moz|ms|o)-/, "");
|
|
10560
|
+
}
|
|
10561
|
+
function quotedValues(note) {
|
|
10562
|
+
var _a;
|
|
10563
|
+
const supported = [];
|
|
10564
|
+
const banned = [];
|
|
10565
|
+
for (const m of note.matchAll(/`([^`]+)`/g)) {
|
|
10566
|
+
const clause = (_a = note.slice(0, m.index).split(/[.;]/).pop()) != null ? _a : "";
|
|
10567
|
+
(/\bsupports\b/i.test(clause) ? supported : banned).push(dashed(m[1].toLowerCase().trim()));
|
|
10554
10568
|
}
|
|
10555
|
-
|
|
10556
|
-
|
|
10557
|
-
|
|
10558
|
-
|
|
10559
|
-
|
|
10560
|
-
|
|
10561
|
-
|
|
10562
|
-
|
|
10563
|
-
|
|
10564
|
-
|
|
10565
|
-
|
|
10566
|
-
|
|
10567
|
-
|
|
10568
|
-
|
|
10569
|
-
|
|
10570
|
-
|
|
10571
|
-
|
|
10572
|
-
|
|
10573
|
-
|
|
10574
|
-
|
|
10575
|
-
|
|
10576
|
-
|
|
10577
|
-
|
|
10578
|
-
|
|
10579
|
-
|
|
10580
|
-
|
|
10581
|
-
|
|
10582
|
-
|
|
10583
|
-
|
|
10584
|
-
|
|
10585
|
-
|
|
10586
|
-
|
|
10587
|
-
|
|
10588
|
-
|
|
10589
|
-
|
|
10590
|
-
|
|
10569
|
+
return { supported, banned };
|
|
10570
|
+
}
|
|
10571
|
+
function isColorOnly(value) {
|
|
10572
|
+
if (value.startsWith("#")) return /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(value);
|
|
10573
|
+
if (parseColor(value)) return true;
|
|
10574
|
+
return ["none", "inherit", "initial", "unset", "revert", "currentcolor"].includes(value) || // A custom property is usually a palette colour, and we cannot resolve it.
|
|
10575
|
+
value.startsWith("var(");
|
|
10576
|
+
}
|
|
10577
|
+
var POSITION_KEYWORDS = ["relative", "absolute", "fixed", "sticky"];
|
|
10578
|
+
var RELATIVE_FONT_UNITS = [
|
|
10579
|
+
"cqmin",
|
|
10580
|
+
"cqmax",
|
|
10581
|
+
"vmin",
|
|
10582
|
+
"vmax",
|
|
10583
|
+
"rlh",
|
|
10584
|
+
"rex",
|
|
10585
|
+
"rch",
|
|
10586
|
+
"ric",
|
|
10587
|
+
"rem",
|
|
10588
|
+
"svw",
|
|
10589
|
+
"svh",
|
|
10590
|
+
"lvw",
|
|
10591
|
+
"lvh",
|
|
10592
|
+
"dvw",
|
|
10593
|
+
"dvh",
|
|
10594
|
+
"cqw",
|
|
10595
|
+
"cqh",
|
|
10596
|
+
"cqi",
|
|
10597
|
+
"cqb",
|
|
10598
|
+
"cap",
|
|
10599
|
+
"ic",
|
|
10600
|
+
"lh",
|
|
10601
|
+
"em",
|
|
10602
|
+
"ex",
|
|
10603
|
+
"ch",
|
|
10604
|
+
"vw",
|
|
10605
|
+
"vh",
|
|
10606
|
+
"vi",
|
|
10607
|
+
"vb"
|
|
10608
|
+
];
|
|
10609
|
+
var NOT_A_PROPERTY_NAME = /* @__PURE__ */ new Set([
|
|
10610
|
+
"all",
|
|
10611
|
+
"allow-discrete",
|
|
10612
|
+
"normal",
|
|
10613
|
+
"ease",
|
|
10614
|
+
"ease-in",
|
|
10615
|
+
"ease-out",
|
|
10616
|
+
"ease-in-out",
|
|
10617
|
+
"linear",
|
|
10618
|
+
"step-start",
|
|
10619
|
+
"step-end",
|
|
10620
|
+
"none",
|
|
10621
|
+
"initial",
|
|
10622
|
+
"inherit",
|
|
10623
|
+
"unset",
|
|
10624
|
+
"revert",
|
|
10625
|
+
"revert-layer"
|
|
10626
|
+
]);
|
|
10627
|
+
function namesAProperty(layer) {
|
|
10628
|
+
return tokens(layer).some((t) => /^[a-z][a-z0-9-]*$/.test(t) && !NOT_A_PROPERTY_NAME.has(t));
|
|
10629
|
+
}
|
|
10630
|
+
function triggers(prop, value, note, noteLc) {
|
|
10631
|
+
switch (prop) {
|
|
10632
|
+
case "margin": {
|
|
10633
|
+
const negative = noteLc.includes("negative");
|
|
10634
|
+
const auto = noteLc.includes("auto");
|
|
10635
|
+
if (!negative && !auto) return true;
|
|
10636
|
+
return negative && hasNegative(value) || auto && /\bauto\b/.test(value);
|
|
10591
10637
|
}
|
|
10592
|
-
|
|
10593
|
-
|
|
10594
|
-
|
|
10595
|
-
|
|
10596
|
-
|
|
10597
|
-
|
|
10598
|
-
const detectedPseudoClasses = /* @__PURE__ */ new Set();
|
|
10599
|
-
const detectedPseudoElements = /* @__PURE__ */ new Set();
|
|
10600
|
-
$("style").each((_, el) => {
|
|
10601
|
-
const cssText = $(el).text();
|
|
10602
|
-
try {
|
|
10603
|
-
const ast = csstree3.parse(cssText, { parseCustomProperty: true, positions: true });
|
|
10604
|
-
csstree3.walk(ast, {
|
|
10605
|
-
enter(node) {
|
|
10606
|
-
if (node.type === "Atrule") {
|
|
10607
|
-
parsedAtRules.add(`@${node.name}`);
|
|
10608
|
-
}
|
|
10609
|
-
if (node.type === "PseudoClassSelector") {
|
|
10610
|
-
detectedPseudoClasses.add(`:${node.name}`);
|
|
10611
|
-
}
|
|
10612
|
-
if (node.type === "PseudoElementSelector") {
|
|
10613
|
-
detectedPseudoElements.add(`::${node.name}`);
|
|
10614
|
-
}
|
|
10615
|
-
if (node.type === "Declaration") {
|
|
10616
|
-
const prop = node.property.toLowerCase();
|
|
10617
|
-
parsedProperties.add(prop);
|
|
10618
|
-
if (node.loc && !propertyLines.has(prop)) {
|
|
10619
|
-
propertyLines.set(prop, node.loc.start.line);
|
|
10620
|
-
}
|
|
10621
|
-
const valueStr = csstree3.generate(node.value);
|
|
10622
|
-
const seenValues = propertyValues.get(prop);
|
|
10623
|
-
if (seenValues) seenValues.push(valueStr);
|
|
10624
|
-
else propertyValues.set(prop, [valueStr]);
|
|
10625
|
-
for (const det of COMPOUND_DETECTORS) {
|
|
10626
|
-
if (prop === det.property && valueStr.includes(det.valueIncludes)) {
|
|
10627
|
-
parsedProperties.add(det.key);
|
|
10628
|
-
if (node.loc && !propertyLines.has(det.key)) {
|
|
10629
|
-
propertyLines.set(det.key, node.loc.start.line);
|
|
10630
|
-
}
|
|
10631
|
-
}
|
|
10632
|
-
}
|
|
10633
|
-
for (const fn of CSS_FUNCTION_DETECTORS) {
|
|
10634
|
-
if (valueStr.includes(fn.pattern)) {
|
|
10635
|
-
detectedCssFunctions.add(fn.key);
|
|
10636
|
-
if (node.loc && !propertyLines.has(fn.key)) {
|
|
10637
|
-
propertyLines.set(fn.key, node.loc.start.line);
|
|
10638
|
-
}
|
|
10639
|
-
}
|
|
10640
|
-
}
|
|
10641
|
-
}
|
|
10642
|
-
}
|
|
10643
|
-
});
|
|
10644
|
-
} catch (e) {
|
|
10638
|
+
case "position": {
|
|
10639
|
+
const used = POSITION_KEYWORDS.find((k) => tokens(value).some((t) => unprefixed(t) === k));
|
|
10640
|
+
if (!used) return false;
|
|
10641
|
+
const m = note.match(/supports\s+.+?\s+but not\s+([^.]+)/i);
|
|
10642
|
+
if (m) return m[1].toLowerCase().includes(used);
|
|
10643
|
+
return true;
|
|
10645
10644
|
}
|
|
10646
|
-
|
|
10647
|
-
|
|
10648
|
-
|
|
10649
|
-
|
|
10650
|
-
|
|
10651
|
-
|
|
10652
|
-
|
|
10653
|
-
|
|
10654
|
-
$("[style]").each((_, el) => {
|
|
10655
|
-
var _a;
|
|
10656
|
-
const style = $(el).attr("style") || "";
|
|
10657
|
-
const props = parseStyleProperties(style);
|
|
10658
|
-
const selector = describeSelector(el);
|
|
10659
|
-
for (const prop of props) {
|
|
10660
|
-
for (const det of COMPOUND_DETECTORS) {
|
|
10661
|
-
if (prop === det.property) {
|
|
10662
|
-
const value2 = getStyleValue(style, prop);
|
|
10663
|
-
if (value2 == null ? void 0 : value2.includes(det.valueIncludes)) {
|
|
10664
|
-
checkPropertySupport(det.key, addWarning, framework, selector);
|
|
10665
|
-
}
|
|
10666
|
-
}
|
|
10645
|
+
case "overflow": {
|
|
10646
|
+
if (!/\b(?:auto|scroll|overlay)\b/.test(value)) return false;
|
|
10647
|
+
if (!noteLc.includes("cannot scroll") && !noteLc.includes("overflow-block")) return true;
|
|
10648
|
+
return noteLc.includes("cannot scroll");
|
|
10649
|
+
}
|
|
10650
|
+
case "font-size": {
|
|
10651
|
+
if (noteLc.includes("percentage") || noteLc.includes("relative")) {
|
|
10652
|
+
return hasUnit(value, RELATIVE_FONT_UNITS) || /\d\s*%/.test(value) || tokens(value).some((t) => t === "smaller" || t === "larger");
|
|
10667
10653
|
}
|
|
10668
|
-
if (
|
|
10669
|
-
|
|
10654
|
+
if (noteLc.includes("`rem`")) return hasUnit(value, ["rem"]);
|
|
10655
|
+
return true;
|
|
10656
|
+
}
|
|
10657
|
+
case "display": {
|
|
10658
|
+
const only = note.match(/only supports\s+([^.]*)/i);
|
|
10659
|
+
if (only) {
|
|
10660
|
+
const allowed = quotedValues(only[1]).banned.map((q) => q.replace(/^display:-?/, "")).filter((q) => /^[a-z-]+$/.test(q));
|
|
10661
|
+
if (!allowed.length) return true;
|
|
10662
|
+
return !allowed.includes(dashed(value));
|
|
10670
10663
|
}
|
|
10671
|
-
const
|
|
10672
|
-
if (
|
|
10673
|
-
|
|
10674
|
-
|
|
10675
|
-
checkPropertySupport(fn.key, addWarning, framework, selector);
|
|
10676
|
-
}
|
|
10677
|
-
}
|
|
10664
|
+
const { banned } = quotedValues(note);
|
|
10665
|
+
if (banned.length) {
|
|
10666
|
+
const v = dashed(value);
|
|
10667
|
+
return banned.includes(v) || banned.includes(unprefixed(v));
|
|
10678
10668
|
}
|
|
10669
|
+
if (noteLc.includes("two-value syntax")) return tokens(value).length > 1;
|
|
10670
|
+
return true;
|
|
10679
10671
|
}
|
|
10680
|
-
|
|
10681
|
-
|
|
10682
|
-
|
|
10683
|
-
|
|
10684
|
-
|
|
10685
|
-
|
|
10686
|
-
|
|
10687
|
-
|
|
10688
|
-
framework,
|
|
10689
|
-
void 0,
|
|
10690
|
-
propertyLines.get(prop),
|
|
10691
|
-
values ? values.join(" ") : void 0
|
|
10692
|
-
);
|
|
10693
|
-
}
|
|
10694
|
-
for (const compound of COMPOUND_VALUE_FEATURES) {
|
|
10695
|
-
if (compound.startsWith(":") || compound.startsWith("::")) continue;
|
|
10696
|
-
if (parsedProperties.has(compound)) {
|
|
10697
|
-
checkPropertySupport(compound, addWarning, framework, void 0, propertyLines.get(compound));
|
|
10672
|
+
case "font-weight": {
|
|
10673
|
+
const nums = bareNumbers(value);
|
|
10674
|
+
if (!nums.length) return false;
|
|
10675
|
+
if (noteLc.includes("font weight")) return nums.some((n) => n !== 400 && n !== 700);
|
|
10676
|
+
if (noteLc.includes("only the following numeric values")) {
|
|
10677
|
+
return nums.some((n) => n % 100 !== 0 || n < 100 || n > 900);
|
|
10678
|
+
}
|
|
10679
|
+
return true;
|
|
10698
10680
|
}
|
|
10699
|
-
|
|
10700
|
-
|
|
10701
|
-
|
|
10702
|
-
checkPropertySupport(pseudo, addWarning, framework);
|
|
10681
|
+
case "border-radius": {
|
|
10682
|
+
if (noteLc.includes("slash")) return topLevelSplit(value, "/").length > 1;
|
|
10683
|
+
return true;
|
|
10703
10684
|
}
|
|
10704
|
-
|
|
10705
|
-
|
|
10706
|
-
|
|
10707
|
-
|
|
10685
|
+
case "text-align": {
|
|
10686
|
+
const { supported, banned } = quotedValues(note);
|
|
10687
|
+
if (!banned.length) return true;
|
|
10688
|
+
return tokens(value).some(
|
|
10689
|
+
(t) => !supported.includes(t) && (banned.includes(t) || banned.includes(unprefixed(t)))
|
|
10690
|
+
);
|
|
10708
10691
|
}
|
|
10692
|
+
case "background": {
|
|
10693
|
+
if (noteLc.includes("only `background-color`")) return !isColorOnly(value);
|
|
10694
|
+
if (noteLc.includes("multiple values")) {
|
|
10695
|
+
return topLevelSplit(value, ",").length > 1 || topLevelSplit(value, "/").length > 1;
|
|
10696
|
+
}
|
|
10697
|
+
return true;
|
|
10698
|
+
}
|
|
10699
|
+
case "letter-spacing": {
|
|
10700
|
+
const negative = noteLc.includes("negative");
|
|
10701
|
+
const em = noteLc.includes("`em`");
|
|
10702
|
+
if (!negative && !em) return true;
|
|
10703
|
+
return negative && hasNegative(value) || em && hasUnit(value, ["em"]);
|
|
10704
|
+
}
|
|
10705
|
+
case "transition": {
|
|
10706
|
+
if (noteLc.includes("`all`")) {
|
|
10707
|
+
if (["none", "inherit", "initial", "unset", "revert"].includes(value)) return false;
|
|
10708
|
+
return topLevelSplit(value, ",").some((layer) => !namesAProperty(layer));
|
|
10709
|
+
}
|
|
10710
|
+
return true;
|
|
10711
|
+
}
|
|
10712
|
+
default:
|
|
10713
|
+
return true;
|
|
10709
10714
|
}
|
|
10710
|
-
for (const fn of detectedCssFunctions) {
|
|
10711
|
-
checkPropertySupport(fn, addWarning, framework, void 0, propertyLines.get(fn));
|
|
10712
|
-
}
|
|
10713
|
-
const severityOrder = { error: 0, warning: 1, info: 2 };
|
|
10714
|
-
warnings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
10715
|
-
return warnings;
|
|
10716
10715
|
}
|
|
10717
|
-
function
|
|
10716
|
+
function caveatApplies(prop, values, notes) {
|
|
10717
|
+
if (!VALUE_CAVEAT_PROPS.has(prop)) return true;
|
|
10718
|
+
if (!(values == null ? void 0 : values.length)) return true;
|
|
10719
|
+
const note = (notes != null ? notes : []).join(" ");
|
|
10720
|
+
const noteLc = note.toLowerCase();
|
|
10721
|
+
return prepare(values).some((value) => triggers(prop, value, note, noteLc));
|
|
10722
|
+
}
|
|
10723
|
+
|
|
10724
|
+
// src/dark-mode-checker.ts
|
|
10725
|
+
var csstree4 = __toESM(require("css-tree"), 1);
|
|
10726
|
+
|
|
10727
|
+
// src/dark-mode.ts
|
|
10728
|
+
var cheerio3 = __toESM(require("cheerio"), 1);
|
|
10729
|
+
var csstree3 = __toESM(require("css-tree"), 1);
|
|
10730
|
+
var LIGHT_THRESHOLD = 0.7;
|
|
10731
|
+
var DARK_THRESHOLD = 0.15;
|
|
10732
|
+
var PREFERS_COLOR_SCHEME_CLIENTS = [
|
|
10733
|
+
"apple-mail-macos",
|
|
10734
|
+
"apple-mail-ios",
|
|
10735
|
+
"samsung-mail",
|
|
10736
|
+
"thunderbird",
|
|
10737
|
+
"hey-mail",
|
|
10738
|
+
"superhuman"
|
|
10739
|
+
];
|
|
10740
|
+
function simulateDarkMode(html, clientId) {
|
|
10741
|
+
var _a, _b;
|
|
10718
10742
|
if (!html || !html.trim()) {
|
|
10719
|
-
return [];
|
|
10743
|
+
return { html: html || "", warnings: [] };
|
|
10720
10744
|
}
|
|
10721
10745
|
if (html.length > MAX_HTML_SIZE) {
|
|
10722
10746
|
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
10723
10747
|
}
|
|
10724
10748
|
const $ = cheerio3.load(html);
|
|
10725
|
-
return analyzeEmailFromDom($, framework);
|
|
10726
|
-
}
|
|
10727
|
-
function getFixType(prop) {
|
|
10728
|
-
return STRUCTURAL_FIX_PROPERTIES.has(prop) ? "structural" : "css";
|
|
10729
|
-
}
|
|
10730
|
-
var VALUE_CAVEAT_PROPS = /* @__PURE__ */ new Set(["margin", "position", "overflow"]);
|
|
10731
|
-
var POSITION_KEYWORDS = ["relative", "absolute", "fixed", "sticky"];
|
|
10732
|
-
function valueTriggersCaveat(prop, value, notes) {
|
|
10733
|
-
const note = (notes != null ? notes : []).join(" ");
|
|
10734
|
-
const noteLc = note.toLowerCase();
|
|
10735
|
-
if (prop === "margin") {
|
|
10736
|
-
if (/(?:^|[\s:(])-\.?\d/.test(value) && noteLc.includes("negative")) return true;
|
|
10737
|
-
if (/\bauto\b/.test(value) && noteLc.includes("auto")) return true;
|
|
10738
|
-
return false;
|
|
10739
|
-
}
|
|
10740
|
-
if (prop === "position") {
|
|
10741
|
-
const used = POSITION_KEYWORDS.find((k) => new RegExp(`\\b${k}\\b`).test(value));
|
|
10742
|
-
if (!used) return false;
|
|
10743
|
-
const m = note.match(/supports\s+.+?\s+but not\s+([^.]+)/i);
|
|
10744
|
-
if (m) return m[1].toLowerCase().includes(used);
|
|
10745
|
-
return used === "fixed" || used === "sticky";
|
|
10746
|
-
}
|
|
10747
|
-
if (prop === "overflow") {
|
|
10748
|
-
if (!/\b(?:auto|scroll)\b/.test(value)) return false;
|
|
10749
|
-
return noteLc.includes("cannot scroll");
|
|
10750
|
-
}
|
|
10751
|
-
return true;
|
|
10752
|
-
}
|
|
10753
|
-
function noteSuffix(notes) {
|
|
10754
|
-
if (!(notes == null ? void 0 : notes.length)) return "";
|
|
10755
|
-
const cleaned = notes.map((n) => n.replace(/^(?:Partial|Buggy|Not supported)\.\s*/i, "").trim()).filter(Boolean);
|
|
10756
|
-
return cleaned.length ? ` ${cleaned.join(" ")}` : "";
|
|
10757
|
-
}
|
|
10758
|
-
function checkPropertySupport(prop, addWarning, framework, selector, line, value) {
|
|
10759
|
-
var _a;
|
|
10760
|
-
const supportData = CSS_SUPPORT[prop];
|
|
10761
|
-
if (!supportData) return;
|
|
10762
|
-
const fixType = getFixType(prop);
|
|
10763
|
-
const valueGated = VALUE_CAVEAT_PROPS.has(prop);
|
|
10764
|
-
for (const client of EMAIL_CLIENTS) {
|
|
10765
|
-
const support = supportData[client.id] || "unknown";
|
|
10766
|
-
const notes = (_a = CSS_SUPPORT_NOTES[prop]) == null ? void 0 : _a[client.id];
|
|
10767
|
-
if (support === "unsupported") {
|
|
10768
|
-
const sug = getSuggestion(prop, client.id, framework);
|
|
10769
|
-
const fix = getCodeFix(prop, client.id, framework);
|
|
10770
|
-
addWarning(__spreadValues(__spreadValues(__spreadValues({
|
|
10771
|
-
severity: "warning",
|
|
10772
|
-
client: client.id,
|
|
10773
|
-
property: prop,
|
|
10774
|
-
message: `${client.name} does not support "${prop}".${noteSuffix(notes)}`,
|
|
10775
|
-
suggestion: sug.text,
|
|
10776
|
-
fix,
|
|
10777
|
-
fixType
|
|
10778
|
-
}, selector ? { selector } : {}), line !== void 0 ? { line } : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
10779
|
-
} else if (support === "partial") {
|
|
10780
|
-
if (valueGated && value !== void 0 && !valueTriggersCaveat(prop, value, notes)) continue;
|
|
10781
|
-
const sug = getSuggestion(prop, client.id, framework);
|
|
10782
|
-
const fix = getCodeFix(prop, client.id, framework);
|
|
10783
|
-
addWarning(__spreadValues(__spreadValues(__spreadValues({
|
|
10784
|
-
severity: "info",
|
|
10785
|
-
client: client.id,
|
|
10786
|
-
property: prop,
|
|
10787
|
-
message: `${client.name} has partial support for "${prop}".${noteSuffix(notes)}`,
|
|
10788
|
-
suggestion: sug.text,
|
|
10789
|
-
fix,
|
|
10790
|
-
fixType
|
|
10791
|
-
}, selector ? { selector } : {}), line !== void 0 ? { line } : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
10792
|
-
}
|
|
10793
|
-
}
|
|
10794
|
-
}
|
|
10795
|
-
function generateCompatibilityScore(warnings) {
|
|
10796
|
-
const result = {};
|
|
10797
|
-
for (const client of EMAIL_CLIENTS) {
|
|
10798
|
-
const clientWarnings = warnings.filter((w) => w.client === client.id);
|
|
10799
|
-
const errorProps = new Set(clientWarnings.filter((w) => w.severity === "error").map((w) => w.property));
|
|
10800
|
-
const warnProps = new Set(clientWarnings.filter((w) => w.severity === "warning").map((w) => w.property));
|
|
10801
|
-
const infoProps = new Set(clientWarnings.filter((w) => w.severity === "info").map((w) => w.property));
|
|
10802
|
-
const errors = errorProps.size;
|
|
10803
|
-
const warns = warnProps.size;
|
|
10804
|
-
const info = infoProps.size;
|
|
10805
|
-
const score = Math.max(0, Math.min(100, 100 - errors * 10 - warns * 3));
|
|
10806
|
-
result[client.id] = { score, errors, warnings: warns, info };
|
|
10807
|
-
}
|
|
10808
|
-
return result;
|
|
10809
|
-
}
|
|
10810
|
-
function warningsForClient(warnings, clientId) {
|
|
10811
|
-
return warnings.filter((w) => w.client === clientId);
|
|
10812
|
-
}
|
|
10813
|
-
function errorWarnings(warnings) {
|
|
10814
|
-
return warnings.filter((w) => w.severity === "error");
|
|
10815
|
-
}
|
|
10816
|
-
function structuralWarnings(warnings) {
|
|
10817
|
-
return warnings.filter((w) => w.fixType === "structural");
|
|
10818
|
-
}
|
|
10819
|
-
|
|
10820
|
-
// src/dark-mode.ts
|
|
10821
|
-
var cheerio4 = __toESM(require("cheerio"), 1);
|
|
10822
|
-
var csstree4 = __toESM(require("css-tree"), 1);
|
|
10823
|
-
var LIGHT_THRESHOLD = 0.7;
|
|
10824
|
-
var DARK_THRESHOLD = 0.15;
|
|
10825
|
-
function simulateDarkMode(html, clientId) {
|
|
10826
|
-
var _a, _b;
|
|
10827
|
-
if (!html || !html.trim()) {
|
|
10828
|
-
return { html: html || "", warnings: [] };
|
|
10829
|
-
}
|
|
10830
|
-
if (html.length > MAX_HTML_SIZE) {
|
|
10831
|
-
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
10832
|
-
}
|
|
10833
|
-
const $ = cheerio4.load(html);
|
|
10834
10749
|
const warnings = [];
|
|
10835
10750
|
$("img").each((_, el) => {
|
|
10836
10751
|
const src = $(el).attr("src") || "";
|
|
@@ -11040,46 +10955,802 @@ function applyColorInversion($, mode) {
|
|
|
11040
10955
|
$("style").each((_, el) => {
|
|
11041
10956
|
const cssText = $(el).text();
|
|
11042
10957
|
try {
|
|
11043
|
-
const ast =
|
|
10958
|
+
const ast = csstree3.parse(cssText, { parseCustomProperty: true });
|
|
11044
10959
|
let modified = false;
|
|
11045
|
-
|
|
10960
|
+
csstree3.walk(ast, {
|
|
11046
10961
|
enter(node) {
|
|
11047
10962
|
if (node.type !== "Declaration") return;
|
|
11048
10963
|
const prop = node.property.toLowerCase();
|
|
11049
10964
|
if (!COLOR_PROPS.has(prop) && prop !== "background") return;
|
|
11050
|
-
const valueStr =
|
|
10965
|
+
const valueStr = csstree3.generate(node.value);
|
|
11051
10966
|
if (prop === "background") {
|
|
11052
10967
|
const bgColor = extractBackgroundColor(valueStr);
|
|
11053
10968
|
if (bgColor) {
|
|
11054
10969
|
const inverted = invertColor(bgColor, mode);
|
|
11055
10970
|
if (inverted) {
|
|
11056
10971
|
const newValue = valueStr.replace(bgColor, inverted);
|
|
11057
|
-
node.value =
|
|
10972
|
+
node.value = csstree3.parse(newValue, { context: "value" });
|
|
11058
10973
|
modified = true;
|
|
11059
10974
|
}
|
|
11060
10975
|
}
|
|
11061
10976
|
} else {
|
|
11062
10977
|
const inverted = invertColor(valueStr, mode);
|
|
11063
10978
|
if (inverted) {
|
|
11064
|
-
node.value =
|
|
10979
|
+
node.value = csstree3.parse(inverted, { context: "value" });
|
|
11065
10980
|
modified = true;
|
|
11066
10981
|
}
|
|
11067
10982
|
}
|
|
11068
10983
|
}
|
|
11069
|
-
});
|
|
11070
|
-
if (modified) {
|
|
11071
|
-
$(el).text(
|
|
10984
|
+
});
|
|
10985
|
+
if (modified) {
|
|
10986
|
+
$(el).text(csstree3.generate(ast));
|
|
10987
|
+
}
|
|
10988
|
+
} catch (e) {
|
|
10989
|
+
}
|
|
10990
|
+
});
|
|
10991
|
+
$("[bgcolor]").each((_, el) => {
|
|
10992
|
+
const bgcolor = $(el).attr("bgcolor") || "";
|
|
10993
|
+
const inverted = invertColor(bgcolor, mode);
|
|
10994
|
+
if (inverted) {
|
|
10995
|
+
$(el).attr("bgcolor", inverted);
|
|
10996
|
+
}
|
|
10997
|
+
});
|
|
10998
|
+
}
|
|
10999
|
+
|
|
11000
|
+
// src/source-location.ts
|
|
11001
|
+
function toLoc(p) {
|
|
11002
|
+
return {
|
|
11003
|
+
line: p.startLine,
|
|
11004
|
+
column: p.startCol,
|
|
11005
|
+
endLine: p.endLine,
|
|
11006
|
+
endColumn: p.endCol,
|
|
11007
|
+
offset: p.startOffset,
|
|
11008
|
+
length: p.endOffset - p.startOffset
|
|
11009
|
+
};
|
|
11010
|
+
}
|
|
11011
|
+
function locOfElement(el) {
|
|
11012
|
+
var _a;
|
|
11013
|
+
const raw = el == null ? void 0 : el.sourceCodeLocation;
|
|
11014
|
+
if (!raw) return void 0;
|
|
11015
|
+
return toLoc((_a = raw.startTag) != null ? _a : raw);
|
|
11016
|
+
}
|
|
11017
|
+
function locOfAttr(el, attr) {
|
|
11018
|
+
var _a, _b, _c, _d;
|
|
11019
|
+
const raw = el == null ? void 0 : el.sourceCodeLocation;
|
|
11020
|
+
if (!raw) return void 0;
|
|
11021
|
+
const attrLoc = (_d = (_a = raw.attrs) == null ? void 0 : _a[attr]) != null ? _d : (_c = (_b = raw.startTag) == null ? void 0 : _b.attrs) == null ? void 0 : _c[attr];
|
|
11022
|
+
return attrLoc ? toLoc(attrLoc) : locOfElement(el);
|
|
11023
|
+
}
|
|
11024
|
+
function locOfFirst($, selector) {
|
|
11025
|
+
const el = $(selector).first()[0];
|
|
11026
|
+
return el ? locOfElement(el) : void 0;
|
|
11027
|
+
}
|
|
11028
|
+
function cssBlockAnchor(styleEl, cssText, source) {
|
|
11029
|
+
var _a;
|
|
11030
|
+
const children = styleEl == null ? void 0 : styleEl.children;
|
|
11031
|
+
if (!children || children.length !== 1) return void 0;
|
|
11032
|
+
const loc = (_a = children[0]) == null ? void 0 : _a.sourceCodeLocation;
|
|
11033
|
+
if (!loc) return void 0;
|
|
11034
|
+
const mapper = source ? crMapper(source.slice(loc.startOffset, loc.endOffset), cssText) : null;
|
|
11035
|
+
const extraBefore = mapper ? (index) => mapper(index) - index : crOffsetter(cssText, loc.endOffset - loc.startOffset);
|
|
11036
|
+
return __spreadValues({ loc, extraBefore }, mapper ? { source } : {});
|
|
11037
|
+
}
|
|
11038
|
+
function crMapper(raw, decoded) {
|
|
11039
|
+
if (raw.length === decoded.length) return (index) => index;
|
|
11040
|
+
const points = [];
|
|
11041
|
+
const extras = [];
|
|
11042
|
+
let r = 0;
|
|
11043
|
+
let d = 0;
|
|
11044
|
+
let extra = 0;
|
|
11045
|
+
while (d < decoded.length) {
|
|
11046
|
+
if (r >= raw.length) return null;
|
|
11047
|
+
if (raw[r] === decoded[d]) {
|
|
11048
|
+
r++;
|
|
11049
|
+
d++;
|
|
11050
|
+
continue;
|
|
11051
|
+
}
|
|
11052
|
+
if (raw[r] === "\r" && decoded[d] === "\n") {
|
|
11053
|
+
const consumed = raw[r + 1] === "\n" ? 2 : 1;
|
|
11054
|
+
extra += consumed - 1;
|
|
11055
|
+
points.push(d);
|
|
11056
|
+
extras.push(extra);
|
|
11057
|
+
r += consumed;
|
|
11058
|
+
d += 1;
|
|
11059
|
+
continue;
|
|
11060
|
+
}
|
|
11061
|
+
return null;
|
|
11062
|
+
}
|
|
11063
|
+
if (r !== raw.length) return null;
|
|
11064
|
+
return (index) => index + lookup(points, extras, index);
|
|
11065
|
+
}
|
|
11066
|
+
function lookup(points, extras, index) {
|
|
11067
|
+
let lo = 0;
|
|
11068
|
+
let hi = points.length - 1;
|
|
11069
|
+
let found = 0;
|
|
11070
|
+
while (lo <= hi) {
|
|
11071
|
+
const mid = lo + hi >> 1;
|
|
11072
|
+
if (points[mid] < index) {
|
|
11073
|
+
found = extras[mid];
|
|
11074
|
+
lo = mid + 1;
|
|
11075
|
+
} else {
|
|
11076
|
+
hi = mid - 1;
|
|
11077
|
+
}
|
|
11078
|
+
}
|
|
11079
|
+
return found;
|
|
11080
|
+
}
|
|
11081
|
+
function findRawOffset(raw, decoded, index, token) {
|
|
11082
|
+
if (!token) return -1;
|
|
11083
|
+
let occurrence = 0;
|
|
11084
|
+
for (let at = decoded.indexOf(token); at !== -1 && at < index; at = decoded.indexOf(token, at + 1)) {
|
|
11085
|
+
occurrence++;
|
|
11086
|
+
}
|
|
11087
|
+
let found = -1;
|
|
11088
|
+
let from = 0;
|
|
11089
|
+
for (let i = 0; i <= occurrence; i++) {
|
|
11090
|
+
found = raw.indexOf(token, from);
|
|
11091
|
+
if (found === -1) return -1;
|
|
11092
|
+
from = found + 1;
|
|
11093
|
+
}
|
|
11094
|
+
return found;
|
|
11095
|
+
}
|
|
11096
|
+
function positionOf(source, offset) {
|
|
11097
|
+
const prefix = source.slice(0, offset);
|
|
11098
|
+
return { line: prefix.split("\n").length, column: offset - prefix.lastIndexOf("\n") };
|
|
11099
|
+
}
|
|
11100
|
+
function crOffsetter(text, rawLength) {
|
|
11101
|
+
const removed = rawLength - text.length;
|
|
11102
|
+
if (removed === 0) return () => 0;
|
|
11103
|
+
const newlines = countNewlines(text, text.length);
|
|
11104
|
+
if (removed !== newlines || newlines === 0) return null;
|
|
11105
|
+
return (index) => countNewlines(text, index);
|
|
11106
|
+
}
|
|
11107
|
+
function countNewlines(text, upTo) {
|
|
11108
|
+
let n = 0;
|
|
11109
|
+
for (let i = 0; i < upTo && i < text.length; i++) if (text.charCodeAt(i) === 10) n++;
|
|
11110
|
+
return n;
|
|
11111
|
+
}
|
|
11112
|
+
function locInCssBlock(anchor, cssLoc) {
|
|
11113
|
+
if (!anchor || !cssLoc) return void 0;
|
|
11114
|
+
const { loc: block, extraBefore } = anchor;
|
|
11115
|
+
if (!extraBefore) {
|
|
11116
|
+
return {
|
|
11117
|
+
line: block.startLine,
|
|
11118
|
+
column: block.startCol,
|
|
11119
|
+
endLine: block.startLine,
|
|
11120
|
+
endColumn: block.startCol,
|
|
11121
|
+
offset: block.startOffset,
|
|
11122
|
+
length: 0
|
|
11123
|
+
};
|
|
11124
|
+
}
|
|
11125
|
+
const line = block.startLine + cssLoc.start.line - 1;
|
|
11126
|
+
const column = cssLoc.start.line === 1 ? block.startCol + cssLoc.start.column - 1 : cssLoc.start.column;
|
|
11127
|
+
const endLine = block.startLine + cssLoc.end.line - 1;
|
|
11128
|
+
const endColumn = cssLoc.end.line === 1 ? block.startCol + cssLoc.end.column - 1 : cssLoc.end.column;
|
|
11129
|
+
const start = block.startOffset + cssLoc.start.offset + extraBefore(cssLoc.start.offset);
|
|
11130
|
+
const end = block.startOffset + cssLoc.end.offset + extraBefore(cssLoc.end.offset);
|
|
11131
|
+
if (anchor.source) {
|
|
11132
|
+
const from = positionOf(anchor.source, start);
|
|
11133
|
+
const to = positionOf(anchor.source, end);
|
|
11134
|
+
return {
|
|
11135
|
+
line: from.line,
|
|
11136
|
+
column: from.column,
|
|
11137
|
+
endLine: to.line,
|
|
11138
|
+
endColumn: to.column,
|
|
11139
|
+
offset: start,
|
|
11140
|
+
length: end - start
|
|
11141
|
+
};
|
|
11142
|
+
}
|
|
11143
|
+
return { line, column, endLine, endColumn, offset: start, length: end - start };
|
|
11144
|
+
}
|
|
11145
|
+
function locInTextNode(node, index, length, source) {
|
|
11146
|
+
var _a;
|
|
11147
|
+
const anchor = node == null ? void 0 : node.sourceCodeLocation;
|
|
11148
|
+
if (!anchor) return void 0;
|
|
11149
|
+
const data = (_a = node.data) != null ? _a : "";
|
|
11150
|
+
const rawLength = anchor.endOffset - anchor.startOffset;
|
|
11151
|
+
if (source) {
|
|
11152
|
+
const raw = source.slice(anchor.startOffset, anchor.endOffset);
|
|
11153
|
+
const token = data.slice(index, index + length);
|
|
11154
|
+
const at = findRawOffset(raw, data, index, token);
|
|
11155
|
+
if (at !== -1) {
|
|
11156
|
+
const start2 = anchor.startOffset + at;
|
|
11157
|
+
const from = positionOf(source, start2);
|
|
11158
|
+
const to = positionOf(source, start2 + token.length);
|
|
11159
|
+
return {
|
|
11160
|
+
line: from.line,
|
|
11161
|
+
column: from.column,
|
|
11162
|
+
endLine: to.line,
|
|
11163
|
+
endColumn: to.column,
|
|
11164
|
+
offset: start2,
|
|
11165
|
+
length: token.length
|
|
11166
|
+
};
|
|
11167
|
+
}
|
|
11168
|
+
}
|
|
11169
|
+
const extraBefore = crOffsetter(data, rawLength);
|
|
11170
|
+
if (!extraBefore) {
|
|
11171
|
+
const clamped = Math.min(length, rawLength);
|
|
11172
|
+
return {
|
|
11173
|
+
line: anchor.startLine,
|
|
11174
|
+
column: anchor.startCol,
|
|
11175
|
+
endLine: anchor.startLine,
|
|
11176
|
+
endColumn: anchor.startCol + clamped,
|
|
11177
|
+
offset: anchor.startOffset,
|
|
11178
|
+
length: clamped
|
|
11179
|
+
};
|
|
11180
|
+
}
|
|
11181
|
+
const start = positionAt(data, index, anchor);
|
|
11182
|
+
const end = positionAt(data, index + length, anchor);
|
|
11183
|
+
const startOffset = anchor.startOffset + index + extraBefore(index);
|
|
11184
|
+
const endOffset = anchor.startOffset + index + length + extraBefore(index + length);
|
|
11185
|
+
return {
|
|
11186
|
+
line: start.line,
|
|
11187
|
+
column: start.column,
|
|
11188
|
+
endLine: end.line,
|
|
11189
|
+
endColumn: end.column,
|
|
11190
|
+
offset: startOffset,
|
|
11191
|
+
length: endOffset - startOffset
|
|
11192
|
+
};
|
|
11193
|
+
}
|
|
11194
|
+
function positionAt(data, index, anchor) {
|
|
11195
|
+
const prefix = data.slice(0, index);
|
|
11196
|
+
const newlines = prefix.split("\n").length - 1;
|
|
11197
|
+
if (newlines === 0) {
|
|
11198
|
+
return { line: anchor.startLine, column: anchor.startCol + index };
|
|
11199
|
+
}
|
|
11200
|
+
return { line: anchor.startLine + newlines, column: index - prefix.lastIndexOf("\n") };
|
|
11201
|
+
}
|
|
11202
|
+
|
|
11203
|
+
// src/dark-mode-checker.ts
|
|
11204
|
+
var DARK_MEDIA_RE = /\(\s*prefers-color-scheme\s*:\s*dark\s*\)/i;
|
|
11205
|
+
var MAX_UNCOVERED_ELEMENTS = 3;
|
|
11206
|
+
function backgroundShorthandColor(value) {
|
|
11207
|
+
if (!value) return null;
|
|
11208
|
+
const trimmed = value.trim();
|
|
11209
|
+
if (parseColor(trimmed)) return trimmed;
|
|
11210
|
+
for (const token of trimmed.split(/\s+/)) {
|
|
11211
|
+
if (token.includes("(")) continue;
|
|
11212
|
+
if (parseColor(token)) return token;
|
|
11213
|
+
}
|
|
11214
|
+
return null;
|
|
11215
|
+
}
|
|
11216
|
+
function isLight(value) {
|
|
11217
|
+
const c = parseColor(value);
|
|
11218
|
+
if (!c || c.a < 0.5) return false;
|
|
11219
|
+
return relativeLuminance(c.r, c.g, c.b) > LIGHT_THRESHOLD;
|
|
11220
|
+
}
|
|
11221
|
+
function collectDarkBlocks($) {
|
|
11222
|
+
const block = { any: [], important: [], rules: 0 };
|
|
11223
|
+
let found = false;
|
|
11224
|
+
$("style").each((_, el) => {
|
|
11225
|
+
const cssText = $(el).text();
|
|
11226
|
+
if (!DARK_MEDIA_RE.test(cssText)) return;
|
|
11227
|
+
let ast;
|
|
11228
|
+
try {
|
|
11229
|
+
ast = csstree4.parse(cssText);
|
|
11230
|
+
} catch (e) {
|
|
11231
|
+
found = true;
|
|
11232
|
+
return;
|
|
11233
|
+
}
|
|
11234
|
+
csstree4.walk(ast, {
|
|
11235
|
+
visit: "Atrule",
|
|
11236
|
+
enter(node) {
|
|
11237
|
+
if (node.type !== "Atrule" || node.name.toLowerCase() !== "media") return;
|
|
11238
|
+
if (!node.prelude || !DARK_MEDIA_RE.test(csstree4.generate(node.prelude))) return;
|
|
11239
|
+
found = true;
|
|
11240
|
+
if (!node.block) return;
|
|
11241
|
+
csstree4.walk(node.block, {
|
|
11242
|
+
visit: "Rule",
|
|
11243
|
+
enter(rule) {
|
|
11244
|
+
if (rule.type !== "Rule") return;
|
|
11245
|
+
block.rules++;
|
|
11246
|
+
let setsBackground = false;
|
|
11247
|
+
let important = false;
|
|
11248
|
+
rule.block.children.forEach((child) => {
|
|
11249
|
+
if (child.type !== "Declaration") return;
|
|
11250
|
+
const prop = child.property.toLowerCase();
|
|
11251
|
+
if (prop !== "background" && prop !== "background-color") return;
|
|
11252
|
+
setsBackground = true;
|
|
11253
|
+
if (child.important) important = true;
|
|
11254
|
+
});
|
|
11255
|
+
if (!setsBackground) return;
|
|
11256
|
+
const selector = csstree4.generate(rule.prelude).trim();
|
|
11257
|
+
if (!selector) return;
|
|
11258
|
+
block.any.push(selector);
|
|
11259
|
+
if (important) block.important.push(selector);
|
|
11260
|
+
}
|
|
11261
|
+
});
|
|
11262
|
+
}
|
|
11263
|
+
});
|
|
11264
|
+
});
|
|
11265
|
+
return found ? block : null;
|
|
11266
|
+
}
|
|
11267
|
+
function matchedElements($, selectors) {
|
|
11268
|
+
const matched = /* @__PURE__ */ new Set();
|
|
11269
|
+
for (const selector of selectors) {
|
|
11270
|
+
try {
|
|
11271
|
+
$(selector).each((_, el) => {
|
|
11272
|
+
matched.add(el);
|
|
11273
|
+
});
|
|
11274
|
+
} catch (e) {
|
|
11275
|
+
}
|
|
11276
|
+
}
|
|
11277
|
+
return matched;
|
|
11278
|
+
}
|
|
11279
|
+
function describeSelector($, el) {
|
|
11280
|
+
var _a;
|
|
11281
|
+
const $el = $(el);
|
|
11282
|
+
const tag = ((_a = el.tagName) == null ? void 0 : _a.toLowerCase()) || "element";
|
|
11283
|
+
const id = $el.attr("id");
|
|
11284
|
+
if (id) return `${tag}#${id}`;
|
|
11285
|
+
const cls = $el.attr("class");
|
|
11286
|
+
if (cls) return `${tag}.${cls.split(/\s+/)[0]}`;
|
|
11287
|
+
return tag;
|
|
11288
|
+
}
|
|
11289
|
+
function checkDarkModeFromDom($) {
|
|
11290
|
+
var _a, _b;
|
|
11291
|
+
const darkBlock = collectDarkBlocks($);
|
|
11292
|
+
if (!darkBlock) return [];
|
|
11293
|
+
const warnings = [];
|
|
11294
|
+
const hasOptIn = $("meta").toArray().some((el) => {
|
|
11295
|
+
const name = ($(el).attr("name") || "").trim().toLowerCase();
|
|
11296
|
+
return name === "color-scheme" || name === "supported-color-schemes";
|
|
11297
|
+
});
|
|
11298
|
+
if (!hasOptIn) {
|
|
11299
|
+
const headLoc = locOfFirst($, "head");
|
|
11300
|
+
for (const clientId of PREFERS_COLOR_SCHEME_CLIENTS) {
|
|
11301
|
+
warnings.push(__spreadValues({
|
|
11302
|
+
severity: "warning",
|
|
11303
|
+
client: clientId,
|
|
11304
|
+
property: "dark-mode-opt-in",
|
|
11305
|
+
message: `The email has @media (prefers-color-scheme: dark) styles but no dark-mode opt-in meta tag. ${(_b = (_a = getClient(clientId)) == null ? void 0 : _a.name) != null ? _b : clientId} may keep the email in light mode, so the dark styles never activate.`,
|
|
11306
|
+
suggestion: 'Add both opt-in tags to <head>: <meta name="color-scheme" content="light dark"> and <meta name="supported-color-schemes" content="light dark">.',
|
|
11307
|
+
fixType: "structural"
|
|
11308
|
+
}, headLoc ? { loc: headLoc, locs: [headLoc] } : {}));
|
|
11309
|
+
}
|
|
11310
|
+
}
|
|
11311
|
+
if (darkBlock.rules === 0) return warnings;
|
|
11312
|
+
const coveredByImportant = matchedElements($, darkBlock.important);
|
|
11313
|
+
const coveredByAny = matchedElements($, darkBlock.any);
|
|
11314
|
+
let uncovered = 0;
|
|
11315
|
+
$("[bgcolor], [style]").each((_, el) => {
|
|
11316
|
+
var _a2;
|
|
11317
|
+
if (uncovered >= MAX_UNCOVERED_ELEMENTS) return false;
|
|
11318
|
+
const $el = $(el);
|
|
11319
|
+
const style = parseInlineStyle($el.attr("style") || "");
|
|
11320
|
+
const inline = (_a2 = style.get("background-color")) != null ? _a2 : backgroundShorthandColor(style.get("background"));
|
|
11321
|
+
const color = inline != null ? inline : $el.attr("bgcolor");
|
|
11322
|
+
if (!color || !isLight(color)) return;
|
|
11323
|
+
if (inline ? coveredByImportant.has(el) : coveredByAny.has(el)) return;
|
|
11324
|
+
uncovered++;
|
|
11325
|
+
const selector = describeSelector($, el);
|
|
11326
|
+
const loc = locOfAttr(el, inline ? "style" : "bgcolor");
|
|
11327
|
+
for (const clientId of PREFERS_COLOR_SCHEME_CLIENTS) {
|
|
11328
|
+
warnings.push(__spreadValues({
|
|
11329
|
+
severity: "warning",
|
|
11330
|
+
client: clientId,
|
|
11331
|
+
property: "dark-mode-coverage",
|
|
11332
|
+
message: `<${selector}> keeps its hardcoded light background (${color}) in dark mode \u2014 the dark block never overrides it. The rest of the email inverts around it, producing a half-inverted render (e.g. light text left on a still-white background).`,
|
|
11333
|
+
suggestion: inline ? `Override it inside @media (prefers-color-scheme: dark) with a dark background-color and !important (an inline style beats a plain rule), or move the colour onto a class the dark block already targets.` : `Override it inside @media (prefers-color-scheme: dark) with a dark background-color, or give the element a class the dark block already targets.`,
|
|
11334
|
+
fixType: "css",
|
|
11335
|
+
selector
|
|
11336
|
+
}, loc ? { loc, locs: [loc] } : {}));
|
|
11337
|
+
}
|
|
11338
|
+
});
|
|
11339
|
+
return warnings;
|
|
11340
|
+
}
|
|
11341
|
+
|
|
11342
|
+
// src/parse-html.ts
|
|
11343
|
+
var cheerio4 = __toESM(require("cheerio"), 1);
|
|
11344
|
+
function loadHtml(html, options) {
|
|
11345
|
+
return (options == null ? void 0 : options.positions) ? cheerio4.load(html, { sourceCodeLocationInfo: true }) : cheerio4.load(html);
|
|
11346
|
+
}
|
|
11347
|
+
function fromHtml(html, empty, fn, options) {
|
|
11348
|
+
if (!html || !html.trim()) return empty;
|
|
11349
|
+
if (html.length > MAX_HTML_SIZE) {
|
|
11350
|
+
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
11351
|
+
}
|
|
11352
|
+
return fn(loadHtml(html, options), html);
|
|
11353
|
+
}
|
|
11354
|
+
|
|
11355
|
+
// src/analyze.ts
|
|
11356
|
+
var HTML_ELEMENT_SELECTORS = {
|
|
11357
|
+
"<style>": "style",
|
|
11358
|
+
"<link>": "link[rel='stylesheet']",
|
|
11359
|
+
"<svg>": "svg",
|
|
11360
|
+
"<video>": "video",
|
|
11361
|
+
"<form>": "form, input, button[type='submit']",
|
|
11362
|
+
"<audio>": "audio",
|
|
11363
|
+
"<picture>": "picture",
|
|
11364
|
+
"<dialog>": "dialog",
|
|
11365
|
+
"<meter>": "meter",
|
|
11366
|
+
"<progress>": "progress",
|
|
11367
|
+
"<select>": "select",
|
|
11368
|
+
"<textarea>": "textarea",
|
|
11369
|
+
"<marquee>": "marquee",
|
|
11370
|
+
"<object>": "object",
|
|
11371
|
+
"<base>": "base"
|
|
11372
|
+
};
|
|
11373
|
+
var HTML_ELEMENT_SEVERITY = {
|
|
11374
|
+
"<style>": "error",
|
|
11375
|
+
"<link>": "error",
|
|
11376
|
+
"<svg>": "error",
|
|
11377
|
+
"<form>": "error",
|
|
11378
|
+
"<video>": "warning",
|
|
11379
|
+
"<audio>": "warning",
|
|
11380
|
+
"<picture>": "warning",
|
|
11381
|
+
"<dialog>": "warning",
|
|
11382
|
+
"<marquee>": "warning",
|
|
11383
|
+
"<meter>": "warning",
|
|
11384
|
+
"<progress>": "warning",
|
|
11385
|
+
"<select>": "warning",
|
|
11386
|
+
"<textarea>": "warning",
|
|
11387
|
+
"<object>": "warning",
|
|
11388
|
+
"<base>": "warning"
|
|
11389
|
+
};
|
|
11390
|
+
var HTML_ELEMENT_MESSAGES = {
|
|
11391
|
+
"<style>": (n) => `${n} strips <style> blocks. Styles must be inlined.`,
|
|
11392
|
+
"<link>": (n) => `${n} does not support external stylesheets.`,
|
|
11393
|
+
"<svg>": (n) => `${n} does not support inline SVG.`,
|
|
11394
|
+
"<video>": (n) => `${n} does not support <video> elements.`,
|
|
11395
|
+
"<form>": (n) => `${n} strips form elements.`
|
|
11396
|
+
};
|
|
11397
|
+
var COMPOUND_DETECTORS = [
|
|
11398
|
+
{ key: "display:flex", property: "display", valueIncludes: "flex" },
|
|
11399
|
+
{ key: "display:grid", property: "display", valueIncludes: "grid" },
|
|
11400
|
+
{ key: "display:none", property: "display", valueIncludes: "none" }
|
|
11401
|
+
];
|
|
11402
|
+
var CSS_FUNCTION_DETECTORS = CSS_FUNCTION_FEATURES.map((fn) => ({
|
|
11403
|
+
key: fn,
|
|
11404
|
+
pattern: `${fn}(`
|
|
11405
|
+
// require opening paren — matches "min(" but not "Minion"
|
|
11406
|
+
}));
|
|
11407
|
+
function analyzeEmailFromDom($, framework, source) {
|
|
11408
|
+
const warnings = [];
|
|
11409
|
+
const seenWarnings = /* @__PURE__ */ new Map();
|
|
11410
|
+
function addWarning(w) {
|
|
11411
|
+
const key = `${w.client}:${w.property}:${w.severity}:${w.selector || ""}`;
|
|
11412
|
+
const existing = seenWarnings.get(key);
|
|
11413
|
+
if (!existing) {
|
|
11414
|
+
seenWarnings.set(key, w);
|
|
11415
|
+
warnings.push(w);
|
|
11416
|
+
return;
|
|
11417
|
+
}
|
|
11418
|
+
if (!existing.locs || !w.locs) return;
|
|
11419
|
+
for (const loc of w.locs) {
|
|
11420
|
+
if (existing.locs.some((l) => l.offset === loc.offset)) continue;
|
|
11421
|
+
if (existing.locs.length >= MAX_WARNING_LOCATIONS) {
|
|
11422
|
+
existing.locsTruncated = true;
|
|
11423
|
+
break;
|
|
11424
|
+
}
|
|
11425
|
+
existing.locs.push(loc);
|
|
11426
|
+
}
|
|
11427
|
+
}
|
|
11428
|
+
function describeSelector2(el) {
|
|
11429
|
+
var _a;
|
|
11430
|
+
const $el = $(el);
|
|
11431
|
+
const tag = ((_a = el.tagName) == null ? void 0 : _a.toLowerCase()) || "";
|
|
11432
|
+
const cls = $el.attr("class");
|
|
11433
|
+
const id = $el.attr("id");
|
|
11434
|
+
if (id) return `${tag}#${id}`;
|
|
11435
|
+
if (cls) return `${tag}.${cls.split(/\s+/)[0]}`;
|
|
11436
|
+
const href = $el.attr("href");
|
|
11437
|
+
if (href) return `${tag}[href]`;
|
|
11438
|
+
return tag;
|
|
11439
|
+
}
|
|
11440
|
+
for (const feature of HTML_ELEMENT_FEATURES) {
|
|
11441
|
+
const selector = HTML_ELEMENT_SELECTORS[feature];
|
|
11442
|
+
if (!selector) continue;
|
|
11443
|
+
const matches = $(selector);
|
|
11444
|
+
if (matches.length === 0) continue;
|
|
11445
|
+
const supportData = CSS_SUPPORT[feature];
|
|
11446
|
+
if (!supportData) continue;
|
|
11447
|
+
const baseSeverity = HTML_ELEMENT_SEVERITY[feature] || "warning";
|
|
11448
|
+
const found = matches.toArray().map((m) => locOfElement(m)).filter((l) => l !== void 0);
|
|
11449
|
+
const featureOccurrences = found.length ? __spreadValues({
|
|
11450
|
+
locs: found.slice(0, MAX_WARNING_LOCATIONS)
|
|
11451
|
+
}, found.length > MAX_WARNING_LOCATIONS ? { truncated: true } : {}) : void 0;
|
|
11452
|
+
const featureLoc = featureOccurrences == null ? void 0 : featureOccurrences.locs[0];
|
|
11453
|
+
for (const client of EMAIL_CLIENTS) {
|
|
11454
|
+
const support = supportData[client.id];
|
|
11455
|
+
if (support === "unsupported") {
|
|
11456
|
+
const msgFn = HTML_ELEMENT_MESSAGES[feature];
|
|
11457
|
+
const message = msgFn ? msgFn(client.name) : `${client.name} does not support ${feature}.`;
|
|
11458
|
+
const sug = getSuggestion(feature, client.id, framework);
|
|
11459
|
+
const fix = getCodeFix(feature, client.id, framework);
|
|
11460
|
+
addWarning(__spreadValues(__spreadValues({
|
|
11461
|
+
severity: baseSeverity,
|
|
11462
|
+
client: client.id,
|
|
11463
|
+
property: feature,
|
|
11464
|
+
message,
|
|
11465
|
+
suggestion: sug.text,
|
|
11466
|
+
fix,
|
|
11467
|
+
fixType: getFixType(feature)
|
|
11468
|
+
}, featureOccurrences ? occurrenceFields(featureOccurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(feature, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
11469
|
+
} else if (support === "partial" && feature === "<style>") {
|
|
11470
|
+
const sug = getSuggestion("<style>:partial", client.id, framework);
|
|
11471
|
+
const fix = getCodeFix("<style>", client.id, framework);
|
|
11472
|
+
addWarning(__spreadValues(__spreadValues({
|
|
11473
|
+
severity: "warning",
|
|
11474
|
+
client: client.id,
|
|
11475
|
+
property: "<style>",
|
|
11476
|
+
message: `${client.name} has partial <style> support (head only, with limitations). Inline styles recommended.`,
|
|
11477
|
+
suggestion: sug.text,
|
|
11478
|
+
fix,
|
|
11479
|
+
fixType: getFixType("<style>")
|
|
11480
|
+
}, featureOccurrences ? occurrenceFields(featureOccurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<style>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
11481
|
+
}
|
|
11482
|
+
}
|
|
11483
|
+
}
|
|
11484
|
+
const parsedAtRules = /* @__PURE__ */ new Set();
|
|
11485
|
+
const selectorLocs = /* @__PURE__ */ new Map();
|
|
11486
|
+
const parsedProperties = /* @__PURE__ */ new Set();
|
|
11487
|
+
const propertyLines = /* @__PURE__ */ new Map();
|
|
11488
|
+
const propertyLocs = /* @__PURE__ */ new Map();
|
|
11489
|
+
const propertyValues = /* @__PURE__ */ new Map();
|
|
11490
|
+
const detectedCssFunctions = /* @__PURE__ */ new Set();
|
|
11491
|
+
const detectedPseudoClasses = /* @__PURE__ */ new Set();
|
|
11492
|
+
const detectedPseudoElements = /* @__PURE__ */ new Set();
|
|
11493
|
+
let blockAnchor;
|
|
11494
|
+
function recordSelectorLoc(key, cssLoc) {
|
|
11495
|
+
if (!cssLoc) return;
|
|
11496
|
+
const loc = locInCssBlock(blockAnchor, cssLoc);
|
|
11497
|
+
if (!loc) return;
|
|
11498
|
+
const seen = selectorLocs.get(key);
|
|
11499
|
+
if (!seen) {
|
|
11500
|
+
selectorLocs.set(key, { locs: [loc] });
|
|
11501
|
+
return;
|
|
11502
|
+
}
|
|
11503
|
+
if (seen.locs.some((l) => l.offset === loc.offset)) return;
|
|
11504
|
+
if (seen.locs.length >= MAX_WARNING_LOCATIONS) {
|
|
11505
|
+
seen.truncated = true;
|
|
11506
|
+
return;
|
|
11507
|
+
}
|
|
11508
|
+
seen.locs.push(loc);
|
|
11509
|
+
}
|
|
11510
|
+
function recordLoc(key, cssLoc, value) {
|
|
11511
|
+
const loc = locInCssBlock(blockAnchor, cssLoc);
|
|
11512
|
+
if (!loc) return;
|
|
11513
|
+
const seen = propertyLocs.get(key);
|
|
11514
|
+
if (!seen) {
|
|
11515
|
+
propertyLocs.set(key, __spreadValues({ locs: [loc] }, value !== void 0 ? { values: [value] } : {}));
|
|
11516
|
+
return;
|
|
11517
|
+
}
|
|
11518
|
+
if (seen.locs.some((l) => l.offset === loc.offset)) return;
|
|
11519
|
+
if (seen.locs.length >= MAX_WARNING_LOCATIONS) {
|
|
11520
|
+
seen.truncated = true;
|
|
11521
|
+
return;
|
|
11522
|
+
}
|
|
11523
|
+
seen.locs.push(loc);
|
|
11524
|
+
if (seen.values && value !== void 0) seen.values.push(value);
|
|
11525
|
+
}
|
|
11526
|
+
$("style").each((_, el) => {
|
|
11527
|
+
const cssText = $(el).text();
|
|
11528
|
+
blockAnchor = cssBlockAnchor(el, cssText, source);
|
|
11529
|
+
try {
|
|
11530
|
+
const ast = csstree5.parse(cssText, { parseCustomProperty: true, positions: true });
|
|
11531
|
+
csstree5.walk(ast, {
|
|
11532
|
+
enter(node) {
|
|
11533
|
+
if (node.type === "Atrule") {
|
|
11534
|
+
parsedAtRules.add(`@${node.name}`);
|
|
11535
|
+
recordSelectorLoc(`@${node.name}`, node.loc);
|
|
11536
|
+
}
|
|
11537
|
+
if (node.type === "PseudoClassSelector") {
|
|
11538
|
+
detectedPseudoClasses.add(`:${node.name}`);
|
|
11539
|
+
recordSelectorLoc(`:${node.name}`, node.loc);
|
|
11540
|
+
}
|
|
11541
|
+
if (node.type === "PseudoElementSelector") {
|
|
11542
|
+
detectedPseudoElements.add(`::${node.name}`);
|
|
11543
|
+
recordSelectorLoc(`::${node.name}`, node.loc);
|
|
11544
|
+
}
|
|
11545
|
+
if (node.type === "Declaration") {
|
|
11546
|
+
const prop = node.property.toLowerCase();
|
|
11547
|
+
parsedProperties.add(prop);
|
|
11548
|
+
const valueStr = csstree5.generate(node.value);
|
|
11549
|
+
const seenValues = propertyValues.get(prop);
|
|
11550
|
+
if (seenValues) seenValues.push(valueStr);
|
|
11551
|
+
else propertyValues.set(prop, [valueStr]);
|
|
11552
|
+
if (node.loc) {
|
|
11553
|
+
if (!propertyLines.has(prop)) propertyLines.set(prop, node.loc.start.line);
|
|
11554
|
+
recordLoc(prop, node.loc, valueStr);
|
|
11555
|
+
}
|
|
11556
|
+
for (const det of COMPOUND_DETECTORS) {
|
|
11557
|
+
if (prop === det.property && valueStr.toLowerCase().includes(det.valueIncludes)) {
|
|
11558
|
+
parsedProperties.add(det.key);
|
|
11559
|
+
if (node.loc) {
|
|
11560
|
+
if (!propertyLines.has(det.key)) propertyLines.set(det.key, node.loc.start.line);
|
|
11561
|
+
recordLoc(det.key, node.loc);
|
|
11562
|
+
}
|
|
11563
|
+
}
|
|
11564
|
+
}
|
|
11565
|
+
for (const fn of CSS_FUNCTION_DETECTORS) {
|
|
11566
|
+
if (valueStr.includes(fn.pattern)) {
|
|
11567
|
+
detectedCssFunctions.add(fn.key);
|
|
11568
|
+
if (node.loc) {
|
|
11569
|
+
if (!propertyLines.has(fn.key)) propertyLines.set(fn.key, node.loc.start.line);
|
|
11570
|
+
recordLoc(fn.key, node.loc);
|
|
11571
|
+
}
|
|
11572
|
+
}
|
|
11573
|
+
}
|
|
11574
|
+
}
|
|
11575
|
+
}
|
|
11576
|
+
});
|
|
11577
|
+
} catch (e) {
|
|
11578
|
+
}
|
|
11579
|
+
});
|
|
11580
|
+
for (const atRule of AT_RULE_FEATURES) {
|
|
11581
|
+
if (!parsedAtRules.has(atRule)) continue;
|
|
11582
|
+
checkPropertySupport(atRule, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(atRule));
|
|
11583
|
+
}
|
|
11584
|
+
const cssPropertiesToCheck = Object.keys(CSS_SUPPORT).filter(
|
|
11585
|
+
(k) => !k.startsWith("<") && !k.startsWith("@")
|
|
11586
|
+
);
|
|
11587
|
+
$("[style]").each((_, el) => {
|
|
11588
|
+
const style = $(el).attr("style") || "";
|
|
11589
|
+
const props = parseStyleProperties(style);
|
|
11590
|
+
const selector = describeSelector2(el);
|
|
11591
|
+
const locs = elementLocs(locOfAttr(el, "style"));
|
|
11592
|
+
for (const prop of props) {
|
|
11593
|
+
for (const det of COMPOUND_DETECTORS) {
|
|
11594
|
+
if (prop === det.property) {
|
|
11595
|
+
const value2 = getStyleValue(style, prop);
|
|
11596
|
+
if (value2 == null ? void 0 : value2.toLowerCase().includes(det.valueIncludes)) {
|
|
11597
|
+
checkPropertySupport(det.key, addWarning, framework, selector, void 0, void 0, locs);
|
|
11598
|
+
}
|
|
11599
|
+
}
|
|
11600
|
+
}
|
|
11601
|
+
if (cssPropertiesToCheck.includes(prop)) {
|
|
11602
|
+
const declared = getStyleValues(style, prop);
|
|
11603
|
+
checkPropertySupport(
|
|
11604
|
+
prop,
|
|
11605
|
+
addWarning,
|
|
11606
|
+
framework,
|
|
11607
|
+
selector,
|
|
11608
|
+
void 0,
|
|
11609
|
+
declared.length ? declared : void 0,
|
|
11610
|
+
locs
|
|
11611
|
+
);
|
|
11612
|
+
}
|
|
11613
|
+
const value = getStyleValue(style, prop);
|
|
11614
|
+
if (value) {
|
|
11615
|
+
for (const fn of CSS_FUNCTION_DETECTORS) {
|
|
11616
|
+
if (value.includes(fn.pattern)) {
|
|
11617
|
+
checkPropertySupport(fn.key, addWarning, framework, selector, void 0, void 0, locs);
|
|
11618
|
+
}
|
|
11619
|
+
}
|
|
11072
11620
|
}
|
|
11073
|
-
} catch (e) {
|
|
11074
11621
|
}
|
|
11075
11622
|
});
|
|
11076
|
-
|
|
11077
|
-
|
|
11078
|
-
|
|
11079
|
-
|
|
11080
|
-
|
|
11623
|
+
for (const prop of parsedProperties) {
|
|
11624
|
+
if (prop.includes(":")) continue;
|
|
11625
|
+
if (!cssPropertiesToCheck.includes(prop)) continue;
|
|
11626
|
+
const values = propertyValues.get(prop);
|
|
11627
|
+
checkPropertySupport(
|
|
11628
|
+
prop,
|
|
11629
|
+
addWarning,
|
|
11630
|
+
framework,
|
|
11631
|
+
void 0,
|
|
11632
|
+
propertyLines.get(prop),
|
|
11633
|
+
values,
|
|
11634
|
+
propertyLocs.get(prop)
|
|
11635
|
+
);
|
|
11636
|
+
}
|
|
11637
|
+
for (const compound of COMPOUND_VALUE_FEATURES) {
|
|
11638
|
+
if (compound.startsWith(":") || compound.startsWith("::")) continue;
|
|
11639
|
+
if (parsedProperties.has(compound)) {
|
|
11640
|
+
checkPropertySupport(compound, addWarning, framework, void 0, propertyLines.get(compound), void 0, propertyLocs.get(compound));
|
|
11081
11641
|
}
|
|
11082
|
-
}
|
|
11642
|
+
}
|
|
11643
|
+
for (const pseudo of detectedPseudoClasses) {
|
|
11644
|
+
if (CSS_SUPPORT[pseudo]) {
|
|
11645
|
+
checkPropertySupport(pseudo, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(pseudo));
|
|
11646
|
+
}
|
|
11647
|
+
}
|
|
11648
|
+
for (const pseudo of detectedPseudoElements) {
|
|
11649
|
+
if (CSS_SUPPORT[pseudo]) {
|
|
11650
|
+
checkPropertySupport(pseudo, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(pseudo));
|
|
11651
|
+
}
|
|
11652
|
+
}
|
|
11653
|
+
for (const fn of detectedCssFunctions) {
|
|
11654
|
+
checkPropertySupport(fn, addWarning, framework, void 0, propertyLines.get(fn), void 0, propertyLocs.get(fn));
|
|
11655
|
+
}
|
|
11656
|
+
for (const w of checkDarkModeFromDom($)) addWarning(w);
|
|
11657
|
+
const severityOrder = { error: 0, warning: 1, info: 2 };
|
|
11658
|
+
warnings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
11659
|
+
return warnings;
|
|
11660
|
+
}
|
|
11661
|
+
function analyzeEmail(html, framework, options) {
|
|
11662
|
+
if (!html || !html.trim()) {
|
|
11663
|
+
return [];
|
|
11664
|
+
}
|
|
11665
|
+
if (html.length > MAX_HTML_SIZE) {
|
|
11666
|
+
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
11667
|
+
}
|
|
11668
|
+
const $ = loadHtml(html, options);
|
|
11669
|
+
return analyzeEmailFromDom($, framework, (options == null ? void 0 : options.positions) ? html : void 0);
|
|
11670
|
+
}
|
|
11671
|
+
function getFixType(prop) {
|
|
11672
|
+
return STRUCTURAL_FIX_PROPERTIES.has(prop) ? "structural" : "css";
|
|
11673
|
+
}
|
|
11674
|
+
function noteSuffix(notes) {
|
|
11675
|
+
if (!(notes == null ? void 0 : notes.length)) return "";
|
|
11676
|
+
const cleaned = notes.map((n) => n.replace(/^(?:Partial|Buggy|Not supported)\.\s*/i, "").trim()).filter(Boolean);
|
|
11677
|
+
return cleaned.length ? ` ${cleaned.join(" ")}` : "";
|
|
11678
|
+
}
|
|
11679
|
+
function checkPropertySupport(prop, addWarning, framework, selector, line, values, occurrences) {
|
|
11680
|
+
var _a, _b, _c, _d, _e, _f;
|
|
11681
|
+
const loc = occurrences == null ? void 0 : occurrences.locs[0];
|
|
11682
|
+
const reportedLine = (_a = loc == null ? void 0 : loc.line) != null ? _a : line;
|
|
11683
|
+
const supportData = CSS_SUPPORT[prop];
|
|
11684
|
+
if (!supportData) return;
|
|
11685
|
+
const fixType = getFixType(prop);
|
|
11686
|
+
for (const client of EMAIL_CLIENTS) {
|
|
11687
|
+
const support = supportData[client.id] || "unknown";
|
|
11688
|
+
const notes = (_b = CSS_SUPPORT_NOTES[prop]) == null ? void 0 : _b[client.id];
|
|
11689
|
+
if (support === "unsupported") {
|
|
11690
|
+
const sug = getSuggestion(prop, client.id, framework);
|
|
11691
|
+
const fix = getCodeFix(prop, client.id, framework);
|
|
11692
|
+
addWarning(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
|
|
11693
|
+
severity: "warning",
|
|
11694
|
+
client: client.id,
|
|
11695
|
+
property: prop,
|
|
11696
|
+
message: `${client.name} does not support "${prop}".${noteSuffix(notes)}`,
|
|
11697
|
+
suggestion: sug.text,
|
|
11698
|
+
fix,
|
|
11699
|
+
fixType
|
|
11700
|
+
}, selector ? { selector } : {}), reportedLine !== void 0 ? { line: reportedLine } : {}), occurrences ? occurrenceFields(occurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
11701
|
+
} else if (support === "partial") {
|
|
11702
|
+
if (!caveatApplies(prop, values, notes)) continue;
|
|
11703
|
+
const hits = triggeringOccurrences(prop, occurrences, notes);
|
|
11704
|
+
const sug = getSuggestion(prop, client.id, framework);
|
|
11705
|
+
const fix = getCodeFix(prop, client.id, framework);
|
|
11706
|
+
addWarning(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
|
|
11707
|
+
severity: "info",
|
|
11708
|
+
client: client.id,
|
|
11709
|
+
property: prop,
|
|
11710
|
+
message: `${client.name} has partial support for "${prop}".${noteSuffix(notes)}`,
|
|
11711
|
+
suggestion: sug.text,
|
|
11712
|
+
fix,
|
|
11713
|
+
fixType
|
|
11714
|
+
}, selector ? { selector } : {}), ((_d = (_c = hits == null ? void 0 : hits.locs[0]) == null ? void 0 : _c.line) != null ? _d : reportedLine) !== void 0 ? { line: (_f = (_e = hits == null ? void 0 : hits.locs[0]) == null ? void 0 : _e.line) != null ? _f : reportedLine } : {}), hits ? occurrenceFields(hits) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
11715
|
+
}
|
|
11716
|
+
}
|
|
11717
|
+
}
|
|
11718
|
+
function generateCompatibilityScore(warnings) {
|
|
11719
|
+
const result = {};
|
|
11720
|
+
for (const client of EMAIL_CLIENTS) {
|
|
11721
|
+
const clientWarnings = warnings.filter((w) => w.client === client.id);
|
|
11722
|
+
const errorProps = new Set(clientWarnings.filter((w) => w.severity === "error").map((w) => w.property));
|
|
11723
|
+
const warnProps = new Set(clientWarnings.filter((w) => w.severity === "warning").map((w) => w.property));
|
|
11724
|
+
const infoProps = new Set(clientWarnings.filter((w) => w.severity === "info").map((w) => w.property));
|
|
11725
|
+
const errors = errorProps.size;
|
|
11726
|
+
const warns = warnProps.size;
|
|
11727
|
+
const info = infoProps.size;
|
|
11728
|
+
const score = Math.max(0, Math.min(100, 100 - errors * 10 - warns * 3));
|
|
11729
|
+
result[client.id] = { score, errors, warnings: warns, info };
|
|
11730
|
+
}
|
|
11731
|
+
return result;
|
|
11732
|
+
}
|
|
11733
|
+
function occurrenceFields({ locs, truncated }) {
|
|
11734
|
+
return __spreadValues({ loc: locs[0], locs: [...locs] }, truncated ? { locsTruncated: true } : {});
|
|
11735
|
+
}
|
|
11736
|
+
function triggeringOccurrences(prop, occurrences, notes) {
|
|
11737
|
+
const values = occurrences == null ? void 0 : occurrences.values;
|
|
11738
|
+
if (!occurrences || !values) return occurrences;
|
|
11739
|
+
const locs = occurrences.locs.filter((_, i) => caveatApplies(prop, [values[i]], notes));
|
|
11740
|
+
if (!locs.length || locs.length === occurrences.locs.length) return occurrences;
|
|
11741
|
+
return __spreadValues({ locs }, occurrences.truncated ? { truncated: true } : {});
|
|
11742
|
+
}
|
|
11743
|
+
function elementLocs(loc) {
|
|
11744
|
+
return loc ? { locs: [loc] } : void 0;
|
|
11745
|
+
}
|
|
11746
|
+
function warningsForClient(warnings, clientId) {
|
|
11747
|
+
return warnings.filter((w) => w.client === clientId);
|
|
11748
|
+
}
|
|
11749
|
+
function errorWarnings(warnings) {
|
|
11750
|
+
return warnings.filter((w) => w.severity === "error");
|
|
11751
|
+
}
|
|
11752
|
+
function structuralWarnings(warnings) {
|
|
11753
|
+
return warnings.filter((w) => w.fixType === "structural");
|
|
11083
11754
|
}
|
|
11084
11755
|
|
|
11085
11756
|
// src/diff.ts
|
|
@@ -11412,16 +12083,6 @@ function extractCode(response) {
|
|
|
11412
12083
|
return response.trim();
|
|
11413
12084
|
}
|
|
11414
12085
|
|
|
11415
|
-
// src/parse-html.ts
|
|
11416
|
-
var cheerio5 = __toESM(require("cheerio"), 1);
|
|
11417
|
-
function fromHtml(html, empty, fn) {
|
|
11418
|
-
if (!html || !html.trim()) return empty;
|
|
11419
|
-
if (html.length > MAX_HTML_SIZE) {
|
|
11420
|
-
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
11421
|
-
}
|
|
11422
|
-
return fn(cheerio5.load(html), html);
|
|
11423
|
-
}
|
|
11424
|
-
|
|
11425
12086
|
// src/spam-scorer.ts
|
|
11426
12087
|
var SPAM_TRIGGER_PHRASES = [
|
|
11427
12088
|
"act now",
|
|
@@ -11890,6 +12551,8 @@ function validateLinksFromDom($) {
|
|
|
11890
12551
|
const href = $(el).attr("href") || "";
|
|
11891
12552
|
const text = $(el).text().trim();
|
|
11892
12553
|
const category = classifyHref(href);
|
|
12554
|
+
const elLoc = locOfElement(el);
|
|
12555
|
+
const hrefLoc = href ? locOfAttr(el, "href") : elLoc;
|
|
11893
12556
|
switch (category) {
|
|
11894
12557
|
case "https":
|
|
11895
12558
|
breakdown.https++;
|
|
@@ -11920,95 +12583,95 @@ function validateLinksFromDom($) {
|
|
|
11920
12583
|
hrefCounts.set(href, (hrefCounts.get(href) || 0) + 1);
|
|
11921
12584
|
}
|
|
11922
12585
|
if (!href || !href.trim()) {
|
|
11923
|
-
issues.push({
|
|
12586
|
+
issues.push(__spreadValues({
|
|
11924
12587
|
severity: "error",
|
|
11925
12588
|
rule: "empty-href",
|
|
11926
12589
|
message: "Link has no href attribute",
|
|
11927
12590
|
text: text.slice(0, 80) || "(no text)"
|
|
11928
|
-
});
|
|
12591
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
11929
12592
|
return;
|
|
11930
12593
|
}
|
|
11931
12594
|
if (category === "javascript" && !isPlaceholderHref(href)) {
|
|
11932
|
-
issues.push({
|
|
12595
|
+
issues.push(__spreadValues({
|
|
11933
12596
|
severity: "error",
|
|
11934
12597
|
rule: "javascript-href",
|
|
11935
12598
|
message: "Link uses javascript: protocol",
|
|
11936
12599
|
href: href.slice(0, 100),
|
|
11937
12600
|
text: text.slice(0, 80) || "(no text)"
|
|
11938
|
-
});
|
|
12601
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
11939
12602
|
return;
|
|
11940
12603
|
}
|
|
11941
12604
|
if (isPlaceholderHref(href)) {
|
|
11942
|
-
issues.push({
|
|
12605
|
+
issues.push(__spreadValues({
|
|
11943
12606
|
severity: "warning",
|
|
11944
12607
|
rule: "placeholder-href",
|
|
11945
12608
|
message: "Link has a placeholder href (# or javascript:void)",
|
|
11946
12609
|
href,
|
|
11947
12610
|
text: text.slice(0, 80) || "(no text)"
|
|
11948
|
-
});
|
|
12611
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
11949
12612
|
return;
|
|
11950
12613
|
}
|
|
11951
12614
|
if (category === "http") {
|
|
11952
|
-
issues.push({
|
|
12615
|
+
issues.push(__spreadValues({
|
|
11953
12616
|
severity: "warning",
|
|
11954
12617
|
rule: "insecure-link",
|
|
11955
12618
|
message: "Link uses HTTP instead of HTTPS",
|
|
11956
12619
|
href: href.slice(0, 120),
|
|
11957
12620
|
text: text.slice(0, 80) || "(no text)"
|
|
11958
|
-
});
|
|
12621
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
11959
12622
|
}
|
|
11960
12623
|
if (category === "protocol-relative") {
|
|
11961
|
-
issues.push({
|
|
12624
|
+
issues.push(__spreadValues({
|
|
11962
12625
|
severity: "warning",
|
|
11963
12626
|
rule: "protocol-relative",
|
|
11964
12627
|
message: "Protocol-relative URL may break in email clients \u2014 use https:// explicitly",
|
|
11965
12628
|
href: href.slice(0, 120),
|
|
11966
12629
|
text: text.slice(0, 80) || "(no text)"
|
|
11967
|
-
});
|
|
12630
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
11968
12631
|
}
|
|
11969
12632
|
if (text && GENERIC_LINK_TEXT.has(text.toLowerCase())) {
|
|
11970
|
-
issues.push({
|
|
12633
|
+
issues.push(__spreadValues({
|
|
11971
12634
|
severity: "warning",
|
|
11972
12635
|
rule: "generic-link-text",
|
|
11973
12636
|
message: `Link text "${text}" is vague \u2014 use descriptive text for accessibility and engagement`,
|
|
11974
12637
|
href: href.slice(0, 120),
|
|
11975
12638
|
text
|
|
11976
|
-
});
|
|
12639
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
11977
12640
|
}
|
|
11978
12641
|
if (!text && !$(el).attr("aria-label") && !$(el).find("img[alt]").length) {
|
|
11979
|
-
issues.push({
|
|
12642
|
+
issues.push(__spreadValues({
|
|
11980
12643
|
severity: "error",
|
|
11981
12644
|
rule: "empty-link-text",
|
|
11982
12645
|
message: "Link has no visible text or aria-label",
|
|
11983
12646
|
href: href.slice(0, 120)
|
|
11984
|
-
});
|
|
12647
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
11985
12648
|
}
|
|
11986
12649
|
if (category === "mailto" && href.trim().toLowerCase() === "mailto:") {
|
|
11987
|
-
issues.push({
|
|
12650
|
+
issues.push(__spreadValues({
|
|
11988
12651
|
severity: "error",
|
|
11989
12652
|
rule: "empty-mailto",
|
|
11990
12653
|
message: "mailto: link has no email address",
|
|
11991
12654
|
href,
|
|
11992
12655
|
text: text.slice(0, 80) || "(no text)"
|
|
11993
|
-
});
|
|
12656
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
11994
12657
|
}
|
|
11995
12658
|
if (category === "tel" && href.trim().toLowerCase() === "tel:") {
|
|
11996
|
-
issues.push({
|
|
12659
|
+
issues.push(__spreadValues({
|
|
11997
12660
|
severity: "error",
|
|
11998
12661
|
rule: "empty-tel",
|
|
11999
12662
|
message: "tel: link has no phone number",
|
|
12000
12663
|
href,
|
|
12001
12664
|
text: text.slice(0, 80) || "(no text)"
|
|
12002
|
-
});
|
|
12665
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
12003
12666
|
}
|
|
12004
12667
|
if (href.length > 2e3) {
|
|
12005
|
-
issues.push({
|
|
12668
|
+
issues.push(__spreadValues({
|
|
12006
12669
|
severity: "info",
|
|
12007
12670
|
rule: "long-url",
|
|
12008
12671
|
message: "URL exceeds 2000 characters \u2014 may be truncated by some email clients",
|
|
12009
12672
|
href: href.slice(0, 120) + "...",
|
|
12010
12673
|
text: text.slice(0, 80) || "(no text)"
|
|
12011
|
-
});
|
|
12674
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
12012
12675
|
}
|
|
12013
12676
|
});
|
|
12014
12677
|
links.each((_, el) => {
|
|
@@ -12017,14 +12680,15 @@ function validateLinksFromDom($) {
|
|
|
12017
12680
|
if (trimmed.startsWith("#") && trimmed.length > 1) {
|
|
12018
12681
|
const targetId = trimmed.slice(1);
|
|
12019
12682
|
const target = $(`[id="${targetId}"]`);
|
|
12683
|
+
const anchorLoc = locOfAttr(el, "href");
|
|
12020
12684
|
if (target.length === 0) {
|
|
12021
|
-
issues.push({
|
|
12685
|
+
issues.push(__spreadValues({
|
|
12022
12686
|
severity: "error",
|
|
12023
12687
|
rule: "broken-anchor",
|
|
12024
12688
|
message: `Anchor link "${trimmed}" points to an element that does not exist`,
|
|
12025
12689
|
href: trimmed,
|
|
12026
12690
|
text: $(el).text().trim().slice(0, 80) || "(no text)"
|
|
12027
|
-
});
|
|
12691
|
+
}, anchorLoc ? { loc: anchorLoc } : {}));
|
|
12028
12692
|
}
|
|
12029
12693
|
}
|
|
12030
12694
|
});
|
|
@@ -12040,8 +12704,8 @@ function validateLinksFromDom($) {
|
|
|
12040
12704
|
}
|
|
12041
12705
|
return { totalLinks, issues, breakdown };
|
|
12042
12706
|
}
|
|
12043
|
-
function validateLinks(html) {
|
|
12044
|
-
return fromHtml(html, EMPTY_LINKS, validateLinksFromDom);
|
|
12707
|
+
function validateLinks(html, options) {
|
|
12708
|
+
return fromHtml(html, EMPTY_LINKS, validateLinksFromDom, options);
|
|
12045
12709
|
}
|
|
12046
12710
|
|
|
12047
12711
|
// src/accessibility-checker.ts
|
|
@@ -12066,24 +12730,31 @@ function describeElement($, el) {
|
|
|
12066
12730
|
function checkLangAttribute($) {
|
|
12067
12731
|
const lang = $("html").attr("lang");
|
|
12068
12732
|
if (!lang || !lang.trim()) {
|
|
12069
|
-
|
|
12733
|
+
const loc = locOfFirst($, "html");
|
|
12734
|
+
return __spreadProps(__spreadValues({
|
|
12070
12735
|
severity: "error",
|
|
12071
12736
|
rule: "missing-lang",
|
|
12072
|
-
message: "Missing lang attribute on <html> element"
|
|
12737
|
+
message: "Missing lang attribute on <html> element"
|
|
12738
|
+
}, loc ? { loc } : {}), {
|
|
12073
12739
|
details: 'Screen readers use the lang attribute to determine pronunciation. Add lang="en" (or appropriate language code).'
|
|
12074
|
-
};
|
|
12740
|
+
});
|
|
12075
12741
|
}
|
|
12076
12742
|
return null;
|
|
12077
12743
|
}
|
|
12744
|
+
function titleLoc($) {
|
|
12745
|
+
return $("title").length ? locOfFirst($, "title") : locOfFirst($, "head");
|
|
12746
|
+
}
|
|
12078
12747
|
function checkTitle($) {
|
|
12079
12748
|
const title = $("title").text().trim();
|
|
12080
12749
|
if (!title) {
|
|
12081
|
-
|
|
12750
|
+
const loc = titleLoc($);
|
|
12751
|
+
return __spreadProps(__spreadValues({
|
|
12082
12752
|
severity: "warning",
|
|
12083
12753
|
rule: "missing-title",
|
|
12084
|
-
message: "Missing or empty <title> element"
|
|
12754
|
+
message: "Missing or empty <title> element"
|
|
12755
|
+
}, loc ? { loc } : {}), {
|
|
12085
12756
|
details: "The <title> helps screen readers identify the email content."
|
|
12086
|
-
};
|
|
12757
|
+
});
|
|
12087
12758
|
}
|
|
12088
12759
|
return null;
|
|
12089
12760
|
}
|
|
@@ -12093,34 +12764,38 @@ function checkImageAlt($) {
|
|
|
12093
12764
|
const alt = $(el).attr("alt");
|
|
12094
12765
|
const src = $(el).attr("src") || "";
|
|
12095
12766
|
const role = $(el).attr("role");
|
|
12767
|
+
const elLoc = locOfElement(el);
|
|
12096
12768
|
if (role === "presentation" || role === "none") return;
|
|
12097
12769
|
if (alt === void 0) {
|
|
12098
|
-
issues.push({
|
|
12770
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12099
12771
|
severity: "error",
|
|
12100
12772
|
rule: "img-missing-alt",
|
|
12101
12773
|
message: "Image missing alt attribute",
|
|
12102
|
-
element: describeElement($, el)
|
|
12774
|
+
element: describeElement($, el)
|
|
12775
|
+
}, elLoc ? { loc: elLoc } : {}), {
|
|
12103
12776
|
details: 'Every image must have an alt attribute. Use alt="" for decorative images.'
|
|
12104
|
-
});
|
|
12777
|
+
}));
|
|
12105
12778
|
} else if (alt.trim() === "") {
|
|
12106
12779
|
const isLikelyContent = !src.includes("spacer") && !src.includes("pixel") && !src.includes("tracking") && !src.includes("1x1") && !src.includes("transparent");
|
|
12107
12780
|
if (isLikelyContent && ($(el).attr("width") || "0") !== "1") {
|
|
12108
|
-
issues.push({
|
|
12781
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12109
12782
|
severity: "info",
|
|
12110
12783
|
rule: "img-empty-alt",
|
|
12111
12784
|
message: "Image has empty alt text \u2014 verify it is decorative",
|
|
12112
|
-
element: describeElement($, el)
|
|
12785
|
+
element: describeElement($, el)
|
|
12786
|
+
}, locOfAttr(el, "alt") ? { loc: locOfAttr(el, "alt") } : {}), {
|
|
12113
12787
|
details: "Empty alt is correct for decorative images, but content images need descriptive alt text."
|
|
12114
|
-
});
|
|
12788
|
+
}));
|
|
12115
12789
|
}
|
|
12116
12790
|
} else if (/\.(png|jpg|jpeg|gif|svg|webp|bmp)$/i.test(alt)) {
|
|
12117
|
-
issues.push({
|
|
12791
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12118
12792
|
severity: "error",
|
|
12119
12793
|
rule: "img-filename-alt",
|
|
12120
12794
|
message: "Image alt text is a filename, not a description",
|
|
12121
|
-
element: describeElement($, el)
|
|
12795
|
+
element: describeElement($, el)
|
|
12796
|
+
}, locOfAttr(el, "alt") ? { loc: locOfAttr(el, "alt") } : {}), {
|
|
12122
12797
|
details: `Alt "${alt}" should describe the image content, not the file name.`
|
|
12123
|
-
});
|
|
12798
|
+
}));
|
|
12124
12799
|
}
|
|
12125
12800
|
});
|
|
12126
12801
|
return issues;
|
|
@@ -12128,28 +12803,31 @@ function checkImageAlt($) {
|
|
|
12128
12803
|
function checkLinkAccessibility($) {
|
|
12129
12804
|
const issues = [];
|
|
12130
12805
|
$("a").each((_, el) => {
|
|
12806
|
+
const elLoc = locOfElement(el);
|
|
12131
12807
|
const text = $(el).text().trim().toLowerCase();
|
|
12132
12808
|
const ariaLabel = $(el).attr("aria-label");
|
|
12133
12809
|
const title = $(el).attr("title");
|
|
12134
12810
|
const imgAlt = $(el).find("img").attr("alt");
|
|
12135
12811
|
if (!text && !ariaLabel && !title && !imgAlt) {
|
|
12136
|
-
issues.push({
|
|
12812
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12137
12813
|
severity: "error",
|
|
12138
12814
|
rule: "link-no-accessible-name",
|
|
12139
12815
|
message: "Link has no accessible name",
|
|
12140
|
-
element: describeElement($, el)
|
|
12816
|
+
element: describeElement($, el)
|
|
12817
|
+
}, elLoc ? { loc: elLoc } : {}), {
|
|
12141
12818
|
details: "Links need visible text, aria-label, or an image with alt text."
|
|
12142
|
-
});
|
|
12819
|
+
}));
|
|
12143
12820
|
return;
|
|
12144
12821
|
}
|
|
12145
12822
|
if (text && GENERIC_LINK_TEXT.has(text) && !ariaLabel) {
|
|
12146
|
-
issues.push({
|
|
12823
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12147
12824
|
severity: "warning",
|
|
12148
12825
|
rule: "link-generic-text",
|
|
12149
12826
|
message: `Link text "${$(el).text().trim()}" is not descriptive`,
|
|
12150
|
-
element: describeElement($, el)
|
|
12827
|
+
element: describeElement($, el)
|
|
12828
|
+
}, elLoc ? { loc: elLoc } : {}), {
|
|
12151
12829
|
details: "Screen readers often list links out of context. Use text that describes the destination."
|
|
12152
|
-
});
|
|
12830
|
+
}));
|
|
12153
12831
|
}
|
|
12154
12832
|
});
|
|
12155
12833
|
return issues;
|
|
@@ -12159,18 +12837,20 @@ function checkTableAccessibility($) {
|
|
|
12159
12837
|
$("table").each((_, el) => {
|
|
12160
12838
|
if ($(el).parents('table[role="presentation"], table[role="none"]').length > 0) return;
|
|
12161
12839
|
const role = $(el).attr("role");
|
|
12840
|
+
const tableLoc = locOfElement(el);
|
|
12162
12841
|
const hasHeaders = $(el).find("th").length > 0;
|
|
12163
12842
|
const looksLikeLayout = !hasHeaders;
|
|
12164
12843
|
if (looksLikeLayout && role !== "presentation" && role !== "none") {
|
|
12165
12844
|
const nestedTables = $(el).find("table").length;
|
|
12166
12845
|
if (nestedTables > 0 || $(el).find("td").length > 2) {
|
|
12167
|
-
issues.push({
|
|
12846
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12168
12847
|
severity: "info",
|
|
12169
12848
|
rule: "table-missing-role",
|
|
12170
|
-
message: 'Layout table missing role="presentation"'
|
|
12849
|
+
message: 'Layout table missing role="presentation"'
|
|
12850
|
+
}, tableLoc ? { loc: tableLoc } : {}), {
|
|
12171
12851
|
element: `<table> with ${$(el).find("td").length} cells`,
|
|
12172
12852
|
details: `Add role="presentation" to tables used for layout so screen readers don't announce them as data tables.`
|
|
12173
|
-
});
|
|
12853
|
+
}));
|
|
12174
12854
|
}
|
|
12175
12855
|
}
|
|
12176
12856
|
});
|
|
@@ -12181,6 +12861,7 @@ function checkTextSizeAndContrast($) {
|
|
|
12181
12861
|
let smallTextCount = 0;
|
|
12182
12862
|
$("[style]").each((_, el) => {
|
|
12183
12863
|
const style = $(el).attr("style") || "";
|
|
12864
|
+
const styleLoc = locOfAttr(el, "style");
|
|
12184
12865
|
const fontSizeMatch = style.match(/font-size\s*:\s*(\d+(?:\.\d+)?)(px|pt)/i);
|
|
12185
12866
|
if (fontSizeMatch) {
|
|
12186
12867
|
const size = parseFloat(fontSizeMatch[1]);
|
|
@@ -12189,13 +12870,14 @@ function checkTextSizeAndContrast($) {
|
|
|
12189
12870
|
if (pxSize < 9 && pxSize > 0) {
|
|
12190
12871
|
smallTextCount++;
|
|
12191
12872
|
if (smallTextCount <= 3) {
|
|
12192
|
-
issues.push({
|
|
12873
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12193
12874
|
severity: "warning",
|
|
12194
12875
|
rule: "small-text",
|
|
12195
12876
|
message: `Very small text (${fontSizeMatch[0].trim()})`,
|
|
12196
|
-
element: describeElement($, el)
|
|
12877
|
+
element: describeElement($, el)
|
|
12878
|
+
}, styleLoc ? { loc: styleLoc } : {}), {
|
|
12197
12879
|
details: "Text smaller than 9px is difficult to read, especially on mobile devices."
|
|
12198
|
-
});
|
|
12880
|
+
}));
|
|
12199
12881
|
}
|
|
12200
12882
|
}
|
|
12201
12883
|
}
|
|
@@ -12240,21 +12922,23 @@ function checkTextSizeAndContrast($) {
|
|
|
12240
12922
|
}
|
|
12241
12923
|
const grade = wcagGrade(ratio);
|
|
12242
12924
|
if (grade === "Fail") {
|
|
12243
|
-
issues.push({
|
|
12925
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12244
12926
|
severity: "error",
|
|
12245
12927
|
rule: "low-contrast",
|
|
12246
12928
|
message: `Low contrast ratio ${ratio.toFixed(1)}:1 \u2014 fails WCAG minimum`,
|
|
12247
|
-
element: describeElement($, el)
|
|
12929
|
+
element: describeElement($, el)
|
|
12930
|
+
}, styleLoc ? { loc: styleLoc } : {}), {
|
|
12248
12931
|
details: `Foreground ${colorValue} on background needs at least ${isLargeText ? "3:1" : "4.5:1"} contrast ratio.`
|
|
12249
|
-
});
|
|
12932
|
+
}));
|
|
12250
12933
|
} else if (!isLargeText && grade === "AA Large") {
|
|
12251
|
-
issues.push({
|
|
12934
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12252
12935
|
severity: "warning",
|
|
12253
12936
|
rule: "low-contrast",
|
|
12254
12937
|
message: `Low contrast ratio ${ratio.toFixed(1)}:1 \u2014 fails WCAG AA for normal text`,
|
|
12255
|
-
element: describeElement($, el)
|
|
12938
|
+
element: describeElement($, el)
|
|
12939
|
+
}, styleLoc ? { loc: styleLoc } : {}), {
|
|
12256
12940
|
details: `Foreground ${colorValue} on background needs at least 4.5:1 for normal-sized text.`
|
|
12257
|
-
});
|
|
12941
|
+
}));
|
|
12258
12942
|
}
|
|
12259
12943
|
}
|
|
12260
12944
|
}
|
|
@@ -12277,29 +12961,32 @@ function checkCharsetDeclaration($) {
|
|
|
12277
12961
|
const content = httpEquiv.attr("content") || "";
|
|
12278
12962
|
if (/charset\s*=/i.test(content)) return null;
|
|
12279
12963
|
}
|
|
12280
|
-
|
|
12964
|
+
const loc = locOfFirst($, "head");
|
|
12965
|
+
return __spreadProps(__spreadValues({
|
|
12281
12966
|
severity: "warning",
|
|
12282
12967
|
rule: "missing-charset",
|
|
12283
|
-
message: "Missing charset declaration"
|
|
12968
|
+
message: "Missing charset declaration"
|
|
12969
|
+
}, loc ? { loc } : {}), {
|
|
12284
12970
|
details: 'Add <meta charset="utf-8"> in <head> to prevent encoding issues across email clients.'
|
|
12285
|
-
};
|
|
12971
|
+
});
|
|
12286
12972
|
}
|
|
12287
12973
|
function checkSemanticStructure($) {
|
|
12288
12974
|
const issues = [];
|
|
12289
12975
|
const headings = [];
|
|
12290
12976
|
$("h1, h2, h3, h4, h5, h6").each((_, el) => {
|
|
12291
12977
|
const level = parseInt(el.tagName.replace(/h/i, ""), 10);
|
|
12292
|
-
headings.push({ level, text: $(el).text().trim().slice(0, 60) });
|
|
12978
|
+
headings.push({ level, text: $(el).text().trim().slice(0, 60), loc: locOfElement(el) });
|
|
12293
12979
|
});
|
|
12294
12980
|
for (let i = 1; i < headings.length; i++) {
|
|
12295
12981
|
const gap = headings[i].level - headings[i - 1].level;
|
|
12296
12982
|
if (gap > 1) {
|
|
12297
|
-
issues.push({
|
|
12983
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12298
12984
|
severity: "info",
|
|
12299
12985
|
rule: "heading-skip",
|
|
12300
|
-
message: `Heading level skipped: h${headings[i - 1].level} to h${headings[i].level}
|
|
12986
|
+
message: `Heading level skipped: h${headings[i - 1].level} to h${headings[i].level}`
|
|
12987
|
+
}, headings[i].loc ? { loc: headings[i].loc } : {}), {
|
|
12301
12988
|
details: "Skipped heading levels can confuse screen readers. Use sequential heading levels."
|
|
12302
|
-
});
|
|
12989
|
+
}));
|
|
12303
12990
|
break;
|
|
12304
12991
|
}
|
|
12305
12992
|
}
|
|
@@ -12340,8 +13027,8 @@ function checkAccessibilityFromDom($) {
|
|
|
12340
13027
|
const score = Math.max(0, 100 - penalty);
|
|
12341
13028
|
return { score, issues };
|
|
12342
13029
|
}
|
|
12343
|
-
function checkAccessibility(html) {
|
|
12344
|
-
return fromHtml(html, EMPTY_ACCESSIBILITY, checkAccessibilityFromDom);
|
|
13030
|
+
function checkAccessibility(html, options) {
|
|
13031
|
+
return fromHtml(html, EMPTY_ACCESSIBILITY, checkAccessibilityFromDom, options);
|
|
12345
13032
|
}
|
|
12346
13033
|
|
|
12347
13034
|
// src/image-analyzer.ts
|
|
@@ -12388,6 +13075,8 @@ function analyzeImagesFromDom($) {
|
|
|
12388
13075
|
const height = (_c = img.attr("height")) != null ? _c : null;
|
|
12389
13076
|
const style = (img.attr("style") || "").toLowerCase();
|
|
12390
13077
|
const imgIssues = [];
|
|
13078
|
+
const elLoc = locOfElement(el);
|
|
13079
|
+
const srcLoc = src ? locOfAttr(el, "src") : elLoc;
|
|
12391
13080
|
const tracking = isTrackingPixel(img);
|
|
12392
13081
|
let dataUriBytes = 0;
|
|
12393
13082
|
if (src.startsWith("data:")) {
|
|
@@ -12411,59 +13100,59 @@ function analyzeImagesFromDom($) {
|
|
|
12411
13100
|
const hasStyleHeight = /height\s*:/.test(style);
|
|
12412
13101
|
if (!hasStyleWidth && !hasStyleHeight) {
|
|
12413
13102
|
imgIssues.push("missing-dimensions");
|
|
12414
|
-
issues.push({
|
|
13103
|
+
issues.push(__spreadValues({
|
|
12415
13104
|
rule: "missing-dimensions",
|
|
12416
13105
|
severity: "warning",
|
|
12417
13106
|
message: "Image missing width/height attributes \u2014 causes layout shifts and Outlook rendering issues.",
|
|
12418
13107
|
src: truncateSrc(src)
|
|
12419
|
-
});
|
|
13108
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12420
13109
|
}
|
|
12421
13110
|
}
|
|
12422
13111
|
if (dataUriBytes > DATA_URI_WARN_BYTES) {
|
|
12423
13112
|
const kb = Math.round(dataUriBytes / 1024);
|
|
12424
13113
|
imgIssues.push("large-data-uri");
|
|
12425
|
-
issues.push({
|
|
13114
|
+
issues.push(__spreadValues({
|
|
12426
13115
|
rule: "large-data-uri",
|
|
12427
13116
|
severity: "warning",
|
|
12428
13117
|
message: `Data URI is ${kb}KB \u2014 consider hosting the image externally to reduce email size.`,
|
|
12429
13118
|
src: truncateSrc(src)
|
|
12430
|
-
});
|
|
13119
|
+
}, srcLoc ? { loc: srcLoc } : {}));
|
|
12431
13120
|
}
|
|
12432
13121
|
if (alt === null) {
|
|
12433
13122
|
imgIssues.push("missing-alt");
|
|
12434
|
-
issues.push({
|
|
13123
|
+
issues.push(__spreadValues({
|
|
12435
13124
|
rule: "missing-alt",
|
|
12436
13125
|
severity: "warning",
|
|
12437
13126
|
message: "Image missing alt attribute \u2014 hurts deliverability and accessibility.",
|
|
12438
13127
|
src: truncateSrc(src)
|
|
12439
|
-
});
|
|
13128
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12440
13129
|
}
|
|
12441
13130
|
if (src.toLowerCase().endsWith(".webp") || src.includes("image/webp")) {
|
|
12442
13131
|
imgIssues.push("webp-format");
|
|
12443
|
-
issues.push({
|
|
13132
|
+
issues.push(__spreadValues({
|
|
12444
13133
|
rule: "webp-format",
|
|
12445
13134
|
severity: "info",
|
|
12446
13135
|
message: "WebP format detected \u2014 not supported by all email clients. Consider PNG or JPEG.",
|
|
12447
13136
|
src: truncateSrc(src)
|
|
12448
|
-
});
|
|
13137
|
+
}, srcLoc ? { loc: srcLoc } : {}));
|
|
12449
13138
|
}
|
|
12450
13139
|
if (src.toLowerCase().endsWith(".svg") || src.includes("image/svg")) {
|
|
12451
13140
|
imgIssues.push("svg-format");
|
|
12452
|
-
issues.push({
|
|
13141
|
+
issues.push(__spreadValues({
|
|
12453
13142
|
rule: "svg-format",
|
|
12454
13143
|
severity: "info",
|
|
12455
13144
|
message: "SVG format detected \u2014 not supported by most email clients. Use PNG instead.",
|
|
12456
13145
|
src: truncateSrc(src)
|
|
12457
|
-
});
|
|
13146
|
+
}, srcLoc ? { loc: srcLoc } : {}));
|
|
12458
13147
|
}
|
|
12459
13148
|
if (!style.includes("display:block") && !style.includes("display: block")) {
|
|
12460
13149
|
imgIssues.push("missing-display-block");
|
|
12461
|
-
issues.push({
|
|
13150
|
+
issues.push(__spreadValues({
|
|
12462
13151
|
rule: "missing-display-block",
|
|
12463
13152
|
severity: "info",
|
|
12464
13153
|
message: "Image without display:block \u2014 may cause unwanted gaps in Outlook.",
|
|
12465
13154
|
src: truncateSrc(src)
|
|
12466
|
-
});
|
|
13155
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12467
13156
|
}
|
|
12468
13157
|
images.push({
|
|
12469
13158
|
src: truncateSrc(src),
|
|
@@ -12501,8 +13190,8 @@ function analyzeImagesFromDom($) {
|
|
|
12501
13190
|
}
|
|
12502
13191
|
return { total: images.length, totalDataUriBytes, issues, images };
|
|
12503
13192
|
}
|
|
12504
|
-
function analyzeImages(html) {
|
|
12505
|
-
return fromHtml(html, EMPTY_IMAGES, analyzeImagesFromDom);
|
|
13193
|
+
function analyzeImages(html, options) {
|
|
13194
|
+
return fromHtml(html, EMPTY_IMAGES, analyzeImagesFromDom, options);
|
|
12506
13195
|
}
|
|
12507
13196
|
|
|
12508
13197
|
// src/inbox-preview.ts
|
|
@@ -12720,11 +13409,57 @@ function checkSize(html) {
|
|
|
12720
13409
|
return fromHtml(html, EMPTY_SIZE, checkSizeFromDom);
|
|
12721
13410
|
}
|
|
12722
13411
|
|
|
13412
|
+
// src/dom-text.ts
|
|
13413
|
+
function visibleTextNodes($) {
|
|
13414
|
+
var _a, _b, _c, _d;
|
|
13415
|
+
const nodes = [];
|
|
13416
|
+
const stack = [...(_b = (_a = $.root()[0]) == null ? void 0 : _a.children) != null ? _b : []].reverse();
|
|
13417
|
+
while (stack.length > 0) {
|
|
13418
|
+
const node = stack.pop();
|
|
13419
|
+
const tag = (_c = node.tagName) == null ? void 0 : _c.toLowerCase();
|
|
13420
|
+
if (tag === "style" || tag === "script" || tag === "head") continue;
|
|
13421
|
+
if (node.type === "text") {
|
|
13422
|
+
nodes.push(node);
|
|
13423
|
+
continue;
|
|
13424
|
+
}
|
|
13425
|
+
const children = (_d = node.children) != null ? _d : [];
|
|
13426
|
+
for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]);
|
|
13427
|
+
}
|
|
13428
|
+
return nodes;
|
|
13429
|
+
}
|
|
13430
|
+
|
|
12723
13431
|
// src/template-checker.ts
|
|
12724
|
-
function checkTemplateVariablesFromDom(
|
|
13432
|
+
function checkTemplateVariablesFromDom($, source) {
|
|
13433
|
+
var _a;
|
|
12725
13434
|
const issues = [];
|
|
12726
13435
|
const seen = /* @__PURE__ */ new Set();
|
|
12727
|
-
const
|
|
13436
|
+
const textNodes = visibleTextNodes($);
|
|
13437
|
+
const positioned = textNodes.some((n) => n.sourceCodeLocation);
|
|
13438
|
+
for (const node of positioned ? textNodes : []) {
|
|
13439
|
+
const data = (_a = node.data) != null ? _a : "";
|
|
13440
|
+
for (const [pattern, label] of TEMPLATE_VARIABLE_PATTERNS) {
|
|
13441
|
+
pattern.lastIndex = 0;
|
|
13442
|
+
let match;
|
|
13443
|
+
while ((match = pattern.exec(data)) !== null) {
|
|
13444
|
+
const variable = match[0];
|
|
13445
|
+
const key = `text:${variable}`;
|
|
13446
|
+
if (seen.has(key)) continue;
|
|
13447
|
+
seen.add(key);
|
|
13448
|
+
const loc = locInTextNode(node, match.index, variable.length, source);
|
|
13449
|
+
issues.push(__spreadValues({
|
|
13450
|
+
rule: "unresolved-variable",
|
|
13451
|
+
severity: "error",
|
|
13452
|
+
message: `Unresolved ${label} variable "${variable}" found in text content.`,
|
|
13453
|
+
variable,
|
|
13454
|
+
location: "text"
|
|
13455
|
+
}, loc ? { loc } : {}));
|
|
13456
|
+
}
|
|
13457
|
+
}
|
|
13458
|
+
}
|
|
13459
|
+
const textContent = textNodes.map((n) => {
|
|
13460
|
+
var _a2;
|
|
13461
|
+
return (_a2 = n.data) != null ? _a2 : "";
|
|
13462
|
+
}).join("");
|
|
12728
13463
|
for (const [pattern, label] of TEMPLATE_VARIABLE_PATTERNS) {
|
|
12729
13464
|
pattern.lastIndex = 0;
|
|
12730
13465
|
let match;
|
|
@@ -12757,13 +13492,14 @@ function checkTemplateVariablesFromDom($) {
|
|
|
12757
13492
|
const key = `attr:${attr}:${variable}`;
|
|
12758
13493
|
if (seen.has(key)) continue;
|
|
12759
13494
|
seen.add(key);
|
|
12760
|
-
|
|
13495
|
+
const loc = locOfAttr(el, attr);
|
|
13496
|
+
issues.push(__spreadValues({
|
|
12761
13497
|
rule: "unresolved-variable",
|
|
12762
13498
|
severity: "error",
|
|
12763
13499
|
message: `Unresolved ${label} variable "${variable}" found in ${attr} attribute.`,
|
|
12764
13500
|
variable,
|
|
12765
13501
|
location: "attribute"
|
|
12766
|
-
});
|
|
13502
|
+
}, loc ? { loc } : {}));
|
|
12767
13503
|
}
|
|
12768
13504
|
}
|
|
12769
13505
|
}
|
|
@@ -12771,17 +13507,17 @@ function checkTemplateVariablesFromDom($) {
|
|
|
12771
13507
|
}
|
|
12772
13508
|
return { unresolvedCount: issues.length, issues };
|
|
12773
13509
|
}
|
|
12774
|
-
function
|
|
12775
|
-
|
|
12776
|
-
|
|
12777
|
-
|
|
12778
|
-
|
|
12779
|
-
|
|
12780
|
-
|
|
13510
|
+
function checkTemplateVariables(html, options) {
|
|
13511
|
+
return fromHtml(
|
|
13512
|
+
html,
|
|
13513
|
+
EMPTY_TEMPLATE,
|
|
13514
|
+
($, h) => checkTemplateVariablesFromDom($, (options == null ? void 0 : options.positions) ? h : void 0),
|
|
13515
|
+
options
|
|
13516
|
+
);
|
|
12781
13517
|
}
|
|
12782
13518
|
|
|
12783
13519
|
// src/overflow-checker.ts
|
|
12784
|
-
var
|
|
13520
|
+
var csstree6 = __toESM(require("css-tree"), 1);
|
|
12785
13521
|
function fixedPxWidth($el) {
|
|
12786
13522
|
const style = $el.attr("style") || "";
|
|
12787
13523
|
const styleMatch = style.match(/(?:^|[;\s])width\s*:\s*(\d+)px/i);
|
|
@@ -12793,89 +13529,142 @@ function fixedPxWidth($el) {
|
|
|
12793
13529
|
function isFluid(style) {
|
|
12794
13530
|
return /max-width\s*:\s*100%/i.test(style) || /width\s*:\s*100%/i.test(style);
|
|
12795
13531
|
}
|
|
12796
|
-
function addWidthIssue(width, label, issues, seen) {
|
|
13532
|
+
function addWidthIssue(width, label, issues, seen, loc) {
|
|
12797
13533
|
const key = `w:${label}:${width}`;
|
|
12798
|
-
|
|
12799
|
-
|
|
12800
|
-
|
|
13534
|
+
const existing = seen.get(key);
|
|
13535
|
+
if (existing) {
|
|
13536
|
+
addOccurrence(existing, loc);
|
|
13537
|
+
return;
|
|
13538
|
+
}
|
|
13539
|
+
const issue = __spreadValues({
|
|
12801
13540
|
rule: "fixed-width-overflow",
|
|
12802
13541
|
severity: "warning",
|
|
12803
13542
|
message: `${label} has a fixed width of ${width}px, wider than the ${EMAIL_MAX_WIDTH}px email frame \u2014 it will force horizontal scrolling, especially on mobile.`,
|
|
12804
13543
|
detail: `Use width:100% with max-width:${EMAIL_MAX_WIDTH}px instead of a fixed width beyond the frame.`
|
|
12805
|
-
});
|
|
13544
|
+
}, loc ? { loc, locs: [loc] } : {});
|
|
13545
|
+
seen.set(key, issue);
|
|
13546
|
+
issues.push(issue);
|
|
13547
|
+
}
|
|
13548
|
+
function locateInNodes(nodes, starts, index, length, source) {
|
|
13549
|
+
var _a;
|
|
13550
|
+
let lo = 0;
|
|
13551
|
+
let hi = starts.length - 1;
|
|
13552
|
+
let found = -1;
|
|
13553
|
+
while (lo <= hi) {
|
|
13554
|
+
const mid = lo + hi >> 1;
|
|
13555
|
+
if (starts[mid] <= index) {
|
|
13556
|
+
found = mid;
|
|
13557
|
+
lo = mid + 1;
|
|
13558
|
+
} else {
|
|
13559
|
+
hi = mid - 1;
|
|
13560
|
+
}
|
|
13561
|
+
}
|
|
13562
|
+
if (found === -1) return void 0;
|
|
13563
|
+
const node = nodes[found];
|
|
13564
|
+
const within = index - starts[found];
|
|
13565
|
+
const available = ((_a = node.data) != null ? _a : "").length - within;
|
|
13566
|
+
return locInTextNode(node, within, Math.min(length, available), source);
|
|
13567
|
+
}
|
|
13568
|
+
function addOccurrence(issue, loc) {
|
|
13569
|
+
if (!loc || !issue.locs) return;
|
|
13570
|
+
if (issue.locs.some((l) => l.offset === loc.offset)) return;
|
|
13571
|
+
if (issue.locs.length >= MAX_WARNING_LOCATIONS) {
|
|
13572
|
+
issue.locsTruncated = true;
|
|
13573
|
+
return;
|
|
13574
|
+
}
|
|
13575
|
+
issue.locs.push(loc);
|
|
12806
13576
|
}
|
|
12807
|
-
function checkOverflowFromDom(
|
|
13577
|
+
function checkOverflowFromDom($, source) {
|
|
13578
|
+
var _a;
|
|
12808
13579
|
const issues = [];
|
|
12809
|
-
const seen = /* @__PURE__ */ new
|
|
13580
|
+
const seen = /* @__PURE__ */ new Map();
|
|
13581
|
+
const tokensSeen = /* @__PURE__ */ new Set();
|
|
12810
13582
|
$("[width], [style*='width']").each((_, el) => {
|
|
12811
13583
|
const $el = $(el);
|
|
12812
13584
|
const width = fixedPxWidth($el);
|
|
12813
13585
|
if (width === null || width <= EMAIL_MAX_WIDTH) return;
|
|
12814
13586
|
if (isFluid($el.attr("style") || "")) return;
|
|
12815
13587
|
const tag = (el.tagName || "element").toLowerCase();
|
|
12816
|
-
|
|
13588
|
+
const fromStyle = /(?:^|[;\s])width\s*:\s*\d+px/i.test($el.attr("style") || "");
|
|
13589
|
+
addWidthIssue(width, `<${tag}>`, issues, seen, locOfAttr(el, fromStyle ? "style" : "width"));
|
|
12817
13590
|
});
|
|
12818
13591
|
$("style").each((_, el) => {
|
|
13592
|
+
const cssText = $(el).text();
|
|
13593
|
+
const anchor = cssBlockAnchor(el, cssText, source);
|
|
12819
13594
|
let ast;
|
|
12820
13595
|
try {
|
|
12821
|
-
ast =
|
|
13596
|
+
ast = csstree6.parse(cssText, { positions: true });
|
|
12822
13597
|
} catch (e) {
|
|
12823
13598
|
return;
|
|
12824
13599
|
}
|
|
12825
|
-
|
|
13600
|
+
csstree6.walk(ast, {
|
|
12826
13601
|
visit: "Rule",
|
|
12827
13602
|
enter(node) {
|
|
12828
13603
|
if (node.type !== "Rule") return;
|
|
12829
13604
|
let widthPx = null;
|
|
12830
13605
|
let fluid = false;
|
|
13606
|
+
let widthLoc;
|
|
12831
13607
|
node.block.children.forEach((child) => {
|
|
12832
13608
|
if (child.type !== "Declaration") return;
|
|
12833
13609
|
const prop = child.property.toLowerCase();
|
|
12834
|
-
const val =
|
|
13610
|
+
const val = csstree6.generate(child.value);
|
|
12835
13611
|
if (prop === "width") {
|
|
12836
13612
|
const m = val.match(/^(\d+)px$/);
|
|
12837
|
-
if (m)
|
|
13613
|
+
if (m) {
|
|
13614
|
+
widthPx = parseInt(m[1], 10);
|
|
13615
|
+
widthLoc = locInCssBlock(anchor, child.loc);
|
|
13616
|
+
}
|
|
12838
13617
|
if (/\b100%/.test(val)) fluid = true;
|
|
12839
13618
|
} else if (prop === "max-width" && /\b100%/.test(val)) {
|
|
12840
13619
|
fluid = true;
|
|
12841
13620
|
}
|
|
12842
13621
|
});
|
|
12843
13622
|
if (widthPx !== null && widthPx > EMAIL_MAX_WIDTH && !fluid) {
|
|
12844
|
-
const selector =
|
|
12845
|
-
addWidthIssue(widthPx, selector || "rule", issues, seen);
|
|
13623
|
+
const selector = csstree6.generate(node.prelude).trim().slice(0, 40);
|
|
13624
|
+
addWidthIssue(widthPx, selector || "rule", issues, seen, widthLoc);
|
|
12846
13625
|
}
|
|
12847
13626
|
}
|
|
12848
13627
|
});
|
|
12849
13628
|
});
|
|
12850
13629
|
const usesWrapGuard = /overflow-wrap|word-break|word-wrap/i.test($.html());
|
|
12851
13630
|
if (!usesWrapGuard) {
|
|
12852
|
-
const
|
|
13631
|
+
const nodes = visibleTextNodes($);
|
|
13632
|
+
const starts = [];
|
|
12853
13633
|
let text = "";
|
|
12854
|
-
|
|
12855
|
-
|
|
12856
|
-
|
|
12857
|
-
|
|
12858
|
-
|
|
12859
|
-
for (const token of text.split(
|
|
12860
|
-
|
|
12861
|
-
|
|
13634
|
+
for (const node of nodes) {
|
|
13635
|
+
starts.push(text.length);
|
|
13636
|
+
text += (_a = node.data) != null ? _a : "";
|
|
13637
|
+
}
|
|
13638
|
+
let at = 0;
|
|
13639
|
+
for (const token of text.split(/(\s+)/)) {
|
|
13640
|
+
const start = at;
|
|
13641
|
+
at += token.length;
|
|
13642
|
+
if (/^\s*$/.test(token)) continue;
|
|
13643
|
+
if (token.length <= UNBREAKABLE_STRING_LENGTH || tokensSeen.has(token)) continue;
|
|
13644
|
+
tokensSeen.add(token);
|
|
12862
13645
|
const preview = token.length > 50 ? `${token.slice(0, 50)}\u2026` : token;
|
|
12863
|
-
|
|
13646
|
+
const loc = locateInNodes(nodes, starts, start, token.length, source);
|
|
13647
|
+
issues.push(__spreadValues({
|
|
12864
13648
|
rule: "unbreakable-string",
|
|
12865
13649
|
severity: "warning",
|
|
12866
13650
|
message: `A ${token.length}-character unbroken string ("${preview}") can't wrap and will force horizontal scrolling on narrow screens.`,
|
|
12867
13651
|
detail: `Add overflow-wrap: anywhere (or word-break: break-word) to its container.`
|
|
12868
|
-
});
|
|
13652
|
+
}, loc ? { loc, locs: [loc] } : {}));
|
|
12869
13653
|
}
|
|
12870
13654
|
}
|
|
12871
13655
|
return { hasOverflow: issues.length > 0, issues };
|
|
12872
13656
|
}
|
|
12873
|
-
function checkOverflow(html) {
|
|
12874
|
-
return fromHtml(
|
|
13657
|
+
function checkOverflow(html, options) {
|
|
13658
|
+
return fromHtml(
|
|
13659
|
+
html,
|
|
13660
|
+
EMPTY_OVERFLOW,
|
|
13661
|
+
($, h) => checkOverflowFromDom($, (options == null ? void 0 : options.positions) ? h : void 0),
|
|
13662
|
+
options
|
|
13663
|
+
);
|
|
12875
13664
|
}
|
|
12876
13665
|
|
|
12877
13666
|
// src/visual-checker.ts
|
|
12878
|
-
var
|
|
13667
|
+
var csstree7 = __toESM(require("css-tree"), 1);
|
|
12879
13668
|
var CSS_WIDE_KEYWORDS = /* @__PURE__ */ new Set(["inherit", "initial", "unset", "revert", "revert-layer"]);
|
|
12880
13669
|
var GRADIENT_RE = /(?:linear|radial|conic)-gradient\(/i;
|
|
12881
13670
|
function isSolidColor(value) {
|
|
@@ -12884,8 +13673,8 @@ function isSolidColor(value) {
|
|
|
12884
13673
|
return c !== null && c.a !== 0;
|
|
12885
13674
|
}
|
|
12886
13675
|
function firstColor(value) {
|
|
12887
|
-
const
|
|
12888
|
-
for (const t of
|
|
13676
|
+
const tokens2 = value.match(/#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\)|\b[a-zA-Z]{3,}\b/g) || [];
|
|
13677
|
+
for (const t of tokens2) {
|
|
12889
13678
|
const lc = t.toLowerCase();
|
|
12890
13679
|
if (lc === "transparent") continue;
|
|
12891
13680
|
if (/^(?:linear|radial|conic|gradient|deg|turn|rad|grad|to|at|from|in|circle|ellipse|closest|farthest|side|corner|url)$/.test(lc)) continue;
|
|
@@ -12922,8 +13711,17 @@ function hasFontFallback(value) {
|
|
|
12922
13711
|
return WEB_SAFE_FONTS.has(t) || GENERIC_FONT_FAMILIES.has(t) || t.startsWith("-apple-system") || t === "blinkmacsystemfont";
|
|
12923
13712
|
});
|
|
12924
13713
|
}
|
|
12925
|
-
function
|
|
12926
|
-
|
|
13714
|
+
function addOccurrence2(issue, loc) {
|
|
13715
|
+
if (!loc || !issue.locs) return;
|
|
13716
|
+
if (issue.locs.some((l) => l.offset === loc.offset)) return;
|
|
13717
|
+
if (issue.locs.length >= MAX_WARNING_LOCATIONS) {
|
|
13718
|
+
issue.locsTruncated = true;
|
|
13719
|
+
return;
|
|
13720
|
+
}
|
|
13721
|
+
issue.locs.push(loc);
|
|
13722
|
+
}
|
|
13723
|
+
function inspectDeclarations(style, issues, seen, locs) {
|
|
13724
|
+
var _a, _b, _c;
|
|
12927
13725
|
const combined = `${(_a = style.get("background-image")) != null ? _a : ""} ${(_b = style.get("background")) != null ? _b : ""}`;
|
|
12928
13726
|
const isGradient = GRADIENT_RE.test(combined);
|
|
12929
13727
|
const isImage = isGradient || /url\(/i.test(combined);
|
|
@@ -12931,30 +13729,40 @@ function inspectDeclarations(style, issues, seen) {
|
|
|
12931
13729
|
const stop = isGradient ? firstColor(combined) : null;
|
|
12932
13730
|
const fix = stop ? `background-color: ${stop};` : `background-color: <solid colour matching the image>;`;
|
|
12933
13731
|
const key = `bg:${fix}`;
|
|
12934
|
-
|
|
12935
|
-
|
|
12936
|
-
|
|
13732
|
+
const loc = (_c = locs == null ? void 0 : locs.get("background-image")) != null ? _c : locs == null ? void 0 : locs.get("background");
|
|
13733
|
+
const existing = seen.get(key);
|
|
13734
|
+
if (existing) {
|
|
13735
|
+
addOccurrence2(existing, loc);
|
|
13736
|
+
} else {
|
|
13737
|
+
const issue = __spreadValues({
|
|
12937
13738
|
rule: "missing-background-fallback",
|
|
12938
13739
|
severity: "warning",
|
|
12939
13740
|
message: `${isGradient ? "Gradient" : "Background image"} has no background-color fallback \u2014 it renders as a blank area (and can hide overlaid text) in Outlook and other clients that drop image backgrounds.`,
|
|
12940
13741
|
detail: isGradient ? `Add a solid fallback beneath the gradient using its first colour stop.` : `Add a solid background-color so the area still has colour when the image is dropped.`,
|
|
12941
13742
|
fix
|
|
12942
|
-
});
|
|
13743
|
+
}, loc ? { loc, locs: [loc] } : {});
|
|
13744
|
+
seen.set(key, issue);
|
|
13745
|
+
issues.push(issue);
|
|
12943
13746
|
}
|
|
12944
13747
|
}
|
|
12945
13748
|
const font = style.get("font-family");
|
|
12946
13749
|
if (font && !CSS_WIDE_KEYWORDS.has(font.trim().toLowerCase()) && !hasFontFallback(font)) {
|
|
12947
13750
|
const fix = `font-family: ${font.trim()}, Arial, sans-serif;`;
|
|
12948
13751
|
const key = `font:${font.trim().toLowerCase()}`;
|
|
12949
|
-
|
|
12950
|
-
|
|
12951
|
-
|
|
13752
|
+
const loc = locs == null ? void 0 : locs.get("font-family");
|
|
13753
|
+
const existing = seen.get(key);
|
|
13754
|
+
if (existing) {
|
|
13755
|
+
addOccurrence2(existing, loc);
|
|
13756
|
+
} else {
|
|
13757
|
+
const issue = __spreadValues({
|
|
12952
13758
|
rule: "missing-font-fallback",
|
|
12953
13759
|
severity: "warning",
|
|
12954
13760
|
message: `font-family "${font.trim()}" has no web-safe fallback \u2014 clients that strip web fonts (Gmail, Outlook) fall back to Times New Roman.`,
|
|
12955
13761
|
detail: `End the stack with a web-safe font and a generic family.`,
|
|
12956
13762
|
fix
|
|
12957
|
-
});
|
|
13763
|
+
}, loc ? { loc, locs: [loc] } : {});
|
|
13764
|
+
seen.set(key, issue);
|
|
13765
|
+
issues.push(issue);
|
|
12958
13766
|
}
|
|
12959
13767
|
}
|
|
12960
13768
|
}
|
|
@@ -12962,35 +13770,52 @@ function ruleToMap(node) {
|
|
|
12962
13770
|
const map = /* @__PURE__ */ new Map();
|
|
12963
13771
|
node.block.children.forEach((child) => {
|
|
12964
13772
|
if (child.type === "Declaration") {
|
|
12965
|
-
map.set(child.property.toLowerCase(),
|
|
13773
|
+
map.set(child.property.toLowerCase(), csstree7.generate(child.value));
|
|
12966
13774
|
}
|
|
12967
13775
|
});
|
|
12968
13776
|
return map;
|
|
12969
13777
|
}
|
|
12970
|
-
function checkVisualFromDom(
|
|
13778
|
+
function checkVisualFromDom($, source) {
|
|
12971
13779
|
const issues = [];
|
|
12972
|
-
const seen = /* @__PURE__ */ new
|
|
13780
|
+
const seen = /* @__PURE__ */ new Map();
|
|
12973
13781
|
$("[style]").each((_, el) => {
|
|
12974
|
-
|
|
13782
|
+
const attrLoc = locOfAttr(el, "style");
|
|
13783
|
+
const style = parseInlineStyle($(el).attr("style") || "");
|
|
13784
|
+
const locs = attrLoc ? new Map([...style.keys()].map((prop) => [prop, attrLoc])) : void 0;
|
|
13785
|
+
inspectDeclarations(style, issues, seen, locs);
|
|
12975
13786
|
});
|
|
12976
13787
|
$("style").each((_, el) => {
|
|
13788
|
+
const cssText = $(el).text();
|
|
13789
|
+
const anchor = cssBlockAnchor(el, cssText, source);
|
|
12977
13790
|
let ast;
|
|
12978
13791
|
try {
|
|
12979
|
-
ast =
|
|
13792
|
+
ast = csstree7.parse(cssText, { positions: true });
|
|
12980
13793
|
} catch (e) {
|
|
12981
13794
|
return;
|
|
12982
13795
|
}
|
|
12983
|
-
|
|
13796
|
+
csstree7.walk(ast, {
|
|
12984
13797
|
visit: "Rule",
|
|
12985
13798
|
enter(node) {
|
|
12986
|
-
if (node.type
|
|
13799
|
+
if (node.type !== "Rule") return;
|
|
13800
|
+
const locs = /* @__PURE__ */ new Map();
|
|
13801
|
+
node.block.children.forEach((child) => {
|
|
13802
|
+
if (child.type !== "Declaration") return;
|
|
13803
|
+
const loc = locInCssBlock(anchor, child.loc);
|
|
13804
|
+
if (loc) locs.set(child.property.toLowerCase(), loc);
|
|
13805
|
+
});
|
|
13806
|
+
inspectDeclarations(ruleToMap(node), issues, seen, locs);
|
|
12987
13807
|
}
|
|
12988
13808
|
});
|
|
12989
13809
|
});
|
|
12990
13810
|
return { issues };
|
|
12991
13811
|
}
|
|
12992
|
-
function checkVisual(html) {
|
|
12993
|
-
return fromHtml(
|
|
13812
|
+
function checkVisual(html, options) {
|
|
13813
|
+
return fromHtml(
|
|
13814
|
+
html,
|
|
13815
|
+
EMPTY_VISUAL,
|
|
13816
|
+
($, h) => checkVisualFromDom($, (options == null ? void 0 : options.positions) ? h : void 0),
|
|
13817
|
+
options
|
|
13818
|
+
);
|
|
12994
13819
|
}
|
|
12995
13820
|
|
|
12996
13821
|
// src/audit.ts
|
|
@@ -13009,7 +13834,8 @@ var EMPTY_AUDIT = {
|
|
|
13009
13834
|
function runAudit($, html, framework, options) {
|
|
13010
13835
|
var _a;
|
|
13011
13836
|
const skip = new Set((_a = options == null ? void 0 : options.skip) != null ? _a : []);
|
|
13012
|
-
const
|
|
13837
|
+
const source = (options == null ? void 0 : options.positions) ? html : void 0;
|
|
13838
|
+
const warnings = skip.has("compatibility") ? [] : analyzeEmailFromDom($, framework, source);
|
|
13013
13839
|
const scores = skip.has("compatibility") ? {} : generateCompatibilityScore(warnings);
|
|
13014
13840
|
const spam = skip.has("spam") ? EMPTY_SPAM : analyzeSpamFromDom($, options == null ? void 0 : options.spam);
|
|
13015
13841
|
const links = skip.has("links") ? EMPTY_LINKS : validateLinksFromDom($);
|
|
@@ -13017,19 +13843,19 @@ function runAudit($, html, framework, options) {
|
|
|
13017
13843
|
const images = skip.has("images") ? EMPTY_IMAGES : analyzeImagesFromDom($);
|
|
13018
13844
|
const inboxPreview = skip.has("inboxPreview") ? EMPTY_INBOX_PREVIEW : extractInboxPreviewFromDom($);
|
|
13019
13845
|
const size = skip.has("size") ? EMPTY_SIZE : checkSizeFromDom($, html);
|
|
13020
|
-
const templateVariables = skip.has("templateVariables") ? EMPTY_TEMPLATE : checkTemplateVariablesFromDom(
|
|
13021
|
-
const overflow = skip.has("overflow") ? EMPTY_OVERFLOW : checkOverflowFromDom(
|
|
13022
|
-
const visual = skip.has("visual") ? EMPTY_VISUAL : checkVisualFromDom(
|
|
13846
|
+
const templateVariables = skip.has("templateVariables") ? EMPTY_TEMPLATE : checkTemplateVariablesFromDom($, source);
|
|
13847
|
+
const overflow = skip.has("overflow") ? EMPTY_OVERFLOW : checkOverflowFromDom($, source);
|
|
13848
|
+
const visual = skip.has("visual") ? EMPTY_VISUAL : checkVisualFromDom($, source);
|
|
13023
13849
|
return { compatibility: { warnings, scores }, spam, links, accessibility, images, inboxPreview, size, templateVariables, overflow, visual };
|
|
13024
13850
|
}
|
|
13025
13851
|
function auditEmail(html, options) {
|
|
13026
|
-
return fromHtml(html, EMPTY_AUDIT, ($, h) => runAudit($, h, options == null ? void 0 : options.framework, options));
|
|
13852
|
+
return fromHtml(html, EMPTY_AUDIT, ($, h) => runAudit($, h, options == null ? void 0 : options.framework, options), options);
|
|
13027
13853
|
}
|
|
13028
13854
|
|
|
13029
13855
|
// src/plain-text.ts
|
|
13030
|
-
var
|
|
13856
|
+
var cheerio5 = __toESM(require("cheerio"), 1);
|
|
13031
13857
|
function toPlainText(html) {
|
|
13032
|
-
const $ =
|
|
13858
|
+
const $ = cheerio5.load(html);
|
|
13033
13859
|
$("style, script, head").remove();
|
|
13034
13860
|
$("[data-skip-in-text='true']").remove();
|
|
13035
13861
|
const lines = [];
|
|
@@ -13064,7 +13890,7 @@ function toPlainText(html) {
|
|
|
13064
13890
|
if (trimmed) lines.push(trimmed);
|
|
13065
13891
|
currentLine = "";
|
|
13066
13892
|
}
|
|
13067
|
-
function
|
|
13893
|
+
function walk8(node) {
|
|
13068
13894
|
var _a;
|
|
13069
13895
|
if (node.type === "text") {
|
|
13070
13896
|
const text = node.data.replace(/\s+/g, " ");
|
|
@@ -13113,7 +13939,7 @@ function toPlainText(html) {
|
|
|
13113
13939
|
currentLine = "- ";
|
|
13114
13940
|
}
|
|
13115
13941
|
for (const child of el.children) {
|
|
13116
|
-
|
|
13942
|
+
walk8(child);
|
|
13117
13943
|
}
|
|
13118
13944
|
if (isBlock) flushLine();
|
|
13119
13945
|
}
|
|
@@ -13121,7 +13947,7 @@ function toPlainText(html) {
|
|
|
13121
13947
|
const root = body.length ? body[0] : $.root()[0];
|
|
13122
13948
|
if (root && "children" in root) {
|
|
13123
13949
|
for (const child of root.children) {
|
|
13124
|
-
|
|
13950
|
+
walk8(child);
|
|
13125
13951
|
}
|
|
13126
13952
|
}
|
|
13127
13953
|
flushLine();
|
|
@@ -13131,7 +13957,6 @@ function toPlainText(html) {
|
|
|
13131
13957
|
}
|
|
13132
13958
|
|
|
13133
13959
|
// src/session.ts
|
|
13134
|
-
var cheerio7 = __toESM(require("cheerio"), 1);
|
|
13135
13960
|
function createSession(html, options) {
|
|
13136
13961
|
if (!html || !html.trim()) {
|
|
13137
13962
|
const fw = options == null ? void 0 : options.framework;
|
|
@@ -13158,16 +13983,17 @@ function createSession(html, options) {
|
|
|
13158
13983
|
if (html.length > MAX_HTML_SIZE) {
|
|
13159
13984
|
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
13160
13985
|
}
|
|
13161
|
-
const $ =
|
|
13986
|
+
const $ = loadHtml(html, options);
|
|
13162
13987
|
const framework = options == null ? void 0 : options.framework;
|
|
13988
|
+
const source = (options == null ? void 0 : options.positions) ? html : void 0;
|
|
13163
13989
|
return {
|
|
13164
13990
|
html,
|
|
13165
13991
|
framework,
|
|
13166
13992
|
audit(opts) {
|
|
13167
|
-
return runAudit($, html, framework, opts);
|
|
13993
|
+
return runAudit($, html, framework, __spreadProps(__spreadValues({}, opts), { positions: options == null ? void 0 : options.positions }));
|
|
13168
13994
|
},
|
|
13169
13995
|
analyze() {
|
|
13170
|
-
return analyzeEmailFromDom($, framework);
|
|
13996
|
+
return analyzeEmailFromDom($, framework, source);
|
|
13171
13997
|
},
|
|
13172
13998
|
score(warnings) {
|
|
13173
13999
|
return generateCompatibilityScore(warnings);
|
|
@@ -13191,13 +14017,13 @@ function createSession(html, options) {
|
|
|
13191
14017
|
return checkSizeFromDom($, html);
|
|
13192
14018
|
},
|
|
13193
14019
|
checkTemplateVariables() {
|
|
13194
|
-
return checkTemplateVariablesFromDom(
|
|
14020
|
+
return checkTemplateVariablesFromDom($, source);
|
|
13195
14021
|
},
|
|
13196
14022
|
checkOverflow() {
|
|
13197
|
-
return checkOverflowFromDom(
|
|
14023
|
+
return checkOverflowFromDom($, source);
|
|
13198
14024
|
},
|
|
13199
14025
|
checkVisual() {
|
|
13200
|
-
return checkVisualFromDom(
|
|
14026
|
+
return checkVisualFromDom($, source);
|
|
13201
14027
|
},
|
|
13202
14028
|
// Transforms create isolated copies since they mutate the DOM
|
|
13203
14029
|
transformForClient(clientId) {
|
|
@@ -13228,18 +14054,22 @@ var CompileError = class extends Error {
|
|
|
13228
14054
|
COMPOUND_VALUE_FEATURES,
|
|
13229
14055
|
CSS_FUNCTION_FEATURES,
|
|
13230
14056
|
CSS_SUPPORT,
|
|
14057
|
+
CSS_SUPPORT_NOTES,
|
|
13231
14058
|
CompileError,
|
|
13232
14059
|
EMAIL_CLIENTS,
|
|
13233
14060
|
EMPTY_DELIVERABILITY,
|
|
13234
14061
|
GENERIC_LINK_TEXT,
|
|
13235
14062
|
HTML_ELEMENT_FEATURES,
|
|
13236
14063
|
MAX_HTML_SIZE,
|
|
14064
|
+
MAX_WARNING_LOCATIONS,
|
|
13237
14065
|
STRUCTURAL_FIX_PROPERTIES,
|
|
14066
|
+
VALUE_CAVEAT_PROPS,
|
|
13238
14067
|
alphaBlend,
|
|
13239
14068
|
analyzeEmail,
|
|
13240
14069
|
analyzeImages,
|
|
13241
14070
|
analyzeSpam,
|
|
13242
14071
|
auditEmail,
|
|
14072
|
+
caveatApplies,
|
|
13243
14073
|
checkAccessibility,
|
|
13244
14074
|
checkOverflow,
|
|
13245
14075
|
checkSize,
|