@emailens/engine 0.10.1 → 0.10.3
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 +985 -230
- 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 +981 -230
- 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,257 @@ 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 locInAttr(attrLoc, source, property, occurrence = 0) {
|
|
11025
|
+
if (!attrLoc || !source) return void 0;
|
|
11026
|
+
const raw = source.slice(attrLoc.offset, attrLoc.offset + attrLoc.length);
|
|
11027
|
+
const open = raw.search(/["']/);
|
|
11028
|
+
const close = raw.lastIndexOf(raw[open]);
|
|
11029
|
+
if (open === -1 || close <= open) return void 0;
|
|
11030
|
+
const found = declarationsIn(raw.slice(open + 1, close), property);
|
|
11031
|
+
const hit = found[occurrence];
|
|
11032
|
+
if (!hit) return void 0;
|
|
11033
|
+
const start = attrLoc.offset + open + 1 + hit.start;
|
|
11034
|
+
const end = attrLoc.offset + open + 1 + hit.end;
|
|
11035
|
+
const from = positionOf(source, start);
|
|
11036
|
+
const to = positionOf(source, end);
|
|
11037
|
+
return {
|
|
11038
|
+
line: from.line,
|
|
11039
|
+
column: from.column,
|
|
11040
|
+
endLine: to.line,
|
|
11041
|
+
endColumn: to.column,
|
|
11042
|
+
offset: start,
|
|
11043
|
+
length: end - start
|
|
11044
|
+
};
|
|
11045
|
+
}
|
|
11046
|
+
function declarationsIn(value, property) {
|
|
11047
|
+
const wanted = property.toLowerCase();
|
|
11048
|
+
const found = [];
|
|
11049
|
+
let depth = 0;
|
|
11050
|
+
let start = 0;
|
|
11051
|
+
const consider = (from, to) => {
|
|
11052
|
+
const text = value.slice(from, to);
|
|
11053
|
+
const colon = text.indexOf(":");
|
|
11054
|
+
if (colon === -1) return;
|
|
11055
|
+
if (text.slice(0, colon).trim().toLowerCase() !== wanted) return;
|
|
11056
|
+
const lead = text.length - text.trimStart().length;
|
|
11057
|
+
const trail = text.length - text.trimEnd().length;
|
|
11058
|
+
if (from + lead < to - trail) found.push({ start: from + lead, end: to - trail });
|
|
11059
|
+
};
|
|
11060
|
+
for (let i = 0; i < value.length; i++) {
|
|
11061
|
+
const c = value[i];
|
|
11062
|
+
if (c === "(") depth++;
|
|
11063
|
+
else if (c === ")") depth = Math.max(0, depth - 1);
|
|
11064
|
+
else if (c === ";" && depth === 0) {
|
|
11065
|
+
consider(start, i);
|
|
11066
|
+
start = i + 1;
|
|
11067
|
+
}
|
|
11068
|
+
}
|
|
11069
|
+
consider(start, value.length);
|
|
11070
|
+
return found;
|
|
11071
|
+
}
|
|
11072
|
+
function locOfFirst($, selector) {
|
|
11073
|
+
const el = $(selector).first()[0];
|
|
11074
|
+
return el ? locOfElement(el) : void 0;
|
|
11075
|
+
}
|
|
11076
|
+
function cssBlockAnchor(styleEl, cssText, source) {
|
|
11077
|
+
var _a;
|
|
11078
|
+
const children = styleEl == null ? void 0 : styleEl.children;
|
|
11079
|
+
if (!children || children.length !== 1) return void 0;
|
|
11080
|
+
const loc = (_a = children[0]) == null ? void 0 : _a.sourceCodeLocation;
|
|
11081
|
+
if (!loc) return void 0;
|
|
11082
|
+
const mapper = source ? crMapper(source.slice(loc.startOffset, loc.endOffset), cssText) : null;
|
|
11083
|
+
const extraBefore = mapper ? (index) => mapper(index) - index : crOffsetter(cssText, loc.endOffset - loc.startOffset);
|
|
11084
|
+
return __spreadValues({ loc, extraBefore }, mapper ? { source } : {});
|
|
11085
|
+
}
|
|
11086
|
+
function crMapper(raw, decoded) {
|
|
11087
|
+
if (raw.length === decoded.length) return (index) => index;
|
|
11088
|
+
const points = [];
|
|
11089
|
+
const extras = [];
|
|
11090
|
+
let r = 0;
|
|
11091
|
+
let d = 0;
|
|
11092
|
+
let extra = 0;
|
|
11093
|
+
while (d < decoded.length) {
|
|
11094
|
+
if (r >= raw.length) return null;
|
|
11095
|
+
if (raw[r] === decoded[d]) {
|
|
11096
|
+
r++;
|
|
11097
|
+
d++;
|
|
11098
|
+
continue;
|
|
11099
|
+
}
|
|
11100
|
+
if (raw[r] === "\r" && decoded[d] === "\n") {
|
|
11101
|
+
const consumed = raw[r + 1] === "\n" ? 2 : 1;
|
|
11102
|
+
extra += consumed - 1;
|
|
11103
|
+
points.push(d);
|
|
11104
|
+
extras.push(extra);
|
|
11105
|
+
r += consumed;
|
|
11106
|
+
d += 1;
|
|
11107
|
+
continue;
|
|
11108
|
+
}
|
|
11109
|
+
return null;
|
|
11110
|
+
}
|
|
11111
|
+
if (r !== raw.length) return null;
|
|
11112
|
+
return (index) => index + lookup(points, extras, index);
|
|
11113
|
+
}
|
|
11114
|
+
function lookup(points, extras, index) {
|
|
11115
|
+
let lo = 0;
|
|
11116
|
+
let hi = points.length - 1;
|
|
11117
|
+
let found = 0;
|
|
11118
|
+
while (lo <= hi) {
|
|
11119
|
+
const mid = lo + hi >> 1;
|
|
11120
|
+
if (points[mid] < index) {
|
|
11121
|
+
found = extras[mid];
|
|
11122
|
+
lo = mid + 1;
|
|
11123
|
+
} else {
|
|
11124
|
+
hi = mid - 1;
|
|
11125
|
+
}
|
|
11126
|
+
}
|
|
11127
|
+
return found;
|
|
11128
|
+
}
|
|
11129
|
+
function findRawOffset(raw, decoded, index, token) {
|
|
11130
|
+
if (!token) return -1;
|
|
11131
|
+
let occurrence = 0;
|
|
11132
|
+
for (let at = decoded.indexOf(token); at !== -1 && at < index; at = decoded.indexOf(token, at + 1)) {
|
|
11133
|
+
occurrence++;
|
|
11134
|
+
}
|
|
11135
|
+
let found = -1;
|
|
11136
|
+
let from = 0;
|
|
11137
|
+
for (let i = 0; i <= occurrence; i++) {
|
|
11138
|
+
found = raw.indexOf(token, from);
|
|
11139
|
+
if (found === -1) return -1;
|
|
11140
|
+
from = found + 1;
|
|
11141
|
+
}
|
|
11142
|
+
return found;
|
|
11143
|
+
}
|
|
11144
|
+
function positionOf(source, offset) {
|
|
11145
|
+
const prefix = source.slice(0, offset);
|
|
11146
|
+
return { line: prefix.split("\n").length, column: offset - prefix.lastIndexOf("\n") };
|
|
11147
|
+
}
|
|
11148
|
+
function crOffsetter(text, rawLength) {
|
|
11149
|
+
const removed = rawLength - text.length;
|
|
11150
|
+
if (removed === 0) return () => 0;
|
|
11151
|
+
const newlines = countNewlines(text, text.length);
|
|
11152
|
+
if (removed !== newlines || newlines === 0) return null;
|
|
11153
|
+
return (index) => countNewlines(text, index);
|
|
11154
|
+
}
|
|
11155
|
+
function countNewlines(text, upTo) {
|
|
11156
|
+
let n = 0;
|
|
11157
|
+
for (let i = 0; i < upTo && i < text.length; i++) if (text.charCodeAt(i) === 10) n++;
|
|
11158
|
+
return n;
|
|
11159
|
+
}
|
|
11160
|
+
function locInCssBlock(anchor, cssLoc) {
|
|
11161
|
+
if (!anchor || !cssLoc) return void 0;
|
|
11162
|
+
const { loc: block, extraBefore } = anchor;
|
|
11163
|
+
if (!extraBefore) {
|
|
11164
|
+
return {
|
|
11165
|
+
line: block.startLine,
|
|
11166
|
+
column: block.startCol,
|
|
11167
|
+
endLine: block.startLine,
|
|
11168
|
+
endColumn: block.startCol,
|
|
11169
|
+
offset: block.startOffset,
|
|
11170
|
+
length: 0
|
|
11171
|
+
};
|
|
11172
|
+
}
|
|
11173
|
+
const line = block.startLine + cssLoc.start.line - 1;
|
|
11174
|
+
const column = cssLoc.start.line === 1 ? block.startCol + cssLoc.start.column - 1 : cssLoc.start.column;
|
|
11175
|
+
const endLine = block.startLine + cssLoc.end.line - 1;
|
|
11176
|
+
const endColumn = cssLoc.end.line === 1 ? block.startCol + cssLoc.end.column - 1 : cssLoc.end.column;
|
|
11177
|
+
const start = block.startOffset + cssLoc.start.offset + extraBefore(cssLoc.start.offset);
|
|
11178
|
+
const end = block.startOffset + cssLoc.end.offset + extraBefore(cssLoc.end.offset);
|
|
11179
|
+
if (anchor.source) {
|
|
11180
|
+
const from = positionOf(anchor.source, start);
|
|
11181
|
+
const to = positionOf(anchor.source, end);
|
|
11182
|
+
return {
|
|
11183
|
+
line: from.line,
|
|
11184
|
+
column: from.column,
|
|
11185
|
+
endLine: to.line,
|
|
11186
|
+
endColumn: to.column,
|
|
11187
|
+
offset: start,
|
|
11188
|
+
length: end - start
|
|
11189
|
+
};
|
|
11190
|
+
}
|
|
11191
|
+
return { line, column, endLine, endColumn, offset: start, length: end - start };
|
|
11192
|
+
}
|
|
11193
|
+
function locInTextNode(node, index, length, source) {
|
|
11194
|
+
var _a;
|
|
11195
|
+
const anchor = node == null ? void 0 : node.sourceCodeLocation;
|
|
11196
|
+
if (!anchor) return void 0;
|
|
11197
|
+
const data = (_a = node.data) != null ? _a : "";
|
|
11198
|
+
const rawLength = anchor.endOffset - anchor.startOffset;
|
|
11199
|
+
if (source) {
|
|
11200
|
+
const raw = source.slice(anchor.startOffset, anchor.endOffset);
|
|
11201
|
+
const token = data.slice(index, index + length);
|
|
11202
|
+
const at = findRawOffset(raw, data, index, token);
|
|
11203
|
+
if (at !== -1) {
|
|
11204
|
+
const start2 = anchor.startOffset + at;
|
|
11205
|
+
const from = positionOf(source, start2);
|
|
11206
|
+
const to = positionOf(source, start2 + token.length);
|
|
11207
|
+
return {
|
|
11208
|
+
line: from.line,
|
|
11209
|
+
column: from.column,
|
|
11210
|
+
endLine: to.line,
|
|
11211
|
+
endColumn: to.column,
|
|
11212
|
+
offset: start2,
|
|
11213
|
+
length: token.length
|
|
11214
|
+
};
|
|
11215
|
+
}
|
|
11216
|
+
}
|
|
11217
|
+
const extraBefore = crOffsetter(data, rawLength);
|
|
11218
|
+
if (!extraBefore) {
|
|
11219
|
+
const clamped = Math.min(length, rawLength);
|
|
11220
|
+
return {
|
|
11221
|
+
line: anchor.startLine,
|
|
11222
|
+
column: anchor.startCol,
|
|
11223
|
+
endLine: anchor.startLine,
|
|
11224
|
+
endColumn: anchor.startCol + clamped,
|
|
11225
|
+
offset: anchor.startOffset,
|
|
11226
|
+
length: clamped
|
|
11227
|
+
};
|
|
11228
|
+
}
|
|
11229
|
+
const start = positionAt(data, index, anchor);
|
|
11230
|
+
const end = positionAt(data, index + length, anchor);
|
|
11231
|
+
const startOffset = anchor.startOffset + index + extraBefore(index);
|
|
11232
|
+
const endOffset = anchor.startOffset + index + length + extraBefore(index + length);
|
|
11233
|
+
return {
|
|
11234
|
+
line: start.line,
|
|
11235
|
+
column: start.column,
|
|
11236
|
+
endLine: end.line,
|
|
11237
|
+
endColumn: end.column,
|
|
11238
|
+
offset: startOffset,
|
|
11239
|
+
length: endOffset - startOffset
|
|
11240
|
+
};
|
|
11241
|
+
}
|
|
11242
|
+
function positionAt(data, index, anchor) {
|
|
11243
|
+
const prefix = data.slice(0, index);
|
|
11244
|
+
const newlines = prefix.split("\n").length - 1;
|
|
11245
|
+
if (newlines === 0) {
|
|
11246
|
+
return { line: anchor.startLine, column: anchor.startCol + index };
|
|
11247
|
+
}
|
|
11248
|
+
return { line: anchor.startLine + newlines, column: index - prefix.lastIndexOf("\n") };
|
|
11249
|
+
}
|
|
11250
|
+
|
|
10759
11251
|
// src/dark-mode-checker.ts
|
|
10760
11252
|
var DARK_MEDIA_RE = /\(\s*prefers-color-scheme\s*:\s*dark\s*\)/i;
|
|
10761
11253
|
var MAX_UNCOVERED_ELEMENTS = 3;
|
|
@@ -10842,7 +11334,7 @@ function describeSelector($, el) {
|
|
|
10842
11334
|
if (cls) return `${tag}.${cls.split(/\s+/)[0]}`;
|
|
10843
11335
|
return tag;
|
|
10844
11336
|
}
|
|
10845
|
-
function checkDarkModeFromDom(
|
|
11337
|
+
function checkDarkModeFromDom($, source) {
|
|
10846
11338
|
var _a, _b;
|
|
10847
11339
|
const darkBlock = collectDarkBlocks($);
|
|
10848
11340
|
if (!darkBlock) return [];
|
|
@@ -10852,15 +11344,16 @@ function checkDarkModeFromDom($) {
|
|
|
10852
11344
|
return name === "color-scheme" || name === "supported-color-schemes";
|
|
10853
11345
|
});
|
|
10854
11346
|
if (!hasOptIn) {
|
|
11347
|
+
const headLoc = locOfFirst($, "head");
|
|
10855
11348
|
for (const clientId of PREFERS_COLOR_SCHEME_CLIENTS) {
|
|
10856
|
-
warnings.push({
|
|
11349
|
+
warnings.push(__spreadValues({
|
|
10857
11350
|
severity: "warning",
|
|
10858
11351
|
client: clientId,
|
|
10859
11352
|
property: "dark-mode-opt-in",
|
|
10860
11353
|
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
11354
|
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
11355
|
fixType: "structural"
|
|
10863
|
-
});
|
|
11356
|
+
}, headLoc ? { loc: headLoc, locs: [headLoc] } : {}));
|
|
10864
11357
|
}
|
|
10865
11358
|
}
|
|
10866
11359
|
if (darkBlock.rules === 0) return warnings;
|
|
@@ -10868,7 +11361,7 @@ function checkDarkModeFromDom($) {
|
|
|
10868
11361
|
const coveredByAny = matchedElements($, darkBlock.any);
|
|
10869
11362
|
let uncovered = 0;
|
|
10870
11363
|
$("[bgcolor], [style]").each((_, el) => {
|
|
10871
|
-
var _a2;
|
|
11364
|
+
var _a2, _b2;
|
|
10872
11365
|
if (uncovered >= MAX_UNCOVERED_ELEMENTS) return false;
|
|
10873
11366
|
const $el = $(el);
|
|
10874
11367
|
const style = parseInlineStyle($el.attr("style") || "");
|
|
@@ -10878,8 +11371,10 @@ function checkDarkModeFromDom($) {
|
|
|
10878
11371
|
if (inline ? coveredByImportant.has(el) : coveredByAny.has(el)) return;
|
|
10879
11372
|
uncovered++;
|
|
10880
11373
|
const selector = describeSelector($, el);
|
|
11374
|
+
const attrLoc = locOfAttr(el, inline ? "style" : "bgcolor");
|
|
11375
|
+
const loc = inline ? (_b2 = locInAttr(attrLoc, source, style.get("background-color") !== void 0 ? "background-color" : "background")) != null ? _b2 : attrLoc : attrLoc;
|
|
10881
11376
|
for (const clientId of PREFERS_COLOR_SCHEME_CLIENTS) {
|
|
10882
|
-
warnings.push({
|
|
11377
|
+
warnings.push(__spreadValues({
|
|
10883
11378
|
severity: "warning",
|
|
10884
11379
|
client: clientId,
|
|
10885
11380
|
property: "dark-mode-coverage",
|
|
@@ -10887,12 +11382,25 @@ function checkDarkModeFromDom($) {
|
|
|
10887
11382
|
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
11383
|
fixType: "css",
|
|
10889
11384
|
selector
|
|
10890
|
-
});
|
|
11385
|
+
}, loc ? { loc, locs: [loc] } : {}));
|
|
10891
11386
|
}
|
|
10892
11387
|
});
|
|
10893
11388
|
return warnings;
|
|
10894
11389
|
}
|
|
10895
11390
|
|
|
11391
|
+
// src/parse-html.ts
|
|
11392
|
+
var cheerio4 = __toESM(require("cheerio"), 1);
|
|
11393
|
+
function loadHtml(html, options) {
|
|
11394
|
+
return (options == null ? void 0 : options.positions) ? cheerio4.load(html, { sourceCodeLocationInfo: true }) : cheerio4.load(html);
|
|
11395
|
+
}
|
|
11396
|
+
function fromHtml(html, empty, fn, options) {
|
|
11397
|
+
if (!html || !html.trim()) return empty;
|
|
11398
|
+
if (html.length > MAX_HTML_SIZE) {
|
|
11399
|
+
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
11400
|
+
}
|
|
11401
|
+
return fn(loadHtml(html, options), html);
|
|
11402
|
+
}
|
|
11403
|
+
|
|
10896
11404
|
// src/analyze.ts
|
|
10897
11405
|
var HTML_ELEMENT_SELECTORS = {
|
|
10898
11406
|
"<style>": "style",
|
|
@@ -10945,14 +11453,25 @@ var CSS_FUNCTION_DETECTORS = CSS_FUNCTION_FEATURES.map((fn) => ({
|
|
|
10945
11453
|
pattern: `${fn}(`
|
|
10946
11454
|
// require opening paren — matches "min(" but not "Minion"
|
|
10947
11455
|
}));
|
|
10948
|
-
function analyzeEmailFromDom($, framework) {
|
|
11456
|
+
function analyzeEmailFromDom($, framework, source) {
|
|
10949
11457
|
const warnings = [];
|
|
10950
|
-
const seenWarnings = /* @__PURE__ */ new
|
|
11458
|
+
const seenWarnings = /* @__PURE__ */ new Map();
|
|
10951
11459
|
function addWarning(w) {
|
|
10952
11460
|
const key = `${w.client}:${w.property}:${w.severity}:${w.selector || ""}`;
|
|
10953
|
-
|
|
10954
|
-
|
|
11461
|
+
const existing = seenWarnings.get(key);
|
|
11462
|
+
if (!existing) {
|
|
11463
|
+
seenWarnings.set(key, w);
|
|
10955
11464
|
warnings.push(w);
|
|
11465
|
+
return;
|
|
11466
|
+
}
|
|
11467
|
+
if (!existing.locs || !w.locs) return;
|
|
11468
|
+
for (const loc of w.locs) {
|
|
11469
|
+
if (existing.locs.some((l) => l.offset === loc.offset)) continue;
|
|
11470
|
+
if (existing.locs.length >= MAX_WARNING_LOCATIONS) {
|
|
11471
|
+
existing.locsTruncated = true;
|
|
11472
|
+
break;
|
|
11473
|
+
}
|
|
11474
|
+
existing.locs.push(loc);
|
|
10956
11475
|
}
|
|
10957
11476
|
}
|
|
10958
11477
|
function describeSelector2(el) {
|
|
@@ -10970,10 +11489,16 @@ function analyzeEmailFromDom($, framework) {
|
|
|
10970
11489
|
for (const feature of HTML_ELEMENT_FEATURES) {
|
|
10971
11490
|
const selector = HTML_ELEMENT_SELECTORS[feature];
|
|
10972
11491
|
if (!selector) continue;
|
|
10973
|
-
|
|
11492
|
+
const matches = $(selector);
|
|
11493
|
+
if (matches.length === 0) continue;
|
|
10974
11494
|
const supportData = CSS_SUPPORT[feature];
|
|
10975
11495
|
if (!supportData) continue;
|
|
10976
11496
|
const baseSeverity = HTML_ELEMENT_SEVERITY[feature] || "warning";
|
|
11497
|
+
const found = matches.toArray().map((m) => locOfElement(m)).filter((l) => l !== void 0);
|
|
11498
|
+
const featureOccurrences = found.length ? __spreadValues({
|
|
11499
|
+
locs: found.slice(0, MAX_WARNING_LOCATIONS)
|
|
11500
|
+
}, found.length > MAX_WARNING_LOCATIONS ? { truncated: true } : {}) : void 0;
|
|
11501
|
+
const featureLoc = featureOccurrences == null ? void 0 : featureOccurrences.locs[0];
|
|
10977
11502
|
for (const client of EMAIL_CLIENTS) {
|
|
10978
11503
|
const support = supportData[client.id];
|
|
10979
11504
|
if (support === "unsupported") {
|
|
@@ -10981,7 +11506,7 @@ function analyzeEmailFromDom($, framework) {
|
|
|
10981
11506
|
const message = msgFn ? msgFn(client.name) : `${client.name} does not support ${feature}.`;
|
|
10982
11507
|
const sug = getSuggestion(feature, client.id, framework);
|
|
10983
11508
|
const fix = getCodeFix(feature, client.id, framework);
|
|
10984
|
-
addWarning(__spreadValues({
|
|
11509
|
+
addWarning(__spreadValues(__spreadValues({
|
|
10985
11510
|
severity: baseSeverity,
|
|
10986
11511
|
client: client.id,
|
|
10987
11512
|
property: feature,
|
|
@@ -10989,11 +11514,11 @@ function analyzeEmailFromDom($, framework) {
|
|
|
10989
11514
|
suggestion: sug.text,
|
|
10990
11515
|
fix,
|
|
10991
11516
|
fixType: getFixType(feature)
|
|
10992
|
-
}, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(feature, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
11517
|
+
}, featureOccurrences ? occurrenceFields(featureOccurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(feature, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
10993
11518
|
} else if (support === "partial" && feature === "<style>") {
|
|
10994
11519
|
const sug = getSuggestion("<style>:partial", client.id, framework);
|
|
10995
11520
|
const fix = getCodeFix("<style>", client.id, framework);
|
|
10996
|
-
addWarning(__spreadValues({
|
|
11521
|
+
addWarning(__spreadValues(__spreadValues({
|
|
10997
11522
|
severity: "warning",
|
|
10998
11523
|
client: client.id,
|
|
10999
11524
|
property: "<style>",
|
|
@@ -11001,55 +11526,97 @@ function analyzeEmailFromDom($, framework) {
|
|
|
11001
11526
|
suggestion: sug.text,
|
|
11002
11527
|
fix,
|
|
11003
11528
|
fixType: getFixType("<style>")
|
|
11004
|
-
}, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<style>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
11529
|
+
}, featureOccurrences ? occurrenceFields(featureOccurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<style>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
11005
11530
|
}
|
|
11006
11531
|
}
|
|
11007
11532
|
}
|
|
11008
11533
|
const parsedAtRules = /* @__PURE__ */ new Set();
|
|
11534
|
+
const selectorLocs = /* @__PURE__ */ new Map();
|
|
11009
11535
|
const parsedProperties = /* @__PURE__ */ new Set();
|
|
11010
11536
|
const propertyLines = /* @__PURE__ */ new Map();
|
|
11537
|
+
const propertyLocs = /* @__PURE__ */ new Map();
|
|
11011
11538
|
const propertyValues = /* @__PURE__ */ new Map();
|
|
11012
11539
|
const detectedCssFunctions = /* @__PURE__ */ new Set();
|
|
11013
11540
|
const detectedPseudoClasses = /* @__PURE__ */ new Set();
|
|
11014
11541
|
const detectedPseudoElements = /* @__PURE__ */ new Set();
|
|
11542
|
+
let blockAnchor;
|
|
11543
|
+
function recordSelectorLoc(key, cssLoc) {
|
|
11544
|
+
if (!cssLoc) return;
|
|
11545
|
+
const loc = locInCssBlock(blockAnchor, cssLoc);
|
|
11546
|
+
if (!loc) return;
|
|
11547
|
+
const seen = selectorLocs.get(key);
|
|
11548
|
+
if (!seen) {
|
|
11549
|
+
selectorLocs.set(key, { locs: [loc] });
|
|
11550
|
+
return;
|
|
11551
|
+
}
|
|
11552
|
+
if (seen.locs.some((l) => l.offset === loc.offset)) return;
|
|
11553
|
+
if (seen.locs.length >= MAX_WARNING_LOCATIONS) {
|
|
11554
|
+
seen.truncated = true;
|
|
11555
|
+
return;
|
|
11556
|
+
}
|
|
11557
|
+
seen.locs.push(loc);
|
|
11558
|
+
}
|
|
11559
|
+
function recordLoc(key, cssLoc, value) {
|
|
11560
|
+
const loc = locInCssBlock(blockAnchor, cssLoc);
|
|
11561
|
+
if (!loc) return;
|
|
11562
|
+
const seen = propertyLocs.get(key);
|
|
11563
|
+
if (!seen) {
|
|
11564
|
+
propertyLocs.set(key, __spreadValues({ locs: [loc] }, value !== void 0 ? { values: [value] } : {}));
|
|
11565
|
+
return;
|
|
11566
|
+
}
|
|
11567
|
+
if (seen.locs.some((l) => l.offset === loc.offset)) return;
|
|
11568
|
+
if (seen.locs.length >= MAX_WARNING_LOCATIONS) {
|
|
11569
|
+
seen.truncated = true;
|
|
11570
|
+
return;
|
|
11571
|
+
}
|
|
11572
|
+
seen.locs.push(loc);
|
|
11573
|
+
if (seen.values && value !== void 0) seen.values.push(value);
|
|
11574
|
+
}
|
|
11015
11575
|
$("style").each((_, el) => {
|
|
11016
11576
|
const cssText = $(el).text();
|
|
11577
|
+
blockAnchor = cssBlockAnchor(el, cssText, source);
|
|
11017
11578
|
try {
|
|
11018
11579
|
const ast = csstree5.parse(cssText, { parseCustomProperty: true, positions: true });
|
|
11019
11580
|
csstree5.walk(ast, {
|
|
11020
11581
|
enter(node) {
|
|
11021
11582
|
if (node.type === "Atrule") {
|
|
11022
11583
|
parsedAtRules.add(`@${node.name}`);
|
|
11584
|
+
recordSelectorLoc(`@${node.name}`, node.loc);
|
|
11023
11585
|
}
|
|
11024
11586
|
if (node.type === "PseudoClassSelector") {
|
|
11025
11587
|
detectedPseudoClasses.add(`:${node.name}`);
|
|
11588
|
+
recordSelectorLoc(`:${node.name}`, node.loc);
|
|
11026
11589
|
}
|
|
11027
11590
|
if (node.type === "PseudoElementSelector") {
|
|
11028
11591
|
detectedPseudoElements.add(`::${node.name}`);
|
|
11592
|
+
recordSelectorLoc(`::${node.name}`, node.loc);
|
|
11029
11593
|
}
|
|
11030
11594
|
if (node.type === "Declaration") {
|
|
11031
11595
|
const prop = node.property.toLowerCase();
|
|
11032
11596
|
parsedProperties.add(prop);
|
|
11033
|
-
if (node.loc && !propertyLines.has(prop)) {
|
|
11034
|
-
propertyLines.set(prop, node.loc.start.line);
|
|
11035
|
-
}
|
|
11036
11597
|
const valueStr = csstree5.generate(node.value);
|
|
11037
11598
|
const seenValues = propertyValues.get(prop);
|
|
11038
11599
|
if (seenValues) seenValues.push(valueStr);
|
|
11039
11600
|
else propertyValues.set(prop, [valueStr]);
|
|
11601
|
+
if (node.loc) {
|
|
11602
|
+
if (!propertyLines.has(prop)) propertyLines.set(prop, node.loc.start.line);
|
|
11603
|
+
recordLoc(prop, node.loc, valueStr);
|
|
11604
|
+
}
|
|
11040
11605
|
for (const det of COMPOUND_DETECTORS) {
|
|
11041
|
-
if (prop === det.property && valueStr.includes(det.valueIncludes)) {
|
|
11606
|
+
if (prop === det.property && valueStr.toLowerCase().includes(det.valueIncludes)) {
|
|
11042
11607
|
parsedProperties.add(det.key);
|
|
11043
|
-
if (node.loc
|
|
11044
|
-
propertyLines.set(det.key, node.loc.start.line);
|
|
11608
|
+
if (node.loc) {
|
|
11609
|
+
if (!propertyLines.has(det.key)) propertyLines.set(det.key, node.loc.start.line);
|
|
11610
|
+
recordLoc(det.key, node.loc);
|
|
11045
11611
|
}
|
|
11046
11612
|
}
|
|
11047
11613
|
}
|
|
11048
11614
|
for (const fn of CSS_FUNCTION_DETECTORS) {
|
|
11049
11615
|
if (valueStr.includes(fn.pattern)) {
|
|
11050
11616
|
detectedCssFunctions.add(fn.key);
|
|
11051
|
-
if (node.loc
|
|
11052
|
-
propertyLines.set(fn.key, node.loc.start.line);
|
|
11617
|
+
if (node.loc) {
|
|
11618
|
+
if (!propertyLines.has(fn.key)) propertyLines.set(fn.key, node.loc.start.line);
|
|
11619
|
+
recordLoc(fn.key, node.loc);
|
|
11053
11620
|
}
|
|
11054
11621
|
}
|
|
11055
11622
|
}
|
|
@@ -11061,33 +11628,69 @@ function analyzeEmailFromDom($, framework) {
|
|
|
11061
11628
|
});
|
|
11062
11629
|
for (const atRule of AT_RULE_FEATURES) {
|
|
11063
11630
|
if (!parsedAtRules.has(atRule)) continue;
|
|
11064
|
-
checkPropertySupport(atRule, addWarning, framework);
|
|
11631
|
+
checkPropertySupport(atRule, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(atRule));
|
|
11065
11632
|
}
|
|
11066
11633
|
const cssPropertiesToCheck = Object.keys(CSS_SUPPORT).filter(
|
|
11067
11634
|
(k) => !k.startsWith("<") && !k.startsWith("@")
|
|
11068
11635
|
);
|
|
11069
11636
|
$("[style]").each((_, el) => {
|
|
11070
|
-
var _a;
|
|
11071
11637
|
const style = $(el).attr("style") || "";
|
|
11072
11638
|
const props = parseStyleProperties(style);
|
|
11073
11639
|
const selector = describeSelector2(el);
|
|
11640
|
+
const attrLoc = locOfAttr(el, "style");
|
|
11641
|
+
const declarationLocs = (prop, occurrence = 0) => {
|
|
11642
|
+
var _a;
|
|
11643
|
+
return (_a = elementLocs(locInAttr(attrLoc, source, prop, occurrence))) != null ? _a : elementLocs(attrLoc);
|
|
11644
|
+
};
|
|
11645
|
+
const locs = elementLocs(attrLoc);
|
|
11074
11646
|
for (const prop of props) {
|
|
11075
11647
|
for (const det of COMPOUND_DETECTORS) {
|
|
11076
11648
|
if (prop === det.property) {
|
|
11077
11649
|
const value2 = getStyleValue(style, prop);
|
|
11078
|
-
if (value2 == null ? void 0 : value2.includes(det.valueIncludes)) {
|
|
11079
|
-
checkPropertySupport(
|
|
11650
|
+
if (value2 == null ? void 0 : value2.toLowerCase().includes(det.valueIncludes)) {
|
|
11651
|
+
checkPropertySupport(
|
|
11652
|
+
det.key,
|
|
11653
|
+
addWarning,
|
|
11654
|
+
framework,
|
|
11655
|
+
selector,
|
|
11656
|
+
void 0,
|
|
11657
|
+
void 0,
|
|
11658
|
+
declarationLocs(prop)
|
|
11659
|
+
);
|
|
11080
11660
|
}
|
|
11081
11661
|
}
|
|
11082
11662
|
}
|
|
11083
11663
|
if (cssPropertiesToCheck.includes(prop)) {
|
|
11084
|
-
|
|
11664
|
+
const declared = getStyleValues(style, prop);
|
|
11665
|
+
const placed = [];
|
|
11666
|
+
declared.forEach((value2, i) => {
|
|
11667
|
+
const at = locInAttr(attrLoc, source, prop, i);
|
|
11668
|
+
if (at) placed.push({ value: value2, loc: at });
|
|
11669
|
+
});
|
|
11670
|
+
const occurrences = placed.length === declared.length && placed.length > 0 ? { locs: placed.map((p) => p.loc), values: placed.map((p) => p.value) } : locs;
|
|
11671
|
+
checkPropertySupport(
|
|
11672
|
+
prop,
|
|
11673
|
+
addWarning,
|
|
11674
|
+
framework,
|
|
11675
|
+
selector,
|
|
11676
|
+
void 0,
|
|
11677
|
+
declared.length ? declared : void 0,
|
|
11678
|
+
occurrences
|
|
11679
|
+
);
|
|
11085
11680
|
}
|
|
11086
11681
|
const value = getStyleValue(style, prop);
|
|
11087
11682
|
if (value) {
|
|
11088
11683
|
for (const fn of CSS_FUNCTION_DETECTORS) {
|
|
11089
11684
|
if (value.includes(fn.pattern)) {
|
|
11090
|
-
checkPropertySupport(
|
|
11685
|
+
checkPropertySupport(
|
|
11686
|
+
fn.key,
|
|
11687
|
+
addWarning,
|
|
11688
|
+
framework,
|
|
11689
|
+
selector,
|
|
11690
|
+
void 0,
|
|
11691
|
+
void 0,
|
|
11692
|
+
declarationLocs(prop)
|
|
11693
|
+
);
|
|
11091
11694
|
}
|
|
11092
11695
|
}
|
|
11093
11696
|
}
|
|
@@ -11103,87 +11706,66 @@ function analyzeEmailFromDom($, framework) {
|
|
|
11103
11706
|
framework,
|
|
11104
11707
|
void 0,
|
|
11105
11708
|
propertyLines.get(prop),
|
|
11106
|
-
values
|
|
11709
|
+
values,
|
|
11710
|
+
propertyLocs.get(prop)
|
|
11107
11711
|
);
|
|
11108
11712
|
}
|
|
11109
11713
|
for (const compound of COMPOUND_VALUE_FEATURES) {
|
|
11110
11714
|
if (compound.startsWith(":") || compound.startsWith("::")) continue;
|
|
11111
11715
|
if (parsedProperties.has(compound)) {
|
|
11112
|
-
checkPropertySupport(compound, addWarning, framework, void 0, propertyLines.get(compound));
|
|
11716
|
+
checkPropertySupport(compound, addWarning, framework, void 0, propertyLines.get(compound), void 0, propertyLocs.get(compound));
|
|
11113
11717
|
}
|
|
11114
11718
|
}
|
|
11115
11719
|
for (const pseudo of detectedPseudoClasses) {
|
|
11116
11720
|
if (CSS_SUPPORT[pseudo]) {
|
|
11117
|
-
checkPropertySupport(pseudo, addWarning, framework);
|
|
11721
|
+
checkPropertySupport(pseudo, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(pseudo));
|
|
11118
11722
|
}
|
|
11119
11723
|
}
|
|
11120
11724
|
for (const pseudo of detectedPseudoElements) {
|
|
11121
11725
|
if (CSS_SUPPORT[pseudo]) {
|
|
11122
|
-
checkPropertySupport(pseudo, addWarning, framework);
|
|
11726
|
+
checkPropertySupport(pseudo, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(pseudo));
|
|
11123
11727
|
}
|
|
11124
11728
|
}
|
|
11125
11729
|
for (const fn of detectedCssFunctions) {
|
|
11126
|
-
checkPropertySupport(fn, addWarning, framework, void 0, propertyLines.get(fn));
|
|
11730
|
+
checkPropertySupport(fn, addWarning, framework, void 0, propertyLines.get(fn), void 0, propertyLocs.get(fn));
|
|
11127
11731
|
}
|
|
11128
|
-
for (const w of checkDarkModeFromDom(
|
|
11732
|
+
for (const w of checkDarkModeFromDom($, source)) addWarning(w);
|
|
11129
11733
|
const severityOrder = { error: 0, warning: 1, info: 2 };
|
|
11130
11734
|
warnings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
11131
11735
|
return warnings;
|
|
11132
11736
|
}
|
|
11133
|
-
function analyzeEmail(html, framework) {
|
|
11737
|
+
function analyzeEmail(html, framework, options) {
|
|
11134
11738
|
if (!html || !html.trim()) {
|
|
11135
11739
|
return [];
|
|
11136
11740
|
}
|
|
11137
11741
|
if (html.length > MAX_HTML_SIZE) {
|
|
11138
11742
|
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
11139
11743
|
}
|
|
11140
|
-
const $ =
|
|
11141
|
-
return analyzeEmailFromDom($, framework);
|
|
11744
|
+
const $ = loadHtml(html, options);
|
|
11745
|
+
return analyzeEmailFromDom($, framework, (options == null ? void 0 : options.positions) ? html : void 0);
|
|
11142
11746
|
}
|
|
11143
11747
|
function getFixType(prop) {
|
|
11144
11748
|
return STRUCTURAL_FIX_PROPERTIES.has(prop) ? "structural" : "css";
|
|
11145
11749
|
}
|
|
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
11750
|
function noteSuffix(notes) {
|
|
11170
11751
|
if (!(notes == null ? void 0 : notes.length)) return "";
|
|
11171
11752
|
const cleaned = notes.map((n) => n.replace(/^(?:Partial|Buggy|Not supported)\.\s*/i, "").trim()).filter(Boolean);
|
|
11172
11753
|
return cleaned.length ? ` ${cleaned.join(" ")}` : "";
|
|
11173
11754
|
}
|
|
11174
|
-
function checkPropertySupport(prop, addWarning, framework, selector, line,
|
|
11175
|
-
var _a;
|
|
11755
|
+
function checkPropertySupport(prop, addWarning, framework, selector, line, values, occurrences) {
|
|
11756
|
+
var _a, _b, _c, _d, _e, _f;
|
|
11757
|
+
const loc = occurrences == null ? void 0 : occurrences.locs[0];
|
|
11758
|
+
const reportedLine = (_a = loc == null ? void 0 : loc.line) != null ? _a : line;
|
|
11176
11759
|
const supportData = CSS_SUPPORT[prop];
|
|
11177
11760
|
if (!supportData) return;
|
|
11178
11761
|
const fixType = getFixType(prop);
|
|
11179
|
-
const valueGated = VALUE_CAVEAT_PROPS.has(prop);
|
|
11180
11762
|
for (const client of EMAIL_CLIENTS) {
|
|
11181
11763
|
const support = supportData[client.id] || "unknown";
|
|
11182
|
-
const notes = (
|
|
11764
|
+
const notes = (_b = CSS_SUPPORT_NOTES[prop]) == null ? void 0 : _b[client.id];
|
|
11183
11765
|
if (support === "unsupported") {
|
|
11184
11766
|
const sug = getSuggestion(prop, client.id, framework);
|
|
11185
11767
|
const fix = getCodeFix(prop, client.id, framework);
|
|
11186
|
-
addWarning(__spreadValues(__spreadValues(__spreadValues({
|
|
11768
|
+
addWarning(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
|
|
11187
11769
|
severity: "warning",
|
|
11188
11770
|
client: client.id,
|
|
11189
11771
|
property: prop,
|
|
@@ -11191,12 +11773,13 @@ function checkPropertySupport(prop, addWarning, framework, selector, line, value
|
|
|
11191
11773
|
suggestion: sug.text,
|
|
11192
11774
|
fix,
|
|
11193
11775
|
fixType
|
|
11194
|
-
}, selector ? { selector } : {}),
|
|
11776
|
+
}, selector ? { selector } : {}), reportedLine !== void 0 ? { line: reportedLine } : {}), occurrences ? occurrenceFields(occurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
11195
11777
|
} else if (support === "partial") {
|
|
11196
|
-
if (
|
|
11778
|
+
if (!caveatApplies(prop, values, notes)) continue;
|
|
11779
|
+
const hits = triggeringOccurrences(prop, occurrences, notes);
|
|
11197
11780
|
const sug = getSuggestion(prop, client.id, framework);
|
|
11198
11781
|
const fix = getCodeFix(prop, client.id, framework);
|
|
11199
|
-
addWarning(__spreadValues(__spreadValues(__spreadValues({
|
|
11782
|
+
addWarning(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
|
|
11200
11783
|
severity: "info",
|
|
11201
11784
|
client: client.id,
|
|
11202
11785
|
property: prop,
|
|
@@ -11204,7 +11787,7 @@ function checkPropertySupport(prop, addWarning, framework, selector, line, value
|
|
|
11204
11787
|
suggestion: sug.text,
|
|
11205
11788
|
fix,
|
|
11206
11789
|
fixType
|
|
11207
|
-
}, selector ? { selector } : {}), line !== void 0 ? { line } : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
11790
|
+
}, 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
11791
|
}
|
|
11209
11792
|
}
|
|
11210
11793
|
}
|
|
@@ -11223,6 +11806,19 @@ function generateCompatibilityScore(warnings) {
|
|
|
11223
11806
|
}
|
|
11224
11807
|
return result;
|
|
11225
11808
|
}
|
|
11809
|
+
function occurrenceFields({ locs, truncated }) {
|
|
11810
|
+
return __spreadValues({ loc: locs[0], locs: [...locs] }, truncated ? { locsTruncated: true } : {});
|
|
11811
|
+
}
|
|
11812
|
+
function triggeringOccurrences(prop, occurrences, notes) {
|
|
11813
|
+
const values = occurrences == null ? void 0 : occurrences.values;
|
|
11814
|
+
if (!occurrences || !values) return occurrences;
|
|
11815
|
+
const locs = occurrences.locs.filter((_, i) => caveatApplies(prop, [values[i]], notes));
|
|
11816
|
+
if (!locs.length || locs.length === occurrences.locs.length) return occurrences;
|
|
11817
|
+
return __spreadValues({ locs }, occurrences.truncated ? { truncated: true } : {});
|
|
11818
|
+
}
|
|
11819
|
+
function elementLocs(loc) {
|
|
11820
|
+
return loc ? { locs: [loc] } : void 0;
|
|
11821
|
+
}
|
|
11226
11822
|
function warningsForClient(warnings, clientId) {
|
|
11227
11823
|
return warnings.filter((w) => w.client === clientId);
|
|
11228
11824
|
}
|
|
@@ -11563,16 +12159,6 @@ function extractCode(response) {
|
|
|
11563
12159
|
return response.trim();
|
|
11564
12160
|
}
|
|
11565
12161
|
|
|
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
12162
|
// src/spam-scorer.ts
|
|
11577
12163
|
var SPAM_TRIGGER_PHRASES = [
|
|
11578
12164
|
"act now",
|
|
@@ -12041,6 +12627,8 @@ function validateLinksFromDom($) {
|
|
|
12041
12627
|
const href = $(el).attr("href") || "";
|
|
12042
12628
|
const text = $(el).text().trim();
|
|
12043
12629
|
const category = classifyHref(href);
|
|
12630
|
+
const elLoc = locOfElement(el);
|
|
12631
|
+
const hrefLoc = href ? locOfAttr(el, "href") : elLoc;
|
|
12044
12632
|
switch (category) {
|
|
12045
12633
|
case "https":
|
|
12046
12634
|
breakdown.https++;
|
|
@@ -12071,95 +12659,95 @@ function validateLinksFromDom($) {
|
|
|
12071
12659
|
hrefCounts.set(href, (hrefCounts.get(href) || 0) + 1);
|
|
12072
12660
|
}
|
|
12073
12661
|
if (!href || !href.trim()) {
|
|
12074
|
-
issues.push({
|
|
12662
|
+
issues.push(__spreadValues({
|
|
12075
12663
|
severity: "error",
|
|
12076
12664
|
rule: "empty-href",
|
|
12077
12665
|
message: "Link has no href attribute",
|
|
12078
12666
|
text: text.slice(0, 80) || "(no text)"
|
|
12079
|
-
});
|
|
12667
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12080
12668
|
return;
|
|
12081
12669
|
}
|
|
12082
12670
|
if (category === "javascript" && !isPlaceholderHref(href)) {
|
|
12083
|
-
issues.push({
|
|
12671
|
+
issues.push(__spreadValues({
|
|
12084
12672
|
severity: "error",
|
|
12085
12673
|
rule: "javascript-href",
|
|
12086
12674
|
message: "Link uses javascript: protocol",
|
|
12087
12675
|
href: href.slice(0, 100),
|
|
12088
12676
|
text: text.slice(0, 80) || "(no text)"
|
|
12089
|
-
});
|
|
12677
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
12090
12678
|
return;
|
|
12091
12679
|
}
|
|
12092
12680
|
if (isPlaceholderHref(href)) {
|
|
12093
|
-
issues.push({
|
|
12681
|
+
issues.push(__spreadValues({
|
|
12094
12682
|
severity: "warning",
|
|
12095
12683
|
rule: "placeholder-href",
|
|
12096
12684
|
message: "Link has a placeholder href (# or javascript:void)",
|
|
12097
12685
|
href,
|
|
12098
12686
|
text: text.slice(0, 80) || "(no text)"
|
|
12099
|
-
});
|
|
12687
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
12100
12688
|
return;
|
|
12101
12689
|
}
|
|
12102
12690
|
if (category === "http") {
|
|
12103
|
-
issues.push({
|
|
12691
|
+
issues.push(__spreadValues({
|
|
12104
12692
|
severity: "warning",
|
|
12105
12693
|
rule: "insecure-link",
|
|
12106
12694
|
message: "Link uses HTTP instead of HTTPS",
|
|
12107
12695
|
href: href.slice(0, 120),
|
|
12108
12696
|
text: text.slice(0, 80) || "(no text)"
|
|
12109
|
-
});
|
|
12697
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
12110
12698
|
}
|
|
12111
12699
|
if (category === "protocol-relative") {
|
|
12112
|
-
issues.push({
|
|
12700
|
+
issues.push(__spreadValues({
|
|
12113
12701
|
severity: "warning",
|
|
12114
12702
|
rule: "protocol-relative",
|
|
12115
12703
|
message: "Protocol-relative URL may break in email clients \u2014 use https:// explicitly",
|
|
12116
12704
|
href: href.slice(0, 120),
|
|
12117
12705
|
text: text.slice(0, 80) || "(no text)"
|
|
12118
|
-
});
|
|
12706
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
12119
12707
|
}
|
|
12120
12708
|
if (text && GENERIC_LINK_TEXT.has(text.toLowerCase())) {
|
|
12121
|
-
issues.push({
|
|
12709
|
+
issues.push(__spreadValues({
|
|
12122
12710
|
severity: "warning",
|
|
12123
12711
|
rule: "generic-link-text",
|
|
12124
12712
|
message: `Link text "${text}" is vague \u2014 use descriptive text for accessibility and engagement`,
|
|
12125
12713
|
href: href.slice(0, 120),
|
|
12126
12714
|
text
|
|
12127
|
-
});
|
|
12715
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12128
12716
|
}
|
|
12129
12717
|
if (!text && !$(el).attr("aria-label") && !$(el).find("img[alt]").length) {
|
|
12130
|
-
issues.push({
|
|
12718
|
+
issues.push(__spreadValues({
|
|
12131
12719
|
severity: "error",
|
|
12132
12720
|
rule: "empty-link-text",
|
|
12133
12721
|
message: "Link has no visible text or aria-label",
|
|
12134
12722
|
href: href.slice(0, 120)
|
|
12135
|
-
});
|
|
12723
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12136
12724
|
}
|
|
12137
12725
|
if (category === "mailto" && href.trim().toLowerCase() === "mailto:") {
|
|
12138
|
-
issues.push({
|
|
12726
|
+
issues.push(__spreadValues({
|
|
12139
12727
|
severity: "error",
|
|
12140
12728
|
rule: "empty-mailto",
|
|
12141
12729
|
message: "mailto: link has no email address",
|
|
12142
12730
|
href,
|
|
12143
12731
|
text: text.slice(0, 80) || "(no text)"
|
|
12144
|
-
});
|
|
12732
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
12145
12733
|
}
|
|
12146
12734
|
if (category === "tel" && href.trim().toLowerCase() === "tel:") {
|
|
12147
|
-
issues.push({
|
|
12735
|
+
issues.push(__spreadValues({
|
|
12148
12736
|
severity: "error",
|
|
12149
12737
|
rule: "empty-tel",
|
|
12150
12738
|
message: "tel: link has no phone number",
|
|
12151
12739
|
href,
|
|
12152
12740
|
text: text.slice(0, 80) || "(no text)"
|
|
12153
|
-
});
|
|
12741
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
12154
12742
|
}
|
|
12155
12743
|
if (href.length > 2e3) {
|
|
12156
|
-
issues.push({
|
|
12744
|
+
issues.push(__spreadValues({
|
|
12157
12745
|
severity: "info",
|
|
12158
12746
|
rule: "long-url",
|
|
12159
12747
|
message: "URL exceeds 2000 characters \u2014 may be truncated by some email clients",
|
|
12160
12748
|
href: href.slice(0, 120) + "...",
|
|
12161
12749
|
text: text.slice(0, 80) || "(no text)"
|
|
12162
|
-
});
|
|
12750
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
12163
12751
|
}
|
|
12164
12752
|
});
|
|
12165
12753
|
links.each((_, el) => {
|
|
@@ -12168,14 +12756,15 @@ function validateLinksFromDom($) {
|
|
|
12168
12756
|
if (trimmed.startsWith("#") && trimmed.length > 1) {
|
|
12169
12757
|
const targetId = trimmed.slice(1);
|
|
12170
12758
|
const target = $(`[id="${targetId}"]`);
|
|
12759
|
+
const anchorLoc = locOfAttr(el, "href");
|
|
12171
12760
|
if (target.length === 0) {
|
|
12172
|
-
issues.push({
|
|
12761
|
+
issues.push(__spreadValues({
|
|
12173
12762
|
severity: "error",
|
|
12174
12763
|
rule: "broken-anchor",
|
|
12175
12764
|
message: `Anchor link "${trimmed}" points to an element that does not exist`,
|
|
12176
12765
|
href: trimmed,
|
|
12177
12766
|
text: $(el).text().trim().slice(0, 80) || "(no text)"
|
|
12178
|
-
});
|
|
12767
|
+
}, anchorLoc ? { loc: anchorLoc } : {}));
|
|
12179
12768
|
}
|
|
12180
12769
|
}
|
|
12181
12770
|
});
|
|
@@ -12191,8 +12780,8 @@ function validateLinksFromDom($) {
|
|
|
12191
12780
|
}
|
|
12192
12781
|
return { totalLinks, issues, breakdown };
|
|
12193
12782
|
}
|
|
12194
|
-
function validateLinks(html) {
|
|
12195
|
-
return fromHtml(html, EMPTY_LINKS, validateLinksFromDom);
|
|
12783
|
+
function validateLinks(html, options) {
|
|
12784
|
+
return fromHtml(html, EMPTY_LINKS, validateLinksFromDom, options);
|
|
12196
12785
|
}
|
|
12197
12786
|
|
|
12198
12787
|
// src/accessibility-checker.ts
|
|
@@ -12217,24 +12806,31 @@ function describeElement($, el) {
|
|
|
12217
12806
|
function checkLangAttribute($) {
|
|
12218
12807
|
const lang = $("html").attr("lang");
|
|
12219
12808
|
if (!lang || !lang.trim()) {
|
|
12220
|
-
|
|
12809
|
+
const loc = locOfFirst($, "html");
|
|
12810
|
+
return __spreadProps(__spreadValues({
|
|
12221
12811
|
severity: "error",
|
|
12222
12812
|
rule: "missing-lang",
|
|
12223
|
-
message: "Missing lang attribute on <html> element"
|
|
12813
|
+
message: "Missing lang attribute on <html> element"
|
|
12814
|
+
}, loc ? { loc } : {}), {
|
|
12224
12815
|
details: 'Screen readers use the lang attribute to determine pronunciation. Add lang="en" (or appropriate language code).'
|
|
12225
|
-
};
|
|
12816
|
+
});
|
|
12226
12817
|
}
|
|
12227
12818
|
return null;
|
|
12228
12819
|
}
|
|
12820
|
+
function titleLoc($) {
|
|
12821
|
+
return $("title").length ? locOfFirst($, "title") : locOfFirst($, "head");
|
|
12822
|
+
}
|
|
12229
12823
|
function checkTitle($) {
|
|
12230
12824
|
const title = $("title").text().trim();
|
|
12231
12825
|
if (!title) {
|
|
12232
|
-
|
|
12826
|
+
const loc = titleLoc($);
|
|
12827
|
+
return __spreadProps(__spreadValues({
|
|
12233
12828
|
severity: "warning",
|
|
12234
12829
|
rule: "missing-title",
|
|
12235
|
-
message: "Missing or empty <title> element"
|
|
12830
|
+
message: "Missing or empty <title> element"
|
|
12831
|
+
}, loc ? { loc } : {}), {
|
|
12236
12832
|
details: "The <title> helps screen readers identify the email content."
|
|
12237
|
-
};
|
|
12833
|
+
});
|
|
12238
12834
|
}
|
|
12239
12835
|
return null;
|
|
12240
12836
|
}
|
|
@@ -12244,34 +12840,38 @@ function checkImageAlt($) {
|
|
|
12244
12840
|
const alt = $(el).attr("alt");
|
|
12245
12841
|
const src = $(el).attr("src") || "";
|
|
12246
12842
|
const role = $(el).attr("role");
|
|
12843
|
+
const elLoc = locOfElement(el);
|
|
12247
12844
|
if (role === "presentation" || role === "none") return;
|
|
12248
12845
|
if (alt === void 0) {
|
|
12249
|
-
issues.push({
|
|
12846
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12250
12847
|
severity: "error",
|
|
12251
12848
|
rule: "img-missing-alt",
|
|
12252
12849
|
message: "Image missing alt attribute",
|
|
12253
|
-
element: describeElement($, el)
|
|
12850
|
+
element: describeElement($, el)
|
|
12851
|
+
}, elLoc ? { loc: elLoc } : {}), {
|
|
12254
12852
|
details: 'Every image must have an alt attribute. Use alt="" for decorative images.'
|
|
12255
|
-
});
|
|
12853
|
+
}));
|
|
12256
12854
|
} else if (alt.trim() === "") {
|
|
12257
12855
|
const isLikelyContent = !src.includes("spacer") && !src.includes("pixel") && !src.includes("tracking") && !src.includes("1x1") && !src.includes("transparent");
|
|
12258
12856
|
if (isLikelyContent && ($(el).attr("width") || "0") !== "1") {
|
|
12259
|
-
issues.push({
|
|
12857
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12260
12858
|
severity: "info",
|
|
12261
12859
|
rule: "img-empty-alt",
|
|
12262
12860
|
message: "Image has empty alt text \u2014 verify it is decorative",
|
|
12263
|
-
element: describeElement($, el)
|
|
12861
|
+
element: describeElement($, el)
|
|
12862
|
+
}, locOfAttr(el, "alt") ? { loc: locOfAttr(el, "alt") } : {}), {
|
|
12264
12863
|
details: "Empty alt is correct for decorative images, but content images need descriptive alt text."
|
|
12265
|
-
});
|
|
12864
|
+
}));
|
|
12266
12865
|
}
|
|
12267
12866
|
} else if (/\.(png|jpg|jpeg|gif|svg|webp|bmp)$/i.test(alt)) {
|
|
12268
|
-
issues.push({
|
|
12867
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12269
12868
|
severity: "error",
|
|
12270
12869
|
rule: "img-filename-alt",
|
|
12271
12870
|
message: "Image alt text is a filename, not a description",
|
|
12272
|
-
element: describeElement($, el)
|
|
12871
|
+
element: describeElement($, el)
|
|
12872
|
+
}, locOfAttr(el, "alt") ? { loc: locOfAttr(el, "alt") } : {}), {
|
|
12273
12873
|
details: `Alt "${alt}" should describe the image content, not the file name.`
|
|
12274
|
-
});
|
|
12874
|
+
}));
|
|
12275
12875
|
}
|
|
12276
12876
|
});
|
|
12277
12877
|
return issues;
|
|
@@ -12279,28 +12879,31 @@ function checkImageAlt($) {
|
|
|
12279
12879
|
function checkLinkAccessibility($) {
|
|
12280
12880
|
const issues = [];
|
|
12281
12881
|
$("a").each((_, el) => {
|
|
12882
|
+
const elLoc = locOfElement(el);
|
|
12282
12883
|
const text = $(el).text().trim().toLowerCase();
|
|
12283
12884
|
const ariaLabel = $(el).attr("aria-label");
|
|
12284
12885
|
const title = $(el).attr("title");
|
|
12285
12886
|
const imgAlt = $(el).find("img").attr("alt");
|
|
12286
12887
|
if (!text && !ariaLabel && !title && !imgAlt) {
|
|
12287
|
-
issues.push({
|
|
12888
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12288
12889
|
severity: "error",
|
|
12289
12890
|
rule: "link-no-accessible-name",
|
|
12290
12891
|
message: "Link has no accessible name",
|
|
12291
|
-
element: describeElement($, el)
|
|
12892
|
+
element: describeElement($, el)
|
|
12893
|
+
}, elLoc ? { loc: elLoc } : {}), {
|
|
12292
12894
|
details: "Links need visible text, aria-label, or an image with alt text."
|
|
12293
|
-
});
|
|
12895
|
+
}));
|
|
12294
12896
|
return;
|
|
12295
12897
|
}
|
|
12296
12898
|
if (text && GENERIC_LINK_TEXT.has(text) && !ariaLabel) {
|
|
12297
|
-
issues.push({
|
|
12899
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12298
12900
|
severity: "warning",
|
|
12299
12901
|
rule: "link-generic-text",
|
|
12300
12902
|
message: `Link text "${$(el).text().trim()}" is not descriptive`,
|
|
12301
|
-
element: describeElement($, el)
|
|
12903
|
+
element: describeElement($, el)
|
|
12904
|
+
}, elLoc ? { loc: elLoc } : {}), {
|
|
12302
12905
|
details: "Screen readers often list links out of context. Use text that describes the destination."
|
|
12303
|
-
});
|
|
12906
|
+
}));
|
|
12304
12907
|
}
|
|
12305
12908
|
});
|
|
12306
12909
|
return issues;
|
|
@@ -12310,18 +12913,20 @@ function checkTableAccessibility($) {
|
|
|
12310
12913
|
$("table").each((_, el) => {
|
|
12311
12914
|
if ($(el).parents('table[role="presentation"], table[role="none"]').length > 0) return;
|
|
12312
12915
|
const role = $(el).attr("role");
|
|
12916
|
+
const tableLoc = locOfElement(el);
|
|
12313
12917
|
const hasHeaders = $(el).find("th").length > 0;
|
|
12314
12918
|
const looksLikeLayout = !hasHeaders;
|
|
12315
12919
|
if (looksLikeLayout && role !== "presentation" && role !== "none") {
|
|
12316
12920
|
const nestedTables = $(el).find("table").length;
|
|
12317
12921
|
if (nestedTables > 0 || $(el).find("td").length > 2) {
|
|
12318
|
-
issues.push({
|
|
12922
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12319
12923
|
severity: "info",
|
|
12320
12924
|
rule: "table-missing-role",
|
|
12321
|
-
message: 'Layout table missing role="presentation"'
|
|
12925
|
+
message: 'Layout table missing role="presentation"'
|
|
12926
|
+
}, tableLoc ? { loc: tableLoc } : {}), {
|
|
12322
12927
|
element: `<table> with ${$(el).find("td").length} cells`,
|
|
12323
12928
|
details: `Add role="presentation" to tables used for layout so screen readers don't announce them as data tables.`
|
|
12324
|
-
});
|
|
12929
|
+
}));
|
|
12325
12930
|
}
|
|
12326
12931
|
}
|
|
12327
12932
|
});
|
|
@@ -12332,6 +12937,7 @@ function checkTextSizeAndContrast($) {
|
|
|
12332
12937
|
let smallTextCount = 0;
|
|
12333
12938
|
$("[style]").each((_, el) => {
|
|
12334
12939
|
const style = $(el).attr("style") || "";
|
|
12940
|
+
const styleLoc = locOfAttr(el, "style");
|
|
12335
12941
|
const fontSizeMatch = style.match(/font-size\s*:\s*(\d+(?:\.\d+)?)(px|pt)/i);
|
|
12336
12942
|
if (fontSizeMatch) {
|
|
12337
12943
|
const size = parseFloat(fontSizeMatch[1]);
|
|
@@ -12340,13 +12946,14 @@ function checkTextSizeAndContrast($) {
|
|
|
12340
12946
|
if (pxSize < 9 && pxSize > 0) {
|
|
12341
12947
|
smallTextCount++;
|
|
12342
12948
|
if (smallTextCount <= 3) {
|
|
12343
|
-
issues.push({
|
|
12949
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12344
12950
|
severity: "warning",
|
|
12345
12951
|
rule: "small-text",
|
|
12346
12952
|
message: `Very small text (${fontSizeMatch[0].trim()})`,
|
|
12347
|
-
element: describeElement($, el)
|
|
12953
|
+
element: describeElement($, el)
|
|
12954
|
+
}, styleLoc ? { loc: styleLoc } : {}), {
|
|
12348
12955
|
details: "Text smaller than 9px is difficult to read, especially on mobile devices."
|
|
12349
|
-
});
|
|
12956
|
+
}));
|
|
12350
12957
|
}
|
|
12351
12958
|
}
|
|
12352
12959
|
}
|
|
@@ -12391,21 +12998,23 @@ function checkTextSizeAndContrast($) {
|
|
|
12391
12998
|
}
|
|
12392
12999
|
const grade = wcagGrade(ratio);
|
|
12393
13000
|
if (grade === "Fail") {
|
|
12394
|
-
issues.push({
|
|
13001
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12395
13002
|
severity: "error",
|
|
12396
13003
|
rule: "low-contrast",
|
|
12397
13004
|
message: `Low contrast ratio ${ratio.toFixed(1)}:1 \u2014 fails WCAG minimum`,
|
|
12398
|
-
element: describeElement($, el)
|
|
13005
|
+
element: describeElement($, el)
|
|
13006
|
+
}, styleLoc ? { loc: styleLoc } : {}), {
|
|
12399
13007
|
details: `Foreground ${colorValue} on background needs at least ${isLargeText ? "3:1" : "4.5:1"} contrast ratio.`
|
|
12400
|
-
});
|
|
13008
|
+
}));
|
|
12401
13009
|
} else if (!isLargeText && grade === "AA Large") {
|
|
12402
|
-
issues.push({
|
|
13010
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12403
13011
|
severity: "warning",
|
|
12404
13012
|
rule: "low-contrast",
|
|
12405
13013
|
message: `Low contrast ratio ${ratio.toFixed(1)}:1 \u2014 fails WCAG AA for normal text`,
|
|
12406
|
-
element: describeElement($, el)
|
|
13014
|
+
element: describeElement($, el)
|
|
13015
|
+
}, styleLoc ? { loc: styleLoc } : {}), {
|
|
12407
13016
|
details: `Foreground ${colorValue} on background needs at least 4.5:1 for normal-sized text.`
|
|
12408
|
-
});
|
|
13017
|
+
}));
|
|
12409
13018
|
}
|
|
12410
13019
|
}
|
|
12411
13020
|
}
|
|
@@ -12428,29 +13037,32 @@ function checkCharsetDeclaration($) {
|
|
|
12428
13037
|
const content = httpEquiv.attr("content") || "";
|
|
12429
13038
|
if (/charset\s*=/i.test(content)) return null;
|
|
12430
13039
|
}
|
|
12431
|
-
|
|
13040
|
+
const loc = locOfFirst($, "head");
|
|
13041
|
+
return __spreadProps(__spreadValues({
|
|
12432
13042
|
severity: "warning",
|
|
12433
13043
|
rule: "missing-charset",
|
|
12434
|
-
message: "Missing charset declaration"
|
|
13044
|
+
message: "Missing charset declaration"
|
|
13045
|
+
}, loc ? { loc } : {}), {
|
|
12435
13046
|
details: 'Add <meta charset="utf-8"> in <head> to prevent encoding issues across email clients.'
|
|
12436
|
-
};
|
|
13047
|
+
});
|
|
12437
13048
|
}
|
|
12438
13049
|
function checkSemanticStructure($) {
|
|
12439
13050
|
const issues = [];
|
|
12440
13051
|
const headings = [];
|
|
12441
13052
|
$("h1, h2, h3, h4, h5, h6").each((_, el) => {
|
|
12442
13053
|
const level = parseInt(el.tagName.replace(/h/i, ""), 10);
|
|
12443
|
-
headings.push({ level, text: $(el).text().trim().slice(0, 60) });
|
|
13054
|
+
headings.push({ level, text: $(el).text().trim().slice(0, 60), loc: locOfElement(el) });
|
|
12444
13055
|
});
|
|
12445
13056
|
for (let i = 1; i < headings.length; i++) {
|
|
12446
13057
|
const gap = headings[i].level - headings[i - 1].level;
|
|
12447
13058
|
if (gap > 1) {
|
|
12448
|
-
issues.push({
|
|
13059
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12449
13060
|
severity: "info",
|
|
12450
13061
|
rule: "heading-skip",
|
|
12451
|
-
message: `Heading level skipped: h${headings[i - 1].level} to h${headings[i].level}
|
|
13062
|
+
message: `Heading level skipped: h${headings[i - 1].level} to h${headings[i].level}`
|
|
13063
|
+
}, headings[i].loc ? { loc: headings[i].loc } : {}), {
|
|
12452
13064
|
details: "Skipped heading levels can confuse screen readers. Use sequential heading levels."
|
|
12453
|
-
});
|
|
13065
|
+
}));
|
|
12454
13066
|
break;
|
|
12455
13067
|
}
|
|
12456
13068
|
}
|
|
@@ -12491,8 +13103,8 @@ function checkAccessibilityFromDom($) {
|
|
|
12491
13103
|
const score = Math.max(0, 100 - penalty);
|
|
12492
13104
|
return { score, issues };
|
|
12493
13105
|
}
|
|
12494
|
-
function checkAccessibility(html) {
|
|
12495
|
-
return fromHtml(html, EMPTY_ACCESSIBILITY, checkAccessibilityFromDom);
|
|
13106
|
+
function checkAccessibility(html, options) {
|
|
13107
|
+
return fromHtml(html, EMPTY_ACCESSIBILITY, checkAccessibilityFromDom, options);
|
|
12496
13108
|
}
|
|
12497
13109
|
|
|
12498
13110
|
// src/image-analyzer.ts
|
|
@@ -12539,6 +13151,8 @@ function analyzeImagesFromDom($) {
|
|
|
12539
13151
|
const height = (_c = img.attr("height")) != null ? _c : null;
|
|
12540
13152
|
const style = (img.attr("style") || "").toLowerCase();
|
|
12541
13153
|
const imgIssues = [];
|
|
13154
|
+
const elLoc = locOfElement(el);
|
|
13155
|
+
const srcLoc = src ? locOfAttr(el, "src") : elLoc;
|
|
12542
13156
|
const tracking = isTrackingPixel(img);
|
|
12543
13157
|
let dataUriBytes = 0;
|
|
12544
13158
|
if (src.startsWith("data:")) {
|
|
@@ -12562,59 +13176,59 @@ function analyzeImagesFromDom($) {
|
|
|
12562
13176
|
const hasStyleHeight = /height\s*:/.test(style);
|
|
12563
13177
|
if (!hasStyleWidth && !hasStyleHeight) {
|
|
12564
13178
|
imgIssues.push("missing-dimensions");
|
|
12565
|
-
issues.push({
|
|
13179
|
+
issues.push(__spreadValues({
|
|
12566
13180
|
rule: "missing-dimensions",
|
|
12567
13181
|
severity: "warning",
|
|
12568
13182
|
message: "Image missing width/height attributes \u2014 causes layout shifts and Outlook rendering issues.",
|
|
12569
13183
|
src: truncateSrc(src)
|
|
12570
|
-
});
|
|
13184
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12571
13185
|
}
|
|
12572
13186
|
}
|
|
12573
13187
|
if (dataUriBytes > DATA_URI_WARN_BYTES) {
|
|
12574
13188
|
const kb = Math.round(dataUriBytes / 1024);
|
|
12575
13189
|
imgIssues.push("large-data-uri");
|
|
12576
|
-
issues.push({
|
|
13190
|
+
issues.push(__spreadValues({
|
|
12577
13191
|
rule: "large-data-uri",
|
|
12578
13192
|
severity: "warning",
|
|
12579
13193
|
message: `Data URI is ${kb}KB \u2014 consider hosting the image externally to reduce email size.`,
|
|
12580
13194
|
src: truncateSrc(src)
|
|
12581
|
-
});
|
|
13195
|
+
}, srcLoc ? { loc: srcLoc } : {}));
|
|
12582
13196
|
}
|
|
12583
13197
|
if (alt === null) {
|
|
12584
13198
|
imgIssues.push("missing-alt");
|
|
12585
|
-
issues.push({
|
|
13199
|
+
issues.push(__spreadValues({
|
|
12586
13200
|
rule: "missing-alt",
|
|
12587
13201
|
severity: "warning",
|
|
12588
13202
|
message: "Image missing alt attribute \u2014 hurts deliverability and accessibility.",
|
|
12589
13203
|
src: truncateSrc(src)
|
|
12590
|
-
});
|
|
13204
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12591
13205
|
}
|
|
12592
13206
|
if (src.toLowerCase().endsWith(".webp") || src.includes("image/webp")) {
|
|
12593
13207
|
imgIssues.push("webp-format");
|
|
12594
|
-
issues.push({
|
|
13208
|
+
issues.push(__spreadValues({
|
|
12595
13209
|
rule: "webp-format",
|
|
12596
13210
|
severity: "info",
|
|
12597
13211
|
message: "WebP format detected \u2014 not supported by all email clients. Consider PNG or JPEG.",
|
|
12598
13212
|
src: truncateSrc(src)
|
|
12599
|
-
});
|
|
13213
|
+
}, srcLoc ? { loc: srcLoc } : {}));
|
|
12600
13214
|
}
|
|
12601
13215
|
if (src.toLowerCase().endsWith(".svg") || src.includes("image/svg")) {
|
|
12602
13216
|
imgIssues.push("svg-format");
|
|
12603
|
-
issues.push({
|
|
13217
|
+
issues.push(__spreadValues({
|
|
12604
13218
|
rule: "svg-format",
|
|
12605
13219
|
severity: "info",
|
|
12606
13220
|
message: "SVG format detected \u2014 not supported by most email clients. Use PNG instead.",
|
|
12607
13221
|
src: truncateSrc(src)
|
|
12608
|
-
});
|
|
13222
|
+
}, srcLoc ? { loc: srcLoc } : {}));
|
|
12609
13223
|
}
|
|
12610
13224
|
if (!style.includes("display:block") && !style.includes("display: block")) {
|
|
12611
13225
|
imgIssues.push("missing-display-block");
|
|
12612
|
-
issues.push({
|
|
13226
|
+
issues.push(__spreadValues({
|
|
12613
13227
|
rule: "missing-display-block",
|
|
12614
13228
|
severity: "info",
|
|
12615
13229
|
message: "Image without display:block \u2014 may cause unwanted gaps in Outlook.",
|
|
12616
13230
|
src: truncateSrc(src)
|
|
12617
|
-
});
|
|
13231
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12618
13232
|
}
|
|
12619
13233
|
images.push({
|
|
12620
13234
|
src: truncateSrc(src),
|
|
@@ -12652,8 +13266,8 @@ function analyzeImagesFromDom($) {
|
|
|
12652
13266
|
}
|
|
12653
13267
|
return { total: images.length, totalDataUriBytes, issues, images };
|
|
12654
13268
|
}
|
|
12655
|
-
function analyzeImages(html) {
|
|
12656
|
-
return fromHtml(html, EMPTY_IMAGES, analyzeImagesFromDom);
|
|
13269
|
+
function analyzeImages(html, options) {
|
|
13270
|
+
return fromHtml(html, EMPTY_IMAGES, analyzeImagesFromDom, options);
|
|
12657
13271
|
}
|
|
12658
13272
|
|
|
12659
13273
|
// src/inbox-preview.ts
|
|
@@ -12871,11 +13485,57 @@ function checkSize(html) {
|
|
|
12871
13485
|
return fromHtml(html, EMPTY_SIZE, checkSizeFromDom);
|
|
12872
13486
|
}
|
|
12873
13487
|
|
|
13488
|
+
// src/dom-text.ts
|
|
13489
|
+
function visibleTextNodes($) {
|
|
13490
|
+
var _a, _b, _c, _d;
|
|
13491
|
+
const nodes = [];
|
|
13492
|
+
const stack = [...(_b = (_a = $.root()[0]) == null ? void 0 : _a.children) != null ? _b : []].reverse();
|
|
13493
|
+
while (stack.length > 0) {
|
|
13494
|
+
const node = stack.pop();
|
|
13495
|
+
const tag = (_c = node.tagName) == null ? void 0 : _c.toLowerCase();
|
|
13496
|
+
if (tag === "style" || tag === "script" || tag === "head") continue;
|
|
13497
|
+
if (node.type === "text") {
|
|
13498
|
+
nodes.push(node);
|
|
13499
|
+
continue;
|
|
13500
|
+
}
|
|
13501
|
+
const children = (_d = node.children) != null ? _d : [];
|
|
13502
|
+
for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]);
|
|
13503
|
+
}
|
|
13504
|
+
return nodes;
|
|
13505
|
+
}
|
|
13506
|
+
|
|
12874
13507
|
// src/template-checker.ts
|
|
12875
|
-
function checkTemplateVariablesFromDom(
|
|
13508
|
+
function checkTemplateVariablesFromDom($, source) {
|
|
13509
|
+
var _a;
|
|
12876
13510
|
const issues = [];
|
|
12877
13511
|
const seen = /* @__PURE__ */ new Set();
|
|
12878
|
-
const
|
|
13512
|
+
const textNodes = visibleTextNodes($);
|
|
13513
|
+
const positioned = textNodes.some((n) => n.sourceCodeLocation);
|
|
13514
|
+
for (const node of positioned ? textNodes : []) {
|
|
13515
|
+
const data = (_a = node.data) != null ? _a : "";
|
|
13516
|
+
for (const [pattern, label] of TEMPLATE_VARIABLE_PATTERNS) {
|
|
13517
|
+
pattern.lastIndex = 0;
|
|
13518
|
+
let match;
|
|
13519
|
+
while ((match = pattern.exec(data)) !== null) {
|
|
13520
|
+
const variable = match[0];
|
|
13521
|
+
const key = `text:${variable}`;
|
|
13522
|
+
if (seen.has(key)) continue;
|
|
13523
|
+
seen.add(key);
|
|
13524
|
+
const loc = locInTextNode(node, match.index, variable.length, source);
|
|
13525
|
+
issues.push(__spreadValues({
|
|
13526
|
+
rule: "unresolved-variable",
|
|
13527
|
+
severity: "error",
|
|
13528
|
+
message: `Unresolved ${label} variable "${variable}" found in text content.`,
|
|
13529
|
+
variable,
|
|
13530
|
+
location: "text"
|
|
13531
|
+
}, loc ? { loc } : {}));
|
|
13532
|
+
}
|
|
13533
|
+
}
|
|
13534
|
+
}
|
|
13535
|
+
const textContent = textNodes.map((n) => {
|
|
13536
|
+
var _a2;
|
|
13537
|
+
return (_a2 = n.data) != null ? _a2 : "";
|
|
13538
|
+
}).join("");
|
|
12879
13539
|
for (const [pattern, label] of TEMPLATE_VARIABLE_PATTERNS) {
|
|
12880
13540
|
pattern.lastIndex = 0;
|
|
12881
13541
|
let match;
|
|
@@ -12908,13 +13568,14 @@ function checkTemplateVariablesFromDom($) {
|
|
|
12908
13568
|
const key = `attr:${attr}:${variable}`;
|
|
12909
13569
|
if (seen.has(key)) continue;
|
|
12910
13570
|
seen.add(key);
|
|
12911
|
-
|
|
13571
|
+
const loc = locOfAttr(el, attr);
|
|
13572
|
+
issues.push(__spreadValues({
|
|
12912
13573
|
rule: "unresolved-variable",
|
|
12913
13574
|
severity: "error",
|
|
12914
13575
|
message: `Unresolved ${label} variable "${variable}" found in ${attr} attribute.`,
|
|
12915
13576
|
variable,
|
|
12916
13577
|
location: "attribute"
|
|
12917
|
-
});
|
|
13578
|
+
}, loc ? { loc } : {}));
|
|
12918
13579
|
}
|
|
12919
13580
|
}
|
|
12920
13581
|
}
|
|
@@ -12922,13 +13583,13 @@ function checkTemplateVariablesFromDom($) {
|
|
|
12922
13583
|
}
|
|
12923
13584
|
return { unresolvedCount: issues.length, issues };
|
|
12924
13585
|
}
|
|
12925
|
-
function
|
|
12926
|
-
|
|
12927
|
-
|
|
12928
|
-
|
|
12929
|
-
|
|
12930
|
-
|
|
12931
|
-
|
|
13586
|
+
function checkTemplateVariables(html, options) {
|
|
13587
|
+
return fromHtml(
|
|
13588
|
+
html,
|
|
13589
|
+
EMPTY_TEMPLATE,
|
|
13590
|
+
($, h) => checkTemplateVariablesFromDom($, (options == null ? void 0 : options.positions) ? h : void 0),
|
|
13591
|
+
options
|
|
13592
|
+
);
|
|
12932
13593
|
}
|
|
12933
13594
|
|
|
12934
13595
|
// src/overflow-checker.ts
|
|
@@ -12944,32 +13605,71 @@ function fixedPxWidth($el) {
|
|
|
12944
13605
|
function isFluid(style) {
|
|
12945
13606
|
return /max-width\s*:\s*100%/i.test(style) || /width\s*:\s*100%/i.test(style);
|
|
12946
13607
|
}
|
|
12947
|
-
function addWidthIssue(width, label, issues, seen) {
|
|
13608
|
+
function addWidthIssue(width, label, issues, seen, loc) {
|
|
12948
13609
|
const key = `w:${label}:${width}`;
|
|
12949
|
-
|
|
12950
|
-
|
|
12951
|
-
|
|
13610
|
+
const existing = seen.get(key);
|
|
13611
|
+
if (existing) {
|
|
13612
|
+
addOccurrence(existing, loc);
|
|
13613
|
+
return;
|
|
13614
|
+
}
|
|
13615
|
+
const issue = __spreadValues({
|
|
12952
13616
|
rule: "fixed-width-overflow",
|
|
12953
13617
|
severity: "warning",
|
|
12954
13618
|
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
13619
|
detail: `Use width:100% with max-width:${EMAIL_MAX_WIDTH}px instead of a fixed width beyond the frame.`
|
|
12956
|
-
});
|
|
13620
|
+
}, loc ? { loc, locs: [loc] } : {});
|
|
13621
|
+
seen.set(key, issue);
|
|
13622
|
+
issues.push(issue);
|
|
13623
|
+
}
|
|
13624
|
+
function locateInNodes(nodes, starts, index, length, source) {
|
|
13625
|
+
var _a;
|
|
13626
|
+
let lo = 0;
|
|
13627
|
+
let hi = starts.length - 1;
|
|
13628
|
+
let found = -1;
|
|
13629
|
+
while (lo <= hi) {
|
|
13630
|
+
const mid = lo + hi >> 1;
|
|
13631
|
+
if (starts[mid] <= index) {
|
|
13632
|
+
found = mid;
|
|
13633
|
+
lo = mid + 1;
|
|
13634
|
+
} else {
|
|
13635
|
+
hi = mid - 1;
|
|
13636
|
+
}
|
|
13637
|
+
}
|
|
13638
|
+
if (found === -1) return void 0;
|
|
13639
|
+
const node = nodes[found];
|
|
13640
|
+
const within = index - starts[found];
|
|
13641
|
+
const available = ((_a = node.data) != null ? _a : "").length - within;
|
|
13642
|
+
return locInTextNode(node, within, Math.min(length, available), source);
|
|
12957
13643
|
}
|
|
12958
|
-
function
|
|
13644
|
+
function addOccurrence(issue, loc) {
|
|
13645
|
+
if (!loc || !issue.locs) return;
|
|
13646
|
+
if (issue.locs.some((l) => l.offset === loc.offset)) return;
|
|
13647
|
+
if (issue.locs.length >= MAX_WARNING_LOCATIONS) {
|
|
13648
|
+
issue.locsTruncated = true;
|
|
13649
|
+
return;
|
|
13650
|
+
}
|
|
13651
|
+
issue.locs.push(loc);
|
|
13652
|
+
}
|
|
13653
|
+
function checkOverflowFromDom($, source) {
|
|
13654
|
+
var _a;
|
|
12959
13655
|
const issues = [];
|
|
12960
|
-
const seen = /* @__PURE__ */ new
|
|
13656
|
+
const seen = /* @__PURE__ */ new Map();
|
|
13657
|
+
const tokensSeen = /* @__PURE__ */ new Set();
|
|
12961
13658
|
$("[width], [style*='width']").each((_, el) => {
|
|
12962
13659
|
const $el = $(el);
|
|
12963
13660
|
const width = fixedPxWidth($el);
|
|
12964
13661
|
if (width === null || width <= EMAIL_MAX_WIDTH) return;
|
|
12965
13662
|
if (isFluid($el.attr("style") || "")) return;
|
|
12966
13663
|
const tag = (el.tagName || "element").toLowerCase();
|
|
12967
|
-
|
|
13664
|
+
const fromStyle = /(?:^|[;\s])width\s*:\s*\d+px/i.test($el.attr("style") || "");
|
|
13665
|
+
addWidthIssue(width, `<${tag}>`, issues, seen, locOfAttr(el, fromStyle ? "style" : "width"));
|
|
12968
13666
|
});
|
|
12969
13667
|
$("style").each((_, el) => {
|
|
13668
|
+
const cssText = $(el).text();
|
|
13669
|
+
const anchor = cssBlockAnchor(el, cssText, source);
|
|
12970
13670
|
let ast;
|
|
12971
13671
|
try {
|
|
12972
|
-
ast = csstree6.parse(
|
|
13672
|
+
ast = csstree6.parse(cssText, { positions: true });
|
|
12973
13673
|
} catch (e) {
|
|
12974
13674
|
return;
|
|
12975
13675
|
}
|
|
@@ -12979,13 +13679,17 @@ function checkOverflowFromDom($) {
|
|
|
12979
13679
|
if (node.type !== "Rule") return;
|
|
12980
13680
|
let widthPx = null;
|
|
12981
13681
|
let fluid = false;
|
|
13682
|
+
let widthLoc;
|
|
12982
13683
|
node.block.children.forEach((child) => {
|
|
12983
13684
|
if (child.type !== "Declaration") return;
|
|
12984
13685
|
const prop = child.property.toLowerCase();
|
|
12985
13686
|
const val = csstree6.generate(child.value);
|
|
12986
13687
|
if (prop === "width") {
|
|
12987
13688
|
const m = val.match(/^(\d+)px$/);
|
|
12988
|
-
if (m)
|
|
13689
|
+
if (m) {
|
|
13690
|
+
widthPx = parseInt(m[1], 10);
|
|
13691
|
+
widthLoc = locInCssBlock(anchor, child.loc);
|
|
13692
|
+
}
|
|
12989
13693
|
if (/\b100%/.test(val)) fluid = true;
|
|
12990
13694
|
} else if (prop === "max-width" && /\b100%/.test(val)) {
|
|
12991
13695
|
fluid = true;
|
|
@@ -12993,36 +13697,46 @@ function checkOverflowFromDom($) {
|
|
|
12993
13697
|
});
|
|
12994
13698
|
if (widthPx !== null && widthPx > EMAIL_MAX_WIDTH && !fluid) {
|
|
12995
13699
|
const selector = csstree6.generate(node.prelude).trim().slice(0, 40);
|
|
12996
|
-
addWidthIssue(widthPx, selector || "rule", issues, seen);
|
|
13700
|
+
addWidthIssue(widthPx, selector || "rule", issues, seen, widthLoc);
|
|
12997
13701
|
}
|
|
12998
13702
|
}
|
|
12999
13703
|
});
|
|
13000
13704
|
});
|
|
13001
13705
|
const usesWrapGuard = /overflow-wrap|word-break|word-wrap/i.test($.html());
|
|
13002
13706
|
if (!usesWrapGuard) {
|
|
13003
|
-
const
|
|
13707
|
+
const nodes = visibleTextNodes($);
|
|
13708
|
+
const starts = [];
|
|
13004
13709
|
let text = "";
|
|
13005
|
-
|
|
13006
|
-
|
|
13007
|
-
|
|
13008
|
-
|
|
13009
|
-
|
|
13010
|
-
for (const token of text.split(
|
|
13011
|
-
|
|
13012
|
-
|
|
13710
|
+
for (const node of nodes) {
|
|
13711
|
+
starts.push(text.length);
|
|
13712
|
+
text += (_a = node.data) != null ? _a : "";
|
|
13713
|
+
}
|
|
13714
|
+
let at = 0;
|
|
13715
|
+
for (const token of text.split(/(\s+)/)) {
|
|
13716
|
+
const start = at;
|
|
13717
|
+
at += token.length;
|
|
13718
|
+
if (/^\s*$/.test(token)) continue;
|
|
13719
|
+
if (token.length <= UNBREAKABLE_STRING_LENGTH || tokensSeen.has(token)) continue;
|
|
13720
|
+
tokensSeen.add(token);
|
|
13013
13721
|
const preview = token.length > 50 ? `${token.slice(0, 50)}\u2026` : token;
|
|
13014
|
-
|
|
13722
|
+
const loc = locateInNodes(nodes, starts, start, token.length, source);
|
|
13723
|
+
issues.push(__spreadValues({
|
|
13015
13724
|
rule: "unbreakable-string",
|
|
13016
13725
|
severity: "warning",
|
|
13017
13726
|
message: `A ${token.length}-character unbroken string ("${preview}") can't wrap and will force horizontal scrolling on narrow screens.`,
|
|
13018
13727
|
detail: `Add overflow-wrap: anywhere (or word-break: break-word) to its container.`
|
|
13019
|
-
});
|
|
13728
|
+
}, loc ? { loc, locs: [loc] } : {}));
|
|
13020
13729
|
}
|
|
13021
13730
|
}
|
|
13022
13731
|
return { hasOverflow: issues.length > 0, issues };
|
|
13023
13732
|
}
|
|
13024
|
-
function checkOverflow(html) {
|
|
13025
|
-
return fromHtml(
|
|
13733
|
+
function checkOverflow(html, options) {
|
|
13734
|
+
return fromHtml(
|
|
13735
|
+
html,
|
|
13736
|
+
EMPTY_OVERFLOW,
|
|
13737
|
+
($, h) => checkOverflowFromDom($, (options == null ? void 0 : options.positions) ? h : void 0),
|
|
13738
|
+
options
|
|
13739
|
+
);
|
|
13026
13740
|
}
|
|
13027
13741
|
|
|
13028
13742
|
// src/visual-checker.ts
|
|
@@ -13035,8 +13749,8 @@ function isSolidColor(value) {
|
|
|
13035
13749
|
return c !== null && c.a !== 0;
|
|
13036
13750
|
}
|
|
13037
13751
|
function firstColor(value) {
|
|
13038
|
-
const
|
|
13039
|
-
for (const t of
|
|
13752
|
+
const tokens2 = value.match(/#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\)|\b[a-zA-Z]{3,}\b/g) || [];
|
|
13753
|
+
for (const t of tokens2) {
|
|
13040
13754
|
const lc = t.toLowerCase();
|
|
13041
13755
|
if (lc === "transparent") continue;
|
|
13042
13756
|
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 +13787,17 @@ function hasFontFallback(value) {
|
|
|
13073
13787
|
return WEB_SAFE_FONTS.has(t) || GENERIC_FONT_FAMILIES.has(t) || t.startsWith("-apple-system") || t === "blinkmacsystemfont";
|
|
13074
13788
|
});
|
|
13075
13789
|
}
|
|
13076
|
-
function
|
|
13077
|
-
|
|
13790
|
+
function addOccurrence2(issue, loc) {
|
|
13791
|
+
if (!loc || !issue.locs) return;
|
|
13792
|
+
if (issue.locs.some((l) => l.offset === loc.offset)) return;
|
|
13793
|
+
if (issue.locs.length >= MAX_WARNING_LOCATIONS) {
|
|
13794
|
+
issue.locsTruncated = true;
|
|
13795
|
+
return;
|
|
13796
|
+
}
|
|
13797
|
+
issue.locs.push(loc);
|
|
13798
|
+
}
|
|
13799
|
+
function inspectDeclarations(style, issues, seen, locs) {
|
|
13800
|
+
var _a, _b, _c;
|
|
13078
13801
|
const combined = `${(_a = style.get("background-image")) != null ? _a : ""} ${(_b = style.get("background")) != null ? _b : ""}`;
|
|
13079
13802
|
const isGradient = GRADIENT_RE.test(combined);
|
|
13080
13803
|
const isImage = isGradient || /url\(/i.test(combined);
|
|
@@ -13082,30 +13805,40 @@ function inspectDeclarations(style, issues, seen) {
|
|
|
13082
13805
|
const stop = isGradient ? firstColor(combined) : null;
|
|
13083
13806
|
const fix = stop ? `background-color: ${stop};` : `background-color: <solid colour matching the image>;`;
|
|
13084
13807
|
const key = `bg:${fix}`;
|
|
13085
|
-
|
|
13086
|
-
|
|
13087
|
-
|
|
13808
|
+
const loc = (_c = locs == null ? void 0 : locs.get("background-image")) != null ? _c : locs == null ? void 0 : locs.get("background");
|
|
13809
|
+
const existing = seen.get(key);
|
|
13810
|
+
if (existing) {
|
|
13811
|
+
addOccurrence2(existing, loc);
|
|
13812
|
+
} else {
|
|
13813
|
+
const issue = __spreadValues({
|
|
13088
13814
|
rule: "missing-background-fallback",
|
|
13089
13815
|
severity: "warning",
|
|
13090
13816
|
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
13817
|
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
13818
|
fix
|
|
13093
|
-
});
|
|
13819
|
+
}, loc ? { loc, locs: [loc] } : {});
|
|
13820
|
+
seen.set(key, issue);
|
|
13821
|
+
issues.push(issue);
|
|
13094
13822
|
}
|
|
13095
13823
|
}
|
|
13096
13824
|
const font = style.get("font-family");
|
|
13097
13825
|
if (font && !CSS_WIDE_KEYWORDS.has(font.trim().toLowerCase()) && !hasFontFallback(font)) {
|
|
13098
13826
|
const fix = `font-family: ${font.trim()}, Arial, sans-serif;`;
|
|
13099
13827
|
const key = `font:${font.trim().toLowerCase()}`;
|
|
13100
|
-
|
|
13101
|
-
|
|
13102
|
-
|
|
13828
|
+
const loc = locs == null ? void 0 : locs.get("font-family");
|
|
13829
|
+
const existing = seen.get(key);
|
|
13830
|
+
if (existing) {
|
|
13831
|
+
addOccurrence2(existing, loc);
|
|
13832
|
+
} else {
|
|
13833
|
+
const issue = __spreadValues({
|
|
13103
13834
|
rule: "missing-font-fallback",
|
|
13104
13835
|
severity: "warning",
|
|
13105
13836
|
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
13837
|
detail: `End the stack with a web-safe font and a generic family.`,
|
|
13107
13838
|
fix
|
|
13108
|
-
});
|
|
13839
|
+
}, loc ? { loc, locs: [loc] } : {});
|
|
13840
|
+
seen.set(key, issue);
|
|
13841
|
+
issues.push(issue);
|
|
13109
13842
|
}
|
|
13110
13843
|
}
|
|
13111
13844
|
}
|
|
@@ -13118,30 +13851,47 @@ function ruleToMap(node) {
|
|
|
13118
13851
|
});
|
|
13119
13852
|
return map;
|
|
13120
13853
|
}
|
|
13121
|
-
function checkVisualFromDom(
|
|
13854
|
+
function checkVisualFromDom($, source) {
|
|
13122
13855
|
const issues = [];
|
|
13123
|
-
const seen = /* @__PURE__ */ new
|
|
13856
|
+
const seen = /* @__PURE__ */ new Map();
|
|
13124
13857
|
$("[style]").each((_, el) => {
|
|
13125
|
-
|
|
13858
|
+
const attrLoc = locOfAttr(el, "style");
|
|
13859
|
+
const style = parseInlineStyle($(el).attr("style") || "");
|
|
13860
|
+
const locs = attrLoc ? new Map([...style.keys()].map((prop) => [prop, attrLoc])) : void 0;
|
|
13861
|
+
inspectDeclarations(style, issues, seen, locs);
|
|
13126
13862
|
});
|
|
13127
13863
|
$("style").each((_, el) => {
|
|
13864
|
+
const cssText = $(el).text();
|
|
13865
|
+
const anchor = cssBlockAnchor(el, cssText, source);
|
|
13128
13866
|
let ast;
|
|
13129
13867
|
try {
|
|
13130
|
-
ast = csstree7.parse(
|
|
13868
|
+
ast = csstree7.parse(cssText, { positions: true });
|
|
13131
13869
|
} catch (e) {
|
|
13132
13870
|
return;
|
|
13133
13871
|
}
|
|
13134
13872
|
csstree7.walk(ast, {
|
|
13135
13873
|
visit: "Rule",
|
|
13136
13874
|
enter(node) {
|
|
13137
|
-
if (node.type
|
|
13875
|
+
if (node.type !== "Rule") return;
|
|
13876
|
+
const locs = /* @__PURE__ */ new Map();
|
|
13877
|
+
node.block.children.forEach((child) => {
|
|
13878
|
+
if (child.type !== "Declaration") return;
|
|
13879
|
+
const loc = locInCssBlock(anchor, child.loc);
|
|
13880
|
+
if (loc) locs.set(child.property.toLowerCase(), loc);
|
|
13881
|
+
});
|
|
13882
|
+
inspectDeclarations(ruleToMap(node), issues, seen, locs);
|
|
13138
13883
|
}
|
|
13139
13884
|
});
|
|
13140
13885
|
});
|
|
13141
13886
|
return { issues };
|
|
13142
13887
|
}
|
|
13143
|
-
function checkVisual(html) {
|
|
13144
|
-
return fromHtml(
|
|
13888
|
+
function checkVisual(html, options) {
|
|
13889
|
+
return fromHtml(
|
|
13890
|
+
html,
|
|
13891
|
+
EMPTY_VISUAL,
|
|
13892
|
+
($, h) => checkVisualFromDom($, (options == null ? void 0 : options.positions) ? h : void 0),
|
|
13893
|
+
options
|
|
13894
|
+
);
|
|
13145
13895
|
}
|
|
13146
13896
|
|
|
13147
13897
|
// src/audit.ts
|
|
@@ -13160,7 +13910,8 @@ var EMPTY_AUDIT = {
|
|
|
13160
13910
|
function runAudit($, html, framework, options) {
|
|
13161
13911
|
var _a;
|
|
13162
13912
|
const skip = new Set((_a = options == null ? void 0 : options.skip) != null ? _a : []);
|
|
13163
|
-
const
|
|
13913
|
+
const source = (options == null ? void 0 : options.positions) ? html : void 0;
|
|
13914
|
+
const warnings = skip.has("compatibility") ? [] : analyzeEmailFromDom($, framework, source);
|
|
13164
13915
|
const scores = skip.has("compatibility") ? {} : generateCompatibilityScore(warnings);
|
|
13165
13916
|
const spam = skip.has("spam") ? EMPTY_SPAM : analyzeSpamFromDom($, options == null ? void 0 : options.spam);
|
|
13166
13917
|
const links = skip.has("links") ? EMPTY_LINKS : validateLinksFromDom($);
|
|
@@ -13168,19 +13919,19 @@ function runAudit($, html, framework, options) {
|
|
|
13168
13919
|
const images = skip.has("images") ? EMPTY_IMAGES : analyzeImagesFromDom($);
|
|
13169
13920
|
const inboxPreview = skip.has("inboxPreview") ? EMPTY_INBOX_PREVIEW : extractInboxPreviewFromDom($);
|
|
13170
13921
|
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(
|
|
13922
|
+
const templateVariables = skip.has("templateVariables") ? EMPTY_TEMPLATE : checkTemplateVariablesFromDom($, source);
|
|
13923
|
+
const overflow = skip.has("overflow") ? EMPTY_OVERFLOW : checkOverflowFromDom($, source);
|
|
13924
|
+
const visual = skip.has("visual") ? EMPTY_VISUAL : checkVisualFromDom($, source);
|
|
13174
13925
|
return { compatibility: { warnings, scores }, spam, links, accessibility, images, inboxPreview, size, templateVariables, overflow, visual };
|
|
13175
13926
|
}
|
|
13176
13927
|
function auditEmail(html, options) {
|
|
13177
|
-
return fromHtml(html, EMPTY_AUDIT, ($, h) => runAudit($, h, options == null ? void 0 : options.framework, options));
|
|
13928
|
+
return fromHtml(html, EMPTY_AUDIT, ($, h) => runAudit($, h, options == null ? void 0 : options.framework, options), options);
|
|
13178
13929
|
}
|
|
13179
13930
|
|
|
13180
13931
|
// src/plain-text.ts
|
|
13181
|
-
var
|
|
13932
|
+
var cheerio5 = __toESM(require("cheerio"), 1);
|
|
13182
13933
|
function toPlainText(html) {
|
|
13183
|
-
const $ =
|
|
13934
|
+
const $ = cheerio5.load(html);
|
|
13184
13935
|
$("style, script, head").remove();
|
|
13185
13936
|
$("[data-skip-in-text='true']").remove();
|
|
13186
13937
|
const lines = [];
|
|
@@ -13282,7 +14033,6 @@ function toPlainText(html) {
|
|
|
13282
14033
|
}
|
|
13283
14034
|
|
|
13284
14035
|
// src/session.ts
|
|
13285
|
-
var cheerio7 = __toESM(require("cheerio"), 1);
|
|
13286
14036
|
function createSession(html, options) {
|
|
13287
14037
|
if (!html || !html.trim()) {
|
|
13288
14038
|
const fw = options == null ? void 0 : options.framework;
|
|
@@ -13309,16 +14059,17 @@ function createSession(html, options) {
|
|
|
13309
14059
|
if (html.length > MAX_HTML_SIZE) {
|
|
13310
14060
|
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
13311
14061
|
}
|
|
13312
|
-
const $ =
|
|
14062
|
+
const $ = loadHtml(html, options);
|
|
13313
14063
|
const framework = options == null ? void 0 : options.framework;
|
|
14064
|
+
const source = (options == null ? void 0 : options.positions) ? html : void 0;
|
|
13314
14065
|
return {
|
|
13315
14066
|
html,
|
|
13316
14067
|
framework,
|
|
13317
14068
|
audit(opts) {
|
|
13318
|
-
return runAudit($, html, framework, opts);
|
|
14069
|
+
return runAudit($, html, framework, __spreadProps(__spreadValues({}, opts), { positions: options == null ? void 0 : options.positions }));
|
|
13319
14070
|
},
|
|
13320
14071
|
analyze() {
|
|
13321
|
-
return analyzeEmailFromDom($, framework);
|
|
14072
|
+
return analyzeEmailFromDom($, framework, source);
|
|
13322
14073
|
},
|
|
13323
14074
|
score(warnings) {
|
|
13324
14075
|
return generateCompatibilityScore(warnings);
|
|
@@ -13342,13 +14093,13 @@ function createSession(html, options) {
|
|
|
13342
14093
|
return checkSizeFromDom($, html);
|
|
13343
14094
|
},
|
|
13344
14095
|
checkTemplateVariables() {
|
|
13345
|
-
return checkTemplateVariablesFromDom(
|
|
14096
|
+
return checkTemplateVariablesFromDom($, source);
|
|
13346
14097
|
},
|
|
13347
14098
|
checkOverflow() {
|
|
13348
|
-
return checkOverflowFromDom(
|
|
14099
|
+
return checkOverflowFromDom($, source);
|
|
13349
14100
|
},
|
|
13350
14101
|
checkVisual() {
|
|
13351
|
-
return checkVisualFromDom(
|
|
14102
|
+
return checkVisualFromDom($, source);
|
|
13352
14103
|
},
|
|
13353
14104
|
// Transforms create isolated copies since they mutate the DOM
|
|
13354
14105
|
transformForClient(clientId) {
|
|
@@ -13379,18 +14130,22 @@ var CompileError = class extends Error {
|
|
|
13379
14130
|
COMPOUND_VALUE_FEATURES,
|
|
13380
14131
|
CSS_FUNCTION_FEATURES,
|
|
13381
14132
|
CSS_SUPPORT,
|
|
14133
|
+
CSS_SUPPORT_NOTES,
|
|
13382
14134
|
CompileError,
|
|
13383
14135
|
EMAIL_CLIENTS,
|
|
13384
14136
|
EMPTY_DELIVERABILITY,
|
|
13385
14137
|
GENERIC_LINK_TEXT,
|
|
13386
14138
|
HTML_ELEMENT_FEATURES,
|
|
13387
14139
|
MAX_HTML_SIZE,
|
|
14140
|
+
MAX_WARNING_LOCATIONS,
|
|
13388
14141
|
STRUCTURAL_FIX_PROPERTIES,
|
|
14142
|
+
VALUE_CAVEAT_PROPS,
|
|
13389
14143
|
alphaBlend,
|
|
13390
14144
|
analyzeEmail,
|
|
13391
14145
|
analyzeImages,
|
|
13392
14146
|
analyzeSpam,
|
|
13393
14147
|
auditEmail,
|
|
14148
|
+
caveatApplies,
|
|
13394
14149
|
checkAccessibility,
|
|
13395
14150
|
checkOverflow,
|
|
13396
14151
|
checkSize,
|