@emailens/engine 0.10.1 → 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 +906 -227
- 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 +902 -227
- 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,9 +10496,231 @@ function transformForAllClients(html, framework) {
|
|
|
10477
10496
|
}
|
|
10478
10497
|
|
|
10479
10498
|
// src/analyze.ts
|
|
10480
|
-
var cheerio4 = __toESM(require("cheerio"), 1);
|
|
10481
10499
|
var csstree5 = __toESM(require("css-tree"), 1);
|
|
10482
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;
|
|
10538
|
+
}
|
|
10539
|
+
}
|
|
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()));
|
|
10568
|
+
}
|
|
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);
|
|
10637
|
+
}
|
|
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;
|
|
10644
|
+
}
|
|
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");
|
|
10653
|
+
}
|
|
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));
|
|
10663
|
+
}
|
|
10664
|
+
const { banned } = quotedValues(note);
|
|
10665
|
+
if (banned.length) {
|
|
10666
|
+
const v = dashed(value);
|
|
10667
|
+
return banned.includes(v) || banned.includes(unprefixed(v));
|
|
10668
|
+
}
|
|
10669
|
+
if (noteLc.includes("two-value syntax")) return tokens(value).length > 1;
|
|
10670
|
+
return true;
|
|
10671
|
+
}
|
|
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;
|
|
10680
|
+
}
|
|
10681
|
+
case "border-radius": {
|
|
10682
|
+
if (noteLc.includes("slash")) return topLevelSplit(value, "/").length > 1;
|
|
10683
|
+
return true;
|
|
10684
|
+
}
|
|
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
|
+
);
|
|
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;
|
|
10714
|
+
}
|
|
10715
|
+
}
|
|
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
|
+
|
|
10483
10724
|
// src/dark-mode-checker.ts
|
|
10484
10725
|
var csstree4 = __toESM(require("css-tree"), 1);
|
|
10485
10726
|
|
|
@@ -10756,6 +10997,209 @@ function applyColorInversion($, mode) {
|
|
|
10756
10997
|
});
|
|
10757
10998
|
}
|
|
10758
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
|
+
|
|
10759
11203
|
// src/dark-mode-checker.ts
|
|
10760
11204
|
var DARK_MEDIA_RE = /\(\s*prefers-color-scheme\s*:\s*dark\s*\)/i;
|
|
10761
11205
|
var MAX_UNCOVERED_ELEMENTS = 3;
|
|
@@ -10852,15 +11296,16 @@ function checkDarkModeFromDom($) {
|
|
|
10852
11296
|
return name === "color-scheme" || name === "supported-color-schemes";
|
|
10853
11297
|
});
|
|
10854
11298
|
if (!hasOptIn) {
|
|
11299
|
+
const headLoc = locOfFirst($, "head");
|
|
10855
11300
|
for (const clientId of PREFERS_COLOR_SCHEME_CLIENTS) {
|
|
10856
|
-
warnings.push({
|
|
11301
|
+
warnings.push(__spreadValues({
|
|
10857
11302
|
severity: "warning",
|
|
10858
11303
|
client: clientId,
|
|
10859
11304
|
property: "dark-mode-opt-in",
|
|
10860
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.`,
|
|
10861
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">.',
|
|
10862
11307
|
fixType: "structural"
|
|
10863
|
-
});
|
|
11308
|
+
}, headLoc ? { loc: headLoc, locs: [headLoc] } : {}));
|
|
10864
11309
|
}
|
|
10865
11310
|
}
|
|
10866
11311
|
if (darkBlock.rules === 0) return warnings;
|
|
@@ -10878,8 +11323,9 @@ function checkDarkModeFromDom($) {
|
|
|
10878
11323
|
if (inline ? coveredByImportant.has(el) : coveredByAny.has(el)) return;
|
|
10879
11324
|
uncovered++;
|
|
10880
11325
|
const selector = describeSelector($, el);
|
|
11326
|
+
const loc = locOfAttr(el, inline ? "style" : "bgcolor");
|
|
10881
11327
|
for (const clientId of PREFERS_COLOR_SCHEME_CLIENTS) {
|
|
10882
|
-
warnings.push({
|
|
11328
|
+
warnings.push(__spreadValues({
|
|
10883
11329
|
severity: "warning",
|
|
10884
11330
|
client: clientId,
|
|
10885
11331
|
property: "dark-mode-coverage",
|
|
@@ -10887,12 +11333,25 @@ function checkDarkModeFromDom($) {
|
|
|
10887
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.`,
|
|
10888
11334
|
fixType: "css",
|
|
10889
11335
|
selector
|
|
10890
|
-
});
|
|
11336
|
+
}, loc ? { loc, locs: [loc] } : {}));
|
|
10891
11337
|
}
|
|
10892
11338
|
});
|
|
10893
11339
|
return warnings;
|
|
10894
11340
|
}
|
|
10895
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
|
+
|
|
10896
11355
|
// src/analyze.ts
|
|
10897
11356
|
var HTML_ELEMENT_SELECTORS = {
|
|
10898
11357
|
"<style>": "style",
|
|
@@ -10945,14 +11404,25 @@ var CSS_FUNCTION_DETECTORS = CSS_FUNCTION_FEATURES.map((fn) => ({
|
|
|
10945
11404
|
pattern: `${fn}(`
|
|
10946
11405
|
// require opening paren — matches "min(" but not "Minion"
|
|
10947
11406
|
}));
|
|
10948
|
-
function analyzeEmailFromDom($, framework) {
|
|
11407
|
+
function analyzeEmailFromDom($, framework, source) {
|
|
10949
11408
|
const warnings = [];
|
|
10950
|
-
const seenWarnings = /* @__PURE__ */ new
|
|
11409
|
+
const seenWarnings = /* @__PURE__ */ new Map();
|
|
10951
11410
|
function addWarning(w) {
|
|
10952
11411
|
const key = `${w.client}:${w.property}:${w.severity}:${w.selector || ""}`;
|
|
10953
|
-
|
|
10954
|
-
|
|
11412
|
+
const existing = seenWarnings.get(key);
|
|
11413
|
+
if (!existing) {
|
|
11414
|
+
seenWarnings.set(key, w);
|
|
10955
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);
|
|
10956
11426
|
}
|
|
10957
11427
|
}
|
|
10958
11428
|
function describeSelector2(el) {
|
|
@@ -10970,10 +11440,16 @@ function analyzeEmailFromDom($, framework) {
|
|
|
10970
11440
|
for (const feature of HTML_ELEMENT_FEATURES) {
|
|
10971
11441
|
const selector = HTML_ELEMENT_SELECTORS[feature];
|
|
10972
11442
|
if (!selector) continue;
|
|
10973
|
-
|
|
11443
|
+
const matches = $(selector);
|
|
11444
|
+
if (matches.length === 0) continue;
|
|
10974
11445
|
const supportData = CSS_SUPPORT[feature];
|
|
10975
11446
|
if (!supportData) continue;
|
|
10976
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];
|
|
10977
11453
|
for (const client of EMAIL_CLIENTS) {
|
|
10978
11454
|
const support = supportData[client.id];
|
|
10979
11455
|
if (support === "unsupported") {
|
|
@@ -10981,7 +11457,7 @@ function analyzeEmailFromDom($, framework) {
|
|
|
10981
11457
|
const message = msgFn ? msgFn(client.name) : `${client.name} does not support ${feature}.`;
|
|
10982
11458
|
const sug = getSuggestion(feature, client.id, framework);
|
|
10983
11459
|
const fix = getCodeFix(feature, client.id, framework);
|
|
10984
|
-
addWarning(__spreadValues({
|
|
11460
|
+
addWarning(__spreadValues(__spreadValues({
|
|
10985
11461
|
severity: baseSeverity,
|
|
10986
11462
|
client: client.id,
|
|
10987
11463
|
property: feature,
|
|
@@ -10989,11 +11465,11 @@ function analyzeEmailFromDom($, framework) {
|
|
|
10989
11465
|
suggestion: sug.text,
|
|
10990
11466
|
fix,
|
|
10991
11467
|
fixType: getFixType(feature)
|
|
10992
|
-
}, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(feature, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
11468
|
+
}, featureOccurrences ? occurrenceFields(featureOccurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(feature, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
10993
11469
|
} else if (support === "partial" && feature === "<style>") {
|
|
10994
11470
|
const sug = getSuggestion("<style>:partial", client.id, framework);
|
|
10995
11471
|
const fix = getCodeFix("<style>", client.id, framework);
|
|
10996
|
-
addWarning(__spreadValues({
|
|
11472
|
+
addWarning(__spreadValues(__spreadValues({
|
|
10997
11473
|
severity: "warning",
|
|
10998
11474
|
client: client.id,
|
|
10999
11475
|
property: "<style>",
|
|
@@ -11001,55 +11477,97 @@ function analyzeEmailFromDom($, framework) {
|
|
|
11001
11477
|
suggestion: sug.text,
|
|
11002
11478
|
fix,
|
|
11003
11479
|
fixType: getFixType("<style>")
|
|
11004
|
-
}, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<style>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
11480
|
+
}, featureOccurrences ? occurrenceFields(featureOccurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<style>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
11005
11481
|
}
|
|
11006
11482
|
}
|
|
11007
11483
|
}
|
|
11008
11484
|
const parsedAtRules = /* @__PURE__ */ new Set();
|
|
11485
|
+
const selectorLocs = /* @__PURE__ */ new Map();
|
|
11009
11486
|
const parsedProperties = /* @__PURE__ */ new Set();
|
|
11010
11487
|
const propertyLines = /* @__PURE__ */ new Map();
|
|
11488
|
+
const propertyLocs = /* @__PURE__ */ new Map();
|
|
11011
11489
|
const propertyValues = /* @__PURE__ */ new Map();
|
|
11012
11490
|
const detectedCssFunctions = /* @__PURE__ */ new Set();
|
|
11013
11491
|
const detectedPseudoClasses = /* @__PURE__ */ new Set();
|
|
11014
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
|
+
}
|
|
11015
11526
|
$("style").each((_, el) => {
|
|
11016
11527
|
const cssText = $(el).text();
|
|
11528
|
+
blockAnchor = cssBlockAnchor(el, cssText, source);
|
|
11017
11529
|
try {
|
|
11018
11530
|
const ast = csstree5.parse(cssText, { parseCustomProperty: true, positions: true });
|
|
11019
11531
|
csstree5.walk(ast, {
|
|
11020
11532
|
enter(node) {
|
|
11021
11533
|
if (node.type === "Atrule") {
|
|
11022
11534
|
parsedAtRules.add(`@${node.name}`);
|
|
11535
|
+
recordSelectorLoc(`@${node.name}`, node.loc);
|
|
11023
11536
|
}
|
|
11024
11537
|
if (node.type === "PseudoClassSelector") {
|
|
11025
11538
|
detectedPseudoClasses.add(`:${node.name}`);
|
|
11539
|
+
recordSelectorLoc(`:${node.name}`, node.loc);
|
|
11026
11540
|
}
|
|
11027
11541
|
if (node.type === "PseudoElementSelector") {
|
|
11028
11542
|
detectedPseudoElements.add(`::${node.name}`);
|
|
11543
|
+
recordSelectorLoc(`::${node.name}`, node.loc);
|
|
11029
11544
|
}
|
|
11030
11545
|
if (node.type === "Declaration") {
|
|
11031
11546
|
const prop = node.property.toLowerCase();
|
|
11032
11547
|
parsedProperties.add(prop);
|
|
11033
|
-
if (node.loc && !propertyLines.has(prop)) {
|
|
11034
|
-
propertyLines.set(prop, node.loc.start.line);
|
|
11035
|
-
}
|
|
11036
11548
|
const valueStr = csstree5.generate(node.value);
|
|
11037
11549
|
const seenValues = propertyValues.get(prop);
|
|
11038
11550
|
if (seenValues) seenValues.push(valueStr);
|
|
11039
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
|
+
}
|
|
11040
11556
|
for (const det of COMPOUND_DETECTORS) {
|
|
11041
|
-
if (prop === det.property && valueStr.includes(det.valueIncludes)) {
|
|
11557
|
+
if (prop === det.property && valueStr.toLowerCase().includes(det.valueIncludes)) {
|
|
11042
11558
|
parsedProperties.add(det.key);
|
|
11043
|
-
if (node.loc
|
|
11044
|
-
propertyLines.set(det.key, node.loc.start.line);
|
|
11559
|
+
if (node.loc) {
|
|
11560
|
+
if (!propertyLines.has(det.key)) propertyLines.set(det.key, node.loc.start.line);
|
|
11561
|
+
recordLoc(det.key, node.loc);
|
|
11045
11562
|
}
|
|
11046
11563
|
}
|
|
11047
11564
|
}
|
|
11048
11565
|
for (const fn of CSS_FUNCTION_DETECTORS) {
|
|
11049
11566
|
if (valueStr.includes(fn.pattern)) {
|
|
11050
11567
|
detectedCssFunctions.add(fn.key);
|
|
11051
|
-
if (node.loc
|
|
11052
|
-
propertyLines.set(fn.key, node.loc.start.line);
|
|
11568
|
+
if (node.loc) {
|
|
11569
|
+
if (!propertyLines.has(fn.key)) propertyLines.set(fn.key, node.loc.start.line);
|
|
11570
|
+
recordLoc(fn.key, node.loc);
|
|
11053
11571
|
}
|
|
11054
11572
|
}
|
|
11055
11573
|
}
|
|
@@ -11061,33 +11579,42 @@ function analyzeEmailFromDom($, framework) {
|
|
|
11061
11579
|
});
|
|
11062
11580
|
for (const atRule of AT_RULE_FEATURES) {
|
|
11063
11581
|
if (!parsedAtRules.has(atRule)) continue;
|
|
11064
|
-
checkPropertySupport(atRule, addWarning, framework);
|
|
11582
|
+
checkPropertySupport(atRule, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(atRule));
|
|
11065
11583
|
}
|
|
11066
11584
|
const cssPropertiesToCheck = Object.keys(CSS_SUPPORT).filter(
|
|
11067
11585
|
(k) => !k.startsWith("<") && !k.startsWith("@")
|
|
11068
11586
|
);
|
|
11069
11587
|
$("[style]").each((_, el) => {
|
|
11070
|
-
var _a;
|
|
11071
11588
|
const style = $(el).attr("style") || "";
|
|
11072
11589
|
const props = parseStyleProperties(style);
|
|
11073
11590
|
const selector = describeSelector2(el);
|
|
11591
|
+
const locs = elementLocs(locOfAttr(el, "style"));
|
|
11074
11592
|
for (const prop of props) {
|
|
11075
11593
|
for (const det of COMPOUND_DETECTORS) {
|
|
11076
11594
|
if (prop === det.property) {
|
|
11077
11595
|
const value2 = getStyleValue(style, prop);
|
|
11078
|
-
if (value2 == null ? void 0 : value2.includes(det.valueIncludes)) {
|
|
11079
|
-
checkPropertySupport(det.key, addWarning, framework, selector);
|
|
11596
|
+
if (value2 == null ? void 0 : value2.toLowerCase().includes(det.valueIncludes)) {
|
|
11597
|
+
checkPropertySupport(det.key, addWarning, framework, selector, void 0, void 0, locs);
|
|
11080
11598
|
}
|
|
11081
11599
|
}
|
|
11082
11600
|
}
|
|
11083
11601
|
if (cssPropertiesToCheck.includes(prop)) {
|
|
11084
|
-
|
|
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
|
+
);
|
|
11085
11612
|
}
|
|
11086
11613
|
const value = getStyleValue(style, prop);
|
|
11087
11614
|
if (value) {
|
|
11088
11615
|
for (const fn of CSS_FUNCTION_DETECTORS) {
|
|
11089
11616
|
if (value.includes(fn.pattern)) {
|
|
11090
|
-
checkPropertySupport(fn.key, addWarning, framework, selector);
|
|
11617
|
+
checkPropertySupport(fn.key, addWarning, framework, selector, void 0, void 0, locs);
|
|
11091
11618
|
}
|
|
11092
11619
|
}
|
|
11093
11620
|
}
|
|
@@ -11103,87 +11630,66 @@ function analyzeEmailFromDom($, framework) {
|
|
|
11103
11630
|
framework,
|
|
11104
11631
|
void 0,
|
|
11105
11632
|
propertyLines.get(prop),
|
|
11106
|
-
values
|
|
11633
|
+
values,
|
|
11634
|
+
propertyLocs.get(prop)
|
|
11107
11635
|
);
|
|
11108
11636
|
}
|
|
11109
11637
|
for (const compound of COMPOUND_VALUE_FEATURES) {
|
|
11110
11638
|
if (compound.startsWith(":") || compound.startsWith("::")) continue;
|
|
11111
11639
|
if (parsedProperties.has(compound)) {
|
|
11112
|
-
checkPropertySupport(compound, addWarning, framework, void 0, propertyLines.get(compound));
|
|
11640
|
+
checkPropertySupport(compound, addWarning, framework, void 0, propertyLines.get(compound), void 0, propertyLocs.get(compound));
|
|
11113
11641
|
}
|
|
11114
11642
|
}
|
|
11115
11643
|
for (const pseudo of detectedPseudoClasses) {
|
|
11116
11644
|
if (CSS_SUPPORT[pseudo]) {
|
|
11117
|
-
checkPropertySupport(pseudo, addWarning, framework);
|
|
11645
|
+
checkPropertySupport(pseudo, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(pseudo));
|
|
11118
11646
|
}
|
|
11119
11647
|
}
|
|
11120
11648
|
for (const pseudo of detectedPseudoElements) {
|
|
11121
11649
|
if (CSS_SUPPORT[pseudo]) {
|
|
11122
|
-
checkPropertySupport(pseudo, addWarning, framework);
|
|
11650
|
+
checkPropertySupport(pseudo, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(pseudo));
|
|
11123
11651
|
}
|
|
11124
11652
|
}
|
|
11125
11653
|
for (const fn of detectedCssFunctions) {
|
|
11126
|
-
checkPropertySupport(fn, addWarning, framework, void 0, propertyLines.get(fn));
|
|
11654
|
+
checkPropertySupport(fn, addWarning, framework, void 0, propertyLines.get(fn), void 0, propertyLocs.get(fn));
|
|
11127
11655
|
}
|
|
11128
11656
|
for (const w of checkDarkModeFromDom($)) addWarning(w);
|
|
11129
11657
|
const severityOrder = { error: 0, warning: 1, info: 2 };
|
|
11130
11658
|
warnings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
11131
11659
|
return warnings;
|
|
11132
11660
|
}
|
|
11133
|
-
function analyzeEmail(html, framework) {
|
|
11661
|
+
function analyzeEmail(html, framework, options) {
|
|
11134
11662
|
if (!html || !html.trim()) {
|
|
11135
11663
|
return [];
|
|
11136
11664
|
}
|
|
11137
11665
|
if (html.length > MAX_HTML_SIZE) {
|
|
11138
11666
|
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
11139
11667
|
}
|
|
11140
|
-
const $ =
|
|
11141
|
-
return analyzeEmailFromDom($, framework);
|
|
11668
|
+
const $ = loadHtml(html, options);
|
|
11669
|
+
return analyzeEmailFromDom($, framework, (options == null ? void 0 : options.positions) ? html : void 0);
|
|
11142
11670
|
}
|
|
11143
11671
|
function getFixType(prop) {
|
|
11144
11672
|
return STRUCTURAL_FIX_PROPERTIES.has(prop) ? "structural" : "css";
|
|
11145
11673
|
}
|
|
11146
|
-
var VALUE_CAVEAT_PROPS = /* @__PURE__ */ new Set(["margin", "position", "overflow"]);
|
|
11147
|
-
var POSITION_KEYWORDS = ["relative", "absolute", "fixed", "sticky"];
|
|
11148
|
-
function valueTriggersCaveat(prop, value, notes) {
|
|
11149
|
-
const note = (notes != null ? notes : []).join(" ");
|
|
11150
|
-
const noteLc = note.toLowerCase();
|
|
11151
|
-
if (prop === "margin") {
|
|
11152
|
-
if (/(?:^|[\s:(])-\.?\d/.test(value) && noteLc.includes("negative")) return true;
|
|
11153
|
-
if (/\bauto\b/.test(value) && noteLc.includes("auto")) return true;
|
|
11154
|
-
return false;
|
|
11155
|
-
}
|
|
11156
|
-
if (prop === "position") {
|
|
11157
|
-
const used = POSITION_KEYWORDS.find((k) => new RegExp(`\\b${k}\\b`).test(value));
|
|
11158
|
-
if (!used) return false;
|
|
11159
|
-
const m = note.match(/supports\s+.+?\s+but not\s+([^.]+)/i);
|
|
11160
|
-
if (m) return m[1].toLowerCase().includes(used);
|
|
11161
|
-
return used === "fixed" || used === "sticky";
|
|
11162
|
-
}
|
|
11163
|
-
if (prop === "overflow") {
|
|
11164
|
-
if (!/\b(?:auto|scroll)\b/.test(value)) return false;
|
|
11165
|
-
return noteLc.includes("cannot scroll");
|
|
11166
|
-
}
|
|
11167
|
-
return true;
|
|
11168
|
-
}
|
|
11169
11674
|
function noteSuffix(notes) {
|
|
11170
11675
|
if (!(notes == null ? void 0 : notes.length)) return "";
|
|
11171
11676
|
const cleaned = notes.map((n) => n.replace(/^(?:Partial|Buggy|Not supported)\.\s*/i, "").trim()).filter(Boolean);
|
|
11172
11677
|
return cleaned.length ? ` ${cleaned.join(" ")}` : "";
|
|
11173
11678
|
}
|
|
11174
|
-
function checkPropertySupport(prop, addWarning, framework, selector, line,
|
|
11175
|
-
var _a;
|
|
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;
|
|
11176
11683
|
const supportData = CSS_SUPPORT[prop];
|
|
11177
11684
|
if (!supportData) return;
|
|
11178
11685
|
const fixType = getFixType(prop);
|
|
11179
|
-
const valueGated = VALUE_CAVEAT_PROPS.has(prop);
|
|
11180
11686
|
for (const client of EMAIL_CLIENTS) {
|
|
11181
11687
|
const support = supportData[client.id] || "unknown";
|
|
11182
|
-
const notes = (
|
|
11688
|
+
const notes = (_b = CSS_SUPPORT_NOTES[prop]) == null ? void 0 : _b[client.id];
|
|
11183
11689
|
if (support === "unsupported") {
|
|
11184
11690
|
const sug = getSuggestion(prop, client.id, framework);
|
|
11185
11691
|
const fix = getCodeFix(prop, client.id, framework);
|
|
11186
|
-
addWarning(__spreadValues(__spreadValues(__spreadValues({
|
|
11692
|
+
addWarning(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
|
|
11187
11693
|
severity: "warning",
|
|
11188
11694
|
client: client.id,
|
|
11189
11695
|
property: prop,
|
|
@@ -11191,12 +11697,13 @@ function checkPropertySupport(prop, addWarning, framework, selector, line, value
|
|
|
11191
11697
|
suggestion: sug.text,
|
|
11192
11698
|
fix,
|
|
11193
11699
|
fixType
|
|
11194
|
-
}, selector ? { selector } : {}),
|
|
11700
|
+
}, selector ? { selector } : {}), reportedLine !== void 0 ? { line: reportedLine } : {}), occurrences ? occurrenceFields(occurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
11195
11701
|
} else if (support === "partial") {
|
|
11196
|
-
if (
|
|
11702
|
+
if (!caveatApplies(prop, values, notes)) continue;
|
|
11703
|
+
const hits = triggeringOccurrences(prop, occurrences, notes);
|
|
11197
11704
|
const sug = getSuggestion(prop, client.id, framework);
|
|
11198
11705
|
const fix = getCodeFix(prop, client.id, framework);
|
|
11199
|
-
addWarning(__spreadValues(__spreadValues(__spreadValues({
|
|
11706
|
+
addWarning(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
|
|
11200
11707
|
severity: "info",
|
|
11201
11708
|
client: client.id,
|
|
11202
11709
|
property: prop,
|
|
@@ -11204,7 +11711,7 @@ function checkPropertySupport(prop, addWarning, framework, selector, line, value
|
|
|
11204
11711
|
suggestion: sug.text,
|
|
11205
11712
|
fix,
|
|
11206
11713
|
fixType
|
|
11207
|
-
}, selector ? { selector } : {}), line !== void 0 ? { line } : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
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 } : {}));
|
|
11208
11715
|
}
|
|
11209
11716
|
}
|
|
11210
11717
|
}
|
|
@@ -11223,6 +11730,19 @@ function generateCompatibilityScore(warnings) {
|
|
|
11223
11730
|
}
|
|
11224
11731
|
return result;
|
|
11225
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
|
+
}
|
|
11226
11746
|
function warningsForClient(warnings, clientId) {
|
|
11227
11747
|
return warnings.filter((w) => w.client === clientId);
|
|
11228
11748
|
}
|
|
@@ -11563,16 +12083,6 @@ function extractCode(response) {
|
|
|
11563
12083
|
return response.trim();
|
|
11564
12084
|
}
|
|
11565
12085
|
|
|
11566
|
-
// src/parse-html.ts
|
|
11567
|
-
var cheerio5 = __toESM(require("cheerio"), 1);
|
|
11568
|
-
function fromHtml(html, empty, fn) {
|
|
11569
|
-
if (!html || !html.trim()) return empty;
|
|
11570
|
-
if (html.length > MAX_HTML_SIZE) {
|
|
11571
|
-
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
11572
|
-
}
|
|
11573
|
-
return fn(cheerio5.load(html), html);
|
|
11574
|
-
}
|
|
11575
|
-
|
|
11576
12086
|
// src/spam-scorer.ts
|
|
11577
12087
|
var SPAM_TRIGGER_PHRASES = [
|
|
11578
12088
|
"act now",
|
|
@@ -12041,6 +12551,8 @@ function validateLinksFromDom($) {
|
|
|
12041
12551
|
const href = $(el).attr("href") || "";
|
|
12042
12552
|
const text = $(el).text().trim();
|
|
12043
12553
|
const category = classifyHref(href);
|
|
12554
|
+
const elLoc = locOfElement(el);
|
|
12555
|
+
const hrefLoc = href ? locOfAttr(el, "href") : elLoc;
|
|
12044
12556
|
switch (category) {
|
|
12045
12557
|
case "https":
|
|
12046
12558
|
breakdown.https++;
|
|
@@ -12071,95 +12583,95 @@ function validateLinksFromDom($) {
|
|
|
12071
12583
|
hrefCounts.set(href, (hrefCounts.get(href) || 0) + 1);
|
|
12072
12584
|
}
|
|
12073
12585
|
if (!href || !href.trim()) {
|
|
12074
|
-
issues.push({
|
|
12586
|
+
issues.push(__spreadValues({
|
|
12075
12587
|
severity: "error",
|
|
12076
12588
|
rule: "empty-href",
|
|
12077
12589
|
message: "Link has no href attribute",
|
|
12078
12590
|
text: text.slice(0, 80) || "(no text)"
|
|
12079
|
-
});
|
|
12591
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12080
12592
|
return;
|
|
12081
12593
|
}
|
|
12082
12594
|
if (category === "javascript" && !isPlaceholderHref(href)) {
|
|
12083
|
-
issues.push({
|
|
12595
|
+
issues.push(__spreadValues({
|
|
12084
12596
|
severity: "error",
|
|
12085
12597
|
rule: "javascript-href",
|
|
12086
12598
|
message: "Link uses javascript: protocol",
|
|
12087
12599
|
href: href.slice(0, 100),
|
|
12088
12600
|
text: text.slice(0, 80) || "(no text)"
|
|
12089
|
-
});
|
|
12601
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
12090
12602
|
return;
|
|
12091
12603
|
}
|
|
12092
12604
|
if (isPlaceholderHref(href)) {
|
|
12093
|
-
issues.push({
|
|
12605
|
+
issues.push(__spreadValues({
|
|
12094
12606
|
severity: "warning",
|
|
12095
12607
|
rule: "placeholder-href",
|
|
12096
12608
|
message: "Link has a placeholder href (# or javascript:void)",
|
|
12097
12609
|
href,
|
|
12098
12610
|
text: text.slice(0, 80) || "(no text)"
|
|
12099
|
-
});
|
|
12611
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
12100
12612
|
return;
|
|
12101
12613
|
}
|
|
12102
12614
|
if (category === "http") {
|
|
12103
|
-
issues.push({
|
|
12615
|
+
issues.push(__spreadValues({
|
|
12104
12616
|
severity: "warning",
|
|
12105
12617
|
rule: "insecure-link",
|
|
12106
12618
|
message: "Link uses HTTP instead of HTTPS",
|
|
12107
12619
|
href: href.slice(0, 120),
|
|
12108
12620
|
text: text.slice(0, 80) || "(no text)"
|
|
12109
|
-
});
|
|
12621
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
12110
12622
|
}
|
|
12111
12623
|
if (category === "protocol-relative") {
|
|
12112
|
-
issues.push({
|
|
12624
|
+
issues.push(__spreadValues({
|
|
12113
12625
|
severity: "warning",
|
|
12114
12626
|
rule: "protocol-relative",
|
|
12115
12627
|
message: "Protocol-relative URL may break in email clients \u2014 use https:// explicitly",
|
|
12116
12628
|
href: href.slice(0, 120),
|
|
12117
12629
|
text: text.slice(0, 80) || "(no text)"
|
|
12118
|
-
});
|
|
12630
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
12119
12631
|
}
|
|
12120
12632
|
if (text && GENERIC_LINK_TEXT.has(text.toLowerCase())) {
|
|
12121
|
-
issues.push({
|
|
12633
|
+
issues.push(__spreadValues({
|
|
12122
12634
|
severity: "warning",
|
|
12123
12635
|
rule: "generic-link-text",
|
|
12124
12636
|
message: `Link text "${text}" is vague \u2014 use descriptive text for accessibility and engagement`,
|
|
12125
12637
|
href: href.slice(0, 120),
|
|
12126
12638
|
text
|
|
12127
|
-
});
|
|
12639
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12128
12640
|
}
|
|
12129
12641
|
if (!text && !$(el).attr("aria-label") && !$(el).find("img[alt]").length) {
|
|
12130
|
-
issues.push({
|
|
12642
|
+
issues.push(__spreadValues({
|
|
12131
12643
|
severity: "error",
|
|
12132
12644
|
rule: "empty-link-text",
|
|
12133
12645
|
message: "Link has no visible text or aria-label",
|
|
12134
12646
|
href: href.slice(0, 120)
|
|
12135
|
-
});
|
|
12647
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12136
12648
|
}
|
|
12137
12649
|
if (category === "mailto" && href.trim().toLowerCase() === "mailto:") {
|
|
12138
|
-
issues.push({
|
|
12650
|
+
issues.push(__spreadValues({
|
|
12139
12651
|
severity: "error",
|
|
12140
12652
|
rule: "empty-mailto",
|
|
12141
12653
|
message: "mailto: link has no email address",
|
|
12142
12654
|
href,
|
|
12143
12655
|
text: text.slice(0, 80) || "(no text)"
|
|
12144
|
-
});
|
|
12656
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
12145
12657
|
}
|
|
12146
12658
|
if (category === "tel" && href.trim().toLowerCase() === "tel:") {
|
|
12147
|
-
issues.push({
|
|
12659
|
+
issues.push(__spreadValues({
|
|
12148
12660
|
severity: "error",
|
|
12149
12661
|
rule: "empty-tel",
|
|
12150
12662
|
message: "tel: link has no phone number",
|
|
12151
12663
|
href,
|
|
12152
12664
|
text: text.slice(0, 80) || "(no text)"
|
|
12153
|
-
});
|
|
12665
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
12154
12666
|
}
|
|
12155
12667
|
if (href.length > 2e3) {
|
|
12156
|
-
issues.push({
|
|
12668
|
+
issues.push(__spreadValues({
|
|
12157
12669
|
severity: "info",
|
|
12158
12670
|
rule: "long-url",
|
|
12159
12671
|
message: "URL exceeds 2000 characters \u2014 may be truncated by some email clients",
|
|
12160
12672
|
href: href.slice(0, 120) + "...",
|
|
12161
12673
|
text: text.slice(0, 80) || "(no text)"
|
|
12162
|
-
});
|
|
12674
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
12163
12675
|
}
|
|
12164
12676
|
});
|
|
12165
12677
|
links.each((_, el) => {
|
|
@@ -12168,14 +12680,15 @@ function validateLinksFromDom($) {
|
|
|
12168
12680
|
if (trimmed.startsWith("#") && trimmed.length > 1) {
|
|
12169
12681
|
const targetId = trimmed.slice(1);
|
|
12170
12682
|
const target = $(`[id="${targetId}"]`);
|
|
12683
|
+
const anchorLoc = locOfAttr(el, "href");
|
|
12171
12684
|
if (target.length === 0) {
|
|
12172
|
-
issues.push({
|
|
12685
|
+
issues.push(__spreadValues({
|
|
12173
12686
|
severity: "error",
|
|
12174
12687
|
rule: "broken-anchor",
|
|
12175
12688
|
message: `Anchor link "${trimmed}" points to an element that does not exist`,
|
|
12176
12689
|
href: trimmed,
|
|
12177
12690
|
text: $(el).text().trim().slice(0, 80) || "(no text)"
|
|
12178
|
-
});
|
|
12691
|
+
}, anchorLoc ? { loc: anchorLoc } : {}));
|
|
12179
12692
|
}
|
|
12180
12693
|
}
|
|
12181
12694
|
});
|
|
@@ -12191,8 +12704,8 @@ function validateLinksFromDom($) {
|
|
|
12191
12704
|
}
|
|
12192
12705
|
return { totalLinks, issues, breakdown };
|
|
12193
12706
|
}
|
|
12194
|
-
function validateLinks(html) {
|
|
12195
|
-
return fromHtml(html, EMPTY_LINKS, validateLinksFromDom);
|
|
12707
|
+
function validateLinks(html, options) {
|
|
12708
|
+
return fromHtml(html, EMPTY_LINKS, validateLinksFromDom, options);
|
|
12196
12709
|
}
|
|
12197
12710
|
|
|
12198
12711
|
// src/accessibility-checker.ts
|
|
@@ -12217,24 +12730,31 @@ function describeElement($, el) {
|
|
|
12217
12730
|
function checkLangAttribute($) {
|
|
12218
12731
|
const lang = $("html").attr("lang");
|
|
12219
12732
|
if (!lang || !lang.trim()) {
|
|
12220
|
-
|
|
12733
|
+
const loc = locOfFirst($, "html");
|
|
12734
|
+
return __spreadProps(__spreadValues({
|
|
12221
12735
|
severity: "error",
|
|
12222
12736
|
rule: "missing-lang",
|
|
12223
|
-
message: "Missing lang attribute on <html> element"
|
|
12737
|
+
message: "Missing lang attribute on <html> element"
|
|
12738
|
+
}, loc ? { loc } : {}), {
|
|
12224
12739
|
details: 'Screen readers use the lang attribute to determine pronunciation. Add lang="en" (or appropriate language code).'
|
|
12225
|
-
};
|
|
12740
|
+
});
|
|
12226
12741
|
}
|
|
12227
12742
|
return null;
|
|
12228
12743
|
}
|
|
12744
|
+
function titleLoc($) {
|
|
12745
|
+
return $("title").length ? locOfFirst($, "title") : locOfFirst($, "head");
|
|
12746
|
+
}
|
|
12229
12747
|
function checkTitle($) {
|
|
12230
12748
|
const title = $("title").text().trim();
|
|
12231
12749
|
if (!title) {
|
|
12232
|
-
|
|
12750
|
+
const loc = titleLoc($);
|
|
12751
|
+
return __spreadProps(__spreadValues({
|
|
12233
12752
|
severity: "warning",
|
|
12234
12753
|
rule: "missing-title",
|
|
12235
|
-
message: "Missing or empty <title> element"
|
|
12754
|
+
message: "Missing or empty <title> element"
|
|
12755
|
+
}, loc ? { loc } : {}), {
|
|
12236
12756
|
details: "The <title> helps screen readers identify the email content."
|
|
12237
|
-
};
|
|
12757
|
+
});
|
|
12238
12758
|
}
|
|
12239
12759
|
return null;
|
|
12240
12760
|
}
|
|
@@ -12244,34 +12764,38 @@ function checkImageAlt($) {
|
|
|
12244
12764
|
const alt = $(el).attr("alt");
|
|
12245
12765
|
const src = $(el).attr("src") || "";
|
|
12246
12766
|
const role = $(el).attr("role");
|
|
12767
|
+
const elLoc = locOfElement(el);
|
|
12247
12768
|
if (role === "presentation" || role === "none") return;
|
|
12248
12769
|
if (alt === void 0) {
|
|
12249
|
-
issues.push({
|
|
12770
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12250
12771
|
severity: "error",
|
|
12251
12772
|
rule: "img-missing-alt",
|
|
12252
12773
|
message: "Image missing alt attribute",
|
|
12253
|
-
element: describeElement($, el)
|
|
12774
|
+
element: describeElement($, el)
|
|
12775
|
+
}, elLoc ? { loc: elLoc } : {}), {
|
|
12254
12776
|
details: 'Every image must have an alt attribute. Use alt="" for decorative images.'
|
|
12255
|
-
});
|
|
12777
|
+
}));
|
|
12256
12778
|
} else if (alt.trim() === "") {
|
|
12257
12779
|
const isLikelyContent = !src.includes("spacer") && !src.includes("pixel") && !src.includes("tracking") && !src.includes("1x1") && !src.includes("transparent");
|
|
12258
12780
|
if (isLikelyContent && ($(el).attr("width") || "0") !== "1") {
|
|
12259
|
-
issues.push({
|
|
12781
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12260
12782
|
severity: "info",
|
|
12261
12783
|
rule: "img-empty-alt",
|
|
12262
12784
|
message: "Image has empty alt text \u2014 verify it is decorative",
|
|
12263
|
-
element: describeElement($, el)
|
|
12785
|
+
element: describeElement($, el)
|
|
12786
|
+
}, locOfAttr(el, "alt") ? { loc: locOfAttr(el, "alt") } : {}), {
|
|
12264
12787
|
details: "Empty alt is correct for decorative images, but content images need descriptive alt text."
|
|
12265
|
-
});
|
|
12788
|
+
}));
|
|
12266
12789
|
}
|
|
12267
12790
|
} else if (/\.(png|jpg|jpeg|gif|svg|webp|bmp)$/i.test(alt)) {
|
|
12268
|
-
issues.push({
|
|
12791
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12269
12792
|
severity: "error",
|
|
12270
12793
|
rule: "img-filename-alt",
|
|
12271
12794
|
message: "Image alt text is a filename, not a description",
|
|
12272
|
-
element: describeElement($, el)
|
|
12795
|
+
element: describeElement($, el)
|
|
12796
|
+
}, locOfAttr(el, "alt") ? { loc: locOfAttr(el, "alt") } : {}), {
|
|
12273
12797
|
details: `Alt "${alt}" should describe the image content, not the file name.`
|
|
12274
|
-
});
|
|
12798
|
+
}));
|
|
12275
12799
|
}
|
|
12276
12800
|
});
|
|
12277
12801
|
return issues;
|
|
@@ -12279,28 +12803,31 @@ function checkImageAlt($) {
|
|
|
12279
12803
|
function checkLinkAccessibility($) {
|
|
12280
12804
|
const issues = [];
|
|
12281
12805
|
$("a").each((_, el) => {
|
|
12806
|
+
const elLoc = locOfElement(el);
|
|
12282
12807
|
const text = $(el).text().trim().toLowerCase();
|
|
12283
12808
|
const ariaLabel = $(el).attr("aria-label");
|
|
12284
12809
|
const title = $(el).attr("title");
|
|
12285
12810
|
const imgAlt = $(el).find("img").attr("alt");
|
|
12286
12811
|
if (!text && !ariaLabel && !title && !imgAlt) {
|
|
12287
|
-
issues.push({
|
|
12812
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12288
12813
|
severity: "error",
|
|
12289
12814
|
rule: "link-no-accessible-name",
|
|
12290
12815
|
message: "Link has no accessible name",
|
|
12291
|
-
element: describeElement($, el)
|
|
12816
|
+
element: describeElement($, el)
|
|
12817
|
+
}, elLoc ? { loc: elLoc } : {}), {
|
|
12292
12818
|
details: "Links need visible text, aria-label, or an image with alt text."
|
|
12293
|
-
});
|
|
12819
|
+
}));
|
|
12294
12820
|
return;
|
|
12295
12821
|
}
|
|
12296
12822
|
if (text && GENERIC_LINK_TEXT.has(text) && !ariaLabel) {
|
|
12297
|
-
issues.push({
|
|
12823
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12298
12824
|
severity: "warning",
|
|
12299
12825
|
rule: "link-generic-text",
|
|
12300
12826
|
message: `Link text "${$(el).text().trim()}" is not descriptive`,
|
|
12301
|
-
element: describeElement($, el)
|
|
12827
|
+
element: describeElement($, el)
|
|
12828
|
+
}, elLoc ? { loc: elLoc } : {}), {
|
|
12302
12829
|
details: "Screen readers often list links out of context. Use text that describes the destination."
|
|
12303
|
-
});
|
|
12830
|
+
}));
|
|
12304
12831
|
}
|
|
12305
12832
|
});
|
|
12306
12833
|
return issues;
|
|
@@ -12310,18 +12837,20 @@ function checkTableAccessibility($) {
|
|
|
12310
12837
|
$("table").each((_, el) => {
|
|
12311
12838
|
if ($(el).parents('table[role="presentation"], table[role="none"]').length > 0) return;
|
|
12312
12839
|
const role = $(el).attr("role");
|
|
12840
|
+
const tableLoc = locOfElement(el);
|
|
12313
12841
|
const hasHeaders = $(el).find("th").length > 0;
|
|
12314
12842
|
const looksLikeLayout = !hasHeaders;
|
|
12315
12843
|
if (looksLikeLayout && role !== "presentation" && role !== "none") {
|
|
12316
12844
|
const nestedTables = $(el).find("table").length;
|
|
12317
12845
|
if (nestedTables > 0 || $(el).find("td").length > 2) {
|
|
12318
|
-
issues.push({
|
|
12846
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12319
12847
|
severity: "info",
|
|
12320
12848
|
rule: "table-missing-role",
|
|
12321
|
-
message: 'Layout table missing role="presentation"'
|
|
12849
|
+
message: 'Layout table missing role="presentation"'
|
|
12850
|
+
}, tableLoc ? { loc: tableLoc } : {}), {
|
|
12322
12851
|
element: `<table> with ${$(el).find("td").length} cells`,
|
|
12323
12852
|
details: `Add role="presentation" to tables used for layout so screen readers don't announce them as data tables.`
|
|
12324
|
-
});
|
|
12853
|
+
}));
|
|
12325
12854
|
}
|
|
12326
12855
|
}
|
|
12327
12856
|
});
|
|
@@ -12332,6 +12861,7 @@ function checkTextSizeAndContrast($) {
|
|
|
12332
12861
|
let smallTextCount = 0;
|
|
12333
12862
|
$("[style]").each((_, el) => {
|
|
12334
12863
|
const style = $(el).attr("style") || "";
|
|
12864
|
+
const styleLoc = locOfAttr(el, "style");
|
|
12335
12865
|
const fontSizeMatch = style.match(/font-size\s*:\s*(\d+(?:\.\d+)?)(px|pt)/i);
|
|
12336
12866
|
if (fontSizeMatch) {
|
|
12337
12867
|
const size = parseFloat(fontSizeMatch[1]);
|
|
@@ -12340,13 +12870,14 @@ function checkTextSizeAndContrast($) {
|
|
|
12340
12870
|
if (pxSize < 9 && pxSize > 0) {
|
|
12341
12871
|
smallTextCount++;
|
|
12342
12872
|
if (smallTextCount <= 3) {
|
|
12343
|
-
issues.push({
|
|
12873
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12344
12874
|
severity: "warning",
|
|
12345
12875
|
rule: "small-text",
|
|
12346
12876
|
message: `Very small text (${fontSizeMatch[0].trim()})`,
|
|
12347
|
-
element: describeElement($, el)
|
|
12877
|
+
element: describeElement($, el)
|
|
12878
|
+
}, styleLoc ? { loc: styleLoc } : {}), {
|
|
12348
12879
|
details: "Text smaller than 9px is difficult to read, especially on mobile devices."
|
|
12349
|
-
});
|
|
12880
|
+
}));
|
|
12350
12881
|
}
|
|
12351
12882
|
}
|
|
12352
12883
|
}
|
|
@@ -12391,21 +12922,23 @@ function checkTextSizeAndContrast($) {
|
|
|
12391
12922
|
}
|
|
12392
12923
|
const grade = wcagGrade(ratio);
|
|
12393
12924
|
if (grade === "Fail") {
|
|
12394
|
-
issues.push({
|
|
12925
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12395
12926
|
severity: "error",
|
|
12396
12927
|
rule: "low-contrast",
|
|
12397
12928
|
message: `Low contrast ratio ${ratio.toFixed(1)}:1 \u2014 fails WCAG minimum`,
|
|
12398
|
-
element: describeElement($, el)
|
|
12929
|
+
element: describeElement($, el)
|
|
12930
|
+
}, styleLoc ? { loc: styleLoc } : {}), {
|
|
12399
12931
|
details: `Foreground ${colorValue} on background needs at least ${isLargeText ? "3:1" : "4.5:1"} contrast ratio.`
|
|
12400
|
-
});
|
|
12932
|
+
}));
|
|
12401
12933
|
} else if (!isLargeText && grade === "AA Large") {
|
|
12402
|
-
issues.push({
|
|
12934
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12403
12935
|
severity: "warning",
|
|
12404
12936
|
rule: "low-contrast",
|
|
12405
12937
|
message: `Low contrast ratio ${ratio.toFixed(1)}:1 \u2014 fails WCAG AA for normal text`,
|
|
12406
|
-
element: describeElement($, el)
|
|
12938
|
+
element: describeElement($, el)
|
|
12939
|
+
}, styleLoc ? { loc: styleLoc } : {}), {
|
|
12407
12940
|
details: `Foreground ${colorValue} on background needs at least 4.5:1 for normal-sized text.`
|
|
12408
|
-
});
|
|
12941
|
+
}));
|
|
12409
12942
|
}
|
|
12410
12943
|
}
|
|
12411
12944
|
}
|
|
@@ -12428,29 +12961,32 @@ function checkCharsetDeclaration($) {
|
|
|
12428
12961
|
const content = httpEquiv.attr("content") || "";
|
|
12429
12962
|
if (/charset\s*=/i.test(content)) return null;
|
|
12430
12963
|
}
|
|
12431
|
-
|
|
12964
|
+
const loc = locOfFirst($, "head");
|
|
12965
|
+
return __spreadProps(__spreadValues({
|
|
12432
12966
|
severity: "warning",
|
|
12433
12967
|
rule: "missing-charset",
|
|
12434
|
-
message: "Missing charset declaration"
|
|
12968
|
+
message: "Missing charset declaration"
|
|
12969
|
+
}, loc ? { loc } : {}), {
|
|
12435
12970
|
details: 'Add <meta charset="utf-8"> in <head> to prevent encoding issues across email clients.'
|
|
12436
|
-
};
|
|
12971
|
+
});
|
|
12437
12972
|
}
|
|
12438
12973
|
function checkSemanticStructure($) {
|
|
12439
12974
|
const issues = [];
|
|
12440
12975
|
const headings = [];
|
|
12441
12976
|
$("h1, h2, h3, h4, h5, h6").each((_, el) => {
|
|
12442
12977
|
const level = parseInt(el.tagName.replace(/h/i, ""), 10);
|
|
12443
|
-
headings.push({ level, text: $(el).text().trim().slice(0, 60) });
|
|
12978
|
+
headings.push({ level, text: $(el).text().trim().slice(0, 60), loc: locOfElement(el) });
|
|
12444
12979
|
});
|
|
12445
12980
|
for (let i = 1; i < headings.length; i++) {
|
|
12446
12981
|
const gap = headings[i].level - headings[i - 1].level;
|
|
12447
12982
|
if (gap > 1) {
|
|
12448
|
-
issues.push({
|
|
12983
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12449
12984
|
severity: "info",
|
|
12450
12985
|
rule: "heading-skip",
|
|
12451
|
-
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 } : {}), {
|
|
12452
12988
|
details: "Skipped heading levels can confuse screen readers. Use sequential heading levels."
|
|
12453
|
-
});
|
|
12989
|
+
}));
|
|
12454
12990
|
break;
|
|
12455
12991
|
}
|
|
12456
12992
|
}
|
|
@@ -12491,8 +13027,8 @@ function checkAccessibilityFromDom($) {
|
|
|
12491
13027
|
const score = Math.max(0, 100 - penalty);
|
|
12492
13028
|
return { score, issues };
|
|
12493
13029
|
}
|
|
12494
|
-
function checkAccessibility(html) {
|
|
12495
|
-
return fromHtml(html, EMPTY_ACCESSIBILITY, checkAccessibilityFromDom);
|
|
13030
|
+
function checkAccessibility(html, options) {
|
|
13031
|
+
return fromHtml(html, EMPTY_ACCESSIBILITY, checkAccessibilityFromDom, options);
|
|
12496
13032
|
}
|
|
12497
13033
|
|
|
12498
13034
|
// src/image-analyzer.ts
|
|
@@ -12539,6 +13075,8 @@ function analyzeImagesFromDom($) {
|
|
|
12539
13075
|
const height = (_c = img.attr("height")) != null ? _c : null;
|
|
12540
13076
|
const style = (img.attr("style") || "").toLowerCase();
|
|
12541
13077
|
const imgIssues = [];
|
|
13078
|
+
const elLoc = locOfElement(el);
|
|
13079
|
+
const srcLoc = src ? locOfAttr(el, "src") : elLoc;
|
|
12542
13080
|
const tracking = isTrackingPixel(img);
|
|
12543
13081
|
let dataUriBytes = 0;
|
|
12544
13082
|
if (src.startsWith("data:")) {
|
|
@@ -12562,59 +13100,59 @@ function analyzeImagesFromDom($) {
|
|
|
12562
13100
|
const hasStyleHeight = /height\s*:/.test(style);
|
|
12563
13101
|
if (!hasStyleWidth && !hasStyleHeight) {
|
|
12564
13102
|
imgIssues.push("missing-dimensions");
|
|
12565
|
-
issues.push({
|
|
13103
|
+
issues.push(__spreadValues({
|
|
12566
13104
|
rule: "missing-dimensions",
|
|
12567
13105
|
severity: "warning",
|
|
12568
13106
|
message: "Image missing width/height attributes \u2014 causes layout shifts and Outlook rendering issues.",
|
|
12569
13107
|
src: truncateSrc(src)
|
|
12570
|
-
});
|
|
13108
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12571
13109
|
}
|
|
12572
13110
|
}
|
|
12573
13111
|
if (dataUriBytes > DATA_URI_WARN_BYTES) {
|
|
12574
13112
|
const kb = Math.round(dataUriBytes / 1024);
|
|
12575
13113
|
imgIssues.push("large-data-uri");
|
|
12576
|
-
issues.push({
|
|
13114
|
+
issues.push(__spreadValues({
|
|
12577
13115
|
rule: "large-data-uri",
|
|
12578
13116
|
severity: "warning",
|
|
12579
13117
|
message: `Data URI is ${kb}KB \u2014 consider hosting the image externally to reduce email size.`,
|
|
12580
13118
|
src: truncateSrc(src)
|
|
12581
|
-
});
|
|
13119
|
+
}, srcLoc ? { loc: srcLoc } : {}));
|
|
12582
13120
|
}
|
|
12583
13121
|
if (alt === null) {
|
|
12584
13122
|
imgIssues.push("missing-alt");
|
|
12585
|
-
issues.push({
|
|
13123
|
+
issues.push(__spreadValues({
|
|
12586
13124
|
rule: "missing-alt",
|
|
12587
13125
|
severity: "warning",
|
|
12588
13126
|
message: "Image missing alt attribute \u2014 hurts deliverability and accessibility.",
|
|
12589
13127
|
src: truncateSrc(src)
|
|
12590
|
-
});
|
|
13128
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12591
13129
|
}
|
|
12592
13130
|
if (src.toLowerCase().endsWith(".webp") || src.includes("image/webp")) {
|
|
12593
13131
|
imgIssues.push("webp-format");
|
|
12594
|
-
issues.push({
|
|
13132
|
+
issues.push(__spreadValues({
|
|
12595
13133
|
rule: "webp-format",
|
|
12596
13134
|
severity: "info",
|
|
12597
13135
|
message: "WebP format detected \u2014 not supported by all email clients. Consider PNG or JPEG.",
|
|
12598
13136
|
src: truncateSrc(src)
|
|
12599
|
-
});
|
|
13137
|
+
}, srcLoc ? { loc: srcLoc } : {}));
|
|
12600
13138
|
}
|
|
12601
13139
|
if (src.toLowerCase().endsWith(".svg") || src.includes("image/svg")) {
|
|
12602
13140
|
imgIssues.push("svg-format");
|
|
12603
|
-
issues.push({
|
|
13141
|
+
issues.push(__spreadValues({
|
|
12604
13142
|
rule: "svg-format",
|
|
12605
13143
|
severity: "info",
|
|
12606
13144
|
message: "SVG format detected \u2014 not supported by most email clients. Use PNG instead.",
|
|
12607
13145
|
src: truncateSrc(src)
|
|
12608
|
-
});
|
|
13146
|
+
}, srcLoc ? { loc: srcLoc } : {}));
|
|
12609
13147
|
}
|
|
12610
13148
|
if (!style.includes("display:block") && !style.includes("display: block")) {
|
|
12611
13149
|
imgIssues.push("missing-display-block");
|
|
12612
|
-
issues.push({
|
|
13150
|
+
issues.push(__spreadValues({
|
|
12613
13151
|
rule: "missing-display-block",
|
|
12614
13152
|
severity: "info",
|
|
12615
13153
|
message: "Image without display:block \u2014 may cause unwanted gaps in Outlook.",
|
|
12616
13154
|
src: truncateSrc(src)
|
|
12617
|
-
});
|
|
13155
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12618
13156
|
}
|
|
12619
13157
|
images.push({
|
|
12620
13158
|
src: truncateSrc(src),
|
|
@@ -12652,8 +13190,8 @@ function analyzeImagesFromDom($) {
|
|
|
12652
13190
|
}
|
|
12653
13191
|
return { total: images.length, totalDataUriBytes, issues, images };
|
|
12654
13192
|
}
|
|
12655
|
-
function analyzeImages(html) {
|
|
12656
|
-
return fromHtml(html, EMPTY_IMAGES, analyzeImagesFromDom);
|
|
13193
|
+
function analyzeImages(html, options) {
|
|
13194
|
+
return fromHtml(html, EMPTY_IMAGES, analyzeImagesFromDom, options);
|
|
12657
13195
|
}
|
|
12658
13196
|
|
|
12659
13197
|
// src/inbox-preview.ts
|
|
@@ -12871,11 +13409,57 @@ function checkSize(html) {
|
|
|
12871
13409
|
return fromHtml(html, EMPTY_SIZE, checkSizeFromDom);
|
|
12872
13410
|
}
|
|
12873
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
|
+
|
|
12874
13431
|
// src/template-checker.ts
|
|
12875
|
-
function checkTemplateVariablesFromDom(
|
|
13432
|
+
function checkTemplateVariablesFromDom($, source) {
|
|
13433
|
+
var _a;
|
|
12876
13434
|
const issues = [];
|
|
12877
13435
|
const seen = /* @__PURE__ */ new Set();
|
|
12878
|
-
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("");
|
|
12879
13463
|
for (const [pattern, label] of TEMPLATE_VARIABLE_PATTERNS) {
|
|
12880
13464
|
pattern.lastIndex = 0;
|
|
12881
13465
|
let match;
|
|
@@ -12908,13 +13492,14 @@ function checkTemplateVariablesFromDom($) {
|
|
|
12908
13492
|
const key = `attr:${attr}:${variable}`;
|
|
12909
13493
|
if (seen.has(key)) continue;
|
|
12910
13494
|
seen.add(key);
|
|
12911
|
-
|
|
13495
|
+
const loc = locOfAttr(el, attr);
|
|
13496
|
+
issues.push(__spreadValues({
|
|
12912
13497
|
rule: "unresolved-variable",
|
|
12913
13498
|
severity: "error",
|
|
12914
13499
|
message: `Unresolved ${label} variable "${variable}" found in ${attr} attribute.`,
|
|
12915
13500
|
variable,
|
|
12916
13501
|
location: "attribute"
|
|
12917
|
-
});
|
|
13502
|
+
}, loc ? { loc } : {}));
|
|
12918
13503
|
}
|
|
12919
13504
|
}
|
|
12920
13505
|
}
|
|
@@ -12922,13 +13507,13 @@ function checkTemplateVariablesFromDom($) {
|
|
|
12922
13507
|
}
|
|
12923
13508
|
return { unresolvedCount: issues.length, issues };
|
|
12924
13509
|
}
|
|
12925
|
-
function
|
|
12926
|
-
|
|
12927
|
-
|
|
12928
|
-
|
|
12929
|
-
|
|
12930
|
-
|
|
12931
|
-
|
|
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
|
+
);
|
|
12932
13517
|
}
|
|
12933
13518
|
|
|
12934
13519
|
// src/overflow-checker.ts
|
|
@@ -12944,32 +13529,71 @@ function fixedPxWidth($el) {
|
|
|
12944
13529
|
function isFluid(style) {
|
|
12945
13530
|
return /max-width\s*:\s*100%/i.test(style) || /width\s*:\s*100%/i.test(style);
|
|
12946
13531
|
}
|
|
12947
|
-
function addWidthIssue(width, label, issues, seen) {
|
|
13532
|
+
function addWidthIssue(width, label, issues, seen, loc) {
|
|
12948
13533
|
const key = `w:${label}:${width}`;
|
|
12949
|
-
|
|
12950
|
-
|
|
12951
|
-
|
|
13534
|
+
const existing = seen.get(key);
|
|
13535
|
+
if (existing) {
|
|
13536
|
+
addOccurrence(existing, loc);
|
|
13537
|
+
return;
|
|
13538
|
+
}
|
|
13539
|
+
const issue = __spreadValues({
|
|
12952
13540
|
rule: "fixed-width-overflow",
|
|
12953
13541
|
severity: "warning",
|
|
12954
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.`,
|
|
12955
13543
|
detail: `Use width:100% with max-width:${EMAIL_MAX_WIDTH}px instead of a fixed width beyond the frame.`
|
|
12956
|
-
});
|
|
13544
|
+
}, loc ? { loc, locs: [loc] } : {});
|
|
13545
|
+
seen.set(key, issue);
|
|
13546
|
+
issues.push(issue);
|
|
12957
13547
|
}
|
|
12958
|
-
function
|
|
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);
|
|
13576
|
+
}
|
|
13577
|
+
function checkOverflowFromDom($, source) {
|
|
13578
|
+
var _a;
|
|
12959
13579
|
const issues = [];
|
|
12960
|
-
const seen = /* @__PURE__ */ new
|
|
13580
|
+
const seen = /* @__PURE__ */ new Map();
|
|
13581
|
+
const tokensSeen = /* @__PURE__ */ new Set();
|
|
12961
13582
|
$("[width], [style*='width']").each((_, el) => {
|
|
12962
13583
|
const $el = $(el);
|
|
12963
13584
|
const width = fixedPxWidth($el);
|
|
12964
13585
|
if (width === null || width <= EMAIL_MAX_WIDTH) return;
|
|
12965
13586
|
if (isFluid($el.attr("style") || "")) return;
|
|
12966
13587
|
const tag = (el.tagName || "element").toLowerCase();
|
|
12967
|
-
|
|
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"));
|
|
12968
13590
|
});
|
|
12969
13591
|
$("style").each((_, el) => {
|
|
13592
|
+
const cssText = $(el).text();
|
|
13593
|
+
const anchor = cssBlockAnchor(el, cssText, source);
|
|
12970
13594
|
let ast;
|
|
12971
13595
|
try {
|
|
12972
|
-
ast = csstree6.parse(
|
|
13596
|
+
ast = csstree6.parse(cssText, { positions: true });
|
|
12973
13597
|
} catch (e) {
|
|
12974
13598
|
return;
|
|
12975
13599
|
}
|
|
@@ -12979,13 +13603,17 @@ function checkOverflowFromDom($) {
|
|
|
12979
13603
|
if (node.type !== "Rule") return;
|
|
12980
13604
|
let widthPx = null;
|
|
12981
13605
|
let fluid = false;
|
|
13606
|
+
let widthLoc;
|
|
12982
13607
|
node.block.children.forEach((child) => {
|
|
12983
13608
|
if (child.type !== "Declaration") return;
|
|
12984
13609
|
const prop = child.property.toLowerCase();
|
|
12985
13610
|
const val = csstree6.generate(child.value);
|
|
12986
13611
|
if (prop === "width") {
|
|
12987
13612
|
const m = val.match(/^(\d+)px$/);
|
|
12988
|
-
if (m)
|
|
13613
|
+
if (m) {
|
|
13614
|
+
widthPx = parseInt(m[1], 10);
|
|
13615
|
+
widthLoc = locInCssBlock(anchor, child.loc);
|
|
13616
|
+
}
|
|
12989
13617
|
if (/\b100%/.test(val)) fluid = true;
|
|
12990
13618
|
} else if (prop === "max-width" && /\b100%/.test(val)) {
|
|
12991
13619
|
fluid = true;
|
|
@@ -12993,36 +13621,46 @@ function checkOverflowFromDom($) {
|
|
|
12993
13621
|
});
|
|
12994
13622
|
if (widthPx !== null && widthPx > EMAIL_MAX_WIDTH && !fluid) {
|
|
12995
13623
|
const selector = csstree6.generate(node.prelude).trim().slice(0, 40);
|
|
12996
|
-
addWidthIssue(widthPx, selector || "rule", issues, seen);
|
|
13624
|
+
addWidthIssue(widthPx, selector || "rule", issues, seen, widthLoc);
|
|
12997
13625
|
}
|
|
12998
13626
|
}
|
|
12999
13627
|
});
|
|
13000
13628
|
});
|
|
13001
13629
|
const usesWrapGuard = /overflow-wrap|word-break|word-wrap/i.test($.html());
|
|
13002
13630
|
if (!usesWrapGuard) {
|
|
13003
|
-
const
|
|
13631
|
+
const nodes = visibleTextNodes($);
|
|
13632
|
+
const starts = [];
|
|
13004
13633
|
let text = "";
|
|
13005
|
-
|
|
13006
|
-
|
|
13007
|
-
|
|
13008
|
-
|
|
13009
|
-
|
|
13010
|
-
for (const token of text.split(
|
|
13011
|
-
|
|
13012
|
-
|
|
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);
|
|
13013
13645
|
const preview = token.length > 50 ? `${token.slice(0, 50)}\u2026` : token;
|
|
13014
|
-
|
|
13646
|
+
const loc = locateInNodes(nodes, starts, start, token.length, source);
|
|
13647
|
+
issues.push(__spreadValues({
|
|
13015
13648
|
rule: "unbreakable-string",
|
|
13016
13649
|
severity: "warning",
|
|
13017
13650
|
message: `A ${token.length}-character unbroken string ("${preview}") can't wrap and will force horizontal scrolling on narrow screens.`,
|
|
13018
13651
|
detail: `Add overflow-wrap: anywhere (or word-break: break-word) to its container.`
|
|
13019
|
-
});
|
|
13652
|
+
}, loc ? { loc, locs: [loc] } : {}));
|
|
13020
13653
|
}
|
|
13021
13654
|
}
|
|
13022
13655
|
return { hasOverflow: issues.length > 0, issues };
|
|
13023
13656
|
}
|
|
13024
|
-
function checkOverflow(html) {
|
|
13025
|
-
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
|
+
);
|
|
13026
13664
|
}
|
|
13027
13665
|
|
|
13028
13666
|
// src/visual-checker.ts
|
|
@@ -13035,8 +13673,8 @@ function isSolidColor(value) {
|
|
|
13035
13673
|
return c !== null && c.a !== 0;
|
|
13036
13674
|
}
|
|
13037
13675
|
function firstColor(value) {
|
|
13038
|
-
const
|
|
13039
|
-
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) {
|
|
13040
13678
|
const lc = t.toLowerCase();
|
|
13041
13679
|
if (lc === "transparent") continue;
|
|
13042
13680
|
if (/^(?:linear|radial|conic|gradient|deg|turn|rad|grad|to|at|from|in|circle|ellipse|closest|farthest|side|corner|url)$/.test(lc)) continue;
|
|
@@ -13073,8 +13711,17 @@ function hasFontFallback(value) {
|
|
|
13073
13711
|
return WEB_SAFE_FONTS.has(t) || GENERIC_FONT_FAMILIES.has(t) || t.startsWith("-apple-system") || t === "blinkmacsystemfont";
|
|
13074
13712
|
});
|
|
13075
13713
|
}
|
|
13076
|
-
function
|
|
13077
|
-
|
|
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;
|
|
13078
13725
|
const combined = `${(_a = style.get("background-image")) != null ? _a : ""} ${(_b = style.get("background")) != null ? _b : ""}`;
|
|
13079
13726
|
const isGradient = GRADIENT_RE.test(combined);
|
|
13080
13727
|
const isImage = isGradient || /url\(/i.test(combined);
|
|
@@ -13082,30 +13729,40 @@ function inspectDeclarations(style, issues, seen) {
|
|
|
13082
13729
|
const stop = isGradient ? firstColor(combined) : null;
|
|
13083
13730
|
const fix = stop ? `background-color: ${stop};` : `background-color: <solid colour matching the image>;`;
|
|
13084
13731
|
const key = `bg:${fix}`;
|
|
13085
|
-
|
|
13086
|
-
|
|
13087
|
-
|
|
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({
|
|
13088
13738
|
rule: "missing-background-fallback",
|
|
13089
13739
|
severity: "warning",
|
|
13090
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.`,
|
|
13091
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.`,
|
|
13092
13742
|
fix
|
|
13093
|
-
});
|
|
13743
|
+
}, loc ? { loc, locs: [loc] } : {});
|
|
13744
|
+
seen.set(key, issue);
|
|
13745
|
+
issues.push(issue);
|
|
13094
13746
|
}
|
|
13095
13747
|
}
|
|
13096
13748
|
const font = style.get("font-family");
|
|
13097
13749
|
if (font && !CSS_WIDE_KEYWORDS.has(font.trim().toLowerCase()) && !hasFontFallback(font)) {
|
|
13098
13750
|
const fix = `font-family: ${font.trim()}, Arial, sans-serif;`;
|
|
13099
13751
|
const key = `font:${font.trim().toLowerCase()}`;
|
|
13100
|
-
|
|
13101
|
-
|
|
13102
|
-
|
|
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({
|
|
13103
13758
|
rule: "missing-font-fallback",
|
|
13104
13759
|
severity: "warning",
|
|
13105
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.`,
|
|
13106
13761
|
detail: `End the stack with a web-safe font and a generic family.`,
|
|
13107
13762
|
fix
|
|
13108
|
-
});
|
|
13763
|
+
}, loc ? { loc, locs: [loc] } : {});
|
|
13764
|
+
seen.set(key, issue);
|
|
13765
|
+
issues.push(issue);
|
|
13109
13766
|
}
|
|
13110
13767
|
}
|
|
13111
13768
|
}
|
|
@@ -13118,30 +13775,47 @@ function ruleToMap(node) {
|
|
|
13118
13775
|
});
|
|
13119
13776
|
return map;
|
|
13120
13777
|
}
|
|
13121
|
-
function checkVisualFromDom(
|
|
13778
|
+
function checkVisualFromDom($, source) {
|
|
13122
13779
|
const issues = [];
|
|
13123
|
-
const seen = /* @__PURE__ */ new
|
|
13780
|
+
const seen = /* @__PURE__ */ new Map();
|
|
13124
13781
|
$("[style]").each((_, el) => {
|
|
13125
|
-
|
|
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);
|
|
13126
13786
|
});
|
|
13127
13787
|
$("style").each((_, el) => {
|
|
13788
|
+
const cssText = $(el).text();
|
|
13789
|
+
const anchor = cssBlockAnchor(el, cssText, source);
|
|
13128
13790
|
let ast;
|
|
13129
13791
|
try {
|
|
13130
|
-
ast = csstree7.parse(
|
|
13792
|
+
ast = csstree7.parse(cssText, { positions: true });
|
|
13131
13793
|
} catch (e) {
|
|
13132
13794
|
return;
|
|
13133
13795
|
}
|
|
13134
13796
|
csstree7.walk(ast, {
|
|
13135
13797
|
visit: "Rule",
|
|
13136
13798
|
enter(node) {
|
|
13137
|
-
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);
|
|
13138
13807
|
}
|
|
13139
13808
|
});
|
|
13140
13809
|
});
|
|
13141
13810
|
return { issues };
|
|
13142
13811
|
}
|
|
13143
|
-
function checkVisual(html) {
|
|
13144
|
-
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
|
+
);
|
|
13145
13819
|
}
|
|
13146
13820
|
|
|
13147
13821
|
// src/audit.ts
|
|
@@ -13160,7 +13834,8 @@ var EMPTY_AUDIT = {
|
|
|
13160
13834
|
function runAudit($, html, framework, options) {
|
|
13161
13835
|
var _a;
|
|
13162
13836
|
const skip = new Set((_a = options == null ? void 0 : options.skip) != null ? _a : []);
|
|
13163
|
-
const
|
|
13837
|
+
const source = (options == null ? void 0 : options.positions) ? html : void 0;
|
|
13838
|
+
const warnings = skip.has("compatibility") ? [] : analyzeEmailFromDom($, framework, source);
|
|
13164
13839
|
const scores = skip.has("compatibility") ? {} : generateCompatibilityScore(warnings);
|
|
13165
13840
|
const spam = skip.has("spam") ? EMPTY_SPAM : analyzeSpamFromDom($, options == null ? void 0 : options.spam);
|
|
13166
13841
|
const links = skip.has("links") ? EMPTY_LINKS : validateLinksFromDom($);
|
|
@@ -13168,19 +13843,19 @@ function runAudit($, html, framework, options) {
|
|
|
13168
13843
|
const images = skip.has("images") ? EMPTY_IMAGES : analyzeImagesFromDom($);
|
|
13169
13844
|
const inboxPreview = skip.has("inboxPreview") ? EMPTY_INBOX_PREVIEW : extractInboxPreviewFromDom($);
|
|
13170
13845
|
const size = skip.has("size") ? EMPTY_SIZE : checkSizeFromDom($, html);
|
|
13171
|
-
const templateVariables = skip.has("templateVariables") ? EMPTY_TEMPLATE : checkTemplateVariablesFromDom(
|
|
13172
|
-
const overflow = skip.has("overflow") ? EMPTY_OVERFLOW : checkOverflowFromDom(
|
|
13173
|
-
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);
|
|
13174
13849
|
return { compatibility: { warnings, scores }, spam, links, accessibility, images, inboxPreview, size, templateVariables, overflow, visual };
|
|
13175
13850
|
}
|
|
13176
13851
|
function auditEmail(html, options) {
|
|
13177
|
-
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);
|
|
13178
13853
|
}
|
|
13179
13854
|
|
|
13180
13855
|
// src/plain-text.ts
|
|
13181
|
-
var
|
|
13856
|
+
var cheerio5 = __toESM(require("cheerio"), 1);
|
|
13182
13857
|
function toPlainText(html) {
|
|
13183
|
-
const $ =
|
|
13858
|
+
const $ = cheerio5.load(html);
|
|
13184
13859
|
$("style, script, head").remove();
|
|
13185
13860
|
$("[data-skip-in-text='true']").remove();
|
|
13186
13861
|
const lines = [];
|
|
@@ -13282,7 +13957,6 @@ function toPlainText(html) {
|
|
|
13282
13957
|
}
|
|
13283
13958
|
|
|
13284
13959
|
// src/session.ts
|
|
13285
|
-
var cheerio7 = __toESM(require("cheerio"), 1);
|
|
13286
13960
|
function createSession(html, options) {
|
|
13287
13961
|
if (!html || !html.trim()) {
|
|
13288
13962
|
const fw = options == null ? void 0 : options.framework;
|
|
@@ -13309,16 +13983,17 @@ function createSession(html, options) {
|
|
|
13309
13983
|
if (html.length > MAX_HTML_SIZE) {
|
|
13310
13984
|
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
13311
13985
|
}
|
|
13312
|
-
const $ =
|
|
13986
|
+
const $ = loadHtml(html, options);
|
|
13313
13987
|
const framework = options == null ? void 0 : options.framework;
|
|
13988
|
+
const source = (options == null ? void 0 : options.positions) ? html : void 0;
|
|
13314
13989
|
return {
|
|
13315
13990
|
html,
|
|
13316
13991
|
framework,
|
|
13317
13992
|
audit(opts) {
|
|
13318
|
-
return runAudit($, html, framework, opts);
|
|
13993
|
+
return runAudit($, html, framework, __spreadProps(__spreadValues({}, opts), { positions: options == null ? void 0 : options.positions }));
|
|
13319
13994
|
},
|
|
13320
13995
|
analyze() {
|
|
13321
|
-
return analyzeEmailFromDom($, framework);
|
|
13996
|
+
return analyzeEmailFromDom($, framework, source);
|
|
13322
13997
|
},
|
|
13323
13998
|
score(warnings) {
|
|
13324
13999
|
return generateCompatibilityScore(warnings);
|
|
@@ -13342,13 +14017,13 @@ function createSession(html, options) {
|
|
|
13342
14017
|
return checkSizeFromDom($, html);
|
|
13343
14018
|
},
|
|
13344
14019
|
checkTemplateVariables() {
|
|
13345
|
-
return checkTemplateVariablesFromDom(
|
|
14020
|
+
return checkTemplateVariablesFromDom($, source);
|
|
13346
14021
|
},
|
|
13347
14022
|
checkOverflow() {
|
|
13348
|
-
return checkOverflowFromDom(
|
|
14023
|
+
return checkOverflowFromDom($, source);
|
|
13349
14024
|
},
|
|
13350
14025
|
checkVisual() {
|
|
13351
|
-
return checkVisualFromDom(
|
|
14026
|
+
return checkVisualFromDom($, source);
|
|
13352
14027
|
},
|
|
13353
14028
|
// Transforms create isolated copies since they mutate the DOM
|
|
13354
14029
|
transformForClient(clientId) {
|
|
@@ -13379,18 +14054,22 @@ var CompileError = class extends Error {
|
|
|
13379
14054
|
COMPOUND_VALUE_FEATURES,
|
|
13380
14055
|
CSS_FUNCTION_FEATURES,
|
|
13381
14056
|
CSS_SUPPORT,
|
|
14057
|
+
CSS_SUPPORT_NOTES,
|
|
13382
14058
|
CompileError,
|
|
13383
14059
|
EMAIL_CLIENTS,
|
|
13384
14060
|
EMPTY_DELIVERABILITY,
|
|
13385
14061
|
GENERIC_LINK_TEXT,
|
|
13386
14062
|
HTML_ELEMENT_FEATURES,
|
|
13387
14063
|
MAX_HTML_SIZE,
|
|
14064
|
+
MAX_WARNING_LOCATIONS,
|
|
13388
14065
|
STRUCTURAL_FIX_PROPERTIES,
|
|
14066
|
+
VALUE_CAVEAT_PROPS,
|
|
13389
14067
|
alphaBlend,
|
|
13390
14068
|
analyzeEmail,
|
|
13391
14069
|
analyzeImages,
|
|
13392
14070
|
analyzeSpam,
|
|
13393
14071
|
auditEmail,
|
|
14072
|
+
caveatApplies,
|
|
13394
14073
|
checkAccessibility,
|
|
13395
14074
|
checkOverflow,
|
|
13396
14075
|
checkSize,
|