@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/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
- deprecated: "2026-10"
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 Set();
11303
+ const seenWarnings = /* @__PURE__ */ new Map();
10849
11304
  function addWarning(w) {
10850
11305
  const key = `${w.client}:${w.property}:${w.severity}:${w.selector || ""}`;
10851
- if (!seenWarnings.has(key)) {
10852
- seenWarnings.add(key);
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
- if ($(selector).length === 0) continue;
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 && !propertyLines.has(det.key)) {
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 && !propertyLines.has(fn.key)) {
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
- checkPropertySupport(prop, addWarning, framework, selector, void 0, (_a = getStyleValue(style, prop)) != null ? _a : void 0);
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 ? values.join(" ") : void 0
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 $ = cheerio4.load(html);
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, value) {
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 = (_a = CSS_SUPPORT_NOTES[prop]) == null ? void 0 : _a[client.id];
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 } : {}), line !== void 0 ? { line } : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
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 (valueGated && value !== void 0 && !valueTriggersCaveat(prop, value, notes)) continue;
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
- return {
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
- return {
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
- return {
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 textContent = extractTextContent($);
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
- issues.push({
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 extractTextContent($) {
12824
- const clone = $.root().clone();
12825
- clone.find("style, script, head").remove();
12826
- return clone.text();
12827
- }
12828
- function checkTemplateVariables(html) {
12829
- return fromHtml(html, EMPTY_TEMPLATE, checkTemplateVariablesFromDom);
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
- if (seen.has(key)) return;
12848
- seen.add(key);
12849
- issues.push({
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 checkOverflowFromDom($) {
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 Set();
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
- addWidthIssue(width, `<${tag}>`, issues, seen);
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($(el).text());
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) widthPx = parseInt(m[1], 10);
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 $body = $("body");
13525
+ const nodes = visibleTextNodes($);
13526
+ const starts = [];
12902
13527
  let text = "";
12903
- if ($body.length) {
12904
- const clone = $body.clone();
12905
- clone.find("style, script").remove();
12906
- text = clone.text();
12907
- }
12908
- for (const token of text.split(/\s+/)) {
12909
- if (token.length <= UNBREAKABLE_STRING_LENGTH || seen.has(token)) continue;
12910
- seen.add(token);
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
- issues.push({
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(html, EMPTY_OVERFLOW, checkOverflowFromDom);
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 tokens = value.match(/#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\)|\b[a-zA-Z]{3,}\b/g) || [];
12937
- for (const t of tokens) {
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 inspectDeclarations(style, issues, seen) {
12975
- var _a, _b;
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
- if (!seen.has(key)) {
12984
- seen.add(key);
12985
- issues.push({
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
- if (!seen.has(key)) {
12999
- seen.add(key);
13000
- issues.push({
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 Set();
13674
+ const seen = /* @__PURE__ */ new Map();
13022
13675
  $("[style]").each((_, el) => {
13023
- inspectDeclarations(parseInlineStyle($(el).attr("style") || ""), issues, seen);
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($(el).text());
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 === "Rule") inspectDeclarations(ruleToMap(node), issues, seen);
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(html, EMPTY_VISUAL, checkVisualFromDom);
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 warnings = skip.has("compatibility") ? [] : analyzeEmailFromDom($, framework);
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 cheerio6 from "cheerio";
13750
+ import * as cheerio5 from "cheerio";
13080
13751
  function toPlainText(html) {
13081
- const $ = cheerio6.load(html);
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 $ = cheerio7.load(html);
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,