@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.js
CHANGED
|
@@ -56,7 +56,11 @@ var EMAIL_CLIENTS = [
|
|
|
56
56
|
engine: "Microsoft Word",
|
|
57
57
|
darkModeSupport: true,
|
|
58
58
|
icon: "monitor",
|
|
59
|
-
|
|
59
|
+
// End of support ~Q2 2029 (Microsoft: "supported until at least 2029").
|
|
60
|
+
// The April 2026 date some sources cite is the opt-out phase, when classic
|
|
61
|
+
// stops being the Windows default, not end of support. October 2026 was
|
|
62
|
+
// wrong: the nearest real Oct date is Oct 2025, for legacy Outlook for Mac.
|
|
63
|
+
deprecated: "2029-06"
|
|
60
64
|
},
|
|
61
65
|
{
|
|
62
66
|
id: "outlook-ios",
|
|
@@ -8889,6 +8893,16 @@ function getStyleValue(style, property) {
|
|
|
8889
8893
|
}
|
|
8890
8894
|
return null;
|
|
8891
8895
|
}
|
|
8896
|
+
function getStyleValues(style, property) {
|
|
8897
|
+
const values = [];
|
|
8898
|
+
for (const part of splitStyleDeclarations(style)) {
|
|
8899
|
+
const colonIndex = part.indexOf(":");
|
|
8900
|
+
if (colonIndex === -1) continue;
|
|
8901
|
+
if (part.slice(0, colonIndex).trim().toLowerCase() !== property) continue;
|
|
8902
|
+
values.push(part.slice(colonIndex + 1).trim());
|
|
8903
|
+
}
|
|
8904
|
+
return values;
|
|
8905
|
+
}
|
|
8892
8906
|
function parseInlineStyle(style) {
|
|
8893
8907
|
const map = /* @__PURE__ */ new Map();
|
|
8894
8908
|
const declarations = splitStyleDeclarations(style);
|
|
@@ -9639,6 +9653,7 @@ function downlevelCSS(html) {
|
|
|
9639
9653
|
|
|
9640
9654
|
// src/constants.ts
|
|
9641
9655
|
var MAX_HTML_SIZE = 2 * 1024 * 1024;
|
|
9656
|
+
var MAX_WARNING_LOCATIONS = 100;
|
|
9642
9657
|
var GENERIC_LINK_TEXT = /* @__PURE__ */ new Set([
|
|
9643
9658
|
"click here",
|
|
9644
9659
|
"here",
|
|
@@ -10375,9 +10390,231 @@ function transformForAllClients(html, framework) {
|
|
|
10375
10390
|
}
|
|
10376
10391
|
|
|
10377
10392
|
// src/analyze.ts
|
|
10378
|
-
import * as cheerio4 from "cheerio";
|
|
10379
10393
|
import * as csstree5 from "css-tree";
|
|
10380
10394
|
|
|
10395
|
+
// src/rules/value-caveats.ts
|
|
10396
|
+
var VALUE_CAVEAT_PROPS = /* @__PURE__ */ new Set([
|
|
10397
|
+
"background",
|
|
10398
|
+
"border-radius",
|
|
10399
|
+
"display",
|
|
10400
|
+
"font-size",
|
|
10401
|
+
"font-weight",
|
|
10402
|
+
"letter-spacing",
|
|
10403
|
+
"margin",
|
|
10404
|
+
"overflow",
|
|
10405
|
+
"position",
|
|
10406
|
+
"text-align",
|
|
10407
|
+
"transition"
|
|
10408
|
+
]);
|
|
10409
|
+
function normalize(value) {
|
|
10410
|
+
return value.replace(/\/\*[\s\S]*?\*\//g, " ").toLowerCase().replace(/!\s*important/g, " ").replace(/;+\s*$/, "").replace(/\s+/g, " ").trim();
|
|
10411
|
+
}
|
|
10412
|
+
var preparedValues = /* @__PURE__ */ new WeakMap();
|
|
10413
|
+
function prepare(values) {
|
|
10414
|
+
let out = preparedValues.get(values);
|
|
10415
|
+
if (!out) {
|
|
10416
|
+
out = [...new Set(values.map(normalize))];
|
|
10417
|
+
preparedValues.set(values, out);
|
|
10418
|
+
}
|
|
10419
|
+
return out;
|
|
10420
|
+
}
|
|
10421
|
+
function topLevelSplit(value, sep) {
|
|
10422
|
+
const parts = [];
|
|
10423
|
+
let depth = 0;
|
|
10424
|
+
let start = 0;
|
|
10425
|
+
for (let i = 0; i < value.length; i++) {
|
|
10426
|
+
const c = value[i];
|
|
10427
|
+
if (c === "(") depth++;
|
|
10428
|
+
else if (c === ")") depth = Math.max(0, depth - 1);
|
|
10429
|
+
else if (c === sep && depth === 0) {
|
|
10430
|
+
parts.push(value.slice(start, i));
|
|
10431
|
+
start = i + 1;
|
|
10432
|
+
}
|
|
10433
|
+
}
|
|
10434
|
+
parts.push(value.slice(start));
|
|
10435
|
+
return parts.map((p) => p.trim()).filter(Boolean);
|
|
10436
|
+
}
|
|
10437
|
+
function tokens(value) {
|
|
10438
|
+
return topLevelSplit(value, " ");
|
|
10439
|
+
}
|
|
10440
|
+
function hasUnit(value, units) {
|
|
10441
|
+
return new RegExp(`\\d(?:${units.join("|")})\\b`).test(value);
|
|
10442
|
+
}
|
|
10443
|
+
function bareNumbers(value) {
|
|
10444
|
+
return tokens(value).filter((t) => /^[+-]?\d+(?:\.\d+)?(?:e[+-]?\d+)?$/.test(t)).map(Number);
|
|
10445
|
+
}
|
|
10446
|
+
function hasNegative(value) {
|
|
10447
|
+
return /(?:^|[\s,(])-\.?\d/.test(value);
|
|
10448
|
+
}
|
|
10449
|
+
function dashed(v) {
|
|
10450
|
+
return v.replace(/\s+/g, "-");
|
|
10451
|
+
}
|
|
10452
|
+
function unprefixed(token) {
|
|
10453
|
+
return token.replace(/^-(?:webkit|moz|ms|o)-/, "");
|
|
10454
|
+
}
|
|
10455
|
+
function quotedValues(note) {
|
|
10456
|
+
var _a;
|
|
10457
|
+
const supported = [];
|
|
10458
|
+
const banned = [];
|
|
10459
|
+
for (const m of note.matchAll(/`([^`]+)`/g)) {
|
|
10460
|
+
const clause = (_a = note.slice(0, m.index).split(/[.;]/).pop()) != null ? _a : "";
|
|
10461
|
+
(/\bsupports\b/i.test(clause) ? supported : banned).push(dashed(m[1].toLowerCase().trim()));
|
|
10462
|
+
}
|
|
10463
|
+
return { supported, banned };
|
|
10464
|
+
}
|
|
10465
|
+
function isColorOnly(value) {
|
|
10466
|
+
if (value.startsWith("#")) return /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(value);
|
|
10467
|
+
if (parseColor(value)) return true;
|
|
10468
|
+
return ["none", "inherit", "initial", "unset", "revert", "currentcolor"].includes(value) || // A custom property is usually a palette colour, and we cannot resolve it.
|
|
10469
|
+
value.startsWith("var(");
|
|
10470
|
+
}
|
|
10471
|
+
var POSITION_KEYWORDS = ["relative", "absolute", "fixed", "sticky"];
|
|
10472
|
+
var RELATIVE_FONT_UNITS = [
|
|
10473
|
+
"cqmin",
|
|
10474
|
+
"cqmax",
|
|
10475
|
+
"vmin",
|
|
10476
|
+
"vmax",
|
|
10477
|
+
"rlh",
|
|
10478
|
+
"rex",
|
|
10479
|
+
"rch",
|
|
10480
|
+
"ric",
|
|
10481
|
+
"rem",
|
|
10482
|
+
"svw",
|
|
10483
|
+
"svh",
|
|
10484
|
+
"lvw",
|
|
10485
|
+
"lvh",
|
|
10486
|
+
"dvw",
|
|
10487
|
+
"dvh",
|
|
10488
|
+
"cqw",
|
|
10489
|
+
"cqh",
|
|
10490
|
+
"cqi",
|
|
10491
|
+
"cqb",
|
|
10492
|
+
"cap",
|
|
10493
|
+
"ic",
|
|
10494
|
+
"lh",
|
|
10495
|
+
"em",
|
|
10496
|
+
"ex",
|
|
10497
|
+
"ch",
|
|
10498
|
+
"vw",
|
|
10499
|
+
"vh",
|
|
10500
|
+
"vi",
|
|
10501
|
+
"vb"
|
|
10502
|
+
];
|
|
10503
|
+
var NOT_A_PROPERTY_NAME = /* @__PURE__ */ new Set([
|
|
10504
|
+
"all",
|
|
10505
|
+
"allow-discrete",
|
|
10506
|
+
"normal",
|
|
10507
|
+
"ease",
|
|
10508
|
+
"ease-in",
|
|
10509
|
+
"ease-out",
|
|
10510
|
+
"ease-in-out",
|
|
10511
|
+
"linear",
|
|
10512
|
+
"step-start",
|
|
10513
|
+
"step-end",
|
|
10514
|
+
"none",
|
|
10515
|
+
"initial",
|
|
10516
|
+
"inherit",
|
|
10517
|
+
"unset",
|
|
10518
|
+
"revert",
|
|
10519
|
+
"revert-layer"
|
|
10520
|
+
]);
|
|
10521
|
+
function namesAProperty(layer) {
|
|
10522
|
+
return tokens(layer).some((t) => /^[a-z][a-z0-9-]*$/.test(t) && !NOT_A_PROPERTY_NAME.has(t));
|
|
10523
|
+
}
|
|
10524
|
+
function triggers(prop, value, note, noteLc) {
|
|
10525
|
+
switch (prop) {
|
|
10526
|
+
case "margin": {
|
|
10527
|
+
const negative = noteLc.includes("negative");
|
|
10528
|
+
const auto = noteLc.includes("auto");
|
|
10529
|
+
if (!negative && !auto) return true;
|
|
10530
|
+
return negative && hasNegative(value) || auto && /\bauto\b/.test(value);
|
|
10531
|
+
}
|
|
10532
|
+
case "position": {
|
|
10533
|
+
const used = POSITION_KEYWORDS.find((k) => tokens(value).some((t) => unprefixed(t) === k));
|
|
10534
|
+
if (!used) return false;
|
|
10535
|
+
const m = note.match(/supports\s+.+?\s+but not\s+([^.]+)/i);
|
|
10536
|
+
if (m) return m[1].toLowerCase().includes(used);
|
|
10537
|
+
return true;
|
|
10538
|
+
}
|
|
10539
|
+
case "overflow": {
|
|
10540
|
+
if (!/\b(?:auto|scroll|overlay)\b/.test(value)) return false;
|
|
10541
|
+
if (!noteLc.includes("cannot scroll") && !noteLc.includes("overflow-block")) return true;
|
|
10542
|
+
return noteLc.includes("cannot scroll");
|
|
10543
|
+
}
|
|
10544
|
+
case "font-size": {
|
|
10545
|
+
if (noteLc.includes("percentage") || noteLc.includes("relative")) {
|
|
10546
|
+
return hasUnit(value, RELATIVE_FONT_UNITS) || /\d\s*%/.test(value) || tokens(value).some((t) => t === "smaller" || t === "larger");
|
|
10547
|
+
}
|
|
10548
|
+
if (noteLc.includes("`rem`")) return hasUnit(value, ["rem"]);
|
|
10549
|
+
return true;
|
|
10550
|
+
}
|
|
10551
|
+
case "display": {
|
|
10552
|
+
const only = note.match(/only supports\s+([^.]*)/i);
|
|
10553
|
+
if (only) {
|
|
10554
|
+
const allowed = quotedValues(only[1]).banned.map((q) => q.replace(/^display:-?/, "")).filter((q) => /^[a-z-]+$/.test(q));
|
|
10555
|
+
if (!allowed.length) return true;
|
|
10556
|
+
return !allowed.includes(dashed(value));
|
|
10557
|
+
}
|
|
10558
|
+
const { banned } = quotedValues(note);
|
|
10559
|
+
if (banned.length) {
|
|
10560
|
+
const v = dashed(value);
|
|
10561
|
+
return banned.includes(v) || banned.includes(unprefixed(v));
|
|
10562
|
+
}
|
|
10563
|
+
if (noteLc.includes("two-value syntax")) return tokens(value).length > 1;
|
|
10564
|
+
return true;
|
|
10565
|
+
}
|
|
10566
|
+
case "font-weight": {
|
|
10567
|
+
const nums = bareNumbers(value);
|
|
10568
|
+
if (!nums.length) return false;
|
|
10569
|
+
if (noteLc.includes("font weight")) return nums.some((n) => n !== 400 && n !== 700);
|
|
10570
|
+
if (noteLc.includes("only the following numeric values")) {
|
|
10571
|
+
return nums.some((n) => n % 100 !== 0 || n < 100 || n > 900);
|
|
10572
|
+
}
|
|
10573
|
+
return true;
|
|
10574
|
+
}
|
|
10575
|
+
case "border-radius": {
|
|
10576
|
+
if (noteLc.includes("slash")) return topLevelSplit(value, "/").length > 1;
|
|
10577
|
+
return true;
|
|
10578
|
+
}
|
|
10579
|
+
case "text-align": {
|
|
10580
|
+
const { supported, banned } = quotedValues(note);
|
|
10581
|
+
if (!banned.length) return true;
|
|
10582
|
+
return tokens(value).some(
|
|
10583
|
+
(t) => !supported.includes(t) && (banned.includes(t) || banned.includes(unprefixed(t)))
|
|
10584
|
+
);
|
|
10585
|
+
}
|
|
10586
|
+
case "background": {
|
|
10587
|
+
if (noteLc.includes("only `background-color`")) return !isColorOnly(value);
|
|
10588
|
+
if (noteLc.includes("multiple values")) {
|
|
10589
|
+
return topLevelSplit(value, ",").length > 1 || topLevelSplit(value, "/").length > 1;
|
|
10590
|
+
}
|
|
10591
|
+
return true;
|
|
10592
|
+
}
|
|
10593
|
+
case "letter-spacing": {
|
|
10594
|
+
const negative = noteLc.includes("negative");
|
|
10595
|
+
const em = noteLc.includes("`em`");
|
|
10596
|
+
if (!negative && !em) return true;
|
|
10597
|
+
return negative && hasNegative(value) || em && hasUnit(value, ["em"]);
|
|
10598
|
+
}
|
|
10599
|
+
case "transition": {
|
|
10600
|
+
if (noteLc.includes("`all`")) {
|
|
10601
|
+
if (["none", "inherit", "initial", "unset", "revert"].includes(value)) return false;
|
|
10602
|
+
return topLevelSplit(value, ",").some((layer) => !namesAProperty(layer));
|
|
10603
|
+
}
|
|
10604
|
+
return true;
|
|
10605
|
+
}
|
|
10606
|
+
default:
|
|
10607
|
+
return true;
|
|
10608
|
+
}
|
|
10609
|
+
}
|
|
10610
|
+
function caveatApplies(prop, values, notes) {
|
|
10611
|
+
if (!VALUE_CAVEAT_PROPS.has(prop)) return true;
|
|
10612
|
+
if (!(values == null ? void 0 : values.length)) return true;
|
|
10613
|
+
const note = (notes != null ? notes : []).join(" ");
|
|
10614
|
+
const noteLc = note.toLowerCase();
|
|
10615
|
+
return prepare(values).some((value) => triggers(prop, value, note, noteLc));
|
|
10616
|
+
}
|
|
10617
|
+
|
|
10381
10618
|
// src/dark-mode-checker.ts
|
|
10382
10619
|
import * as csstree4 from "css-tree";
|
|
10383
10620
|
|
|
@@ -10654,6 +10891,257 @@ function applyColorInversion($, mode) {
|
|
|
10654
10891
|
});
|
|
10655
10892
|
}
|
|
10656
10893
|
|
|
10894
|
+
// src/source-location.ts
|
|
10895
|
+
function toLoc(p) {
|
|
10896
|
+
return {
|
|
10897
|
+
line: p.startLine,
|
|
10898
|
+
column: p.startCol,
|
|
10899
|
+
endLine: p.endLine,
|
|
10900
|
+
endColumn: p.endCol,
|
|
10901
|
+
offset: p.startOffset,
|
|
10902
|
+
length: p.endOffset - p.startOffset
|
|
10903
|
+
};
|
|
10904
|
+
}
|
|
10905
|
+
function locOfElement(el) {
|
|
10906
|
+
var _a;
|
|
10907
|
+
const raw = el == null ? void 0 : el.sourceCodeLocation;
|
|
10908
|
+
if (!raw) return void 0;
|
|
10909
|
+
return toLoc((_a = raw.startTag) != null ? _a : raw);
|
|
10910
|
+
}
|
|
10911
|
+
function locOfAttr(el, attr) {
|
|
10912
|
+
var _a, _b, _c, _d;
|
|
10913
|
+
const raw = el == null ? void 0 : el.sourceCodeLocation;
|
|
10914
|
+
if (!raw) return void 0;
|
|
10915
|
+
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];
|
|
10916
|
+
return attrLoc ? toLoc(attrLoc) : locOfElement(el);
|
|
10917
|
+
}
|
|
10918
|
+
function locInAttr(attrLoc, source, property, occurrence = 0) {
|
|
10919
|
+
if (!attrLoc || !source) return void 0;
|
|
10920
|
+
const raw = source.slice(attrLoc.offset, attrLoc.offset + attrLoc.length);
|
|
10921
|
+
const open = raw.search(/["']/);
|
|
10922
|
+
const close = raw.lastIndexOf(raw[open]);
|
|
10923
|
+
if (open === -1 || close <= open) return void 0;
|
|
10924
|
+
const found = declarationsIn(raw.slice(open + 1, close), property);
|
|
10925
|
+
const hit = found[occurrence];
|
|
10926
|
+
if (!hit) return void 0;
|
|
10927
|
+
const start = attrLoc.offset + open + 1 + hit.start;
|
|
10928
|
+
const end = attrLoc.offset + open + 1 + hit.end;
|
|
10929
|
+
const from = positionOf(source, start);
|
|
10930
|
+
const to = positionOf(source, end);
|
|
10931
|
+
return {
|
|
10932
|
+
line: from.line,
|
|
10933
|
+
column: from.column,
|
|
10934
|
+
endLine: to.line,
|
|
10935
|
+
endColumn: to.column,
|
|
10936
|
+
offset: start,
|
|
10937
|
+
length: end - start
|
|
10938
|
+
};
|
|
10939
|
+
}
|
|
10940
|
+
function declarationsIn(value, property) {
|
|
10941
|
+
const wanted = property.toLowerCase();
|
|
10942
|
+
const found = [];
|
|
10943
|
+
let depth = 0;
|
|
10944
|
+
let start = 0;
|
|
10945
|
+
const consider = (from, to) => {
|
|
10946
|
+
const text = value.slice(from, to);
|
|
10947
|
+
const colon = text.indexOf(":");
|
|
10948
|
+
if (colon === -1) return;
|
|
10949
|
+
if (text.slice(0, colon).trim().toLowerCase() !== wanted) return;
|
|
10950
|
+
const lead = text.length - text.trimStart().length;
|
|
10951
|
+
const trail = text.length - text.trimEnd().length;
|
|
10952
|
+
if (from + lead < to - trail) found.push({ start: from + lead, end: to - trail });
|
|
10953
|
+
};
|
|
10954
|
+
for (let i = 0; i < value.length; i++) {
|
|
10955
|
+
const c = value[i];
|
|
10956
|
+
if (c === "(") depth++;
|
|
10957
|
+
else if (c === ")") depth = Math.max(0, depth - 1);
|
|
10958
|
+
else if (c === ";" && depth === 0) {
|
|
10959
|
+
consider(start, i);
|
|
10960
|
+
start = i + 1;
|
|
10961
|
+
}
|
|
10962
|
+
}
|
|
10963
|
+
consider(start, value.length);
|
|
10964
|
+
return found;
|
|
10965
|
+
}
|
|
10966
|
+
function locOfFirst($, selector) {
|
|
10967
|
+
const el = $(selector).first()[0];
|
|
10968
|
+
return el ? locOfElement(el) : void 0;
|
|
10969
|
+
}
|
|
10970
|
+
function cssBlockAnchor(styleEl, cssText, source) {
|
|
10971
|
+
var _a;
|
|
10972
|
+
const children = styleEl == null ? void 0 : styleEl.children;
|
|
10973
|
+
if (!children || children.length !== 1) return void 0;
|
|
10974
|
+
const loc = (_a = children[0]) == null ? void 0 : _a.sourceCodeLocation;
|
|
10975
|
+
if (!loc) return void 0;
|
|
10976
|
+
const mapper = source ? crMapper(source.slice(loc.startOffset, loc.endOffset), cssText) : null;
|
|
10977
|
+
const extraBefore = mapper ? (index) => mapper(index) - index : crOffsetter(cssText, loc.endOffset - loc.startOffset);
|
|
10978
|
+
return __spreadValues({ loc, extraBefore }, mapper ? { source } : {});
|
|
10979
|
+
}
|
|
10980
|
+
function crMapper(raw, decoded) {
|
|
10981
|
+
if (raw.length === decoded.length) return (index) => index;
|
|
10982
|
+
const points = [];
|
|
10983
|
+
const extras = [];
|
|
10984
|
+
let r = 0;
|
|
10985
|
+
let d = 0;
|
|
10986
|
+
let extra = 0;
|
|
10987
|
+
while (d < decoded.length) {
|
|
10988
|
+
if (r >= raw.length) return null;
|
|
10989
|
+
if (raw[r] === decoded[d]) {
|
|
10990
|
+
r++;
|
|
10991
|
+
d++;
|
|
10992
|
+
continue;
|
|
10993
|
+
}
|
|
10994
|
+
if (raw[r] === "\r" && decoded[d] === "\n") {
|
|
10995
|
+
const consumed = raw[r + 1] === "\n" ? 2 : 1;
|
|
10996
|
+
extra += consumed - 1;
|
|
10997
|
+
points.push(d);
|
|
10998
|
+
extras.push(extra);
|
|
10999
|
+
r += consumed;
|
|
11000
|
+
d += 1;
|
|
11001
|
+
continue;
|
|
11002
|
+
}
|
|
11003
|
+
return null;
|
|
11004
|
+
}
|
|
11005
|
+
if (r !== raw.length) return null;
|
|
11006
|
+
return (index) => index + lookup(points, extras, index);
|
|
11007
|
+
}
|
|
11008
|
+
function lookup(points, extras, index) {
|
|
11009
|
+
let lo = 0;
|
|
11010
|
+
let hi = points.length - 1;
|
|
11011
|
+
let found = 0;
|
|
11012
|
+
while (lo <= hi) {
|
|
11013
|
+
const mid = lo + hi >> 1;
|
|
11014
|
+
if (points[mid] < index) {
|
|
11015
|
+
found = extras[mid];
|
|
11016
|
+
lo = mid + 1;
|
|
11017
|
+
} else {
|
|
11018
|
+
hi = mid - 1;
|
|
11019
|
+
}
|
|
11020
|
+
}
|
|
11021
|
+
return found;
|
|
11022
|
+
}
|
|
11023
|
+
function findRawOffset(raw, decoded, index, token) {
|
|
11024
|
+
if (!token) return -1;
|
|
11025
|
+
let occurrence = 0;
|
|
11026
|
+
for (let at = decoded.indexOf(token); at !== -1 && at < index; at = decoded.indexOf(token, at + 1)) {
|
|
11027
|
+
occurrence++;
|
|
11028
|
+
}
|
|
11029
|
+
let found = -1;
|
|
11030
|
+
let from = 0;
|
|
11031
|
+
for (let i = 0; i <= occurrence; i++) {
|
|
11032
|
+
found = raw.indexOf(token, from);
|
|
11033
|
+
if (found === -1) return -1;
|
|
11034
|
+
from = found + 1;
|
|
11035
|
+
}
|
|
11036
|
+
return found;
|
|
11037
|
+
}
|
|
11038
|
+
function positionOf(source, offset) {
|
|
11039
|
+
const prefix = source.slice(0, offset);
|
|
11040
|
+
return { line: prefix.split("\n").length, column: offset - prefix.lastIndexOf("\n") };
|
|
11041
|
+
}
|
|
11042
|
+
function crOffsetter(text, rawLength) {
|
|
11043
|
+
const removed = rawLength - text.length;
|
|
11044
|
+
if (removed === 0) return () => 0;
|
|
11045
|
+
const newlines = countNewlines(text, text.length);
|
|
11046
|
+
if (removed !== newlines || newlines === 0) return null;
|
|
11047
|
+
return (index) => countNewlines(text, index);
|
|
11048
|
+
}
|
|
11049
|
+
function countNewlines(text, upTo) {
|
|
11050
|
+
let n = 0;
|
|
11051
|
+
for (let i = 0; i < upTo && i < text.length; i++) if (text.charCodeAt(i) === 10) n++;
|
|
11052
|
+
return n;
|
|
11053
|
+
}
|
|
11054
|
+
function locInCssBlock(anchor, cssLoc) {
|
|
11055
|
+
if (!anchor || !cssLoc) return void 0;
|
|
11056
|
+
const { loc: block, extraBefore } = anchor;
|
|
11057
|
+
if (!extraBefore) {
|
|
11058
|
+
return {
|
|
11059
|
+
line: block.startLine,
|
|
11060
|
+
column: block.startCol,
|
|
11061
|
+
endLine: block.startLine,
|
|
11062
|
+
endColumn: block.startCol,
|
|
11063
|
+
offset: block.startOffset,
|
|
11064
|
+
length: 0
|
|
11065
|
+
};
|
|
11066
|
+
}
|
|
11067
|
+
const line = block.startLine + cssLoc.start.line - 1;
|
|
11068
|
+
const column = cssLoc.start.line === 1 ? block.startCol + cssLoc.start.column - 1 : cssLoc.start.column;
|
|
11069
|
+
const endLine = block.startLine + cssLoc.end.line - 1;
|
|
11070
|
+
const endColumn = cssLoc.end.line === 1 ? block.startCol + cssLoc.end.column - 1 : cssLoc.end.column;
|
|
11071
|
+
const start = block.startOffset + cssLoc.start.offset + extraBefore(cssLoc.start.offset);
|
|
11072
|
+
const end = block.startOffset + cssLoc.end.offset + extraBefore(cssLoc.end.offset);
|
|
11073
|
+
if (anchor.source) {
|
|
11074
|
+
const from = positionOf(anchor.source, start);
|
|
11075
|
+
const to = positionOf(anchor.source, end);
|
|
11076
|
+
return {
|
|
11077
|
+
line: from.line,
|
|
11078
|
+
column: from.column,
|
|
11079
|
+
endLine: to.line,
|
|
11080
|
+
endColumn: to.column,
|
|
11081
|
+
offset: start,
|
|
11082
|
+
length: end - start
|
|
11083
|
+
};
|
|
11084
|
+
}
|
|
11085
|
+
return { line, column, endLine, endColumn, offset: start, length: end - start };
|
|
11086
|
+
}
|
|
11087
|
+
function locInTextNode(node, index, length, source) {
|
|
11088
|
+
var _a;
|
|
11089
|
+
const anchor = node == null ? void 0 : node.sourceCodeLocation;
|
|
11090
|
+
if (!anchor) return void 0;
|
|
11091
|
+
const data = (_a = node.data) != null ? _a : "";
|
|
11092
|
+
const rawLength = anchor.endOffset - anchor.startOffset;
|
|
11093
|
+
if (source) {
|
|
11094
|
+
const raw = source.slice(anchor.startOffset, anchor.endOffset);
|
|
11095
|
+
const token = data.slice(index, index + length);
|
|
11096
|
+
const at = findRawOffset(raw, data, index, token);
|
|
11097
|
+
if (at !== -1) {
|
|
11098
|
+
const start2 = anchor.startOffset + at;
|
|
11099
|
+
const from = positionOf(source, start2);
|
|
11100
|
+
const to = positionOf(source, start2 + token.length);
|
|
11101
|
+
return {
|
|
11102
|
+
line: from.line,
|
|
11103
|
+
column: from.column,
|
|
11104
|
+
endLine: to.line,
|
|
11105
|
+
endColumn: to.column,
|
|
11106
|
+
offset: start2,
|
|
11107
|
+
length: token.length
|
|
11108
|
+
};
|
|
11109
|
+
}
|
|
11110
|
+
}
|
|
11111
|
+
const extraBefore = crOffsetter(data, rawLength);
|
|
11112
|
+
if (!extraBefore) {
|
|
11113
|
+
const clamped = Math.min(length, rawLength);
|
|
11114
|
+
return {
|
|
11115
|
+
line: anchor.startLine,
|
|
11116
|
+
column: anchor.startCol,
|
|
11117
|
+
endLine: anchor.startLine,
|
|
11118
|
+
endColumn: anchor.startCol + clamped,
|
|
11119
|
+
offset: anchor.startOffset,
|
|
11120
|
+
length: clamped
|
|
11121
|
+
};
|
|
11122
|
+
}
|
|
11123
|
+
const start = positionAt(data, index, anchor);
|
|
11124
|
+
const end = positionAt(data, index + length, anchor);
|
|
11125
|
+
const startOffset = anchor.startOffset + index + extraBefore(index);
|
|
11126
|
+
const endOffset = anchor.startOffset + index + length + extraBefore(index + length);
|
|
11127
|
+
return {
|
|
11128
|
+
line: start.line,
|
|
11129
|
+
column: start.column,
|
|
11130
|
+
endLine: end.line,
|
|
11131
|
+
endColumn: end.column,
|
|
11132
|
+
offset: startOffset,
|
|
11133
|
+
length: endOffset - startOffset
|
|
11134
|
+
};
|
|
11135
|
+
}
|
|
11136
|
+
function positionAt(data, index, anchor) {
|
|
11137
|
+
const prefix = data.slice(0, index);
|
|
11138
|
+
const newlines = prefix.split("\n").length - 1;
|
|
11139
|
+
if (newlines === 0) {
|
|
11140
|
+
return { line: anchor.startLine, column: anchor.startCol + index };
|
|
11141
|
+
}
|
|
11142
|
+
return { line: anchor.startLine + newlines, column: index - prefix.lastIndexOf("\n") };
|
|
11143
|
+
}
|
|
11144
|
+
|
|
10657
11145
|
// src/dark-mode-checker.ts
|
|
10658
11146
|
var DARK_MEDIA_RE = /\(\s*prefers-color-scheme\s*:\s*dark\s*\)/i;
|
|
10659
11147
|
var MAX_UNCOVERED_ELEMENTS = 3;
|
|
@@ -10740,7 +11228,7 @@ function describeSelector($, el) {
|
|
|
10740
11228
|
if (cls) return `${tag}.${cls.split(/\s+/)[0]}`;
|
|
10741
11229
|
return tag;
|
|
10742
11230
|
}
|
|
10743
|
-
function checkDarkModeFromDom(
|
|
11231
|
+
function checkDarkModeFromDom($, source) {
|
|
10744
11232
|
var _a, _b;
|
|
10745
11233
|
const darkBlock = collectDarkBlocks($);
|
|
10746
11234
|
if (!darkBlock) return [];
|
|
@@ -10750,15 +11238,16 @@ function checkDarkModeFromDom($) {
|
|
|
10750
11238
|
return name === "color-scheme" || name === "supported-color-schemes";
|
|
10751
11239
|
});
|
|
10752
11240
|
if (!hasOptIn) {
|
|
11241
|
+
const headLoc = locOfFirst($, "head");
|
|
10753
11242
|
for (const clientId of PREFERS_COLOR_SCHEME_CLIENTS) {
|
|
10754
|
-
warnings.push({
|
|
11243
|
+
warnings.push(__spreadValues({
|
|
10755
11244
|
severity: "warning",
|
|
10756
11245
|
client: clientId,
|
|
10757
11246
|
property: "dark-mode-opt-in",
|
|
10758
11247
|
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.`,
|
|
10759
11248
|
suggestion: 'Add both opt-in tags to <head>: <meta name="color-scheme" content="light dark"> and <meta name="supported-color-schemes" content="light dark">.',
|
|
10760
11249
|
fixType: "structural"
|
|
10761
|
-
});
|
|
11250
|
+
}, headLoc ? { loc: headLoc, locs: [headLoc] } : {}));
|
|
10762
11251
|
}
|
|
10763
11252
|
}
|
|
10764
11253
|
if (darkBlock.rules === 0) return warnings;
|
|
@@ -10766,7 +11255,7 @@ function checkDarkModeFromDom($) {
|
|
|
10766
11255
|
const coveredByAny = matchedElements($, darkBlock.any);
|
|
10767
11256
|
let uncovered = 0;
|
|
10768
11257
|
$("[bgcolor], [style]").each((_, el) => {
|
|
10769
|
-
var _a2;
|
|
11258
|
+
var _a2, _b2;
|
|
10770
11259
|
if (uncovered >= MAX_UNCOVERED_ELEMENTS) return false;
|
|
10771
11260
|
const $el = $(el);
|
|
10772
11261
|
const style = parseInlineStyle($el.attr("style") || "");
|
|
@@ -10776,8 +11265,10 @@ function checkDarkModeFromDom($) {
|
|
|
10776
11265
|
if (inline ? coveredByImportant.has(el) : coveredByAny.has(el)) return;
|
|
10777
11266
|
uncovered++;
|
|
10778
11267
|
const selector = describeSelector($, el);
|
|
11268
|
+
const attrLoc = locOfAttr(el, inline ? "style" : "bgcolor");
|
|
11269
|
+
const loc = inline ? (_b2 = locInAttr(attrLoc, source, style.get("background-color") !== void 0 ? "background-color" : "background")) != null ? _b2 : attrLoc : attrLoc;
|
|
10779
11270
|
for (const clientId of PREFERS_COLOR_SCHEME_CLIENTS) {
|
|
10780
|
-
warnings.push({
|
|
11271
|
+
warnings.push(__spreadValues({
|
|
10781
11272
|
severity: "warning",
|
|
10782
11273
|
client: clientId,
|
|
10783
11274
|
property: "dark-mode-coverage",
|
|
@@ -10785,12 +11276,25 @@ function checkDarkModeFromDom($) {
|
|
|
10785
11276
|
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.`,
|
|
10786
11277
|
fixType: "css",
|
|
10787
11278
|
selector
|
|
10788
|
-
});
|
|
11279
|
+
}, loc ? { loc, locs: [loc] } : {}));
|
|
10789
11280
|
}
|
|
10790
11281
|
});
|
|
10791
11282
|
return warnings;
|
|
10792
11283
|
}
|
|
10793
11284
|
|
|
11285
|
+
// src/parse-html.ts
|
|
11286
|
+
import * as cheerio4 from "cheerio";
|
|
11287
|
+
function loadHtml(html, options) {
|
|
11288
|
+
return (options == null ? void 0 : options.positions) ? cheerio4.load(html, { sourceCodeLocationInfo: true }) : cheerio4.load(html);
|
|
11289
|
+
}
|
|
11290
|
+
function fromHtml(html, empty, fn, options) {
|
|
11291
|
+
if (!html || !html.trim()) return empty;
|
|
11292
|
+
if (html.length > MAX_HTML_SIZE) {
|
|
11293
|
+
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
11294
|
+
}
|
|
11295
|
+
return fn(loadHtml(html, options), html);
|
|
11296
|
+
}
|
|
11297
|
+
|
|
10794
11298
|
// src/analyze.ts
|
|
10795
11299
|
var HTML_ELEMENT_SELECTORS = {
|
|
10796
11300
|
"<style>": "style",
|
|
@@ -10843,14 +11347,25 @@ var CSS_FUNCTION_DETECTORS = CSS_FUNCTION_FEATURES.map((fn) => ({
|
|
|
10843
11347
|
pattern: `${fn}(`
|
|
10844
11348
|
// require opening paren — matches "min(" but not "Minion"
|
|
10845
11349
|
}));
|
|
10846
|
-
function analyzeEmailFromDom($, framework) {
|
|
11350
|
+
function analyzeEmailFromDom($, framework, source) {
|
|
10847
11351
|
const warnings = [];
|
|
10848
|
-
const seenWarnings = /* @__PURE__ */ new
|
|
11352
|
+
const seenWarnings = /* @__PURE__ */ new Map();
|
|
10849
11353
|
function addWarning(w) {
|
|
10850
11354
|
const key = `${w.client}:${w.property}:${w.severity}:${w.selector || ""}`;
|
|
10851
|
-
|
|
10852
|
-
|
|
11355
|
+
const existing = seenWarnings.get(key);
|
|
11356
|
+
if (!existing) {
|
|
11357
|
+
seenWarnings.set(key, w);
|
|
10853
11358
|
warnings.push(w);
|
|
11359
|
+
return;
|
|
11360
|
+
}
|
|
11361
|
+
if (!existing.locs || !w.locs) return;
|
|
11362
|
+
for (const loc of w.locs) {
|
|
11363
|
+
if (existing.locs.some((l) => l.offset === loc.offset)) continue;
|
|
11364
|
+
if (existing.locs.length >= MAX_WARNING_LOCATIONS) {
|
|
11365
|
+
existing.locsTruncated = true;
|
|
11366
|
+
break;
|
|
11367
|
+
}
|
|
11368
|
+
existing.locs.push(loc);
|
|
10854
11369
|
}
|
|
10855
11370
|
}
|
|
10856
11371
|
function describeSelector2(el) {
|
|
@@ -10868,10 +11383,16 @@ function analyzeEmailFromDom($, framework) {
|
|
|
10868
11383
|
for (const feature of HTML_ELEMENT_FEATURES) {
|
|
10869
11384
|
const selector = HTML_ELEMENT_SELECTORS[feature];
|
|
10870
11385
|
if (!selector) continue;
|
|
10871
|
-
|
|
11386
|
+
const matches = $(selector);
|
|
11387
|
+
if (matches.length === 0) continue;
|
|
10872
11388
|
const supportData = CSS_SUPPORT[feature];
|
|
10873
11389
|
if (!supportData) continue;
|
|
10874
11390
|
const baseSeverity = HTML_ELEMENT_SEVERITY[feature] || "warning";
|
|
11391
|
+
const found = matches.toArray().map((m) => locOfElement(m)).filter((l) => l !== void 0);
|
|
11392
|
+
const featureOccurrences = found.length ? __spreadValues({
|
|
11393
|
+
locs: found.slice(0, MAX_WARNING_LOCATIONS)
|
|
11394
|
+
}, found.length > MAX_WARNING_LOCATIONS ? { truncated: true } : {}) : void 0;
|
|
11395
|
+
const featureLoc = featureOccurrences == null ? void 0 : featureOccurrences.locs[0];
|
|
10875
11396
|
for (const client of EMAIL_CLIENTS) {
|
|
10876
11397
|
const support = supportData[client.id];
|
|
10877
11398
|
if (support === "unsupported") {
|
|
@@ -10879,7 +11400,7 @@ function analyzeEmailFromDom($, framework) {
|
|
|
10879
11400
|
const message = msgFn ? msgFn(client.name) : `${client.name} does not support ${feature}.`;
|
|
10880
11401
|
const sug = getSuggestion(feature, client.id, framework);
|
|
10881
11402
|
const fix = getCodeFix(feature, client.id, framework);
|
|
10882
|
-
addWarning(__spreadValues({
|
|
11403
|
+
addWarning(__spreadValues(__spreadValues({
|
|
10883
11404
|
severity: baseSeverity,
|
|
10884
11405
|
client: client.id,
|
|
10885
11406
|
property: feature,
|
|
@@ -10887,11 +11408,11 @@ function analyzeEmailFromDom($, framework) {
|
|
|
10887
11408
|
suggestion: sug.text,
|
|
10888
11409
|
fix,
|
|
10889
11410
|
fixType: getFixType(feature)
|
|
10890
|
-
}, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(feature, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
11411
|
+
}, featureOccurrences ? occurrenceFields(featureOccurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(feature, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
10891
11412
|
} else if (support === "partial" && feature === "<style>") {
|
|
10892
11413
|
const sug = getSuggestion("<style>:partial", client.id, framework);
|
|
10893
11414
|
const fix = getCodeFix("<style>", client.id, framework);
|
|
10894
|
-
addWarning(__spreadValues({
|
|
11415
|
+
addWarning(__spreadValues(__spreadValues({
|
|
10895
11416
|
severity: "warning",
|
|
10896
11417
|
client: client.id,
|
|
10897
11418
|
property: "<style>",
|
|
@@ -10899,55 +11420,97 @@ function analyzeEmailFromDom($, framework) {
|
|
|
10899
11420
|
suggestion: sug.text,
|
|
10900
11421
|
fix,
|
|
10901
11422
|
fixType: getFixType("<style>")
|
|
10902
|
-
}, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<style>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
11423
|
+
}, featureOccurrences ? occurrenceFields(featureOccurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<style>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
10903
11424
|
}
|
|
10904
11425
|
}
|
|
10905
11426
|
}
|
|
10906
11427
|
const parsedAtRules = /* @__PURE__ */ new Set();
|
|
11428
|
+
const selectorLocs = /* @__PURE__ */ new Map();
|
|
10907
11429
|
const parsedProperties = /* @__PURE__ */ new Set();
|
|
10908
11430
|
const propertyLines = /* @__PURE__ */ new Map();
|
|
11431
|
+
const propertyLocs = /* @__PURE__ */ new Map();
|
|
10909
11432
|
const propertyValues = /* @__PURE__ */ new Map();
|
|
10910
11433
|
const detectedCssFunctions = /* @__PURE__ */ new Set();
|
|
10911
11434
|
const detectedPseudoClasses = /* @__PURE__ */ new Set();
|
|
10912
11435
|
const detectedPseudoElements = /* @__PURE__ */ new Set();
|
|
11436
|
+
let blockAnchor;
|
|
11437
|
+
function recordSelectorLoc(key, cssLoc) {
|
|
11438
|
+
if (!cssLoc) return;
|
|
11439
|
+
const loc = locInCssBlock(blockAnchor, cssLoc);
|
|
11440
|
+
if (!loc) return;
|
|
11441
|
+
const seen = selectorLocs.get(key);
|
|
11442
|
+
if (!seen) {
|
|
11443
|
+
selectorLocs.set(key, { locs: [loc] });
|
|
11444
|
+
return;
|
|
11445
|
+
}
|
|
11446
|
+
if (seen.locs.some((l) => l.offset === loc.offset)) return;
|
|
11447
|
+
if (seen.locs.length >= MAX_WARNING_LOCATIONS) {
|
|
11448
|
+
seen.truncated = true;
|
|
11449
|
+
return;
|
|
11450
|
+
}
|
|
11451
|
+
seen.locs.push(loc);
|
|
11452
|
+
}
|
|
11453
|
+
function recordLoc(key, cssLoc, value) {
|
|
11454
|
+
const loc = locInCssBlock(blockAnchor, cssLoc);
|
|
11455
|
+
if (!loc) return;
|
|
11456
|
+
const seen = propertyLocs.get(key);
|
|
11457
|
+
if (!seen) {
|
|
11458
|
+
propertyLocs.set(key, __spreadValues({ locs: [loc] }, value !== void 0 ? { values: [value] } : {}));
|
|
11459
|
+
return;
|
|
11460
|
+
}
|
|
11461
|
+
if (seen.locs.some((l) => l.offset === loc.offset)) return;
|
|
11462
|
+
if (seen.locs.length >= MAX_WARNING_LOCATIONS) {
|
|
11463
|
+
seen.truncated = true;
|
|
11464
|
+
return;
|
|
11465
|
+
}
|
|
11466
|
+
seen.locs.push(loc);
|
|
11467
|
+
if (seen.values && value !== void 0) seen.values.push(value);
|
|
11468
|
+
}
|
|
10913
11469
|
$("style").each((_, el) => {
|
|
10914
11470
|
const cssText = $(el).text();
|
|
11471
|
+
blockAnchor = cssBlockAnchor(el, cssText, source);
|
|
10915
11472
|
try {
|
|
10916
11473
|
const ast = csstree5.parse(cssText, { parseCustomProperty: true, positions: true });
|
|
10917
11474
|
csstree5.walk(ast, {
|
|
10918
11475
|
enter(node) {
|
|
10919
11476
|
if (node.type === "Atrule") {
|
|
10920
11477
|
parsedAtRules.add(`@${node.name}`);
|
|
11478
|
+
recordSelectorLoc(`@${node.name}`, node.loc);
|
|
10921
11479
|
}
|
|
10922
11480
|
if (node.type === "PseudoClassSelector") {
|
|
10923
11481
|
detectedPseudoClasses.add(`:${node.name}`);
|
|
11482
|
+
recordSelectorLoc(`:${node.name}`, node.loc);
|
|
10924
11483
|
}
|
|
10925
11484
|
if (node.type === "PseudoElementSelector") {
|
|
10926
11485
|
detectedPseudoElements.add(`::${node.name}`);
|
|
11486
|
+
recordSelectorLoc(`::${node.name}`, node.loc);
|
|
10927
11487
|
}
|
|
10928
11488
|
if (node.type === "Declaration") {
|
|
10929
11489
|
const prop = node.property.toLowerCase();
|
|
10930
11490
|
parsedProperties.add(prop);
|
|
10931
|
-
if (node.loc && !propertyLines.has(prop)) {
|
|
10932
|
-
propertyLines.set(prop, node.loc.start.line);
|
|
10933
|
-
}
|
|
10934
11491
|
const valueStr = csstree5.generate(node.value);
|
|
10935
11492
|
const seenValues = propertyValues.get(prop);
|
|
10936
11493
|
if (seenValues) seenValues.push(valueStr);
|
|
10937
11494
|
else propertyValues.set(prop, [valueStr]);
|
|
11495
|
+
if (node.loc) {
|
|
11496
|
+
if (!propertyLines.has(prop)) propertyLines.set(prop, node.loc.start.line);
|
|
11497
|
+
recordLoc(prop, node.loc, valueStr);
|
|
11498
|
+
}
|
|
10938
11499
|
for (const det of COMPOUND_DETECTORS) {
|
|
10939
|
-
if (prop === det.property && valueStr.includes(det.valueIncludes)) {
|
|
11500
|
+
if (prop === det.property && valueStr.toLowerCase().includes(det.valueIncludes)) {
|
|
10940
11501
|
parsedProperties.add(det.key);
|
|
10941
|
-
if (node.loc
|
|
10942
|
-
propertyLines.set(det.key, node.loc.start.line);
|
|
11502
|
+
if (node.loc) {
|
|
11503
|
+
if (!propertyLines.has(det.key)) propertyLines.set(det.key, node.loc.start.line);
|
|
11504
|
+
recordLoc(det.key, node.loc);
|
|
10943
11505
|
}
|
|
10944
11506
|
}
|
|
10945
11507
|
}
|
|
10946
11508
|
for (const fn of CSS_FUNCTION_DETECTORS) {
|
|
10947
11509
|
if (valueStr.includes(fn.pattern)) {
|
|
10948
11510
|
detectedCssFunctions.add(fn.key);
|
|
10949
|
-
if (node.loc
|
|
10950
|
-
propertyLines.set(fn.key, node.loc.start.line);
|
|
11511
|
+
if (node.loc) {
|
|
11512
|
+
if (!propertyLines.has(fn.key)) propertyLines.set(fn.key, node.loc.start.line);
|
|
11513
|
+
recordLoc(fn.key, node.loc);
|
|
10951
11514
|
}
|
|
10952
11515
|
}
|
|
10953
11516
|
}
|
|
@@ -10959,33 +11522,69 @@ function analyzeEmailFromDom($, framework) {
|
|
|
10959
11522
|
});
|
|
10960
11523
|
for (const atRule of AT_RULE_FEATURES) {
|
|
10961
11524
|
if (!parsedAtRules.has(atRule)) continue;
|
|
10962
|
-
checkPropertySupport(atRule, addWarning, framework);
|
|
11525
|
+
checkPropertySupport(atRule, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(atRule));
|
|
10963
11526
|
}
|
|
10964
11527
|
const cssPropertiesToCheck = Object.keys(CSS_SUPPORT).filter(
|
|
10965
11528
|
(k) => !k.startsWith("<") && !k.startsWith("@")
|
|
10966
11529
|
);
|
|
10967
11530
|
$("[style]").each((_, el) => {
|
|
10968
|
-
var _a;
|
|
10969
11531
|
const style = $(el).attr("style") || "";
|
|
10970
11532
|
const props = parseStyleProperties(style);
|
|
10971
11533
|
const selector = describeSelector2(el);
|
|
11534
|
+
const attrLoc = locOfAttr(el, "style");
|
|
11535
|
+
const declarationLocs = (prop, occurrence = 0) => {
|
|
11536
|
+
var _a;
|
|
11537
|
+
return (_a = elementLocs(locInAttr(attrLoc, source, prop, occurrence))) != null ? _a : elementLocs(attrLoc);
|
|
11538
|
+
};
|
|
11539
|
+
const locs = elementLocs(attrLoc);
|
|
10972
11540
|
for (const prop of props) {
|
|
10973
11541
|
for (const det of COMPOUND_DETECTORS) {
|
|
10974
11542
|
if (prop === det.property) {
|
|
10975
11543
|
const value2 = getStyleValue(style, prop);
|
|
10976
|
-
if (value2 == null ? void 0 : value2.includes(det.valueIncludes)) {
|
|
10977
|
-
checkPropertySupport(
|
|
11544
|
+
if (value2 == null ? void 0 : value2.toLowerCase().includes(det.valueIncludes)) {
|
|
11545
|
+
checkPropertySupport(
|
|
11546
|
+
det.key,
|
|
11547
|
+
addWarning,
|
|
11548
|
+
framework,
|
|
11549
|
+
selector,
|
|
11550
|
+
void 0,
|
|
11551
|
+
void 0,
|
|
11552
|
+
declarationLocs(prop)
|
|
11553
|
+
);
|
|
10978
11554
|
}
|
|
10979
11555
|
}
|
|
10980
11556
|
}
|
|
10981
11557
|
if (cssPropertiesToCheck.includes(prop)) {
|
|
10982
|
-
|
|
11558
|
+
const declared = getStyleValues(style, prop);
|
|
11559
|
+
const placed = [];
|
|
11560
|
+
declared.forEach((value2, i) => {
|
|
11561
|
+
const at = locInAttr(attrLoc, source, prop, i);
|
|
11562
|
+
if (at) placed.push({ value: value2, loc: at });
|
|
11563
|
+
});
|
|
11564
|
+
const occurrences = placed.length === declared.length && placed.length > 0 ? { locs: placed.map((p) => p.loc), values: placed.map((p) => p.value) } : locs;
|
|
11565
|
+
checkPropertySupport(
|
|
11566
|
+
prop,
|
|
11567
|
+
addWarning,
|
|
11568
|
+
framework,
|
|
11569
|
+
selector,
|
|
11570
|
+
void 0,
|
|
11571
|
+
declared.length ? declared : void 0,
|
|
11572
|
+
occurrences
|
|
11573
|
+
);
|
|
10983
11574
|
}
|
|
10984
11575
|
const value = getStyleValue(style, prop);
|
|
10985
11576
|
if (value) {
|
|
10986
11577
|
for (const fn of CSS_FUNCTION_DETECTORS) {
|
|
10987
11578
|
if (value.includes(fn.pattern)) {
|
|
10988
|
-
checkPropertySupport(
|
|
11579
|
+
checkPropertySupport(
|
|
11580
|
+
fn.key,
|
|
11581
|
+
addWarning,
|
|
11582
|
+
framework,
|
|
11583
|
+
selector,
|
|
11584
|
+
void 0,
|
|
11585
|
+
void 0,
|
|
11586
|
+
declarationLocs(prop)
|
|
11587
|
+
);
|
|
10989
11588
|
}
|
|
10990
11589
|
}
|
|
10991
11590
|
}
|
|
@@ -11001,87 +11600,66 @@ function analyzeEmailFromDom($, framework) {
|
|
|
11001
11600
|
framework,
|
|
11002
11601
|
void 0,
|
|
11003
11602
|
propertyLines.get(prop),
|
|
11004
|
-
values
|
|
11603
|
+
values,
|
|
11604
|
+
propertyLocs.get(prop)
|
|
11005
11605
|
);
|
|
11006
11606
|
}
|
|
11007
11607
|
for (const compound of COMPOUND_VALUE_FEATURES) {
|
|
11008
11608
|
if (compound.startsWith(":") || compound.startsWith("::")) continue;
|
|
11009
11609
|
if (parsedProperties.has(compound)) {
|
|
11010
|
-
checkPropertySupport(compound, addWarning, framework, void 0, propertyLines.get(compound));
|
|
11610
|
+
checkPropertySupport(compound, addWarning, framework, void 0, propertyLines.get(compound), void 0, propertyLocs.get(compound));
|
|
11011
11611
|
}
|
|
11012
11612
|
}
|
|
11013
11613
|
for (const pseudo of detectedPseudoClasses) {
|
|
11014
11614
|
if (CSS_SUPPORT[pseudo]) {
|
|
11015
|
-
checkPropertySupport(pseudo, addWarning, framework);
|
|
11615
|
+
checkPropertySupport(pseudo, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(pseudo));
|
|
11016
11616
|
}
|
|
11017
11617
|
}
|
|
11018
11618
|
for (const pseudo of detectedPseudoElements) {
|
|
11019
11619
|
if (CSS_SUPPORT[pseudo]) {
|
|
11020
|
-
checkPropertySupport(pseudo, addWarning, framework);
|
|
11620
|
+
checkPropertySupport(pseudo, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(pseudo));
|
|
11021
11621
|
}
|
|
11022
11622
|
}
|
|
11023
11623
|
for (const fn of detectedCssFunctions) {
|
|
11024
|
-
checkPropertySupport(fn, addWarning, framework, void 0, propertyLines.get(fn));
|
|
11624
|
+
checkPropertySupport(fn, addWarning, framework, void 0, propertyLines.get(fn), void 0, propertyLocs.get(fn));
|
|
11025
11625
|
}
|
|
11026
|
-
for (const w of checkDarkModeFromDom(
|
|
11626
|
+
for (const w of checkDarkModeFromDom($, source)) addWarning(w);
|
|
11027
11627
|
const severityOrder = { error: 0, warning: 1, info: 2 };
|
|
11028
11628
|
warnings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
11029
11629
|
return warnings;
|
|
11030
11630
|
}
|
|
11031
|
-
function analyzeEmail(html, framework) {
|
|
11631
|
+
function analyzeEmail(html, framework, options) {
|
|
11032
11632
|
if (!html || !html.trim()) {
|
|
11033
11633
|
return [];
|
|
11034
11634
|
}
|
|
11035
11635
|
if (html.length > MAX_HTML_SIZE) {
|
|
11036
11636
|
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
11037
11637
|
}
|
|
11038
|
-
const $ =
|
|
11039
|
-
return analyzeEmailFromDom($, framework);
|
|
11638
|
+
const $ = loadHtml(html, options);
|
|
11639
|
+
return analyzeEmailFromDom($, framework, (options == null ? void 0 : options.positions) ? html : void 0);
|
|
11040
11640
|
}
|
|
11041
11641
|
function getFixType(prop) {
|
|
11042
11642
|
return STRUCTURAL_FIX_PROPERTIES.has(prop) ? "structural" : "css";
|
|
11043
11643
|
}
|
|
11044
|
-
var VALUE_CAVEAT_PROPS = /* @__PURE__ */ new Set(["margin", "position", "overflow"]);
|
|
11045
|
-
var POSITION_KEYWORDS = ["relative", "absolute", "fixed", "sticky"];
|
|
11046
|
-
function valueTriggersCaveat(prop, value, notes) {
|
|
11047
|
-
const note = (notes != null ? notes : []).join(" ");
|
|
11048
|
-
const noteLc = note.toLowerCase();
|
|
11049
|
-
if (prop === "margin") {
|
|
11050
|
-
if (/(?:^|[\s:(])-\.?\d/.test(value) && noteLc.includes("negative")) return true;
|
|
11051
|
-
if (/\bauto\b/.test(value) && noteLc.includes("auto")) return true;
|
|
11052
|
-
return false;
|
|
11053
|
-
}
|
|
11054
|
-
if (prop === "position") {
|
|
11055
|
-
const used = POSITION_KEYWORDS.find((k) => new RegExp(`\\b${k}\\b`).test(value));
|
|
11056
|
-
if (!used) return false;
|
|
11057
|
-
const m = note.match(/supports\s+.+?\s+but not\s+([^.]+)/i);
|
|
11058
|
-
if (m) return m[1].toLowerCase().includes(used);
|
|
11059
|
-
return used === "fixed" || used === "sticky";
|
|
11060
|
-
}
|
|
11061
|
-
if (prop === "overflow") {
|
|
11062
|
-
if (!/\b(?:auto|scroll)\b/.test(value)) return false;
|
|
11063
|
-
return noteLc.includes("cannot scroll");
|
|
11064
|
-
}
|
|
11065
|
-
return true;
|
|
11066
|
-
}
|
|
11067
11644
|
function noteSuffix(notes) {
|
|
11068
11645
|
if (!(notes == null ? void 0 : notes.length)) return "";
|
|
11069
11646
|
const cleaned = notes.map((n) => n.replace(/^(?:Partial|Buggy|Not supported)\.\s*/i, "").trim()).filter(Boolean);
|
|
11070
11647
|
return cleaned.length ? ` ${cleaned.join(" ")}` : "";
|
|
11071
11648
|
}
|
|
11072
|
-
function checkPropertySupport(prop, addWarning, framework, selector, line,
|
|
11073
|
-
var _a;
|
|
11649
|
+
function checkPropertySupport(prop, addWarning, framework, selector, line, values, occurrences) {
|
|
11650
|
+
var _a, _b, _c, _d, _e, _f;
|
|
11651
|
+
const loc = occurrences == null ? void 0 : occurrences.locs[0];
|
|
11652
|
+
const reportedLine = (_a = loc == null ? void 0 : loc.line) != null ? _a : line;
|
|
11074
11653
|
const supportData = CSS_SUPPORT[prop];
|
|
11075
11654
|
if (!supportData) return;
|
|
11076
11655
|
const fixType = getFixType(prop);
|
|
11077
|
-
const valueGated = VALUE_CAVEAT_PROPS.has(prop);
|
|
11078
11656
|
for (const client of EMAIL_CLIENTS) {
|
|
11079
11657
|
const support = supportData[client.id] || "unknown";
|
|
11080
|
-
const notes = (
|
|
11658
|
+
const notes = (_b = CSS_SUPPORT_NOTES[prop]) == null ? void 0 : _b[client.id];
|
|
11081
11659
|
if (support === "unsupported") {
|
|
11082
11660
|
const sug = getSuggestion(prop, client.id, framework);
|
|
11083
11661
|
const fix = getCodeFix(prop, client.id, framework);
|
|
11084
|
-
addWarning(__spreadValues(__spreadValues(__spreadValues({
|
|
11662
|
+
addWarning(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
|
|
11085
11663
|
severity: "warning",
|
|
11086
11664
|
client: client.id,
|
|
11087
11665
|
property: prop,
|
|
@@ -11089,12 +11667,13 @@ function checkPropertySupport(prop, addWarning, framework, selector, line, value
|
|
|
11089
11667
|
suggestion: sug.text,
|
|
11090
11668
|
fix,
|
|
11091
11669
|
fixType
|
|
11092
|
-
}, selector ? { selector } : {}),
|
|
11670
|
+
}, selector ? { selector } : {}), reportedLine !== void 0 ? { line: reportedLine } : {}), occurrences ? occurrenceFields(occurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
11093
11671
|
} else if (support === "partial") {
|
|
11094
|
-
if (
|
|
11672
|
+
if (!caveatApplies(prop, values, notes)) continue;
|
|
11673
|
+
const hits = triggeringOccurrences(prop, occurrences, notes);
|
|
11095
11674
|
const sug = getSuggestion(prop, client.id, framework);
|
|
11096
11675
|
const fix = getCodeFix(prop, client.id, framework);
|
|
11097
|
-
addWarning(__spreadValues(__spreadValues(__spreadValues({
|
|
11676
|
+
addWarning(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
|
|
11098
11677
|
severity: "info",
|
|
11099
11678
|
client: client.id,
|
|
11100
11679
|
property: prop,
|
|
@@ -11102,7 +11681,7 @@ function checkPropertySupport(prop, addWarning, framework, selector, line, value
|
|
|
11102
11681
|
suggestion: sug.text,
|
|
11103
11682
|
fix,
|
|
11104
11683
|
fixType
|
|
11105
|
-
}, selector ? { selector } : {}), line !== void 0 ? { line } : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
11684
|
+
}, 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 } : {}));
|
|
11106
11685
|
}
|
|
11107
11686
|
}
|
|
11108
11687
|
}
|
|
@@ -11121,6 +11700,19 @@ function generateCompatibilityScore(warnings) {
|
|
|
11121
11700
|
}
|
|
11122
11701
|
return result;
|
|
11123
11702
|
}
|
|
11703
|
+
function occurrenceFields({ locs, truncated }) {
|
|
11704
|
+
return __spreadValues({ loc: locs[0], locs: [...locs] }, truncated ? { locsTruncated: true } : {});
|
|
11705
|
+
}
|
|
11706
|
+
function triggeringOccurrences(prop, occurrences, notes) {
|
|
11707
|
+
const values = occurrences == null ? void 0 : occurrences.values;
|
|
11708
|
+
if (!occurrences || !values) return occurrences;
|
|
11709
|
+
const locs = occurrences.locs.filter((_, i) => caveatApplies(prop, [values[i]], notes));
|
|
11710
|
+
if (!locs.length || locs.length === occurrences.locs.length) return occurrences;
|
|
11711
|
+
return __spreadValues({ locs }, occurrences.truncated ? { truncated: true } : {});
|
|
11712
|
+
}
|
|
11713
|
+
function elementLocs(loc) {
|
|
11714
|
+
return loc ? { locs: [loc] } : void 0;
|
|
11715
|
+
}
|
|
11124
11716
|
function warningsForClient(warnings, clientId) {
|
|
11125
11717
|
return warnings.filter((w) => w.client === clientId);
|
|
11126
11718
|
}
|
|
@@ -11461,16 +12053,6 @@ function extractCode(response) {
|
|
|
11461
12053
|
return response.trim();
|
|
11462
12054
|
}
|
|
11463
12055
|
|
|
11464
|
-
// src/parse-html.ts
|
|
11465
|
-
import * as cheerio5 from "cheerio";
|
|
11466
|
-
function fromHtml(html, empty, fn) {
|
|
11467
|
-
if (!html || !html.trim()) return empty;
|
|
11468
|
-
if (html.length > MAX_HTML_SIZE) {
|
|
11469
|
-
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
11470
|
-
}
|
|
11471
|
-
return fn(cheerio5.load(html), html);
|
|
11472
|
-
}
|
|
11473
|
-
|
|
11474
12056
|
// src/spam-scorer.ts
|
|
11475
12057
|
var SPAM_TRIGGER_PHRASES = [
|
|
11476
12058
|
"act now",
|
|
@@ -11939,6 +12521,8 @@ function validateLinksFromDom($) {
|
|
|
11939
12521
|
const href = $(el).attr("href") || "";
|
|
11940
12522
|
const text = $(el).text().trim();
|
|
11941
12523
|
const category = classifyHref(href);
|
|
12524
|
+
const elLoc = locOfElement(el);
|
|
12525
|
+
const hrefLoc = href ? locOfAttr(el, "href") : elLoc;
|
|
11942
12526
|
switch (category) {
|
|
11943
12527
|
case "https":
|
|
11944
12528
|
breakdown.https++;
|
|
@@ -11969,95 +12553,95 @@ function validateLinksFromDom($) {
|
|
|
11969
12553
|
hrefCounts.set(href, (hrefCounts.get(href) || 0) + 1);
|
|
11970
12554
|
}
|
|
11971
12555
|
if (!href || !href.trim()) {
|
|
11972
|
-
issues.push({
|
|
12556
|
+
issues.push(__spreadValues({
|
|
11973
12557
|
severity: "error",
|
|
11974
12558
|
rule: "empty-href",
|
|
11975
12559
|
message: "Link has no href attribute",
|
|
11976
12560
|
text: text.slice(0, 80) || "(no text)"
|
|
11977
|
-
});
|
|
12561
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
11978
12562
|
return;
|
|
11979
12563
|
}
|
|
11980
12564
|
if (category === "javascript" && !isPlaceholderHref(href)) {
|
|
11981
|
-
issues.push({
|
|
12565
|
+
issues.push(__spreadValues({
|
|
11982
12566
|
severity: "error",
|
|
11983
12567
|
rule: "javascript-href",
|
|
11984
12568
|
message: "Link uses javascript: protocol",
|
|
11985
12569
|
href: href.slice(0, 100),
|
|
11986
12570
|
text: text.slice(0, 80) || "(no text)"
|
|
11987
|
-
});
|
|
12571
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
11988
12572
|
return;
|
|
11989
12573
|
}
|
|
11990
12574
|
if (isPlaceholderHref(href)) {
|
|
11991
|
-
issues.push({
|
|
12575
|
+
issues.push(__spreadValues({
|
|
11992
12576
|
severity: "warning",
|
|
11993
12577
|
rule: "placeholder-href",
|
|
11994
12578
|
message: "Link has a placeholder href (# or javascript:void)",
|
|
11995
12579
|
href,
|
|
11996
12580
|
text: text.slice(0, 80) || "(no text)"
|
|
11997
|
-
});
|
|
12581
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
11998
12582
|
return;
|
|
11999
12583
|
}
|
|
12000
12584
|
if (category === "http") {
|
|
12001
|
-
issues.push({
|
|
12585
|
+
issues.push(__spreadValues({
|
|
12002
12586
|
severity: "warning",
|
|
12003
12587
|
rule: "insecure-link",
|
|
12004
12588
|
message: "Link uses HTTP instead of HTTPS",
|
|
12005
12589
|
href: href.slice(0, 120),
|
|
12006
12590
|
text: text.slice(0, 80) || "(no text)"
|
|
12007
|
-
});
|
|
12591
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
12008
12592
|
}
|
|
12009
12593
|
if (category === "protocol-relative") {
|
|
12010
|
-
issues.push({
|
|
12594
|
+
issues.push(__spreadValues({
|
|
12011
12595
|
severity: "warning",
|
|
12012
12596
|
rule: "protocol-relative",
|
|
12013
12597
|
message: "Protocol-relative URL may break in email clients \u2014 use https:// explicitly",
|
|
12014
12598
|
href: href.slice(0, 120),
|
|
12015
12599
|
text: text.slice(0, 80) || "(no text)"
|
|
12016
|
-
});
|
|
12600
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
12017
12601
|
}
|
|
12018
12602
|
if (text && GENERIC_LINK_TEXT.has(text.toLowerCase())) {
|
|
12019
|
-
issues.push({
|
|
12603
|
+
issues.push(__spreadValues({
|
|
12020
12604
|
severity: "warning",
|
|
12021
12605
|
rule: "generic-link-text",
|
|
12022
12606
|
message: `Link text "${text}" is vague \u2014 use descriptive text for accessibility and engagement`,
|
|
12023
12607
|
href: href.slice(0, 120),
|
|
12024
12608
|
text
|
|
12025
|
-
});
|
|
12609
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12026
12610
|
}
|
|
12027
12611
|
if (!text && !$(el).attr("aria-label") && !$(el).find("img[alt]").length) {
|
|
12028
|
-
issues.push({
|
|
12612
|
+
issues.push(__spreadValues({
|
|
12029
12613
|
severity: "error",
|
|
12030
12614
|
rule: "empty-link-text",
|
|
12031
12615
|
message: "Link has no visible text or aria-label",
|
|
12032
12616
|
href: href.slice(0, 120)
|
|
12033
|
-
});
|
|
12617
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12034
12618
|
}
|
|
12035
12619
|
if (category === "mailto" && href.trim().toLowerCase() === "mailto:") {
|
|
12036
|
-
issues.push({
|
|
12620
|
+
issues.push(__spreadValues({
|
|
12037
12621
|
severity: "error",
|
|
12038
12622
|
rule: "empty-mailto",
|
|
12039
12623
|
message: "mailto: link has no email address",
|
|
12040
12624
|
href,
|
|
12041
12625
|
text: text.slice(0, 80) || "(no text)"
|
|
12042
|
-
});
|
|
12626
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
12043
12627
|
}
|
|
12044
12628
|
if (category === "tel" && href.trim().toLowerCase() === "tel:") {
|
|
12045
|
-
issues.push({
|
|
12629
|
+
issues.push(__spreadValues({
|
|
12046
12630
|
severity: "error",
|
|
12047
12631
|
rule: "empty-tel",
|
|
12048
12632
|
message: "tel: link has no phone number",
|
|
12049
12633
|
href,
|
|
12050
12634
|
text: text.slice(0, 80) || "(no text)"
|
|
12051
|
-
});
|
|
12635
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
12052
12636
|
}
|
|
12053
12637
|
if (href.length > 2e3) {
|
|
12054
|
-
issues.push({
|
|
12638
|
+
issues.push(__spreadValues({
|
|
12055
12639
|
severity: "info",
|
|
12056
12640
|
rule: "long-url",
|
|
12057
12641
|
message: "URL exceeds 2000 characters \u2014 may be truncated by some email clients",
|
|
12058
12642
|
href: href.slice(0, 120) + "...",
|
|
12059
12643
|
text: text.slice(0, 80) || "(no text)"
|
|
12060
|
-
});
|
|
12644
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
12061
12645
|
}
|
|
12062
12646
|
});
|
|
12063
12647
|
links.each((_, el) => {
|
|
@@ -12066,14 +12650,15 @@ function validateLinksFromDom($) {
|
|
|
12066
12650
|
if (trimmed.startsWith("#") && trimmed.length > 1) {
|
|
12067
12651
|
const targetId = trimmed.slice(1);
|
|
12068
12652
|
const target = $(`[id="${targetId}"]`);
|
|
12653
|
+
const anchorLoc = locOfAttr(el, "href");
|
|
12069
12654
|
if (target.length === 0) {
|
|
12070
|
-
issues.push({
|
|
12655
|
+
issues.push(__spreadValues({
|
|
12071
12656
|
severity: "error",
|
|
12072
12657
|
rule: "broken-anchor",
|
|
12073
12658
|
message: `Anchor link "${trimmed}" points to an element that does not exist`,
|
|
12074
12659
|
href: trimmed,
|
|
12075
12660
|
text: $(el).text().trim().slice(0, 80) || "(no text)"
|
|
12076
|
-
});
|
|
12661
|
+
}, anchorLoc ? { loc: anchorLoc } : {}));
|
|
12077
12662
|
}
|
|
12078
12663
|
}
|
|
12079
12664
|
});
|
|
@@ -12089,8 +12674,8 @@ function validateLinksFromDom($) {
|
|
|
12089
12674
|
}
|
|
12090
12675
|
return { totalLinks, issues, breakdown };
|
|
12091
12676
|
}
|
|
12092
|
-
function validateLinks(html) {
|
|
12093
|
-
return fromHtml(html, EMPTY_LINKS, validateLinksFromDom);
|
|
12677
|
+
function validateLinks(html, options) {
|
|
12678
|
+
return fromHtml(html, EMPTY_LINKS, validateLinksFromDom, options);
|
|
12094
12679
|
}
|
|
12095
12680
|
|
|
12096
12681
|
// src/accessibility-checker.ts
|
|
@@ -12115,24 +12700,31 @@ function describeElement($, el) {
|
|
|
12115
12700
|
function checkLangAttribute($) {
|
|
12116
12701
|
const lang = $("html").attr("lang");
|
|
12117
12702
|
if (!lang || !lang.trim()) {
|
|
12118
|
-
|
|
12703
|
+
const loc = locOfFirst($, "html");
|
|
12704
|
+
return __spreadProps(__spreadValues({
|
|
12119
12705
|
severity: "error",
|
|
12120
12706
|
rule: "missing-lang",
|
|
12121
|
-
message: "Missing lang attribute on <html> element"
|
|
12707
|
+
message: "Missing lang attribute on <html> element"
|
|
12708
|
+
}, loc ? { loc } : {}), {
|
|
12122
12709
|
details: 'Screen readers use the lang attribute to determine pronunciation. Add lang="en" (or appropriate language code).'
|
|
12123
|
-
};
|
|
12710
|
+
});
|
|
12124
12711
|
}
|
|
12125
12712
|
return null;
|
|
12126
12713
|
}
|
|
12714
|
+
function titleLoc($) {
|
|
12715
|
+
return $("title").length ? locOfFirst($, "title") : locOfFirst($, "head");
|
|
12716
|
+
}
|
|
12127
12717
|
function checkTitle($) {
|
|
12128
12718
|
const title = $("title").text().trim();
|
|
12129
12719
|
if (!title) {
|
|
12130
|
-
|
|
12720
|
+
const loc = titleLoc($);
|
|
12721
|
+
return __spreadProps(__spreadValues({
|
|
12131
12722
|
severity: "warning",
|
|
12132
12723
|
rule: "missing-title",
|
|
12133
|
-
message: "Missing or empty <title> element"
|
|
12724
|
+
message: "Missing or empty <title> element"
|
|
12725
|
+
}, loc ? { loc } : {}), {
|
|
12134
12726
|
details: "The <title> helps screen readers identify the email content."
|
|
12135
|
-
};
|
|
12727
|
+
});
|
|
12136
12728
|
}
|
|
12137
12729
|
return null;
|
|
12138
12730
|
}
|
|
@@ -12142,34 +12734,38 @@ function checkImageAlt($) {
|
|
|
12142
12734
|
const alt = $(el).attr("alt");
|
|
12143
12735
|
const src = $(el).attr("src") || "";
|
|
12144
12736
|
const role = $(el).attr("role");
|
|
12737
|
+
const elLoc = locOfElement(el);
|
|
12145
12738
|
if (role === "presentation" || role === "none") return;
|
|
12146
12739
|
if (alt === void 0) {
|
|
12147
|
-
issues.push({
|
|
12740
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12148
12741
|
severity: "error",
|
|
12149
12742
|
rule: "img-missing-alt",
|
|
12150
12743
|
message: "Image missing alt attribute",
|
|
12151
|
-
element: describeElement($, el)
|
|
12744
|
+
element: describeElement($, el)
|
|
12745
|
+
}, elLoc ? { loc: elLoc } : {}), {
|
|
12152
12746
|
details: 'Every image must have an alt attribute. Use alt="" for decorative images.'
|
|
12153
|
-
});
|
|
12747
|
+
}));
|
|
12154
12748
|
} else if (alt.trim() === "") {
|
|
12155
12749
|
const isLikelyContent = !src.includes("spacer") && !src.includes("pixel") && !src.includes("tracking") && !src.includes("1x1") && !src.includes("transparent");
|
|
12156
12750
|
if (isLikelyContent && ($(el).attr("width") || "0") !== "1") {
|
|
12157
|
-
issues.push({
|
|
12751
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12158
12752
|
severity: "info",
|
|
12159
12753
|
rule: "img-empty-alt",
|
|
12160
12754
|
message: "Image has empty alt text \u2014 verify it is decorative",
|
|
12161
|
-
element: describeElement($, el)
|
|
12755
|
+
element: describeElement($, el)
|
|
12756
|
+
}, locOfAttr(el, "alt") ? { loc: locOfAttr(el, "alt") } : {}), {
|
|
12162
12757
|
details: "Empty alt is correct for decorative images, but content images need descriptive alt text."
|
|
12163
|
-
});
|
|
12758
|
+
}));
|
|
12164
12759
|
}
|
|
12165
12760
|
} else if (/\.(png|jpg|jpeg|gif|svg|webp|bmp)$/i.test(alt)) {
|
|
12166
|
-
issues.push({
|
|
12761
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12167
12762
|
severity: "error",
|
|
12168
12763
|
rule: "img-filename-alt",
|
|
12169
12764
|
message: "Image alt text is a filename, not a description",
|
|
12170
|
-
element: describeElement($, el)
|
|
12765
|
+
element: describeElement($, el)
|
|
12766
|
+
}, locOfAttr(el, "alt") ? { loc: locOfAttr(el, "alt") } : {}), {
|
|
12171
12767
|
details: `Alt "${alt}" should describe the image content, not the file name.`
|
|
12172
|
-
});
|
|
12768
|
+
}));
|
|
12173
12769
|
}
|
|
12174
12770
|
});
|
|
12175
12771
|
return issues;
|
|
@@ -12177,28 +12773,31 @@ function checkImageAlt($) {
|
|
|
12177
12773
|
function checkLinkAccessibility($) {
|
|
12178
12774
|
const issues = [];
|
|
12179
12775
|
$("a").each((_, el) => {
|
|
12776
|
+
const elLoc = locOfElement(el);
|
|
12180
12777
|
const text = $(el).text().trim().toLowerCase();
|
|
12181
12778
|
const ariaLabel = $(el).attr("aria-label");
|
|
12182
12779
|
const title = $(el).attr("title");
|
|
12183
12780
|
const imgAlt = $(el).find("img").attr("alt");
|
|
12184
12781
|
if (!text && !ariaLabel && !title && !imgAlt) {
|
|
12185
|
-
issues.push({
|
|
12782
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12186
12783
|
severity: "error",
|
|
12187
12784
|
rule: "link-no-accessible-name",
|
|
12188
12785
|
message: "Link has no accessible name",
|
|
12189
|
-
element: describeElement($, el)
|
|
12786
|
+
element: describeElement($, el)
|
|
12787
|
+
}, elLoc ? { loc: elLoc } : {}), {
|
|
12190
12788
|
details: "Links need visible text, aria-label, or an image with alt text."
|
|
12191
|
-
});
|
|
12789
|
+
}));
|
|
12192
12790
|
return;
|
|
12193
12791
|
}
|
|
12194
12792
|
if (text && GENERIC_LINK_TEXT.has(text) && !ariaLabel) {
|
|
12195
|
-
issues.push({
|
|
12793
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12196
12794
|
severity: "warning",
|
|
12197
12795
|
rule: "link-generic-text",
|
|
12198
12796
|
message: `Link text "${$(el).text().trim()}" is not descriptive`,
|
|
12199
|
-
element: describeElement($, el)
|
|
12797
|
+
element: describeElement($, el)
|
|
12798
|
+
}, elLoc ? { loc: elLoc } : {}), {
|
|
12200
12799
|
details: "Screen readers often list links out of context. Use text that describes the destination."
|
|
12201
|
-
});
|
|
12800
|
+
}));
|
|
12202
12801
|
}
|
|
12203
12802
|
});
|
|
12204
12803
|
return issues;
|
|
@@ -12208,18 +12807,20 @@ function checkTableAccessibility($) {
|
|
|
12208
12807
|
$("table").each((_, el) => {
|
|
12209
12808
|
if ($(el).parents('table[role="presentation"], table[role="none"]').length > 0) return;
|
|
12210
12809
|
const role = $(el).attr("role");
|
|
12810
|
+
const tableLoc = locOfElement(el);
|
|
12211
12811
|
const hasHeaders = $(el).find("th").length > 0;
|
|
12212
12812
|
const looksLikeLayout = !hasHeaders;
|
|
12213
12813
|
if (looksLikeLayout && role !== "presentation" && role !== "none") {
|
|
12214
12814
|
const nestedTables = $(el).find("table").length;
|
|
12215
12815
|
if (nestedTables > 0 || $(el).find("td").length > 2) {
|
|
12216
|
-
issues.push({
|
|
12816
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12217
12817
|
severity: "info",
|
|
12218
12818
|
rule: "table-missing-role",
|
|
12219
|
-
message: 'Layout table missing role="presentation"'
|
|
12819
|
+
message: 'Layout table missing role="presentation"'
|
|
12820
|
+
}, tableLoc ? { loc: tableLoc } : {}), {
|
|
12220
12821
|
element: `<table> with ${$(el).find("td").length} cells`,
|
|
12221
12822
|
details: `Add role="presentation" to tables used for layout so screen readers don't announce them as data tables.`
|
|
12222
|
-
});
|
|
12823
|
+
}));
|
|
12223
12824
|
}
|
|
12224
12825
|
}
|
|
12225
12826
|
});
|
|
@@ -12230,6 +12831,7 @@ function checkTextSizeAndContrast($) {
|
|
|
12230
12831
|
let smallTextCount = 0;
|
|
12231
12832
|
$("[style]").each((_, el) => {
|
|
12232
12833
|
const style = $(el).attr("style") || "";
|
|
12834
|
+
const styleLoc = locOfAttr(el, "style");
|
|
12233
12835
|
const fontSizeMatch = style.match(/font-size\s*:\s*(\d+(?:\.\d+)?)(px|pt)/i);
|
|
12234
12836
|
if (fontSizeMatch) {
|
|
12235
12837
|
const size = parseFloat(fontSizeMatch[1]);
|
|
@@ -12238,13 +12840,14 @@ function checkTextSizeAndContrast($) {
|
|
|
12238
12840
|
if (pxSize < 9 && pxSize > 0) {
|
|
12239
12841
|
smallTextCount++;
|
|
12240
12842
|
if (smallTextCount <= 3) {
|
|
12241
|
-
issues.push({
|
|
12843
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12242
12844
|
severity: "warning",
|
|
12243
12845
|
rule: "small-text",
|
|
12244
12846
|
message: `Very small text (${fontSizeMatch[0].trim()})`,
|
|
12245
|
-
element: describeElement($, el)
|
|
12847
|
+
element: describeElement($, el)
|
|
12848
|
+
}, styleLoc ? { loc: styleLoc } : {}), {
|
|
12246
12849
|
details: "Text smaller than 9px is difficult to read, especially on mobile devices."
|
|
12247
|
-
});
|
|
12850
|
+
}));
|
|
12248
12851
|
}
|
|
12249
12852
|
}
|
|
12250
12853
|
}
|
|
@@ -12289,21 +12892,23 @@ function checkTextSizeAndContrast($) {
|
|
|
12289
12892
|
}
|
|
12290
12893
|
const grade = wcagGrade(ratio);
|
|
12291
12894
|
if (grade === "Fail") {
|
|
12292
|
-
issues.push({
|
|
12895
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12293
12896
|
severity: "error",
|
|
12294
12897
|
rule: "low-contrast",
|
|
12295
12898
|
message: `Low contrast ratio ${ratio.toFixed(1)}:1 \u2014 fails WCAG minimum`,
|
|
12296
|
-
element: describeElement($, el)
|
|
12899
|
+
element: describeElement($, el)
|
|
12900
|
+
}, styleLoc ? { loc: styleLoc } : {}), {
|
|
12297
12901
|
details: `Foreground ${colorValue} on background needs at least ${isLargeText ? "3:1" : "4.5:1"} contrast ratio.`
|
|
12298
|
-
});
|
|
12902
|
+
}));
|
|
12299
12903
|
} else if (!isLargeText && grade === "AA Large") {
|
|
12300
|
-
issues.push({
|
|
12904
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12301
12905
|
severity: "warning",
|
|
12302
12906
|
rule: "low-contrast",
|
|
12303
12907
|
message: `Low contrast ratio ${ratio.toFixed(1)}:1 \u2014 fails WCAG AA for normal text`,
|
|
12304
|
-
element: describeElement($, el)
|
|
12908
|
+
element: describeElement($, el)
|
|
12909
|
+
}, styleLoc ? { loc: styleLoc } : {}), {
|
|
12305
12910
|
details: `Foreground ${colorValue} on background needs at least 4.5:1 for normal-sized text.`
|
|
12306
|
-
});
|
|
12911
|
+
}));
|
|
12307
12912
|
}
|
|
12308
12913
|
}
|
|
12309
12914
|
}
|
|
@@ -12326,29 +12931,32 @@ function checkCharsetDeclaration($) {
|
|
|
12326
12931
|
const content = httpEquiv.attr("content") || "";
|
|
12327
12932
|
if (/charset\s*=/i.test(content)) return null;
|
|
12328
12933
|
}
|
|
12329
|
-
|
|
12934
|
+
const loc = locOfFirst($, "head");
|
|
12935
|
+
return __spreadProps(__spreadValues({
|
|
12330
12936
|
severity: "warning",
|
|
12331
12937
|
rule: "missing-charset",
|
|
12332
|
-
message: "Missing charset declaration"
|
|
12938
|
+
message: "Missing charset declaration"
|
|
12939
|
+
}, loc ? { loc } : {}), {
|
|
12333
12940
|
details: 'Add <meta charset="utf-8"> in <head> to prevent encoding issues across email clients.'
|
|
12334
|
-
};
|
|
12941
|
+
});
|
|
12335
12942
|
}
|
|
12336
12943
|
function checkSemanticStructure($) {
|
|
12337
12944
|
const issues = [];
|
|
12338
12945
|
const headings = [];
|
|
12339
12946
|
$("h1, h2, h3, h4, h5, h6").each((_, el) => {
|
|
12340
12947
|
const level = parseInt(el.tagName.replace(/h/i, ""), 10);
|
|
12341
|
-
headings.push({ level, text: $(el).text().trim().slice(0, 60) });
|
|
12948
|
+
headings.push({ level, text: $(el).text().trim().slice(0, 60), loc: locOfElement(el) });
|
|
12342
12949
|
});
|
|
12343
12950
|
for (let i = 1; i < headings.length; i++) {
|
|
12344
12951
|
const gap = headings[i].level - headings[i - 1].level;
|
|
12345
12952
|
if (gap > 1) {
|
|
12346
|
-
issues.push({
|
|
12953
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12347
12954
|
severity: "info",
|
|
12348
12955
|
rule: "heading-skip",
|
|
12349
|
-
message: `Heading level skipped: h${headings[i - 1].level} to h${headings[i].level}
|
|
12956
|
+
message: `Heading level skipped: h${headings[i - 1].level} to h${headings[i].level}`
|
|
12957
|
+
}, headings[i].loc ? { loc: headings[i].loc } : {}), {
|
|
12350
12958
|
details: "Skipped heading levels can confuse screen readers. Use sequential heading levels."
|
|
12351
|
-
});
|
|
12959
|
+
}));
|
|
12352
12960
|
break;
|
|
12353
12961
|
}
|
|
12354
12962
|
}
|
|
@@ -12389,8 +12997,8 @@ function checkAccessibilityFromDom($) {
|
|
|
12389
12997
|
const score = Math.max(0, 100 - penalty);
|
|
12390
12998
|
return { score, issues };
|
|
12391
12999
|
}
|
|
12392
|
-
function checkAccessibility(html) {
|
|
12393
|
-
return fromHtml(html, EMPTY_ACCESSIBILITY, checkAccessibilityFromDom);
|
|
13000
|
+
function checkAccessibility(html, options) {
|
|
13001
|
+
return fromHtml(html, EMPTY_ACCESSIBILITY, checkAccessibilityFromDom, options);
|
|
12394
13002
|
}
|
|
12395
13003
|
|
|
12396
13004
|
// src/image-analyzer.ts
|
|
@@ -12437,6 +13045,8 @@ function analyzeImagesFromDom($) {
|
|
|
12437
13045
|
const height = (_c = img.attr("height")) != null ? _c : null;
|
|
12438
13046
|
const style = (img.attr("style") || "").toLowerCase();
|
|
12439
13047
|
const imgIssues = [];
|
|
13048
|
+
const elLoc = locOfElement(el);
|
|
13049
|
+
const srcLoc = src ? locOfAttr(el, "src") : elLoc;
|
|
12440
13050
|
const tracking = isTrackingPixel(img);
|
|
12441
13051
|
let dataUriBytes = 0;
|
|
12442
13052
|
if (src.startsWith("data:")) {
|
|
@@ -12460,59 +13070,59 @@ function analyzeImagesFromDom($) {
|
|
|
12460
13070
|
const hasStyleHeight = /height\s*:/.test(style);
|
|
12461
13071
|
if (!hasStyleWidth && !hasStyleHeight) {
|
|
12462
13072
|
imgIssues.push("missing-dimensions");
|
|
12463
|
-
issues.push({
|
|
13073
|
+
issues.push(__spreadValues({
|
|
12464
13074
|
rule: "missing-dimensions",
|
|
12465
13075
|
severity: "warning",
|
|
12466
13076
|
message: "Image missing width/height attributes \u2014 causes layout shifts and Outlook rendering issues.",
|
|
12467
13077
|
src: truncateSrc(src)
|
|
12468
|
-
});
|
|
13078
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12469
13079
|
}
|
|
12470
13080
|
}
|
|
12471
13081
|
if (dataUriBytes > DATA_URI_WARN_BYTES) {
|
|
12472
13082
|
const kb = Math.round(dataUriBytes / 1024);
|
|
12473
13083
|
imgIssues.push("large-data-uri");
|
|
12474
|
-
issues.push({
|
|
13084
|
+
issues.push(__spreadValues({
|
|
12475
13085
|
rule: "large-data-uri",
|
|
12476
13086
|
severity: "warning",
|
|
12477
13087
|
message: `Data URI is ${kb}KB \u2014 consider hosting the image externally to reduce email size.`,
|
|
12478
13088
|
src: truncateSrc(src)
|
|
12479
|
-
});
|
|
13089
|
+
}, srcLoc ? { loc: srcLoc } : {}));
|
|
12480
13090
|
}
|
|
12481
13091
|
if (alt === null) {
|
|
12482
13092
|
imgIssues.push("missing-alt");
|
|
12483
|
-
issues.push({
|
|
13093
|
+
issues.push(__spreadValues({
|
|
12484
13094
|
rule: "missing-alt",
|
|
12485
13095
|
severity: "warning",
|
|
12486
13096
|
message: "Image missing alt attribute \u2014 hurts deliverability and accessibility.",
|
|
12487
13097
|
src: truncateSrc(src)
|
|
12488
|
-
});
|
|
13098
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12489
13099
|
}
|
|
12490
13100
|
if (src.toLowerCase().endsWith(".webp") || src.includes("image/webp")) {
|
|
12491
13101
|
imgIssues.push("webp-format");
|
|
12492
|
-
issues.push({
|
|
13102
|
+
issues.push(__spreadValues({
|
|
12493
13103
|
rule: "webp-format",
|
|
12494
13104
|
severity: "info",
|
|
12495
13105
|
message: "WebP format detected \u2014 not supported by all email clients. Consider PNG or JPEG.",
|
|
12496
13106
|
src: truncateSrc(src)
|
|
12497
|
-
});
|
|
13107
|
+
}, srcLoc ? { loc: srcLoc } : {}));
|
|
12498
13108
|
}
|
|
12499
13109
|
if (src.toLowerCase().endsWith(".svg") || src.includes("image/svg")) {
|
|
12500
13110
|
imgIssues.push("svg-format");
|
|
12501
|
-
issues.push({
|
|
13111
|
+
issues.push(__spreadValues({
|
|
12502
13112
|
rule: "svg-format",
|
|
12503
13113
|
severity: "info",
|
|
12504
13114
|
message: "SVG format detected \u2014 not supported by most email clients. Use PNG instead.",
|
|
12505
13115
|
src: truncateSrc(src)
|
|
12506
|
-
});
|
|
13116
|
+
}, srcLoc ? { loc: srcLoc } : {}));
|
|
12507
13117
|
}
|
|
12508
13118
|
if (!style.includes("display:block") && !style.includes("display: block")) {
|
|
12509
13119
|
imgIssues.push("missing-display-block");
|
|
12510
|
-
issues.push({
|
|
13120
|
+
issues.push(__spreadValues({
|
|
12511
13121
|
rule: "missing-display-block",
|
|
12512
13122
|
severity: "info",
|
|
12513
13123
|
message: "Image without display:block \u2014 may cause unwanted gaps in Outlook.",
|
|
12514
13124
|
src: truncateSrc(src)
|
|
12515
|
-
});
|
|
13125
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12516
13126
|
}
|
|
12517
13127
|
images.push({
|
|
12518
13128
|
src: truncateSrc(src),
|
|
@@ -12550,8 +13160,8 @@ function analyzeImagesFromDom($) {
|
|
|
12550
13160
|
}
|
|
12551
13161
|
return { total: images.length, totalDataUriBytes, issues, images };
|
|
12552
13162
|
}
|
|
12553
|
-
function analyzeImages(html) {
|
|
12554
|
-
return fromHtml(html, EMPTY_IMAGES, analyzeImagesFromDom);
|
|
13163
|
+
function analyzeImages(html, options) {
|
|
13164
|
+
return fromHtml(html, EMPTY_IMAGES, analyzeImagesFromDom, options);
|
|
12555
13165
|
}
|
|
12556
13166
|
|
|
12557
13167
|
// src/inbox-preview.ts
|
|
@@ -12769,11 +13379,57 @@ function checkSize(html) {
|
|
|
12769
13379
|
return fromHtml(html, EMPTY_SIZE, checkSizeFromDom);
|
|
12770
13380
|
}
|
|
12771
13381
|
|
|
13382
|
+
// src/dom-text.ts
|
|
13383
|
+
function visibleTextNodes($) {
|
|
13384
|
+
var _a, _b, _c, _d;
|
|
13385
|
+
const nodes = [];
|
|
13386
|
+
const stack = [...(_b = (_a = $.root()[0]) == null ? void 0 : _a.children) != null ? _b : []].reverse();
|
|
13387
|
+
while (stack.length > 0) {
|
|
13388
|
+
const node = stack.pop();
|
|
13389
|
+
const tag = (_c = node.tagName) == null ? void 0 : _c.toLowerCase();
|
|
13390
|
+
if (tag === "style" || tag === "script" || tag === "head") continue;
|
|
13391
|
+
if (node.type === "text") {
|
|
13392
|
+
nodes.push(node);
|
|
13393
|
+
continue;
|
|
13394
|
+
}
|
|
13395
|
+
const children = (_d = node.children) != null ? _d : [];
|
|
13396
|
+
for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]);
|
|
13397
|
+
}
|
|
13398
|
+
return nodes;
|
|
13399
|
+
}
|
|
13400
|
+
|
|
12772
13401
|
// src/template-checker.ts
|
|
12773
|
-
function checkTemplateVariablesFromDom(
|
|
13402
|
+
function checkTemplateVariablesFromDom($, source) {
|
|
13403
|
+
var _a;
|
|
12774
13404
|
const issues = [];
|
|
12775
13405
|
const seen = /* @__PURE__ */ new Set();
|
|
12776
|
-
const
|
|
13406
|
+
const textNodes = visibleTextNodes($);
|
|
13407
|
+
const positioned = textNodes.some((n) => n.sourceCodeLocation);
|
|
13408
|
+
for (const node of positioned ? textNodes : []) {
|
|
13409
|
+
const data = (_a = node.data) != null ? _a : "";
|
|
13410
|
+
for (const [pattern, label] of TEMPLATE_VARIABLE_PATTERNS) {
|
|
13411
|
+
pattern.lastIndex = 0;
|
|
13412
|
+
let match;
|
|
13413
|
+
while ((match = pattern.exec(data)) !== null) {
|
|
13414
|
+
const variable = match[0];
|
|
13415
|
+
const key = `text:${variable}`;
|
|
13416
|
+
if (seen.has(key)) continue;
|
|
13417
|
+
seen.add(key);
|
|
13418
|
+
const loc = locInTextNode(node, match.index, variable.length, source);
|
|
13419
|
+
issues.push(__spreadValues({
|
|
13420
|
+
rule: "unresolved-variable",
|
|
13421
|
+
severity: "error",
|
|
13422
|
+
message: `Unresolved ${label} variable "${variable}" found in text content.`,
|
|
13423
|
+
variable,
|
|
13424
|
+
location: "text"
|
|
13425
|
+
}, loc ? { loc } : {}));
|
|
13426
|
+
}
|
|
13427
|
+
}
|
|
13428
|
+
}
|
|
13429
|
+
const textContent = textNodes.map((n) => {
|
|
13430
|
+
var _a2;
|
|
13431
|
+
return (_a2 = n.data) != null ? _a2 : "";
|
|
13432
|
+
}).join("");
|
|
12777
13433
|
for (const [pattern, label] of TEMPLATE_VARIABLE_PATTERNS) {
|
|
12778
13434
|
pattern.lastIndex = 0;
|
|
12779
13435
|
let match;
|
|
@@ -12806,13 +13462,14 @@ function checkTemplateVariablesFromDom($) {
|
|
|
12806
13462
|
const key = `attr:${attr}:${variable}`;
|
|
12807
13463
|
if (seen.has(key)) continue;
|
|
12808
13464
|
seen.add(key);
|
|
12809
|
-
|
|
13465
|
+
const loc = locOfAttr(el, attr);
|
|
13466
|
+
issues.push(__spreadValues({
|
|
12810
13467
|
rule: "unresolved-variable",
|
|
12811
13468
|
severity: "error",
|
|
12812
13469
|
message: `Unresolved ${label} variable "${variable}" found in ${attr} attribute.`,
|
|
12813
13470
|
variable,
|
|
12814
13471
|
location: "attribute"
|
|
12815
|
-
});
|
|
13472
|
+
}, loc ? { loc } : {}));
|
|
12816
13473
|
}
|
|
12817
13474
|
}
|
|
12818
13475
|
}
|
|
@@ -12820,13 +13477,13 @@ function checkTemplateVariablesFromDom($) {
|
|
|
12820
13477
|
}
|
|
12821
13478
|
return { unresolvedCount: issues.length, issues };
|
|
12822
13479
|
}
|
|
12823
|
-
function
|
|
12824
|
-
|
|
12825
|
-
|
|
12826
|
-
|
|
12827
|
-
|
|
12828
|
-
|
|
12829
|
-
|
|
13480
|
+
function checkTemplateVariables(html, options) {
|
|
13481
|
+
return fromHtml(
|
|
13482
|
+
html,
|
|
13483
|
+
EMPTY_TEMPLATE,
|
|
13484
|
+
($, h) => checkTemplateVariablesFromDom($, (options == null ? void 0 : options.positions) ? h : void 0),
|
|
13485
|
+
options
|
|
13486
|
+
);
|
|
12830
13487
|
}
|
|
12831
13488
|
|
|
12832
13489
|
// src/overflow-checker.ts
|
|
@@ -12842,32 +13499,71 @@ function fixedPxWidth($el) {
|
|
|
12842
13499
|
function isFluid(style) {
|
|
12843
13500
|
return /max-width\s*:\s*100%/i.test(style) || /width\s*:\s*100%/i.test(style);
|
|
12844
13501
|
}
|
|
12845
|
-
function addWidthIssue(width, label, issues, seen) {
|
|
13502
|
+
function addWidthIssue(width, label, issues, seen, loc) {
|
|
12846
13503
|
const key = `w:${label}:${width}`;
|
|
12847
|
-
|
|
12848
|
-
|
|
12849
|
-
|
|
13504
|
+
const existing = seen.get(key);
|
|
13505
|
+
if (existing) {
|
|
13506
|
+
addOccurrence(existing, loc);
|
|
13507
|
+
return;
|
|
13508
|
+
}
|
|
13509
|
+
const issue = __spreadValues({
|
|
12850
13510
|
rule: "fixed-width-overflow",
|
|
12851
13511
|
severity: "warning",
|
|
12852
13512
|
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.`,
|
|
12853
13513
|
detail: `Use width:100% with max-width:${EMAIL_MAX_WIDTH}px instead of a fixed width beyond the frame.`
|
|
12854
|
-
});
|
|
13514
|
+
}, loc ? { loc, locs: [loc] } : {});
|
|
13515
|
+
seen.set(key, issue);
|
|
13516
|
+
issues.push(issue);
|
|
13517
|
+
}
|
|
13518
|
+
function locateInNodes(nodes, starts, index, length, source) {
|
|
13519
|
+
var _a;
|
|
13520
|
+
let lo = 0;
|
|
13521
|
+
let hi = starts.length - 1;
|
|
13522
|
+
let found = -1;
|
|
13523
|
+
while (lo <= hi) {
|
|
13524
|
+
const mid = lo + hi >> 1;
|
|
13525
|
+
if (starts[mid] <= index) {
|
|
13526
|
+
found = mid;
|
|
13527
|
+
lo = mid + 1;
|
|
13528
|
+
} else {
|
|
13529
|
+
hi = mid - 1;
|
|
13530
|
+
}
|
|
13531
|
+
}
|
|
13532
|
+
if (found === -1) return void 0;
|
|
13533
|
+
const node = nodes[found];
|
|
13534
|
+
const within = index - starts[found];
|
|
13535
|
+
const available = ((_a = node.data) != null ? _a : "").length - within;
|
|
13536
|
+
return locInTextNode(node, within, Math.min(length, available), source);
|
|
12855
13537
|
}
|
|
12856
|
-
function
|
|
13538
|
+
function addOccurrence(issue, loc) {
|
|
13539
|
+
if (!loc || !issue.locs) return;
|
|
13540
|
+
if (issue.locs.some((l) => l.offset === loc.offset)) return;
|
|
13541
|
+
if (issue.locs.length >= MAX_WARNING_LOCATIONS) {
|
|
13542
|
+
issue.locsTruncated = true;
|
|
13543
|
+
return;
|
|
13544
|
+
}
|
|
13545
|
+
issue.locs.push(loc);
|
|
13546
|
+
}
|
|
13547
|
+
function checkOverflowFromDom($, source) {
|
|
13548
|
+
var _a;
|
|
12857
13549
|
const issues = [];
|
|
12858
|
-
const seen = /* @__PURE__ */ new
|
|
13550
|
+
const seen = /* @__PURE__ */ new Map();
|
|
13551
|
+
const tokensSeen = /* @__PURE__ */ new Set();
|
|
12859
13552
|
$("[width], [style*='width']").each((_, el) => {
|
|
12860
13553
|
const $el = $(el);
|
|
12861
13554
|
const width = fixedPxWidth($el);
|
|
12862
13555
|
if (width === null || width <= EMAIL_MAX_WIDTH) return;
|
|
12863
13556
|
if (isFluid($el.attr("style") || "")) return;
|
|
12864
13557
|
const tag = (el.tagName || "element").toLowerCase();
|
|
12865
|
-
|
|
13558
|
+
const fromStyle = /(?:^|[;\s])width\s*:\s*\d+px/i.test($el.attr("style") || "");
|
|
13559
|
+
addWidthIssue(width, `<${tag}>`, issues, seen, locOfAttr(el, fromStyle ? "style" : "width"));
|
|
12866
13560
|
});
|
|
12867
13561
|
$("style").each((_, el) => {
|
|
13562
|
+
const cssText = $(el).text();
|
|
13563
|
+
const anchor = cssBlockAnchor(el, cssText, source);
|
|
12868
13564
|
let ast;
|
|
12869
13565
|
try {
|
|
12870
|
-
ast = csstree6.parse(
|
|
13566
|
+
ast = csstree6.parse(cssText, { positions: true });
|
|
12871
13567
|
} catch (e) {
|
|
12872
13568
|
return;
|
|
12873
13569
|
}
|
|
@@ -12877,13 +13573,17 @@ function checkOverflowFromDom($) {
|
|
|
12877
13573
|
if (node.type !== "Rule") return;
|
|
12878
13574
|
let widthPx = null;
|
|
12879
13575
|
let fluid = false;
|
|
13576
|
+
let widthLoc;
|
|
12880
13577
|
node.block.children.forEach((child) => {
|
|
12881
13578
|
if (child.type !== "Declaration") return;
|
|
12882
13579
|
const prop = child.property.toLowerCase();
|
|
12883
13580
|
const val = csstree6.generate(child.value);
|
|
12884
13581
|
if (prop === "width") {
|
|
12885
13582
|
const m = val.match(/^(\d+)px$/);
|
|
12886
|
-
if (m)
|
|
13583
|
+
if (m) {
|
|
13584
|
+
widthPx = parseInt(m[1], 10);
|
|
13585
|
+
widthLoc = locInCssBlock(anchor, child.loc);
|
|
13586
|
+
}
|
|
12887
13587
|
if (/\b100%/.test(val)) fluid = true;
|
|
12888
13588
|
} else if (prop === "max-width" && /\b100%/.test(val)) {
|
|
12889
13589
|
fluid = true;
|
|
@@ -12891,36 +13591,46 @@ function checkOverflowFromDom($) {
|
|
|
12891
13591
|
});
|
|
12892
13592
|
if (widthPx !== null && widthPx > EMAIL_MAX_WIDTH && !fluid) {
|
|
12893
13593
|
const selector = csstree6.generate(node.prelude).trim().slice(0, 40);
|
|
12894
|
-
addWidthIssue(widthPx, selector || "rule", issues, seen);
|
|
13594
|
+
addWidthIssue(widthPx, selector || "rule", issues, seen, widthLoc);
|
|
12895
13595
|
}
|
|
12896
13596
|
}
|
|
12897
13597
|
});
|
|
12898
13598
|
});
|
|
12899
13599
|
const usesWrapGuard = /overflow-wrap|word-break|word-wrap/i.test($.html());
|
|
12900
13600
|
if (!usesWrapGuard) {
|
|
12901
|
-
const
|
|
13601
|
+
const nodes = visibleTextNodes($);
|
|
13602
|
+
const starts = [];
|
|
12902
13603
|
let text = "";
|
|
12903
|
-
|
|
12904
|
-
|
|
12905
|
-
|
|
12906
|
-
|
|
12907
|
-
|
|
12908
|
-
for (const token of text.split(
|
|
12909
|
-
|
|
12910
|
-
|
|
13604
|
+
for (const node of nodes) {
|
|
13605
|
+
starts.push(text.length);
|
|
13606
|
+
text += (_a = node.data) != null ? _a : "";
|
|
13607
|
+
}
|
|
13608
|
+
let at = 0;
|
|
13609
|
+
for (const token of text.split(/(\s+)/)) {
|
|
13610
|
+
const start = at;
|
|
13611
|
+
at += token.length;
|
|
13612
|
+
if (/^\s*$/.test(token)) continue;
|
|
13613
|
+
if (token.length <= UNBREAKABLE_STRING_LENGTH || tokensSeen.has(token)) continue;
|
|
13614
|
+
tokensSeen.add(token);
|
|
12911
13615
|
const preview = token.length > 50 ? `${token.slice(0, 50)}\u2026` : token;
|
|
12912
|
-
|
|
13616
|
+
const loc = locateInNodes(nodes, starts, start, token.length, source);
|
|
13617
|
+
issues.push(__spreadValues({
|
|
12913
13618
|
rule: "unbreakable-string",
|
|
12914
13619
|
severity: "warning",
|
|
12915
13620
|
message: `A ${token.length}-character unbroken string ("${preview}") can't wrap and will force horizontal scrolling on narrow screens.`,
|
|
12916
13621
|
detail: `Add overflow-wrap: anywhere (or word-break: break-word) to its container.`
|
|
12917
|
-
});
|
|
13622
|
+
}, loc ? { loc, locs: [loc] } : {}));
|
|
12918
13623
|
}
|
|
12919
13624
|
}
|
|
12920
13625
|
return { hasOverflow: issues.length > 0, issues };
|
|
12921
13626
|
}
|
|
12922
|
-
function checkOverflow(html) {
|
|
12923
|
-
return fromHtml(
|
|
13627
|
+
function checkOverflow(html, options) {
|
|
13628
|
+
return fromHtml(
|
|
13629
|
+
html,
|
|
13630
|
+
EMPTY_OVERFLOW,
|
|
13631
|
+
($, h) => checkOverflowFromDom($, (options == null ? void 0 : options.positions) ? h : void 0),
|
|
13632
|
+
options
|
|
13633
|
+
);
|
|
12924
13634
|
}
|
|
12925
13635
|
|
|
12926
13636
|
// src/visual-checker.ts
|
|
@@ -12933,8 +13643,8 @@ function isSolidColor(value) {
|
|
|
12933
13643
|
return c !== null && c.a !== 0;
|
|
12934
13644
|
}
|
|
12935
13645
|
function firstColor(value) {
|
|
12936
|
-
const
|
|
12937
|
-
for (const t of
|
|
13646
|
+
const tokens2 = value.match(/#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\)|\b[a-zA-Z]{3,}\b/g) || [];
|
|
13647
|
+
for (const t of tokens2) {
|
|
12938
13648
|
const lc = t.toLowerCase();
|
|
12939
13649
|
if (lc === "transparent") continue;
|
|
12940
13650
|
if (/^(?:linear|radial|conic|gradient|deg|turn|rad|grad|to|at|from|in|circle|ellipse|closest|farthest|side|corner|url)$/.test(lc)) continue;
|
|
@@ -12971,8 +13681,17 @@ function hasFontFallback(value) {
|
|
|
12971
13681
|
return WEB_SAFE_FONTS.has(t) || GENERIC_FONT_FAMILIES.has(t) || t.startsWith("-apple-system") || t === "blinkmacsystemfont";
|
|
12972
13682
|
});
|
|
12973
13683
|
}
|
|
12974
|
-
function
|
|
12975
|
-
|
|
13684
|
+
function addOccurrence2(issue, loc) {
|
|
13685
|
+
if (!loc || !issue.locs) return;
|
|
13686
|
+
if (issue.locs.some((l) => l.offset === loc.offset)) return;
|
|
13687
|
+
if (issue.locs.length >= MAX_WARNING_LOCATIONS) {
|
|
13688
|
+
issue.locsTruncated = true;
|
|
13689
|
+
return;
|
|
13690
|
+
}
|
|
13691
|
+
issue.locs.push(loc);
|
|
13692
|
+
}
|
|
13693
|
+
function inspectDeclarations(style, issues, seen, locs) {
|
|
13694
|
+
var _a, _b, _c;
|
|
12976
13695
|
const combined = `${(_a = style.get("background-image")) != null ? _a : ""} ${(_b = style.get("background")) != null ? _b : ""}`;
|
|
12977
13696
|
const isGradient = GRADIENT_RE.test(combined);
|
|
12978
13697
|
const isImage = isGradient || /url\(/i.test(combined);
|
|
@@ -12980,30 +13699,40 @@ function inspectDeclarations(style, issues, seen) {
|
|
|
12980
13699
|
const stop = isGradient ? firstColor(combined) : null;
|
|
12981
13700
|
const fix = stop ? `background-color: ${stop};` : `background-color: <solid colour matching the image>;`;
|
|
12982
13701
|
const key = `bg:${fix}`;
|
|
12983
|
-
|
|
12984
|
-
|
|
12985
|
-
|
|
13702
|
+
const loc = (_c = locs == null ? void 0 : locs.get("background-image")) != null ? _c : locs == null ? void 0 : locs.get("background");
|
|
13703
|
+
const existing = seen.get(key);
|
|
13704
|
+
if (existing) {
|
|
13705
|
+
addOccurrence2(existing, loc);
|
|
13706
|
+
} else {
|
|
13707
|
+
const issue = __spreadValues({
|
|
12986
13708
|
rule: "missing-background-fallback",
|
|
12987
13709
|
severity: "warning",
|
|
12988
13710
|
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.`,
|
|
12989
13711
|
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.`,
|
|
12990
13712
|
fix
|
|
12991
|
-
});
|
|
13713
|
+
}, loc ? { loc, locs: [loc] } : {});
|
|
13714
|
+
seen.set(key, issue);
|
|
13715
|
+
issues.push(issue);
|
|
12992
13716
|
}
|
|
12993
13717
|
}
|
|
12994
13718
|
const font = style.get("font-family");
|
|
12995
13719
|
if (font && !CSS_WIDE_KEYWORDS.has(font.trim().toLowerCase()) && !hasFontFallback(font)) {
|
|
12996
13720
|
const fix = `font-family: ${font.trim()}, Arial, sans-serif;`;
|
|
12997
13721
|
const key = `font:${font.trim().toLowerCase()}`;
|
|
12998
|
-
|
|
12999
|
-
|
|
13000
|
-
|
|
13722
|
+
const loc = locs == null ? void 0 : locs.get("font-family");
|
|
13723
|
+
const existing = seen.get(key);
|
|
13724
|
+
if (existing) {
|
|
13725
|
+
addOccurrence2(existing, loc);
|
|
13726
|
+
} else {
|
|
13727
|
+
const issue = __spreadValues({
|
|
13001
13728
|
rule: "missing-font-fallback",
|
|
13002
13729
|
severity: "warning",
|
|
13003
13730
|
message: `font-family "${font.trim()}" has no web-safe fallback \u2014 clients that strip web fonts (Gmail, Outlook) fall back to Times New Roman.`,
|
|
13004
13731
|
detail: `End the stack with a web-safe font and a generic family.`,
|
|
13005
13732
|
fix
|
|
13006
|
-
});
|
|
13733
|
+
}, loc ? { loc, locs: [loc] } : {});
|
|
13734
|
+
seen.set(key, issue);
|
|
13735
|
+
issues.push(issue);
|
|
13007
13736
|
}
|
|
13008
13737
|
}
|
|
13009
13738
|
}
|
|
@@ -13016,30 +13745,47 @@ function ruleToMap(node) {
|
|
|
13016
13745
|
});
|
|
13017
13746
|
return map;
|
|
13018
13747
|
}
|
|
13019
|
-
function checkVisualFromDom(
|
|
13748
|
+
function checkVisualFromDom($, source) {
|
|
13020
13749
|
const issues = [];
|
|
13021
|
-
const seen = /* @__PURE__ */ new
|
|
13750
|
+
const seen = /* @__PURE__ */ new Map();
|
|
13022
13751
|
$("[style]").each((_, el) => {
|
|
13023
|
-
|
|
13752
|
+
const attrLoc = locOfAttr(el, "style");
|
|
13753
|
+
const style = parseInlineStyle($(el).attr("style") || "");
|
|
13754
|
+
const locs = attrLoc ? new Map([...style.keys()].map((prop) => [prop, attrLoc])) : void 0;
|
|
13755
|
+
inspectDeclarations(style, issues, seen, locs);
|
|
13024
13756
|
});
|
|
13025
13757
|
$("style").each((_, el) => {
|
|
13758
|
+
const cssText = $(el).text();
|
|
13759
|
+
const anchor = cssBlockAnchor(el, cssText, source);
|
|
13026
13760
|
let ast;
|
|
13027
13761
|
try {
|
|
13028
|
-
ast = csstree7.parse(
|
|
13762
|
+
ast = csstree7.parse(cssText, { positions: true });
|
|
13029
13763
|
} catch (e) {
|
|
13030
13764
|
return;
|
|
13031
13765
|
}
|
|
13032
13766
|
csstree7.walk(ast, {
|
|
13033
13767
|
visit: "Rule",
|
|
13034
13768
|
enter(node) {
|
|
13035
|
-
if (node.type
|
|
13769
|
+
if (node.type !== "Rule") return;
|
|
13770
|
+
const locs = /* @__PURE__ */ new Map();
|
|
13771
|
+
node.block.children.forEach((child) => {
|
|
13772
|
+
if (child.type !== "Declaration") return;
|
|
13773
|
+
const loc = locInCssBlock(anchor, child.loc);
|
|
13774
|
+
if (loc) locs.set(child.property.toLowerCase(), loc);
|
|
13775
|
+
});
|
|
13776
|
+
inspectDeclarations(ruleToMap(node), issues, seen, locs);
|
|
13036
13777
|
}
|
|
13037
13778
|
});
|
|
13038
13779
|
});
|
|
13039
13780
|
return { issues };
|
|
13040
13781
|
}
|
|
13041
|
-
function checkVisual(html) {
|
|
13042
|
-
return fromHtml(
|
|
13782
|
+
function checkVisual(html, options) {
|
|
13783
|
+
return fromHtml(
|
|
13784
|
+
html,
|
|
13785
|
+
EMPTY_VISUAL,
|
|
13786
|
+
($, h) => checkVisualFromDom($, (options == null ? void 0 : options.positions) ? h : void 0),
|
|
13787
|
+
options
|
|
13788
|
+
);
|
|
13043
13789
|
}
|
|
13044
13790
|
|
|
13045
13791
|
// src/audit.ts
|
|
@@ -13058,7 +13804,8 @@ var EMPTY_AUDIT = {
|
|
|
13058
13804
|
function runAudit($, html, framework, options) {
|
|
13059
13805
|
var _a;
|
|
13060
13806
|
const skip = new Set((_a = options == null ? void 0 : options.skip) != null ? _a : []);
|
|
13061
|
-
const
|
|
13807
|
+
const source = (options == null ? void 0 : options.positions) ? html : void 0;
|
|
13808
|
+
const warnings = skip.has("compatibility") ? [] : analyzeEmailFromDom($, framework, source);
|
|
13062
13809
|
const scores = skip.has("compatibility") ? {} : generateCompatibilityScore(warnings);
|
|
13063
13810
|
const spam = skip.has("spam") ? EMPTY_SPAM : analyzeSpamFromDom($, options == null ? void 0 : options.spam);
|
|
13064
13811
|
const links = skip.has("links") ? EMPTY_LINKS : validateLinksFromDom($);
|
|
@@ -13066,19 +13813,19 @@ function runAudit($, html, framework, options) {
|
|
|
13066
13813
|
const images = skip.has("images") ? EMPTY_IMAGES : analyzeImagesFromDom($);
|
|
13067
13814
|
const inboxPreview = skip.has("inboxPreview") ? EMPTY_INBOX_PREVIEW : extractInboxPreviewFromDom($);
|
|
13068
13815
|
const size = skip.has("size") ? EMPTY_SIZE : checkSizeFromDom($, html);
|
|
13069
|
-
const templateVariables = skip.has("templateVariables") ? EMPTY_TEMPLATE : checkTemplateVariablesFromDom(
|
|
13070
|
-
const overflow = skip.has("overflow") ? EMPTY_OVERFLOW : checkOverflowFromDom(
|
|
13071
|
-
const visual = skip.has("visual") ? EMPTY_VISUAL : checkVisualFromDom(
|
|
13816
|
+
const templateVariables = skip.has("templateVariables") ? EMPTY_TEMPLATE : checkTemplateVariablesFromDom($, source);
|
|
13817
|
+
const overflow = skip.has("overflow") ? EMPTY_OVERFLOW : checkOverflowFromDom($, source);
|
|
13818
|
+
const visual = skip.has("visual") ? EMPTY_VISUAL : checkVisualFromDom($, source);
|
|
13072
13819
|
return { compatibility: { warnings, scores }, spam, links, accessibility, images, inboxPreview, size, templateVariables, overflow, visual };
|
|
13073
13820
|
}
|
|
13074
13821
|
function auditEmail(html, options) {
|
|
13075
|
-
return fromHtml(html, EMPTY_AUDIT, ($, h) => runAudit($, h, options == null ? void 0 : options.framework, options));
|
|
13822
|
+
return fromHtml(html, EMPTY_AUDIT, ($, h) => runAudit($, h, options == null ? void 0 : options.framework, options), options);
|
|
13076
13823
|
}
|
|
13077
13824
|
|
|
13078
13825
|
// src/plain-text.ts
|
|
13079
|
-
import * as
|
|
13826
|
+
import * as cheerio5 from "cheerio";
|
|
13080
13827
|
function toPlainText(html) {
|
|
13081
|
-
const $ =
|
|
13828
|
+
const $ = cheerio5.load(html);
|
|
13082
13829
|
$("style, script, head").remove();
|
|
13083
13830
|
$("[data-skip-in-text='true']").remove();
|
|
13084
13831
|
const lines = [];
|
|
@@ -13180,7 +13927,6 @@ function toPlainText(html) {
|
|
|
13180
13927
|
}
|
|
13181
13928
|
|
|
13182
13929
|
// src/session.ts
|
|
13183
|
-
import * as cheerio7 from "cheerio";
|
|
13184
13930
|
function createSession(html, options) {
|
|
13185
13931
|
if (!html || !html.trim()) {
|
|
13186
13932
|
const fw = options == null ? void 0 : options.framework;
|
|
@@ -13207,16 +13953,17 @@ function createSession(html, options) {
|
|
|
13207
13953
|
if (html.length > MAX_HTML_SIZE) {
|
|
13208
13954
|
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
13209
13955
|
}
|
|
13210
|
-
const $ =
|
|
13956
|
+
const $ = loadHtml(html, options);
|
|
13211
13957
|
const framework = options == null ? void 0 : options.framework;
|
|
13958
|
+
const source = (options == null ? void 0 : options.positions) ? html : void 0;
|
|
13212
13959
|
return {
|
|
13213
13960
|
html,
|
|
13214
13961
|
framework,
|
|
13215
13962
|
audit(opts) {
|
|
13216
|
-
return runAudit($, html, framework, opts);
|
|
13963
|
+
return runAudit($, html, framework, __spreadProps(__spreadValues({}, opts), { positions: options == null ? void 0 : options.positions }));
|
|
13217
13964
|
},
|
|
13218
13965
|
analyze() {
|
|
13219
|
-
return analyzeEmailFromDom($, framework);
|
|
13966
|
+
return analyzeEmailFromDom($, framework, source);
|
|
13220
13967
|
},
|
|
13221
13968
|
score(warnings) {
|
|
13222
13969
|
return generateCompatibilityScore(warnings);
|
|
@@ -13240,13 +13987,13 @@ function createSession(html, options) {
|
|
|
13240
13987
|
return checkSizeFromDom($, html);
|
|
13241
13988
|
},
|
|
13242
13989
|
checkTemplateVariables() {
|
|
13243
|
-
return checkTemplateVariablesFromDom(
|
|
13990
|
+
return checkTemplateVariablesFromDom($, source);
|
|
13244
13991
|
},
|
|
13245
13992
|
checkOverflow() {
|
|
13246
|
-
return checkOverflowFromDom(
|
|
13993
|
+
return checkOverflowFromDom($, source);
|
|
13247
13994
|
},
|
|
13248
13995
|
checkVisual() {
|
|
13249
|
-
return checkVisualFromDom(
|
|
13996
|
+
return checkVisualFromDom($, source);
|
|
13250
13997
|
},
|
|
13251
13998
|
// Transforms create isolated copies since they mutate the DOM
|
|
13252
13999
|
transformForClient(clientId) {
|
|
@@ -13266,18 +14013,22 @@ export {
|
|
|
13266
14013
|
COMPOUND_VALUE_FEATURES,
|
|
13267
14014
|
CSS_FUNCTION_FEATURES,
|
|
13268
14015
|
CSS_SUPPORT,
|
|
14016
|
+
CSS_SUPPORT_NOTES,
|
|
13269
14017
|
CompileError,
|
|
13270
14018
|
EMAIL_CLIENTS,
|
|
13271
14019
|
EMPTY_DELIVERABILITY,
|
|
13272
14020
|
GENERIC_LINK_TEXT,
|
|
13273
14021
|
HTML_ELEMENT_FEATURES,
|
|
13274
14022
|
MAX_HTML_SIZE,
|
|
14023
|
+
MAX_WARNING_LOCATIONS,
|
|
13275
14024
|
STRUCTURAL_FIX_PROPERTIES,
|
|
14025
|
+
VALUE_CAVEAT_PROPS,
|
|
13276
14026
|
alphaBlend,
|
|
13277
14027
|
analyzeEmail,
|
|
13278
14028
|
analyzeImages,
|
|
13279
14029
|
analyzeSpam,
|
|
13280
14030
|
auditEmail,
|
|
14031
|
+
caveatApplies,
|
|
13281
14032
|
checkAccessibility,
|
|
13282
14033
|
checkOverflow,
|
|
13283
14034
|
checkSize,
|