@emailens/engine 0.10.0 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +258 -247
- package/dist/compile/index.d.cts +2 -2
- package/dist/compile/index.d.ts +2 -2
- package/dist/index.cjs +1348 -518
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +64 -14
- package/dist/index.d.ts +64 -14
- package/dist/index.js +1344 -518
- package/dist/index.js.map +1 -1
- package/dist/{react-email-B1Rd5itD.d.cts → react-email-DTzVpGgB.d.cts} +1 -1
- package/dist/{react-email-D4XyNztP.d.ts → react-email-DwBWB2kr.d.ts} +1 -1
- package/dist/server.d.cts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/{types-DEAn3IiX.d.cts → types-BLR3-Fzo.d.cts} +58 -1
- package/dist/{types-DEAn3IiX.d.ts → types-BLR3-Fzo.d.ts} +58 -1
- package/package.json +124 -123
package/dist/index.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,360 +10390,256 @@ function transformForAllClients(html, framework) {
|
|
|
10375
10390
|
}
|
|
10376
10391
|
|
|
10377
10392
|
// src/analyze.ts
|
|
10378
|
-
import * as
|
|
10379
|
-
|
|
10380
|
-
|
|
10381
|
-
|
|
10382
|
-
"
|
|
10383
|
-
"
|
|
10384
|
-
"
|
|
10385
|
-
"
|
|
10386
|
-
"
|
|
10387
|
-
"
|
|
10388
|
-
"
|
|
10389
|
-
"
|
|
10390
|
-
"
|
|
10391
|
-
"
|
|
10392
|
-
"
|
|
10393
|
-
|
|
10394
|
-
|
|
10395
|
-
"
|
|
10396
|
-
}
|
|
10397
|
-
var
|
|
10398
|
-
|
|
10399
|
-
|
|
10400
|
-
|
|
10401
|
-
|
|
10402
|
-
|
|
10403
|
-
|
|
10404
|
-
|
|
10405
|
-
|
|
10406
|
-
|
|
10407
|
-
|
|
10408
|
-
|
|
10409
|
-
|
|
10410
|
-
|
|
10411
|
-
|
|
10412
|
-
|
|
10413
|
-
|
|
10414
|
-
|
|
10415
|
-
|
|
10416
|
-
|
|
10417
|
-
"<svg>": (n) => `${n} does not support inline SVG.`,
|
|
10418
|
-
"<video>": (n) => `${n} does not support <video> elements.`,
|
|
10419
|
-
"<form>": (n) => `${n} strips form elements.`
|
|
10420
|
-
};
|
|
10421
|
-
var COMPOUND_DETECTORS = [
|
|
10422
|
-
{ key: "display:flex", property: "display", valueIncludes: "flex" },
|
|
10423
|
-
{ key: "display:grid", property: "display", valueIncludes: "grid" },
|
|
10424
|
-
{ key: "display:none", property: "display", valueIncludes: "none" }
|
|
10425
|
-
];
|
|
10426
|
-
var CSS_FUNCTION_DETECTORS = CSS_FUNCTION_FEATURES.map((fn) => ({
|
|
10427
|
-
key: fn,
|
|
10428
|
-
pattern: `${fn}(`
|
|
10429
|
-
// require opening paren — matches "min(" but not "Minion"
|
|
10430
|
-
}));
|
|
10431
|
-
function analyzeEmailFromDom($, framework) {
|
|
10432
|
-
const warnings = [];
|
|
10433
|
-
const seenWarnings = /* @__PURE__ */ new Set();
|
|
10434
|
-
function addWarning(w) {
|
|
10435
|
-
const key = `${w.client}:${w.property}:${w.severity}:${w.selector || ""}`;
|
|
10436
|
-
if (!seenWarnings.has(key)) {
|
|
10437
|
-
seenWarnings.add(key);
|
|
10438
|
-
warnings.push(w);
|
|
10393
|
+
import * as csstree5 from "css-tree";
|
|
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;
|
|
10439
10432
|
}
|
|
10440
10433
|
}
|
|
10441
|
-
|
|
10442
|
-
|
|
10443
|
-
|
|
10444
|
-
|
|
10445
|
-
|
|
10446
|
-
|
|
10447
|
-
|
|
10448
|
-
|
|
10449
|
-
|
|
10450
|
-
|
|
10451
|
-
|
|
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()));
|
|
10452
10462
|
}
|
|
10453
|
-
|
|
10454
|
-
|
|
10455
|
-
|
|
10456
|
-
|
|
10457
|
-
|
|
10458
|
-
|
|
10459
|
-
|
|
10460
|
-
|
|
10461
|
-
|
|
10462
|
-
|
|
10463
|
-
|
|
10464
|
-
|
|
10465
|
-
|
|
10466
|
-
|
|
10467
|
-
|
|
10468
|
-
|
|
10469
|
-
|
|
10470
|
-
|
|
10471
|
-
|
|
10472
|
-
|
|
10473
|
-
|
|
10474
|
-
|
|
10475
|
-
|
|
10476
|
-
|
|
10477
|
-
|
|
10478
|
-
|
|
10479
|
-
|
|
10480
|
-
|
|
10481
|
-
|
|
10482
|
-
|
|
10483
|
-
|
|
10484
|
-
|
|
10485
|
-
|
|
10486
|
-
|
|
10487
|
-
|
|
10488
|
-
|
|
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);
|
|
10489
10531
|
}
|
|
10490
|
-
|
|
10491
|
-
|
|
10492
|
-
|
|
10493
|
-
|
|
10494
|
-
|
|
10495
|
-
|
|
10496
|
-
const detectedPseudoClasses = /* @__PURE__ */ new Set();
|
|
10497
|
-
const detectedPseudoElements = /* @__PURE__ */ new Set();
|
|
10498
|
-
$("style").each((_, el) => {
|
|
10499
|
-
const cssText = $(el).text();
|
|
10500
|
-
try {
|
|
10501
|
-
const ast = csstree3.parse(cssText, { parseCustomProperty: true, positions: true });
|
|
10502
|
-
csstree3.walk(ast, {
|
|
10503
|
-
enter(node) {
|
|
10504
|
-
if (node.type === "Atrule") {
|
|
10505
|
-
parsedAtRules.add(`@${node.name}`);
|
|
10506
|
-
}
|
|
10507
|
-
if (node.type === "PseudoClassSelector") {
|
|
10508
|
-
detectedPseudoClasses.add(`:${node.name}`);
|
|
10509
|
-
}
|
|
10510
|
-
if (node.type === "PseudoElementSelector") {
|
|
10511
|
-
detectedPseudoElements.add(`::${node.name}`);
|
|
10512
|
-
}
|
|
10513
|
-
if (node.type === "Declaration") {
|
|
10514
|
-
const prop = node.property.toLowerCase();
|
|
10515
|
-
parsedProperties.add(prop);
|
|
10516
|
-
if (node.loc && !propertyLines.has(prop)) {
|
|
10517
|
-
propertyLines.set(prop, node.loc.start.line);
|
|
10518
|
-
}
|
|
10519
|
-
const valueStr = csstree3.generate(node.value);
|
|
10520
|
-
const seenValues = propertyValues.get(prop);
|
|
10521
|
-
if (seenValues) seenValues.push(valueStr);
|
|
10522
|
-
else propertyValues.set(prop, [valueStr]);
|
|
10523
|
-
for (const det of COMPOUND_DETECTORS) {
|
|
10524
|
-
if (prop === det.property && valueStr.includes(det.valueIncludes)) {
|
|
10525
|
-
parsedProperties.add(det.key);
|
|
10526
|
-
if (node.loc && !propertyLines.has(det.key)) {
|
|
10527
|
-
propertyLines.set(det.key, node.loc.start.line);
|
|
10528
|
-
}
|
|
10529
|
-
}
|
|
10530
|
-
}
|
|
10531
|
-
for (const fn of CSS_FUNCTION_DETECTORS) {
|
|
10532
|
-
if (valueStr.includes(fn.pattern)) {
|
|
10533
|
-
detectedCssFunctions.add(fn.key);
|
|
10534
|
-
if (node.loc && !propertyLines.has(fn.key)) {
|
|
10535
|
-
propertyLines.set(fn.key, node.loc.start.line);
|
|
10536
|
-
}
|
|
10537
|
-
}
|
|
10538
|
-
}
|
|
10539
|
-
}
|
|
10540
|
-
}
|
|
10541
|
-
});
|
|
10542
|
-
} catch (e) {
|
|
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;
|
|
10543
10538
|
}
|
|
10544
|
-
|
|
10545
|
-
|
|
10546
|
-
|
|
10547
|
-
|
|
10548
|
-
|
|
10549
|
-
|
|
10550
|
-
|
|
10551
|
-
|
|
10552
|
-
$("[style]").each((_, el) => {
|
|
10553
|
-
var _a;
|
|
10554
|
-
const style = $(el).attr("style") || "";
|
|
10555
|
-
const props = parseStyleProperties(style);
|
|
10556
|
-
const selector = describeSelector(el);
|
|
10557
|
-
for (const prop of props) {
|
|
10558
|
-
for (const det of COMPOUND_DETECTORS) {
|
|
10559
|
-
if (prop === det.property) {
|
|
10560
|
-
const value2 = getStyleValue(style, prop);
|
|
10561
|
-
if (value2 == null ? void 0 : value2.includes(det.valueIncludes)) {
|
|
10562
|
-
checkPropertySupport(det.key, addWarning, framework, selector);
|
|
10563
|
-
}
|
|
10564
|
-
}
|
|
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");
|
|
10565
10547
|
}
|
|
10566
|
-
if (
|
|
10567
|
-
|
|
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));
|
|
10568
10557
|
}
|
|
10569
|
-
const
|
|
10570
|
-
if (
|
|
10571
|
-
|
|
10572
|
-
|
|
10573
|
-
checkPropertySupport(fn.key, addWarning, framework, selector);
|
|
10574
|
-
}
|
|
10575
|
-
}
|
|
10558
|
+
const { banned } = quotedValues(note);
|
|
10559
|
+
if (banned.length) {
|
|
10560
|
+
const v = dashed(value);
|
|
10561
|
+
return banned.includes(v) || banned.includes(unprefixed(v));
|
|
10576
10562
|
}
|
|
10563
|
+
if (noteLc.includes("two-value syntax")) return tokens(value).length > 1;
|
|
10564
|
+
return true;
|
|
10577
10565
|
}
|
|
10578
|
-
|
|
10579
|
-
|
|
10580
|
-
|
|
10581
|
-
|
|
10582
|
-
|
|
10583
|
-
|
|
10584
|
-
|
|
10585
|
-
|
|
10586
|
-
framework,
|
|
10587
|
-
void 0,
|
|
10588
|
-
propertyLines.get(prop),
|
|
10589
|
-
values ? values.join(" ") : void 0
|
|
10590
|
-
);
|
|
10591
|
-
}
|
|
10592
|
-
for (const compound of COMPOUND_VALUE_FEATURES) {
|
|
10593
|
-
if (compound.startsWith(":") || compound.startsWith("::")) continue;
|
|
10594
|
-
if (parsedProperties.has(compound)) {
|
|
10595
|
-
checkPropertySupport(compound, addWarning, framework, void 0, propertyLines.get(compound));
|
|
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;
|
|
10596
10574
|
}
|
|
10597
|
-
|
|
10598
|
-
|
|
10599
|
-
|
|
10600
|
-
checkPropertySupport(pseudo, addWarning, framework);
|
|
10575
|
+
case "border-radius": {
|
|
10576
|
+
if (noteLc.includes("slash")) return topLevelSplit(value, "/").length > 1;
|
|
10577
|
+
return true;
|
|
10601
10578
|
}
|
|
10602
|
-
|
|
10603
|
-
|
|
10604
|
-
|
|
10605
|
-
|
|
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
|
+
);
|
|
10606
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;
|
|
10607
10608
|
}
|
|
10608
|
-
for (const fn of detectedCssFunctions) {
|
|
10609
|
-
checkPropertySupport(fn, addWarning, framework, void 0, propertyLines.get(fn));
|
|
10610
|
-
}
|
|
10611
|
-
const severityOrder = { error: 0, warning: 1, info: 2 };
|
|
10612
|
-
warnings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
10613
|
-
return warnings;
|
|
10614
10609
|
}
|
|
10615
|
-
function
|
|
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
|
+
|
|
10618
|
+
// src/dark-mode-checker.ts
|
|
10619
|
+
import * as csstree4 from "css-tree";
|
|
10620
|
+
|
|
10621
|
+
// src/dark-mode.ts
|
|
10622
|
+
import * as cheerio3 from "cheerio";
|
|
10623
|
+
import * as csstree3 from "css-tree";
|
|
10624
|
+
var LIGHT_THRESHOLD = 0.7;
|
|
10625
|
+
var DARK_THRESHOLD = 0.15;
|
|
10626
|
+
var PREFERS_COLOR_SCHEME_CLIENTS = [
|
|
10627
|
+
"apple-mail-macos",
|
|
10628
|
+
"apple-mail-ios",
|
|
10629
|
+
"samsung-mail",
|
|
10630
|
+
"thunderbird",
|
|
10631
|
+
"hey-mail",
|
|
10632
|
+
"superhuman"
|
|
10633
|
+
];
|
|
10634
|
+
function simulateDarkMode(html, clientId) {
|
|
10635
|
+
var _a, _b;
|
|
10616
10636
|
if (!html || !html.trim()) {
|
|
10617
|
-
return [];
|
|
10637
|
+
return { html: html || "", warnings: [] };
|
|
10618
10638
|
}
|
|
10619
10639
|
if (html.length > MAX_HTML_SIZE) {
|
|
10620
10640
|
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
10621
10641
|
}
|
|
10622
10642
|
const $ = cheerio3.load(html);
|
|
10623
|
-
return analyzeEmailFromDom($, framework);
|
|
10624
|
-
}
|
|
10625
|
-
function getFixType(prop) {
|
|
10626
|
-
return STRUCTURAL_FIX_PROPERTIES.has(prop) ? "structural" : "css";
|
|
10627
|
-
}
|
|
10628
|
-
var VALUE_CAVEAT_PROPS = /* @__PURE__ */ new Set(["margin", "position", "overflow"]);
|
|
10629
|
-
var POSITION_KEYWORDS = ["relative", "absolute", "fixed", "sticky"];
|
|
10630
|
-
function valueTriggersCaveat(prop, value, notes) {
|
|
10631
|
-
const note = (notes != null ? notes : []).join(" ");
|
|
10632
|
-
const noteLc = note.toLowerCase();
|
|
10633
|
-
if (prop === "margin") {
|
|
10634
|
-
if (/(?:^|[\s:(])-\.?\d/.test(value) && noteLc.includes("negative")) return true;
|
|
10635
|
-
if (/\bauto\b/.test(value) && noteLc.includes("auto")) return true;
|
|
10636
|
-
return false;
|
|
10637
|
-
}
|
|
10638
|
-
if (prop === "position") {
|
|
10639
|
-
const used = POSITION_KEYWORDS.find((k) => new RegExp(`\\b${k}\\b`).test(value));
|
|
10640
|
-
if (!used) return false;
|
|
10641
|
-
const m = note.match(/supports\s+.+?\s+but not\s+([^.]+)/i);
|
|
10642
|
-
if (m) return m[1].toLowerCase().includes(used);
|
|
10643
|
-
return used === "fixed" || used === "sticky";
|
|
10644
|
-
}
|
|
10645
|
-
if (prop === "overflow") {
|
|
10646
|
-
if (!/\b(?:auto|scroll)\b/.test(value)) return false;
|
|
10647
|
-
return noteLc.includes("cannot scroll");
|
|
10648
|
-
}
|
|
10649
|
-
return true;
|
|
10650
|
-
}
|
|
10651
|
-
function noteSuffix(notes) {
|
|
10652
|
-
if (!(notes == null ? void 0 : notes.length)) return "";
|
|
10653
|
-
const cleaned = notes.map((n) => n.replace(/^(?:Partial|Buggy|Not supported)\.\s*/i, "").trim()).filter(Boolean);
|
|
10654
|
-
return cleaned.length ? ` ${cleaned.join(" ")}` : "";
|
|
10655
|
-
}
|
|
10656
|
-
function checkPropertySupport(prop, addWarning, framework, selector, line, value) {
|
|
10657
|
-
var _a;
|
|
10658
|
-
const supportData = CSS_SUPPORT[prop];
|
|
10659
|
-
if (!supportData) return;
|
|
10660
|
-
const fixType = getFixType(prop);
|
|
10661
|
-
const valueGated = VALUE_CAVEAT_PROPS.has(prop);
|
|
10662
|
-
for (const client of EMAIL_CLIENTS) {
|
|
10663
|
-
const support = supportData[client.id] || "unknown";
|
|
10664
|
-
const notes = (_a = CSS_SUPPORT_NOTES[prop]) == null ? void 0 : _a[client.id];
|
|
10665
|
-
if (support === "unsupported") {
|
|
10666
|
-
const sug = getSuggestion(prop, client.id, framework);
|
|
10667
|
-
const fix = getCodeFix(prop, client.id, framework);
|
|
10668
|
-
addWarning(__spreadValues(__spreadValues(__spreadValues({
|
|
10669
|
-
severity: "warning",
|
|
10670
|
-
client: client.id,
|
|
10671
|
-
property: prop,
|
|
10672
|
-
message: `${client.name} does not support "${prop}".${noteSuffix(notes)}`,
|
|
10673
|
-
suggestion: sug.text,
|
|
10674
|
-
fix,
|
|
10675
|
-
fixType
|
|
10676
|
-
}, selector ? { selector } : {}), line !== void 0 ? { line } : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
10677
|
-
} else if (support === "partial") {
|
|
10678
|
-
if (valueGated && value !== void 0 && !valueTriggersCaveat(prop, value, notes)) continue;
|
|
10679
|
-
const sug = getSuggestion(prop, client.id, framework);
|
|
10680
|
-
const fix = getCodeFix(prop, client.id, framework);
|
|
10681
|
-
addWarning(__spreadValues(__spreadValues(__spreadValues({
|
|
10682
|
-
severity: "info",
|
|
10683
|
-
client: client.id,
|
|
10684
|
-
property: prop,
|
|
10685
|
-
message: `${client.name} has partial support for "${prop}".${noteSuffix(notes)}`,
|
|
10686
|
-
suggestion: sug.text,
|
|
10687
|
-
fix,
|
|
10688
|
-
fixType
|
|
10689
|
-
}, selector ? { selector } : {}), line !== void 0 ? { line } : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
10690
|
-
}
|
|
10691
|
-
}
|
|
10692
|
-
}
|
|
10693
|
-
function generateCompatibilityScore(warnings) {
|
|
10694
|
-
const result = {};
|
|
10695
|
-
for (const client of EMAIL_CLIENTS) {
|
|
10696
|
-
const clientWarnings = warnings.filter((w) => w.client === client.id);
|
|
10697
|
-
const errorProps = new Set(clientWarnings.filter((w) => w.severity === "error").map((w) => w.property));
|
|
10698
|
-
const warnProps = new Set(clientWarnings.filter((w) => w.severity === "warning").map((w) => w.property));
|
|
10699
|
-
const infoProps = new Set(clientWarnings.filter((w) => w.severity === "info").map((w) => w.property));
|
|
10700
|
-
const errors = errorProps.size;
|
|
10701
|
-
const warns = warnProps.size;
|
|
10702
|
-
const info = infoProps.size;
|
|
10703
|
-
const score = Math.max(0, Math.min(100, 100 - errors * 10 - warns * 3));
|
|
10704
|
-
result[client.id] = { score, errors, warnings: warns, info };
|
|
10705
|
-
}
|
|
10706
|
-
return result;
|
|
10707
|
-
}
|
|
10708
|
-
function warningsForClient(warnings, clientId) {
|
|
10709
|
-
return warnings.filter((w) => w.client === clientId);
|
|
10710
|
-
}
|
|
10711
|
-
function errorWarnings(warnings) {
|
|
10712
|
-
return warnings.filter((w) => w.severity === "error");
|
|
10713
|
-
}
|
|
10714
|
-
function structuralWarnings(warnings) {
|
|
10715
|
-
return warnings.filter((w) => w.fixType === "structural");
|
|
10716
|
-
}
|
|
10717
|
-
|
|
10718
|
-
// src/dark-mode.ts
|
|
10719
|
-
import * as cheerio4 from "cheerio";
|
|
10720
|
-
import * as csstree4 from "css-tree";
|
|
10721
|
-
var LIGHT_THRESHOLD = 0.7;
|
|
10722
|
-
var DARK_THRESHOLD = 0.15;
|
|
10723
|
-
function simulateDarkMode(html, clientId) {
|
|
10724
|
-
var _a, _b;
|
|
10725
|
-
if (!html || !html.trim()) {
|
|
10726
|
-
return { html: html || "", warnings: [] };
|
|
10727
|
-
}
|
|
10728
|
-
if (html.length > MAX_HTML_SIZE) {
|
|
10729
|
-
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
10730
|
-
}
|
|
10731
|
-
const $ = cheerio4.load(html);
|
|
10732
10643
|
const warnings = [];
|
|
10733
10644
|
$("img").each((_, el) => {
|
|
10734
10645
|
const src = $(el).attr("src") || "";
|
|
@@ -10938,46 +10849,802 @@ function applyColorInversion($, mode) {
|
|
|
10938
10849
|
$("style").each((_, el) => {
|
|
10939
10850
|
const cssText = $(el).text();
|
|
10940
10851
|
try {
|
|
10941
|
-
const ast =
|
|
10852
|
+
const ast = csstree3.parse(cssText, { parseCustomProperty: true });
|
|
10942
10853
|
let modified = false;
|
|
10943
|
-
|
|
10854
|
+
csstree3.walk(ast, {
|
|
10944
10855
|
enter(node) {
|
|
10945
10856
|
if (node.type !== "Declaration") return;
|
|
10946
10857
|
const prop = node.property.toLowerCase();
|
|
10947
10858
|
if (!COLOR_PROPS.has(prop) && prop !== "background") return;
|
|
10948
|
-
const valueStr =
|
|
10859
|
+
const valueStr = csstree3.generate(node.value);
|
|
10949
10860
|
if (prop === "background") {
|
|
10950
10861
|
const bgColor = extractBackgroundColor(valueStr);
|
|
10951
10862
|
if (bgColor) {
|
|
10952
10863
|
const inverted = invertColor(bgColor, mode);
|
|
10953
10864
|
if (inverted) {
|
|
10954
10865
|
const newValue = valueStr.replace(bgColor, inverted);
|
|
10955
|
-
node.value =
|
|
10866
|
+
node.value = csstree3.parse(newValue, { context: "value" });
|
|
10956
10867
|
modified = true;
|
|
10957
10868
|
}
|
|
10958
10869
|
}
|
|
10959
10870
|
} else {
|
|
10960
10871
|
const inverted = invertColor(valueStr, mode);
|
|
10961
10872
|
if (inverted) {
|
|
10962
|
-
node.value =
|
|
10873
|
+
node.value = csstree3.parse(inverted, { context: "value" });
|
|
10963
10874
|
modified = true;
|
|
10964
10875
|
}
|
|
10965
10876
|
}
|
|
10966
10877
|
}
|
|
10967
|
-
});
|
|
10968
|
-
if (modified) {
|
|
10969
|
-
$(el).text(
|
|
10878
|
+
});
|
|
10879
|
+
if (modified) {
|
|
10880
|
+
$(el).text(csstree3.generate(ast));
|
|
10881
|
+
}
|
|
10882
|
+
} catch (e) {
|
|
10883
|
+
}
|
|
10884
|
+
});
|
|
10885
|
+
$("[bgcolor]").each((_, el) => {
|
|
10886
|
+
const bgcolor = $(el).attr("bgcolor") || "";
|
|
10887
|
+
const inverted = invertColor(bgcolor, mode);
|
|
10888
|
+
if (inverted) {
|
|
10889
|
+
$(el).attr("bgcolor", inverted);
|
|
10890
|
+
}
|
|
10891
|
+
});
|
|
10892
|
+
}
|
|
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 locOfFirst($, selector) {
|
|
10919
|
+
const el = $(selector).first()[0];
|
|
10920
|
+
return el ? locOfElement(el) : void 0;
|
|
10921
|
+
}
|
|
10922
|
+
function cssBlockAnchor(styleEl, cssText, source) {
|
|
10923
|
+
var _a;
|
|
10924
|
+
const children = styleEl == null ? void 0 : styleEl.children;
|
|
10925
|
+
if (!children || children.length !== 1) return void 0;
|
|
10926
|
+
const loc = (_a = children[0]) == null ? void 0 : _a.sourceCodeLocation;
|
|
10927
|
+
if (!loc) return void 0;
|
|
10928
|
+
const mapper = source ? crMapper(source.slice(loc.startOffset, loc.endOffset), cssText) : null;
|
|
10929
|
+
const extraBefore = mapper ? (index) => mapper(index) - index : crOffsetter(cssText, loc.endOffset - loc.startOffset);
|
|
10930
|
+
return __spreadValues({ loc, extraBefore }, mapper ? { source } : {});
|
|
10931
|
+
}
|
|
10932
|
+
function crMapper(raw, decoded) {
|
|
10933
|
+
if (raw.length === decoded.length) return (index) => index;
|
|
10934
|
+
const points = [];
|
|
10935
|
+
const extras = [];
|
|
10936
|
+
let r = 0;
|
|
10937
|
+
let d = 0;
|
|
10938
|
+
let extra = 0;
|
|
10939
|
+
while (d < decoded.length) {
|
|
10940
|
+
if (r >= raw.length) return null;
|
|
10941
|
+
if (raw[r] === decoded[d]) {
|
|
10942
|
+
r++;
|
|
10943
|
+
d++;
|
|
10944
|
+
continue;
|
|
10945
|
+
}
|
|
10946
|
+
if (raw[r] === "\r" && decoded[d] === "\n") {
|
|
10947
|
+
const consumed = raw[r + 1] === "\n" ? 2 : 1;
|
|
10948
|
+
extra += consumed - 1;
|
|
10949
|
+
points.push(d);
|
|
10950
|
+
extras.push(extra);
|
|
10951
|
+
r += consumed;
|
|
10952
|
+
d += 1;
|
|
10953
|
+
continue;
|
|
10954
|
+
}
|
|
10955
|
+
return null;
|
|
10956
|
+
}
|
|
10957
|
+
if (r !== raw.length) return null;
|
|
10958
|
+
return (index) => index + lookup(points, extras, index);
|
|
10959
|
+
}
|
|
10960
|
+
function lookup(points, extras, index) {
|
|
10961
|
+
let lo = 0;
|
|
10962
|
+
let hi = points.length - 1;
|
|
10963
|
+
let found = 0;
|
|
10964
|
+
while (lo <= hi) {
|
|
10965
|
+
const mid = lo + hi >> 1;
|
|
10966
|
+
if (points[mid] < index) {
|
|
10967
|
+
found = extras[mid];
|
|
10968
|
+
lo = mid + 1;
|
|
10969
|
+
} else {
|
|
10970
|
+
hi = mid - 1;
|
|
10971
|
+
}
|
|
10972
|
+
}
|
|
10973
|
+
return found;
|
|
10974
|
+
}
|
|
10975
|
+
function findRawOffset(raw, decoded, index, token) {
|
|
10976
|
+
if (!token) return -1;
|
|
10977
|
+
let occurrence = 0;
|
|
10978
|
+
for (let at = decoded.indexOf(token); at !== -1 && at < index; at = decoded.indexOf(token, at + 1)) {
|
|
10979
|
+
occurrence++;
|
|
10980
|
+
}
|
|
10981
|
+
let found = -1;
|
|
10982
|
+
let from = 0;
|
|
10983
|
+
for (let i = 0; i <= occurrence; i++) {
|
|
10984
|
+
found = raw.indexOf(token, from);
|
|
10985
|
+
if (found === -1) return -1;
|
|
10986
|
+
from = found + 1;
|
|
10987
|
+
}
|
|
10988
|
+
return found;
|
|
10989
|
+
}
|
|
10990
|
+
function positionOf(source, offset) {
|
|
10991
|
+
const prefix = source.slice(0, offset);
|
|
10992
|
+
return { line: prefix.split("\n").length, column: offset - prefix.lastIndexOf("\n") };
|
|
10993
|
+
}
|
|
10994
|
+
function crOffsetter(text, rawLength) {
|
|
10995
|
+
const removed = rawLength - text.length;
|
|
10996
|
+
if (removed === 0) return () => 0;
|
|
10997
|
+
const newlines = countNewlines(text, text.length);
|
|
10998
|
+
if (removed !== newlines || newlines === 0) return null;
|
|
10999
|
+
return (index) => countNewlines(text, index);
|
|
11000
|
+
}
|
|
11001
|
+
function countNewlines(text, upTo) {
|
|
11002
|
+
let n = 0;
|
|
11003
|
+
for (let i = 0; i < upTo && i < text.length; i++) if (text.charCodeAt(i) === 10) n++;
|
|
11004
|
+
return n;
|
|
11005
|
+
}
|
|
11006
|
+
function locInCssBlock(anchor, cssLoc) {
|
|
11007
|
+
if (!anchor || !cssLoc) return void 0;
|
|
11008
|
+
const { loc: block, extraBefore } = anchor;
|
|
11009
|
+
if (!extraBefore) {
|
|
11010
|
+
return {
|
|
11011
|
+
line: block.startLine,
|
|
11012
|
+
column: block.startCol,
|
|
11013
|
+
endLine: block.startLine,
|
|
11014
|
+
endColumn: block.startCol,
|
|
11015
|
+
offset: block.startOffset,
|
|
11016
|
+
length: 0
|
|
11017
|
+
};
|
|
11018
|
+
}
|
|
11019
|
+
const line = block.startLine + cssLoc.start.line - 1;
|
|
11020
|
+
const column = cssLoc.start.line === 1 ? block.startCol + cssLoc.start.column - 1 : cssLoc.start.column;
|
|
11021
|
+
const endLine = block.startLine + cssLoc.end.line - 1;
|
|
11022
|
+
const endColumn = cssLoc.end.line === 1 ? block.startCol + cssLoc.end.column - 1 : cssLoc.end.column;
|
|
11023
|
+
const start = block.startOffset + cssLoc.start.offset + extraBefore(cssLoc.start.offset);
|
|
11024
|
+
const end = block.startOffset + cssLoc.end.offset + extraBefore(cssLoc.end.offset);
|
|
11025
|
+
if (anchor.source) {
|
|
11026
|
+
const from = positionOf(anchor.source, start);
|
|
11027
|
+
const to = positionOf(anchor.source, end);
|
|
11028
|
+
return {
|
|
11029
|
+
line: from.line,
|
|
11030
|
+
column: from.column,
|
|
11031
|
+
endLine: to.line,
|
|
11032
|
+
endColumn: to.column,
|
|
11033
|
+
offset: start,
|
|
11034
|
+
length: end - start
|
|
11035
|
+
};
|
|
11036
|
+
}
|
|
11037
|
+
return { line, column, endLine, endColumn, offset: start, length: end - start };
|
|
11038
|
+
}
|
|
11039
|
+
function locInTextNode(node, index, length, source) {
|
|
11040
|
+
var _a;
|
|
11041
|
+
const anchor = node == null ? void 0 : node.sourceCodeLocation;
|
|
11042
|
+
if (!anchor) return void 0;
|
|
11043
|
+
const data = (_a = node.data) != null ? _a : "";
|
|
11044
|
+
const rawLength = anchor.endOffset - anchor.startOffset;
|
|
11045
|
+
if (source) {
|
|
11046
|
+
const raw = source.slice(anchor.startOffset, anchor.endOffset);
|
|
11047
|
+
const token = data.slice(index, index + length);
|
|
11048
|
+
const at = findRawOffset(raw, data, index, token);
|
|
11049
|
+
if (at !== -1) {
|
|
11050
|
+
const start2 = anchor.startOffset + at;
|
|
11051
|
+
const from = positionOf(source, start2);
|
|
11052
|
+
const to = positionOf(source, start2 + token.length);
|
|
11053
|
+
return {
|
|
11054
|
+
line: from.line,
|
|
11055
|
+
column: from.column,
|
|
11056
|
+
endLine: to.line,
|
|
11057
|
+
endColumn: to.column,
|
|
11058
|
+
offset: start2,
|
|
11059
|
+
length: token.length
|
|
11060
|
+
};
|
|
11061
|
+
}
|
|
11062
|
+
}
|
|
11063
|
+
const extraBefore = crOffsetter(data, rawLength);
|
|
11064
|
+
if (!extraBefore) {
|
|
11065
|
+
const clamped = Math.min(length, rawLength);
|
|
11066
|
+
return {
|
|
11067
|
+
line: anchor.startLine,
|
|
11068
|
+
column: anchor.startCol,
|
|
11069
|
+
endLine: anchor.startLine,
|
|
11070
|
+
endColumn: anchor.startCol + clamped,
|
|
11071
|
+
offset: anchor.startOffset,
|
|
11072
|
+
length: clamped
|
|
11073
|
+
};
|
|
11074
|
+
}
|
|
11075
|
+
const start = positionAt(data, index, anchor);
|
|
11076
|
+
const end = positionAt(data, index + length, anchor);
|
|
11077
|
+
const startOffset = anchor.startOffset + index + extraBefore(index);
|
|
11078
|
+
const endOffset = anchor.startOffset + index + length + extraBefore(index + length);
|
|
11079
|
+
return {
|
|
11080
|
+
line: start.line,
|
|
11081
|
+
column: start.column,
|
|
11082
|
+
endLine: end.line,
|
|
11083
|
+
endColumn: end.column,
|
|
11084
|
+
offset: startOffset,
|
|
11085
|
+
length: endOffset - startOffset
|
|
11086
|
+
};
|
|
11087
|
+
}
|
|
11088
|
+
function positionAt(data, index, anchor) {
|
|
11089
|
+
const prefix = data.slice(0, index);
|
|
11090
|
+
const newlines = prefix.split("\n").length - 1;
|
|
11091
|
+
if (newlines === 0) {
|
|
11092
|
+
return { line: anchor.startLine, column: anchor.startCol + index };
|
|
11093
|
+
}
|
|
11094
|
+
return { line: anchor.startLine + newlines, column: index - prefix.lastIndexOf("\n") };
|
|
11095
|
+
}
|
|
11096
|
+
|
|
11097
|
+
// src/dark-mode-checker.ts
|
|
11098
|
+
var DARK_MEDIA_RE = /\(\s*prefers-color-scheme\s*:\s*dark\s*\)/i;
|
|
11099
|
+
var MAX_UNCOVERED_ELEMENTS = 3;
|
|
11100
|
+
function backgroundShorthandColor(value) {
|
|
11101
|
+
if (!value) return null;
|
|
11102
|
+
const trimmed = value.trim();
|
|
11103
|
+
if (parseColor(trimmed)) return trimmed;
|
|
11104
|
+
for (const token of trimmed.split(/\s+/)) {
|
|
11105
|
+
if (token.includes("(")) continue;
|
|
11106
|
+
if (parseColor(token)) return token;
|
|
11107
|
+
}
|
|
11108
|
+
return null;
|
|
11109
|
+
}
|
|
11110
|
+
function isLight(value) {
|
|
11111
|
+
const c = parseColor(value);
|
|
11112
|
+
if (!c || c.a < 0.5) return false;
|
|
11113
|
+
return relativeLuminance(c.r, c.g, c.b) > LIGHT_THRESHOLD;
|
|
11114
|
+
}
|
|
11115
|
+
function collectDarkBlocks($) {
|
|
11116
|
+
const block = { any: [], important: [], rules: 0 };
|
|
11117
|
+
let found = false;
|
|
11118
|
+
$("style").each((_, el) => {
|
|
11119
|
+
const cssText = $(el).text();
|
|
11120
|
+
if (!DARK_MEDIA_RE.test(cssText)) return;
|
|
11121
|
+
let ast;
|
|
11122
|
+
try {
|
|
11123
|
+
ast = csstree4.parse(cssText);
|
|
11124
|
+
} catch (e) {
|
|
11125
|
+
found = true;
|
|
11126
|
+
return;
|
|
11127
|
+
}
|
|
11128
|
+
csstree4.walk(ast, {
|
|
11129
|
+
visit: "Atrule",
|
|
11130
|
+
enter(node) {
|
|
11131
|
+
if (node.type !== "Atrule" || node.name.toLowerCase() !== "media") return;
|
|
11132
|
+
if (!node.prelude || !DARK_MEDIA_RE.test(csstree4.generate(node.prelude))) return;
|
|
11133
|
+
found = true;
|
|
11134
|
+
if (!node.block) return;
|
|
11135
|
+
csstree4.walk(node.block, {
|
|
11136
|
+
visit: "Rule",
|
|
11137
|
+
enter(rule) {
|
|
11138
|
+
if (rule.type !== "Rule") return;
|
|
11139
|
+
block.rules++;
|
|
11140
|
+
let setsBackground = false;
|
|
11141
|
+
let important = false;
|
|
11142
|
+
rule.block.children.forEach((child) => {
|
|
11143
|
+
if (child.type !== "Declaration") return;
|
|
11144
|
+
const prop = child.property.toLowerCase();
|
|
11145
|
+
if (prop !== "background" && prop !== "background-color") return;
|
|
11146
|
+
setsBackground = true;
|
|
11147
|
+
if (child.important) important = true;
|
|
11148
|
+
});
|
|
11149
|
+
if (!setsBackground) return;
|
|
11150
|
+
const selector = csstree4.generate(rule.prelude).trim();
|
|
11151
|
+
if (!selector) return;
|
|
11152
|
+
block.any.push(selector);
|
|
11153
|
+
if (important) block.important.push(selector);
|
|
11154
|
+
}
|
|
11155
|
+
});
|
|
11156
|
+
}
|
|
11157
|
+
});
|
|
11158
|
+
});
|
|
11159
|
+
return found ? block : null;
|
|
11160
|
+
}
|
|
11161
|
+
function matchedElements($, selectors) {
|
|
11162
|
+
const matched = /* @__PURE__ */ new Set();
|
|
11163
|
+
for (const selector of selectors) {
|
|
11164
|
+
try {
|
|
11165
|
+
$(selector).each((_, el) => {
|
|
11166
|
+
matched.add(el);
|
|
11167
|
+
});
|
|
11168
|
+
} catch (e) {
|
|
11169
|
+
}
|
|
11170
|
+
}
|
|
11171
|
+
return matched;
|
|
11172
|
+
}
|
|
11173
|
+
function describeSelector($, el) {
|
|
11174
|
+
var _a;
|
|
11175
|
+
const $el = $(el);
|
|
11176
|
+
const tag = ((_a = el.tagName) == null ? void 0 : _a.toLowerCase()) || "element";
|
|
11177
|
+
const id = $el.attr("id");
|
|
11178
|
+
if (id) return `${tag}#${id}`;
|
|
11179
|
+
const cls = $el.attr("class");
|
|
11180
|
+
if (cls) return `${tag}.${cls.split(/\s+/)[0]}`;
|
|
11181
|
+
return tag;
|
|
11182
|
+
}
|
|
11183
|
+
function checkDarkModeFromDom($) {
|
|
11184
|
+
var _a, _b;
|
|
11185
|
+
const darkBlock = collectDarkBlocks($);
|
|
11186
|
+
if (!darkBlock) return [];
|
|
11187
|
+
const warnings = [];
|
|
11188
|
+
const hasOptIn = $("meta").toArray().some((el) => {
|
|
11189
|
+
const name = ($(el).attr("name") || "").trim().toLowerCase();
|
|
11190
|
+
return name === "color-scheme" || name === "supported-color-schemes";
|
|
11191
|
+
});
|
|
11192
|
+
if (!hasOptIn) {
|
|
11193
|
+
const headLoc = locOfFirst($, "head");
|
|
11194
|
+
for (const clientId of PREFERS_COLOR_SCHEME_CLIENTS) {
|
|
11195
|
+
warnings.push(__spreadValues({
|
|
11196
|
+
severity: "warning",
|
|
11197
|
+
client: clientId,
|
|
11198
|
+
property: "dark-mode-opt-in",
|
|
11199
|
+
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.`,
|
|
11200
|
+
suggestion: 'Add both opt-in tags to <head>: <meta name="color-scheme" content="light dark"> and <meta name="supported-color-schemes" content="light dark">.',
|
|
11201
|
+
fixType: "structural"
|
|
11202
|
+
}, headLoc ? { loc: headLoc, locs: [headLoc] } : {}));
|
|
11203
|
+
}
|
|
11204
|
+
}
|
|
11205
|
+
if (darkBlock.rules === 0) return warnings;
|
|
11206
|
+
const coveredByImportant = matchedElements($, darkBlock.important);
|
|
11207
|
+
const coveredByAny = matchedElements($, darkBlock.any);
|
|
11208
|
+
let uncovered = 0;
|
|
11209
|
+
$("[bgcolor], [style]").each((_, el) => {
|
|
11210
|
+
var _a2;
|
|
11211
|
+
if (uncovered >= MAX_UNCOVERED_ELEMENTS) return false;
|
|
11212
|
+
const $el = $(el);
|
|
11213
|
+
const style = parseInlineStyle($el.attr("style") || "");
|
|
11214
|
+
const inline = (_a2 = style.get("background-color")) != null ? _a2 : backgroundShorthandColor(style.get("background"));
|
|
11215
|
+
const color = inline != null ? inline : $el.attr("bgcolor");
|
|
11216
|
+
if (!color || !isLight(color)) return;
|
|
11217
|
+
if (inline ? coveredByImportant.has(el) : coveredByAny.has(el)) return;
|
|
11218
|
+
uncovered++;
|
|
11219
|
+
const selector = describeSelector($, el);
|
|
11220
|
+
const loc = locOfAttr(el, inline ? "style" : "bgcolor");
|
|
11221
|
+
for (const clientId of PREFERS_COLOR_SCHEME_CLIENTS) {
|
|
11222
|
+
warnings.push(__spreadValues({
|
|
11223
|
+
severity: "warning",
|
|
11224
|
+
client: clientId,
|
|
11225
|
+
property: "dark-mode-coverage",
|
|
11226
|
+
message: `<${selector}> keeps its hardcoded light background (${color}) in dark mode \u2014 the dark block never overrides it. The rest of the email inverts around it, producing a half-inverted render (e.g. light text left on a still-white background).`,
|
|
11227
|
+
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.`,
|
|
11228
|
+
fixType: "css",
|
|
11229
|
+
selector
|
|
11230
|
+
}, loc ? { loc, locs: [loc] } : {}));
|
|
11231
|
+
}
|
|
11232
|
+
});
|
|
11233
|
+
return warnings;
|
|
11234
|
+
}
|
|
11235
|
+
|
|
11236
|
+
// src/parse-html.ts
|
|
11237
|
+
import * as cheerio4 from "cheerio";
|
|
11238
|
+
function loadHtml(html, options) {
|
|
11239
|
+
return (options == null ? void 0 : options.positions) ? cheerio4.load(html, { sourceCodeLocationInfo: true }) : cheerio4.load(html);
|
|
11240
|
+
}
|
|
11241
|
+
function fromHtml(html, empty, fn, options) {
|
|
11242
|
+
if (!html || !html.trim()) return empty;
|
|
11243
|
+
if (html.length > MAX_HTML_SIZE) {
|
|
11244
|
+
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
11245
|
+
}
|
|
11246
|
+
return fn(loadHtml(html, options), html);
|
|
11247
|
+
}
|
|
11248
|
+
|
|
11249
|
+
// src/analyze.ts
|
|
11250
|
+
var HTML_ELEMENT_SELECTORS = {
|
|
11251
|
+
"<style>": "style",
|
|
11252
|
+
"<link>": "link[rel='stylesheet']",
|
|
11253
|
+
"<svg>": "svg",
|
|
11254
|
+
"<video>": "video",
|
|
11255
|
+
"<form>": "form, input, button[type='submit']",
|
|
11256
|
+
"<audio>": "audio",
|
|
11257
|
+
"<picture>": "picture",
|
|
11258
|
+
"<dialog>": "dialog",
|
|
11259
|
+
"<meter>": "meter",
|
|
11260
|
+
"<progress>": "progress",
|
|
11261
|
+
"<select>": "select",
|
|
11262
|
+
"<textarea>": "textarea",
|
|
11263
|
+
"<marquee>": "marquee",
|
|
11264
|
+
"<object>": "object",
|
|
11265
|
+
"<base>": "base"
|
|
11266
|
+
};
|
|
11267
|
+
var HTML_ELEMENT_SEVERITY = {
|
|
11268
|
+
"<style>": "error",
|
|
11269
|
+
"<link>": "error",
|
|
11270
|
+
"<svg>": "error",
|
|
11271
|
+
"<form>": "error",
|
|
11272
|
+
"<video>": "warning",
|
|
11273
|
+
"<audio>": "warning",
|
|
11274
|
+
"<picture>": "warning",
|
|
11275
|
+
"<dialog>": "warning",
|
|
11276
|
+
"<marquee>": "warning",
|
|
11277
|
+
"<meter>": "warning",
|
|
11278
|
+
"<progress>": "warning",
|
|
11279
|
+
"<select>": "warning",
|
|
11280
|
+
"<textarea>": "warning",
|
|
11281
|
+
"<object>": "warning",
|
|
11282
|
+
"<base>": "warning"
|
|
11283
|
+
};
|
|
11284
|
+
var HTML_ELEMENT_MESSAGES = {
|
|
11285
|
+
"<style>": (n) => `${n} strips <style> blocks. Styles must be inlined.`,
|
|
11286
|
+
"<link>": (n) => `${n} does not support external stylesheets.`,
|
|
11287
|
+
"<svg>": (n) => `${n} does not support inline SVG.`,
|
|
11288
|
+
"<video>": (n) => `${n} does not support <video> elements.`,
|
|
11289
|
+
"<form>": (n) => `${n} strips form elements.`
|
|
11290
|
+
};
|
|
11291
|
+
var COMPOUND_DETECTORS = [
|
|
11292
|
+
{ key: "display:flex", property: "display", valueIncludes: "flex" },
|
|
11293
|
+
{ key: "display:grid", property: "display", valueIncludes: "grid" },
|
|
11294
|
+
{ key: "display:none", property: "display", valueIncludes: "none" }
|
|
11295
|
+
];
|
|
11296
|
+
var CSS_FUNCTION_DETECTORS = CSS_FUNCTION_FEATURES.map((fn) => ({
|
|
11297
|
+
key: fn,
|
|
11298
|
+
pattern: `${fn}(`
|
|
11299
|
+
// require opening paren — matches "min(" but not "Minion"
|
|
11300
|
+
}));
|
|
11301
|
+
function analyzeEmailFromDom($, framework, source) {
|
|
11302
|
+
const warnings = [];
|
|
11303
|
+
const seenWarnings = /* @__PURE__ */ new Map();
|
|
11304
|
+
function addWarning(w) {
|
|
11305
|
+
const key = `${w.client}:${w.property}:${w.severity}:${w.selector || ""}`;
|
|
11306
|
+
const existing = seenWarnings.get(key);
|
|
11307
|
+
if (!existing) {
|
|
11308
|
+
seenWarnings.set(key, w);
|
|
11309
|
+
warnings.push(w);
|
|
11310
|
+
return;
|
|
11311
|
+
}
|
|
11312
|
+
if (!existing.locs || !w.locs) return;
|
|
11313
|
+
for (const loc of w.locs) {
|
|
11314
|
+
if (existing.locs.some((l) => l.offset === loc.offset)) continue;
|
|
11315
|
+
if (existing.locs.length >= MAX_WARNING_LOCATIONS) {
|
|
11316
|
+
existing.locsTruncated = true;
|
|
11317
|
+
break;
|
|
11318
|
+
}
|
|
11319
|
+
existing.locs.push(loc);
|
|
11320
|
+
}
|
|
11321
|
+
}
|
|
11322
|
+
function describeSelector2(el) {
|
|
11323
|
+
var _a;
|
|
11324
|
+
const $el = $(el);
|
|
11325
|
+
const tag = ((_a = el.tagName) == null ? void 0 : _a.toLowerCase()) || "";
|
|
11326
|
+
const cls = $el.attr("class");
|
|
11327
|
+
const id = $el.attr("id");
|
|
11328
|
+
if (id) return `${tag}#${id}`;
|
|
11329
|
+
if (cls) return `${tag}.${cls.split(/\s+/)[0]}`;
|
|
11330
|
+
const href = $el.attr("href");
|
|
11331
|
+
if (href) return `${tag}[href]`;
|
|
11332
|
+
return tag;
|
|
11333
|
+
}
|
|
11334
|
+
for (const feature of HTML_ELEMENT_FEATURES) {
|
|
11335
|
+
const selector = HTML_ELEMENT_SELECTORS[feature];
|
|
11336
|
+
if (!selector) continue;
|
|
11337
|
+
const matches = $(selector);
|
|
11338
|
+
if (matches.length === 0) continue;
|
|
11339
|
+
const supportData = CSS_SUPPORT[feature];
|
|
11340
|
+
if (!supportData) continue;
|
|
11341
|
+
const baseSeverity = HTML_ELEMENT_SEVERITY[feature] || "warning";
|
|
11342
|
+
const found = matches.toArray().map((m) => locOfElement(m)).filter((l) => l !== void 0);
|
|
11343
|
+
const featureOccurrences = found.length ? __spreadValues({
|
|
11344
|
+
locs: found.slice(0, MAX_WARNING_LOCATIONS)
|
|
11345
|
+
}, found.length > MAX_WARNING_LOCATIONS ? { truncated: true } : {}) : void 0;
|
|
11346
|
+
const featureLoc = featureOccurrences == null ? void 0 : featureOccurrences.locs[0];
|
|
11347
|
+
for (const client of EMAIL_CLIENTS) {
|
|
11348
|
+
const support = supportData[client.id];
|
|
11349
|
+
if (support === "unsupported") {
|
|
11350
|
+
const msgFn = HTML_ELEMENT_MESSAGES[feature];
|
|
11351
|
+
const message = msgFn ? msgFn(client.name) : `${client.name} does not support ${feature}.`;
|
|
11352
|
+
const sug = getSuggestion(feature, client.id, framework);
|
|
11353
|
+
const fix = getCodeFix(feature, client.id, framework);
|
|
11354
|
+
addWarning(__spreadValues(__spreadValues({
|
|
11355
|
+
severity: baseSeverity,
|
|
11356
|
+
client: client.id,
|
|
11357
|
+
property: feature,
|
|
11358
|
+
message,
|
|
11359
|
+
suggestion: sug.text,
|
|
11360
|
+
fix,
|
|
11361
|
+
fixType: getFixType(feature)
|
|
11362
|
+
}, featureOccurrences ? occurrenceFields(featureOccurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(feature, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
11363
|
+
} else if (support === "partial" && feature === "<style>") {
|
|
11364
|
+
const sug = getSuggestion("<style>:partial", client.id, framework);
|
|
11365
|
+
const fix = getCodeFix("<style>", client.id, framework);
|
|
11366
|
+
addWarning(__spreadValues(__spreadValues({
|
|
11367
|
+
severity: "warning",
|
|
11368
|
+
client: client.id,
|
|
11369
|
+
property: "<style>",
|
|
11370
|
+
message: `${client.name} has partial <style> support (head only, with limitations). Inline styles recommended.`,
|
|
11371
|
+
suggestion: sug.text,
|
|
11372
|
+
fix,
|
|
11373
|
+
fixType: getFixType("<style>")
|
|
11374
|
+
}, featureOccurrences ? occurrenceFields(featureOccurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<style>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
11375
|
+
}
|
|
11376
|
+
}
|
|
11377
|
+
}
|
|
11378
|
+
const parsedAtRules = /* @__PURE__ */ new Set();
|
|
11379
|
+
const selectorLocs = /* @__PURE__ */ new Map();
|
|
11380
|
+
const parsedProperties = /* @__PURE__ */ new Set();
|
|
11381
|
+
const propertyLines = /* @__PURE__ */ new Map();
|
|
11382
|
+
const propertyLocs = /* @__PURE__ */ new Map();
|
|
11383
|
+
const propertyValues = /* @__PURE__ */ new Map();
|
|
11384
|
+
const detectedCssFunctions = /* @__PURE__ */ new Set();
|
|
11385
|
+
const detectedPseudoClasses = /* @__PURE__ */ new Set();
|
|
11386
|
+
const detectedPseudoElements = /* @__PURE__ */ new Set();
|
|
11387
|
+
let blockAnchor;
|
|
11388
|
+
function recordSelectorLoc(key, cssLoc) {
|
|
11389
|
+
if (!cssLoc) return;
|
|
11390
|
+
const loc = locInCssBlock(blockAnchor, cssLoc);
|
|
11391
|
+
if (!loc) return;
|
|
11392
|
+
const seen = selectorLocs.get(key);
|
|
11393
|
+
if (!seen) {
|
|
11394
|
+
selectorLocs.set(key, { locs: [loc] });
|
|
11395
|
+
return;
|
|
11396
|
+
}
|
|
11397
|
+
if (seen.locs.some((l) => l.offset === loc.offset)) return;
|
|
11398
|
+
if (seen.locs.length >= MAX_WARNING_LOCATIONS) {
|
|
11399
|
+
seen.truncated = true;
|
|
11400
|
+
return;
|
|
11401
|
+
}
|
|
11402
|
+
seen.locs.push(loc);
|
|
11403
|
+
}
|
|
11404
|
+
function recordLoc(key, cssLoc, value) {
|
|
11405
|
+
const loc = locInCssBlock(blockAnchor, cssLoc);
|
|
11406
|
+
if (!loc) return;
|
|
11407
|
+
const seen = propertyLocs.get(key);
|
|
11408
|
+
if (!seen) {
|
|
11409
|
+
propertyLocs.set(key, __spreadValues({ locs: [loc] }, value !== void 0 ? { values: [value] } : {}));
|
|
11410
|
+
return;
|
|
11411
|
+
}
|
|
11412
|
+
if (seen.locs.some((l) => l.offset === loc.offset)) return;
|
|
11413
|
+
if (seen.locs.length >= MAX_WARNING_LOCATIONS) {
|
|
11414
|
+
seen.truncated = true;
|
|
11415
|
+
return;
|
|
11416
|
+
}
|
|
11417
|
+
seen.locs.push(loc);
|
|
11418
|
+
if (seen.values && value !== void 0) seen.values.push(value);
|
|
11419
|
+
}
|
|
11420
|
+
$("style").each((_, el) => {
|
|
11421
|
+
const cssText = $(el).text();
|
|
11422
|
+
blockAnchor = cssBlockAnchor(el, cssText, source);
|
|
11423
|
+
try {
|
|
11424
|
+
const ast = csstree5.parse(cssText, { parseCustomProperty: true, positions: true });
|
|
11425
|
+
csstree5.walk(ast, {
|
|
11426
|
+
enter(node) {
|
|
11427
|
+
if (node.type === "Atrule") {
|
|
11428
|
+
parsedAtRules.add(`@${node.name}`);
|
|
11429
|
+
recordSelectorLoc(`@${node.name}`, node.loc);
|
|
11430
|
+
}
|
|
11431
|
+
if (node.type === "PseudoClassSelector") {
|
|
11432
|
+
detectedPseudoClasses.add(`:${node.name}`);
|
|
11433
|
+
recordSelectorLoc(`:${node.name}`, node.loc);
|
|
11434
|
+
}
|
|
11435
|
+
if (node.type === "PseudoElementSelector") {
|
|
11436
|
+
detectedPseudoElements.add(`::${node.name}`);
|
|
11437
|
+
recordSelectorLoc(`::${node.name}`, node.loc);
|
|
11438
|
+
}
|
|
11439
|
+
if (node.type === "Declaration") {
|
|
11440
|
+
const prop = node.property.toLowerCase();
|
|
11441
|
+
parsedProperties.add(prop);
|
|
11442
|
+
const valueStr = csstree5.generate(node.value);
|
|
11443
|
+
const seenValues = propertyValues.get(prop);
|
|
11444
|
+
if (seenValues) seenValues.push(valueStr);
|
|
11445
|
+
else propertyValues.set(prop, [valueStr]);
|
|
11446
|
+
if (node.loc) {
|
|
11447
|
+
if (!propertyLines.has(prop)) propertyLines.set(prop, node.loc.start.line);
|
|
11448
|
+
recordLoc(prop, node.loc, valueStr);
|
|
11449
|
+
}
|
|
11450
|
+
for (const det of COMPOUND_DETECTORS) {
|
|
11451
|
+
if (prop === det.property && valueStr.toLowerCase().includes(det.valueIncludes)) {
|
|
11452
|
+
parsedProperties.add(det.key);
|
|
11453
|
+
if (node.loc) {
|
|
11454
|
+
if (!propertyLines.has(det.key)) propertyLines.set(det.key, node.loc.start.line);
|
|
11455
|
+
recordLoc(det.key, node.loc);
|
|
11456
|
+
}
|
|
11457
|
+
}
|
|
11458
|
+
}
|
|
11459
|
+
for (const fn of CSS_FUNCTION_DETECTORS) {
|
|
11460
|
+
if (valueStr.includes(fn.pattern)) {
|
|
11461
|
+
detectedCssFunctions.add(fn.key);
|
|
11462
|
+
if (node.loc) {
|
|
11463
|
+
if (!propertyLines.has(fn.key)) propertyLines.set(fn.key, node.loc.start.line);
|
|
11464
|
+
recordLoc(fn.key, node.loc);
|
|
11465
|
+
}
|
|
11466
|
+
}
|
|
11467
|
+
}
|
|
11468
|
+
}
|
|
11469
|
+
}
|
|
11470
|
+
});
|
|
11471
|
+
} catch (e) {
|
|
11472
|
+
}
|
|
11473
|
+
});
|
|
11474
|
+
for (const atRule of AT_RULE_FEATURES) {
|
|
11475
|
+
if (!parsedAtRules.has(atRule)) continue;
|
|
11476
|
+
checkPropertySupport(atRule, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(atRule));
|
|
11477
|
+
}
|
|
11478
|
+
const cssPropertiesToCheck = Object.keys(CSS_SUPPORT).filter(
|
|
11479
|
+
(k) => !k.startsWith("<") && !k.startsWith("@")
|
|
11480
|
+
);
|
|
11481
|
+
$("[style]").each((_, el) => {
|
|
11482
|
+
const style = $(el).attr("style") || "";
|
|
11483
|
+
const props = parseStyleProperties(style);
|
|
11484
|
+
const selector = describeSelector2(el);
|
|
11485
|
+
const locs = elementLocs(locOfAttr(el, "style"));
|
|
11486
|
+
for (const prop of props) {
|
|
11487
|
+
for (const det of COMPOUND_DETECTORS) {
|
|
11488
|
+
if (prop === det.property) {
|
|
11489
|
+
const value2 = getStyleValue(style, prop);
|
|
11490
|
+
if (value2 == null ? void 0 : value2.toLowerCase().includes(det.valueIncludes)) {
|
|
11491
|
+
checkPropertySupport(det.key, addWarning, framework, selector, void 0, void 0, locs);
|
|
11492
|
+
}
|
|
11493
|
+
}
|
|
11494
|
+
}
|
|
11495
|
+
if (cssPropertiesToCheck.includes(prop)) {
|
|
11496
|
+
const declared = getStyleValues(style, prop);
|
|
11497
|
+
checkPropertySupport(
|
|
11498
|
+
prop,
|
|
11499
|
+
addWarning,
|
|
11500
|
+
framework,
|
|
11501
|
+
selector,
|
|
11502
|
+
void 0,
|
|
11503
|
+
declared.length ? declared : void 0,
|
|
11504
|
+
locs
|
|
11505
|
+
);
|
|
11506
|
+
}
|
|
11507
|
+
const value = getStyleValue(style, prop);
|
|
11508
|
+
if (value) {
|
|
11509
|
+
for (const fn of CSS_FUNCTION_DETECTORS) {
|
|
11510
|
+
if (value.includes(fn.pattern)) {
|
|
11511
|
+
checkPropertySupport(fn.key, addWarning, framework, selector, void 0, void 0, locs);
|
|
11512
|
+
}
|
|
11513
|
+
}
|
|
10970
11514
|
}
|
|
10971
|
-
} catch (e) {
|
|
10972
11515
|
}
|
|
10973
11516
|
});
|
|
10974
|
-
|
|
10975
|
-
|
|
10976
|
-
|
|
10977
|
-
|
|
10978
|
-
|
|
11517
|
+
for (const prop of parsedProperties) {
|
|
11518
|
+
if (prop.includes(":")) continue;
|
|
11519
|
+
if (!cssPropertiesToCheck.includes(prop)) continue;
|
|
11520
|
+
const values = propertyValues.get(prop);
|
|
11521
|
+
checkPropertySupport(
|
|
11522
|
+
prop,
|
|
11523
|
+
addWarning,
|
|
11524
|
+
framework,
|
|
11525
|
+
void 0,
|
|
11526
|
+
propertyLines.get(prop),
|
|
11527
|
+
values,
|
|
11528
|
+
propertyLocs.get(prop)
|
|
11529
|
+
);
|
|
11530
|
+
}
|
|
11531
|
+
for (const compound of COMPOUND_VALUE_FEATURES) {
|
|
11532
|
+
if (compound.startsWith(":") || compound.startsWith("::")) continue;
|
|
11533
|
+
if (parsedProperties.has(compound)) {
|
|
11534
|
+
checkPropertySupport(compound, addWarning, framework, void 0, propertyLines.get(compound), void 0, propertyLocs.get(compound));
|
|
10979
11535
|
}
|
|
10980
|
-
}
|
|
11536
|
+
}
|
|
11537
|
+
for (const pseudo of detectedPseudoClasses) {
|
|
11538
|
+
if (CSS_SUPPORT[pseudo]) {
|
|
11539
|
+
checkPropertySupport(pseudo, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(pseudo));
|
|
11540
|
+
}
|
|
11541
|
+
}
|
|
11542
|
+
for (const pseudo of detectedPseudoElements) {
|
|
11543
|
+
if (CSS_SUPPORT[pseudo]) {
|
|
11544
|
+
checkPropertySupport(pseudo, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(pseudo));
|
|
11545
|
+
}
|
|
11546
|
+
}
|
|
11547
|
+
for (const fn of detectedCssFunctions) {
|
|
11548
|
+
checkPropertySupport(fn, addWarning, framework, void 0, propertyLines.get(fn), void 0, propertyLocs.get(fn));
|
|
11549
|
+
}
|
|
11550
|
+
for (const w of checkDarkModeFromDom($)) addWarning(w);
|
|
11551
|
+
const severityOrder = { error: 0, warning: 1, info: 2 };
|
|
11552
|
+
warnings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
11553
|
+
return warnings;
|
|
11554
|
+
}
|
|
11555
|
+
function analyzeEmail(html, framework, options) {
|
|
11556
|
+
if (!html || !html.trim()) {
|
|
11557
|
+
return [];
|
|
11558
|
+
}
|
|
11559
|
+
if (html.length > MAX_HTML_SIZE) {
|
|
11560
|
+
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
11561
|
+
}
|
|
11562
|
+
const $ = loadHtml(html, options);
|
|
11563
|
+
return analyzeEmailFromDom($, framework, (options == null ? void 0 : options.positions) ? html : void 0);
|
|
11564
|
+
}
|
|
11565
|
+
function getFixType(prop) {
|
|
11566
|
+
return STRUCTURAL_FIX_PROPERTIES.has(prop) ? "structural" : "css";
|
|
11567
|
+
}
|
|
11568
|
+
function noteSuffix(notes) {
|
|
11569
|
+
if (!(notes == null ? void 0 : notes.length)) return "";
|
|
11570
|
+
const cleaned = notes.map((n) => n.replace(/^(?:Partial|Buggy|Not supported)\.\s*/i, "").trim()).filter(Boolean);
|
|
11571
|
+
return cleaned.length ? ` ${cleaned.join(" ")}` : "";
|
|
11572
|
+
}
|
|
11573
|
+
function checkPropertySupport(prop, addWarning, framework, selector, line, values, occurrences) {
|
|
11574
|
+
var _a, _b, _c, _d, _e, _f;
|
|
11575
|
+
const loc = occurrences == null ? void 0 : occurrences.locs[0];
|
|
11576
|
+
const reportedLine = (_a = loc == null ? void 0 : loc.line) != null ? _a : line;
|
|
11577
|
+
const supportData = CSS_SUPPORT[prop];
|
|
11578
|
+
if (!supportData) return;
|
|
11579
|
+
const fixType = getFixType(prop);
|
|
11580
|
+
for (const client of EMAIL_CLIENTS) {
|
|
11581
|
+
const support = supportData[client.id] || "unknown";
|
|
11582
|
+
const notes = (_b = CSS_SUPPORT_NOTES[prop]) == null ? void 0 : _b[client.id];
|
|
11583
|
+
if (support === "unsupported") {
|
|
11584
|
+
const sug = getSuggestion(prop, client.id, framework);
|
|
11585
|
+
const fix = getCodeFix(prop, client.id, framework);
|
|
11586
|
+
addWarning(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
|
|
11587
|
+
severity: "warning",
|
|
11588
|
+
client: client.id,
|
|
11589
|
+
property: prop,
|
|
11590
|
+
message: `${client.name} does not support "${prop}".${noteSuffix(notes)}`,
|
|
11591
|
+
suggestion: sug.text,
|
|
11592
|
+
fix,
|
|
11593
|
+
fixType
|
|
11594
|
+
}, selector ? { selector } : {}), reportedLine !== void 0 ? { line: reportedLine } : {}), occurrences ? occurrenceFields(occurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
11595
|
+
} else if (support === "partial") {
|
|
11596
|
+
if (!caveatApplies(prop, values, notes)) continue;
|
|
11597
|
+
const hits = triggeringOccurrences(prop, occurrences, notes);
|
|
11598
|
+
const sug = getSuggestion(prop, client.id, framework);
|
|
11599
|
+
const fix = getCodeFix(prop, client.id, framework);
|
|
11600
|
+
addWarning(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
|
|
11601
|
+
severity: "info",
|
|
11602
|
+
client: client.id,
|
|
11603
|
+
property: prop,
|
|
11604
|
+
message: `${client.name} has partial support for "${prop}".${noteSuffix(notes)}`,
|
|
11605
|
+
suggestion: sug.text,
|
|
11606
|
+
fix,
|
|
11607
|
+
fixType
|
|
11608
|
+
}, 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 } : {}));
|
|
11609
|
+
}
|
|
11610
|
+
}
|
|
11611
|
+
}
|
|
11612
|
+
function generateCompatibilityScore(warnings) {
|
|
11613
|
+
const result = {};
|
|
11614
|
+
for (const client of EMAIL_CLIENTS) {
|
|
11615
|
+
const clientWarnings = warnings.filter((w) => w.client === client.id);
|
|
11616
|
+
const errorProps = new Set(clientWarnings.filter((w) => w.severity === "error").map((w) => w.property));
|
|
11617
|
+
const warnProps = new Set(clientWarnings.filter((w) => w.severity === "warning").map((w) => w.property));
|
|
11618
|
+
const infoProps = new Set(clientWarnings.filter((w) => w.severity === "info").map((w) => w.property));
|
|
11619
|
+
const errors = errorProps.size;
|
|
11620
|
+
const warns = warnProps.size;
|
|
11621
|
+
const info = infoProps.size;
|
|
11622
|
+
const score = Math.max(0, Math.min(100, 100 - errors * 10 - warns * 3));
|
|
11623
|
+
result[client.id] = { score, errors, warnings: warns, info };
|
|
11624
|
+
}
|
|
11625
|
+
return result;
|
|
11626
|
+
}
|
|
11627
|
+
function occurrenceFields({ locs, truncated }) {
|
|
11628
|
+
return __spreadValues({ loc: locs[0], locs: [...locs] }, truncated ? { locsTruncated: true } : {});
|
|
11629
|
+
}
|
|
11630
|
+
function triggeringOccurrences(prop, occurrences, notes) {
|
|
11631
|
+
const values = occurrences == null ? void 0 : occurrences.values;
|
|
11632
|
+
if (!occurrences || !values) return occurrences;
|
|
11633
|
+
const locs = occurrences.locs.filter((_, i) => caveatApplies(prop, [values[i]], notes));
|
|
11634
|
+
if (!locs.length || locs.length === occurrences.locs.length) return occurrences;
|
|
11635
|
+
return __spreadValues({ locs }, occurrences.truncated ? { truncated: true } : {});
|
|
11636
|
+
}
|
|
11637
|
+
function elementLocs(loc) {
|
|
11638
|
+
return loc ? { locs: [loc] } : void 0;
|
|
11639
|
+
}
|
|
11640
|
+
function warningsForClient(warnings, clientId) {
|
|
11641
|
+
return warnings.filter((w) => w.client === clientId);
|
|
11642
|
+
}
|
|
11643
|
+
function errorWarnings(warnings) {
|
|
11644
|
+
return warnings.filter((w) => w.severity === "error");
|
|
11645
|
+
}
|
|
11646
|
+
function structuralWarnings(warnings) {
|
|
11647
|
+
return warnings.filter((w) => w.fixType === "structural");
|
|
10981
11648
|
}
|
|
10982
11649
|
|
|
10983
11650
|
// src/diff.ts
|
|
@@ -11310,16 +11977,6 @@ function extractCode(response) {
|
|
|
11310
11977
|
return response.trim();
|
|
11311
11978
|
}
|
|
11312
11979
|
|
|
11313
|
-
// src/parse-html.ts
|
|
11314
|
-
import * as cheerio5 from "cheerio";
|
|
11315
|
-
function fromHtml(html, empty, fn) {
|
|
11316
|
-
if (!html || !html.trim()) return empty;
|
|
11317
|
-
if (html.length > MAX_HTML_SIZE) {
|
|
11318
|
-
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
11319
|
-
}
|
|
11320
|
-
return fn(cheerio5.load(html), html);
|
|
11321
|
-
}
|
|
11322
|
-
|
|
11323
11980
|
// src/spam-scorer.ts
|
|
11324
11981
|
var SPAM_TRIGGER_PHRASES = [
|
|
11325
11982
|
"act now",
|
|
@@ -11788,6 +12445,8 @@ function validateLinksFromDom($) {
|
|
|
11788
12445
|
const href = $(el).attr("href") || "";
|
|
11789
12446
|
const text = $(el).text().trim();
|
|
11790
12447
|
const category = classifyHref(href);
|
|
12448
|
+
const elLoc = locOfElement(el);
|
|
12449
|
+
const hrefLoc = href ? locOfAttr(el, "href") : elLoc;
|
|
11791
12450
|
switch (category) {
|
|
11792
12451
|
case "https":
|
|
11793
12452
|
breakdown.https++;
|
|
@@ -11818,95 +12477,95 @@ function validateLinksFromDom($) {
|
|
|
11818
12477
|
hrefCounts.set(href, (hrefCounts.get(href) || 0) + 1);
|
|
11819
12478
|
}
|
|
11820
12479
|
if (!href || !href.trim()) {
|
|
11821
|
-
issues.push({
|
|
12480
|
+
issues.push(__spreadValues({
|
|
11822
12481
|
severity: "error",
|
|
11823
12482
|
rule: "empty-href",
|
|
11824
12483
|
message: "Link has no href attribute",
|
|
11825
12484
|
text: text.slice(0, 80) || "(no text)"
|
|
11826
|
-
});
|
|
12485
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
11827
12486
|
return;
|
|
11828
12487
|
}
|
|
11829
12488
|
if (category === "javascript" && !isPlaceholderHref(href)) {
|
|
11830
|
-
issues.push({
|
|
12489
|
+
issues.push(__spreadValues({
|
|
11831
12490
|
severity: "error",
|
|
11832
12491
|
rule: "javascript-href",
|
|
11833
12492
|
message: "Link uses javascript: protocol",
|
|
11834
12493
|
href: href.slice(0, 100),
|
|
11835
12494
|
text: text.slice(0, 80) || "(no text)"
|
|
11836
|
-
});
|
|
12495
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
11837
12496
|
return;
|
|
11838
12497
|
}
|
|
11839
12498
|
if (isPlaceholderHref(href)) {
|
|
11840
|
-
issues.push({
|
|
12499
|
+
issues.push(__spreadValues({
|
|
11841
12500
|
severity: "warning",
|
|
11842
12501
|
rule: "placeholder-href",
|
|
11843
12502
|
message: "Link has a placeholder href (# or javascript:void)",
|
|
11844
12503
|
href,
|
|
11845
12504
|
text: text.slice(0, 80) || "(no text)"
|
|
11846
|
-
});
|
|
12505
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
11847
12506
|
return;
|
|
11848
12507
|
}
|
|
11849
12508
|
if (category === "http") {
|
|
11850
|
-
issues.push({
|
|
12509
|
+
issues.push(__spreadValues({
|
|
11851
12510
|
severity: "warning",
|
|
11852
12511
|
rule: "insecure-link",
|
|
11853
12512
|
message: "Link uses HTTP instead of HTTPS",
|
|
11854
12513
|
href: href.slice(0, 120),
|
|
11855
12514
|
text: text.slice(0, 80) || "(no text)"
|
|
11856
|
-
});
|
|
12515
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
11857
12516
|
}
|
|
11858
12517
|
if (category === "protocol-relative") {
|
|
11859
|
-
issues.push({
|
|
12518
|
+
issues.push(__spreadValues({
|
|
11860
12519
|
severity: "warning",
|
|
11861
12520
|
rule: "protocol-relative",
|
|
11862
12521
|
message: "Protocol-relative URL may break in email clients \u2014 use https:// explicitly",
|
|
11863
12522
|
href: href.slice(0, 120),
|
|
11864
12523
|
text: text.slice(0, 80) || "(no text)"
|
|
11865
|
-
});
|
|
12524
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
11866
12525
|
}
|
|
11867
12526
|
if (text && GENERIC_LINK_TEXT.has(text.toLowerCase())) {
|
|
11868
|
-
issues.push({
|
|
12527
|
+
issues.push(__spreadValues({
|
|
11869
12528
|
severity: "warning",
|
|
11870
12529
|
rule: "generic-link-text",
|
|
11871
12530
|
message: `Link text "${text}" is vague \u2014 use descriptive text for accessibility and engagement`,
|
|
11872
12531
|
href: href.slice(0, 120),
|
|
11873
12532
|
text
|
|
11874
|
-
});
|
|
12533
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
11875
12534
|
}
|
|
11876
12535
|
if (!text && !$(el).attr("aria-label") && !$(el).find("img[alt]").length) {
|
|
11877
|
-
issues.push({
|
|
12536
|
+
issues.push(__spreadValues({
|
|
11878
12537
|
severity: "error",
|
|
11879
12538
|
rule: "empty-link-text",
|
|
11880
12539
|
message: "Link has no visible text or aria-label",
|
|
11881
12540
|
href: href.slice(0, 120)
|
|
11882
|
-
});
|
|
12541
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
11883
12542
|
}
|
|
11884
12543
|
if (category === "mailto" && href.trim().toLowerCase() === "mailto:") {
|
|
11885
|
-
issues.push({
|
|
12544
|
+
issues.push(__spreadValues({
|
|
11886
12545
|
severity: "error",
|
|
11887
12546
|
rule: "empty-mailto",
|
|
11888
12547
|
message: "mailto: link has no email address",
|
|
11889
12548
|
href,
|
|
11890
12549
|
text: text.slice(0, 80) || "(no text)"
|
|
11891
|
-
});
|
|
12550
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
11892
12551
|
}
|
|
11893
12552
|
if (category === "tel" && href.trim().toLowerCase() === "tel:") {
|
|
11894
|
-
issues.push({
|
|
12553
|
+
issues.push(__spreadValues({
|
|
11895
12554
|
severity: "error",
|
|
11896
12555
|
rule: "empty-tel",
|
|
11897
12556
|
message: "tel: link has no phone number",
|
|
11898
12557
|
href,
|
|
11899
12558
|
text: text.slice(0, 80) || "(no text)"
|
|
11900
|
-
});
|
|
12559
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
11901
12560
|
}
|
|
11902
12561
|
if (href.length > 2e3) {
|
|
11903
|
-
issues.push({
|
|
12562
|
+
issues.push(__spreadValues({
|
|
11904
12563
|
severity: "info",
|
|
11905
12564
|
rule: "long-url",
|
|
11906
12565
|
message: "URL exceeds 2000 characters \u2014 may be truncated by some email clients",
|
|
11907
12566
|
href: href.slice(0, 120) + "...",
|
|
11908
12567
|
text: text.slice(0, 80) || "(no text)"
|
|
11909
|
-
});
|
|
12568
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
11910
12569
|
}
|
|
11911
12570
|
});
|
|
11912
12571
|
links.each((_, el) => {
|
|
@@ -11915,14 +12574,15 @@ function validateLinksFromDom($) {
|
|
|
11915
12574
|
if (trimmed.startsWith("#") && trimmed.length > 1) {
|
|
11916
12575
|
const targetId = trimmed.slice(1);
|
|
11917
12576
|
const target = $(`[id="${targetId}"]`);
|
|
12577
|
+
const anchorLoc = locOfAttr(el, "href");
|
|
11918
12578
|
if (target.length === 0) {
|
|
11919
|
-
issues.push({
|
|
12579
|
+
issues.push(__spreadValues({
|
|
11920
12580
|
severity: "error",
|
|
11921
12581
|
rule: "broken-anchor",
|
|
11922
12582
|
message: `Anchor link "${trimmed}" points to an element that does not exist`,
|
|
11923
12583
|
href: trimmed,
|
|
11924
12584
|
text: $(el).text().trim().slice(0, 80) || "(no text)"
|
|
11925
|
-
});
|
|
12585
|
+
}, anchorLoc ? { loc: anchorLoc } : {}));
|
|
11926
12586
|
}
|
|
11927
12587
|
}
|
|
11928
12588
|
});
|
|
@@ -11938,8 +12598,8 @@ function validateLinksFromDom($) {
|
|
|
11938
12598
|
}
|
|
11939
12599
|
return { totalLinks, issues, breakdown };
|
|
11940
12600
|
}
|
|
11941
|
-
function validateLinks(html) {
|
|
11942
|
-
return fromHtml(html, EMPTY_LINKS, validateLinksFromDom);
|
|
12601
|
+
function validateLinks(html, options) {
|
|
12602
|
+
return fromHtml(html, EMPTY_LINKS, validateLinksFromDom, options);
|
|
11943
12603
|
}
|
|
11944
12604
|
|
|
11945
12605
|
// src/accessibility-checker.ts
|
|
@@ -11964,24 +12624,31 @@ function describeElement($, el) {
|
|
|
11964
12624
|
function checkLangAttribute($) {
|
|
11965
12625
|
const lang = $("html").attr("lang");
|
|
11966
12626
|
if (!lang || !lang.trim()) {
|
|
11967
|
-
|
|
12627
|
+
const loc = locOfFirst($, "html");
|
|
12628
|
+
return __spreadProps(__spreadValues({
|
|
11968
12629
|
severity: "error",
|
|
11969
12630
|
rule: "missing-lang",
|
|
11970
|
-
message: "Missing lang attribute on <html> element"
|
|
12631
|
+
message: "Missing lang attribute on <html> element"
|
|
12632
|
+
}, loc ? { loc } : {}), {
|
|
11971
12633
|
details: 'Screen readers use the lang attribute to determine pronunciation. Add lang="en" (or appropriate language code).'
|
|
11972
|
-
};
|
|
12634
|
+
});
|
|
11973
12635
|
}
|
|
11974
12636
|
return null;
|
|
11975
12637
|
}
|
|
12638
|
+
function titleLoc($) {
|
|
12639
|
+
return $("title").length ? locOfFirst($, "title") : locOfFirst($, "head");
|
|
12640
|
+
}
|
|
11976
12641
|
function checkTitle($) {
|
|
11977
12642
|
const title = $("title").text().trim();
|
|
11978
12643
|
if (!title) {
|
|
11979
|
-
|
|
12644
|
+
const loc = titleLoc($);
|
|
12645
|
+
return __spreadProps(__spreadValues({
|
|
11980
12646
|
severity: "warning",
|
|
11981
12647
|
rule: "missing-title",
|
|
11982
|
-
message: "Missing or empty <title> element"
|
|
12648
|
+
message: "Missing or empty <title> element"
|
|
12649
|
+
}, loc ? { loc } : {}), {
|
|
11983
12650
|
details: "The <title> helps screen readers identify the email content."
|
|
11984
|
-
};
|
|
12651
|
+
});
|
|
11985
12652
|
}
|
|
11986
12653
|
return null;
|
|
11987
12654
|
}
|
|
@@ -11991,34 +12658,38 @@ function checkImageAlt($) {
|
|
|
11991
12658
|
const alt = $(el).attr("alt");
|
|
11992
12659
|
const src = $(el).attr("src") || "";
|
|
11993
12660
|
const role = $(el).attr("role");
|
|
12661
|
+
const elLoc = locOfElement(el);
|
|
11994
12662
|
if (role === "presentation" || role === "none") return;
|
|
11995
12663
|
if (alt === void 0) {
|
|
11996
|
-
issues.push({
|
|
12664
|
+
issues.push(__spreadProps(__spreadValues({
|
|
11997
12665
|
severity: "error",
|
|
11998
12666
|
rule: "img-missing-alt",
|
|
11999
12667
|
message: "Image missing alt attribute",
|
|
12000
|
-
element: describeElement($, el)
|
|
12668
|
+
element: describeElement($, el)
|
|
12669
|
+
}, elLoc ? { loc: elLoc } : {}), {
|
|
12001
12670
|
details: 'Every image must have an alt attribute. Use alt="" for decorative images.'
|
|
12002
|
-
});
|
|
12671
|
+
}));
|
|
12003
12672
|
} else if (alt.trim() === "") {
|
|
12004
12673
|
const isLikelyContent = !src.includes("spacer") && !src.includes("pixel") && !src.includes("tracking") && !src.includes("1x1") && !src.includes("transparent");
|
|
12005
12674
|
if (isLikelyContent && ($(el).attr("width") || "0") !== "1") {
|
|
12006
|
-
issues.push({
|
|
12675
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12007
12676
|
severity: "info",
|
|
12008
12677
|
rule: "img-empty-alt",
|
|
12009
12678
|
message: "Image has empty alt text \u2014 verify it is decorative",
|
|
12010
|
-
element: describeElement($, el)
|
|
12679
|
+
element: describeElement($, el)
|
|
12680
|
+
}, locOfAttr(el, "alt") ? { loc: locOfAttr(el, "alt") } : {}), {
|
|
12011
12681
|
details: "Empty alt is correct for decorative images, but content images need descriptive alt text."
|
|
12012
|
-
});
|
|
12682
|
+
}));
|
|
12013
12683
|
}
|
|
12014
12684
|
} else if (/\.(png|jpg|jpeg|gif|svg|webp|bmp)$/i.test(alt)) {
|
|
12015
|
-
issues.push({
|
|
12685
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12016
12686
|
severity: "error",
|
|
12017
12687
|
rule: "img-filename-alt",
|
|
12018
12688
|
message: "Image alt text is a filename, not a description",
|
|
12019
|
-
element: describeElement($, el)
|
|
12689
|
+
element: describeElement($, el)
|
|
12690
|
+
}, locOfAttr(el, "alt") ? { loc: locOfAttr(el, "alt") } : {}), {
|
|
12020
12691
|
details: `Alt "${alt}" should describe the image content, not the file name.`
|
|
12021
|
-
});
|
|
12692
|
+
}));
|
|
12022
12693
|
}
|
|
12023
12694
|
});
|
|
12024
12695
|
return issues;
|
|
@@ -12026,28 +12697,31 @@ function checkImageAlt($) {
|
|
|
12026
12697
|
function checkLinkAccessibility($) {
|
|
12027
12698
|
const issues = [];
|
|
12028
12699
|
$("a").each((_, el) => {
|
|
12700
|
+
const elLoc = locOfElement(el);
|
|
12029
12701
|
const text = $(el).text().trim().toLowerCase();
|
|
12030
12702
|
const ariaLabel = $(el).attr("aria-label");
|
|
12031
12703
|
const title = $(el).attr("title");
|
|
12032
12704
|
const imgAlt = $(el).find("img").attr("alt");
|
|
12033
12705
|
if (!text && !ariaLabel && !title && !imgAlt) {
|
|
12034
|
-
issues.push({
|
|
12706
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12035
12707
|
severity: "error",
|
|
12036
12708
|
rule: "link-no-accessible-name",
|
|
12037
12709
|
message: "Link has no accessible name",
|
|
12038
|
-
element: describeElement($, el)
|
|
12710
|
+
element: describeElement($, el)
|
|
12711
|
+
}, elLoc ? { loc: elLoc } : {}), {
|
|
12039
12712
|
details: "Links need visible text, aria-label, or an image with alt text."
|
|
12040
|
-
});
|
|
12713
|
+
}));
|
|
12041
12714
|
return;
|
|
12042
12715
|
}
|
|
12043
12716
|
if (text && GENERIC_LINK_TEXT.has(text) && !ariaLabel) {
|
|
12044
|
-
issues.push({
|
|
12717
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12045
12718
|
severity: "warning",
|
|
12046
12719
|
rule: "link-generic-text",
|
|
12047
12720
|
message: `Link text "${$(el).text().trim()}" is not descriptive`,
|
|
12048
|
-
element: describeElement($, el)
|
|
12721
|
+
element: describeElement($, el)
|
|
12722
|
+
}, elLoc ? { loc: elLoc } : {}), {
|
|
12049
12723
|
details: "Screen readers often list links out of context. Use text that describes the destination."
|
|
12050
|
-
});
|
|
12724
|
+
}));
|
|
12051
12725
|
}
|
|
12052
12726
|
});
|
|
12053
12727
|
return issues;
|
|
@@ -12057,18 +12731,20 @@ function checkTableAccessibility($) {
|
|
|
12057
12731
|
$("table").each((_, el) => {
|
|
12058
12732
|
if ($(el).parents('table[role="presentation"], table[role="none"]').length > 0) return;
|
|
12059
12733
|
const role = $(el).attr("role");
|
|
12734
|
+
const tableLoc = locOfElement(el);
|
|
12060
12735
|
const hasHeaders = $(el).find("th").length > 0;
|
|
12061
12736
|
const looksLikeLayout = !hasHeaders;
|
|
12062
12737
|
if (looksLikeLayout && role !== "presentation" && role !== "none") {
|
|
12063
12738
|
const nestedTables = $(el).find("table").length;
|
|
12064
12739
|
if (nestedTables > 0 || $(el).find("td").length > 2) {
|
|
12065
|
-
issues.push({
|
|
12740
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12066
12741
|
severity: "info",
|
|
12067
12742
|
rule: "table-missing-role",
|
|
12068
|
-
message: 'Layout table missing role="presentation"'
|
|
12743
|
+
message: 'Layout table missing role="presentation"'
|
|
12744
|
+
}, tableLoc ? { loc: tableLoc } : {}), {
|
|
12069
12745
|
element: `<table> with ${$(el).find("td").length} cells`,
|
|
12070
12746
|
details: `Add role="presentation" to tables used for layout so screen readers don't announce them as data tables.`
|
|
12071
|
-
});
|
|
12747
|
+
}));
|
|
12072
12748
|
}
|
|
12073
12749
|
}
|
|
12074
12750
|
});
|
|
@@ -12079,6 +12755,7 @@ function checkTextSizeAndContrast($) {
|
|
|
12079
12755
|
let smallTextCount = 0;
|
|
12080
12756
|
$("[style]").each((_, el) => {
|
|
12081
12757
|
const style = $(el).attr("style") || "";
|
|
12758
|
+
const styleLoc = locOfAttr(el, "style");
|
|
12082
12759
|
const fontSizeMatch = style.match(/font-size\s*:\s*(\d+(?:\.\d+)?)(px|pt)/i);
|
|
12083
12760
|
if (fontSizeMatch) {
|
|
12084
12761
|
const size = parseFloat(fontSizeMatch[1]);
|
|
@@ -12087,13 +12764,14 @@ function checkTextSizeAndContrast($) {
|
|
|
12087
12764
|
if (pxSize < 9 && pxSize > 0) {
|
|
12088
12765
|
smallTextCount++;
|
|
12089
12766
|
if (smallTextCount <= 3) {
|
|
12090
|
-
issues.push({
|
|
12767
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12091
12768
|
severity: "warning",
|
|
12092
12769
|
rule: "small-text",
|
|
12093
12770
|
message: `Very small text (${fontSizeMatch[0].trim()})`,
|
|
12094
|
-
element: describeElement($, el)
|
|
12771
|
+
element: describeElement($, el)
|
|
12772
|
+
}, styleLoc ? { loc: styleLoc } : {}), {
|
|
12095
12773
|
details: "Text smaller than 9px is difficult to read, especially on mobile devices."
|
|
12096
|
-
});
|
|
12774
|
+
}));
|
|
12097
12775
|
}
|
|
12098
12776
|
}
|
|
12099
12777
|
}
|
|
@@ -12138,21 +12816,23 @@ function checkTextSizeAndContrast($) {
|
|
|
12138
12816
|
}
|
|
12139
12817
|
const grade = wcagGrade(ratio);
|
|
12140
12818
|
if (grade === "Fail") {
|
|
12141
|
-
issues.push({
|
|
12819
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12142
12820
|
severity: "error",
|
|
12143
12821
|
rule: "low-contrast",
|
|
12144
12822
|
message: `Low contrast ratio ${ratio.toFixed(1)}:1 \u2014 fails WCAG minimum`,
|
|
12145
|
-
element: describeElement($, el)
|
|
12823
|
+
element: describeElement($, el)
|
|
12824
|
+
}, styleLoc ? { loc: styleLoc } : {}), {
|
|
12146
12825
|
details: `Foreground ${colorValue} on background needs at least ${isLargeText ? "3:1" : "4.5:1"} contrast ratio.`
|
|
12147
|
-
});
|
|
12826
|
+
}));
|
|
12148
12827
|
} else if (!isLargeText && grade === "AA Large") {
|
|
12149
|
-
issues.push({
|
|
12828
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12150
12829
|
severity: "warning",
|
|
12151
12830
|
rule: "low-contrast",
|
|
12152
12831
|
message: `Low contrast ratio ${ratio.toFixed(1)}:1 \u2014 fails WCAG AA for normal text`,
|
|
12153
|
-
element: describeElement($, el)
|
|
12832
|
+
element: describeElement($, el)
|
|
12833
|
+
}, styleLoc ? { loc: styleLoc } : {}), {
|
|
12154
12834
|
details: `Foreground ${colorValue} on background needs at least 4.5:1 for normal-sized text.`
|
|
12155
|
-
});
|
|
12835
|
+
}));
|
|
12156
12836
|
}
|
|
12157
12837
|
}
|
|
12158
12838
|
}
|
|
@@ -12175,29 +12855,32 @@ function checkCharsetDeclaration($) {
|
|
|
12175
12855
|
const content = httpEquiv.attr("content") || "";
|
|
12176
12856
|
if (/charset\s*=/i.test(content)) return null;
|
|
12177
12857
|
}
|
|
12178
|
-
|
|
12858
|
+
const loc = locOfFirst($, "head");
|
|
12859
|
+
return __spreadProps(__spreadValues({
|
|
12179
12860
|
severity: "warning",
|
|
12180
12861
|
rule: "missing-charset",
|
|
12181
|
-
message: "Missing charset declaration"
|
|
12862
|
+
message: "Missing charset declaration"
|
|
12863
|
+
}, loc ? { loc } : {}), {
|
|
12182
12864
|
details: 'Add <meta charset="utf-8"> in <head> to prevent encoding issues across email clients.'
|
|
12183
|
-
};
|
|
12865
|
+
});
|
|
12184
12866
|
}
|
|
12185
12867
|
function checkSemanticStructure($) {
|
|
12186
12868
|
const issues = [];
|
|
12187
12869
|
const headings = [];
|
|
12188
12870
|
$("h1, h2, h3, h4, h5, h6").each((_, el) => {
|
|
12189
12871
|
const level = parseInt(el.tagName.replace(/h/i, ""), 10);
|
|
12190
|
-
headings.push({ level, text: $(el).text().trim().slice(0, 60) });
|
|
12872
|
+
headings.push({ level, text: $(el).text().trim().slice(0, 60), loc: locOfElement(el) });
|
|
12191
12873
|
});
|
|
12192
12874
|
for (let i = 1; i < headings.length; i++) {
|
|
12193
12875
|
const gap = headings[i].level - headings[i - 1].level;
|
|
12194
12876
|
if (gap > 1) {
|
|
12195
|
-
issues.push({
|
|
12877
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12196
12878
|
severity: "info",
|
|
12197
12879
|
rule: "heading-skip",
|
|
12198
|
-
message: `Heading level skipped: h${headings[i - 1].level} to h${headings[i].level}
|
|
12880
|
+
message: `Heading level skipped: h${headings[i - 1].level} to h${headings[i].level}`
|
|
12881
|
+
}, headings[i].loc ? { loc: headings[i].loc } : {}), {
|
|
12199
12882
|
details: "Skipped heading levels can confuse screen readers. Use sequential heading levels."
|
|
12200
|
-
});
|
|
12883
|
+
}));
|
|
12201
12884
|
break;
|
|
12202
12885
|
}
|
|
12203
12886
|
}
|
|
@@ -12238,8 +12921,8 @@ function checkAccessibilityFromDom($) {
|
|
|
12238
12921
|
const score = Math.max(0, 100 - penalty);
|
|
12239
12922
|
return { score, issues };
|
|
12240
12923
|
}
|
|
12241
|
-
function checkAccessibility(html) {
|
|
12242
|
-
return fromHtml(html, EMPTY_ACCESSIBILITY, checkAccessibilityFromDom);
|
|
12924
|
+
function checkAccessibility(html, options) {
|
|
12925
|
+
return fromHtml(html, EMPTY_ACCESSIBILITY, checkAccessibilityFromDom, options);
|
|
12243
12926
|
}
|
|
12244
12927
|
|
|
12245
12928
|
// src/image-analyzer.ts
|
|
@@ -12286,6 +12969,8 @@ function analyzeImagesFromDom($) {
|
|
|
12286
12969
|
const height = (_c = img.attr("height")) != null ? _c : null;
|
|
12287
12970
|
const style = (img.attr("style") || "").toLowerCase();
|
|
12288
12971
|
const imgIssues = [];
|
|
12972
|
+
const elLoc = locOfElement(el);
|
|
12973
|
+
const srcLoc = src ? locOfAttr(el, "src") : elLoc;
|
|
12289
12974
|
const tracking = isTrackingPixel(img);
|
|
12290
12975
|
let dataUriBytes = 0;
|
|
12291
12976
|
if (src.startsWith("data:")) {
|
|
@@ -12309,59 +12994,59 @@ function analyzeImagesFromDom($) {
|
|
|
12309
12994
|
const hasStyleHeight = /height\s*:/.test(style);
|
|
12310
12995
|
if (!hasStyleWidth && !hasStyleHeight) {
|
|
12311
12996
|
imgIssues.push("missing-dimensions");
|
|
12312
|
-
issues.push({
|
|
12997
|
+
issues.push(__spreadValues({
|
|
12313
12998
|
rule: "missing-dimensions",
|
|
12314
12999
|
severity: "warning",
|
|
12315
13000
|
message: "Image missing width/height attributes \u2014 causes layout shifts and Outlook rendering issues.",
|
|
12316
13001
|
src: truncateSrc(src)
|
|
12317
|
-
});
|
|
13002
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12318
13003
|
}
|
|
12319
13004
|
}
|
|
12320
13005
|
if (dataUriBytes > DATA_URI_WARN_BYTES) {
|
|
12321
13006
|
const kb = Math.round(dataUriBytes / 1024);
|
|
12322
13007
|
imgIssues.push("large-data-uri");
|
|
12323
|
-
issues.push({
|
|
13008
|
+
issues.push(__spreadValues({
|
|
12324
13009
|
rule: "large-data-uri",
|
|
12325
13010
|
severity: "warning",
|
|
12326
13011
|
message: `Data URI is ${kb}KB \u2014 consider hosting the image externally to reduce email size.`,
|
|
12327
13012
|
src: truncateSrc(src)
|
|
12328
|
-
});
|
|
13013
|
+
}, srcLoc ? { loc: srcLoc } : {}));
|
|
12329
13014
|
}
|
|
12330
13015
|
if (alt === null) {
|
|
12331
13016
|
imgIssues.push("missing-alt");
|
|
12332
|
-
issues.push({
|
|
13017
|
+
issues.push(__spreadValues({
|
|
12333
13018
|
rule: "missing-alt",
|
|
12334
13019
|
severity: "warning",
|
|
12335
13020
|
message: "Image missing alt attribute \u2014 hurts deliverability and accessibility.",
|
|
12336
13021
|
src: truncateSrc(src)
|
|
12337
|
-
});
|
|
13022
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12338
13023
|
}
|
|
12339
13024
|
if (src.toLowerCase().endsWith(".webp") || src.includes("image/webp")) {
|
|
12340
13025
|
imgIssues.push("webp-format");
|
|
12341
|
-
issues.push({
|
|
13026
|
+
issues.push(__spreadValues({
|
|
12342
13027
|
rule: "webp-format",
|
|
12343
13028
|
severity: "info",
|
|
12344
13029
|
message: "WebP format detected \u2014 not supported by all email clients. Consider PNG or JPEG.",
|
|
12345
13030
|
src: truncateSrc(src)
|
|
12346
|
-
});
|
|
13031
|
+
}, srcLoc ? { loc: srcLoc } : {}));
|
|
12347
13032
|
}
|
|
12348
13033
|
if (src.toLowerCase().endsWith(".svg") || src.includes("image/svg")) {
|
|
12349
13034
|
imgIssues.push("svg-format");
|
|
12350
|
-
issues.push({
|
|
13035
|
+
issues.push(__spreadValues({
|
|
12351
13036
|
rule: "svg-format",
|
|
12352
13037
|
severity: "info",
|
|
12353
13038
|
message: "SVG format detected \u2014 not supported by most email clients. Use PNG instead.",
|
|
12354
13039
|
src: truncateSrc(src)
|
|
12355
|
-
});
|
|
13040
|
+
}, srcLoc ? { loc: srcLoc } : {}));
|
|
12356
13041
|
}
|
|
12357
13042
|
if (!style.includes("display:block") && !style.includes("display: block")) {
|
|
12358
13043
|
imgIssues.push("missing-display-block");
|
|
12359
|
-
issues.push({
|
|
13044
|
+
issues.push(__spreadValues({
|
|
12360
13045
|
rule: "missing-display-block",
|
|
12361
13046
|
severity: "info",
|
|
12362
13047
|
message: "Image without display:block \u2014 may cause unwanted gaps in Outlook.",
|
|
12363
13048
|
src: truncateSrc(src)
|
|
12364
|
-
});
|
|
13049
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12365
13050
|
}
|
|
12366
13051
|
images.push({
|
|
12367
13052
|
src: truncateSrc(src),
|
|
@@ -12399,8 +13084,8 @@ function analyzeImagesFromDom($) {
|
|
|
12399
13084
|
}
|
|
12400
13085
|
return { total: images.length, totalDataUriBytes, issues, images };
|
|
12401
13086
|
}
|
|
12402
|
-
function analyzeImages(html) {
|
|
12403
|
-
return fromHtml(html, EMPTY_IMAGES, analyzeImagesFromDom);
|
|
13087
|
+
function analyzeImages(html, options) {
|
|
13088
|
+
return fromHtml(html, EMPTY_IMAGES, analyzeImagesFromDom, options);
|
|
12404
13089
|
}
|
|
12405
13090
|
|
|
12406
13091
|
// src/inbox-preview.ts
|
|
@@ -12618,11 +13303,57 @@ function checkSize(html) {
|
|
|
12618
13303
|
return fromHtml(html, EMPTY_SIZE, checkSizeFromDom);
|
|
12619
13304
|
}
|
|
12620
13305
|
|
|
13306
|
+
// src/dom-text.ts
|
|
13307
|
+
function visibleTextNodes($) {
|
|
13308
|
+
var _a, _b, _c, _d;
|
|
13309
|
+
const nodes = [];
|
|
13310
|
+
const stack = [...(_b = (_a = $.root()[0]) == null ? void 0 : _a.children) != null ? _b : []].reverse();
|
|
13311
|
+
while (stack.length > 0) {
|
|
13312
|
+
const node = stack.pop();
|
|
13313
|
+
const tag = (_c = node.tagName) == null ? void 0 : _c.toLowerCase();
|
|
13314
|
+
if (tag === "style" || tag === "script" || tag === "head") continue;
|
|
13315
|
+
if (node.type === "text") {
|
|
13316
|
+
nodes.push(node);
|
|
13317
|
+
continue;
|
|
13318
|
+
}
|
|
13319
|
+
const children = (_d = node.children) != null ? _d : [];
|
|
13320
|
+
for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]);
|
|
13321
|
+
}
|
|
13322
|
+
return nodes;
|
|
13323
|
+
}
|
|
13324
|
+
|
|
12621
13325
|
// src/template-checker.ts
|
|
12622
|
-
function checkTemplateVariablesFromDom(
|
|
13326
|
+
function checkTemplateVariablesFromDom($, source) {
|
|
13327
|
+
var _a;
|
|
12623
13328
|
const issues = [];
|
|
12624
13329
|
const seen = /* @__PURE__ */ new Set();
|
|
12625
|
-
const
|
|
13330
|
+
const textNodes = visibleTextNodes($);
|
|
13331
|
+
const positioned = textNodes.some((n) => n.sourceCodeLocation);
|
|
13332
|
+
for (const node of positioned ? textNodes : []) {
|
|
13333
|
+
const data = (_a = node.data) != null ? _a : "";
|
|
13334
|
+
for (const [pattern, label] of TEMPLATE_VARIABLE_PATTERNS) {
|
|
13335
|
+
pattern.lastIndex = 0;
|
|
13336
|
+
let match;
|
|
13337
|
+
while ((match = pattern.exec(data)) !== null) {
|
|
13338
|
+
const variable = match[0];
|
|
13339
|
+
const key = `text:${variable}`;
|
|
13340
|
+
if (seen.has(key)) continue;
|
|
13341
|
+
seen.add(key);
|
|
13342
|
+
const loc = locInTextNode(node, match.index, variable.length, source);
|
|
13343
|
+
issues.push(__spreadValues({
|
|
13344
|
+
rule: "unresolved-variable",
|
|
13345
|
+
severity: "error",
|
|
13346
|
+
message: `Unresolved ${label} variable "${variable}" found in text content.`,
|
|
13347
|
+
variable,
|
|
13348
|
+
location: "text"
|
|
13349
|
+
}, loc ? { loc } : {}));
|
|
13350
|
+
}
|
|
13351
|
+
}
|
|
13352
|
+
}
|
|
13353
|
+
const textContent = textNodes.map((n) => {
|
|
13354
|
+
var _a2;
|
|
13355
|
+
return (_a2 = n.data) != null ? _a2 : "";
|
|
13356
|
+
}).join("");
|
|
12626
13357
|
for (const [pattern, label] of TEMPLATE_VARIABLE_PATTERNS) {
|
|
12627
13358
|
pattern.lastIndex = 0;
|
|
12628
13359
|
let match;
|
|
@@ -12655,13 +13386,14 @@ function checkTemplateVariablesFromDom($) {
|
|
|
12655
13386
|
const key = `attr:${attr}:${variable}`;
|
|
12656
13387
|
if (seen.has(key)) continue;
|
|
12657
13388
|
seen.add(key);
|
|
12658
|
-
|
|
13389
|
+
const loc = locOfAttr(el, attr);
|
|
13390
|
+
issues.push(__spreadValues({
|
|
12659
13391
|
rule: "unresolved-variable",
|
|
12660
13392
|
severity: "error",
|
|
12661
13393
|
message: `Unresolved ${label} variable "${variable}" found in ${attr} attribute.`,
|
|
12662
13394
|
variable,
|
|
12663
13395
|
location: "attribute"
|
|
12664
|
-
});
|
|
13396
|
+
}, loc ? { loc } : {}));
|
|
12665
13397
|
}
|
|
12666
13398
|
}
|
|
12667
13399
|
}
|
|
@@ -12669,17 +13401,17 @@ function checkTemplateVariablesFromDom($) {
|
|
|
12669
13401
|
}
|
|
12670
13402
|
return { unresolvedCount: issues.length, issues };
|
|
12671
13403
|
}
|
|
12672
|
-
function
|
|
12673
|
-
|
|
12674
|
-
|
|
12675
|
-
|
|
12676
|
-
|
|
12677
|
-
|
|
12678
|
-
|
|
13404
|
+
function checkTemplateVariables(html, options) {
|
|
13405
|
+
return fromHtml(
|
|
13406
|
+
html,
|
|
13407
|
+
EMPTY_TEMPLATE,
|
|
13408
|
+
($, h) => checkTemplateVariablesFromDom($, (options == null ? void 0 : options.positions) ? h : void 0),
|
|
13409
|
+
options
|
|
13410
|
+
);
|
|
12679
13411
|
}
|
|
12680
13412
|
|
|
12681
13413
|
// src/overflow-checker.ts
|
|
12682
|
-
import * as
|
|
13414
|
+
import * as csstree6 from "css-tree";
|
|
12683
13415
|
function fixedPxWidth($el) {
|
|
12684
13416
|
const style = $el.attr("style") || "";
|
|
12685
13417
|
const styleMatch = style.match(/(?:^|[;\s])width\s*:\s*(\d+)px/i);
|
|
@@ -12691,89 +13423,142 @@ function fixedPxWidth($el) {
|
|
|
12691
13423
|
function isFluid(style) {
|
|
12692
13424
|
return /max-width\s*:\s*100%/i.test(style) || /width\s*:\s*100%/i.test(style);
|
|
12693
13425
|
}
|
|
12694
|
-
function addWidthIssue(width, label, issues, seen) {
|
|
13426
|
+
function addWidthIssue(width, label, issues, seen, loc) {
|
|
12695
13427
|
const key = `w:${label}:${width}`;
|
|
12696
|
-
|
|
12697
|
-
|
|
12698
|
-
|
|
13428
|
+
const existing = seen.get(key);
|
|
13429
|
+
if (existing) {
|
|
13430
|
+
addOccurrence(existing, loc);
|
|
13431
|
+
return;
|
|
13432
|
+
}
|
|
13433
|
+
const issue = __spreadValues({
|
|
12699
13434
|
rule: "fixed-width-overflow",
|
|
12700
13435
|
severity: "warning",
|
|
12701
13436
|
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.`,
|
|
12702
13437
|
detail: `Use width:100% with max-width:${EMAIL_MAX_WIDTH}px instead of a fixed width beyond the frame.`
|
|
12703
|
-
});
|
|
13438
|
+
}, loc ? { loc, locs: [loc] } : {});
|
|
13439
|
+
seen.set(key, issue);
|
|
13440
|
+
issues.push(issue);
|
|
13441
|
+
}
|
|
13442
|
+
function locateInNodes(nodes, starts, index, length, source) {
|
|
13443
|
+
var _a;
|
|
13444
|
+
let lo = 0;
|
|
13445
|
+
let hi = starts.length - 1;
|
|
13446
|
+
let found = -1;
|
|
13447
|
+
while (lo <= hi) {
|
|
13448
|
+
const mid = lo + hi >> 1;
|
|
13449
|
+
if (starts[mid] <= index) {
|
|
13450
|
+
found = mid;
|
|
13451
|
+
lo = mid + 1;
|
|
13452
|
+
} else {
|
|
13453
|
+
hi = mid - 1;
|
|
13454
|
+
}
|
|
13455
|
+
}
|
|
13456
|
+
if (found === -1) return void 0;
|
|
13457
|
+
const node = nodes[found];
|
|
13458
|
+
const within = index - starts[found];
|
|
13459
|
+
const available = ((_a = node.data) != null ? _a : "").length - within;
|
|
13460
|
+
return locInTextNode(node, within, Math.min(length, available), source);
|
|
13461
|
+
}
|
|
13462
|
+
function addOccurrence(issue, loc) {
|
|
13463
|
+
if (!loc || !issue.locs) return;
|
|
13464
|
+
if (issue.locs.some((l) => l.offset === loc.offset)) return;
|
|
13465
|
+
if (issue.locs.length >= MAX_WARNING_LOCATIONS) {
|
|
13466
|
+
issue.locsTruncated = true;
|
|
13467
|
+
return;
|
|
13468
|
+
}
|
|
13469
|
+
issue.locs.push(loc);
|
|
12704
13470
|
}
|
|
12705
|
-
function checkOverflowFromDom(
|
|
13471
|
+
function checkOverflowFromDom($, source) {
|
|
13472
|
+
var _a;
|
|
12706
13473
|
const issues = [];
|
|
12707
|
-
const seen = /* @__PURE__ */ new
|
|
13474
|
+
const seen = /* @__PURE__ */ new Map();
|
|
13475
|
+
const tokensSeen = /* @__PURE__ */ new Set();
|
|
12708
13476
|
$("[width], [style*='width']").each((_, el) => {
|
|
12709
13477
|
const $el = $(el);
|
|
12710
13478
|
const width = fixedPxWidth($el);
|
|
12711
13479
|
if (width === null || width <= EMAIL_MAX_WIDTH) return;
|
|
12712
13480
|
if (isFluid($el.attr("style") || "")) return;
|
|
12713
13481
|
const tag = (el.tagName || "element").toLowerCase();
|
|
12714
|
-
|
|
13482
|
+
const fromStyle = /(?:^|[;\s])width\s*:\s*\d+px/i.test($el.attr("style") || "");
|
|
13483
|
+
addWidthIssue(width, `<${tag}>`, issues, seen, locOfAttr(el, fromStyle ? "style" : "width"));
|
|
12715
13484
|
});
|
|
12716
13485
|
$("style").each((_, el) => {
|
|
13486
|
+
const cssText = $(el).text();
|
|
13487
|
+
const anchor = cssBlockAnchor(el, cssText, source);
|
|
12717
13488
|
let ast;
|
|
12718
13489
|
try {
|
|
12719
|
-
ast =
|
|
13490
|
+
ast = csstree6.parse(cssText, { positions: true });
|
|
12720
13491
|
} catch (e) {
|
|
12721
13492
|
return;
|
|
12722
13493
|
}
|
|
12723
|
-
|
|
13494
|
+
csstree6.walk(ast, {
|
|
12724
13495
|
visit: "Rule",
|
|
12725
13496
|
enter(node) {
|
|
12726
13497
|
if (node.type !== "Rule") return;
|
|
12727
13498
|
let widthPx = null;
|
|
12728
13499
|
let fluid = false;
|
|
13500
|
+
let widthLoc;
|
|
12729
13501
|
node.block.children.forEach((child) => {
|
|
12730
13502
|
if (child.type !== "Declaration") return;
|
|
12731
13503
|
const prop = child.property.toLowerCase();
|
|
12732
|
-
const val =
|
|
13504
|
+
const val = csstree6.generate(child.value);
|
|
12733
13505
|
if (prop === "width") {
|
|
12734
13506
|
const m = val.match(/^(\d+)px$/);
|
|
12735
|
-
if (m)
|
|
13507
|
+
if (m) {
|
|
13508
|
+
widthPx = parseInt(m[1], 10);
|
|
13509
|
+
widthLoc = locInCssBlock(anchor, child.loc);
|
|
13510
|
+
}
|
|
12736
13511
|
if (/\b100%/.test(val)) fluid = true;
|
|
12737
13512
|
} else if (prop === "max-width" && /\b100%/.test(val)) {
|
|
12738
13513
|
fluid = true;
|
|
12739
13514
|
}
|
|
12740
13515
|
});
|
|
12741
13516
|
if (widthPx !== null && widthPx > EMAIL_MAX_WIDTH && !fluid) {
|
|
12742
|
-
const selector =
|
|
12743
|
-
addWidthIssue(widthPx, selector || "rule", issues, seen);
|
|
13517
|
+
const selector = csstree6.generate(node.prelude).trim().slice(0, 40);
|
|
13518
|
+
addWidthIssue(widthPx, selector || "rule", issues, seen, widthLoc);
|
|
12744
13519
|
}
|
|
12745
13520
|
}
|
|
12746
13521
|
});
|
|
12747
13522
|
});
|
|
12748
13523
|
const usesWrapGuard = /overflow-wrap|word-break|word-wrap/i.test($.html());
|
|
12749
13524
|
if (!usesWrapGuard) {
|
|
12750
|
-
const
|
|
13525
|
+
const nodes = visibleTextNodes($);
|
|
13526
|
+
const starts = [];
|
|
12751
13527
|
let text = "";
|
|
12752
|
-
|
|
12753
|
-
|
|
12754
|
-
|
|
12755
|
-
|
|
12756
|
-
|
|
12757
|
-
for (const token of text.split(
|
|
12758
|
-
|
|
12759
|
-
|
|
13528
|
+
for (const node of nodes) {
|
|
13529
|
+
starts.push(text.length);
|
|
13530
|
+
text += (_a = node.data) != null ? _a : "";
|
|
13531
|
+
}
|
|
13532
|
+
let at = 0;
|
|
13533
|
+
for (const token of text.split(/(\s+)/)) {
|
|
13534
|
+
const start = at;
|
|
13535
|
+
at += token.length;
|
|
13536
|
+
if (/^\s*$/.test(token)) continue;
|
|
13537
|
+
if (token.length <= UNBREAKABLE_STRING_LENGTH || tokensSeen.has(token)) continue;
|
|
13538
|
+
tokensSeen.add(token);
|
|
12760
13539
|
const preview = token.length > 50 ? `${token.slice(0, 50)}\u2026` : token;
|
|
12761
|
-
|
|
13540
|
+
const loc = locateInNodes(nodes, starts, start, token.length, source);
|
|
13541
|
+
issues.push(__spreadValues({
|
|
12762
13542
|
rule: "unbreakable-string",
|
|
12763
13543
|
severity: "warning",
|
|
12764
13544
|
message: `A ${token.length}-character unbroken string ("${preview}") can't wrap and will force horizontal scrolling on narrow screens.`,
|
|
12765
13545
|
detail: `Add overflow-wrap: anywhere (or word-break: break-word) to its container.`
|
|
12766
|
-
});
|
|
13546
|
+
}, loc ? { loc, locs: [loc] } : {}));
|
|
12767
13547
|
}
|
|
12768
13548
|
}
|
|
12769
13549
|
return { hasOverflow: issues.length > 0, issues };
|
|
12770
13550
|
}
|
|
12771
|
-
function checkOverflow(html) {
|
|
12772
|
-
return fromHtml(
|
|
13551
|
+
function checkOverflow(html, options) {
|
|
13552
|
+
return fromHtml(
|
|
13553
|
+
html,
|
|
13554
|
+
EMPTY_OVERFLOW,
|
|
13555
|
+
($, h) => checkOverflowFromDom($, (options == null ? void 0 : options.positions) ? h : void 0),
|
|
13556
|
+
options
|
|
13557
|
+
);
|
|
12773
13558
|
}
|
|
12774
13559
|
|
|
12775
13560
|
// src/visual-checker.ts
|
|
12776
|
-
import * as
|
|
13561
|
+
import * as csstree7 from "css-tree";
|
|
12777
13562
|
var CSS_WIDE_KEYWORDS = /* @__PURE__ */ new Set(["inherit", "initial", "unset", "revert", "revert-layer"]);
|
|
12778
13563
|
var GRADIENT_RE = /(?:linear|radial|conic)-gradient\(/i;
|
|
12779
13564
|
function isSolidColor(value) {
|
|
@@ -12782,8 +13567,8 @@ function isSolidColor(value) {
|
|
|
12782
13567
|
return c !== null && c.a !== 0;
|
|
12783
13568
|
}
|
|
12784
13569
|
function firstColor(value) {
|
|
12785
|
-
const
|
|
12786
|
-
for (const t of
|
|
13570
|
+
const tokens2 = value.match(/#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\)|\b[a-zA-Z]{3,}\b/g) || [];
|
|
13571
|
+
for (const t of tokens2) {
|
|
12787
13572
|
const lc = t.toLowerCase();
|
|
12788
13573
|
if (lc === "transparent") continue;
|
|
12789
13574
|
if (/^(?:linear|radial|conic|gradient|deg|turn|rad|grad|to|at|from|in|circle|ellipse|closest|farthest|side|corner|url)$/.test(lc)) continue;
|
|
@@ -12820,8 +13605,17 @@ function hasFontFallback(value) {
|
|
|
12820
13605
|
return WEB_SAFE_FONTS.has(t) || GENERIC_FONT_FAMILIES.has(t) || t.startsWith("-apple-system") || t === "blinkmacsystemfont";
|
|
12821
13606
|
});
|
|
12822
13607
|
}
|
|
12823
|
-
function
|
|
12824
|
-
|
|
13608
|
+
function addOccurrence2(issue, loc) {
|
|
13609
|
+
if (!loc || !issue.locs) return;
|
|
13610
|
+
if (issue.locs.some((l) => l.offset === loc.offset)) return;
|
|
13611
|
+
if (issue.locs.length >= MAX_WARNING_LOCATIONS) {
|
|
13612
|
+
issue.locsTruncated = true;
|
|
13613
|
+
return;
|
|
13614
|
+
}
|
|
13615
|
+
issue.locs.push(loc);
|
|
13616
|
+
}
|
|
13617
|
+
function inspectDeclarations(style, issues, seen, locs) {
|
|
13618
|
+
var _a, _b, _c;
|
|
12825
13619
|
const combined = `${(_a = style.get("background-image")) != null ? _a : ""} ${(_b = style.get("background")) != null ? _b : ""}`;
|
|
12826
13620
|
const isGradient = GRADIENT_RE.test(combined);
|
|
12827
13621
|
const isImage = isGradient || /url\(/i.test(combined);
|
|
@@ -12829,30 +13623,40 @@ function inspectDeclarations(style, issues, seen) {
|
|
|
12829
13623
|
const stop = isGradient ? firstColor(combined) : null;
|
|
12830
13624
|
const fix = stop ? `background-color: ${stop};` : `background-color: <solid colour matching the image>;`;
|
|
12831
13625
|
const key = `bg:${fix}`;
|
|
12832
|
-
|
|
12833
|
-
|
|
12834
|
-
|
|
13626
|
+
const loc = (_c = locs == null ? void 0 : locs.get("background-image")) != null ? _c : locs == null ? void 0 : locs.get("background");
|
|
13627
|
+
const existing = seen.get(key);
|
|
13628
|
+
if (existing) {
|
|
13629
|
+
addOccurrence2(existing, loc);
|
|
13630
|
+
} else {
|
|
13631
|
+
const issue = __spreadValues({
|
|
12835
13632
|
rule: "missing-background-fallback",
|
|
12836
13633
|
severity: "warning",
|
|
12837
13634
|
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.`,
|
|
12838
13635
|
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.`,
|
|
12839
13636
|
fix
|
|
12840
|
-
});
|
|
13637
|
+
}, loc ? { loc, locs: [loc] } : {});
|
|
13638
|
+
seen.set(key, issue);
|
|
13639
|
+
issues.push(issue);
|
|
12841
13640
|
}
|
|
12842
13641
|
}
|
|
12843
13642
|
const font = style.get("font-family");
|
|
12844
13643
|
if (font && !CSS_WIDE_KEYWORDS.has(font.trim().toLowerCase()) && !hasFontFallback(font)) {
|
|
12845
13644
|
const fix = `font-family: ${font.trim()}, Arial, sans-serif;`;
|
|
12846
13645
|
const key = `font:${font.trim().toLowerCase()}`;
|
|
12847
|
-
|
|
12848
|
-
|
|
12849
|
-
|
|
13646
|
+
const loc = locs == null ? void 0 : locs.get("font-family");
|
|
13647
|
+
const existing = seen.get(key);
|
|
13648
|
+
if (existing) {
|
|
13649
|
+
addOccurrence2(existing, loc);
|
|
13650
|
+
} else {
|
|
13651
|
+
const issue = __spreadValues({
|
|
12850
13652
|
rule: "missing-font-fallback",
|
|
12851
13653
|
severity: "warning",
|
|
12852
13654
|
message: `font-family "${font.trim()}" has no web-safe fallback \u2014 clients that strip web fonts (Gmail, Outlook) fall back to Times New Roman.`,
|
|
12853
13655
|
detail: `End the stack with a web-safe font and a generic family.`,
|
|
12854
13656
|
fix
|
|
12855
|
-
});
|
|
13657
|
+
}, loc ? { loc, locs: [loc] } : {});
|
|
13658
|
+
seen.set(key, issue);
|
|
13659
|
+
issues.push(issue);
|
|
12856
13660
|
}
|
|
12857
13661
|
}
|
|
12858
13662
|
}
|
|
@@ -12860,35 +13664,52 @@ function ruleToMap(node) {
|
|
|
12860
13664
|
const map = /* @__PURE__ */ new Map();
|
|
12861
13665
|
node.block.children.forEach((child) => {
|
|
12862
13666
|
if (child.type === "Declaration") {
|
|
12863
|
-
map.set(child.property.toLowerCase(),
|
|
13667
|
+
map.set(child.property.toLowerCase(), csstree7.generate(child.value));
|
|
12864
13668
|
}
|
|
12865
13669
|
});
|
|
12866
13670
|
return map;
|
|
12867
13671
|
}
|
|
12868
|
-
function checkVisualFromDom(
|
|
13672
|
+
function checkVisualFromDom($, source) {
|
|
12869
13673
|
const issues = [];
|
|
12870
|
-
const seen = /* @__PURE__ */ new
|
|
13674
|
+
const seen = /* @__PURE__ */ new Map();
|
|
12871
13675
|
$("[style]").each((_, el) => {
|
|
12872
|
-
|
|
13676
|
+
const attrLoc = locOfAttr(el, "style");
|
|
13677
|
+
const style = parseInlineStyle($(el).attr("style") || "");
|
|
13678
|
+
const locs = attrLoc ? new Map([...style.keys()].map((prop) => [prop, attrLoc])) : void 0;
|
|
13679
|
+
inspectDeclarations(style, issues, seen, locs);
|
|
12873
13680
|
});
|
|
12874
13681
|
$("style").each((_, el) => {
|
|
13682
|
+
const cssText = $(el).text();
|
|
13683
|
+
const anchor = cssBlockAnchor(el, cssText, source);
|
|
12875
13684
|
let ast;
|
|
12876
13685
|
try {
|
|
12877
|
-
ast =
|
|
13686
|
+
ast = csstree7.parse(cssText, { positions: true });
|
|
12878
13687
|
} catch (e) {
|
|
12879
13688
|
return;
|
|
12880
13689
|
}
|
|
12881
|
-
|
|
13690
|
+
csstree7.walk(ast, {
|
|
12882
13691
|
visit: "Rule",
|
|
12883
13692
|
enter(node) {
|
|
12884
|
-
if (node.type
|
|
13693
|
+
if (node.type !== "Rule") return;
|
|
13694
|
+
const locs = /* @__PURE__ */ new Map();
|
|
13695
|
+
node.block.children.forEach((child) => {
|
|
13696
|
+
if (child.type !== "Declaration") return;
|
|
13697
|
+
const loc = locInCssBlock(anchor, child.loc);
|
|
13698
|
+
if (loc) locs.set(child.property.toLowerCase(), loc);
|
|
13699
|
+
});
|
|
13700
|
+
inspectDeclarations(ruleToMap(node), issues, seen, locs);
|
|
12885
13701
|
}
|
|
12886
13702
|
});
|
|
12887
13703
|
});
|
|
12888
13704
|
return { issues };
|
|
12889
13705
|
}
|
|
12890
|
-
function checkVisual(html) {
|
|
12891
|
-
return fromHtml(
|
|
13706
|
+
function checkVisual(html, options) {
|
|
13707
|
+
return fromHtml(
|
|
13708
|
+
html,
|
|
13709
|
+
EMPTY_VISUAL,
|
|
13710
|
+
($, h) => checkVisualFromDom($, (options == null ? void 0 : options.positions) ? h : void 0),
|
|
13711
|
+
options
|
|
13712
|
+
);
|
|
12892
13713
|
}
|
|
12893
13714
|
|
|
12894
13715
|
// src/audit.ts
|
|
@@ -12907,7 +13728,8 @@ var EMPTY_AUDIT = {
|
|
|
12907
13728
|
function runAudit($, html, framework, options) {
|
|
12908
13729
|
var _a;
|
|
12909
13730
|
const skip = new Set((_a = options == null ? void 0 : options.skip) != null ? _a : []);
|
|
12910
|
-
const
|
|
13731
|
+
const source = (options == null ? void 0 : options.positions) ? html : void 0;
|
|
13732
|
+
const warnings = skip.has("compatibility") ? [] : analyzeEmailFromDom($, framework, source);
|
|
12911
13733
|
const scores = skip.has("compatibility") ? {} : generateCompatibilityScore(warnings);
|
|
12912
13734
|
const spam = skip.has("spam") ? EMPTY_SPAM : analyzeSpamFromDom($, options == null ? void 0 : options.spam);
|
|
12913
13735
|
const links = skip.has("links") ? EMPTY_LINKS : validateLinksFromDom($);
|
|
@@ -12915,19 +13737,19 @@ function runAudit($, html, framework, options) {
|
|
|
12915
13737
|
const images = skip.has("images") ? EMPTY_IMAGES : analyzeImagesFromDom($);
|
|
12916
13738
|
const inboxPreview = skip.has("inboxPreview") ? EMPTY_INBOX_PREVIEW : extractInboxPreviewFromDom($);
|
|
12917
13739
|
const size = skip.has("size") ? EMPTY_SIZE : checkSizeFromDom($, html);
|
|
12918
|
-
const templateVariables = skip.has("templateVariables") ? EMPTY_TEMPLATE : checkTemplateVariablesFromDom(
|
|
12919
|
-
const overflow = skip.has("overflow") ? EMPTY_OVERFLOW : checkOverflowFromDom(
|
|
12920
|
-
const visual = skip.has("visual") ? EMPTY_VISUAL : checkVisualFromDom(
|
|
13740
|
+
const templateVariables = skip.has("templateVariables") ? EMPTY_TEMPLATE : checkTemplateVariablesFromDom($, source);
|
|
13741
|
+
const overflow = skip.has("overflow") ? EMPTY_OVERFLOW : checkOverflowFromDom($, source);
|
|
13742
|
+
const visual = skip.has("visual") ? EMPTY_VISUAL : checkVisualFromDom($, source);
|
|
12921
13743
|
return { compatibility: { warnings, scores }, spam, links, accessibility, images, inboxPreview, size, templateVariables, overflow, visual };
|
|
12922
13744
|
}
|
|
12923
13745
|
function auditEmail(html, options) {
|
|
12924
|
-
return fromHtml(html, EMPTY_AUDIT, ($, h) => runAudit($, h, options == null ? void 0 : options.framework, options));
|
|
13746
|
+
return fromHtml(html, EMPTY_AUDIT, ($, h) => runAudit($, h, options == null ? void 0 : options.framework, options), options);
|
|
12925
13747
|
}
|
|
12926
13748
|
|
|
12927
13749
|
// src/plain-text.ts
|
|
12928
|
-
import * as
|
|
13750
|
+
import * as cheerio5 from "cheerio";
|
|
12929
13751
|
function toPlainText(html) {
|
|
12930
|
-
const $ =
|
|
13752
|
+
const $ = cheerio5.load(html);
|
|
12931
13753
|
$("style, script, head").remove();
|
|
12932
13754
|
$("[data-skip-in-text='true']").remove();
|
|
12933
13755
|
const lines = [];
|
|
@@ -12962,7 +13784,7 @@ function toPlainText(html) {
|
|
|
12962
13784
|
if (trimmed) lines.push(trimmed);
|
|
12963
13785
|
currentLine = "";
|
|
12964
13786
|
}
|
|
12965
|
-
function
|
|
13787
|
+
function walk8(node) {
|
|
12966
13788
|
var _a;
|
|
12967
13789
|
if (node.type === "text") {
|
|
12968
13790
|
const text = node.data.replace(/\s+/g, " ");
|
|
@@ -13011,7 +13833,7 @@ function toPlainText(html) {
|
|
|
13011
13833
|
currentLine = "- ";
|
|
13012
13834
|
}
|
|
13013
13835
|
for (const child of el.children) {
|
|
13014
|
-
|
|
13836
|
+
walk8(child);
|
|
13015
13837
|
}
|
|
13016
13838
|
if (isBlock) flushLine();
|
|
13017
13839
|
}
|
|
@@ -13019,7 +13841,7 @@ function toPlainText(html) {
|
|
|
13019
13841
|
const root = body.length ? body[0] : $.root()[0];
|
|
13020
13842
|
if (root && "children" in root) {
|
|
13021
13843
|
for (const child of root.children) {
|
|
13022
|
-
|
|
13844
|
+
walk8(child);
|
|
13023
13845
|
}
|
|
13024
13846
|
}
|
|
13025
13847
|
flushLine();
|
|
@@ -13029,7 +13851,6 @@ function toPlainText(html) {
|
|
|
13029
13851
|
}
|
|
13030
13852
|
|
|
13031
13853
|
// src/session.ts
|
|
13032
|
-
import * as cheerio7 from "cheerio";
|
|
13033
13854
|
function createSession(html, options) {
|
|
13034
13855
|
if (!html || !html.trim()) {
|
|
13035
13856
|
const fw = options == null ? void 0 : options.framework;
|
|
@@ -13056,16 +13877,17 @@ function createSession(html, options) {
|
|
|
13056
13877
|
if (html.length > MAX_HTML_SIZE) {
|
|
13057
13878
|
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
13058
13879
|
}
|
|
13059
|
-
const $ =
|
|
13880
|
+
const $ = loadHtml(html, options);
|
|
13060
13881
|
const framework = options == null ? void 0 : options.framework;
|
|
13882
|
+
const source = (options == null ? void 0 : options.positions) ? html : void 0;
|
|
13061
13883
|
return {
|
|
13062
13884
|
html,
|
|
13063
13885
|
framework,
|
|
13064
13886
|
audit(opts) {
|
|
13065
|
-
return runAudit($, html, framework, opts);
|
|
13887
|
+
return runAudit($, html, framework, __spreadProps(__spreadValues({}, opts), { positions: options == null ? void 0 : options.positions }));
|
|
13066
13888
|
},
|
|
13067
13889
|
analyze() {
|
|
13068
|
-
return analyzeEmailFromDom($, framework);
|
|
13890
|
+
return analyzeEmailFromDom($, framework, source);
|
|
13069
13891
|
},
|
|
13070
13892
|
score(warnings) {
|
|
13071
13893
|
return generateCompatibilityScore(warnings);
|
|
@@ -13089,13 +13911,13 @@ function createSession(html, options) {
|
|
|
13089
13911
|
return checkSizeFromDom($, html);
|
|
13090
13912
|
},
|
|
13091
13913
|
checkTemplateVariables() {
|
|
13092
|
-
return checkTemplateVariablesFromDom(
|
|
13914
|
+
return checkTemplateVariablesFromDom($, source);
|
|
13093
13915
|
},
|
|
13094
13916
|
checkOverflow() {
|
|
13095
|
-
return checkOverflowFromDom(
|
|
13917
|
+
return checkOverflowFromDom($, source);
|
|
13096
13918
|
},
|
|
13097
13919
|
checkVisual() {
|
|
13098
|
-
return checkVisualFromDom(
|
|
13920
|
+
return checkVisualFromDom($, source);
|
|
13099
13921
|
},
|
|
13100
13922
|
// Transforms create isolated copies since they mutate the DOM
|
|
13101
13923
|
transformForClient(clientId) {
|
|
@@ -13115,18 +13937,22 @@ export {
|
|
|
13115
13937
|
COMPOUND_VALUE_FEATURES,
|
|
13116
13938
|
CSS_FUNCTION_FEATURES,
|
|
13117
13939
|
CSS_SUPPORT,
|
|
13940
|
+
CSS_SUPPORT_NOTES,
|
|
13118
13941
|
CompileError,
|
|
13119
13942
|
EMAIL_CLIENTS,
|
|
13120
13943
|
EMPTY_DELIVERABILITY,
|
|
13121
13944
|
GENERIC_LINK_TEXT,
|
|
13122
13945
|
HTML_ELEMENT_FEATURES,
|
|
13123
13946
|
MAX_HTML_SIZE,
|
|
13947
|
+
MAX_WARNING_LOCATIONS,
|
|
13124
13948
|
STRUCTURAL_FIX_PROPERTIES,
|
|
13949
|
+
VALUE_CAVEAT_PROPS,
|
|
13125
13950
|
alphaBlend,
|
|
13126
13951
|
analyzeEmail,
|
|
13127
13952
|
analyzeImages,
|
|
13128
13953
|
analyzeSpam,
|
|
13129
13954
|
auditEmail,
|
|
13955
|
+
caveatApplies,
|
|
13130
13956
|
checkAccessibility,
|
|
13131
13957
|
checkOverflow,
|
|
13132
13958
|
checkSize,
|