@emailens/engine 0.10.1 → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,257 @@ 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 locInAttr(attrLoc, source, property, occurrence = 0) {
11025
+ if (!attrLoc || !source) return void 0;
11026
+ const raw = source.slice(attrLoc.offset, attrLoc.offset + attrLoc.length);
11027
+ const open = raw.search(/["']/);
11028
+ const close = raw.lastIndexOf(raw[open]);
11029
+ if (open === -1 || close <= open) return void 0;
11030
+ const found = declarationsIn(raw.slice(open + 1, close), property);
11031
+ const hit = found[occurrence];
11032
+ if (!hit) return void 0;
11033
+ const start = attrLoc.offset + open + 1 + hit.start;
11034
+ const end = attrLoc.offset + open + 1 + hit.end;
11035
+ const from = positionOf(source, start);
11036
+ const to = positionOf(source, end);
11037
+ return {
11038
+ line: from.line,
11039
+ column: from.column,
11040
+ endLine: to.line,
11041
+ endColumn: to.column,
11042
+ offset: start,
11043
+ length: end - start
11044
+ };
11045
+ }
11046
+ function declarationsIn(value, property) {
11047
+ const wanted = property.toLowerCase();
11048
+ const found = [];
11049
+ let depth = 0;
11050
+ let start = 0;
11051
+ const consider = (from, to) => {
11052
+ const text = value.slice(from, to);
11053
+ const colon = text.indexOf(":");
11054
+ if (colon === -1) return;
11055
+ if (text.slice(0, colon).trim().toLowerCase() !== wanted) return;
11056
+ const lead = text.length - text.trimStart().length;
11057
+ const trail = text.length - text.trimEnd().length;
11058
+ if (from + lead < to - trail) found.push({ start: from + lead, end: to - trail });
11059
+ };
11060
+ for (let i = 0; i < value.length; i++) {
11061
+ const c = value[i];
11062
+ if (c === "(") depth++;
11063
+ else if (c === ")") depth = Math.max(0, depth - 1);
11064
+ else if (c === ";" && depth === 0) {
11065
+ consider(start, i);
11066
+ start = i + 1;
11067
+ }
11068
+ }
11069
+ consider(start, value.length);
11070
+ return found;
11071
+ }
11072
+ function locOfFirst($, selector) {
11073
+ const el = $(selector).first()[0];
11074
+ return el ? locOfElement(el) : void 0;
11075
+ }
11076
+ function cssBlockAnchor(styleEl, cssText, source) {
11077
+ var _a;
11078
+ const children = styleEl == null ? void 0 : styleEl.children;
11079
+ if (!children || children.length !== 1) return void 0;
11080
+ const loc = (_a = children[0]) == null ? void 0 : _a.sourceCodeLocation;
11081
+ if (!loc) return void 0;
11082
+ const mapper = source ? crMapper(source.slice(loc.startOffset, loc.endOffset), cssText) : null;
11083
+ const extraBefore = mapper ? (index) => mapper(index) - index : crOffsetter(cssText, loc.endOffset - loc.startOffset);
11084
+ return __spreadValues({ loc, extraBefore }, mapper ? { source } : {});
11085
+ }
11086
+ function crMapper(raw, decoded) {
11087
+ if (raw.length === decoded.length) return (index) => index;
11088
+ const points = [];
11089
+ const extras = [];
11090
+ let r = 0;
11091
+ let d = 0;
11092
+ let extra = 0;
11093
+ while (d < decoded.length) {
11094
+ if (r >= raw.length) return null;
11095
+ if (raw[r] === decoded[d]) {
11096
+ r++;
11097
+ d++;
11098
+ continue;
11099
+ }
11100
+ if (raw[r] === "\r" && decoded[d] === "\n") {
11101
+ const consumed = raw[r + 1] === "\n" ? 2 : 1;
11102
+ extra += consumed - 1;
11103
+ points.push(d);
11104
+ extras.push(extra);
11105
+ r += consumed;
11106
+ d += 1;
11107
+ continue;
11108
+ }
11109
+ return null;
11110
+ }
11111
+ if (r !== raw.length) return null;
11112
+ return (index) => index + lookup(points, extras, index);
11113
+ }
11114
+ function lookup(points, extras, index) {
11115
+ let lo = 0;
11116
+ let hi = points.length - 1;
11117
+ let found = 0;
11118
+ while (lo <= hi) {
11119
+ const mid = lo + hi >> 1;
11120
+ if (points[mid] < index) {
11121
+ found = extras[mid];
11122
+ lo = mid + 1;
11123
+ } else {
11124
+ hi = mid - 1;
11125
+ }
11126
+ }
11127
+ return found;
11128
+ }
11129
+ function findRawOffset(raw, decoded, index, token) {
11130
+ if (!token) return -1;
11131
+ let occurrence = 0;
11132
+ for (let at = decoded.indexOf(token); at !== -1 && at < index; at = decoded.indexOf(token, at + 1)) {
11133
+ occurrence++;
11134
+ }
11135
+ let found = -1;
11136
+ let from = 0;
11137
+ for (let i = 0; i <= occurrence; i++) {
11138
+ found = raw.indexOf(token, from);
11139
+ if (found === -1) return -1;
11140
+ from = found + 1;
11141
+ }
11142
+ return found;
11143
+ }
11144
+ function positionOf(source, offset) {
11145
+ const prefix = source.slice(0, offset);
11146
+ return { line: prefix.split("\n").length, column: offset - prefix.lastIndexOf("\n") };
11147
+ }
11148
+ function crOffsetter(text, rawLength) {
11149
+ const removed = rawLength - text.length;
11150
+ if (removed === 0) return () => 0;
11151
+ const newlines = countNewlines(text, text.length);
11152
+ if (removed !== newlines || newlines === 0) return null;
11153
+ return (index) => countNewlines(text, index);
11154
+ }
11155
+ function countNewlines(text, upTo) {
11156
+ let n = 0;
11157
+ for (let i = 0; i < upTo && i < text.length; i++) if (text.charCodeAt(i) === 10) n++;
11158
+ return n;
11159
+ }
11160
+ function locInCssBlock(anchor, cssLoc) {
11161
+ if (!anchor || !cssLoc) return void 0;
11162
+ const { loc: block, extraBefore } = anchor;
11163
+ if (!extraBefore) {
11164
+ return {
11165
+ line: block.startLine,
11166
+ column: block.startCol,
11167
+ endLine: block.startLine,
11168
+ endColumn: block.startCol,
11169
+ offset: block.startOffset,
11170
+ length: 0
11171
+ };
11172
+ }
11173
+ const line = block.startLine + cssLoc.start.line - 1;
11174
+ const column = cssLoc.start.line === 1 ? block.startCol + cssLoc.start.column - 1 : cssLoc.start.column;
11175
+ const endLine = block.startLine + cssLoc.end.line - 1;
11176
+ const endColumn = cssLoc.end.line === 1 ? block.startCol + cssLoc.end.column - 1 : cssLoc.end.column;
11177
+ const start = block.startOffset + cssLoc.start.offset + extraBefore(cssLoc.start.offset);
11178
+ const end = block.startOffset + cssLoc.end.offset + extraBefore(cssLoc.end.offset);
11179
+ if (anchor.source) {
11180
+ const from = positionOf(anchor.source, start);
11181
+ const to = positionOf(anchor.source, end);
11182
+ return {
11183
+ line: from.line,
11184
+ column: from.column,
11185
+ endLine: to.line,
11186
+ endColumn: to.column,
11187
+ offset: start,
11188
+ length: end - start
11189
+ };
11190
+ }
11191
+ return { line, column, endLine, endColumn, offset: start, length: end - start };
11192
+ }
11193
+ function locInTextNode(node, index, length, source) {
11194
+ var _a;
11195
+ const anchor = node == null ? void 0 : node.sourceCodeLocation;
11196
+ if (!anchor) return void 0;
11197
+ const data = (_a = node.data) != null ? _a : "";
11198
+ const rawLength = anchor.endOffset - anchor.startOffset;
11199
+ if (source) {
11200
+ const raw = source.slice(anchor.startOffset, anchor.endOffset);
11201
+ const token = data.slice(index, index + length);
11202
+ const at = findRawOffset(raw, data, index, token);
11203
+ if (at !== -1) {
11204
+ const start2 = anchor.startOffset + at;
11205
+ const from = positionOf(source, start2);
11206
+ const to = positionOf(source, start2 + token.length);
11207
+ return {
11208
+ line: from.line,
11209
+ column: from.column,
11210
+ endLine: to.line,
11211
+ endColumn: to.column,
11212
+ offset: start2,
11213
+ length: token.length
11214
+ };
11215
+ }
11216
+ }
11217
+ const extraBefore = crOffsetter(data, rawLength);
11218
+ if (!extraBefore) {
11219
+ const clamped = Math.min(length, rawLength);
11220
+ return {
11221
+ line: anchor.startLine,
11222
+ column: anchor.startCol,
11223
+ endLine: anchor.startLine,
11224
+ endColumn: anchor.startCol + clamped,
11225
+ offset: anchor.startOffset,
11226
+ length: clamped
11227
+ };
11228
+ }
11229
+ const start = positionAt(data, index, anchor);
11230
+ const end = positionAt(data, index + length, anchor);
11231
+ const startOffset = anchor.startOffset + index + extraBefore(index);
11232
+ const endOffset = anchor.startOffset + index + length + extraBefore(index + length);
11233
+ return {
11234
+ line: start.line,
11235
+ column: start.column,
11236
+ endLine: end.line,
11237
+ endColumn: end.column,
11238
+ offset: startOffset,
11239
+ length: endOffset - startOffset
11240
+ };
11241
+ }
11242
+ function positionAt(data, index, anchor) {
11243
+ const prefix = data.slice(0, index);
11244
+ const newlines = prefix.split("\n").length - 1;
11245
+ if (newlines === 0) {
11246
+ return { line: anchor.startLine, column: anchor.startCol + index };
11247
+ }
11248
+ return { line: anchor.startLine + newlines, column: index - prefix.lastIndexOf("\n") };
11249
+ }
11250
+
10759
11251
  // src/dark-mode-checker.ts
10760
11252
  var DARK_MEDIA_RE = /\(\s*prefers-color-scheme\s*:\s*dark\s*\)/i;
10761
11253
  var MAX_UNCOVERED_ELEMENTS = 3;
@@ -10842,7 +11334,7 @@ function describeSelector($, el) {
10842
11334
  if (cls) return `${tag}.${cls.split(/\s+/)[0]}`;
10843
11335
  return tag;
10844
11336
  }
10845
- function checkDarkModeFromDom($) {
11337
+ function checkDarkModeFromDom($, source) {
10846
11338
  var _a, _b;
10847
11339
  const darkBlock = collectDarkBlocks($);
10848
11340
  if (!darkBlock) return [];
@@ -10852,15 +11344,16 @@ function checkDarkModeFromDom($) {
10852
11344
  return name === "color-scheme" || name === "supported-color-schemes";
10853
11345
  });
10854
11346
  if (!hasOptIn) {
11347
+ const headLoc = locOfFirst($, "head");
10855
11348
  for (const clientId of PREFERS_COLOR_SCHEME_CLIENTS) {
10856
- warnings.push({
11349
+ warnings.push(__spreadValues({
10857
11350
  severity: "warning",
10858
11351
  client: clientId,
10859
11352
  property: "dark-mode-opt-in",
10860
11353
  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
11354
  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
11355
  fixType: "structural"
10863
- });
11356
+ }, headLoc ? { loc: headLoc, locs: [headLoc] } : {}));
10864
11357
  }
10865
11358
  }
10866
11359
  if (darkBlock.rules === 0) return warnings;
@@ -10868,7 +11361,7 @@ function checkDarkModeFromDom($) {
10868
11361
  const coveredByAny = matchedElements($, darkBlock.any);
10869
11362
  let uncovered = 0;
10870
11363
  $("[bgcolor], [style]").each((_, el) => {
10871
- var _a2;
11364
+ var _a2, _b2;
10872
11365
  if (uncovered >= MAX_UNCOVERED_ELEMENTS) return false;
10873
11366
  const $el = $(el);
10874
11367
  const style = parseInlineStyle($el.attr("style") || "");
@@ -10878,8 +11371,10 @@ function checkDarkModeFromDom($) {
10878
11371
  if (inline ? coveredByImportant.has(el) : coveredByAny.has(el)) return;
10879
11372
  uncovered++;
10880
11373
  const selector = describeSelector($, el);
11374
+ const attrLoc = locOfAttr(el, inline ? "style" : "bgcolor");
11375
+ const loc = inline ? (_b2 = locInAttr(attrLoc, source, style.get("background-color") !== void 0 ? "background-color" : "background")) != null ? _b2 : attrLoc : attrLoc;
10881
11376
  for (const clientId of PREFERS_COLOR_SCHEME_CLIENTS) {
10882
- warnings.push({
11377
+ warnings.push(__spreadValues({
10883
11378
  severity: "warning",
10884
11379
  client: clientId,
10885
11380
  property: "dark-mode-coverage",
@@ -10887,12 +11382,25 @@ function checkDarkModeFromDom($) {
10887
11382
  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
11383
  fixType: "css",
10889
11384
  selector
10890
- });
11385
+ }, loc ? { loc, locs: [loc] } : {}));
10891
11386
  }
10892
11387
  });
10893
11388
  return warnings;
10894
11389
  }
10895
11390
 
11391
+ // src/parse-html.ts
11392
+ var cheerio4 = __toESM(require("cheerio"), 1);
11393
+ function loadHtml(html, options) {
11394
+ return (options == null ? void 0 : options.positions) ? cheerio4.load(html, { sourceCodeLocationInfo: true }) : cheerio4.load(html);
11395
+ }
11396
+ function fromHtml(html, empty, fn, options) {
11397
+ if (!html || !html.trim()) return empty;
11398
+ if (html.length > MAX_HTML_SIZE) {
11399
+ throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
11400
+ }
11401
+ return fn(loadHtml(html, options), html);
11402
+ }
11403
+
10896
11404
  // src/analyze.ts
10897
11405
  var HTML_ELEMENT_SELECTORS = {
10898
11406
  "<style>": "style",
@@ -10945,14 +11453,25 @@ var CSS_FUNCTION_DETECTORS = CSS_FUNCTION_FEATURES.map((fn) => ({
10945
11453
  pattern: `${fn}(`
10946
11454
  // require opening paren — matches "min(" but not "Minion"
10947
11455
  }));
10948
- function analyzeEmailFromDom($, framework) {
11456
+ function analyzeEmailFromDom($, framework, source) {
10949
11457
  const warnings = [];
10950
- const seenWarnings = /* @__PURE__ */ new Set();
11458
+ const seenWarnings = /* @__PURE__ */ new Map();
10951
11459
  function addWarning(w) {
10952
11460
  const key = `${w.client}:${w.property}:${w.severity}:${w.selector || ""}`;
10953
- if (!seenWarnings.has(key)) {
10954
- seenWarnings.add(key);
11461
+ const existing = seenWarnings.get(key);
11462
+ if (!existing) {
11463
+ seenWarnings.set(key, w);
10955
11464
  warnings.push(w);
11465
+ return;
11466
+ }
11467
+ if (!existing.locs || !w.locs) return;
11468
+ for (const loc of w.locs) {
11469
+ if (existing.locs.some((l) => l.offset === loc.offset)) continue;
11470
+ if (existing.locs.length >= MAX_WARNING_LOCATIONS) {
11471
+ existing.locsTruncated = true;
11472
+ break;
11473
+ }
11474
+ existing.locs.push(loc);
10956
11475
  }
10957
11476
  }
10958
11477
  function describeSelector2(el) {
@@ -10970,10 +11489,16 @@ function analyzeEmailFromDom($, framework) {
10970
11489
  for (const feature of HTML_ELEMENT_FEATURES) {
10971
11490
  const selector = HTML_ELEMENT_SELECTORS[feature];
10972
11491
  if (!selector) continue;
10973
- if ($(selector).length === 0) continue;
11492
+ const matches = $(selector);
11493
+ if (matches.length === 0) continue;
10974
11494
  const supportData = CSS_SUPPORT[feature];
10975
11495
  if (!supportData) continue;
10976
11496
  const baseSeverity = HTML_ELEMENT_SEVERITY[feature] || "warning";
11497
+ const found = matches.toArray().map((m) => locOfElement(m)).filter((l) => l !== void 0);
11498
+ const featureOccurrences = found.length ? __spreadValues({
11499
+ locs: found.slice(0, MAX_WARNING_LOCATIONS)
11500
+ }, found.length > MAX_WARNING_LOCATIONS ? { truncated: true } : {}) : void 0;
11501
+ const featureLoc = featureOccurrences == null ? void 0 : featureOccurrences.locs[0];
10977
11502
  for (const client of EMAIL_CLIENTS) {
10978
11503
  const support = supportData[client.id];
10979
11504
  if (support === "unsupported") {
@@ -10981,7 +11506,7 @@ function analyzeEmailFromDom($, framework) {
10981
11506
  const message = msgFn ? msgFn(client.name) : `${client.name} does not support ${feature}.`;
10982
11507
  const sug = getSuggestion(feature, client.id, framework);
10983
11508
  const fix = getCodeFix(feature, client.id, framework);
10984
- addWarning(__spreadValues({
11509
+ addWarning(__spreadValues(__spreadValues({
10985
11510
  severity: baseSeverity,
10986
11511
  client: client.id,
10987
11512
  property: feature,
@@ -10989,11 +11514,11 @@ function analyzeEmailFromDom($, framework) {
10989
11514
  suggestion: sug.text,
10990
11515
  fix,
10991
11516
  fixType: getFixType(feature)
10992
- }, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(feature, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
11517
+ }, featureOccurrences ? occurrenceFields(featureOccurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(feature, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
10993
11518
  } else if (support === "partial" && feature === "<style>") {
10994
11519
  const sug = getSuggestion("<style>:partial", client.id, framework);
10995
11520
  const fix = getCodeFix("<style>", client.id, framework);
10996
- addWarning(__spreadValues({
11521
+ addWarning(__spreadValues(__spreadValues({
10997
11522
  severity: "warning",
10998
11523
  client: client.id,
10999
11524
  property: "<style>",
@@ -11001,55 +11526,97 @@ function analyzeEmailFromDom($, framework) {
11001
11526
  suggestion: sug.text,
11002
11527
  fix,
11003
11528
  fixType: getFixType("<style>")
11004
- }, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<style>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
11529
+ }, featureOccurrences ? occurrenceFields(featureOccurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<style>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
11005
11530
  }
11006
11531
  }
11007
11532
  }
11008
11533
  const parsedAtRules = /* @__PURE__ */ new Set();
11534
+ const selectorLocs = /* @__PURE__ */ new Map();
11009
11535
  const parsedProperties = /* @__PURE__ */ new Set();
11010
11536
  const propertyLines = /* @__PURE__ */ new Map();
11537
+ const propertyLocs = /* @__PURE__ */ new Map();
11011
11538
  const propertyValues = /* @__PURE__ */ new Map();
11012
11539
  const detectedCssFunctions = /* @__PURE__ */ new Set();
11013
11540
  const detectedPseudoClasses = /* @__PURE__ */ new Set();
11014
11541
  const detectedPseudoElements = /* @__PURE__ */ new Set();
11542
+ let blockAnchor;
11543
+ function recordSelectorLoc(key, cssLoc) {
11544
+ if (!cssLoc) return;
11545
+ const loc = locInCssBlock(blockAnchor, cssLoc);
11546
+ if (!loc) return;
11547
+ const seen = selectorLocs.get(key);
11548
+ if (!seen) {
11549
+ selectorLocs.set(key, { locs: [loc] });
11550
+ return;
11551
+ }
11552
+ if (seen.locs.some((l) => l.offset === loc.offset)) return;
11553
+ if (seen.locs.length >= MAX_WARNING_LOCATIONS) {
11554
+ seen.truncated = true;
11555
+ return;
11556
+ }
11557
+ seen.locs.push(loc);
11558
+ }
11559
+ function recordLoc(key, cssLoc, value) {
11560
+ const loc = locInCssBlock(blockAnchor, cssLoc);
11561
+ if (!loc) return;
11562
+ const seen = propertyLocs.get(key);
11563
+ if (!seen) {
11564
+ propertyLocs.set(key, __spreadValues({ locs: [loc] }, value !== void 0 ? { values: [value] } : {}));
11565
+ return;
11566
+ }
11567
+ if (seen.locs.some((l) => l.offset === loc.offset)) return;
11568
+ if (seen.locs.length >= MAX_WARNING_LOCATIONS) {
11569
+ seen.truncated = true;
11570
+ return;
11571
+ }
11572
+ seen.locs.push(loc);
11573
+ if (seen.values && value !== void 0) seen.values.push(value);
11574
+ }
11015
11575
  $("style").each((_, el) => {
11016
11576
  const cssText = $(el).text();
11577
+ blockAnchor = cssBlockAnchor(el, cssText, source);
11017
11578
  try {
11018
11579
  const ast = csstree5.parse(cssText, { parseCustomProperty: true, positions: true });
11019
11580
  csstree5.walk(ast, {
11020
11581
  enter(node) {
11021
11582
  if (node.type === "Atrule") {
11022
11583
  parsedAtRules.add(`@${node.name}`);
11584
+ recordSelectorLoc(`@${node.name}`, node.loc);
11023
11585
  }
11024
11586
  if (node.type === "PseudoClassSelector") {
11025
11587
  detectedPseudoClasses.add(`:${node.name}`);
11588
+ recordSelectorLoc(`:${node.name}`, node.loc);
11026
11589
  }
11027
11590
  if (node.type === "PseudoElementSelector") {
11028
11591
  detectedPseudoElements.add(`::${node.name}`);
11592
+ recordSelectorLoc(`::${node.name}`, node.loc);
11029
11593
  }
11030
11594
  if (node.type === "Declaration") {
11031
11595
  const prop = node.property.toLowerCase();
11032
11596
  parsedProperties.add(prop);
11033
- if (node.loc && !propertyLines.has(prop)) {
11034
- propertyLines.set(prop, node.loc.start.line);
11035
- }
11036
11597
  const valueStr = csstree5.generate(node.value);
11037
11598
  const seenValues = propertyValues.get(prop);
11038
11599
  if (seenValues) seenValues.push(valueStr);
11039
11600
  else propertyValues.set(prop, [valueStr]);
11601
+ if (node.loc) {
11602
+ if (!propertyLines.has(prop)) propertyLines.set(prop, node.loc.start.line);
11603
+ recordLoc(prop, node.loc, valueStr);
11604
+ }
11040
11605
  for (const det of COMPOUND_DETECTORS) {
11041
- if (prop === det.property && valueStr.includes(det.valueIncludes)) {
11606
+ if (prop === det.property && valueStr.toLowerCase().includes(det.valueIncludes)) {
11042
11607
  parsedProperties.add(det.key);
11043
- if (node.loc && !propertyLines.has(det.key)) {
11044
- propertyLines.set(det.key, node.loc.start.line);
11608
+ if (node.loc) {
11609
+ if (!propertyLines.has(det.key)) propertyLines.set(det.key, node.loc.start.line);
11610
+ recordLoc(det.key, node.loc);
11045
11611
  }
11046
11612
  }
11047
11613
  }
11048
11614
  for (const fn of CSS_FUNCTION_DETECTORS) {
11049
11615
  if (valueStr.includes(fn.pattern)) {
11050
11616
  detectedCssFunctions.add(fn.key);
11051
- if (node.loc && !propertyLines.has(fn.key)) {
11052
- propertyLines.set(fn.key, node.loc.start.line);
11617
+ if (node.loc) {
11618
+ if (!propertyLines.has(fn.key)) propertyLines.set(fn.key, node.loc.start.line);
11619
+ recordLoc(fn.key, node.loc);
11053
11620
  }
11054
11621
  }
11055
11622
  }
@@ -11061,33 +11628,69 @@ function analyzeEmailFromDom($, framework) {
11061
11628
  });
11062
11629
  for (const atRule of AT_RULE_FEATURES) {
11063
11630
  if (!parsedAtRules.has(atRule)) continue;
11064
- checkPropertySupport(atRule, addWarning, framework);
11631
+ checkPropertySupport(atRule, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(atRule));
11065
11632
  }
11066
11633
  const cssPropertiesToCheck = Object.keys(CSS_SUPPORT).filter(
11067
11634
  (k) => !k.startsWith("<") && !k.startsWith("@")
11068
11635
  );
11069
11636
  $("[style]").each((_, el) => {
11070
- var _a;
11071
11637
  const style = $(el).attr("style") || "";
11072
11638
  const props = parseStyleProperties(style);
11073
11639
  const selector = describeSelector2(el);
11640
+ const attrLoc = locOfAttr(el, "style");
11641
+ const declarationLocs = (prop, occurrence = 0) => {
11642
+ var _a;
11643
+ return (_a = elementLocs(locInAttr(attrLoc, source, prop, occurrence))) != null ? _a : elementLocs(attrLoc);
11644
+ };
11645
+ const locs = elementLocs(attrLoc);
11074
11646
  for (const prop of props) {
11075
11647
  for (const det of COMPOUND_DETECTORS) {
11076
11648
  if (prop === det.property) {
11077
11649
  const value2 = getStyleValue(style, prop);
11078
- if (value2 == null ? void 0 : value2.includes(det.valueIncludes)) {
11079
- checkPropertySupport(det.key, addWarning, framework, selector);
11650
+ if (value2 == null ? void 0 : value2.toLowerCase().includes(det.valueIncludes)) {
11651
+ checkPropertySupport(
11652
+ det.key,
11653
+ addWarning,
11654
+ framework,
11655
+ selector,
11656
+ void 0,
11657
+ void 0,
11658
+ declarationLocs(prop)
11659
+ );
11080
11660
  }
11081
11661
  }
11082
11662
  }
11083
11663
  if (cssPropertiesToCheck.includes(prop)) {
11084
- checkPropertySupport(prop, addWarning, framework, selector, void 0, (_a = getStyleValue(style, prop)) != null ? _a : void 0);
11664
+ const declared = getStyleValues(style, prop);
11665
+ const placed = [];
11666
+ declared.forEach((value2, i) => {
11667
+ const at = locInAttr(attrLoc, source, prop, i);
11668
+ if (at) placed.push({ value: value2, loc: at });
11669
+ });
11670
+ const occurrences = placed.length === declared.length && placed.length > 0 ? { locs: placed.map((p) => p.loc), values: placed.map((p) => p.value) } : locs;
11671
+ checkPropertySupport(
11672
+ prop,
11673
+ addWarning,
11674
+ framework,
11675
+ selector,
11676
+ void 0,
11677
+ declared.length ? declared : void 0,
11678
+ occurrences
11679
+ );
11085
11680
  }
11086
11681
  const value = getStyleValue(style, prop);
11087
11682
  if (value) {
11088
11683
  for (const fn of CSS_FUNCTION_DETECTORS) {
11089
11684
  if (value.includes(fn.pattern)) {
11090
- checkPropertySupport(fn.key, addWarning, framework, selector);
11685
+ checkPropertySupport(
11686
+ fn.key,
11687
+ addWarning,
11688
+ framework,
11689
+ selector,
11690
+ void 0,
11691
+ void 0,
11692
+ declarationLocs(prop)
11693
+ );
11091
11694
  }
11092
11695
  }
11093
11696
  }
@@ -11103,87 +11706,66 @@ function analyzeEmailFromDom($, framework) {
11103
11706
  framework,
11104
11707
  void 0,
11105
11708
  propertyLines.get(prop),
11106
- values ? values.join(" ") : void 0
11709
+ values,
11710
+ propertyLocs.get(prop)
11107
11711
  );
11108
11712
  }
11109
11713
  for (const compound of COMPOUND_VALUE_FEATURES) {
11110
11714
  if (compound.startsWith(":") || compound.startsWith("::")) continue;
11111
11715
  if (parsedProperties.has(compound)) {
11112
- checkPropertySupport(compound, addWarning, framework, void 0, propertyLines.get(compound));
11716
+ checkPropertySupport(compound, addWarning, framework, void 0, propertyLines.get(compound), void 0, propertyLocs.get(compound));
11113
11717
  }
11114
11718
  }
11115
11719
  for (const pseudo of detectedPseudoClasses) {
11116
11720
  if (CSS_SUPPORT[pseudo]) {
11117
- checkPropertySupport(pseudo, addWarning, framework);
11721
+ checkPropertySupport(pseudo, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(pseudo));
11118
11722
  }
11119
11723
  }
11120
11724
  for (const pseudo of detectedPseudoElements) {
11121
11725
  if (CSS_SUPPORT[pseudo]) {
11122
- checkPropertySupport(pseudo, addWarning, framework);
11726
+ checkPropertySupport(pseudo, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(pseudo));
11123
11727
  }
11124
11728
  }
11125
11729
  for (const fn of detectedCssFunctions) {
11126
- checkPropertySupport(fn, addWarning, framework, void 0, propertyLines.get(fn));
11730
+ checkPropertySupport(fn, addWarning, framework, void 0, propertyLines.get(fn), void 0, propertyLocs.get(fn));
11127
11731
  }
11128
- for (const w of checkDarkModeFromDom($)) addWarning(w);
11732
+ for (const w of checkDarkModeFromDom($, source)) addWarning(w);
11129
11733
  const severityOrder = { error: 0, warning: 1, info: 2 };
11130
11734
  warnings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
11131
11735
  return warnings;
11132
11736
  }
11133
- function analyzeEmail(html, framework) {
11737
+ function analyzeEmail(html, framework, options) {
11134
11738
  if (!html || !html.trim()) {
11135
11739
  return [];
11136
11740
  }
11137
11741
  if (html.length > MAX_HTML_SIZE) {
11138
11742
  throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
11139
11743
  }
11140
- const $ = cheerio4.load(html);
11141
- return analyzeEmailFromDom($, framework);
11744
+ const $ = loadHtml(html, options);
11745
+ return analyzeEmailFromDom($, framework, (options == null ? void 0 : options.positions) ? html : void 0);
11142
11746
  }
11143
11747
  function getFixType(prop) {
11144
11748
  return STRUCTURAL_FIX_PROPERTIES.has(prop) ? "structural" : "css";
11145
11749
  }
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
11750
  function noteSuffix(notes) {
11170
11751
  if (!(notes == null ? void 0 : notes.length)) return "";
11171
11752
  const cleaned = notes.map((n) => n.replace(/^(?:Partial|Buggy|Not supported)\.\s*/i, "").trim()).filter(Boolean);
11172
11753
  return cleaned.length ? ` ${cleaned.join(" ")}` : "";
11173
11754
  }
11174
- function checkPropertySupport(prop, addWarning, framework, selector, line, value) {
11175
- var _a;
11755
+ function checkPropertySupport(prop, addWarning, framework, selector, line, values, occurrences) {
11756
+ var _a, _b, _c, _d, _e, _f;
11757
+ const loc = occurrences == null ? void 0 : occurrences.locs[0];
11758
+ const reportedLine = (_a = loc == null ? void 0 : loc.line) != null ? _a : line;
11176
11759
  const supportData = CSS_SUPPORT[prop];
11177
11760
  if (!supportData) return;
11178
11761
  const fixType = getFixType(prop);
11179
- const valueGated = VALUE_CAVEAT_PROPS.has(prop);
11180
11762
  for (const client of EMAIL_CLIENTS) {
11181
11763
  const support = supportData[client.id] || "unknown";
11182
- const notes = (_a = CSS_SUPPORT_NOTES[prop]) == null ? void 0 : _a[client.id];
11764
+ const notes = (_b = CSS_SUPPORT_NOTES[prop]) == null ? void 0 : _b[client.id];
11183
11765
  if (support === "unsupported") {
11184
11766
  const sug = getSuggestion(prop, client.id, framework);
11185
11767
  const fix = getCodeFix(prop, client.id, framework);
11186
- addWarning(__spreadValues(__spreadValues(__spreadValues({
11768
+ addWarning(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
11187
11769
  severity: "warning",
11188
11770
  client: client.id,
11189
11771
  property: prop,
@@ -11191,12 +11773,13 @@ function checkPropertySupport(prop, addWarning, framework, selector, line, value
11191
11773
  suggestion: sug.text,
11192
11774
  fix,
11193
11775
  fixType
11194
- }, selector ? { selector } : {}), line !== void 0 ? { line } : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
11776
+ }, selector ? { selector } : {}), reportedLine !== void 0 ? { line: reportedLine } : {}), occurrences ? occurrenceFields(occurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
11195
11777
  } else if (support === "partial") {
11196
- if (valueGated && value !== void 0 && !valueTriggersCaveat(prop, value, notes)) continue;
11778
+ if (!caveatApplies(prop, values, notes)) continue;
11779
+ const hits = triggeringOccurrences(prop, occurrences, notes);
11197
11780
  const sug = getSuggestion(prop, client.id, framework);
11198
11781
  const fix = getCodeFix(prop, client.id, framework);
11199
- addWarning(__spreadValues(__spreadValues(__spreadValues({
11782
+ addWarning(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
11200
11783
  severity: "info",
11201
11784
  client: client.id,
11202
11785
  property: prop,
@@ -11204,7 +11787,7 @@ function checkPropertySupport(prop, addWarning, framework, selector, line, value
11204
11787
  suggestion: sug.text,
11205
11788
  fix,
11206
11789
  fixType
11207
- }, selector ? { selector } : {}), line !== void 0 ? { line } : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
11790
+ }, 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
11791
  }
11209
11792
  }
11210
11793
  }
@@ -11223,6 +11806,19 @@ function generateCompatibilityScore(warnings) {
11223
11806
  }
11224
11807
  return result;
11225
11808
  }
11809
+ function occurrenceFields({ locs, truncated }) {
11810
+ return __spreadValues({ loc: locs[0], locs: [...locs] }, truncated ? { locsTruncated: true } : {});
11811
+ }
11812
+ function triggeringOccurrences(prop, occurrences, notes) {
11813
+ const values = occurrences == null ? void 0 : occurrences.values;
11814
+ if (!occurrences || !values) return occurrences;
11815
+ const locs = occurrences.locs.filter((_, i) => caveatApplies(prop, [values[i]], notes));
11816
+ if (!locs.length || locs.length === occurrences.locs.length) return occurrences;
11817
+ return __spreadValues({ locs }, occurrences.truncated ? { truncated: true } : {});
11818
+ }
11819
+ function elementLocs(loc) {
11820
+ return loc ? { locs: [loc] } : void 0;
11821
+ }
11226
11822
  function warningsForClient(warnings, clientId) {
11227
11823
  return warnings.filter((w) => w.client === clientId);
11228
11824
  }
@@ -11563,16 +12159,6 @@ function extractCode(response) {
11563
12159
  return response.trim();
11564
12160
  }
11565
12161
 
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
12162
  // src/spam-scorer.ts
11577
12163
  var SPAM_TRIGGER_PHRASES = [
11578
12164
  "act now",
@@ -12041,6 +12627,8 @@ function validateLinksFromDom($) {
12041
12627
  const href = $(el).attr("href") || "";
12042
12628
  const text = $(el).text().trim();
12043
12629
  const category = classifyHref(href);
12630
+ const elLoc = locOfElement(el);
12631
+ const hrefLoc = href ? locOfAttr(el, "href") : elLoc;
12044
12632
  switch (category) {
12045
12633
  case "https":
12046
12634
  breakdown.https++;
@@ -12071,95 +12659,95 @@ function validateLinksFromDom($) {
12071
12659
  hrefCounts.set(href, (hrefCounts.get(href) || 0) + 1);
12072
12660
  }
12073
12661
  if (!href || !href.trim()) {
12074
- issues.push({
12662
+ issues.push(__spreadValues({
12075
12663
  severity: "error",
12076
12664
  rule: "empty-href",
12077
12665
  message: "Link has no href attribute",
12078
12666
  text: text.slice(0, 80) || "(no text)"
12079
- });
12667
+ }, elLoc ? { loc: elLoc } : {}));
12080
12668
  return;
12081
12669
  }
12082
12670
  if (category === "javascript" && !isPlaceholderHref(href)) {
12083
- issues.push({
12671
+ issues.push(__spreadValues({
12084
12672
  severity: "error",
12085
12673
  rule: "javascript-href",
12086
12674
  message: "Link uses javascript: protocol",
12087
12675
  href: href.slice(0, 100),
12088
12676
  text: text.slice(0, 80) || "(no text)"
12089
- });
12677
+ }, hrefLoc ? { loc: hrefLoc } : {}));
12090
12678
  return;
12091
12679
  }
12092
12680
  if (isPlaceholderHref(href)) {
12093
- issues.push({
12681
+ issues.push(__spreadValues({
12094
12682
  severity: "warning",
12095
12683
  rule: "placeholder-href",
12096
12684
  message: "Link has a placeholder href (# or javascript:void)",
12097
12685
  href,
12098
12686
  text: text.slice(0, 80) || "(no text)"
12099
- });
12687
+ }, hrefLoc ? { loc: hrefLoc } : {}));
12100
12688
  return;
12101
12689
  }
12102
12690
  if (category === "http") {
12103
- issues.push({
12691
+ issues.push(__spreadValues({
12104
12692
  severity: "warning",
12105
12693
  rule: "insecure-link",
12106
12694
  message: "Link uses HTTP instead of HTTPS",
12107
12695
  href: href.slice(0, 120),
12108
12696
  text: text.slice(0, 80) || "(no text)"
12109
- });
12697
+ }, hrefLoc ? { loc: hrefLoc } : {}));
12110
12698
  }
12111
12699
  if (category === "protocol-relative") {
12112
- issues.push({
12700
+ issues.push(__spreadValues({
12113
12701
  severity: "warning",
12114
12702
  rule: "protocol-relative",
12115
12703
  message: "Protocol-relative URL may break in email clients \u2014 use https:// explicitly",
12116
12704
  href: href.slice(0, 120),
12117
12705
  text: text.slice(0, 80) || "(no text)"
12118
- });
12706
+ }, hrefLoc ? { loc: hrefLoc } : {}));
12119
12707
  }
12120
12708
  if (text && GENERIC_LINK_TEXT.has(text.toLowerCase())) {
12121
- issues.push({
12709
+ issues.push(__spreadValues({
12122
12710
  severity: "warning",
12123
12711
  rule: "generic-link-text",
12124
12712
  message: `Link text "${text}" is vague \u2014 use descriptive text for accessibility and engagement`,
12125
12713
  href: href.slice(0, 120),
12126
12714
  text
12127
- });
12715
+ }, elLoc ? { loc: elLoc } : {}));
12128
12716
  }
12129
12717
  if (!text && !$(el).attr("aria-label") && !$(el).find("img[alt]").length) {
12130
- issues.push({
12718
+ issues.push(__spreadValues({
12131
12719
  severity: "error",
12132
12720
  rule: "empty-link-text",
12133
12721
  message: "Link has no visible text or aria-label",
12134
12722
  href: href.slice(0, 120)
12135
- });
12723
+ }, elLoc ? { loc: elLoc } : {}));
12136
12724
  }
12137
12725
  if (category === "mailto" && href.trim().toLowerCase() === "mailto:") {
12138
- issues.push({
12726
+ issues.push(__spreadValues({
12139
12727
  severity: "error",
12140
12728
  rule: "empty-mailto",
12141
12729
  message: "mailto: link has no email address",
12142
12730
  href,
12143
12731
  text: text.slice(0, 80) || "(no text)"
12144
- });
12732
+ }, hrefLoc ? { loc: hrefLoc } : {}));
12145
12733
  }
12146
12734
  if (category === "tel" && href.trim().toLowerCase() === "tel:") {
12147
- issues.push({
12735
+ issues.push(__spreadValues({
12148
12736
  severity: "error",
12149
12737
  rule: "empty-tel",
12150
12738
  message: "tel: link has no phone number",
12151
12739
  href,
12152
12740
  text: text.slice(0, 80) || "(no text)"
12153
- });
12741
+ }, hrefLoc ? { loc: hrefLoc } : {}));
12154
12742
  }
12155
12743
  if (href.length > 2e3) {
12156
- issues.push({
12744
+ issues.push(__spreadValues({
12157
12745
  severity: "info",
12158
12746
  rule: "long-url",
12159
12747
  message: "URL exceeds 2000 characters \u2014 may be truncated by some email clients",
12160
12748
  href: href.slice(0, 120) + "...",
12161
12749
  text: text.slice(0, 80) || "(no text)"
12162
- });
12750
+ }, hrefLoc ? { loc: hrefLoc } : {}));
12163
12751
  }
12164
12752
  });
12165
12753
  links.each((_, el) => {
@@ -12168,14 +12756,15 @@ function validateLinksFromDom($) {
12168
12756
  if (trimmed.startsWith("#") && trimmed.length > 1) {
12169
12757
  const targetId = trimmed.slice(1);
12170
12758
  const target = $(`[id="${targetId}"]`);
12759
+ const anchorLoc = locOfAttr(el, "href");
12171
12760
  if (target.length === 0) {
12172
- issues.push({
12761
+ issues.push(__spreadValues({
12173
12762
  severity: "error",
12174
12763
  rule: "broken-anchor",
12175
12764
  message: `Anchor link "${trimmed}" points to an element that does not exist`,
12176
12765
  href: trimmed,
12177
12766
  text: $(el).text().trim().slice(0, 80) || "(no text)"
12178
- });
12767
+ }, anchorLoc ? { loc: anchorLoc } : {}));
12179
12768
  }
12180
12769
  }
12181
12770
  });
@@ -12191,8 +12780,8 @@ function validateLinksFromDom($) {
12191
12780
  }
12192
12781
  return { totalLinks, issues, breakdown };
12193
12782
  }
12194
- function validateLinks(html) {
12195
- return fromHtml(html, EMPTY_LINKS, validateLinksFromDom);
12783
+ function validateLinks(html, options) {
12784
+ return fromHtml(html, EMPTY_LINKS, validateLinksFromDom, options);
12196
12785
  }
12197
12786
 
12198
12787
  // src/accessibility-checker.ts
@@ -12217,24 +12806,31 @@ function describeElement($, el) {
12217
12806
  function checkLangAttribute($) {
12218
12807
  const lang = $("html").attr("lang");
12219
12808
  if (!lang || !lang.trim()) {
12220
- return {
12809
+ const loc = locOfFirst($, "html");
12810
+ return __spreadProps(__spreadValues({
12221
12811
  severity: "error",
12222
12812
  rule: "missing-lang",
12223
- message: "Missing lang attribute on <html> element",
12813
+ message: "Missing lang attribute on <html> element"
12814
+ }, loc ? { loc } : {}), {
12224
12815
  details: 'Screen readers use the lang attribute to determine pronunciation. Add lang="en" (or appropriate language code).'
12225
- };
12816
+ });
12226
12817
  }
12227
12818
  return null;
12228
12819
  }
12820
+ function titleLoc($) {
12821
+ return $("title").length ? locOfFirst($, "title") : locOfFirst($, "head");
12822
+ }
12229
12823
  function checkTitle($) {
12230
12824
  const title = $("title").text().trim();
12231
12825
  if (!title) {
12232
- return {
12826
+ const loc = titleLoc($);
12827
+ return __spreadProps(__spreadValues({
12233
12828
  severity: "warning",
12234
12829
  rule: "missing-title",
12235
- message: "Missing or empty <title> element",
12830
+ message: "Missing or empty <title> element"
12831
+ }, loc ? { loc } : {}), {
12236
12832
  details: "The <title> helps screen readers identify the email content."
12237
- };
12833
+ });
12238
12834
  }
12239
12835
  return null;
12240
12836
  }
@@ -12244,34 +12840,38 @@ function checkImageAlt($) {
12244
12840
  const alt = $(el).attr("alt");
12245
12841
  const src = $(el).attr("src") || "";
12246
12842
  const role = $(el).attr("role");
12843
+ const elLoc = locOfElement(el);
12247
12844
  if (role === "presentation" || role === "none") return;
12248
12845
  if (alt === void 0) {
12249
- issues.push({
12846
+ issues.push(__spreadProps(__spreadValues({
12250
12847
  severity: "error",
12251
12848
  rule: "img-missing-alt",
12252
12849
  message: "Image missing alt attribute",
12253
- element: describeElement($, el),
12850
+ element: describeElement($, el)
12851
+ }, elLoc ? { loc: elLoc } : {}), {
12254
12852
  details: 'Every image must have an alt attribute. Use alt="" for decorative images.'
12255
- });
12853
+ }));
12256
12854
  } else if (alt.trim() === "") {
12257
12855
  const isLikelyContent = !src.includes("spacer") && !src.includes("pixel") && !src.includes("tracking") && !src.includes("1x1") && !src.includes("transparent");
12258
12856
  if (isLikelyContent && ($(el).attr("width") || "0") !== "1") {
12259
- issues.push({
12857
+ issues.push(__spreadProps(__spreadValues({
12260
12858
  severity: "info",
12261
12859
  rule: "img-empty-alt",
12262
12860
  message: "Image has empty alt text \u2014 verify it is decorative",
12263
- element: describeElement($, el),
12861
+ element: describeElement($, el)
12862
+ }, locOfAttr(el, "alt") ? { loc: locOfAttr(el, "alt") } : {}), {
12264
12863
  details: "Empty alt is correct for decorative images, but content images need descriptive alt text."
12265
- });
12864
+ }));
12266
12865
  }
12267
12866
  } else if (/\.(png|jpg|jpeg|gif|svg|webp|bmp)$/i.test(alt)) {
12268
- issues.push({
12867
+ issues.push(__spreadProps(__spreadValues({
12269
12868
  severity: "error",
12270
12869
  rule: "img-filename-alt",
12271
12870
  message: "Image alt text is a filename, not a description",
12272
- element: describeElement($, el),
12871
+ element: describeElement($, el)
12872
+ }, locOfAttr(el, "alt") ? { loc: locOfAttr(el, "alt") } : {}), {
12273
12873
  details: `Alt "${alt}" should describe the image content, not the file name.`
12274
- });
12874
+ }));
12275
12875
  }
12276
12876
  });
12277
12877
  return issues;
@@ -12279,28 +12879,31 @@ function checkImageAlt($) {
12279
12879
  function checkLinkAccessibility($) {
12280
12880
  const issues = [];
12281
12881
  $("a").each((_, el) => {
12882
+ const elLoc = locOfElement(el);
12282
12883
  const text = $(el).text().trim().toLowerCase();
12283
12884
  const ariaLabel = $(el).attr("aria-label");
12284
12885
  const title = $(el).attr("title");
12285
12886
  const imgAlt = $(el).find("img").attr("alt");
12286
12887
  if (!text && !ariaLabel && !title && !imgAlt) {
12287
- issues.push({
12888
+ issues.push(__spreadProps(__spreadValues({
12288
12889
  severity: "error",
12289
12890
  rule: "link-no-accessible-name",
12290
12891
  message: "Link has no accessible name",
12291
- element: describeElement($, el),
12892
+ element: describeElement($, el)
12893
+ }, elLoc ? { loc: elLoc } : {}), {
12292
12894
  details: "Links need visible text, aria-label, or an image with alt text."
12293
- });
12895
+ }));
12294
12896
  return;
12295
12897
  }
12296
12898
  if (text && GENERIC_LINK_TEXT.has(text) && !ariaLabel) {
12297
- issues.push({
12899
+ issues.push(__spreadProps(__spreadValues({
12298
12900
  severity: "warning",
12299
12901
  rule: "link-generic-text",
12300
12902
  message: `Link text "${$(el).text().trim()}" is not descriptive`,
12301
- element: describeElement($, el),
12903
+ element: describeElement($, el)
12904
+ }, elLoc ? { loc: elLoc } : {}), {
12302
12905
  details: "Screen readers often list links out of context. Use text that describes the destination."
12303
- });
12906
+ }));
12304
12907
  }
12305
12908
  });
12306
12909
  return issues;
@@ -12310,18 +12913,20 @@ function checkTableAccessibility($) {
12310
12913
  $("table").each((_, el) => {
12311
12914
  if ($(el).parents('table[role="presentation"], table[role="none"]').length > 0) return;
12312
12915
  const role = $(el).attr("role");
12916
+ const tableLoc = locOfElement(el);
12313
12917
  const hasHeaders = $(el).find("th").length > 0;
12314
12918
  const looksLikeLayout = !hasHeaders;
12315
12919
  if (looksLikeLayout && role !== "presentation" && role !== "none") {
12316
12920
  const nestedTables = $(el).find("table").length;
12317
12921
  if (nestedTables > 0 || $(el).find("td").length > 2) {
12318
- issues.push({
12922
+ issues.push(__spreadProps(__spreadValues({
12319
12923
  severity: "info",
12320
12924
  rule: "table-missing-role",
12321
- message: 'Layout table missing role="presentation"',
12925
+ message: 'Layout table missing role="presentation"'
12926
+ }, tableLoc ? { loc: tableLoc } : {}), {
12322
12927
  element: `<table> with ${$(el).find("td").length} cells`,
12323
12928
  details: `Add role="presentation" to tables used for layout so screen readers don't announce them as data tables.`
12324
- });
12929
+ }));
12325
12930
  }
12326
12931
  }
12327
12932
  });
@@ -12332,6 +12937,7 @@ function checkTextSizeAndContrast($) {
12332
12937
  let smallTextCount = 0;
12333
12938
  $("[style]").each((_, el) => {
12334
12939
  const style = $(el).attr("style") || "";
12940
+ const styleLoc = locOfAttr(el, "style");
12335
12941
  const fontSizeMatch = style.match(/font-size\s*:\s*(\d+(?:\.\d+)?)(px|pt)/i);
12336
12942
  if (fontSizeMatch) {
12337
12943
  const size = parseFloat(fontSizeMatch[1]);
@@ -12340,13 +12946,14 @@ function checkTextSizeAndContrast($) {
12340
12946
  if (pxSize < 9 && pxSize > 0) {
12341
12947
  smallTextCount++;
12342
12948
  if (smallTextCount <= 3) {
12343
- issues.push({
12949
+ issues.push(__spreadProps(__spreadValues({
12344
12950
  severity: "warning",
12345
12951
  rule: "small-text",
12346
12952
  message: `Very small text (${fontSizeMatch[0].trim()})`,
12347
- element: describeElement($, el),
12953
+ element: describeElement($, el)
12954
+ }, styleLoc ? { loc: styleLoc } : {}), {
12348
12955
  details: "Text smaller than 9px is difficult to read, especially on mobile devices."
12349
- });
12956
+ }));
12350
12957
  }
12351
12958
  }
12352
12959
  }
@@ -12391,21 +12998,23 @@ function checkTextSizeAndContrast($) {
12391
12998
  }
12392
12999
  const grade = wcagGrade(ratio);
12393
13000
  if (grade === "Fail") {
12394
- issues.push({
13001
+ issues.push(__spreadProps(__spreadValues({
12395
13002
  severity: "error",
12396
13003
  rule: "low-contrast",
12397
13004
  message: `Low contrast ratio ${ratio.toFixed(1)}:1 \u2014 fails WCAG minimum`,
12398
- element: describeElement($, el),
13005
+ element: describeElement($, el)
13006
+ }, styleLoc ? { loc: styleLoc } : {}), {
12399
13007
  details: `Foreground ${colorValue} on background needs at least ${isLargeText ? "3:1" : "4.5:1"} contrast ratio.`
12400
- });
13008
+ }));
12401
13009
  } else if (!isLargeText && grade === "AA Large") {
12402
- issues.push({
13010
+ issues.push(__spreadProps(__spreadValues({
12403
13011
  severity: "warning",
12404
13012
  rule: "low-contrast",
12405
13013
  message: `Low contrast ratio ${ratio.toFixed(1)}:1 \u2014 fails WCAG AA for normal text`,
12406
- element: describeElement($, el),
13014
+ element: describeElement($, el)
13015
+ }, styleLoc ? { loc: styleLoc } : {}), {
12407
13016
  details: `Foreground ${colorValue} on background needs at least 4.5:1 for normal-sized text.`
12408
- });
13017
+ }));
12409
13018
  }
12410
13019
  }
12411
13020
  }
@@ -12428,29 +13037,32 @@ function checkCharsetDeclaration($) {
12428
13037
  const content = httpEquiv.attr("content") || "";
12429
13038
  if (/charset\s*=/i.test(content)) return null;
12430
13039
  }
12431
- return {
13040
+ const loc = locOfFirst($, "head");
13041
+ return __spreadProps(__spreadValues({
12432
13042
  severity: "warning",
12433
13043
  rule: "missing-charset",
12434
- message: "Missing charset declaration",
13044
+ message: "Missing charset declaration"
13045
+ }, loc ? { loc } : {}), {
12435
13046
  details: 'Add <meta charset="utf-8"> in <head> to prevent encoding issues across email clients.'
12436
- };
13047
+ });
12437
13048
  }
12438
13049
  function checkSemanticStructure($) {
12439
13050
  const issues = [];
12440
13051
  const headings = [];
12441
13052
  $("h1, h2, h3, h4, h5, h6").each((_, el) => {
12442
13053
  const level = parseInt(el.tagName.replace(/h/i, ""), 10);
12443
- headings.push({ level, text: $(el).text().trim().slice(0, 60) });
13054
+ headings.push({ level, text: $(el).text().trim().slice(0, 60), loc: locOfElement(el) });
12444
13055
  });
12445
13056
  for (let i = 1; i < headings.length; i++) {
12446
13057
  const gap = headings[i].level - headings[i - 1].level;
12447
13058
  if (gap > 1) {
12448
- issues.push({
13059
+ issues.push(__spreadProps(__spreadValues({
12449
13060
  severity: "info",
12450
13061
  rule: "heading-skip",
12451
- message: `Heading level skipped: h${headings[i - 1].level} to h${headings[i].level}`,
13062
+ message: `Heading level skipped: h${headings[i - 1].level} to h${headings[i].level}`
13063
+ }, headings[i].loc ? { loc: headings[i].loc } : {}), {
12452
13064
  details: "Skipped heading levels can confuse screen readers. Use sequential heading levels."
12453
- });
13065
+ }));
12454
13066
  break;
12455
13067
  }
12456
13068
  }
@@ -12491,8 +13103,8 @@ function checkAccessibilityFromDom($) {
12491
13103
  const score = Math.max(0, 100 - penalty);
12492
13104
  return { score, issues };
12493
13105
  }
12494
- function checkAccessibility(html) {
12495
- return fromHtml(html, EMPTY_ACCESSIBILITY, checkAccessibilityFromDom);
13106
+ function checkAccessibility(html, options) {
13107
+ return fromHtml(html, EMPTY_ACCESSIBILITY, checkAccessibilityFromDom, options);
12496
13108
  }
12497
13109
 
12498
13110
  // src/image-analyzer.ts
@@ -12539,6 +13151,8 @@ function analyzeImagesFromDom($) {
12539
13151
  const height = (_c = img.attr("height")) != null ? _c : null;
12540
13152
  const style = (img.attr("style") || "").toLowerCase();
12541
13153
  const imgIssues = [];
13154
+ const elLoc = locOfElement(el);
13155
+ const srcLoc = src ? locOfAttr(el, "src") : elLoc;
12542
13156
  const tracking = isTrackingPixel(img);
12543
13157
  let dataUriBytes = 0;
12544
13158
  if (src.startsWith("data:")) {
@@ -12562,59 +13176,59 @@ function analyzeImagesFromDom($) {
12562
13176
  const hasStyleHeight = /height\s*:/.test(style);
12563
13177
  if (!hasStyleWidth && !hasStyleHeight) {
12564
13178
  imgIssues.push("missing-dimensions");
12565
- issues.push({
13179
+ issues.push(__spreadValues({
12566
13180
  rule: "missing-dimensions",
12567
13181
  severity: "warning",
12568
13182
  message: "Image missing width/height attributes \u2014 causes layout shifts and Outlook rendering issues.",
12569
13183
  src: truncateSrc(src)
12570
- });
13184
+ }, elLoc ? { loc: elLoc } : {}));
12571
13185
  }
12572
13186
  }
12573
13187
  if (dataUriBytes > DATA_URI_WARN_BYTES) {
12574
13188
  const kb = Math.round(dataUriBytes / 1024);
12575
13189
  imgIssues.push("large-data-uri");
12576
- issues.push({
13190
+ issues.push(__spreadValues({
12577
13191
  rule: "large-data-uri",
12578
13192
  severity: "warning",
12579
13193
  message: `Data URI is ${kb}KB \u2014 consider hosting the image externally to reduce email size.`,
12580
13194
  src: truncateSrc(src)
12581
- });
13195
+ }, srcLoc ? { loc: srcLoc } : {}));
12582
13196
  }
12583
13197
  if (alt === null) {
12584
13198
  imgIssues.push("missing-alt");
12585
- issues.push({
13199
+ issues.push(__spreadValues({
12586
13200
  rule: "missing-alt",
12587
13201
  severity: "warning",
12588
13202
  message: "Image missing alt attribute \u2014 hurts deliverability and accessibility.",
12589
13203
  src: truncateSrc(src)
12590
- });
13204
+ }, elLoc ? { loc: elLoc } : {}));
12591
13205
  }
12592
13206
  if (src.toLowerCase().endsWith(".webp") || src.includes("image/webp")) {
12593
13207
  imgIssues.push("webp-format");
12594
- issues.push({
13208
+ issues.push(__spreadValues({
12595
13209
  rule: "webp-format",
12596
13210
  severity: "info",
12597
13211
  message: "WebP format detected \u2014 not supported by all email clients. Consider PNG or JPEG.",
12598
13212
  src: truncateSrc(src)
12599
- });
13213
+ }, srcLoc ? { loc: srcLoc } : {}));
12600
13214
  }
12601
13215
  if (src.toLowerCase().endsWith(".svg") || src.includes("image/svg")) {
12602
13216
  imgIssues.push("svg-format");
12603
- issues.push({
13217
+ issues.push(__spreadValues({
12604
13218
  rule: "svg-format",
12605
13219
  severity: "info",
12606
13220
  message: "SVG format detected \u2014 not supported by most email clients. Use PNG instead.",
12607
13221
  src: truncateSrc(src)
12608
- });
13222
+ }, srcLoc ? { loc: srcLoc } : {}));
12609
13223
  }
12610
13224
  if (!style.includes("display:block") && !style.includes("display: block")) {
12611
13225
  imgIssues.push("missing-display-block");
12612
- issues.push({
13226
+ issues.push(__spreadValues({
12613
13227
  rule: "missing-display-block",
12614
13228
  severity: "info",
12615
13229
  message: "Image without display:block \u2014 may cause unwanted gaps in Outlook.",
12616
13230
  src: truncateSrc(src)
12617
- });
13231
+ }, elLoc ? { loc: elLoc } : {}));
12618
13232
  }
12619
13233
  images.push({
12620
13234
  src: truncateSrc(src),
@@ -12652,8 +13266,8 @@ function analyzeImagesFromDom($) {
12652
13266
  }
12653
13267
  return { total: images.length, totalDataUriBytes, issues, images };
12654
13268
  }
12655
- function analyzeImages(html) {
12656
- return fromHtml(html, EMPTY_IMAGES, analyzeImagesFromDom);
13269
+ function analyzeImages(html, options) {
13270
+ return fromHtml(html, EMPTY_IMAGES, analyzeImagesFromDom, options);
12657
13271
  }
12658
13272
 
12659
13273
  // src/inbox-preview.ts
@@ -12871,11 +13485,57 @@ function checkSize(html) {
12871
13485
  return fromHtml(html, EMPTY_SIZE, checkSizeFromDom);
12872
13486
  }
12873
13487
 
13488
+ // src/dom-text.ts
13489
+ function visibleTextNodes($) {
13490
+ var _a, _b, _c, _d;
13491
+ const nodes = [];
13492
+ const stack = [...(_b = (_a = $.root()[0]) == null ? void 0 : _a.children) != null ? _b : []].reverse();
13493
+ while (stack.length > 0) {
13494
+ const node = stack.pop();
13495
+ const tag = (_c = node.tagName) == null ? void 0 : _c.toLowerCase();
13496
+ if (tag === "style" || tag === "script" || tag === "head") continue;
13497
+ if (node.type === "text") {
13498
+ nodes.push(node);
13499
+ continue;
13500
+ }
13501
+ const children = (_d = node.children) != null ? _d : [];
13502
+ for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]);
13503
+ }
13504
+ return nodes;
13505
+ }
13506
+
12874
13507
  // src/template-checker.ts
12875
- function checkTemplateVariablesFromDom($) {
13508
+ function checkTemplateVariablesFromDom($, source) {
13509
+ var _a;
12876
13510
  const issues = [];
12877
13511
  const seen = /* @__PURE__ */ new Set();
12878
- const textContent = extractTextContent($);
13512
+ const textNodes = visibleTextNodes($);
13513
+ const positioned = textNodes.some((n) => n.sourceCodeLocation);
13514
+ for (const node of positioned ? textNodes : []) {
13515
+ const data = (_a = node.data) != null ? _a : "";
13516
+ for (const [pattern, label] of TEMPLATE_VARIABLE_PATTERNS) {
13517
+ pattern.lastIndex = 0;
13518
+ let match;
13519
+ while ((match = pattern.exec(data)) !== null) {
13520
+ const variable = match[0];
13521
+ const key = `text:${variable}`;
13522
+ if (seen.has(key)) continue;
13523
+ seen.add(key);
13524
+ const loc = locInTextNode(node, match.index, variable.length, source);
13525
+ issues.push(__spreadValues({
13526
+ rule: "unresolved-variable",
13527
+ severity: "error",
13528
+ message: `Unresolved ${label} variable "${variable}" found in text content.`,
13529
+ variable,
13530
+ location: "text"
13531
+ }, loc ? { loc } : {}));
13532
+ }
13533
+ }
13534
+ }
13535
+ const textContent = textNodes.map((n) => {
13536
+ var _a2;
13537
+ return (_a2 = n.data) != null ? _a2 : "";
13538
+ }).join("");
12879
13539
  for (const [pattern, label] of TEMPLATE_VARIABLE_PATTERNS) {
12880
13540
  pattern.lastIndex = 0;
12881
13541
  let match;
@@ -12908,13 +13568,14 @@ function checkTemplateVariablesFromDom($) {
12908
13568
  const key = `attr:${attr}:${variable}`;
12909
13569
  if (seen.has(key)) continue;
12910
13570
  seen.add(key);
12911
- issues.push({
13571
+ const loc = locOfAttr(el, attr);
13572
+ issues.push(__spreadValues({
12912
13573
  rule: "unresolved-variable",
12913
13574
  severity: "error",
12914
13575
  message: `Unresolved ${label} variable "${variable}" found in ${attr} attribute.`,
12915
13576
  variable,
12916
13577
  location: "attribute"
12917
- });
13578
+ }, loc ? { loc } : {}));
12918
13579
  }
12919
13580
  }
12920
13581
  }
@@ -12922,13 +13583,13 @@ function checkTemplateVariablesFromDom($) {
12922
13583
  }
12923
13584
  return { unresolvedCount: issues.length, issues };
12924
13585
  }
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);
13586
+ function checkTemplateVariables(html, options) {
13587
+ return fromHtml(
13588
+ html,
13589
+ EMPTY_TEMPLATE,
13590
+ ($, h) => checkTemplateVariablesFromDom($, (options == null ? void 0 : options.positions) ? h : void 0),
13591
+ options
13592
+ );
12932
13593
  }
12933
13594
 
12934
13595
  // src/overflow-checker.ts
@@ -12944,32 +13605,71 @@ function fixedPxWidth($el) {
12944
13605
  function isFluid(style) {
12945
13606
  return /max-width\s*:\s*100%/i.test(style) || /width\s*:\s*100%/i.test(style);
12946
13607
  }
12947
- function addWidthIssue(width, label, issues, seen) {
13608
+ function addWidthIssue(width, label, issues, seen, loc) {
12948
13609
  const key = `w:${label}:${width}`;
12949
- if (seen.has(key)) return;
12950
- seen.add(key);
12951
- issues.push({
13610
+ const existing = seen.get(key);
13611
+ if (existing) {
13612
+ addOccurrence(existing, loc);
13613
+ return;
13614
+ }
13615
+ const issue = __spreadValues({
12952
13616
  rule: "fixed-width-overflow",
12953
13617
  severity: "warning",
12954
13618
  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
13619
  detail: `Use width:100% with max-width:${EMAIL_MAX_WIDTH}px instead of a fixed width beyond the frame.`
12956
- });
13620
+ }, loc ? { loc, locs: [loc] } : {});
13621
+ seen.set(key, issue);
13622
+ issues.push(issue);
13623
+ }
13624
+ function locateInNodes(nodes, starts, index, length, source) {
13625
+ var _a;
13626
+ let lo = 0;
13627
+ let hi = starts.length - 1;
13628
+ let found = -1;
13629
+ while (lo <= hi) {
13630
+ const mid = lo + hi >> 1;
13631
+ if (starts[mid] <= index) {
13632
+ found = mid;
13633
+ lo = mid + 1;
13634
+ } else {
13635
+ hi = mid - 1;
13636
+ }
13637
+ }
13638
+ if (found === -1) return void 0;
13639
+ const node = nodes[found];
13640
+ const within = index - starts[found];
13641
+ const available = ((_a = node.data) != null ? _a : "").length - within;
13642
+ return locInTextNode(node, within, Math.min(length, available), source);
12957
13643
  }
12958
- function checkOverflowFromDom($) {
13644
+ function addOccurrence(issue, loc) {
13645
+ if (!loc || !issue.locs) return;
13646
+ if (issue.locs.some((l) => l.offset === loc.offset)) return;
13647
+ if (issue.locs.length >= MAX_WARNING_LOCATIONS) {
13648
+ issue.locsTruncated = true;
13649
+ return;
13650
+ }
13651
+ issue.locs.push(loc);
13652
+ }
13653
+ function checkOverflowFromDom($, source) {
13654
+ var _a;
12959
13655
  const issues = [];
12960
- const seen = /* @__PURE__ */ new Set();
13656
+ const seen = /* @__PURE__ */ new Map();
13657
+ const tokensSeen = /* @__PURE__ */ new Set();
12961
13658
  $("[width], [style*='width']").each((_, el) => {
12962
13659
  const $el = $(el);
12963
13660
  const width = fixedPxWidth($el);
12964
13661
  if (width === null || width <= EMAIL_MAX_WIDTH) return;
12965
13662
  if (isFluid($el.attr("style") || "")) return;
12966
13663
  const tag = (el.tagName || "element").toLowerCase();
12967
- addWidthIssue(width, `<${tag}>`, issues, seen);
13664
+ const fromStyle = /(?:^|[;\s])width\s*:\s*\d+px/i.test($el.attr("style") || "");
13665
+ addWidthIssue(width, `<${tag}>`, issues, seen, locOfAttr(el, fromStyle ? "style" : "width"));
12968
13666
  });
12969
13667
  $("style").each((_, el) => {
13668
+ const cssText = $(el).text();
13669
+ const anchor = cssBlockAnchor(el, cssText, source);
12970
13670
  let ast;
12971
13671
  try {
12972
- ast = csstree6.parse($(el).text());
13672
+ ast = csstree6.parse(cssText, { positions: true });
12973
13673
  } catch (e) {
12974
13674
  return;
12975
13675
  }
@@ -12979,13 +13679,17 @@ function checkOverflowFromDom($) {
12979
13679
  if (node.type !== "Rule") return;
12980
13680
  let widthPx = null;
12981
13681
  let fluid = false;
13682
+ let widthLoc;
12982
13683
  node.block.children.forEach((child) => {
12983
13684
  if (child.type !== "Declaration") return;
12984
13685
  const prop = child.property.toLowerCase();
12985
13686
  const val = csstree6.generate(child.value);
12986
13687
  if (prop === "width") {
12987
13688
  const m = val.match(/^(\d+)px$/);
12988
- if (m) widthPx = parseInt(m[1], 10);
13689
+ if (m) {
13690
+ widthPx = parseInt(m[1], 10);
13691
+ widthLoc = locInCssBlock(anchor, child.loc);
13692
+ }
12989
13693
  if (/\b100%/.test(val)) fluid = true;
12990
13694
  } else if (prop === "max-width" && /\b100%/.test(val)) {
12991
13695
  fluid = true;
@@ -12993,36 +13697,46 @@ function checkOverflowFromDom($) {
12993
13697
  });
12994
13698
  if (widthPx !== null && widthPx > EMAIL_MAX_WIDTH && !fluid) {
12995
13699
  const selector = csstree6.generate(node.prelude).trim().slice(0, 40);
12996
- addWidthIssue(widthPx, selector || "rule", issues, seen);
13700
+ addWidthIssue(widthPx, selector || "rule", issues, seen, widthLoc);
12997
13701
  }
12998
13702
  }
12999
13703
  });
13000
13704
  });
13001
13705
  const usesWrapGuard = /overflow-wrap|word-break|word-wrap/i.test($.html());
13002
13706
  if (!usesWrapGuard) {
13003
- const $body = $("body");
13707
+ const nodes = visibleTextNodes($);
13708
+ const starts = [];
13004
13709
  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);
13710
+ for (const node of nodes) {
13711
+ starts.push(text.length);
13712
+ text += (_a = node.data) != null ? _a : "";
13713
+ }
13714
+ let at = 0;
13715
+ for (const token of text.split(/(\s+)/)) {
13716
+ const start = at;
13717
+ at += token.length;
13718
+ if (/^\s*$/.test(token)) continue;
13719
+ if (token.length <= UNBREAKABLE_STRING_LENGTH || tokensSeen.has(token)) continue;
13720
+ tokensSeen.add(token);
13013
13721
  const preview = token.length > 50 ? `${token.slice(0, 50)}\u2026` : token;
13014
- issues.push({
13722
+ const loc = locateInNodes(nodes, starts, start, token.length, source);
13723
+ issues.push(__spreadValues({
13015
13724
  rule: "unbreakable-string",
13016
13725
  severity: "warning",
13017
13726
  message: `A ${token.length}-character unbroken string ("${preview}") can't wrap and will force horizontal scrolling on narrow screens.`,
13018
13727
  detail: `Add overflow-wrap: anywhere (or word-break: break-word) to its container.`
13019
- });
13728
+ }, loc ? { loc, locs: [loc] } : {}));
13020
13729
  }
13021
13730
  }
13022
13731
  return { hasOverflow: issues.length > 0, issues };
13023
13732
  }
13024
- function checkOverflow(html) {
13025
- return fromHtml(html, EMPTY_OVERFLOW, checkOverflowFromDom);
13733
+ function checkOverflow(html, options) {
13734
+ return fromHtml(
13735
+ html,
13736
+ EMPTY_OVERFLOW,
13737
+ ($, h) => checkOverflowFromDom($, (options == null ? void 0 : options.positions) ? h : void 0),
13738
+ options
13739
+ );
13026
13740
  }
13027
13741
 
13028
13742
  // src/visual-checker.ts
@@ -13035,8 +13749,8 @@ function isSolidColor(value) {
13035
13749
  return c !== null && c.a !== 0;
13036
13750
  }
13037
13751
  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) {
13752
+ const tokens2 = value.match(/#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\)|\b[a-zA-Z]{3,}\b/g) || [];
13753
+ for (const t of tokens2) {
13040
13754
  const lc = t.toLowerCase();
13041
13755
  if (lc === "transparent") continue;
13042
13756
  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 +13787,17 @@ function hasFontFallback(value) {
13073
13787
  return WEB_SAFE_FONTS.has(t) || GENERIC_FONT_FAMILIES.has(t) || t.startsWith("-apple-system") || t === "blinkmacsystemfont";
13074
13788
  });
13075
13789
  }
13076
- function inspectDeclarations(style, issues, seen) {
13077
- var _a, _b;
13790
+ function addOccurrence2(issue, loc) {
13791
+ if (!loc || !issue.locs) return;
13792
+ if (issue.locs.some((l) => l.offset === loc.offset)) return;
13793
+ if (issue.locs.length >= MAX_WARNING_LOCATIONS) {
13794
+ issue.locsTruncated = true;
13795
+ return;
13796
+ }
13797
+ issue.locs.push(loc);
13798
+ }
13799
+ function inspectDeclarations(style, issues, seen, locs) {
13800
+ var _a, _b, _c;
13078
13801
  const combined = `${(_a = style.get("background-image")) != null ? _a : ""} ${(_b = style.get("background")) != null ? _b : ""}`;
13079
13802
  const isGradient = GRADIENT_RE.test(combined);
13080
13803
  const isImage = isGradient || /url\(/i.test(combined);
@@ -13082,30 +13805,40 @@ function inspectDeclarations(style, issues, seen) {
13082
13805
  const stop = isGradient ? firstColor(combined) : null;
13083
13806
  const fix = stop ? `background-color: ${stop};` : `background-color: <solid colour matching the image>;`;
13084
13807
  const key = `bg:${fix}`;
13085
- if (!seen.has(key)) {
13086
- seen.add(key);
13087
- issues.push({
13808
+ const loc = (_c = locs == null ? void 0 : locs.get("background-image")) != null ? _c : locs == null ? void 0 : locs.get("background");
13809
+ const existing = seen.get(key);
13810
+ if (existing) {
13811
+ addOccurrence2(existing, loc);
13812
+ } else {
13813
+ const issue = __spreadValues({
13088
13814
  rule: "missing-background-fallback",
13089
13815
  severity: "warning",
13090
13816
  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
13817
  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
13818
  fix
13093
- });
13819
+ }, loc ? { loc, locs: [loc] } : {});
13820
+ seen.set(key, issue);
13821
+ issues.push(issue);
13094
13822
  }
13095
13823
  }
13096
13824
  const font = style.get("font-family");
13097
13825
  if (font && !CSS_WIDE_KEYWORDS.has(font.trim().toLowerCase()) && !hasFontFallback(font)) {
13098
13826
  const fix = `font-family: ${font.trim()}, Arial, sans-serif;`;
13099
13827
  const key = `font:${font.trim().toLowerCase()}`;
13100
- if (!seen.has(key)) {
13101
- seen.add(key);
13102
- issues.push({
13828
+ const loc = locs == null ? void 0 : locs.get("font-family");
13829
+ const existing = seen.get(key);
13830
+ if (existing) {
13831
+ addOccurrence2(existing, loc);
13832
+ } else {
13833
+ const issue = __spreadValues({
13103
13834
  rule: "missing-font-fallback",
13104
13835
  severity: "warning",
13105
13836
  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
13837
  detail: `End the stack with a web-safe font and a generic family.`,
13107
13838
  fix
13108
- });
13839
+ }, loc ? { loc, locs: [loc] } : {});
13840
+ seen.set(key, issue);
13841
+ issues.push(issue);
13109
13842
  }
13110
13843
  }
13111
13844
  }
@@ -13118,30 +13851,47 @@ function ruleToMap(node) {
13118
13851
  });
13119
13852
  return map;
13120
13853
  }
13121
- function checkVisualFromDom($) {
13854
+ function checkVisualFromDom($, source) {
13122
13855
  const issues = [];
13123
- const seen = /* @__PURE__ */ new Set();
13856
+ const seen = /* @__PURE__ */ new Map();
13124
13857
  $("[style]").each((_, el) => {
13125
- inspectDeclarations(parseInlineStyle($(el).attr("style") || ""), issues, seen);
13858
+ const attrLoc = locOfAttr(el, "style");
13859
+ const style = parseInlineStyle($(el).attr("style") || "");
13860
+ const locs = attrLoc ? new Map([...style.keys()].map((prop) => [prop, attrLoc])) : void 0;
13861
+ inspectDeclarations(style, issues, seen, locs);
13126
13862
  });
13127
13863
  $("style").each((_, el) => {
13864
+ const cssText = $(el).text();
13865
+ const anchor = cssBlockAnchor(el, cssText, source);
13128
13866
  let ast;
13129
13867
  try {
13130
- ast = csstree7.parse($(el).text());
13868
+ ast = csstree7.parse(cssText, { positions: true });
13131
13869
  } catch (e) {
13132
13870
  return;
13133
13871
  }
13134
13872
  csstree7.walk(ast, {
13135
13873
  visit: "Rule",
13136
13874
  enter(node) {
13137
- if (node.type === "Rule") inspectDeclarations(ruleToMap(node), issues, seen);
13875
+ if (node.type !== "Rule") return;
13876
+ const locs = /* @__PURE__ */ new Map();
13877
+ node.block.children.forEach((child) => {
13878
+ if (child.type !== "Declaration") return;
13879
+ const loc = locInCssBlock(anchor, child.loc);
13880
+ if (loc) locs.set(child.property.toLowerCase(), loc);
13881
+ });
13882
+ inspectDeclarations(ruleToMap(node), issues, seen, locs);
13138
13883
  }
13139
13884
  });
13140
13885
  });
13141
13886
  return { issues };
13142
13887
  }
13143
- function checkVisual(html) {
13144
- return fromHtml(html, EMPTY_VISUAL, checkVisualFromDom);
13888
+ function checkVisual(html, options) {
13889
+ return fromHtml(
13890
+ html,
13891
+ EMPTY_VISUAL,
13892
+ ($, h) => checkVisualFromDom($, (options == null ? void 0 : options.positions) ? h : void 0),
13893
+ options
13894
+ );
13145
13895
  }
13146
13896
 
13147
13897
  // src/audit.ts
@@ -13160,7 +13910,8 @@ var EMPTY_AUDIT = {
13160
13910
  function runAudit($, html, framework, options) {
13161
13911
  var _a;
13162
13912
  const skip = new Set((_a = options == null ? void 0 : options.skip) != null ? _a : []);
13163
- const warnings = skip.has("compatibility") ? [] : analyzeEmailFromDom($, framework);
13913
+ const source = (options == null ? void 0 : options.positions) ? html : void 0;
13914
+ const warnings = skip.has("compatibility") ? [] : analyzeEmailFromDom($, framework, source);
13164
13915
  const scores = skip.has("compatibility") ? {} : generateCompatibilityScore(warnings);
13165
13916
  const spam = skip.has("spam") ? EMPTY_SPAM : analyzeSpamFromDom($, options == null ? void 0 : options.spam);
13166
13917
  const links = skip.has("links") ? EMPTY_LINKS : validateLinksFromDom($);
@@ -13168,19 +13919,19 @@ function runAudit($, html, framework, options) {
13168
13919
  const images = skip.has("images") ? EMPTY_IMAGES : analyzeImagesFromDom($);
13169
13920
  const inboxPreview = skip.has("inboxPreview") ? EMPTY_INBOX_PREVIEW : extractInboxPreviewFromDom($);
13170
13921
  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($);
13922
+ const templateVariables = skip.has("templateVariables") ? EMPTY_TEMPLATE : checkTemplateVariablesFromDom($, source);
13923
+ const overflow = skip.has("overflow") ? EMPTY_OVERFLOW : checkOverflowFromDom($, source);
13924
+ const visual = skip.has("visual") ? EMPTY_VISUAL : checkVisualFromDom($, source);
13174
13925
  return { compatibility: { warnings, scores }, spam, links, accessibility, images, inboxPreview, size, templateVariables, overflow, visual };
13175
13926
  }
13176
13927
  function auditEmail(html, options) {
13177
- return fromHtml(html, EMPTY_AUDIT, ($, h) => runAudit($, h, options == null ? void 0 : options.framework, options));
13928
+ return fromHtml(html, EMPTY_AUDIT, ($, h) => runAudit($, h, options == null ? void 0 : options.framework, options), options);
13178
13929
  }
13179
13930
 
13180
13931
  // src/plain-text.ts
13181
- var cheerio6 = __toESM(require("cheerio"), 1);
13932
+ var cheerio5 = __toESM(require("cheerio"), 1);
13182
13933
  function toPlainText(html) {
13183
- const $ = cheerio6.load(html);
13934
+ const $ = cheerio5.load(html);
13184
13935
  $("style, script, head").remove();
13185
13936
  $("[data-skip-in-text='true']").remove();
13186
13937
  const lines = [];
@@ -13282,7 +14033,6 @@ function toPlainText(html) {
13282
14033
  }
13283
14034
 
13284
14035
  // src/session.ts
13285
- var cheerio7 = __toESM(require("cheerio"), 1);
13286
14036
  function createSession(html, options) {
13287
14037
  if (!html || !html.trim()) {
13288
14038
  const fw = options == null ? void 0 : options.framework;
@@ -13309,16 +14059,17 @@ function createSession(html, options) {
13309
14059
  if (html.length > MAX_HTML_SIZE) {
13310
14060
  throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
13311
14061
  }
13312
- const $ = cheerio7.load(html);
14062
+ const $ = loadHtml(html, options);
13313
14063
  const framework = options == null ? void 0 : options.framework;
14064
+ const source = (options == null ? void 0 : options.positions) ? html : void 0;
13314
14065
  return {
13315
14066
  html,
13316
14067
  framework,
13317
14068
  audit(opts) {
13318
- return runAudit($, html, framework, opts);
14069
+ return runAudit($, html, framework, __spreadProps(__spreadValues({}, opts), { positions: options == null ? void 0 : options.positions }));
13319
14070
  },
13320
14071
  analyze() {
13321
- return analyzeEmailFromDom($, framework);
14072
+ return analyzeEmailFromDom($, framework, source);
13322
14073
  },
13323
14074
  score(warnings) {
13324
14075
  return generateCompatibilityScore(warnings);
@@ -13342,13 +14093,13 @@ function createSession(html, options) {
13342
14093
  return checkSizeFromDom($, html);
13343
14094
  },
13344
14095
  checkTemplateVariables() {
13345
- return checkTemplateVariablesFromDom($);
14096
+ return checkTemplateVariablesFromDom($, source);
13346
14097
  },
13347
14098
  checkOverflow() {
13348
- return checkOverflowFromDom($);
14099
+ return checkOverflowFromDom($, source);
13349
14100
  },
13350
14101
  checkVisual() {
13351
- return checkVisualFromDom($);
14102
+ return checkVisualFromDom($, source);
13352
14103
  },
13353
14104
  // Transforms create isolated copies since they mutate the DOM
13354
14105
  transformForClient(clientId) {
@@ -13379,18 +14130,22 @@ var CompileError = class extends Error {
13379
14130
  COMPOUND_VALUE_FEATURES,
13380
14131
  CSS_FUNCTION_FEATURES,
13381
14132
  CSS_SUPPORT,
14133
+ CSS_SUPPORT_NOTES,
13382
14134
  CompileError,
13383
14135
  EMAIL_CLIENTS,
13384
14136
  EMPTY_DELIVERABILITY,
13385
14137
  GENERIC_LINK_TEXT,
13386
14138
  HTML_ELEMENT_FEATURES,
13387
14139
  MAX_HTML_SIZE,
14140
+ MAX_WARNING_LOCATIONS,
13388
14141
  STRUCTURAL_FIX_PROPERTIES,
14142
+ VALUE_CAVEAT_PROPS,
13389
14143
  alphaBlend,
13390
14144
  analyzeEmail,
13391
14145
  analyzeImages,
13392
14146
  analyzeSpam,
13393
14147
  auditEmail,
14148
+ caveatApplies,
13394
14149
  checkAccessibility,
13395
14150
  checkOverflow,
13396
14151
  checkSize,