@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.cjs CHANGED
@@ -64,18 +64,22 @@ __export(index_exports, {
64
64
  COMPOUND_VALUE_FEATURES: () => COMPOUND_VALUE_FEATURES,
65
65
  CSS_FUNCTION_FEATURES: () => CSS_FUNCTION_FEATURES,
66
66
  CSS_SUPPORT: () => CSS_SUPPORT,
67
+ CSS_SUPPORT_NOTES: () => CSS_SUPPORT_NOTES,
67
68
  CompileError: () => CompileError,
68
69
  EMAIL_CLIENTS: () => EMAIL_CLIENTS,
69
70
  EMPTY_DELIVERABILITY: () => EMPTY_DELIVERABILITY,
70
71
  GENERIC_LINK_TEXT: () => GENERIC_LINK_TEXT,
71
72
  HTML_ELEMENT_FEATURES: () => HTML_ELEMENT_FEATURES,
72
73
  MAX_HTML_SIZE: () => MAX_HTML_SIZE,
74
+ MAX_WARNING_LOCATIONS: () => MAX_WARNING_LOCATIONS,
73
75
  STRUCTURAL_FIX_PROPERTIES: () => STRUCTURAL_FIX_PROPERTIES,
76
+ VALUE_CAVEAT_PROPS: () => VALUE_CAVEAT_PROPS,
74
77
  alphaBlend: () => alphaBlend,
75
78
  analyzeEmail: () => analyzeEmail,
76
79
  analyzeImages: () => analyzeImages,
77
80
  analyzeSpam: () => analyzeSpam,
78
81
  auditEmail: () => auditEmail,
82
+ caveatApplies: () => caveatApplies,
79
83
  checkAccessibility: () => checkAccessibility,
80
84
  checkOverflow: () => checkOverflow,
81
85
  checkSize: () => checkSize,
@@ -158,7 +162,11 @@ var EMAIL_CLIENTS = [
158
162
  engine: "Microsoft Word",
159
163
  darkModeSupport: true,
160
164
  icon: "monitor",
161
- deprecated: "2026-10"
165
+ // End of support ~Q2 2029 (Microsoft: "supported until at least 2029").
166
+ // The April 2026 date some sources cite is the opt-out phase, when classic
167
+ // stops being the Windows default, not end of support. October 2026 was
168
+ // wrong: the nearest real Oct date is Oct 2025, for legacy Outlook for Mac.
169
+ deprecated: "2029-06"
162
170
  },
163
171
  {
164
172
  id: "outlook-ios",
@@ -8991,6 +8999,16 @@ function getStyleValue(style, property) {
8991
8999
  }
8992
9000
  return null;
8993
9001
  }
9002
+ function getStyleValues(style, property) {
9003
+ const values = [];
9004
+ for (const part of splitStyleDeclarations(style)) {
9005
+ const colonIndex = part.indexOf(":");
9006
+ if (colonIndex === -1) continue;
9007
+ if (part.slice(0, colonIndex).trim().toLowerCase() !== property) continue;
9008
+ values.push(part.slice(colonIndex + 1).trim());
9009
+ }
9010
+ return values;
9011
+ }
8994
9012
  function parseInlineStyle(style) {
8995
9013
  const map = /* @__PURE__ */ new Map();
8996
9014
  const declarations = splitStyleDeclarations(style);
@@ -9741,6 +9759,7 @@ function downlevelCSS(html) {
9741
9759
 
9742
9760
  // src/constants.ts
9743
9761
  var MAX_HTML_SIZE = 2 * 1024 * 1024;
9762
+ var MAX_WARNING_LOCATIONS = 100;
9744
9763
  var GENERIC_LINK_TEXT = /* @__PURE__ */ new Set([
9745
9764
  "click here",
9746
9765
  "here",
@@ -10477,9 +10496,231 @@ function transformForAllClients(html, framework) {
10477
10496
  }
10478
10497
 
10479
10498
  // src/analyze.ts
10480
- var cheerio4 = __toESM(require("cheerio"), 1);
10481
10499
  var csstree5 = __toESM(require("css-tree"), 1);
10482
10500
 
10501
+ // src/rules/value-caveats.ts
10502
+ var VALUE_CAVEAT_PROPS = /* @__PURE__ */ new Set([
10503
+ "background",
10504
+ "border-radius",
10505
+ "display",
10506
+ "font-size",
10507
+ "font-weight",
10508
+ "letter-spacing",
10509
+ "margin",
10510
+ "overflow",
10511
+ "position",
10512
+ "text-align",
10513
+ "transition"
10514
+ ]);
10515
+ function normalize(value) {
10516
+ return value.replace(/\/\*[\s\S]*?\*\//g, " ").toLowerCase().replace(/!\s*important/g, " ").replace(/;+\s*$/, "").replace(/\s+/g, " ").trim();
10517
+ }
10518
+ var preparedValues = /* @__PURE__ */ new WeakMap();
10519
+ function prepare(values) {
10520
+ let out = preparedValues.get(values);
10521
+ if (!out) {
10522
+ out = [...new Set(values.map(normalize))];
10523
+ preparedValues.set(values, out);
10524
+ }
10525
+ return out;
10526
+ }
10527
+ function topLevelSplit(value, sep) {
10528
+ const parts = [];
10529
+ let depth = 0;
10530
+ let start = 0;
10531
+ for (let i = 0; i < value.length; i++) {
10532
+ const c = value[i];
10533
+ if (c === "(") depth++;
10534
+ else if (c === ")") depth = Math.max(0, depth - 1);
10535
+ else if (c === sep && depth === 0) {
10536
+ parts.push(value.slice(start, i));
10537
+ start = i + 1;
10538
+ }
10539
+ }
10540
+ parts.push(value.slice(start));
10541
+ return parts.map((p) => p.trim()).filter(Boolean);
10542
+ }
10543
+ function tokens(value) {
10544
+ return topLevelSplit(value, " ");
10545
+ }
10546
+ function hasUnit(value, units) {
10547
+ return new RegExp(`\\d(?:${units.join("|")})\\b`).test(value);
10548
+ }
10549
+ function bareNumbers(value) {
10550
+ return tokens(value).filter((t) => /^[+-]?\d+(?:\.\d+)?(?:e[+-]?\d+)?$/.test(t)).map(Number);
10551
+ }
10552
+ function hasNegative(value) {
10553
+ return /(?:^|[\s,(])-\.?\d/.test(value);
10554
+ }
10555
+ function dashed(v) {
10556
+ return v.replace(/\s+/g, "-");
10557
+ }
10558
+ function unprefixed(token) {
10559
+ return token.replace(/^-(?:webkit|moz|ms|o)-/, "");
10560
+ }
10561
+ function quotedValues(note) {
10562
+ var _a;
10563
+ const supported = [];
10564
+ const banned = [];
10565
+ for (const m of note.matchAll(/`([^`]+)`/g)) {
10566
+ const clause = (_a = note.slice(0, m.index).split(/[.;]/).pop()) != null ? _a : "";
10567
+ (/\bsupports\b/i.test(clause) ? supported : banned).push(dashed(m[1].toLowerCase().trim()));
10568
+ }
10569
+ return { supported, banned };
10570
+ }
10571
+ function isColorOnly(value) {
10572
+ if (value.startsWith("#")) return /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(value);
10573
+ if (parseColor(value)) return true;
10574
+ return ["none", "inherit", "initial", "unset", "revert", "currentcolor"].includes(value) || // A custom property is usually a palette colour, and we cannot resolve it.
10575
+ value.startsWith("var(");
10576
+ }
10577
+ var POSITION_KEYWORDS = ["relative", "absolute", "fixed", "sticky"];
10578
+ var RELATIVE_FONT_UNITS = [
10579
+ "cqmin",
10580
+ "cqmax",
10581
+ "vmin",
10582
+ "vmax",
10583
+ "rlh",
10584
+ "rex",
10585
+ "rch",
10586
+ "ric",
10587
+ "rem",
10588
+ "svw",
10589
+ "svh",
10590
+ "lvw",
10591
+ "lvh",
10592
+ "dvw",
10593
+ "dvh",
10594
+ "cqw",
10595
+ "cqh",
10596
+ "cqi",
10597
+ "cqb",
10598
+ "cap",
10599
+ "ic",
10600
+ "lh",
10601
+ "em",
10602
+ "ex",
10603
+ "ch",
10604
+ "vw",
10605
+ "vh",
10606
+ "vi",
10607
+ "vb"
10608
+ ];
10609
+ var NOT_A_PROPERTY_NAME = /* @__PURE__ */ new Set([
10610
+ "all",
10611
+ "allow-discrete",
10612
+ "normal",
10613
+ "ease",
10614
+ "ease-in",
10615
+ "ease-out",
10616
+ "ease-in-out",
10617
+ "linear",
10618
+ "step-start",
10619
+ "step-end",
10620
+ "none",
10621
+ "initial",
10622
+ "inherit",
10623
+ "unset",
10624
+ "revert",
10625
+ "revert-layer"
10626
+ ]);
10627
+ function namesAProperty(layer) {
10628
+ return tokens(layer).some((t) => /^[a-z][a-z0-9-]*$/.test(t) && !NOT_A_PROPERTY_NAME.has(t));
10629
+ }
10630
+ function triggers(prop, value, note, noteLc) {
10631
+ switch (prop) {
10632
+ case "margin": {
10633
+ const negative = noteLc.includes("negative");
10634
+ const auto = noteLc.includes("auto");
10635
+ if (!negative && !auto) return true;
10636
+ return negative && hasNegative(value) || auto && /\bauto\b/.test(value);
10637
+ }
10638
+ case "position": {
10639
+ const used = POSITION_KEYWORDS.find((k) => tokens(value).some((t) => unprefixed(t) === k));
10640
+ if (!used) return false;
10641
+ const m = note.match(/supports\s+.+?\s+but not\s+([^.]+)/i);
10642
+ if (m) return m[1].toLowerCase().includes(used);
10643
+ return true;
10644
+ }
10645
+ case "overflow": {
10646
+ if (!/\b(?:auto|scroll|overlay)\b/.test(value)) return false;
10647
+ if (!noteLc.includes("cannot scroll") && !noteLc.includes("overflow-block")) return true;
10648
+ return noteLc.includes("cannot scroll");
10649
+ }
10650
+ case "font-size": {
10651
+ if (noteLc.includes("percentage") || noteLc.includes("relative")) {
10652
+ return hasUnit(value, RELATIVE_FONT_UNITS) || /\d\s*%/.test(value) || tokens(value).some((t) => t === "smaller" || t === "larger");
10653
+ }
10654
+ if (noteLc.includes("`rem`")) return hasUnit(value, ["rem"]);
10655
+ return true;
10656
+ }
10657
+ case "display": {
10658
+ const only = note.match(/only supports\s+([^.]*)/i);
10659
+ if (only) {
10660
+ const allowed = quotedValues(only[1]).banned.map((q) => q.replace(/^display:-?/, "")).filter((q) => /^[a-z-]+$/.test(q));
10661
+ if (!allowed.length) return true;
10662
+ return !allowed.includes(dashed(value));
10663
+ }
10664
+ const { banned } = quotedValues(note);
10665
+ if (banned.length) {
10666
+ const v = dashed(value);
10667
+ return banned.includes(v) || banned.includes(unprefixed(v));
10668
+ }
10669
+ if (noteLc.includes("two-value syntax")) return tokens(value).length > 1;
10670
+ return true;
10671
+ }
10672
+ case "font-weight": {
10673
+ const nums = bareNumbers(value);
10674
+ if (!nums.length) return false;
10675
+ if (noteLc.includes("font weight")) return nums.some((n) => n !== 400 && n !== 700);
10676
+ if (noteLc.includes("only the following numeric values")) {
10677
+ return nums.some((n) => n % 100 !== 0 || n < 100 || n > 900);
10678
+ }
10679
+ return true;
10680
+ }
10681
+ case "border-radius": {
10682
+ if (noteLc.includes("slash")) return topLevelSplit(value, "/").length > 1;
10683
+ return true;
10684
+ }
10685
+ case "text-align": {
10686
+ const { supported, banned } = quotedValues(note);
10687
+ if (!banned.length) return true;
10688
+ return tokens(value).some(
10689
+ (t) => !supported.includes(t) && (banned.includes(t) || banned.includes(unprefixed(t)))
10690
+ );
10691
+ }
10692
+ case "background": {
10693
+ if (noteLc.includes("only `background-color`")) return !isColorOnly(value);
10694
+ if (noteLc.includes("multiple values")) {
10695
+ return topLevelSplit(value, ",").length > 1 || topLevelSplit(value, "/").length > 1;
10696
+ }
10697
+ return true;
10698
+ }
10699
+ case "letter-spacing": {
10700
+ const negative = noteLc.includes("negative");
10701
+ const em = noteLc.includes("`em`");
10702
+ if (!negative && !em) return true;
10703
+ return negative && hasNegative(value) || em && hasUnit(value, ["em"]);
10704
+ }
10705
+ case "transition": {
10706
+ if (noteLc.includes("`all`")) {
10707
+ if (["none", "inherit", "initial", "unset", "revert"].includes(value)) return false;
10708
+ return topLevelSplit(value, ",").some((layer) => !namesAProperty(layer));
10709
+ }
10710
+ return true;
10711
+ }
10712
+ default:
10713
+ return true;
10714
+ }
10715
+ }
10716
+ function caveatApplies(prop, values, notes) {
10717
+ if (!VALUE_CAVEAT_PROPS.has(prop)) return true;
10718
+ if (!(values == null ? void 0 : values.length)) return true;
10719
+ const note = (notes != null ? notes : []).join(" ");
10720
+ const noteLc = note.toLowerCase();
10721
+ return prepare(values).some((value) => triggers(prop, value, note, noteLc));
10722
+ }
10723
+
10483
10724
  // src/dark-mode-checker.ts
10484
10725
  var csstree4 = __toESM(require("css-tree"), 1);
10485
10726
 
@@ -10756,6 +10997,209 @@ function applyColorInversion($, mode) {
10756
10997
  });
10757
10998
  }
10758
10999
 
11000
+ // src/source-location.ts
11001
+ function toLoc(p) {
11002
+ return {
11003
+ line: p.startLine,
11004
+ column: p.startCol,
11005
+ endLine: p.endLine,
11006
+ endColumn: p.endCol,
11007
+ offset: p.startOffset,
11008
+ length: p.endOffset - p.startOffset
11009
+ };
11010
+ }
11011
+ function locOfElement(el) {
11012
+ var _a;
11013
+ const raw = el == null ? void 0 : el.sourceCodeLocation;
11014
+ if (!raw) return void 0;
11015
+ return toLoc((_a = raw.startTag) != null ? _a : raw);
11016
+ }
11017
+ function locOfAttr(el, attr) {
11018
+ var _a, _b, _c, _d;
11019
+ const raw = el == null ? void 0 : el.sourceCodeLocation;
11020
+ if (!raw) return void 0;
11021
+ const attrLoc = (_d = (_a = raw.attrs) == null ? void 0 : _a[attr]) != null ? _d : (_c = (_b = raw.startTag) == null ? void 0 : _b.attrs) == null ? void 0 : _c[attr];
11022
+ return attrLoc ? toLoc(attrLoc) : locOfElement(el);
11023
+ }
11024
+ function locOfFirst($, selector) {
11025
+ const el = $(selector).first()[0];
11026
+ return el ? locOfElement(el) : void 0;
11027
+ }
11028
+ function cssBlockAnchor(styleEl, cssText, source) {
11029
+ var _a;
11030
+ const children = styleEl == null ? void 0 : styleEl.children;
11031
+ if (!children || children.length !== 1) return void 0;
11032
+ const loc = (_a = children[0]) == null ? void 0 : _a.sourceCodeLocation;
11033
+ if (!loc) return void 0;
11034
+ const mapper = source ? crMapper(source.slice(loc.startOffset, loc.endOffset), cssText) : null;
11035
+ const extraBefore = mapper ? (index) => mapper(index) - index : crOffsetter(cssText, loc.endOffset - loc.startOffset);
11036
+ return __spreadValues({ loc, extraBefore }, mapper ? { source } : {});
11037
+ }
11038
+ function crMapper(raw, decoded) {
11039
+ if (raw.length === decoded.length) return (index) => index;
11040
+ const points = [];
11041
+ const extras = [];
11042
+ let r = 0;
11043
+ let d = 0;
11044
+ let extra = 0;
11045
+ while (d < decoded.length) {
11046
+ if (r >= raw.length) return null;
11047
+ if (raw[r] === decoded[d]) {
11048
+ r++;
11049
+ d++;
11050
+ continue;
11051
+ }
11052
+ if (raw[r] === "\r" && decoded[d] === "\n") {
11053
+ const consumed = raw[r + 1] === "\n" ? 2 : 1;
11054
+ extra += consumed - 1;
11055
+ points.push(d);
11056
+ extras.push(extra);
11057
+ r += consumed;
11058
+ d += 1;
11059
+ continue;
11060
+ }
11061
+ return null;
11062
+ }
11063
+ if (r !== raw.length) return null;
11064
+ return (index) => index + lookup(points, extras, index);
11065
+ }
11066
+ function lookup(points, extras, index) {
11067
+ let lo = 0;
11068
+ let hi = points.length - 1;
11069
+ let found = 0;
11070
+ while (lo <= hi) {
11071
+ const mid = lo + hi >> 1;
11072
+ if (points[mid] < index) {
11073
+ found = extras[mid];
11074
+ lo = mid + 1;
11075
+ } else {
11076
+ hi = mid - 1;
11077
+ }
11078
+ }
11079
+ return found;
11080
+ }
11081
+ function findRawOffset(raw, decoded, index, token) {
11082
+ if (!token) return -1;
11083
+ let occurrence = 0;
11084
+ for (let at = decoded.indexOf(token); at !== -1 && at < index; at = decoded.indexOf(token, at + 1)) {
11085
+ occurrence++;
11086
+ }
11087
+ let found = -1;
11088
+ let from = 0;
11089
+ for (let i = 0; i <= occurrence; i++) {
11090
+ found = raw.indexOf(token, from);
11091
+ if (found === -1) return -1;
11092
+ from = found + 1;
11093
+ }
11094
+ return found;
11095
+ }
11096
+ function positionOf(source, offset) {
11097
+ const prefix = source.slice(0, offset);
11098
+ return { line: prefix.split("\n").length, column: offset - prefix.lastIndexOf("\n") };
11099
+ }
11100
+ function crOffsetter(text, rawLength) {
11101
+ const removed = rawLength - text.length;
11102
+ if (removed === 0) return () => 0;
11103
+ const newlines = countNewlines(text, text.length);
11104
+ if (removed !== newlines || newlines === 0) return null;
11105
+ return (index) => countNewlines(text, index);
11106
+ }
11107
+ function countNewlines(text, upTo) {
11108
+ let n = 0;
11109
+ for (let i = 0; i < upTo && i < text.length; i++) if (text.charCodeAt(i) === 10) n++;
11110
+ return n;
11111
+ }
11112
+ function locInCssBlock(anchor, cssLoc) {
11113
+ if (!anchor || !cssLoc) return void 0;
11114
+ const { loc: block, extraBefore } = anchor;
11115
+ if (!extraBefore) {
11116
+ return {
11117
+ line: block.startLine,
11118
+ column: block.startCol,
11119
+ endLine: block.startLine,
11120
+ endColumn: block.startCol,
11121
+ offset: block.startOffset,
11122
+ length: 0
11123
+ };
11124
+ }
11125
+ const line = block.startLine + cssLoc.start.line - 1;
11126
+ const column = cssLoc.start.line === 1 ? block.startCol + cssLoc.start.column - 1 : cssLoc.start.column;
11127
+ const endLine = block.startLine + cssLoc.end.line - 1;
11128
+ const endColumn = cssLoc.end.line === 1 ? block.startCol + cssLoc.end.column - 1 : cssLoc.end.column;
11129
+ const start = block.startOffset + cssLoc.start.offset + extraBefore(cssLoc.start.offset);
11130
+ const end = block.startOffset + cssLoc.end.offset + extraBefore(cssLoc.end.offset);
11131
+ if (anchor.source) {
11132
+ const from = positionOf(anchor.source, start);
11133
+ const to = positionOf(anchor.source, end);
11134
+ return {
11135
+ line: from.line,
11136
+ column: from.column,
11137
+ endLine: to.line,
11138
+ endColumn: to.column,
11139
+ offset: start,
11140
+ length: end - start
11141
+ };
11142
+ }
11143
+ return { line, column, endLine, endColumn, offset: start, length: end - start };
11144
+ }
11145
+ function locInTextNode(node, index, length, source) {
11146
+ var _a;
11147
+ const anchor = node == null ? void 0 : node.sourceCodeLocation;
11148
+ if (!anchor) return void 0;
11149
+ const data = (_a = node.data) != null ? _a : "";
11150
+ const rawLength = anchor.endOffset - anchor.startOffset;
11151
+ if (source) {
11152
+ const raw = source.slice(anchor.startOffset, anchor.endOffset);
11153
+ const token = data.slice(index, index + length);
11154
+ const at = findRawOffset(raw, data, index, token);
11155
+ if (at !== -1) {
11156
+ const start2 = anchor.startOffset + at;
11157
+ const from = positionOf(source, start2);
11158
+ const to = positionOf(source, start2 + token.length);
11159
+ return {
11160
+ line: from.line,
11161
+ column: from.column,
11162
+ endLine: to.line,
11163
+ endColumn: to.column,
11164
+ offset: start2,
11165
+ length: token.length
11166
+ };
11167
+ }
11168
+ }
11169
+ const extraBefore = crOffsetter(data, rawLength);
11170
+ if (!extraBefore) {
11171
+ const clamped = Math.min(length, rawLength);
11172
+ return {
11173
+ line: anchor.startLine,
11174
+ column: anchor.startCol,
11175
+ endLine: anchor.startLine,
11176
+ endColumn: anchor.startCol + clamped,
11177
+ offset: anchor.startOffset,
11178
+ length: clamped
11179
+ };
11180
+ }
11181
+ const start = positionAt(data, index, anchor);
11182
+ const end = positionAt(data, index + length, anchor);
11183
+ const startOffset = anchor.startOffset + index + extraBefore(index);
11184
+ const endOffset = anchor.startOffset + index + length + extraBefore(index + length);
11185
+ return {
11186
+ line: start.line,
11187
+ column: start.column,
11188
+ endLine: end.line,
11189
+ endColumn: end.column,
11190
+ offset: startOffset,
11191
+ length: endOffset - startOffset
11192
+ };
11193
+ }
11194
+ function positionAt(data, index, anchor) {
11195
+ const prefix = data.slice(0, index);
11196
+ const newlines = prefix.split("\n").length - 1;
11197
+ if (newlines === 0) {
11198
+ return { line: anchor.startLine, column: anchor.startCol + index };
11199
+ }
11200
+ return { line: anchor.startLine + newlines, column: index - prefix.lastIndexOf("\n") };
11201
+ }
11202
+
10759
11203
  // src/dark-mode-checker.ts
10760
11204
  var DARK_MEDIA_RE = /\(\s*prefers-color-scheme\s*:\s*dark\s*\)/i;
10761
11205
  var MAX_UNCOVERED_ELEMENTS = 3;
@@ -10852,15 +11296,16 @@ function checkDarkModeFromDom($) {
10852
11296
  return name === "color-scheme" || name === "supported-color-schemes";
10853
11297
  });
10854
11298
  if (!hasOptIn) {
11299
+ const headLoc = locOfFirst($, "head");
10855
11300
  for (const clientId of PREFERS_COLOR_SCHEME_CLIENTS) {
10856
- warnings.push({
11301
+ warnings.push(__spreadValues({
10857
11302
  severity: "warning",
10858
11303
  client: clientId,
10859
11304
  property: "dark-mode-opt-in",
10860
11305
  message: `The email has @media (prefers-color-scheme: dark) styles but no dark-mode opt-in meta tag. ${(_b = (_a = getClient(clientId)) == null ? void 0 : _a.name) != null ? _b : clientId} may keep the email in light mode, so the dark styles never activate.`,
10861
11306
  suggestion: 'Add both opt-in tags to <head>: <meta name="color-scheme" content="light dark"> and <meta name="supported-color-schemes" content="light dark">.',
10862
11307
  fixType: "structural"
10863
- });
11308
+ }, headLoc ? { loc: headLoc, locs: [headLoc] } : {}));
10864
11309
  }
10865
11310
  }
10866
11311
  if (darkBlock.rules === 0) return warnings;
@@ -10878,8 +11323,9 @@ function checkDarkModeFromDom($) {
10878
11323
  if (inline ? coveredByImportant.has(el) : coveredByAny.has(el)) return;
10879
11324
  uncovered++;
10880
11325
  const selector = describeSelector($, el);
11326
+ const loc = locOfAttr(el, inline ? "style" : "bgcolor");
10881
11327
  for (const clientId of PREFERS_COLOR_SCHEME_CLIENTS) {
10882
- warnings.push({
11328
+ warnings.push(__spreadValues({
10883
11329
  severity: "warning",
10884
11330
  client: clientId,
10885
11331
  property: "dark-mode-coverage",
@@ -10887,12 +11333,25 @@ function checkDarkModeFromDom($) {
10887
11333
  suggestion: inline ? `Override it inside @media (prefers-color-scheme: dark) with a dark background-color and !important (an inline style beats a plain rule), or move the colour onto a class the dark block already targets.` : `Override it inside @media (prefers-color-scheme: dark) with a dark background-color, or give the element a class the dark block already targets.`,
10888
11334
  fixType: "css",
10889
11335
  selector
10890
- });
11336
+ }, loc ? { loc, locs: [loc] } : {}));
10891
11337
  }
10892
11338
  });
10893
11339
  return warnings;
10894
11340
  }
10895
11341
 
11342
+ // src/parse-html.ts
11343
+ var cheerio4 = __toESM(require("cheerio"), 1);
11344
+ function loadHtml(html, options) {
11345
+ return (options == null ? void 0 : options.positions) ? cheerio4.load(html, { sourceCodeLocationInfo: true }) : cheerio4.load(html);
11346
+ }
11347
+ function fromHtml(html, empty, fn, options) {
11348
+ if (!html || !html.trim()) return empty;
11349
+ if (html.length > MAX_HTML_SIZE) {
11350
+ throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
11351
+ }
11352
+ return fn(loadHtml(html, options), html);
11353
+ }
11354
+
10896
11355
  // src/analyze.ts
10897
11356
  var HTML_ELEMENT_SELECTORS = {
10898
11357
  "<style>": "style",
@@ -10945,14 +11404,25 @@ var CSS_FUNCTION_DETECTORS = CSS_FUNCTION_FEATURES.map((fn) => ({
10945
11404
  pattern: `${fn}(`
10946
11405
  // require opening paren — matches "min(" but not "Minion"
10947
11406
  }));
10948
- function analyzeEmailFromDom($, framework) {
11407
+ function analyzeEmailFromDom($, framework, source) {
10949
11408
  const warnings = [];
10950
- const seenWarnings = /* @__PURE__ */ new Set();
11409
+ const seenWarnings = /* @__PURE__ */ new Map();
10951
11410
  function addWarning(w) {
10952
11411
  const key = `${w.client}:${w.property}:${w.severity}:${w.selector || ""}`;
10953
- if (!seenWarnings.has(key)) {
10954
- seenWarnings.add(key);
11412
+ const existing = seenWarnings.get(key);
11413
+ if (!existing) {
11414
+ seenWarnings.set(key, w);
10955
11415
  warnings.push(w);
11416
+ return;
11417
+ }
11418
+ if (!existing.locs || !w.locs) return;
11419
+ for (const loc of w.locs) {
11420
+ if (existing.locs.some((l) => l.offset === loc.offset)) continue;
11421
+ if (existing.locs.length >= MAX_WARNING_LOCATIONS) {
11422
+ existing.locsTruncated = true;
11423
+ break;
11424
+ }
11425
+ existing.locs.push(loc);
10956
11426
  }
10957
11427
  }
10958
11428
  function describeSelector2(el) {
@@ -10970,10 +11440,16 @@ function analyzeEmailFromDom($, framework) {
10970
11440
  for (const feature of HTML_ELEMENT_FEATURES) {
10971
11441
  const selector = HTML_ELEMENT_SELECTORS[feature];
10972
11442
  if (!selector) continue;
10973
- if ($(selector).length === 0) continue;
11443
+ const matches = $(selector);
11444
+ if (matches.length === 0) continue;
10974
11445
  const supportData = CSS_SUPPORT[feature];
10975
11446
  if (!supportData) continue;
10976
11447
  const baseSeverity = HTML_ELEMENT_SEVERITY[feature] || "warning";
11448
+ const found = matches.toArray().map((m) => locOfElement(m)).filter((l) => l !== void 0);
11449
+ const featureOccurrences = found.length ? __spreadValues({
11450
+ locs: found.slice(0, MAX_WARNING_LOCATIONS)
11451
+ }, found.length > MAX_WARNING_LOCATIONS ? { truncated: true } : {}) : void 0;
11452
+ const featureLoc = featureOccurrences == null ? void 0 : featureOccurrences.locs[0];
10977
11453
  for (const client of EMAIL_CLIENTS) {
10978
11454
  const support = supportData[client.id];
10979
11455
  if (support === "unsupported") {
@@ -10981,7 +11457,7 @@ function analyzeEmailFromDom($, framework) {
10981
11457
  const message = msgFn ? msgFn(client.name) : `${client.name} does not support ${feature}.`;
10982
11458
  const sug = getSuggestion(feature, client.id, framework);
10983
11459
  const fix = getCodeFix(feature, client.id, framework);
10984
- addWarning(__spreadValues({
11460
+ addWarning(__spreadValues(__spreadValues({
10985
11461
  severity: baseSeverity,
10986
11462
  client: client.id,
10987
11463
  property: feature,
@@ -10989,11 +11465,11 @@ function analyzeEmailFromDom($, framework) {
10989
11465
  suggestion: sug.text,
10990
11466
  fix,
10991
11467
  fixType: getFixType(feature)
10992
- }, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(feature, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
11468
+ }, featureOccurrences ? occurrenceFields(featureOccurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(feature, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
10993
11469
  } else if (support === "partial" && feature === "<style>") {
10994
11470
  const sug = getSuggestion("<style>:partial", client.id, framework);
10995
11471
  const fix = getCodeFix("<style>", client.id, framework);
10996
- addWarning(__spreadValues({
11472
+ addWarning(__spreadValues(__spreadValues({
10997
11473
  severity: "warning",
10998
11474
  client: client.id,
10999
11475
  property: "<style>",
@@ -11001,55 +11477,97 @@ function analyzeEmailFromDom($, framework) {
11001
11477
  suggestion: sug.text,
11002
11478
  fix,
11003
11479
  fixType: getFixType("<style>")
11004
- }, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<style>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
11480
+ }, featureOccurrences ? occurrenceFields(featureOccurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<style>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
11005
11481
  }
11006
11482
  }
11007
11483
  }
11008
11484
  const parsedAtRules = /* @__PURE__ */ new Set();
11485
+ const selectorLocs = /* @__PURE__ */ new Map();
11009
11486
  const parsedProperties = /* @__PURE__ */ new Set();
11010
11487
  const propertyLines = /* @__PURE__ */ new Map();
11488
+ const propertyLocs = /* @__PURE__ */ new Map();
11011
11489
  const propertyValues = /* @__PURE__ */ new Map();
11012
11490
  const detectedCssFunctions = /* @__PURE__ */ new Set();
11013
11491
  const detectedPseudoClasses = /* @__PURE__ */ new Set();
11014
11492
  const detectedPseudoElements = /* @__PURE__ */ new Set();
11493
+ let blockAnchor;
11494
+ function recordSelectorLoc(key, cssLoc) {
11495
+ if (!cssLoc) return;
11496
+ const loc = locInCssBlock(blockAnchor, cssLoc);
11497
+ if (!loc) return;
11498
+ const seen = selectorLocs.get(key);
11499
+ if (!seen) {
11500
+ selectorLocs.set(key, { locs: [loc] });
11501
+ return;
11502
+ }
11503
+ if (seen.locs.some((l) => l.offset === loc.offset)) return;
11504
+ if (seen.locs.length >= MAX_WARNING_LOCATIONS) {
11505
+ seen.truncated = true;
11506
+ return;
11507
+ }
11508
+ seen.locs.push(loc);
11509
+ }
11510
+ function recordLoc(key, cssLoc, value) {
11511
+ const loc = locInCssBlock(blockAnchor, cssLoc);
11512
+ if (!loc) return;
11513
+ const seen = propertyLocs.get(key);
11514
+ if (!seen) {
11515
+ propertyLocs.set(key, __spreadValues({ locs: [loc] }, value !== void 0 ? { values: [value] } : {}));
11516
+ return;
11517
+ }
11518
+ if (seen.locs.some((l) => l.offset === loc.offset)) return;
11519
+ if (seen.locs.length >= MAX_WARNING_LOCATIONS) {
11520
+ seen.truncated = true;
11521
+ return;
11522
+ }
11523
+ seen.locs.push(loc);
11524
+ if (seen.values && value !== void 0) seen.values.push(value);
11525
+ }
11015
11526
  $("style").each((_, el) => {
11016
11527
  const cssText = $(el).text();
11528
+ blockAnchor = cssBlockAnchor(el, cssText, source);
11017
11529
  try {
11018
11530
  const ast = csstree5.parse(cssText, { parseCustomProperty: true, positions: true });
11019
11531
  csstree5.walk(ast, {
11020
11532
  enter(node) {
11021
11533
  if (node.type === "Atrule") {
11022
11534
  parsedAtRules.add(`@${node.name}`);
11535
+ recordSelectorLoc(`@${node.name}`, node.loc);
11023
11536
  }
11024
11537
  if (node.type === "PseudoClassSelector") {
11025
11538
  detectedPseudoClasses.add(`:${node.name}`);
11539
+ recordSelectorLoc(`:${node.name}`, node.loc);
11026
11540
  }
11027
11541
  if (node.type === "PseudoElementSelector") {
11028
11542
  detectedPseudoElements.add(`::${node.name}`);
11543
+ recordSelectorLoc(`::${node.name}`, node.loc);
11029
11544
  }
11030
11545
  if (node.type === "Declaration") {
11031
11546
  const prop = node.property.toLowerCase();
11032
11547
  parsedProperties.add(prop);
11033
- if (node.loc && !propertyLines.has(prop)) {
11034
- propertyLines.set(prop, node.loc.start.line);
11035
- }
11036
11548
  const valueStr = csstree5.generate(node.value);
11037
11549
  const seenValues = propertyValues.get(prop);
11038
11550
  if (seenValues) seenValues.push(valueStr);
11039
11551
  else propertyValues.set(prop, [valueStr]);
11552
+ if (node.loc) {
11553
+ if (!propertyLines.has(prop)) propertyLines.set(prop, node.loc.start.line);
11554
+ recordLoc(prop, node.loc, valueStr);
11555
+ }
11040
11556
  for (const det of COMPOUND_DETECTORS) {
11041
- if (prop === det.property && valueStr.includes(det.valueIncludes)) {
11557
+ if (prop === det.property && valueStr.toLowerCase().includes(det.valueIncludes)) {
11042
11558
  parsedProperties.add(det.key);
11043
- if (node.loc && !propertyLines.has(det.key)) {
11044
- propertyLines.set(det.key, node.loc.start.line);
11559
+ if (node.loc) {
11560
+ if (!propertyLines.has(det.key)) propertyLines.set(det.key, node.loc.start.line);
11561
+ recordLoc(det.key, node.loc);
11045
11562
  }
11046
11563
  }
11047
11564
  }
11048
11565
  for (const fn of CSS_FUNCTION_DETECTORS) {
11049
11566
  if (valueStr.includes(fn.pattern)) {
11050
11567
  detectedCssFunctions.add(fn.key);
11051
- if (node.loc && !propertyLines.has(fn.key)) {
11052
- propertyLines.set(fn.key, node.loc.start.line);
11568
+ if (node.loc) {
11569
+ if (!propertyLines.has(fn.key)) propertyLines.set(fn.key, node.loc.start.line);
11570
+ recordLoc(fn.key, node.loc);
11053
11571
  }
11054
11572
  }
11055
11573
  }
@@ -11061,33 +11579,42 @@ function analyzeEmailFromDom($, framework) {
11061
11579
  });
11062
11580
  for (const atRule of AT_RULE_FEATURES) {
11063
11581
  if (!parsedAtRules.has(atRule)) continue;
11064
- checkPropertySupport(atRule, addWarning, framework);
11582
+ checkPropertySupport(atRule, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(atRule));
11065
11583
  }
11066
11584
  const cssPropertiesToCheck = Object.keys(CSS_SUPPORT).filter(
11067
11585
  (k) => !k.startsWith("<") && !k.startsWith("@")
11068
11586
  );
11069
11587
  $("[style]").each((_, el) => {
11070
- var _a;
11071
11588
  const style = $(el).attr("style") || "";
11072
11589
  const props = parseStyleProperties(style);
11073
11590
  const selector = describeSelector2(el);
11591
+ const locs = elementLocs(locOfAttr(el, "style"));
11074
11592
  for (const prop of props) {
11075
11593
  for (const det of COMPOUND_DETECTORS) {
11076
11594
  if (prop === det.property) {
11077
11595
  const value2 = getStyleValue(style, prop);
11078
- if (value2 == null ? void 0 : value2.includes(det.valueIncludes)) {
11079
- checkPropertySupport(det.key, addWarning, framework, selector);
11596
+ if (value2 == null ? void 0 : value2.toLowerCase().includes(det.valueIncludes)) {
11597
+ checkPropertySupport(det.key, addWarning, framework, selector, void 0, void 0, locs);
11080
11598
  }
11081
11599
  }
11082
11600
  }
11083
11601
  if (cssPropertiesToCheck.includes(prop)) {
11084
- checkPropertySupport(prop, addWarning, framework, selector, void 0, (_a = getStyleValue(style, prop)) != null ? _a : void 0);
11602
+ const declared = getStyleValues(style, prop);
11603
+ checkPropertySupport(
11604
+ prop,
11605
+ addWarning,
11606
+ framework,
11607
+ selector,
11608
+ void 0,
11609
+ declared.length ? declared : void 0,
11610
+ locs
11611
+ );
11085
11612
  }
11086
11613
  const value = getStyleValue(style, prop);
11087
11614
  if (value) {
11088
11615
  for (const fn of CSS_FUNCTION_DETECTORS) {
11089
11616
  if (value.includes(fn.pattern)) {
11090
- checkPropertySupport(fn.key, addWarning, framework, selector);
11617
+ checkPropertySupport(fn.key, addWarning, framework, selector, void 0, void 0, locs);
11091
11618
  }
11092
11619
  }
11093
11620
  }
@@ -11103,87 +11630,66 @@ function analyzeEmailFromDom($, framework) {
11103
11630
  framework,
11104
11631
  void 0,
11105
11632
  propertyLines.get(prop),
11106
- values ? values.join(" ") : void 0
11633
+ values,
11634
+ propertyLocs.get(prop)
11107
11635
  );
11108
11636
  }
11109
11637
  for (const compound of COMPOUND_VALUE_FEATURES) {
11110
11638
  if (compound.startsWith(":") || compound.startsWith("::")) continue;
11111
11639
  if (parsedProperties.has(compound)) {
11112
- checkPropertySupport(compound, addWarning, framework, void 0, propertyLines.get(compound));
11640
+ checkPropertySupport(compound, addWarning, framework, void 0, propertyLines.get(compound), void 0, propertyLocs.get(compound));
11113
11641
  }
11114
11642
  }
11115
11643
  for (const pseudo of detectedPseudoClasses) {
11116
11644
  if (CSS_SUPPORT[pseudo]) {
11117
- checkPropertySupport(pseudo, addWarning, framework);
11645
+ checkPropertySupport(pseudo, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(pseudo));
11118
11646
  }
11119
11647
  }
11120
11648
  for (const pseudo of detectedPseudoElements) {
11121
11649
  if (CSS_SUPPORT[pseudo]) {
11122
- checkPropertySupport(pseudo, addWarning, framework);
11650
+ checkPropertySupport(pseudo, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(pseudo));
11123
11651
  }
11124
11652
  }
11125
11653
  for (const fn of detectedCssFunctions) {
11126
- checkPropertySupport(fn, addWarning, framework, void 0, propertyLines.get(fn));
11654
+ checkPropertySupport(fn, addWarning, framework, void 0, propertyLines.get(fn), void 0, propertyLocs.get(fn));
11127
11655
  }
11128
11656
  for (const w of checkDarkModeFromDom($)) addWarning(w);
11129
11657
  const severityOrder = { error: 0, warning: 1, info: 2 };
11130
11658
  warnings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
11131
11659
  return warnings;
11132
11660
  }
11133
- function analyzeEmail(html, framework) {
11661
+ function analyzeEmail(html, framework, options) {
11134
11662
  if (!html || !html.trim()) {
11135
11663
  return [];
11136
11664
  }
11137
11665
  if (html.length > MAX_HTML_SIZE) {
11138
11666
  throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
11139
11667
  }
11140
- const $ = cheerio4.load(html);
11141
- return analyzeEmailFromDom($, framework);
11668
+ const $ = loadHtml(html, options);
11669
+ return analyzeEmailFromDom($, framework, (options == null ? void 0 : options.positions) ? html : void 0);
11142
11670
  }
11143
11671
  function getFixType(prop) {
11144
11672
  return STRUCTURAL_FIX_PROPERTIES.has(prop) ? "structural" : "css";
11145
11673
  }
11146
- var VALUE_CAVEAT_PROPS = /* @__PURE__ */ new Set(["margin", "position", "overflow"]);
11147
- var POSITION_KEYWORDS = ["relative", "absolute", "fixed", "sticky"];
11148
- function valueTriggersCaveat(prop, value, notes) {
11149
- const note = (notes != null ? notes : []).join(" ");
11150
- const noteLc = note.toLowerCase();
11151
- if (prop === "margin") {
11152
- if (/(?:^|[\s:(])-\.?\d/.test(value) && noteLc.includes("negative")) return true;
11153
- if (/\bauto\b/.test(value) && noteLc.includes("auto")) return true;
11154
- return false;
11155
- }
11156
- if (prop === "position") {
11157
- const used = POSITION_KEYWORDS.find((k) => new RegExp(`\\b${k}\\b`).test(value));
11158
- if (!used) return false;
11159
- const m = note.match(/supports\s+.+?\s+but not\s+([^.]+)/i);
11160
- if (m) return m[1].toLowerCase().includes(used);
11161
- return used === "fixed" || used === "sticky";
11162
- }
11163
- if (prop === "overflow") {
11164
- if (!/\b(?:auto|scroll)\b/.test(value)) return false;
11165
- return noteLc.includes("cannot scroll");
11166
- }
11167
- return true;
11168
- }
11169
11674
  function noteSuffix(notes) {
11170
11675
  if (!(notes == null ? void 0 : notes.length)) return "";
11171
11676
  const cleaned = notes.map((n) => n.replace(/^(?:Partial|Buggy|Not supported)\.\s*/i, "").trim()).filter(Boolean);
11172
11677
  return cleaned.length ? ` ${cleaned.join(" ")}` : "";
11173
11678
  }
11174
- function checkPropertySupport(prop, addWarning, framework, selector, line, value) {
11175
- var _a;
11679
+ function checkPropertySupport(prop, addWarning, framework, selector, line, values, occurrences) {
11680
+ var _a, _b, _c, _d, _e, _f;
11681
+ const loc = occurrences == null ? void 0 : occurrences.locs[0];
11682
+ const reportedLine = (_a = loc == null ? void 0 : loc.line) != null ? _a : line;
11176
11683
  const supportData = CSS_SUPPORT[prop];
11177
11684
  if (!supportData) return;
11178
11685
  const fixType = getFixType(prop);
11179
- const valueGated = VALUE_CAVEAT_PROPS.has(prop);
11180
11686
  for (const client of EMAIL_CLIENTS) {
11181
11687
  const support = supportData[client.id] || "unknown";
11182
- const notes = (_a = CSS_SUPPORT_NOTES[prop]) == null ? void 0 : _a[client.id];
11688
+ const notes = (_b = CSS_SUPPORT_NOTES[prop]) == null ? void 0 : _b[client.id];
11183
11689
  if (support === "unsupported") {
11184
11690
  const sug = getSuggestion(prop, client.id, framework);
11185
11691
  const fix = getCodeFix(prop, client.id, framework);
11186
- addWarning(__spreadValues(__spreadValues(__spreadValues({
11692
+ addWarning(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
11187
11693
  severity: "warning",
11188
11694
  client: client.id,
11189
11695
  property: prop,
@@ -11191,12 +11697,13 @@ function checkPropertySupport(prop, addWarning, framework, selector, line, value
11191
11697
  suggestion: sug.text,
11192
11698
  fix,
11193
11699
  fixType
11194
- }, selector ? { selector } : {}), line !== void 0 ? { line } : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
11700
+ }, selector ? { selector } : {}), reportedLine !== void 0 ? { line: reportedLine } : {}), occurrences ? occurrenceFields(occurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
11195
11701
  } else if (support === "partial") {
11196
- if (valueGated && value !== void 0 && !valueTriggersCaveat(prop, value, notes)) continue;
11702
+ if (!caveatApplies(prop, values, notes)) continue;
11703
+ const hits = triggeringOccurrences(prop, occurrences, notes);
11197
11704
  const sug = getSuggestion(prop, client.id, framework);
11198
11705
  const fix = getCodeFix(prop, client.id, framework);
11199
- addWarning(__spreadValues(__spreadValues(__spreadValues({
11706
+ addWarning(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
11200
11707
  severity: "info",
11201
11708
  client: client.id,
11202
11709
  property: prop,
@@ -11204,7 +11711,7 @@ function checkPropertySupport(prop, addWarning, framework, selector, line, value
11204
11711
  suggestion: sug.text,
11205
11712
  fix,
11206
11713
  fixType
11207
- }, selector ? { selector } : {}), line !== void 0 ? { line } : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
11714
+ }, selector ? { selector } : {}), ((_d = (_c = hits == null ? void 0 : hits.locs[0]) == null ? void 0 : _c.line) != null ? _d : reportedLine) !== void 0 ? { line: (_f = (_e = hits == null ? void 0 : hits.locs[0]) == null ? void 0 : _e.line) != null ? _f : reportedLine } : {}), hits ? occurrenceFields(hits) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
11208
11715
  }
11209
11716
  }
11210
11717
  }
@@ -11223,6 +11730,19 @@ function generateCompatibilityScore(warnings) {
11223
11730
  }
11224
11731
  return result;
11225
11732
  }
11733
+ function occurrenceFields({ locs, truncated }) {
11734
+ return __spreadValues({ loc: locs[0], locs: [...locs] }, truncated ? { locsTruncated: true } : {});
11735
+ }
11736
+ function triggeringOccurrences(prop, occurrences, notes) {
11737
+ const values = occurrences == null ? void 0 : occurrences.values;
11738
+ if (!occurrences || !values) return occurrences;
11739
+ const locs = occurrences.locs.filter((_, i) => caveatApplies(prop, [values[i]], notes));
11740
+ if (!locs.length || locs.length === occurrences.locs.length) return occurrences;
11741
+ return __spreadValues({ locs }, occurrences.truncated ? { truncated: true } : {});
11742
+ }
11743
+ function elementLocs(loc) {
11744
+ return loc ? { locs: [loc] } : void 0;
11745
+ }
11226
11746
  function warningsForClient(warnings, clientId) {
11227
11747
  return warnings.filter((w) => w.client === clientId);
11228
11748
  }
@@ -11563,16 +12083,6 @@ function extractCode(response) {
11563
12083
  return response.trim();
11564
12084
  }
11565
12085
 
11566
- // src/parse-html.ts
11567
- var cheerio5 = __toESM(require("cheerio"), 1);
11568
- function fromHtml(html, empty, fn) {
11569
- if (!html || !html.trim()) return empty;
11570
- if (html.length > MAX_HTML_SIZE) {
11571
- throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
11572
- }
11573
- return fn(cheerio5.load(html), html);
11574
- }
11575
-
11576
12086
  // src/spam-scorer.ts
11577
12087
  var SPAM_TRIGGER_PHRASES = [
11578
12088
  "act now",
@@ -12041,6 +12551,8 @@ function validateLinksFromDom($) {
12041
12551
  const href = $(el).attr("href") || "";
12042
12552
  const text = $(el).text().trim();
12043
12553
  const category = classifyHref(href);
12554
+ const elLoc = locOfElement(el);
12555
+ const hrefLoc = href ? locOfAttr(el, "href") : elLoc;
12044
12556
  switch (category) {
12045
12557
  case "https":
12046
12558
  breakdown.https++;
@@ -12071,95 +12583,95 @@ function validateLinksFromDom($) {
12071
12583
  hrefCounts.set(href, (hrefCounts.get(href) || 0) + 1);
12072
12584
  }
12073
12585
  if (!href || !href.trim()) {
12074
- issues.push({
12586
+ issues.push(__spreadValues({
12075
12587
  severity: "error",
12076
12588
  rule: "empty-href",
12077
12589
  message: "Link has no href attribute",
12078
12590
  text: text.slice(0, 80) || "(no text)"
12079
- });
12591
+ }, elLoc ? { loc: elLoc } : {}));
12080
12592
  return;
12081
12593
  }
12082
12594
  if (category === "javascript" && !isPlaceholderHref(href)) {
12083
- issues.push({
12595
+ issues.push(__spreadValues({
12084
12596
  severity: "error",
12085
12597
  rule: "javascript-href",
12086
12598
  message: "Link uses javascript: protocol",
12087
12599
  href: href.slice(0, 100),
12088
12600
  text: text.slice(0, 80) || "(no text)"
12089
- });
12601
+ }, hrefLoc ? { loc: hrefLoc } : {}));
12090
12602
  return;
12091
12603
  }
12092
12604
  if (isPlaceholderHref(href)) {
12093
- issues.push({
12605
+ issues.push(__spreadValues({
12094
12606
  severity: "warning",
12095
12607
  rule: "placeholder-href",
12096
12608
  message: "Link has a placeholder href (# or javascript:void)",
12097
12609
  href,
12098
12610
  text: text.slice(0, 80) || "(no text)"
12099
- });
12611
+ }, hrefLoc ? { loc: hrefLoc } : {}));
12100
12612
  return;
12101
12613
  }
12102
12614
  if (category === "http") {
12103
- issues.push({
12615
+ issues.push(__spreadValues({
12104
12616
  severity: "warning",
12105
12617
  rule: "insecure-link",
12106
12618
  message: "Link uses HTTP instead of HTTPS",
12107
12619
  href: href.slice(0, 120),
12108
12620
  text: text.slice(0, 80) || "(no text)"
12109
- });
12621
+ }, hrefLoc ? { loc: hrefLoc } : {}));
12110
12622
  }
12111
12623
  if (category === "protocol-relative") {
12112
- issues.push({
12624
+ issues.push(__spreadValues({
12113
12625
  severity: "warning",
12114
12626
  rule: "protocol-relative",
12115
12627
  message: "Protocol-relative URL may break in email clients \u2014 use https:// explicitly",
12116
12628
  href: href.slice(0, 120),
12117
12629
  text: text.slice(0, 80) || "(no text)"
12118
- });
12630
+ }, hrefLoc ? { loc: hrefLoc } : {}));
12119
12631
  }
12120
12632
  if (text && GENERIC_LINK_TEXT.has(text.toLowerCase())) {
12121
- issues.push({
12633
+ issues.push(__spreadValues({
12122
12634
  severity: "warning",
12123
12635
  rule: "generic-link-text",
12124
12636
  message: `Link text "${text}" is vague \u2014 use descriptive text for accessibility and engagement`,
12125
12637
  href: href.slice(0, 120),
12126
12638
  text
12127
- });
12639
+ }, elLoc ? { loc: elLoc } : {}));
12128
12640
  }
12129
12641
  if (!text && !$(el).attr("aria-label") && !$(el).find("img[alt]").length) {
12130
- issues.push({
12642
+ issues.push(__spreadValues({
12131
12643
  severity: "error",
12132
12644
  rule: "empty-link-text",
12133
12645
  message: "Link has no visible text or aria-label",
12134
12646
  href: href.slice(0, 120)
12135
- });
12647
+ }, elLoc ? { loc: elLoc } : {}));
12136
12648
  }
12137
12649
  if (category === "mailto" && href.trim().toLowerCase() === "mailto:") {
12138
- issues.push({
12650
+ issues.push(__spreadValues({
12139
12651
  severity: "error",
12140
12652
  rule: "empty-mailto",
12141
12653
  message: "mailto: link has no email address",
12142
12654
  href,
12143
12655
  text: text.slice(0, 80) || "(no text)"
12144
- });
12656
+ }, hrefLoc ? { loc: hrefLoc } : {}));
12145
12657
  }
12146
12658
  if (category === "tel" && href.trim().toLowerCase() === "tel:") {
12147
- issues.push({
12659
+ issues.push(__spreadValues({
12148
12660
  severity: "error",
12149
12661
  rule: "empty-tel",
12150
12662
  message: "tel: link has no phone number",
12151
12663
  href,
12152
12664
  text: text.slice(0, 80) || "(no text)"
12153
- });
12665
+ }, hrefLoc ? { loc: hrefLoc } : {}));
12154
12666
  }
12155
12667
  if (href.length > 2e3) {
12156
- issues.push({
12668
+ issues.push(__spreadValues({
12157
12669
  severity: "info",
12158
12670
  rule: "long-url",
12159
12671
  message: "URL exceeds 2000 characters \u2014 may be truncated by some email clients",
12160
12672
  href: href.slice(0, 120) + "...",
12161
12673
  text: text.slice(0, 80) || "(no text)"
12162
- });
12674
+ }, hrefLoc ? { loc: hrefLoc } : {}));
12163
12675
  }
12164
12676
  });
12165
12677
  links.each((_, el) => {
@@ -12168,14 +12680,15 @@ function validateLinksFromDom($) {
12168
12680
  if (trimmed.startsWith("#") && trimmed.length > 1) {
12169
12681
  const targetId = trimmed.slice(1);
12170
12682
  const target = $(`[id="${targetId}"]`);
12683
+ const anchorLoc = locOfAttr(el, "href");
12171
12684
  if (target.length === 0) {
12172
- issues.push({
12685
+ issues.push(__spreadValues({
12173
12686
  severity: "error",
12174
12687
  rule: "broken-anchor",
12175
12688
  message: `Anchor link "${trimmed}" points to an element that does not exist`,
12176
12689
  href: trimmed,
12177
12690
  text: $(el).text().trim().slice(0, 80) || "(no text)"
12178
- });
12691
+ }, anchorLoc ? { loc: anchorLoc } : {}));
12179
12692
  }
12180
12693
  }
12181
12694
  });
@@ -12191,8 +12704,8 @@ function validateLinksFromDom($) {
12191
12704
  }
12192
12705
  return { totalLinks, issues, breakdown };
12193
12706
  }
12194
- function validateLinks(html) {
12195
- return fromHtml(html, EMPTY_LINKS, validateLinksFromDom);
12707
+ function validateLinks(html, options) {
12708
+ return fromHtml(html, EMPTY_LINKS, validateLinksFromDom, options);
12196
12709
  }
12197
12710
 
12198
12711
  // src/accessibility-checker.ts
@@ -12217,24 +12730,31 @@ function describeElement($, el) {
12217
12730
  function checkLangAttribute($) {
12218
12731
  const lang = $("html").attr("lang");
12219
12732
  if (!lang || !lang.trim()) {
12220
- return {
12733
+ const loc = locOfFirst($, "html");
12734
+ return __spreadProps(__spreadValues({
12221
12735
  severity: "error",
12222
12736
  rule: "missing-lang",
12223
- message: "Missing lang attribute on <html> element",
12737
+ message: "Missing lang attribute on <html> element"
12738
+ }, loc ? { loc } : {}), {
12224
12739
  details: 'Screen readers use the lang attribute to determine pronunciation. Add lang="en" (or appropriate language code).'
12225
- };
12740
+ });
12226
12741
  }
12227
12742
  return null;
12228
12743
  }
12744
+ function titleLoc($) {
12745
+ return $("title").length ? locOfFirst($, "title") : locOfFirst($, "head");
12746
+ }
12229
12747
  function checkTitle($) {
12230
12748
  const title = $("title").text().trim();
12231
12749
  if (!title) {
12232
- return {
12750
+ const loc = titleLoc($);
12751
+ return __spreadProps(__spreadValues({
12233
12752
  severity: "warning",
12234
12753
  rule: "missing-title",
12235
- message: "Missing or empty <title> element",
12754
+ message: "Missing or empty <title> element"
12755
+ }, loc ? { loc } : {}), {
12236
12756
  details: "The <title> helps screen readers identify the email content."
12237
- };
12757
+ });
12238
12758
  }
12239
12759
  return null;
12240
12760
  }
@@ -12244,34 +12764,38 @@ function checkImageAlt($) {
12244
12764
  const alt = $(el).attr("alt");
12245
12765
  const src = $(el).attr("src") || "";
12246
12766
  const role = $(el).attr("role");
12767
+ const elLoc = locOfElement(el);
12247
12768
  if (role === "presentation" || role === "none") return;
12248
12769
  if (alt === void 0) {
12249
- issues.push({
12770
+ issues.push(__spreadProps(__spreadValues({
12250
12771
  severity: "error",
12251
12772
  rule: "img-missing-alt",
12252
12773
  message: "Image missing alt attribute",
12253
- element: describeElement($, el),
12774
+ element: describeElement($, el)
12775
+ }, elLoc ? { loc: elLoc } : {}), {
12254
12776
  details: 'Every image must have an alt attribute. Use alt="" for decorative images.'
12255
- });
12777
+ }));
12256
12778
  } else if (alt.trim() === "") {
12257
12779
  const isLikelyContent = !src.includes("spacer") && !src.includes("pixel") && !src.includes("tracking") && !src.includes("1x1") && !src.includes("transparent");
12258
12780
  if (isLikelyContent && ($(el).attr("width") || "0") !== "1") {
12259
- issues.push({
12781
+ issues.push(__spreadProps(__spreadValues({
12260
12782
  severity: "info",
12261
12783
  rule: "img-empty-alt",
12262
12784
  message: "Image has empty alt text \u2014 verify it is decorative",
12263
- element: describeElement($, el),
12785
+ element: describeElement($, el)
12786
+ }, locOfAttr(el, "alt") ? { loc: locOfAttr(el, "alt") } : {}), {
12264
12787
  details: "Empty alt is correct for decorative images, but content images need descriptive alt text."
12265
- });
12788
+ }));
12266
12789
  }
12267
12790
  } else if (/\.(png|jpg|jpeg|gif|svg|webp|bmp)$/i.test(alt)) {
12268
- issues.push({
12791
+ issues.push(__spreadProps(__spreadValues({
12269
12792
  severity: "error",
12270
12793
  rule: "img-filename-alt",
12271
12794
  message: "Image alt text is a filename, not a description",
12272
- element: describeElement($, el),
12795
+ element: describeElement($, el)
12796
+ }, locOfAttr(el, "alt") ? { loc: locOfAttr(el, "alt") } : {}), {
12273
12797
  details: `Alt "${alt}" should describe the image content, not the file name.`
12274
- });
12798
+ }));
12275
12799
  }
12276
12800
  });
12277
12801
  return issues;
@@ -12279,28 +12803,31 @@ function checkImageAlt($) {
12279
12803
  function checkLinkAccessibility($) {
12280
12804
  const issues = [];
12281
12805
  $("a").each((_, el) => {
12806
+ const elLoc = locOfElement(el);
12282
12807
  const text = $(el).text().trim().toLowerCase();
12283
12808
  const ariaLabel = $(el).attr("aria-label");
12284
12809
  const title = $(el).attr("title");
12285
12810
  const imgAlt = $(el).find("img").attr("alt");
12286
12811
  if (!text && !ariaLabel && !title && !imgAlt) {
12287
- issues.push({
12812
+ issues.push(__spreadProps(__spreadValues({
12288
12813
  severity: "error",
12289
12814
  rule: "link-no-accessible-name",
12290
12815
  message: "Link has no accessible name",
12291
- element: describeElement($, el),
12816
+ element: describeElement($, el)
12817
+ }, elLoc ? { loc: elLoc } : {}), {
12292
12818
  details: "Links need visible text, aria-label, or an image with alt text."
12293
- });
12819
+ }));
12294
12820
  return;
12295
12821
  }
12296
12822
  if (text && GENERIC_LINK_TEXT.has(text) && !ariaLabel) {
12297
- issues.push({
12823
+ issues.push(__spreadProps(__spreadValues({
12298
12824
  severity: "warning",
12299
12825
  rule: "link-generic-text",
12300
12826
  message: `Link text "${$(el).text().trim()}" is not descriptive`,
12301
- element: describeElement($, el),
12827
+ element: describeElement($, el)
12828
+ }, elLoc ? { loc: elLoc } : {}), {
12302
12829
  details: "Screen readers often list links out of context. Use text that describes the destination."
12303
- });
12830
+ }));
12304
12831
  }
12305
12832
  });
12306
12833
  return issues;
@@ -12310,18 +12837,20 @@ function checkTableAccessibility($) {
12310
12837
  $("table").each((_, el) => {
12311
12838
  if ($(el).parents('table[role="presentation"], table[role="none"]').length > 0) return;
12312
12839
  const role = $(el).attr("role");
12840
+ const tableLoc = locOfElement(el);
12313
12841
  const hasHeaders = $(el).find("th").length > 0;
12314
12842
  const looksLikeLayout = !hasHeaders;
12315
12843
  if (looksLikeLayout && role !== "presentation" && role !== "none") {
12316
12844
  const nestedTables = $(el).find("table").length;
12317
12845
  if (nestedTables > 0 || $(el).find("td").length > 2) {
12318
- issues.push({
12846
+ issues.push(__spreadProps(__spreadValues({
12319
12847
  severity: "info",
12320
12848
  rule: "table-missing-role",
12321
- message: 'Layout table missing role="presentation"',
12849
+ message: 'Layout table missing role="presentation"'
12850
+ }, tableLoc ? { loc: tableLoc } : {}), {
12322
12851
  element: `<table> with ${$(el).find("td").length} cells`,
12323
12852
  details: `Add role="presentation" to tables used for layout so screen readers don't announce them as data tables.`
12324
- });
12853
+ }));
12325
12854
  }
12326
12855
  }
12327
12856
  });
@@ -12332,6 +12861,7 @@ function checkTextSizeAndContrast($) {
12332
12861
  let smallTextCount = 0;
12333
12862
  $("[style]").each((_, el) => {
12334
12863
  const style = $(el).attr("style") || "";
12864
+ const styleLoc = locOfAttr(el, "style");
12335
12865
  const fontSizeMatch = style.match(/font-size\s*:\s*(\d+(?:\.\d+)?)(px|pt)/i);
12336
12866
  if (fontSizeMatch) {
12337
12867
  const size = parseFloat(fontSizeMatch[1]);
@@ -12340,13 +12870,14 @@ function checkTextSizeAndContrast($) {
12340
12870
  if (pxSize < 9 && pxSize > 0) {
12341
12871
  smallTextCount++;
12342
12872
  if (smallTextCount <= 3) {
12343
- issues.push({
12873
+ issues.push(__spreadProps(__spreadValues({
12344
12874
  severity: "warning",
12345
12875
  rule: "small-text",
12346
12876
  message: `Very small text (${fontSizeMatch[0].trim()})`,
12347
- element: describeElement($, el),
12877
+ element: describeElement($, el)
12878
+ }, styleLoc ? { loc: styleLoc } : {}), {
12348
12879
  details: "Text smaller than 9px is difficult to read, especially on mobile devices."
12349
- });
12880
+ }));
12350
12881
  }
12351
12882
  }
12352
12883
  }
@@ -12391,21 +12922,23 @@ function checkTextSizeAndContrast($) {
12391
12922
  }
12392
12923
  const grade = wcagGrade(ratio);
12393
12924
  if (grade === "Fail") {
12394
- issues.push({
12925
+ issues.push(__spreadProps(__spreadValues({
12395
12926
  severity: "error",
12396
12927
  rule: "low-contrast",
12397
12928
  message: `Low contrast ratio ${ratio.toFixed(1)}:1 \u2014 fails WCAG minimum`,
12398
- element: describeElement($, el),
12929
+ element: describeElement($, el)
12930
+ }, styleLoc ? { loc: styleLoc } : {}), {
12399
12931
  details: `Foreground ${colorValue} on background needs at least ${isLargeText ? "3:1" : "4.5:1"} contrast ratio.`
12400
- });
12932
+ }));
12401
12933
  } else if (!isLargeText && grade === "AA Large") {
12402
- issues.push({
12934
+ issues.push(__spreadProps(__spreadValues({
12403
12935
  severity: "warning",
12404
12936
  rule: "low-contrast",
12405
12937
  message: `Low contrast ratio ${ratio.toFixed(1)}:1 \u2014 fails WCAG AA for normal text`,
12406
- element: describeElement($, el),
12938
+ element: describeElement($, el)
12939
+ }, styleLoc ? { loc: styleLoc } : {}), {
12407
12940
  details: `Foreground ${colorValue} on background needs at least 4.5:1 for normal-sized text.`
12408
- });
12941
+ }));
12409
12942
  }
12410
12943
  }
12411
12944
  }
@@ -12428,29 +12961,32 @@ function checkCharsetDeclaration($) {
12428
12961
  const content = httpEquiv.attr("content") || "";
12429
12962
  if (/charset\s*=/i.test(content)) return null;
12430
12963
  }
12431
- return {
12964
+ const loc = locOfFirst($, "head");
12965
+ return __spreadProps(__spreadValues({
12432
12966
  severity: "warning",
12433
12967
  rule: "missing-charset",
12434
- message: "Missing charset declaration",
12968
+ message: "Missing charset declaration"
12969
+ }, loc ? { loc } : {}), {
12435
12970
  details: 'Add <meta charset="utf-8"> in <head> to prevent encoding issues across email clients.'
12436
- };
12971
+ });
12437
12972
  }
12438
12973
  function checkSemanticStructure($) {
12439
12974
  const issues = [];
12440
12975
  const headings = [];
12441
12976
  $("h1, h2, h3, h4, h5, h6").each((_, el) => {
12442
12977
  const level = parseInt(el.tagName.replace(/h/i, ""), 10);
12443
- headings.push({ level, text: $(el).text().trim().slice(0, 60) });
12978
+ headings.push({ level, text: $(el).text().trim().slice(0, 60), loc: locOfElement(el) });
12444
12979
  });
12445
12980
  for (let i = 1; i < headings.length; i++) {
12446
12981
  const gap = headings[i].level - headings[i - 1].level;
12447
12982
  if (gap > 1) {
12448
- issues.push({
12983
+ issues.push(__spreadProps(__spreadValues({
12449
12984
  severity: "info",
12450
12985
  rule: "heading-skip",
12451
- message: `Heading level skipped: h${headings[i - 1].level} to h${headings[i].level}`,
12986
+ message: `Heading level skipped: h${headings[i - 1].level} to h${headings[i].level}`
12987
+ }, headings[i].loc ? { loc: headings[i].loc } : {}), {
12452
12988
  details: "Skipped heading levels can confuse screen readers. Use sequential heading levels."
12453
- });
12989
+ }));
12454
12990
  break;
12455
12991
  }
12456
12992
  }
@@ -12491,8 +13027,8 @@ function checkAccessibilityFromDom($) {
12491
13027
  const score = Math.max(0, 100 - penalty);
12492
13028
  return { score, issues };
12493
13029
  }
12494
- function checkAccessibility(html) {
12495
- return fromHtml(html, EMPTY_ACCESSIBILITY, checkAccessibilityFromDom);
13030
+ function checkAccessibility(html, options) {
13031
+ return fromHtml(html, EMPTY_ACCESSIBILITY, checkAccessibilityFromDom, options);
12496
13032
  }
12497
13033
 
12498
13034
  // src/image-analyzer.ts
@@ -12539,6 +13075,8 @@ function analyzeImagesFromDom($) {
12539
13075
  const height = (_c = img.attr("height")) != null ? _c : null;
12540
13076
  const style = (img.attr("style") || "").toLowerCase();
12541
13077
  const imgIssues = [];
13078
+ const elLoc = locOfElement(el);
13079
+ const srcLoc = src ? locOfAttr(el, "src") : elLoc;
12542
13080
  const tracking = isTrackingPixel(img);
12543
13081
  let dataUriBytes = 0;
12544
13082
  if (src.startsWith("data:")) {
@@ -12562,59 +13100,59 @@ function analyzeImagesFromDom($) {
12562
13100
  const hasStyleHeight = /height\s*:/.test(style);
12563
13101
  if (!hasStyleWidth && !hasStyleHeight) {
12564
13102
  imgIssues.push("missing-dimensions");
12565
- issues.push({
13103
+ issues.push(__spreadValues({
12566
13104
  rule: "missing-dimensions",
12567
13105
  severity: "warning",
12568
13106
  message: "Image missing width/height attributes \u2014 causes layout shifts and Outlook rendering issues.",
12569
13107
  src: truncateSrc(src)
12570
- });
13108
+ }, elLoc ? { loc: elLoc } : {}));
12571
13109
  }
12572
13110
  }
12573
13111
  if (dataUriBytes > DATA_URI_WARN_BYTES) {
12574
13112
  const kb = Math.round(dataUriBytes / 1024);
12575
13113
  imgIssues.push("large-data-uri");
12576
- issues.push({
13114
+ issues.push(__spreadValues({
12577
13115
  rule: "large-data-uri",
12578
13116
  severity: "warning",
12579
13117
  message: `Data URI is ${kb}KB \u2014 consider hosting the image externally to reduce email size.`,
12580
13118
  src: truncateSrc(src)
12581
- });
13119
+ }, srcLoc ? { loc: srcLoc } : {}));
12582
13120
  }
12583
13121
  if (alt === null) {
12584
13122
  imgIssues.push("missing-alt");
12585
- issues.push({
13123
+ issues.push(__spreadValues({
12586
13124
  rule: "missing-alt",
12587
13125
  severity: "warning",
12588
13126
  message: "Image missing alt attribute \u2014 hurts deliverability and accessibility.",
12589
13127
  src: truncateSrc(src)
12590
- });
13128
+ }, elLoc ? { loc: elLoc } : {}));
12591
13129
  }
12592
13130
  if (src.toLowerCase().endsWith(".webp") || src.includes("image/webp")) {
12593
13131
  imgIssues.push("webp-format");
12594
- issues.push({
13132
+ issues.push(__spreadValues({
12595
13133
  rule: "webp-format",
12596
13134
  severity: "info",
12597
13135
  message: "WebP format detected \u2014 not supported by all email clients. Consider PNG or JPEG.",
12598
13136
  src: truncateSrc(src)
12599
- });
13137
+ }, srcLoc ? { loc: srcLoc } : {}));
12600
13138
  }
12601
13139
  if (src.toLowerCase().endsWith(".svg") || src.includes("image/svg")) {
12602
13140
  imgIssues.push("svg-format");
12603
- issues.push({
13141
+ issues.push(__spreadValues({
12604
13142
  rule: "svg-format",
12605
13143
  severity: "info",
12606
13144
  message: "SVG format detected \u2014 not supported by most email clients. Use PNG instead.",
12607
13145
  src: truncateSrc(src)
12608
- });
13146
+ }, srcLoc ? { loc: srcLoc } : {}));
12609
13147
  }
12610
13148
  if (!style.includes("display:block") && !style.includes("display: block")) {
12611
13149
  imgIssues.push("missing-display-block");
12612
- issues.push({
13150
+ issues.push(__spreadValues({
12613
13151
  rule: "missing-display-block",
12614
13152
  severity: "info",
12615
13153
  message: "Image without display:block \u2014 may cause unwanted gaps in Outlook.",
12616
13154
  src: truncateSrc(src)
12617
- });
13155
+ }, elLoc ? { loc: elLoc } : {}));
12618
13156
  }
12619
13157
  images.push({
12620
13158
  src: truncateSrc(src),
@@ -12652,8 +13190,8 @@ function analyzeImagesFromDom($) {
12652
13190
  }
12653
13191
  return { total: images.length, totalDataUriBytes, issues, images };
12654
13192
  }
12655
- function analyzeImages(html) {
12656
- return fromHtml(html, EMPTY_IMAGES, analyzeImagesFromDom);
13193
+ function analyzeImages(html, options) {
13194
+ return fromHtml(html, EMPTY_IMAGES, analyzeImagesFromDom, options);
12657
13195
  }
12658
13196
 
12659
13197
  // src/inbox-preview.ts
@@ -12871,11 +13409,57 @@ function checkSize(html) {
12871
13409
  return fromHtml(html, EMPTY_SIZE, checkSizeFromDom);
12872
13410
  }
12873
13411
 
13412
+ // src/dom-text.ts
13413
+ function visibleTextNodes($) {
13414
+ var _a, _b, _c, _d;
13415
+ const nodes = [];
13416
+ const stack = [...(_b = (_a = $.root()[0]) == null ? void 0 : _a.children) != null ? _b : []].reverse();
13417
+ while (stack.length > 0) {
13418
+ const node = stack.pop();
13419
+ const tag = (_c = node.tagName) == null ? void 0 : _c.toLowerCase();
13420
+ if (tag === "style" || tag === "script" || tag === "head") continue;
13421
+ if (node.type === "text") {
13422
+ nodes.push(node);
13423
+ continue;
13424
+ }
13425
+ const children = (_d = node.children) != null ? _d : [];
13426
+ for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]);
13427
+ }
13428
+ return nodes;
13429
+ }
13430
+
12874
13431
  // src/template-checker.ts
12875
- function checkTemplateVariablesFromDom($) {
13432
+ function checkTemplateVariablesFromDom($, source) {
13433
+ var _a;
12876
13434
  const issues = [];
12877
13435
  const seen = /* @__PURE__ */ new Set();
12878
- const textContent = extractTextContent($);
13436
+ const textNodes = visibleTextNodes($);
13437
+ const positioned = textNodes.some((n) => n.sourceCodeLocation);
13438
+ for (const node of positioned ? textNodes : []) {
13439
+ const data = (_a = node.data) != null ? _a : "";
13440
+ for (const [pattern, label] of TEMPLATE_VARIABLE_PATTERNS) {
13441
+ pattern.lastIndex = 0;
13442
+ let match;
13443
+ while ((match = pattern.exec(data)) !== null) {
13444
+ const variable = match[0];
13445
+ const key = `text:${variable}`;
13446
+ if (seen.has(key)) continue;
13447
+ seen.add(key);
13448
+ const loc = locInTextNode(node, match.index, variable.length, source);
13449
+ issues.push(__spreadValues({
13450
+ rule: "unresolved-variable",
13451
+ severity: "error",
13452
+ message: `Unresolved ${label} variable "${variable}" found in text content.`,
13453
+ variable,
13454
+ location: "text"
13455
+ }, loc ? { loc } : {}));
13456
+ }
13457
+ }
13458
+ }
13459
+ const textContent = textNodes.map((n) => {
13460
+ var _a2;
13461
+ return (_a2 = n.data) != null ? _a2 : "";
13462
+ }).join("");
12879
13463
  for (const [pattern, label] of TEMPLATE_VARIABLE_PATTERNS) {
12880
13464
  pattern.lastIndex = 0;
12881
13465
  let match;
@@ -12908,13 +13492,14 @@ function checkTemplateVariablesFromDom($) {
12908
13492
  const key = `attr:${attr}:${variable}`;
12909
13493
  if (seen.has(key)) continue;
12910
13494
  seen.add(key);
12911
- issues.push({
13495
+ const loc = locOfAttr(el, attr);
13496
+ issues.push(__spreadValues({
12912
13497
  rule: "unresolved-variable",
12913
13498
  severity: "error",
12914
13499
  message: `Unresolved ${label} variable "${variable}" found in ${attr} attribute.`,
12915
13500
  variable,
12916
13501
  location: "attribute"
12917
- });
13502
+ }, loc ? { loc } : {}));
12918
13503
  }
12919
13504
  }
12920
13505
  }
@@ -12922,13 +13507,13 @@ function checkTemplateVariablesFromDom($) {
12922
13507
  }
12923
13508
  return { unresolvedCount: issues.length, issues };
12924
13509
  }
12925
- function extractTextContent($) {
12926
- const clone = $.root().clone();
12927
- clone.find("style, script, head").remove();
12928
- return clone.text();
12929
- }
12930
- function checkTemplateVariables(html) {
12931
- return fromHtml(html, EMPTY_TEMPLATE, checkTemplateVariablesFromDom);
13510
+ function checkTemplateVariables(html, options) {
13511
+ return fromHtml(
13512
+ html,
13513
+ EMPTY_TEMPLATE,
13514
+ ($, h) => checkTemplateVariablesFromDom($, (options == null ? void 0 : options.positions) ? h : void 0),
13515
+ options
13516
+ );
12932
13517
  }
12933
13518
 
12934
13519
  // src/overflow-checker.ts
@@ -12944,32 +13529,71 @@ function fixedPxWidth($el) {
12944
13529
  function isFluid(style) {
12945
13530
  return /max-width\s*:\s*100%/i.test(style) || /width\s*:\s*100%/i.test(style);
12946
13531
  }
12947
- function addWidthIssue(width, label, issues, seen) {
13532
+ function addWidthIssue(width, label, issues, seen, loc) {
12948
13533
  const key = `w:${label}:${width}`;
12949
- if (seen.has(key)) return;
12950
- seen.add(key);
12951
- issues.push({
13534
+ const existing = seen.get(key);
13535
+ if (existing) {
13536
+ addOccurrence(existing, loc);
13537
+ return;
13538
+ }
13539
+ const issue = __spreadValues({
12952
13540
  rule: "fixed-width-overflow",
12953
13541
  severity: "warning",
12954
13542
  message: `${label} has a fixed width of ${width}px, wider than the ${EMAIL_MAX_WIDTH}px email frame \u2014 it will force horizontal scrolling, especially on mobile.`,
12955
13543
  detail: `Use width:100% with max-width:${EMAIL_MAX_WIDTH}px instead of a fixed width beyond the frame.`
12956
- });
13544
+ }, loc ? { loc, locs: [loc] } : {});
13545
+ seen.set(key, issue);
13546
+ issues.push(issue);
12957
13547
  }
12958
- function checkOverflowFromDom($) {
13548
+ function locateInNodes(nodes, starts, index, length, source) {
13549
+ var _a;
13550
+ let lo = 0;
13551
+ let hi = starts.length - 1;
13552
+ let found = -1;
13553
+ while (lo <= hi) {
13554
+ const mid = lo + hi >> 1;
13555
+ if (starts[mid] <= index) {
13556
+ found = mid;
13557
+ lo = mid + 1;
13558
+ } else {
13559
+ hi = mid - 1;
13560
+ }
13561
+ }
13562
+ if (found === -1) return void 0;
13563
+ const node = nodes[found];
13564
+ const within = index - starts[found];
13565
+ const available = ((_a = node.data) != null ? _a : "").length - within;
13566
+ return locInTextNode(node, within, Math.min(length, available), source);
13567
+ }
13568
+ function addOccurrence(issue, loc) {
13569
+ if (!loc || !issue.locs) return;
13570
+ if (issue.locs.some((l) => l.offset === loc.offset)) return;
13571
+ if (issue.locs.length >= MAX_WARNING_LOCATIONS) {
13572
+ issue.locsTruncated = true;
13573
+ return;
13574
+ }
13575
+ issue.locs.push(loc);
13576
+ }
13577
+ function checkOverflowFromDom($, source) {
13578
+ var _a;
12959
13579
  const issues = [];
12960
- const seen = /* @__PURE__ */ new Set();
13580
+ const seen = /* @__PURE__ */ new Map();
13581
+ const tokensSeen = /* @__PURE__ */ new Set();
12961
13582
  $("[width], [style*='width']").each((_, el) => {
12962
13583
  const $el = $(el);
12963
13584
  const width = fixedPxWidth($el);
12964
13585
  if (width === null || width <= EMAIL_MAX_WIDTH) return;
12965
13586
  if (isFluid($el.attr("style") || "")) return;
12966
13587
  const tag = (el.tagName || "element").toLowerCase();
12967
- addWidthIssue(width, `<${tag}>`, issues, seen);
13588
+ const fromStyle = /(?:^|[;\s])width\s*:\s*\d+px/i.test($el.attr("style") || "");
13589
+ addWidthIssue(width, `<${tag}>`, issues, seen, locOfAttr(el, fromStyle ? "style" : "width"));
12968
13590
  });
12969
13591
  $("style").each((_, el) => {
13592
+ const cssText = $(el).text();
13593
+ const anchor = cssBlockAnchor(el, cssText, source);
12970
13594
  let ast;
12971
13595
  try {
12972
- ast = csstree6.parse($(el).text());
13596
+ ast = csstree6.parse(cssText, { positions: true });
12973
13597
  } catch (e) {
12974
13598
  return;
12975
13599
  }
@@ -12979,13 +13603,17 @@ function checkOverflowFromDom($) {
12979
13603
  if (node.type !== "Rule") return;
12980
13604
  let widthPx = null;
12981
13605
  let fluid = false;
13606
+ let widthLoc;
12982
13607
  node.block.children.forEach((child) => {
12983
13608
  if (child.type !== "Declaration") return;
12984
13609
  const prop = child.property.toLowerCase();
12985
13610
  const val = csstree6.generate(child.value);
12986
13611
  if (prop === "width") {
12987
13612
  const m = val.match(/^(\d+)px$/);
12988
- if (m) widthPx = parseInt(m[1], 10);
13613
+ if (m) {
13614
+ widthPx = parseInt(m[1], 10);
13615
+ widthLoc = locInCssBlock(anchor, child.loc);
13616
+ }
12989
13617
  if (/\b100%/.test(val)) fluid = true;
12990
13618
  } else if (prop === "max-width" && /\b100%/.test(val)) {
12991
13619
  fluid = true;
@@ -12993,36 +13621,46 @@ function checkOverflowFromDom($) {
12993
13621
  });
12994
13622
  if (widthPx !== null && widthPx > EMAIL_MAX_WIDTH && !fluid) {
12995
13623
  const selector = csstree6.generate(node.prelude).trim().slice(0, 40);
12996
- addWidthIssue(widthPx, selector || "rule", issues, seen);
13624
+ addWidthIssue(widthPx, selector || "rule", issues, seen, widthLoc);
12997
13625
  }
12998
13626
  }
12999
13627
  });
13000
13628
  });
13001
13629
  const usesWrapGuard = /overflow-wrap|word-break|word-wrap/i.test($.html());
13002
13630
  if (!usesWrapGuard) {
13003
- const $body = $("body");
13631
+ const nodes = visibleTextNodes($);
13632
+ const starts = [];
13004
13633
  let text = "";
13005
- if ($body.length) {
13006
- const clone = $body.clone();
13007
- clone.find("style, script").remove();
13008
- text = clone.text();
13009
- }
13010
- for (const token of text.split(/\s+/)) {
13011
- if (token.length <= UNBREAKABLE_STRING_LENGTH || seen.has(token)) continue;
13012
- seen.add(token);
13634
+ for (const node of nodes) {
13635
+ starts.push(text.length);
13636
+ text += (_a = node.data) != null ? _a : "";
13637
+ }
13638
+ let at = 0;
13639
+ for (const token of text.split(/(\s+)/)) {
13640
+ const start = at;
13641
+ at += token.length;
13642
+ if (/^\s*$/.test(token)) continue;
13643
+ if (token.length <= UNBREAKABLE_STRING_LENGTH || tokensSeen.has(token)) continue;
13644
+ tokensSeen.add(token);
13013
13645
  const preview = token.length > 50 ? `${token.slice(0, 50)}\u2026` : token;
13014
- issues.push({
13646
+ const loc = locateInNodes(nodes, starts, start, token.length, source);
13647
+ issues.push(__spreadValues({
13015
13648
  rule: "unbreakable-string",
13016
13649
  severity: "warning",
13017
13650
  message: `A ${token.length}-character unbroken string ("${preview}") can't wrap and will force horizontal scrolling on narrow screens.`,
13018
13651
  detail: `Add overflow-wrap: anywhere (or word-break: break-word) to its container.`
13019
- });
13652
+ }, loc ? { loc, locs: [loc] } : {}));
13020
13653
  }
13021
13654
  }
13022
13655
  return { hasOverflow: issues.length > 0, issues };
13023
13656
  }
13024
- function checkOverflow(html) {
13025
- return fromHtml(html, EMPTY_OVERFLOW, checkOverflowFromDom);
13657
+ function checkOverflow(html, options) {
13658
+ return fromHtml(
13659
+ html,
13660
+ EMPTY_OVERFLOW,
13661
+ ($, h) => checkOverflowFromDom($, (options == null ? void 0 : options.positions) ? h : void 0),
13662
+ options
13663
+ );
13026
13664
  }
13027
13665
 
13028
13666
  // src/visual-checker.ts
@@ -13035,8 +13673,8 @@ function isSolidColor(value) {
13035
13673
  return c !== null && c.a !== 0;
13036
13674
  }
13037
13675
  function firstColor(value) {
13038
- const tokens = value.match(/#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\)|\b[a-zA-Z]{3,}\b/g) || [];
13039
- for (const t of tokens) {
13676
+ const tokens2 = value.match(/#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\)|\b[a-zA-Z]{3,}\b/g) || [];
13677
+ for (const t of tokens2) {
13040
13678
  const lc = t.toLowerCase();
13041
13679
  if (lc === "transparent") continue;
13042
13680
  if (/^(?:linear|radial|conic|gradient|deg|turn|rad|grad|to|at|from|in|circle|ellipse|closest|farthest|side|corner|url)$/.test(lc)) continue;
@@ -13073,8 +13711,17 @@ function hasFontFallback(value) {
13073
13711
  return WEB_SAFE_FONTS.has(t) || GENERIC_FONT_FAMILIES.has(t) || t.startsWith("-apple-system") || t === "blinkmacsystemfont";
13074
13712
  });
13075
13713
  }
13076
- function inspectDeclarations(style, issues, seen) {
13077
- var _a, _b;
13714
+ function addOccurrence2(issue, loc) {
13715
+ if (!loc || !issue.locs) return;
13716
+ if (issue.locs.some((l) => l.offset === loc.offset)) return;
13717
+ if (issue.locs.length >= MAX_WARNING_LOCATIONS) {
13718
+ issue.locsTruncated = true;
13719
+ return;
13720
+ }
13721
+ issue.locs.push(loc);
13722
+ }
13723
+ function inspectDeclarations(style, issues, seen, locs) {
13724
+ var _a, _b, _c;
13078
13725
  const combined = `${(_a = style.get("background-image")) != null ? _a : ""} ${(_b = style.get("background")) != null ? _b : ""}`;
13079
13726
  const isGradient = GRADIENT_RE.test(combined);
13080
13727
  const isImage = isGradient || /url\(/i.test(combined);
@@ -13082,30 +13729,40 @@ function inspectDeclarations(style, issues, seen) {
13082
13729
  const stop = isGradient ? firstColor(combined) : null;
13083
13730
  const fix = stop ? `background-color: ${stop};` : `background-color: <solid colour matching the image>;`;
13084
13731
  const key = `bg:${fix}`;
13085
- if (!seen.has(key)) {
13086
- seen.add(key);
13087
- issues.push({
13732
+ const loc = (_c = locs == null ? void 0 : locs.get("background-image")) != null ? _c : locs == null ? void 0 : locs.get("background");
13733
+ const existing = seen.get(key);
13734
+ if (existing) {
13735
+ addOccurrence2(existing, loc);
13736
+ } else {
13737
+ const issue = __spreadValues({
13088
13738
  rule: "missing-background-fallback",
13089
13739
  severity: "warning",
13090
13740
  message: `${isGradient ? "Gradient" : "Background image"} has no background-color fallback \u2014 it renders as a blank area (and can hide overlaid text) in Outlook and other clients that drop image backgrounds.`,
13091
13741
  detail: isGradient ? `Add a solid fallback beneath the gradient using its first colour stop.` : `Add a solid background-color so the area still has colour when the image is dropped.`,
13092
13742
  fix
13093
- });
13743
+ }, loc ? { loc, locs: [loc] } : {});
13744
+ seen.set(key, issue);
13745
+ issues.push(issue);
13094
13746
  }
13095
13747
  }
13096
13748
  const font = style.get("font-family");
13097
13749
  if (font && !CSS_WIDE_KEYWORDS.has(font.trim().toLowerCase()) && !hasFontFallback(font)) {
13098
13750
  const fix = `font-family: ${font.trim()}, Arial, sans-serif;`;
13099
13751
  const key = `font:${font.trim().toLowerCase()}`;
13100
- if (!seen.has(key)) {
13101
- seen.add(key);
13102
- issues.push({
13752
+ const loc = locs == null ? void 0 : locs.get("font-family");
13753
+ const existing = seen.get(key);
13754
+ if (existing) {
13755
+ addOccurrence2(existing, loc);
13756
+ } else {
13757
+ const issue = __spreadValues({
13103
13758
  rule: "missing-font-fallback",
13104
13759
  severity: "warning",
13105
13760
  message: `font-family "${font.trim()}" has no web-safe fallback \u2014 clients that strip web fonts (Gmail, Outlook) fall back to Times New Roman.`,
13106
13761
  detail: `End the stack with a web-safe font and a generic family.`,
13107
13762
  fix
13108
- });
13763
+ }, loc ? { loc, locs: [loc] } : {});
13764
+ seen.set(key, issue);
13765
+ issues.push(issue);
13109
13766
  }
13110
13767
  }
13111
13768
  }
@@ -13118,30 +13775,47 @@ function ruleToMap(node) {
13118
13775
  });
13119
13776
  return map;
13120
13777
  }
13121
- function checkVisualFromDom($) {
13778
+ function checkVisualFromDom($, source) {
13122
13779
  const issues = [];
13123
- const seen = /* @__PURE__ */ new Set();
13780
+ const seen = /* @__PURE__ */ new Map();
13124
13781
  $("[style]").each((_, el) => {
13125
- inspectDeclarations(parseInlineStyle($(el).attr("style") || ""), issues, seen);
13782
+ const attrLoc = locOfAttr(el, "style");
13783
+ const style = parseInlineStyle($(el).attr("style") || "");
13784
+ const locs = attrLoc ? new Map([...style.keys()].map((prop) => [prop, attrLoc])) : void 0;
13785
+ inspectDeclarations(style, issues, seen, locs);
13126
13786
  });
13127
13787
  $("style").each((_, el) => {
13788
+ const cssText = $(el).text();
13789
+ const anchor = cssBlockAnchor(el, cssText, source);
13128
13790
  let ast;
13129
13791
  try {
13130
- ast = csstree7.parse($(el).text());
13792
+ ast = csstree7.parse(cssText, { positions: true });
13131
13793
  } catch (e) {
13132
13794
  return;
13133
13795
  }
13134
13796
  csstree7.walk(ast, {
13135
13797
  visit: "Rule",
13136
13798
  enter(node) {
13137
- if (node.type === "Rule") inspectDeclarations(ruleToMap(node), issues, seen);
13799
+ if (node.type !== "Rule") return;
13800
+ const locs = /* @__PURE__ */ new Map();
13801
+ node.block.children.forEach((child) => {
13802
+ if (child.type !== "Declaration") return;
13803
+ const loc = locInCssBlock(anchor, child.loc);
13804
+ if (loc) locs.set(child.property.toLowerCase(), loc);
13805
+ });
13806
+ inspectDeclarations(ruleToMap(node), issues, seen, locs);
13138
13807
  }
13139
13808
  });
13140
13809
  });
13141
13810
  return { issues };
13142
13811
  }
13143
- function checkVisual(html) {
13144
- return fromHtml(html, EMPTY_VISUAL, checkVisualFromDom);
13812
+ function checkVisual(html, options) {
13813
+ return fromHtml(
13814
+ html,
13815
+ EMPTY_VISUAL,
13816
+ ($, h) => checkVisualFromDom($, (options == null ? void 0 : options.positions) ? h : void 0),
13817
+ options
13818
+ );
13145
13819
  }
13146
13820
 
13147
13821
  // src/audit.ts
@@ -13160,7 +13834,8 @@ var EMPTY_AUDIT = {
13160
13834
  function runAudit($, html, framework, options) {
13161
13835
  var _a;
13162
13836
  const skip = new Set((_a = options == null ? void 0 : options.skip) != null ? _a : []);
13163
- const warnings = skip.has("compatibility") ? [] : analyzeEmailFromDom($, framework);
13837
+ const source = (options == null ? void 0 : options.positions) ? html : void 0;
13838
+ const warnings = skip.has("compatibility") ? [] : analyzeEmailFromDom($, framework, source);
13164
13839
  const scores = skip.has("compatibility") ? {} : generateCompatibilityScore(warnings);
13165
13840
  const spam = skip.has("spam") ? EMPTY_SPAM : analyzeSpamFromDom($, options == null ? void 0 : options.spam);
13166
13841
  const links = skip.has("links") ? EMPTY_LINKS : validateLinksFromDom($);
@@ -13168,19 +13843,19 @@ function runAudit($, html, framework, options) {
13168
13843
  const images = skip.has("images") ? EMPTY_IMAGES : analyzeImagesFromDom($);
13169
13844
  const inboxPreview = skip.has("inboxPreview") ? EMPTY_INBOX_PREVIEW : extractInboxPreviewFromDom($);
13170
13845
  const size = skip.has("size") ? EMPTY_SIZE : checkSizeFromDom($, html);
13171
- const templateVariables = skip.has("templateVariables") ? EMPTY_TEMPLATE : checkTemplateVariablesFromDom($);
13172
- const overflow = skip.has("overflow") ? EMPTY_OVERFLOW : checkOverflowFromDom($);
13173
- const visual = skip.has("visual") ? EMPTY_VISUAL : checkVisualFromDom($);
13846
+ const templateVariables = skip.has("templateVariables") ? EMPTY_TEMPLATE : checkTemplateVariablesFromDom($, source);
13847
+ const overflow = skip.has("overflow") ? EMPTY_OVERFLOW : checkOverflowFromDom($, source);
13848
+ const visual = skip.has("visual") ? EMPTY_VISUAL : checkVisualFromDom($, source);
13174
13849
  return { compatibility: { warnings, scores }, spam, links, accessibility, images, inboxPreview, size, templateVariables, overflow, visual };
13175
13850
  }
13176
13851
  function auditEmail(html, options) {
13177
- return fromHtml(html, EMPTY_AUDIT, ($, h) => runAudit($, h, options == null ? void 0 : options.framework, options));
13852
+ return fromHtml(html, EMPTY_AUDIT, ($, h) => runAudit($, h, options == null ? void 0 : options.framework, options), options);
13178
13853
  }
13179
13854
 
13180
13855
  // src/plain-text.ts
13181
- var cheerio6 = __toESM(require("cheerio"), 1);
13856
+ var cheerio5 = __toESM(require("cheerio"), 1);
13182
13857
  function toPlainText(html) {
13183
- const $ = cheerio6.load(html);
13858
+ const $ = cheerio5.load(html);
13184
13859
  $("style, script, head").remove();
13185
13860
  $("[data-skip-in-text='true']").remove();
13186
13861
  const lines = [];
@@ -13282,7 +13957,6 @@ function toPlainText(html) {
13282
13957
  }
13283
13958
 
13284
13959
  // src/session.ts
13285
- var cheerio7 = __toESM(require("cheerio"), 1);
13286
13960
  function createSession(html, options) {
13287
13961
  if (!html || !html.trim()) {
13288
13962
  const fw = options == null ? void 0 : options.framework;
@@ -13309,16 +13983,17 @@ function createSession(html, options) {
13309
13983
  if (html.length > MAX_HTML_SIZE) {
13310
13984
  throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
13311
13985
  }
13312
- const $ = cheerio7.load(html);
13986
+ const $ = loadHtml(html, options);
13313
13987
  const framework = options == null ? void 0 : options.framework;
13988
+ const source = (options == null ? void 0 : options.positions) ? html : void 0;
13314
13989
  return {
13315
13990
  html,
13316
13991
  framework,
13317
13992
  audit(opts) {
13318
- return runAudit($, html, framework, opts);
13993
+ return runAudit($, html, framework, __spreadProps(__spreadValues({}, opts), { positions: options == null ? void 0 : options.positions }));
13319
13994
  },
13320
13995
  analyze() {
13321
- return analyzeEmailFromDom($, framework);
13996
+ return analyzeEmailFromDom($, framework, source);
13322
13997
  },
13323
13998
  score(warnings) {
13324
13999
  return generateCompatibilityScore(warnings);
@@ -13342,13 +14017,13 @@ function createSession(html, options) {
13342
14017
  return checkSizeFromDom($, html);
13343
14018
  },
13344
14019
  checkTemplateVariables() {
13345
- return checkTemplateVariablesFromDom($);
14020
+ return checkTemplateVariablesFromDom($, source);
13346
14021
  },
13347
14022
  checkOverflow() {
13348
- return checkOverflowFromDom($);
14023
+ return checkOverflowFromDom($, source);
13349
14024
  },
13350
14025
  checkVisual() {
13351
- return checkVisualFromDom($);
14026
+ return checkVisualFromDom($, source);
13352
14027
  },
13353
14028
  // Transforms create isolated copies since they mutate the DOM
13354
14029
  transformForClient(clientId) {
@@ -13379,18 +14054,22 @@ var CompileError = class extends Error {
13379
14054
  COMPOUND_VALUE_FEATURES,
13380
14055
  CSS_FUNCTION_FEATURES,
13381
14056
  CSS_SUPPORT,
14057
+ CSS_SUPPORT_NOTES,
13382
14058
  CompileError,
13383
14059
  EMAIL_CLIENTS,
13384
14060
  EMPTY_DELIVERABILITY,
13385
14061
  GENERIC_LINK_TEXT,
13386
14062
  HTML_ELEMENT_FEATURES,
13387
14063
  MAX_HTML_SIZE,
14064
+ MAX_WARNING_LOCATIONS,
13388
14065
  STRUCTURAL_FIX_PROPERTIES,
14066
+ VALUE_CAVEAT_PROPS,
13389
14067
  alphaBlend,
13390
14068
  analyzeEmail,
13391
14069
  analyzeImages,
13392
14070
  analyzeSpam,
13393
14071
  auditEmail,
14072
+ caveatApplies,
13394
14073
  checkAccessibility,
13395
14074
  checkOverflow,
13396
14075
  checkSize,