@emailens/engine 0.10.1 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +258 -247
- package/dist/compile/index.d.cts +2 -2
- package/dist/compile/index.d.ts +2 -2
- package/dist/index.cjs +906 -227
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +64 -14
- package/dist/index.d.ts +64 -14
- package/dist/index.js +902 -227
- package/dist/index.js.map +1 -1
- package/dist/{react-email-B1Rd5itD.d.cts → react-email-DTzVpGgB.d.cts} +1 -1
- package/dist/{react-email-D4XyNztP.d.ts → react-email-DwBWB2kr.d.ts} +1 -1
- package/dist/server.d.cts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/{types-DEAn3IiX.d.cts → types-BLR3-Fzo.d.cts} +58 -1
- package/dist/{types-DEAn3IiX.d.ts → types-BLR3-Fzo.d.ts} +58 -1
- package/package.json +124 -123
package/dist/index.js
CHANGED
|
@@ -56,7 +56,11 @@ var EMAIL_CLIENTS = [
|
|
|
56
56
|
engine: "Microsoft Word",
|
|
57
57
|
darkModeSupport: true,
|
|
58
58
|
icon: "monitor",
|
|
59
|
-
|
|
59
|
+
// End of support ~Q2 2029 (Microsoft: "supported until at least 2029").
|
|
60
|
+
// The April 2026 date some sources cite is the opt-out phase, when classic
|
|
61
|
+
// stops being the Windows default, not end of support. October 2026 was
|
|
62
|
+
// wrong: the nearest real Oct date is Oct 2025, for legacy Outlook for Mac.
|
|
63
|
+
deprecated: "2029-06"
|
|
60
64
|
},
|
|
61
65
|
{
|
|
62
66
|
id: "outlook-ios",
|
|
@@ -8889,6 +8893,16 @@ function getStyleValue(style, property) {
|
|
|
8889
8893
|
}
|
|
8890
8894
|
return null;
|
|
8891
8895
|
}
|
|
8896
|
+
function getStyleValues(style, property) {
|
|
8897
|
+
const values = [];
|
|
8898
|
+
for (const part of splitStyleDeclarations(style)) {
|
|
8899
|
+
const colonIndex = part.indexOf(":");
|
|
8900
|
+
if (colonIndex === -1) continue;
|
|
8901
|
+
if (part.slice(0, colonIndex).trim().toLowerCase() !== property) continue;
|
|
8902
|
+
values.push(part.slice(colonIndex + 1).trim());
|
|
8903
|
+
}
|
|
8904
|
+
return values;
|
|
8905
|
+
}
|
|
8892
8906
|
function parseInlineStyle(style) {
|
|
8893
8907
|
const map = /* @__PURE__ */ new Map();
|
|
8894
8908
|
const declarations = splitStyleDeclarations(style);
|
|
@@ -9639,6 +9653,7 @@ function downlevelCSS(html) {
|
|
|
9639
9653
|
|
|
9640
9654
|
// src/constants.ts
|
|
9641
9655
|
var MAX_HTML_SIZE = 2 * 1024 * 1024;
|
|
9656
|
+
var MAX_WARNING_LOCATIONS = 100;
|
|
9642
9657
|
var GENERIC_LINK_TEXT = /* @__PURE__ */ new Set([
|
|
9643
9658
|
"click here",
|
|
9644
9659
|
"here",
|
|
@@ -10375,9 +10390,231 @@ function transformForAllClients(html, framework) {
|
|
|
10375
10390
|
}
|
|
10376
10391
|
|
|
10377
10392
|
// src/analyze.ts
|
|
10378
|
-
import * as cheerio4 from "cheerio";
|
|
10379
10393
|
import * as csstree5 from "css-tree";
|
|
10380
10394
|
|
|
10395
|
+
// src/rules/value-caveats.ts
|
|
10396
|
+
var VALUE_CAVEAT_PROPS = /* @__PURE__ */ new Set([
|
|
10397
|
+
"background",
|
|
10398
|
+
"border-radius",
|
|
10399
|
+
"display",
|
|
10400
|
+
"font-size",
|
|
10401
|
+
"font-weight",
|
|
10402
|
+
"letter-spacing",
|
|
10403
|
+
"margin",
|
|
10404
|
+
"overflow",
|
|
10405
|
+
"position",
|
|
10406
|
+
"text-align",
|
|
10407
|
+
"transition"
|
|
10408
|
+
]);
|
|
10409
|
+
function normalize(value) {
|
|
10410
|
+
return value.replace(/\/\*[\s\S]*?\*\//g, " ").toLowerCase().replace(/!\s*important/g, " ").replace(/;+\s*$/, "").replace(/\s+/g, " ").trim();
|
|
10411
|
+
}
|
|
10412
|
+
var preparedValues = /* @__PURE__ */ new WeakMap();
|
|
10413
|
+
function prepare(values) {
|
|
10414
|
+
let out = preparedValues.get(values);
|
|
10415
|
+
if (!out) {
|
|
10416
|
+
out = [...new Set(values.map(normalize))];
|
|
10417
|
+
preparedValues.set(values, out);
|
|
10418
|
+
}
|
|
10419
|
+
return out;
|
|
10420
|
+
}
|
|
10421
|
+
function topLevelSplit(value, sep) {
|
|
10422
|
+
const parts = [];
|
|
10423
|
+
let depth = 0;
|
|
10424
|
+
let start = 0;
|
|
10425
|
+
for (let i = 0; i < value.length; i++) {
|
|
10426
|
+
const c = value[i];
|
|
10427
|
+
if (c === "(") depth++;
|
|
10428
|
+
else if (c === ")") depth = Math.max(0, depth - 1);
|
|
10429
|
+
else if (c === sep && depth === 0) {
|
|
10430
|
+
parts.push(value.slice(start, i));
|
|
10431
|
+
start = i + 1;
|
|
10432
|
+
}
|
|
10433
|
+
}
|
|
10434
|
+
parts.push(value.slice(start));
|
|
10435
|
+
return parts.map((p) => p.trim()).filter(Boolean);
|
|
10436
|
+
}
|
|
10437
|
+
function tokens(value) {
|
|
10438
|
+
return topLevelSplit(value, " ");
|
|
10439
|
+
}
|
|
10440
|
+
function hasUnit(value, units) {
|
|
10441
|
+
return new RegExp(`\\d(?:${units.join("|")})\\b`).test(value);
|
|
10442
|
+
}
|
|
10443
|
+
function bareNumbers(value) {
|
|
10444
|
+
return tokens(value).filter((t) => /^[+-]?\d+(?:\.\d+)?(?:e[+-]?\d+)?$/.test(t)).map(Number);
|
|
10445
|
+
}
|
|
10446
|
+
function hasNegative(value) {
|
|
10447
|
+
return /(?:^|[\s,(])-\.?\d/.test(value);
|
|
10448
|
+
}
|
|
10449
|
+
function dashed(v) {
|
|
10450
|
+
return v.replace(/\s+/g, "-");
|
|
10451
|
+
}
|
|
10452
|
+
function unprefixed(token) {
|
|
10453
|
+
return token.replace(/^-(?:webkit|moz|ms|o)-/, "");
|
|
10454
|
+
}
|
|
10455
|
+
function quotedValues(note) {
|
|
10456
|
+
var _a;
|
|
10457
|
+
const supported = [];
|
|
10458
|
+
const banned = [];
|
|
10459
|
+
for (const m of note.matchAll(/`([^`]+)`/g)) {
|
|
10460
|
+
const clause = (_a = note.slice(0, m.index).split(/[.;]/).pop()) != null ? _a : "";
|
|
10461
|
+
(/\bsupports\b/i.test(clause) ? supported : banned).push(dashed(m[1].toLowerCase().trim()));
|
|
10462
|
+
}
|
|
10463
|
+
return { supported, banned };
|
|
10464
|
+
}
|
|
10465
|
+
function isColorOnly(value) {
|
|
10466
|
+
if (value.startsWith("#")) return /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(value);
|
|
10467
|
+
if (parseColor(value)) return true;
|
|
10468
|
+
return ["none", "inherit", "initial", "unset", "revert", "currentcolor"].includes(value) || // A custom property is usually a palette colour, and we cannot resolve it.
|
|
10469
|
+
value.startsWith("var(");
|
|
10470
|
+
}
|
|
10471
|
+
var POSITION_KEYWORDS = ["relative", "absolute", "fixed", "sticky"];
|
|
10472
|
+
var RELATIVE_FONT_UNITS = [
|
|
10473
|
+
"cqmin",
|
|
10474
|
+
"cqmax",
|
|
10475
|
+
"vmin",
|
|
10476
|
+
"vmax",
|
|
10477
|
+
"rlh",
|
|
10478
|
+
"rex",
|
|
10479
|
+
"rch",
|
|
10480
|
+
"ric",
|
|
10481
|
+
"rem",
|
|
10482
|
+
"svw",
|
|
10483
|
+
"svh",
|
|
10484
|
+
"lvw",
|
|
10485
|
+
"lvh",
|
|
10486
|
+
"dvw",
|
|
10487
|
+
"dvh",
|
|
10488
|
+
"cqw",
|
|
10489
|
+
"cqh",
|
|
10490
|
+
"cqi",
|
|
10491
|
+
"cqb",
|
|
10492
|
+
"cap",
|
|
10493
|
+
"ic",
|
|
10494
|
+
"lh",
|
|
10495
|
+
"em",
|
|
10496
|
+
"ex",
|
|
10497
|
+
"ch",
|
|
10498
|
+
"vw",
|
|
10499
|
+
"vh",
|
|
10500
|
+
"vi",
|
|
10501
|
+
"vb"
|
|
10502
|
+
];
|
|
10503
|
+
var NOT_A_PROPERTY_NAME = /* @__PURE__ */ new Set([
|
|
10504
|
+
"all",
|
|
10505
|
+
"allow-discrete",
|
|
10506
|
+
"normal",
|
|
10507
|
+
"ease",
|
|
10508
|
+
"ease-in",
|
|
10509
|
+
"ease-out",
|
|
10510
|
+
"ease-in-out",
|
|
10511
|
+
"linear",
|
|
10512
|
+
"step-start",
|
|
10513
|
+
"step-end",
|
|
10514
|
+
"none",
|
|
10515
|
+
"initial",
|
|
10516
|
+
"inherit",
|
|
10517
|
+
"unset",
|
|
10518
|
+
"revert",
|
|
10519
|
+
"revert-layer"
|
|
10520
|
+
]);
|
|
10521
|
+
function namesAProperty(layer) {
|
|
10522
|
+
return tokens(layer).some((t) => /^[a-z][a-z0-9-]*$/.test(t) && !NOT_A_PROPERTY_NAME.has(t));
|
|
10523
|
+
}
|
|
10524
|
+
function triggers(prop, value, note, noteLc) {
|
|
10525
|
+
switch (prop) {
|
|
10526
|
+
case "margin": {
|
|
10527
|
+
const negative = noteLc.includes("negative");
|
|
10528
|
+
const auto = noteLc.includes("auto");
|
|
10529
|
+
if (!negative && !auto) return true;
|
|
10530
|
+
return negative && hasNegative(value) || auto && /\bauto\b/.test(value);
|
|
10531
|
+
}
|
|
10532
|
+
case "position": {
|
|
10533
|
+
const used = POSITION_KEYWORDS.find((k) => tokens(value).some((t) => unprefixed(t) === k));
|
|
10534
|
+
if (!used) return false;
|
|
10535
|
+
const m = note.match(/supports\s+.+?\s+but not\s+([^.]+)/i);
|
|
10536
|
+
if (m) return m[1].toLowerCase().includes(used);
|
|
10537
|
+
return true;
|
|
10538
|
+
}
|
|
10539
|
+
case "overflow": {
|
|
10540
|
+
if (!/\b(?:auto|scroll|overlay)\b/.test(value)) return false;
|
|
10541
|
+
if (!noteLc.includes("cannot scroll") && !noteLc.includes("overflow-block")) return true;
|
|
10542
|
+
return noteLc.includes("cannot scroll");
|
|
10543
|
+
}
|
|
10544
|
+
case "font-size": {
|
|
10545
|
+
if (noteLc.includes("percentage") || noteLc.includes("relative")) {
|
|
10546
|
+
return hasUnit(value, RELATIVE_FONT_UNITS) || /\d\s*%/.test(value) || tokens(value).some((t) => t === "smaller" || t === "larger");
|
|
10547
|
+
}
|
|
10548
|
+
if (noteLc.includes("`rem`")) return hasUnit(value, ["rem"]);
|
|
10549
|
+
return true;
|
|
10550
|
+
}
|
|
10551
|
+
case "display": {
|
|
10552
|
+
const only = note.match(/only supports\s+([^.]*)/i);
|
|
10553
|
+
if (only) {
|
|
10554
|
+
const allowed = quotedValues(only[1]).banned.map((q) => q.replace(/^display:-?/, "")).filter((q) => /^[a-z-]+$/.test(q));
|
|
10555
|
+
if (!allowed.length) return true;
|
|
10556
|
+
return !allowed.includes(dashed(value));
|
|
10557
|
+
}
|
|
10558
|
+
const { banned } = quotedValues(note);
|
|
10559
|
+
if (banned.length) {
|
|
10560
|
+
const v = dashed(value);
|
|
10561
|
+
return banned.includes(v) || banned.includes(unprefixed(v));
|
|
10562
|
+
}
|
|
10563
|
+
if (noteLc.includes("two-value syntax")) return tokens(value).length > 1;
|
|
10564
|
+
return true;
|
|
10565
|
+
}
|
|
10566
|
+
case "font-weight": {
|
|
10567
|
+
const nums = bareNumbers(value);
|
|
10568
|
+
if (!nums.length) return false;
|
|
10569
|
+
if (noteLc.includes("font weight")) return nums.some((n) => n !== 400 && n !== 700);
|
|
10570
|
+
if (noteLc.includes("only the following numeric values")) {
|
|
10571
|
+
return nums.some((n) => n % 100 !== 0 || n < 100 || n > 900);
|
|
10572
|
+
}
|
|
10573
|
+
return true;
|
|
10574
|
+
}
|
|
10575
|
+
case "border-radius": {
|
|
10576
|
+
if (noteLc.includes("slash")) return topLevelSplit(value, "/").length > 1;
|
|
10577
|
+
return true;
|
|
10578
|
+
}
|
|
10579
|
+
case "text-align": {
|
|
10580
|
+
const { supported, banned } = quotedValues(note);
|
|
10581
|
+
if (!banned.length) return true;
|
|
10582
|
+
return tokens(value).some(
|
|
10583
|
+
(t) => !supported.includes(t) && (banned.includes(t) || banned.includes(unprefixed(t)))
|
|
10584
|
+
);
|
|
10585
|
+
}
|
|
10586
|
+
case "background": {
|
|
10587
|
+
if (noteLc.includes("only `background-color`")) return !isColorOnly(value);
|
|
10588
|
+
if (noteLc.includes("multiple values")) {
|
|
10589
|
+
return topLevelSplit(value, ",").length > 1 || topLevelSplit(value, "/").length > 1;
|
|
10590
|
+
}
|
|
10591
|
+
return true;
|
|
10592
|
+
}
|
|
10593
|
+
case "letter-spacing": {
|
|
10594
|
+
const negative = noteLc.includes("negative");
|
|
10595
|
+
const em = noteLc.includes("`em`");
|
|
10596
|
+
if (!negative && !em) return true;
|
|
10597
|
+
return negative && hasNegative(value) || em && hasUnit(value, ["em"]);
|
|
10598
|
+
}
|
|
10599
|
+
case "transition": {
|
|
10600
|
+
if (noteLc.includes("`all`")) {
|
|
10601
|
+
if (["none", "inherit", "initial", "unset", "revert"].includes(value)) return false;
|
|
10602
|
+
return topLevelSplit(value, ",").some((layer) => !namesAProperty(layer));
|
|
10603
|
+
}
|
|
10604
|
+
return true;
|
|
10605
|
+
}
|
|
10606
|
+
default:
|
|
10607
|
+
return true;
|
|
10608
|
+
}
|
|
10609
|
+
}
|
|
10610
|
+
function caveatApplies(prop, values, notes) {
|
|
10611
|
+
if (!VALUE_CAVEAT_PROPS.has(prop)) return true;
|
|
10612
|
+
if (!(values == null ? void 0 : values.length)) return true;
|
|
10613
|
+
const note = (notes != null ? notes : []).join(" ");
|
|
10614
|
+
const noteLc = note.toLowerCase();
|
|
10615
|
+
return prepare(values).some((value) => triggers(prop, value, note, noteLc));
|
|
10616
|
+
}
|
|
10617
|
+
|
|
10381
10618
|
// src/dark-mode-checker.ts
|
|
10382
10619
|
import * as csstree4 from "css-tree";
|
|
10383
10620
|
|
|
@@ -10654,6 +10891,209 @@ function applyColorInversion($, mode) {
|
|
|
10654
10891
|
});
|
|
10655
10892
|
}
|
|
10656
10893
|
|
|
10894
|
+
// src/source-location.ts
|
|
10895
|
+
function toLoc(p) {
|
|
10896
|
+
return {
|
|
10897
|
+
line: p.startLine,
|
|
10898
|
+
column: p.startCol,
|
|
10899
|
+
endLine: p.endLine,
|
|
10900
|
+
endColumn: p.endCol,
|
|
10901
|
+
offset: p.startOffset,
|
|
10902
|
+
length: p.endOffset - p.startOffset
|
|
10903
|
+
};
|
|
10904
|
+
}
|
|
10905
|
+
function locOfElement(el) {
|
|
10906
|
+
var _a;
|
|
10907
|
+
const raw = el == null ? void 0 : el.sourceCodeLocation;
|
|
10908
|
+
if (!raw) return void 0;
|
|
10909
|
+
return toLoc((_a = raw.startTag) != null ? _a : raw);
|
|
10910
|
+
}
|
|
10911
|
+
function locOfAttr(el, attr) {
|
|
10912
|
+
var _a, _b, _c, _d;
|
|
10913
|
+
const raw = el == null ? void 0 : el.sourceCodeLocation;
|
|
10914
|
+
if (!raw) return void 0;
|
|
10915
|
+
const attrLoc = (_d = (_a = raw.attrs) == null ? void 0 : _a[attr]) != null ? _d : (_c = (_b = raw.startTag) == null ? void 0 : _b.attrs) == null ? void 0 : _c[attr];
|
|
10916
|
+
return attrLoc ? toLoc(attrLoc) : locOfElement(el);
|
|
10917
|
+
}
|
|
10918
|
+
function 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
|
+
|
|
10657
11097
|
// src/dark-mode-checker.ts
|
|
10658
11098
|
var DARK_MEDIA_RE = /\(\s*prefers-color-scheme\s*:\s*dark\s*\)/i;
|
|
10659
11099
|
var MAX_UNCOVERED_ELEMENTS = 3;
|
|
@@ -10750,15 +11190,16 @@ function checkDarkModeFromDom($) {
|
|
|
10750
11190
|
return name === "color-scheme" || name === "supported-color-schemes";
|
|
10751
11191
|
});
|
|
10752
11192
|
if (!hasOptIn) {
|
|
11193
|
+
const headLoc = locOfFirst($, "head");
|
|
10753
11194
|
for (const clientId of PREFERS_COLOR_SCHEME_CLIENTS) {
|
|
10754
|
-
warnings.push({
|
|
11195
|
+
warnings.push(__spreadValues({
|
|
10755
11196
|
severity: "warning",
|
|
10756
11197
|
client: clientId,
|
|
10757
11198
|
property: "dark-mode-opt-in",
|
|
10758
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.`,
|
|
10759
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">.',
|
|
10760
11201
|
fixType: "structural"
|
|
10761
|
-
});
|
|
11202
|
+
}, headLoc ? { loc: headLoc, locs: [headLoc] } : {}));
|
|
10762
11203
|
}
|
|
10763
11204
|
}
|
|
10764
11205
|
if (darkBlock.rules === 0) return warnings;
|
|
@@ -10776,8 +11217,9 @@ function checkDarkModeFromDom($) {
|
|
|
10776
11217
|
if (inline ? coveredByImportant.has(el) : coveredByAny.has(el)) return;
|
|
10777
11218
|
uncovered++;
|
|
10778
11219
|
const selector = describeSelector($, el);
|
|
11220
|
+
const loc = locOfAttr(el, inline ? "style" : "bgcolor");
|
|
10779
11221
|
for (const clientId of PREFERS_COLOR_SCHEME_CLIENTS) {
|
|
10780
|
-
warnings.push({
|
|
11222
|
+
warnings.push(__spreadValues({
|
|
10781
11223
|
severity: "warning",
|
|
10782
11224
|
client: clientId,
|
|
10783
11225
|
property: "dark-mode-coverage",
|
|
@@ -10785,12 +11227,25 @@ function checkDarkModeFromDom($) {
|
|
|
10785
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.`,
|
|
10786
11228
|
fixType: "css",
|
|
10787
11229
|
selector
|
|
10788
|
-
});
|
|
11230
|
+
}, loc ? { loc, locs: [loc] } : {}));
|
|
10789
11231
|
}
|
|
10790
11232
|
});
|
|
10791
11233
|
return warnings;
|
|
10792
11234
|
}
|
|
10793
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
|
+
|
|
10794
11249
|
// src/analyze.ts
|
|
10795
11250
|
var HTML_ELEMENT_SELECTORS = {
|
|
10796
11251
|
"<style>": "style",
|
|
@@ -10843,14 +11298,25 @@ var CSS_FUNCTION_DETECTORS = CSS_FUNCTION_FEATURES.map((fn) => ({
|
|
|
10843
11298
|
pattern: `${fn}(`
|
|
10844
11299
|
// require opening paren — matches "min(" but not "Minion"
|
|
10845
11300
|
}));
|
|
10846
|
-
function analyzeEmailFromDom($, framework) {
|
|
11301
|
+
function analyzeEmailFromDom($, framework, source) {
|
|
10847
11302
|
const warnings = [];
|
|
10848
|
-
const seenWarnings = /* @__PURE__ */ new
|
|
11303
|
+
const seenWarnings = /* @__PURE__ */ new Map();
|
|
10849
11304
|
function addWarning(w) {
|
|
10850
11305
|
const key = `${w.client}:${w.property}:${w.severity}:${w.selector || ""}`;
|
|
10851
|
-
|
|
10852
|
-
|
|
11306
|
+
const existing = seenWarnings.get(key);
|
|
11307
|
+
if (!existing) {
|
|
11308
|
+
seenWarnings.set(key, w);
|
|
10853
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);
|
|
10854
11320
|
}
|
|
10855
11321
|
}
|
|
10856
11322
|
function describeSelector2(el) {
|
|
@@ -10868,10 +11334,16 @@ function analyzeEmailFromDom($, framework) {
|
|
|
10868
11334
|
for (const feature of HTML_ELEMENT_FEATURES) {
|
|
10869
11335
|
const selector = HTML_ELEMENT_SELECTORS[feature];
|
|
10870
11336
|
if (!selector) continue;
|
|
10871
|
-
|
|
11337
|
+
const matches = $(selector);
|
|
11338
|
+
if (matches.length === 0) continue;
|
|
10872
11339
|
const supportData = CSS_SUPPORT[feature];
|
|
10873
11340
|
if (!supportData) continue;
|
|
10874
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];
|
|
10875
11347
|
for (const client of EMAIL_CLIENTS) {
|
|
10876
11348
|
const support = supportData[client.id];
|
|
10877
11349
|
if (support === "unsupported") {
|
|
@@ -10879,7 +11351,7 @@ function analyzeEmailFromDom($, framework) {
|
|
|
10879
11351
|
const message = msgFn ? msgFn(client.name) : `${client.name} does not support ${feature}.`;
|
|
10880
11352
|
const sug = getSuggestion(feature, client.id, framework);
|
|
10881
11353
|
const fix = getCodeFix(feature, client.id, framework);
|
|
10882
|
-
addWarning(__spreadValues({
|
|
11354
|
+
addWarning(__spreadValues(__spreadValues({
|
|
10883
11355
|
severity: baseSeverity,
|
|
10884
11356
|
client: client.id,
|
|
10885
11357
|
property: feature,
|
|
@@ -10887,11 +11359,11 @@ function analyzeEmailFromDom($, framework) {
|
|
|
10887
11359
|
suggestion: sug.text,
|
|
10888
11360
|
fix,
|
|
10889
11361
|
fixType: getFixType(feature)
|
|
10890
|
-
}, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(feature, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
11362
|
+
}, featureOccurrences ? occurrenceFields(featureOccurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(feature, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
10891
11363
|
} else if (support === "partial" && feature === "<style>") {
|
|
10892
11364
|
const sug = getSuggestion("<style>:partial", client.id, framework);
|
|
10893
11365
|
const fix = getCodeFix("<style>", client.id, framework);
|
|
10894
|
-
addWarning(__spreadValues({
|
|
11366
|
+
addWarning(__spreadValues(__spreadValues({
|
|
10895
11367
|
severity: "warning",
|
|
10896
11368
|
client: client.id,
|
|
10897
11369
|
property: "<style>",
|
|
@@ -10899,55 +11371,97 @@ function analyzeEmailFromDom($, framework) {
|
|
|
10899
11371
|
suggestion: sug.text,
|
|
10900
11372
|
fix,
|
|
10901
11373
|
fixType: getFixType("<style>")
|
|
10902
|
-
}, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<style>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
11374
|
+
}, featureOccurrences ? occurrenceFields(featureOccurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<style>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
10903
11375
|
}
|
|
10904
11376
|
}
|
|
10905
11377
|
}
|
|
10906
11378
|
const parsedAtRules = /* @__PURE__ */ new Set();
|
|
11379
|
+
const selectorLocs = /* @__PURE__ */ new Map();
|
|
10907
11380
|
const parsedProperties = /* @__PURE__ */ new Set();
|
|
10908
11381
|
const propertyLines = /* @__PURE__ */ new Map();
|
|
11382
|
+
const propertyLocs = /* @__PURE__ */ new Map();
|
|
10909
11383
|
const propertyValues = /* @__PURE__ */ new Map();
|
|
10910
11384
|
const detectedCssFunctions = /* @__PURE__ */ new Set();
|
|
10911
11385
|
const detectedPseudoClasses = /* @__PURE__ */ new Set();
|
|
10912
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
|
+
}
|
|
10913
11420
|
$("style").each((_, el) => {
|
|
10914
11421
|
const cssText = $(el).text();
|
|
11422
|
+
blockAnchor = cssBlockAnchor(el, cssText, source);
|
|
10915
11423
|
try {
|
|
10916
11424
|
const ast = csstree5.parse(cssText, { parseCustomProperty: true, positions: true });
|
|
10917
11425
|
csstree5.walk(ast, {
|
|
10918
11426
|
enter(node) {
|
|
10919
11427
|
if (node.type === "Atrule") {
|
|
10920
11428
|
parsedAtRules.add(`@${node.name}`);
|
|
11429
|
+
recordSelectorLoc(`@${node.name}`, node.loc);
|
|
10921
11430
|
}
|
|
10922
11431
|
if (node.type === "PseudoClassSelector") {
|
|
10923
11432
|
detectedPseudoClasses.add(`:${node.name}`);
|
|
11433
|
+
recordSelectorLoc(`:${node.name}`, node.loc);
|
|
10924
11434
|
}
|
|
10925
11435
|
if (node.type === "PseudoElementSelector") {
|
|
10926
11436
|
detectedPseudoElements.add(`::${node.name}`);
|
|
11437
|
+
recordSelectorLoc(`::${node.name}`, node.loc);
|
|
10927
11438
|
}
|
|
10928
11439
|
if (node.type === "Declaration") {
|
|
10929
11440
|
const prop = node.property.toLowerCase();
|
|
10930
11441
|
parsedProperties.add(prop);
|
|
10931
|
-
if (node.loc && !propertyLines.has(prop)) {
|
|
10932
|
-
propertyLines.set(prop, node.loc.start.line);
|
|
10933
|
-
}
|
|
10934
11442
|
const valueStr = csstree5.generate(node.value);
|
|
10935
11443
|
const seenValues = propertyValues.get(prop);
|
|
10936
11444
|
if (seenValues) seenValues.push(valueStr);
|
|
10937
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
|
+
}
|
|
10938
11450
|
for (const det of COMPOUND_DETECTORS) {
|
|
10939
|
-
if (prop === det.property && valueStr.includes(det.valueIncludes)) {
|
|
11451
|
+
if (prop === det.property && valueStr.toLowerCase().includes(det.valueIncludes)) {
|
|
10940
11452
|
parsedProperties.add(det.key);
|
|
10941
|
-
if (node.loc
|
|
10942
|
-
propertyLines.set(det.key, node.loc.start.line);
|
|
11453
|
+
if (node.loc) {
|
|
11454
|
+
if (!propertyLines.has(det.key)) propertyLines.set(det.key, node.loc.start.line);
|
|
11455
|
+
recordLoc(det.key, node.loc);
|
|
10943
11456
|
}
|
|
10944
11457
|
}
|
|
10945
11458
|
}
|
|
10946
11459
|
for (const fn of CSS_FUNCTION_DETECTORS) {
|
|
10947
11460
|
if (valueStr.includes(fn.pattern)) {
|
|
10948
11461
|
detectedCssFunctions.add(fn.key);
|
|
10949
|
-
if (node.loc
|
|
10950
|
-
propertyLines.set(fn.key, node.loc.start.line);
|
|
11462
|
+
if (node.loc) {
|
|
11463
|
+
if (!propertyLines.has(fn.key)) propertyLines.set(fn.key, node.loc.start.line);
|
|
11464
|
+
recordLoc(fn.key, node.loc);
|
|
10951
11465
|
}
|
|
10952
11466
|
}
|
|
10953
11467
|
}
|
|
@@ -10959,33 +11473,42 @@ function analyzeEmailFromDom($, framework) {
|
|
|
10959
11473
|
});
|
|
10960
11474
|
for (const atRule of AT_RULE_FEATURES) {
|
|
10961
11475
|
if (!parsedAtRules.has(atRule)) continue;
|
|
10962
|
-
checkPropertySupport(atRule, addWarning, framework);
|
|
11476
|
+
checkPropertySupport(atRule, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(atRule));
|
|
10963
11477
|
}
|
|
10964
11478
|
const cssPropertiesToCheck = Object.keys(CSS_SUPPORT).filter(
|
|
10965
11479
|
(k) => !k.startsWith("<") && !k.startsWith("@")
|
|
10966
11480
|
);
|
|
10967
11481
|
$("[style]").each((_, el) => {
|
|
10968
|
-
var _a;
|
|
10969
11482
|
const style = $(el).attr("style") || "";
|
|
10970
11483
|
const props = parseStyleProperties(style);
|
|
10971
11484
|
const selector = describeSelector2(el);
|
|
11485
|
+
const locs = elementLocs(locOfAttr(el, "style"));
|
|
10972
11486
|
for (const prop of props) {
|
|
10973
11487
|
for (const det of COMPOUND_DETECTORS) {
|
|
10974
11488
|
if (prop === det.property) {
|
|
10975
11489
|
const value2 = getStyleValue(style, prop);
|
|
10976
|
-
if (value2 == null ? void 0 : value2.includes(det.valueIncludes)) {
|
|
10977
|
-
checkPropertySupport(det.key, addWarning, framework, selector);
|
|
11490
|
+
if (value2 == null ? void 0 : value2.toLowerCase().includes(det.valueIncludes)) {
|
|
11491
|
+
checkPropertySupport(det.key, addWarning, framework, selector, void 0, void 0, locs);
|
|
10978
11492
|
}
|
|
10979
11493
|
}
|
|
10980
11494
|
}
|
|
10981
11495
|
if (cssPropertiesToCheck.includes(prop)) {
|
|
10982
|
-
|
|
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
|
+
);
|
|
10983
11506
|
}
|
|
10984
11507
|
const value = getStyleValue(style, prop);
|
|
10985
11508
|
if (value) {
|
|
10986
11509
|
for (const fn of CSS_FUNCTION_DETECTORS) {
|
|
10987
11510
|
if (value.includes(fn.pattern)) {
|
|
10988
|
-
checkPropertySupport(fn.key, addWarning, framework, selector);
|
|
11511
|
+
checkPropertySupport(fn.key, addWarning, framework, selector, void 0, void 0, locs);
|
|
10989
11512
|
}
|
|
10990
11513
|
}
|
|
10991
11514
|
}
|
|
@@ -11001,87 +11524,66 @@ function analyzeEmailFromDom($, framework) {
|
|
|
11001
11524
|
framework,
|
|
11002
11525
|
void 0,
|
|
11003
11526
|
propertyLines.get(prop),
|
|
11004
|
-
values
|
|
11527
|
+
values,
|
|
11528
|
+
propertyLocs.get(prop)
|
|
11005
11529
|
);
|
|
11006
11530
|
}
|
|
11007
11531
|
for (const compound of COMPOUND_VALUE_FEATURES) {
|
|
11008
11532
|
if (compound.startsWith(":") || compound.startsWith("::")) continue;
|
|
11009
11533
|
if (parsedProperties.has(compound)) {
|
|
11010
|
-
checkPropertySupport(compound, addWarning, framework, void 0, propertyLines.get(compound));
|
|
11534
|
+
checkPropertySupport(compound, addWarning, framework, void 0, propertyLines.get(compound), void 0, propertyLocs.get(compound));
|
|
11011
11535
|
}
|
|
11012
11536
|
}
|
|
11013
11537
|
for (const pseudo of detectedPseudoClasses) {
|
|
11014
11538
|
if (CSS_SUPPORT[pseudo]) {
|
|
11015
|
-
checkPropertySupport(pseudo, addWarning, framework);
|
|
11539
|
+
checkPropertySupport(pseudo, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(pseudo));
|
|
11016
11540
|
}
|
|
11017
11541
|
}
|
|
11018
11542
|
for (const pseudo of detectedPseudoElements) {
|
|
11019
11543
|
if (CSS_SUPPORT[pseudo]) {
|
|
11020
|
-
checkPropertySupport(pseudo, addWarning, framework);
|
|
11544
|
+
checkPropertySupport(pseudo, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(pseudo));
|
|
11021
11545
|
}
|
|
11022
11546
|
}
|
|
11023
11547
|
for (const fn of detectedCssFunctions) {
|
|
11024
|
-
checkPropertySupport(fn, addWarning, framework, void 0, propertyLines.get(fn));
|
|
11548
|
+
checkPropertySupport(fn, addWarning, framework, void 0, propertyLines.get(fn), void 0, propertyLocs.get(fn));
|
|
11025
11549
|
}
|
|
11026
11550
|
for (const w of checkDarkModeFromDom($)) addWarning(w);
|
|
11027
11551
|
const severityOrder = { error: 0, warning: 1, info: 2 };
|
|
11028
11552
|
warnings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
11029
11553
|
return warnings;
|
|
11030
11554
|
}
|
|
11031
|
-
function analyzeEmail(html, framework) {
|
|
11555
|
+
function analyzeEmail(html, framework, options) {
|
|
11032
11556
|
if (!html || !html.trim()) {
|
|
11033
11557
|
return [];
|
|
11034
11558
|
}
|
|
11035
11559
|
if (html.length > MAX_HTML_SIZE) {
|
|
11036
11560
|
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
11037
11561
|
}
|
|
11038
|
-
const $ =
|
|
11039
|
-
return analyzeEmailFromDom($, framework);
|
|
11562
|
+
const $ = loadHtml(html, options);
|
|
11563
|
+
return analyzeEmailFromDom($, framework, (options == null ? void 0 : options.positions) ? html : void 0);
|
|
11040
11564
|
}
|
|
11041
11565
|
function getFixType(prop) {
|
|
11042
11566
|
return STRUCTURAL_FIX_PROPERTIES.has(prop) ? "structural" : "css";
|
|
11043
11567
|
}
|
|
11044
|
-
var VALUE_CAVEAT_PROPS = /* @__PURE__ */ new Set(["margin", "position", "overflow"]);
|
|
11045
|
-
var POSITION_KEYWORDS = ["relative", "absolute", "fixed", "sticky"];
|
|
11046
|
-
function valueTriggersCaveat(prop, value, notes) {
|
|
11047
|
-
const note = (notes != null ? notes : []).join(" ");
|
|
11048
|
-
const noteLc = note.toLowerCase();
|
|
11049
|
-
if (prop === "margin") {
|
|
11050
|
-
if (/(?:^|[\s:(])-\.?\d/.test(value) && noteLc.includes("negative")) return true;
|
|
11051
|
-
if (/\bauto\b/.test(value) && noteLc.includes("auto")) return true;
|
|
11052
|
-
return false;
|
|
11053
|
-
}
|
|
11054
|
-
if (prop === "position") {
|
|
11055
|
-
const used = POSITION_KEYWORDS.find((k) => new RegExp(`\\b${k}\\b`).test(value));
|
|
11056
|
-
if (!used) return false;
|
|
11057
|
-
const m = note.match(/supports\s+.+?\s+but not\s+([^.]+)/i);
|
|
11058
|
-
if (m) return m[1].toLowerCase().includes(used);
|
|
11059
|
-
return used === "fixed" || used === "sticky";
|
|
11060
|
-
}
|
|
11061
|
-
if (prop === "overflow") {
|
|
11062
|
-
if (!/\b(?:auto|scroll)\b/.test(value)) return false;
|
|
11063
|
-
return noteLc.includes("cannot scroll");
|
|
11064
|
-
}
|
|
11065
|
-
return true;
|
|
11066
|
-
}
|
|
11067
11568
|
function noteSuffix(notes) {
|
|
11068
11569
|
if (!(notes == null ? void 0 : notes.length)) return "";
|
|
11069
11570
|
const cleaned = notes.map((n) => n.replace(/^(?:Partial|Buggy|Not supported)\.\s*/i, "").trim()).filter(Boolean);
|
|
11070
11571
|
return cleaned.length ? ` ${cleaned.join(" ")}` : "";
|
|
11071
11572
|
}
|
|
11072
|
-
function checkPropertySupport(prop, addWarning, framework, selector, line,
|
|
11073
|
-
var _a;
|
|
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;
|
|
11074
11577
|
const supportData = CSS_SUPPORT[prop];
|
|
11075
11578
|
if (!supportData) return;
|
|
11076
11579
|
const fixType = getFixType(prop);
|
|
11077
|
-
const valueGated = VALUE_CAVEAT_PROPS.has(prop);
|
|
11078
11580
|
for (const client of EMAIL_CLIENTS) {
|
|
11079
11581
|
const support = supportData[client.id] || "unknown";
|
|
11080
|
-
const notes = (
|
|
11582
|
+
const notes = (_b = CSS_SUPPORT_NOTES[prop]) == null ? void 0 : _b[client.id];
|
|
11081
11583
|
if (support === "unsupported") {
|
|
11082
11584
|
const sug = getSuggestion(prop, client.id, framework);
|
|
11083
11585
|
const fix = getCodeFix(prop, client.id, framework);
|
|
11084
|
-
addWarning(__spreadValues(__spreadValues(__spreadValues({
|
|
11586
|
+
addWarning(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
|
|
11085
11587
|
severity: "warning",
|
|
11086
11588
|
client: client.id,
|
|
11087
11589
|
property: prop,
|
|
@@ -11089,12 +11591,13 @@ function checkPropertySupport(prop, addWarning, framework, selector, line, value
|
|
|
11089
11591
|
suggestion: sug.text,
|
|
11090
11592
|
fix,
|
|
11091
11593
|
fixType
|
|
11092
|
-
}, selector ? { selector } : {}),
|
|
11594
|
+
}, selector ? { selector } : {}), reportedLine !== void 0 ? { line: reportedLine } : {}), occurrences ? occurrenceFields(occurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
11093
11595
|
} else if (support === "partial") {
|
|
11094
|
-
if (
|
|
11596
|
+
if (!caveatApplies(prop, values, notes)) continue;
|
|
11597
|
+
const hits = triggeringOccurrences(prop, occurrences, notes);
|
|
11095
11598
|
const sug = getSuggestion(prop, client.id, framework);
|
|
11096
11599
|
const fix = getCodeFix(prop, client.id, framework);
|
|
11097
|
-
addWarning(__spreadValues(__spreadValues(__spreadValues({
|
|
11600
|
+
addWarning(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
|
|
11098
11601
|
severity: "info",
|
|
11099
11602
|
client: client.id,
|
|
11100
11603
|
property: prop,
|
|
@@ -11102,7 +11605,7 @@ function checkPropertySupport(prop, addWarning, framework, selector, line, value
|
|
|
11102
11605
|
suggestion: sug.text,
|
|
11103
11606
|
fix,
|
|
11104
11607
|
fixType
|
|
11105
|
-
}, selector ? { selector } : {}), line !== void 0 ? { line } : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
|
|
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 } : {}));
|
|
11106
11609
|
}
|
|
11107
11610
|
}
|
|
11108
11611
|
}
|
|
@@ -11121,6 +11624,19 @@ function generateCompatibilityScore(warnings) {
|
|
|
11121
11624
|
}
|
|
11122
11625
|
return result;
|
|
11123
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
|
+
}
|
|
11124
11640
|
function warningsForClient(warnings, clientId) {
|
|
11125
11641
|
return warnings.filter((w) => w.client === clientId);
|
|
11126
11642
|
}
|
|
@@ -11461,16 +11977,6 @@ function extractCode(response) {
|
|
|
11461
11977
|
return response.trim();
|
|
11462
11978
|
}
|
|
11463
11979
|
|
|
11464
|
-
// src/parse-html.ts
|
|
11465
|
-
import * as cheerio5 from "cheerio";
|
|
11466
|
-
function fromHtml(html, empty, fn) {
|
|
11467
|
-
if (!html || !html.trim()) return empty;
|
|
11468
|
-
if (html.length > MAX_HTML_SIZE) {
|
|
11469
|
-
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
11470
|
-
}
|
|
11471
|
-
return fn(cheerio5.load(html), html);
|
|
11472
|
-
}
|
|
11473
|
-
|
|
11474
11980
|
// src/spam-scorer.ts
|
|
11475
11981
|
var SPAM_TRIGGER_PHRASES = [
|
|
11476
11982
|
"act now",
|
|
@@ -11939,6 +12445,8 @@ function validateLinksFromDom($) {
|
|
|
11939
12445
|
const href = $(el).attr("href") || "";
|
|
11940
12446
|
const text = $(el).text().trim();
|
|
11941
12447
|
const category = classifyHref(href);
|
|
12448
|
+
const elLoc = locOfElement(el);
|
|
12449
|
+
const hrefLoc = href ? locOfAttr(el, "href") : elLoc;
|
|
11942
12450
|
switch (category) {
|
|
11943
12451
|
case "https":
|
|
11944
12452
|
breakdown.https++;
|
|
@@ -11969,95 +12477,95 @@ function validateLinksFromDom($) {
|
|
|
11969
12477
|
hrefCounts.set(href, (hrefCounts.get(href) || 0) + 1);
|
|
11970
12478
|
}
|
|
11971
12479
|
if (!href || !href.trim()) {
|
|
11972
|
-
issues.push({
|
|
12480
|
+
issues.push(__spreadValues({
|
|
11973
12481
|
severity: "error",
|
|
11974
12482
|
rule: "empty-href",
|
|
11975
12483
|
message: "Link has no href attribute",
|
|
11976
12484
|
text: text.slice(0, 80) || "(no text)"
|
|
11977
|
-
});
|
|
12485
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
11978
12486
|
return;
|
|
11979
12487
|
}
|
|
11980
12488
|
if (category === "javascript" && !isPlaceholderHref(href)) {
|
|
11981
|
-
issues.push({
|
|
12489
|
+
issues.push(__spreadValues({
|
|
11982
12490
|
severity: "error",
|
|
11983
12491
|
rule: "javascript-href",
|
|
11984
12492
|
message: "Link uses javascript: protocol",
|
|
11985
12493
|
href: href.slice(0, 100),
|
|
11986
12494
|
text: text.slice(0, 80) || "(no text)"
|
|
11987
|
-
});
|
|
12495
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
11988
12496
|
return;
|
|
11989
12497
|
}
|
|
11990
12498
|
if (isPlaceholderHref(href)) {
|
|
11991
|
-
issues.push({
|
|
12499
|
+
issues.push(__spreadValues({
|
|
11992
12500
|
severity: "warning",
|
|
11993
12501
|
rule: "placeholder-href",
|
|
11994
12502
|
message: "Link has a placeholder href (# or javascript:void)",
|
|
11995
12503
|
href,
|
|
11996
12504
|
text: text.slice(0, 80) || "(no text)"
|
|
11997
|
-
});
|
|
12505
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
11998
12506
|
return;
|
|
11999
12507
|
}
|
|
12000
12508
|
if (category === "http") {
|
|
12001
|
-
issues.push({
|
|
12509
|
+
issues.push(__spreadValues({
|
|
12002
12510
|
severity: "warning",
|
|
12003
12511
|
rule: "insecure-link",
|
|
12004
12512
|
message: "Link uses HTTP instead of HTTPS",
|
|
12005
12513
|
href: href.slice(0, 120),
|
|
12006
12514
|
text: text.slice(0, 80) || "(no text)"
|
|
12007
|
-
});
|
|
12515
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
12008
12516
|
}
|
|
12009
12517
|
if (category === "protocol-relative") {
|
|
12010
|
-
issues.push({
|
|
12518
|
+
issues.push(__spreadValues({
|
|
12011
12519
|
severity: "warning",
|
|
12012
12520
|
rule: "protocol-relative",
|
|
12013
12521
|
message: "Protocol-relative URL may break in email clients \u2014 use https:// explicitly",
|
|
12014
12522
|
href: href.slice(0, 120),
|
|
12015
12523
|
text: text.slice(0, 80) || "(no text)"
|
|
12016
|
-
});
|
|
12524
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
12017
12525
|
}
|
|
12018
12526
|
if (text && GENERIC_LINK_TEXT.has(text.toLowerCase())) {
|
|
12019
|
-
issues.push({
|
|
12527
|
+
issues.push(__spreadValues({
|
|
12020
12528
|
severity: "warning",
|
|
12021
12529
|
rule: "generic-link-text",
|
|
12022
12530
|
message: `Link text "${text}" is vague \u2014 use descriptive text for accessibility and engagement`,
|
|
12023
12531
|
href: href.slice(0, 120),
|
|
12024
12532
|
text
|
|
12025
|
-
});
|
|
12533
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12026
12534
|
}
|
|
12027
12535
|
if (!text && !$(el).attr("aria-label") && !$(el).find("img[alt]").length) {
|
|
12028
|
-
issues.push({
|
|
12536
|
+
issues.push(__spreadValues({
|
|
12029
12537
|
severity: "error",
|
|
12030
12538
|
rule: "empty-link-text",
|
|
12031
12539
|
message: "Link has no visible text or aria-label",
|
|
12032
12540
|
href: href.slice(0, 120)
|
|
12033
|
-
});
|
|
12541
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12034
12542
|
}
|
|
12035
12543
|
if (category === "mailto" && href.trim().toLowerCase() === "mailto:") {
|
|
12036
|
-
issues.push({
|
|
12544
|
+
issues.push(__spreadValues({
|
|
12037
12545
|
severity: "error",
|
|
12038
12546
|
rule: "empty-mailto",
|
|
12039
12547
|
message: "mailto: link has no email address",
|
|
12040
12548
|
href,
|
|
12041
12549
|
text: text.slice(0, 80) || "(no text)"
|
|
12042
|
-
});
|
|
12550
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
12043
12551
|
}
|
|
12044
12552
|
if (category === "tel" && href.trim().toLowerCase() === "tel:") {
|
|
12045
|
-
issues.push({
|
|
12553
|
+
issues.push(__spreadValues({
|
|
12046
12554
|
severity: "error",
|
|
12047
12555
|
rule: "empty-tel",
|
|
12048
12556
|
message: "tel: link has no phone number",
|
|
12049
12557
|
href,
|
|
12050
12558
|
text: text.slice(0, 80) || "(no text)"
|
|
12051
|
-
});
|
|
12559
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
12052
12560
|
}
|
|
12053
12561
|
if (href.length > 2e3) {
|
|
12054
|
-
issues.push({
|
|
12562
|
+
issues.push(__spreadValues({
|
|
12055
12563
|
severity: "info",
|
|
12056
12564
|
rule: "long-url",
|
|
12057
12565
|
message: "URL exceeds 2000 characters \u2014 may be truncated by some email clients",
|
|
12058
12566
|
href: href.slice(0, 120) + "...",
|
|
12059
12567
|
text: text.slice(0, 80) || "(no text)"
|
|
12060
|
-
});
|
|
12568
|
+
}, hrefLoc ? { loc: hrefLoc } : {}));
|
|
12061
12569
|
}
|
|
12062
12570
|
});
|
|
12063
12571
|
links.each((_, el) => {
|
|
@@ -12066,14 +12574,15 @@ function validateLinksFromDom($) {
|
|
|
12066
12574
|
if (trimmed.startsWith("#") && trimmed.length > 1) {
|
|
12067
12575
|
const targetId = trimmed.slice(1);
|
|
12068
12576
|
const target = $(`[id="${targetId}"]`);
|
|
12577
|
+
const anchorLoc = locOfAttr(el, "href");
|
|
12069
12578
|
if (target.length === 0) {
|
|
12070
|
-
issues.push({
|
|
12579
|
+
issues.push(__spreadValues({
|
|
12071
12580
|
severity: "error",
|
|
12072
12581
|
rule: "broken-anchor",
|
|
12073
12582
|
message: `Anchor link "${trimmed}" points to an element that does not exist`,
|
|
12074
12583
|
href: trimmed,
|
|
12075
12584
|
text: $(el).text().trim().slice(0, 80) || "(no text)"
|
|
12076
|
-
});
|
|
12585
|
+
}, anchorLoc ? { loc: anchorLoc } : {}));
|
|
12077
12586
|
}
|
|
12078
12587
|
}
|
|
12079
12588
|
});
|
|
@@ -12089,8 +12598,8 @@ function validateLinksFromDom($) {
|
|
|
12089
12598
|
}
|
|
12090
12599
|
return { totalLinks, issues, breakdown };
|
|
12091
12600
|
}
|
|
12092
|
-
function validateLinks(html) {
|
|
12093
|
-
return fromHtml(html, EMPTY_LINKS, validateLinksFromDom);
|
|
12601
|
+
function validateLinks(html, options) {
|
|
12602
|
+
return fromHtml(html, EMPTY_LINKS, validateLinksFromDom, options);
|
|
12094
12603
|
}
|
|
12095
12604
|
|
|
12096
12605
|
// src/accessibility-checker.ts
|
|
@@ -12115,24 +12624,31 @@ function describeElement($, el) {
|
|
|
12115
12624
|
function checkLangAttribute($) {
|
|
12116
12625
|
const lang = $("html").attr("lang");
|
|
12117
12626
|
if (!lang || !lang.trim()) {
|
|
12118
|
-
|
|
12627
|
+
const loc = locOfFirst($, "html");
|
|
12628
|
+
return __spreadProps(__spreadValues({
|
|
12119
12629
|
severity: "error",
|
|
12120
12630
|
rule: "missing-lang",
|
|
12121
|
-
message: "Missing lang attribute on <html> element"
|
|
12631
|
+
message: "Missing lang attribute on <html> element"
|
|
12632
|
+
}, loc ? { loc } : {}), {
|
|
12122
12633
|
details: 'Screen readers use the lang attribute to determine pronunciation. Add lang="en" (or appropriate language code).'
|
|
12123
|
-
};
|
|
12634
|
+
});
|
|
12124
12635
|
}
|
|
12125
12636
|
return null;
|
|
12126
12637
|
}
|
|
12638
|
+
function titleLoc($) {
|
|
12639
|
+
return $("title").length ? locOfFirst($, "title") : locOfFirst($, "head");
|
|
12640
|
+
}
|
|
12127
12641
|
function checkTitle($) {
|
|
12128
12642
|
const title = $("title").text().trim();
|
|
12129
12643
|
if (!title) {
|
|
12130
|
-
|
|
12644
|
+
const loc = titleLoc($);
|
|
12645
|
+
return __spreadProps(__spreadValues({
|
|
12131
12646
|
severity: "warning",
|
|
12132
12647
|
rule: "missing-title",
|
|
12133
|
-
message: "Missing or empty <title> element"
|
|
12648
|
+
message: "Missing or empty <title> element"
|
|
12649
|
+
}, loc ? { loc } : {}), {
|
|
12134
12650
|
details: "The <title> helps screen readers identify the email content."
|
|
12135
|
-
};
|
|
12651
|
+
});
|
|
12136
12652
|
}
|
|
12137
12653
|
return null;
|
|
12138
12654
|
}
|
|
@@ -12142,34 +12658,38 @@ function checkImageAlt($) {
|
|
|
12142
12658
|
const alt = $(el).attr("alt");
|
|
12143
12659
|
const src = $(el).attr("src") || "";
|
|
12144
12660
|
const role = $(el).attr("role");
|
|
12661
|
+
const elLoc = locOfElement(el);
|
|
12145
12662
|
if (role === "presentation" || role === "none") return;
|
|
12146
12663
|
if (alt === void 0) {
|
|
12147
|
-
issues.push({
|
|
12664
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12148
12665
|
severity: "error",
|
|
12149
12666
|
rule: "img-missing-alt",
|
|
12150
12667
|
message: "Image missing alt attribute",
|
|
12151
|
-
element: describeElement($, el)
|
|
12668
|
+
element: describeElement($, el)
|
|
12669
|
+
}, elLoc ? { loc: elLoc } : {}), {
|
|
12152
12670
|
details: 'Every image must have an alt attribute. Use alt="" for decorative images.'
|
|
12153
|
-
});
|
|
12671
|
+
}));
|
|
12154
12672
|
} else if (alt.trim() === "") {
|
|
12155
12673
|
const isLikelyContent = !src.includes("spacer") && !src.includes("pixel") && !src.includes("tracking") && !src.includes("1x1") && !src.includes("transparent");
|
|
12156
12674
|
if (isLikelyContent && ($(el).attr("width") || "0") !== "1") {
|
|
12157
|
-
issues.push({
|
|
12675
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12158
12676
|
severity: "info",
|
|
12159
12677
|
rule: "img-empty-alt",
|
|
12160
12678
|
message: "Image has empty alt text \u2014 verify it is decorative",
|
|
12161
|
-
element: describeElement($, el)
|
|
12679
|
+
element: describeElement($, el)
|
|
12680
|
+
}, locOfAttr(el, "alt") ? { loc: locOfAttr(el, "alt") } : {}), {
|
|
12162
12681
|
details: "Empty alt is correct for decorative images, but content images need descriptive alt text."
|
|
12163
|
-
});
|
|
12682
|
+
}));
|
|
12164
12683
|
}
|
|
12165
12684
|
} else if (/\.(png|jpg|jpeg|gif|svg|webp|bmp)$/i.test(alt)) {
|
|
12166
|
-
issues.push({
|
|
12685
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12167
12686
|
severity: "error",
|
|
12168
12687
|
rule: "img-filename-alt",
|
|
12169
12688
|
message: "Image alt text is a filename, not a description",
|
|
12170
|
-
element: describeElement($, el)
|
|
12689
|
+
element: describeElement($, el)
|
|
12690
|
+
}, locOfAttr(el, "alt") ? { loc: locOfAttr(el, "alt") } : {}), {
|
|
12171
12691
|
details: `Alt "${alt}" should describe the image content, not the file name.`
|
|
12172
|
-
});
|
|
12692
|
+
}));
|
|
12173
12693
|
}
|
|
12174
12694
|
});
|
|
12175
12695
|
return issues;
|
|
@@ -12177,28 +12697,31 @@ function checkImageAlt($) {
|
|
|
12177
12697
|
function checkLinkAccessibility($) {
|
|
12178
12698
|
const issues = [];
|
|
12179
12699
|
$("a").each((_, el) => {
|
|
12700
|
+
const elLoc = locOfElement(el);
|
|
12180
12701
|
const text = $(el).text().trim().toLowerCase();
|
|
12181
12702
|
const ariaLabel = $(el).attr("aria-label");
|
|
12182
12703
|
const title = $(el).attr("title");
|
|
12183
12704
|
const imgAlt = $(el).find("img").attr("alt");
|
|
12184
12705
|
if (!text && !ariaLabel && !title && !imgAlt) {
|
|
12185
|
-
issues.push({
|
|
12706
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12186
12707
|
severity: "error",
|
|
12187
12708
|
rule: "link-no-accessible-name",
|
|
12188
12709
|
message: "Link has no accessible name",
|
|
12189
|
-
element: describeElement($, el)
|
|
12710
|
+
element: describeElement($, el)
|
|
12711
|
+
}, elLoc ? { loc: elLoc } : {}), {
|
|
12190
12712
|
details: "Links need visible text, aria-label, or an image with alt text."
|
|
12191
|
-
});
|
|
12713
|
+
}));
|
|
12192
12714
|
return;
|
|
12193
12715
|
}
|
|
12194
12716
|
if (text && GENERIC_LINK_TEXT.has(text) && !ariaLabel) {
|
|
12195
|
-
issues.push({
|
|
12717
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12196
12718
|
severity: "warning",
|
|
12197
12719
|
rule: "link-generic-text",
|
|
12198
12720
|
message: `Link text "${$(el).text().trim()}" is not descriptive`,
|
|
12199
|
-
element: describeElement($, el)
|
|
12721
|
+
element: describeElement($, el)
|
|
12722
|
+
}, elLoc ? { loc: elLoc } : {}), {
|
|
12200
12723
|
details: "Screen readers often list links out of context. Use text that describes the destination."
|
|
12201
|
-
});
|
|
12724
|
+
}));
|
|
12202
12725
|
}
|
|
12203
12726
|
});
|
|
12204
12727
|
return issues;
|
|
@@ -12208,18 +12731,20 @@ function checkTableAccessibility($) {
|
|
|
12208
12731
|
$("table").each((_, el) => {
|
|
12209
12732
|
if ($(el).parents('table[role="presentation"], table[role="none"]').length > 0) return;
|
|
12210
12733
|
const role = $(el).attr("role");
|
|
12734
|
+
const tableLoc = locOfElement(el);
|
|
12211
12735
|
const hasHeaders = $(el).find("th").length > 0;
|
|
12212
12736
|
const looksLikeLayout = !hasHeaders;
|
|
12213
12737
|
if (looksLikeLayout && role !== "presentation" && role !== "none") {
|
|
12214
12738
|
const nestedTables = $(el).find("table").length;
|
|
12215
12739
|
if (nestedTables > 0 || $(el).find("td").length > 2) {
|
|
12216
|
-
issues.push({
|
|
12740
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12217
12741
|
severity: "info",
|
|
12218
12742
|
rule: "table-missing-role",
|
|
12219
|
-
message: 'Layout table missing role="presentation"'
|
|
12743
|
+
message: 'Layout table missing role="presentation"'
|
|
12744
|
+
}, tableLoc ? { loc: tableLoc } : {}), {
|
|
12220
12745
|
element: `<table> with ${$(el).find("td").length} cells`,
|
|
12221
12746
|
details: `Add role="presentation" to tables used for layout so screen readers don't announce them as data tables.`
|
|
12222
|
-
});
|
|
12747
|
+
}));
|
|
12223
12748
|
}
|
|
12224
12749
|
}
|
|
12225
12750
|
});
|
|
@@ -12230,6 +12755,7 @@ function checkTextSizeAndContrast($) {
|
|
|
12230
12755
|
let smallTextCount = 0;
|
|
12231
12756
|
$("[style]").each((_, el) => {
|
|
12232
12757
|
const style = $(el).attr("style") || "";
|
|
12758
|
+
const styleLoc = locOfAttr(el, "style");
|
|
12233
12759
|
const fontSizeMatch = style.match(/font-size\s*:\s*(\d+(?:\.\d+)?)(px|pt)/i);
|
|
12234
12760
|
if (fontSizeMatch) {
|
|
12235
12761
|
const size = parseFloat(fontSizeMatch[1]);
|
|
@@ -12238,13 +12764,14 @@ function checkTextSizeAndContrast($) {
|
|
|
12238
12764
|
if (pxSize < 9 && pxSize > 0) {
|
|
12239
12765
|
smallTextCount++;
|
|
12240
12766
|
if (smallTextCount <= 3) {
|
|
12241
|
-
issues.push({
|
|
12767
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12242
12768
|
severity: "warning",
|
|
12243
12769
|
rule: "small-text",
|
|
12244
12770
|
message: `Very small text (${fontSizeMatch[0].trim()})`,
|
|
12245
|
-
element: describeElement($, el)
|
|
12771
|
+
element: describeElement($, el)
|
|
12772
|
+
}, styleLoc ? { loc: styleLoc } : {}), {
|
|
12246
12773
|
details: "Text smaller than 9px is difficult to read, especially on mobile devices."
|
|
12247
|
-
});
|
|
12774
|
+
}));
|
|
12248
12775
|
}
|
|
12249
12776
|
}
|
|
12250
12777
|
}
|
|
@@ -12289,21 +12816,23 @@ function checkTextSizeAndContrast($) {
|
|
|
12289
12816
|
}
|
|
12290
12817
|
const grade = wcagGrade(ratio);
|
|
12291
12818
|
if (grade === "Fail") {
|
|
12292
|
-
issues.push({
|
|
12819
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12293
12820
|
severity: "error",
|
|
12294
12821
|
rule: "low-contrast",
|
|
12295
12822
|
message: `Low contrast ratio ${ratio.toFixed(1)}:1 \u2014 fails WCAG minimum`,
|
|
12296
|
-
element: describeElement($, el)
|
|
12823
|
+
element: describeElement($, el)
|
|
12824
|
+
}, styleLoc ? { loc: styleLoc } : {}), {
|
|
12297
12825
|
details: `Foreground ${colorValue} on background needs at least ${isLargeText ? "3:1" : "4.5:1"} contrast ratio.`
|
|
12298
|
-
});
|
|
12826
|
+
}));
|
|
12299
12827
|
} else if (!isLargeText && grade === "AA Large") {
|
|
12300
|
-
issues.push({
|
|
12828
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12301
12829
|
severity: "warning",
|
|
12302
12830
|
rule: "low-contrast",
|
|
12303
12831
|
message: `Low contrast ratio ${ratio.toFixed(1)}:1 \u2014 fails WCAG AA for normal text`,
|
|
12304
|
-
element: describeElement($, el)
|
|
12832
|
+
element: describeElement($, el)
|
|
12833
|
+
}, styleLoc ? { loc: styleLoc } : {}), {
|
|
12305
12834
|
details: `Foreground ${colorValue} on background needs at least 4.5:1 for normal-sized text.`
|
|
12306
|
-
});
|
|
12835
|
+
}));
|
|
12307
12836
|
}
|
|
12308
12837
|
}
|
|
12309
12838
|
}
|
|
@@ -12326,29 +12855,32 @@ function checkCharsetDeclaration($) {
|
|
|
12326
12855
|
const content = httpEquiv.attr("content") || "";
|
|
12327
12856
|
if (/charset\s*=/i.test(content)) return null;
|
|
12328
12857
|
}
|
|
12329
|
-
|
|
12858
|
+
const loc = locOfFirst($, "head");
|
|
12859
|
+
return __spreadProps(__spreadValues({
|
|
12330
12860
|
severity: "warning",
|
|
12331
12861
|
rule: "missing-charset",
|
|
12332
|
-
message: "Missing charset declaration"
|
|
12862
|
+
message: "Missing charset declaration"
|
|
12863
|
+
}, loc ? { loc } : {}), {
|
|
12333
12864
|
details: 'Add <meta charset="utf-8"> in <head> to prevent encoding issues across email clients.'
|
|
12334
|
-
};
|
|
12865
|
+
});
|
|
12335
12866
|
}
|
|
12336
12867
|
function checkSemanticStructure($) {
|
|
12337
12868
|
const issues = [];
|
|
12338
12869
|
const headings = [];
|
|
12339
12870
|
$("h1, h2, h3, h4, h5, h6").each((_, el) => {
|
|
12340
12871
|
const level = parseInt(el.tagName.replace(/h/i, ""), 10);
|
|
12341
|
-
headings.push({ level, text: $(el).text().trim().slice(0, 60) });
|
|
12872
|
+
headings.push({ level, text: $(el).text().trim().slice(0, 60), loc: locOfElement(el) });
|
|
12342
12873
|
});
|
|
12343
12874
|
for (let i = 1; i < headings.length; i++) {
|
|
12344
12875
|
const gap = headings[i].level - headings[i - 1].level;
|
|
12345
12876
|
if (gap > 1) {
|
|
12346
|
-
issues.push({
|
|
12877
|
+
issues.push(__spreadProps(__spreadValues({
|
|
12347
12878
|
severity: "info",
|
|
12348
12879
|
rule: "heading-skip",
|
|
12349
|
-
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 } : {}), {
|
|
12350
12882
|
details: "Skipped heading levels can confuse screen readers. Use sequential heading levels."
|
|
12351
|
-
});
|
|
12883
|
+
}));
|
|
12352
12884
|
break;
|
|
12353
12885
|
}
|
|
12354
12886
|
}
|
|
@@ -12389,8 +12921,8 @@ function checkAccessibilityFromDom($) {
|
|
|
12389
12921
|
const score = Math.max(0, 100 - penalty);
|
|
12390
12922
|
return { score, issues };
|
|
12391
12923
|
}
|
|
12392
|
-
function checkAccessibility(html) {
|
|
12393
|
-
return fromHtml(html, EMPTY_ACCESSIBILITY, checkAccessibilityFromDom);
|
|
12924
|
+
function checkAccessibility(html, options) {
|
|
12925
|
+
return fromHtml(html, EMPTY_ACCESSIBILITY, checkAccessibilityFromDom, options);
|
|
12394
12926
|
}
|
|
12395
12927
|
|
|
12396
12928
|
// src/image-analyzer.ts
|
|
@@ -12437,6 +12969,8 @@ function analyzeImagesFromDom($) {
|
|
|
12437
12969
|
const height = (_c = img.attr("height")) != null ? _c : null;
|
|
12438
12970
|
const style = (img.attr("style") || "").toLowerCase();
|
|
12439
12971
|
const imgIssues = [];
|
|
12972
|
+
const elLoc = locOfElement(el);
|
|
12973
|
+
const srcLoc = src ? locOfAttr(el, "src") : elLoc;
|
|
12440
12974
|
const tracking = isTrackingPixel(img);
|
|
12441
12975
|
let dataUriBytes = 0;
|
|
12442
12976
|
if (src.startsWith("data:")) {
|
|
@@ -12460,59 +12994,59 @@ function analyzeImagesFromDom($) {
|
|
|
12460
12994
|
const hasStyleHeight = /height\s*:/.test(style);
|
|
12461
12995
|
if (!hasStyleWidth && !hasStyleHeight) {
|
|
12462
12996
|
imgIssues.push("missing-dimensions");
|
|
12463
|
-
issues.push({
|
|
12997
|
+
issues.push(__spreadValues({
|
|
12464
12998
|
rule: "missing-dimensions",
|
|
12465
12999
|
severity: "warning",
|
|
12466
13000
|
message: "Image missing width/height attributes \u2014 causes layout shifts and Outlook rendering issues.",
|
|
12467
13001
|
src: truncateSrc(src)
|
|
12468
|
-
});
|
|
13002
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12469
13003
|
}
|
|
12470
13004
|
}
|
|
12471
13005
|
if (dataUriBytes > DATA_URI_WARN_BYTES) {
|
|
12472
13006
|
const kb = Math.round(dataUriBytes / 1024);
|
|
12473
13007
|
imgIssues.push("large-data-uri");
|
|
12474
|
-
issues.push({
|
|
13008
|
+
issues.push(__spreadValues({
|
|
12475
13009
|
rule: "large-data-uri",
|
|
12476
13010
|
severity: "warning",
|
|
12477
13011
|
message: `Data URI is ${kb}KB \u2014 consider hosting the image externally to reduce email size.`,
|
|
12478
13012
|
src: truncateSrc(src)
|
|
12479
|
-
});
|
|
13013
|
+
}, srcLoc ? { loc: srcLoc } : {}));
|
|
12480
13014
|
}
|
|
12481
13015
|
if (alt === null) {
|
|
12482
13016
|
imgIssues.push("missing-alt");
|
|
12483
|
-
issues.push({
|
|
13017
|
+
issues.push(__spreadValues({
|
|
12484
13018
|
rule: "missing-alt",
|
|
12485
13019
|
severity: "warning",
|
|
12486
13020
|
message: "Image missing alt attribute \u2014 hurts deliverability and accessibility.",
|
|
12487
13021
|
src: truncateSrc(src)
|
|
12488
|
-
});
|
|
13022
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12489
13023
|
}
|
|
12490
13024
|
if (src.toLowerCase().endsWith(".webp") || src.includes("image/webp")) {
|
|
12491
13025
|
imgIssues.push("webp-format");
|
|
12492
|
-
issues.push({
|
|
13026
|
+
issues.push(__spreadValues({
|
|
12493
13027
|
rule: "webp-format",
|
|
12494
13028
|
severity: "info",
|
|
12495
13029
|
message: "WebP format detected \u2014 not supported by all email clients. Consider PNG or JPEG.",
|
|
12496
13030
|
src: truncateSrc(src)
|
|
12497
|
-
});
|
|
13031
|
+
}, srcLoc ? { loc: srcLoc } : {}));
|
|
12498
13032
|
}
|
|
12499
13033
|
if (src.toLowerCase().endsWith(".svg") || src.includes("image/svg")) {
|
|
12500
13034
|
imgIssues.push("svg-format");
|
|
12501
|
-
issues.push({
|
|
13035
|
+
issues.push(__spreadValues({
|
|
12502
13036
|
rule: "svg-format",
|
|
12503
13037
|
severity: "info",
|
|
12504
13038
|
message: "SVG format detected \u2014 not supported by most email clients. Use PNG instead.",
|
|
12505
13039
|
src: truncateSrc(src)
|
|
12506
|
-
});
|
|
13040
|
+
}, srcLoc ? { loc: srcLoc } : {}));
|
|
12507
13041
|
}
|
|
12508
13042
|
if (!style.includes("display:block") && !style.includes("display: block")) {
|
|
12509
13043
|
imgIssues.push("missing-display-block");
|
|
12510
|
-
issues.push({
|
|
13044
|
+
issues.push(__spreadValues({
|
|
12511
13045
|
rule: "missing-display-block",
|
|
12512
13046
|
severity: "info",
|
|
12513
13047
|
message: "Image without display:block \u2014 may cause unwanted gaps in Outlook.",
|
|
12514
13048
|
src: truncateSrc(src)
|
|
12515
|
-
});
|
|
13049
|
+
}, elLoc ? { loc: elLoc } : {}));
|
|
12516
13050
|
}
|
|
12517
13051
|
images.push({
|
|
12518
13052
|
src: truncateSrc(src),
|
|
@@ -12550,8 +13084,8 @@ function analyzeImagesFromDom($) {
|
|
|
12550
13084
|
}
|
|
12551
13085
|
return { total: images.length, totalDataUriBytes, issues, images };
|
|
12552
13086
|
}
|
|
12553
|
-
function analyzeImages(html) {
|
|
12554
|
-
return fromHtml(html, EMPTY_IMAGES, analyzeImagesFromDom);
|
|
13087
|
+
function analyzeImages(html, options) {
|
|
13088
|
+
return fromHtml(html, EMPTY_IMAGES, analyzeImagesFromDom, options);
|
|
12555
13089
|
}
|
|
12556
13090
|
|
|
12557
13091
|
// src/inbox-preview.ts
|
|
@@ -12769,11 +13303,57 @@ function checkSize(html) {
|
|
|
12769
13303
|
return fromHtml(html, EMPTY_SIZE, checkSizeFromDom);
|
|
12770
13304
|
}
|
|
12771
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
|
+
|
|
12772
13325
|
// src/template-checker.ts
|
|
12773
|
-
function checkTemplateVariablesFromDom(
|
|
13326
|
+
function checkTemplateVariablesFromDom($, source) {
|
|
13327
|
+
var _a;
|
|
12774
13328
|
const issues = [];
|
|
12775
13329
|
const seen = /* @__PURE__ */ new Set();
|
|
12776
|
-
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("");
|
|
12777
13357
|
for (const [pattern, label] of TEMPLATE_VARIABLE_PATTERNS) {
|
|
12778
13358
|
pattern.lastIndex = 0;
|
|
12779
13359
|
let match;
|
|
@@ -12806,13 +13386,14 @@ function checkTemplateVariablesFromDom($) {
|
|
|
12806
13386
|
const key = `attr:${attr}:${variable}`;
|
|
12807
13387
|
if (seen.has(key)) continue;
|
|
12808
13388
|
seen.add(key);
|
|
12809
|
-
|
|
13389
|
+
const loc = locOfAttr(el, attr);
|
|
13390
|
+
issues.push(__spreadValues({
|
|
12810
13391
|
rule: "unresolved-variable",
|
|
12811
13392
|
severity: "error",
|
|
12812
13393
|
message: `Unresolved ${label} variable "${variable}" found in ${attr} attribute.`,
|
|
12813
13394
|
variable,
|
|
12814
13395
|
location: "attribute"
|
|
12815
|
-
});
|
|
13396
|
+
}, loc ? { loc } : {}));
|
|
12816
13397
|
}
|
|
12817
13398
|
}
|
|
12818
13399
|
}
|
|
@@ -12820,13 +13401,13 @@ function checkTemplateVariablesFromDom($) {
|
|
|
12820
13401
|
}
|
|
12821
13402
|
return { unresolvedCount: issues.length, issues };
|
|
12822
13403
|
}
|
|
12823
|
-
function
|
|
12824
|
-
|
|
12825
|
-
|
|
12826
|
-
|
|
12827
|
-
|
|
12828
|
-
|
|
12829
|
-
|
|
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
|
+
);
|
|
12830
13411
|
}
|
|
12831
13412
|
|
|
12832
13413
|
// src/overflow-checker.ts
|
|
@@ -12842,32 +13423,71 @@ function fixedPxWidth($el) {
|
|
|
12842
13423
|
function isFluid(style) {
|
|
12843
13424
|
return /max-width\s*:\s*100%/i.test(style) || /width\s*:\s*100%/i.test(style);
|
|
12844
13425
|
}
|
|
12845
|
-
function addWidthIssue(width, label, issues, seen) {
|
|
13426
|
+
function addWidthIssue(width, label, issues, seen, loc) {
|
|
12846
13427
|
const key = `w:${label}:${width}`;
|
|
12847
|
-
|
|
12848
|
-
|
|
12849
|
-
|
|
13428
|
+
const existing = seen.get(key);
|
|
13429
|
+
if (existing) {
|
|
13430
|
+
addOccurrence(existing, loc);
|
|
13431
|
+
return;
|
|
13432
|
+
}
|
|
13433
|
+
const issue = __spreadValues({
|
|
12850
13434
|
rule: "fixed-width-overflow",
|
|
12851
13435
|
severity: "warning",
|
|
12852
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.`,
|
|
12853
13437
|
detail: `Use width:100% with max-width:${EMAIL_MAX_WIDTH}px instead of a fixed width beyond the frame.`
|
|
12854
|
-
});
|
|
13438
|
+
}, loc ? { loc, locs: [loc] } : {});
|
|
13439
|
+
seen.set(key, issue);
|
|
13440
|
+
issues.push(issue);
|
|
12855
13441
|
}
|
|
12856
|
-
function
|
|
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);
|
|
13470
|
+
}
|
|
13471
|
+
function checkOverflowFromDom($, source) {
|
|
13472
|
+
var _a;
|
|
12857
13473
|
const issues = [];
|
|
12858
|
-
const seen = /* @__PURE__ */ new
|
|
13474
|
+
const seen = /* @__PURE__ */ new Map();
|
|
13475
|
+
const tokensSeen = /* @__PURE__ */ new Set();
|
|
12859
13476
|
$("[width], [style*='width']").each((_, el) => {
|
|
12860
13477
|
const $el = $(el);
|
|
12861
13478
|
const width = fixedPxWidth($el);
|
|
12862
13479
|
if (width === null || width <= EMAIL_MAX_WIDTH) return;
|
|
12863
13480
|
if (isFluid($el.attr("style") || "")) return;
|
|
12864
13481
|
const tag = (el.tagName || "element").toLowerCase();
|
|
12865
|
-
|
|
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"));
|
|
12866
13484
|
});
|
|
12867
13485
|
$("style").each((_, el) => {
|
|
13486
|
+
const cssText = $(el).text();
|
|
13487
|
+
const anchor = cssBlockAnchor(el, cssText, source);
|
|
12868
13488
|
let ast;
|
|
12869
13489
|
try {
|
|
12870
|
-
ast = csstree6.parse(
|
|
13490
|
+
ast = csstree6.parse(cssText, { positions: true });
|
|
12871
13491
|
} catch (e) {
|
|
12872
13492
|
return;
|
|
12873
13493
|
}
|
|
@@ -12877,13 +13497,17 @@ function checkOverflowFromDom($) {
|
|
|
12877
13497
|
if (node.type !== "Rule") return;
|
|
12878
13498
|
let widthPx = null;
|
|
12879
13499
|
let fluid = false;
|
|
13500
|
+
let widthLoc;
|
|
12880
13501
|
node.block.children.forEach((child) => {
|
|
12881
13502
|
if (child.type !== "Declaration") return;
|
|
12882
13503
|
const prop = child.property.toLowerCase();
|
|
12883
13504
|
const val = csstree6.generate(child.value);
|
|
12884
13505
|
if (prop === "width") {
|
|
12885
13506
|
const m = val.match(/^(\d+)px$/);
|
|
12886
|
-
if (m)
|
|
13507
|
+
if (m) {
|
|
13508
|
+
widthPx = parseInt(m[1], 10);
|
|
13509
|
+
widthLoc = locInCssBlock(anchor, child.loc);
|
|
13510
|
+
}
|
|
12887
13511
|
if (/\b100%/.test(val)) fluid = true;
|
|
12888
13512
|
} else if (prop === "max-width" && /\b100%/.test(val)) {
|
|
12889
13513
|
fluid = true;
|
|
@@ -12891,36 +13515,46 @@ function checkOverflowFromDom($) {
|
|
|
12891
13515
|
});
|
|
12892
13516
|
if (widthPx !== null && widthPx > EMAIL_MAX_WIDTH && !fluid) {
|
|
12893
13517
|
const selector = csstree6.generate(node.prelude).trim().slice(0, 40);
|
|
12894
|
-
addWidthIssue(widthPx, selector || "rule", issues, seen);
|
|
13518
|
+
addWidthIssue(widthPx, selector || "rule", issues, seen, widthLoc);
|
|
12895
13519
|
}
|
|
12896
13520
|
}
|
|
12897
13521
|
});
|
|
12898
13522
|
});
|
|
12899
13523
|
const usesWrapGuard = /overflow-wrap|word-break|word-wrap/i.test($.html());
|
|
12900
13524
|
if (!usesWrapGuard) {
|
|
12901
|
-
const
|
|
13525
|
+
const nodes = visibleTextNodes($);
|
|
13526
|
+
const starts = [];
|
|
12902
13527
|
let text = "";
|
|
12903
|
-
|
|
12904
|
-
|
|
12905
|
-
|
|
12906
|
-
|
|
12907
|
-
|
|
12908
|
-
for (const token of text.split(
|
|
12909
|
-
|
|
12910
|
-
|
|
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);
|
|
12911
13539
|
const preview = token.length > 50 ? `${token.slice(0, 50)}\u2026` : token;
|
|
12912
|
-
|
|
13540
|
+
const loc = locateInNodes(nodes, starts, start, token.length, source);
|
|
13541
|
+
issues.push(__spreadValues({
|
|
12913
13542
|
rule: "unbreakable-string",
|
|
12914
13543
|
severity: "warning",
|
|
12915
13544
|
message: `A ${token.length}-character unbroken string ("${preview}") can't wrap and will force horizontal scrolling on narrow screens.`,
|
|
12916
13545
|
detail: `Add overflow-wrap: anywhere (or word-break: break-word) to its container.`
|
|
12917
|
-
});
|
|
13546
|
+
}, loc ? { loc, locs: [loc] } : {}));
|
|
12918
13547
|
}
|
|
12919
13548
|
}
|
|
12920
13549
|
return { hasOverflow: issues.length > 0, issues };
|
|
12921
13550
|
}
|
|
12922
|
-
function checkOverflow(html) {
|
|
12923
|
-
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
|
+
);
|
|
12924
13558
|
}
|
|
12925
13559
|
|
|
12926
13560
|
// src/visual-checker.ts
|
|
@@ -12933,8 +13567,8 @@ function isSolidColor(value) {
|
|
|
12933
13567
|
return c !== null && c.a !== 0;
|
|
12934
13568
|
}
|
|
12935
13569
|
function firstColor(value) {
|
|
12936
|
-
const
|
|
12937
|
-
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) {
|
|
12938
13572
|
const lc = t.toLowerCase();
|
|
12939
13573
|
if (lc === "transparent") continue;
|
|
12940
13574
|
if (/^(?:linear|radial|conic|gradient|deg|turn|rad|grad|to|at|from|in|circle|ellipse|closest|farthest|side|corner|url)$/.test(lc)) continue;
|
|
@@ -12971,8 +13605,17 @@ function hasFontFallback(value) {
|
|
|
12971
13605
|
return WEB_SAFE_FONTS.has(t) || GENERIC_FONT_FAMILIES.has(t) || t.startsWith("-apple-system") || t === "blinkmacsystemfont";
|
|
12972
13606
|
});
|
|
12973
13607
|
}
|
|
12974
|
-
function
|
|
12975
|
-
|
|
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;
|
|
12976
13619
|
const combined = `${(_a = style.get("background-image")) != null ? _a : ""} ${(_b = style.get("background")) != null ? _b : ""}`;
|
|
12977
13620
|
const isGradient = GRADIENT_RE.test(combined);
|
|
12978
13621
|
const isImage = isGradient || /url\(/i.test(combined);
|
|
@@ -12980,30 +13623,40 @@ function inspectDeclarations(style, issues, seen) {
|
|
|
12980
13623
|
const stop = isGradient ? firstColor(combined) : null;
|
|
12981
13624
|
const fix = stop ? `background-color: ${stop};` : `background-color: <solid colour matching the image>;`;
|
|
12982
13625
|
const key = `bg:${fix}`;
|
|
12983
|
-
|
|
12984
|
-
|
|
12985
|
-
|
|
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({
|
|
12986
13632
|
rule: "missing-background-fallback",
|
|
12987
13633
|
severity: "warning",
|
|
12988
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.`,
|
|
12989
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.`,
|
|
12990
13636
|
fix
|
|
12991
|
-
});
|
|
13637
|
+
}, loc ? { loc, locs: [loc] } : {});
|
|
13638
|
+
seen.set(key, issue);
|
|
13639
|
+
issues.push(issue);
|
|
12992
13640
|
}
|
|
12993
13641
|
}
|
|
12994
13642
|
const font = style.get("font-family");
|
|
12995
13643
|
if (font && !CSS_WIDE_KEYWORDS.has(font.trim().toLowerCase()) && !hasFontFallback(font)) {
|
|
12996
13644
|
const fix = `font-family: ${font.trim()}, Arial, sans-serif;`;
|
|
12997
13645
|
const key = `font:${font.trim().toLowerCase()}`;
|
|
12998
|
-
|
|
12999
|
-
|
|
13000
|
-
|
|
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({
|
|
13001
13652
|
rule: "missing-font-fallback",
|
|
13002
13653
|
severity: "warning",
|
|
13003
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.`,
|
|
13004
13655
|
detail: `End the stack with a web-safe font and a generic family.`,
|
|
13005
13656
|
fix
|
|
13006
|
-
});
|
|
13657
|
+
}, loc ? { loc, locs: [loc] } : {});
|
|
13658
|
+
seen.set(key, issue);
|
|
13659
|
+
issues.push(issue);
|
|
13007
13660
|
}
|
|
13008
13661
|
}
|
|
13009
13662
|
}
|
|
@@ -13016,30 +13669,47 @@ function ruleToMap(node) {
|
|
|
13016
13669
|
});
|
|
13017
13670
|
return map;
|
|
13018
13671
|
}
|
|
13019
|
-
function checkVisualFromDom(
|
|
13672
|
+
function checkVisualFromDom($, source) {
|
|
13020
13673
|
const issues = [];
|
|
13021
|
-
const seen = /* @__PURE__ */ new
|
|
13674
|
+
const seen = /* @__PURE__ */ new Map();
|
|
13022
13675
|
$("[style]").each((_, el) => {
|
|
13023
|
-
|
|
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);
|
|
13024
13680
|
});
|
|
13025
13681
|
$("style").each((_, el) => {
|
|
13682
|
+
const cssText = $(el).text();
|
|
13683
|
+
const anchor = cssBlockAnchor(el, cssText, source);
|
|
13026
13684
|
let ast;
|
|
13027
13685
|
try {
|
|
13028
|
-
ast = csstree7.parse(
|
|
13686
|
+
ast = csstree7.parse(cssText, { positions: true });
|
|
13029
13687
|
} catch (e) {
|
|
13030
13688
|
return;
|
|
13031
13689
|
}
|
|
13032
13690
|
csstree7.walk(ast, {
|
|
13033
13691
|
visit: "Rule",
|
|
13034
13692
|
enter(node) {
|
|
13035
|
-
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);
|
|
13036
13701
|
}
|
|
13037
13702
|
});
|
|
13038
13703
|
});
|
|
13039
13704
|
return { issues };
|
|
13040
13705
|
}
|
|
13041
|
-
function checkVisual(html) {
|
|
13042
|
-
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
|
+
);
|
|
13043
13713
|
}
|
|
13044
13714
|
|
|
13045
13715
|
// src/audit.ts
|
|
@@ -13058,7 +13728,8 @@ var EMPTY_AUDIT = {
|
|
|
13058
13728
|
function runAudit($, html, framework, options) {
|
|
13059
13729
|
var _a;
|
|
13060
13730
|
const skip = new Set((_a = options == null ? void 0 : options.skip) != null ? _a : []);
|
|
13061
|
-
const
|
|
13731
|
+
const source = (options == null ? void 0 : options.positions) ? html : void 0;
|
|
13732
|
+
const warnings = skip.has("compatibility") ? [] : analyzeEmailFromDom($, framework, source);
|
|
13062
13733
|
const scores = skip.has("compatibility") ? {} : generateCompatibilityScore(warnings);
|
|
13063
13734
|
const spam = skip.has("spam") ? EMPTY_SPAM : analyzeSpamFromDom($, options == null ? void 0 : options.spam);
|
|
13064
13735
|
const links = skip.has("links") ? EMPTY_LINKS : validateLinksFromDom($);
|
|
@@ -13066,19 +13737,19 @@ function runAudit($, html, framework, options) {
|
|
|
13066
13737
|
const images = skip.has("images") ? EMPTY_IMAGES : analyzeImagesFromDom($);
|
|
13067
13738
|
const inboxPreview = skip.has("inboxPreview") ? EMPTY_INBOX_PREVIEW : extractInboxPreviewFromDom($);
|
|
13068
13739
|
const size = skip.has("size") ? EMPTY_SIZE : checkSizeFromDom($, html);
|
|
13069
|
-
const templateVariables = skip.has("templateVariables") ? EMPTY_TEMPLATE : checkTemplateVariablesFromDom(
|
|
13070
|
-
const overflow = skip.has("overflow") ? EMPTY_OVERFLOW : checkOverflowFromDom(
|
|
13071
|
-
const visual = skip.has("visual") ? EMPTY_VISUAL : checkVisualFromDom(
|
|
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);
|
|
13072
13743
|
return { compatibility: { warnings, scores }, spam, links, accessibility, images, inboxPreview, size, templateVariables, overflow, visual };
|
|
13073
13744
|
}
|
|
13074
13745
|
function auditEmail(html, options) {
|
|
13075
|
-
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);
|
|
13076
13747
|
}
|
|
13077
13748
|
|
|
13078
13749
|
// src/plain-text.ts
|
|
13079
|
-
import * as
|
|
13750
|
+
import * as cheerio5 from "cheerio";
|
|
13080
13751
|
function toPlainText(html) {
|
|
13081
|
-
const $ =
|
|
13752
|
+
const $ = cheerio5.load(html);
|
|
13082
13753
|
$("style, script, head").remove();
|
|
13083
13754
|
$("[data-skip-in-text='true']").remove();
|
|
13084
13755
|
const lines = [];
|
|
@@ -13180,7 +13851,6 @@ function toPlainText(html) {
|
|
|
13180
13851
|
}
|
|
13181
13852
|
|
|
13182
13853
|
// src/session.ts
|
|
13183
|
-
import * as cheerio7 from "cheerio";
|
|
13184
13854
|
function createSession(html, options) {
|
|
13185
13855
|
if (!html || !html.trim()) {
|
|
13186
13856
|
const fw = options == null ? void 0 : options.framework;
|
|
@@ -13207,16 +13877,17 @@ function createSession(html, options) {
|
|
|
13207
13877
|
if (html.length > MAX_HTML_SIZE) {
|
|
13208
13878
|
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
13209
13879
|
}
|
|
13210
|
-
const $ =
|
|
13880
|
+
const $ = loadHtml(html, options);
|
|
13211
13881
|
const framework = options == null ? void 0 : options.framework;
|
|
13882
|
+
const source = (options == null ? void 0 : options.positions) ? html : void 0;
|
|
13212
13883
|
return {
|
|
13213
13884
|
html,
|
|
13214
13885
|
framework,
|
|
13215
13886
|
audit(opts) {
|
|
13216
|
-
return runAudit($, html, framework, opts);
|
|
13887
|
+
return runAudit($, html, framework, __spreadProps(__spreadValues({}, opts), { positions: options == null ? void 0 : options.positions }));
|
|
13217
13888
|
},
|
|
13218
13889
|
analyze() {
|
|
13219
|
-
return analyzeEmailFromDom($, framework);
|
|
13890
|
+
return analyzeEmailFromDom($, framework, source);
|
|
13220
13891
|
},
|
|
13221
13892
|
score(warnings) {
|
|
13222
13893
|
return generateCompatibilityScore(warnings);
|
|
@@ -13240,13 +13911,13 @@ function createSession(html, options) {
|
|
|
13240
13911
|
return checkSizeFromDom($, html);
|
|
13241
13912
|
},
|
|
13242
13913
|
checkTemplateVariables() {
|
|
13243
|
-
return checkTemplateVariablesFromDom(
|
|
13914
|
+
return checkTemplateVariablesFromDom($, source);
|
|
13244
13915
|
},
|
|
13245
13916
|
checkOverflow() {
|
|
13246
|
-
return checkOverflowFromDom(
|
|
13917
|
+
return checkOverflowFromDom($, source);
|
|
13247
13918
|
},
|
|
13248
13919
|
checkVisual() {
|
|
13249
|
-
return checkVisualFromDom(
|
|
13920
|
+
return checkVisualFromDom($, source);
|
|
13250
13921
|
},
|
|
13251
13922
|
// Transforms create isolated copies since they mutate the DOM
|
|
13252
13923
|
transformForClient(clientId) {
|
|
@@ -13266,18 +13937,22 @@ export {
|
|
|
13266
13937
|
COMPOUND_VALUE_FEATURES,
|
|
13267
13938
|
CSS_FUNCTION_FEATURES,
|
|
13268
13939
|
CSS_SUPPORT,
|
|
13940
|
+
CSS_SUPPORT_NOTES,
|
|
13269
13941
|
CompileError,
|
|
13270
13942
|
EMAIL_CLIENTS,
|
|
13271
13943
|
EMPTY_DELIVERABILITY,
|
|
13272
13944
|
GENERIC_LINK_TEXT,
|
|
13273
13945
|
HTML_ELEMENT_FEATURES,
|
|
13274
13946
|
MAX_HTML_SIZE,
|
|
13947
|
+
MAX_WARNING_LOCATIONS,
|
|
13275
13948
|
STRUCTURAL_FIX_PROPERTIES,
|
|
13949
|
+
VALUE_CAVEAT_PROPS,
|
|
13276
13950
|
alphaBlend,
|
|
13277
13951
|
analyzeEmail,
|
|
13278
13952
|
analyzeImages,
|
|
13279
13953
|
analyzeSpam,
|
|
13280
13954
|
auditEmail,
|
|
13955
|
+
caveatApplies,
|
|
13281
13956
|
checkAccessibility,
|
|
13282
13957
|
checkOverflow,
|
|
13283
13958
|
checkSize,
|