@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.js CHANGED
@@ -56,7 +56,11 @@ var EMAIL_CLIENTS = [
56
56
  engine: "Microsoft Word",
57
57
  darkModeSupport: true,
58
58
  icon: "monitor",
59
- deprecated: "2026-10"
59
+ // End of support ~Q2 2029 (Microsoft: "supported until at least 2029").
60
+ // The April 2026 date some sources cite is the opt-out phase, when classic
61
+ // stops being the Windows default, not end of support. October 2026 was
62
+ // wrong: the nearest real Oct date is Oct 2025, for legacy Outlook for Mac.
63
+ deprecated: "2029-06"
60
64
  },
61
65
  {
62
66
  id: "outlook-ios",
@@ -8889,6 +8893,16 @@ function getStyleValue(style, property) {
8889
8893
  }
8890
8894
  return null;
8891
8895
  }
8896
+ function getStyleValues(style, property) {
8897
+ const values = [];
8898
+ for (const part of splitStyleDeclarations(style)) {
8899
+ const colonIndex = part.indexOf(":");
8900
+ if (colonIndex === -1) continue;
8901
+ if (part.slice(0, colonIndex).trim().toLowerCase() !== property) continue;
8902
+ values.push(part.slice(colonIndex + 1).trim());
8903
+ }
8904
+ return values;
8905
+ }
8892
8906
  function parseInlineStyle(style) {
8893
8907
  const map = /* @__PURE__ */ new Map();
8894
8908
  const declarations = splitStyleDeclarations(style);
@@ -9639,6 +9653,7 @@ function downlevelCSS(html) {
9639
9653
 
9640
9654
  // src/constants.ts
9641
9655
  var MAX_HTML_SIZE = 2 * 1024 * 1024;
9656
+ var MAX_WARNING_LOCATIONS = 100;
9642
9657
  var GENERIC_LINK_TEXT = /* @__PURE__ */ new Set([
9643
9658
  "click here",
9644
9659
  "here",
@@ -10375,9 +10390,231 @@ function transformForAllClients(html, framework) {
10375
10390
  }
10376
10391
 
10377
10392
  // src/analyze.ts
10378
- import * as cheerio4 from "cheerio";
10379
10393
  import * as csstree5 from "css-tree";
10380
10394
 
10395
+ // src/rules/value-caveats.ts
10396
+ var VALUE_CAVEAT_PROPS = /* @__PURE__ */ new Set([
10397
+ "background",
10398
+ "border-radius",
10399
+ "display",
10400
+ "font-size",
10401
+ "font-weight",
10402
+ "letter-spacing",
10403
+ "margin",
10404
+ "overflow",
10405
+ "position",
10406
+ "text-align",
10407
+ "transition"
10408
+ ]);
10409
+ function normalize(value) {
10410
+ return value.replace(/\/\*[\s\S]*?\*\//g, " ").toLowerCase().replace(/!\s*important/g, " ").replace(/;+\s*$/, "").replace(/\s+/g, " ").trim();
10411
+ }
10412
+ var preparedValues = /* @__PURE__ */ new WeakMap();
10413
+ function prepare(values) {
10414
+ let out = preparedValues.get(values);
10415
+ if (!out) {
10416
+ out = [...new Set(values.map(normalize))];
10417
+ preparedValues.set(values, out);
10418
+ }
10419
+ return out;
10420
+ }
10421
+ function topLevelSplit(value, sep) {
10422
+ const parts = [];
10423
+ let depth = 0;
10424
+ let start = 0;
10425
+ for (let i = 0; i < value.length; i++) {
10426
+ const c = value[i];
10427
+ if (c === "(") depth++;
10428
+ else if (c === ")") depth = Math.max(0, depth - 1);
10429
+ else if (c === sep && depth === 0) {
10430
+ parts.push(value.slice(start, i));
10431
+ start = i + 1;
10432
+ }
10433
+ }
10434
+ parts.push(value.slice(start));
10435
+ return parts.map((p) => p.trim()).filter(Boolean);
10436
+ }
10437
+ function tokens(value) {
10438
+ return topLevelSplit(value, " ");
10439
+ }
10440
+ function hasUnit(value, units) {
10441
+ return new RegExp(`\\d(?:${units.join("|")})\\b`).test(value);
10442
+ }
10443
+ function bareNumbers(value) {
10444
+ return tokens(value).filter((t) => /^[+-]?\d+(?:\.\d+)?(?:e[+-]?\d+)?$/.test(t)).map(Number);
10445
+ }
10446
+ function hasNegative(value) {
10447
+ return /(?:^|[\s,(])-\.?\d/.test(value);
10448
+ }
10449
+ function dashed(v) {
10450
+ return v.replace(/\s+/g, "-");
10451
+ }
10452
+ function unprefixed(token) {
10453
+ return token.replace(/^-(?:webkit|moz|ms|o)-/, "");
10454
+ }
10455
+ function quotedValues(note) {
10456
+ var _a;
10457
+ const supported = [];
10458
+ const banned = [];
10459
+ for (const m of note.matchAll(/`([^`]+)`/g)) {
10460
+ const clause = (_a = note.slice(0, m.index).split(/[.;]/).pop()) != null ? _a : "";
10461
+ (/\bsupports\b/i.test(clause) ? supported : banned).push(dashed(m[1].toLowerCase().trim()));
10462
+ }
10463
+ return { supported, banned };
10464
+ }
10465
+ function isColorOnly(value) {
10466
+ if (value.startsWith("#")) return /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(value);
10467
+ if (parseColor(value)) return true;
10468
+ return ["none", "inherit", "initial", "unset", "revert", "currentcolor"].includes(value) || // A custom property is usually a palette colour, and we cannot resolve it.
10469
+ value.startsWith("var(");
10470
+ }
10471
+ var POSITION_KEYWORDS = ["relative", "absolute", "fixed", "sticky"];
10472
+ var RELATIVE_FONT_UNITS = [
10473
+ "cqmin",
10474
+ "cqmax",
10475
+ "vmin",
10476
+ "vmax",
10477
+ "rlh",
10478
+ "rex",
10479
+ "rch",
10480
+ "ric",
10481
+ "rem",
10482
+ "svw",
10483
+ "svh",
10484
+ "lvw",
10485
+ "lvh",
10486
+ "dvw",
10487
+ "dvh",
10488
+ "cqw",
10489
+ "cqh",
10490
+ "cqi",
10491
+ "cqb",
10492
+ "cap",
10493
+ "ic",
10494
+ "lh",
10495
+ "em",
10496
+ "ex",
10497
+ "ch",
10498
+ "vw",
10499
+ "vh",
10500
+ "vi",
10501
+ "vb"
10502
+ ];
10503
+ var NOT_A_PROPERTY_NAME = /* @__PURE__ */ new Set([
10504
+ "all",
10505
+ "allow-discrete",
10506
+ "normal",
10507
+ "ease",
10508
+ "ease-in",
10509
+ "ease-out",
10510
+ "ease-in-out",
10511
+ "linear",
10512
+ "step-start",
10513
+ "step-end",
10514
+ "none",
10515
+ "initial",
10516
+ "inherit",
10517
+ "unset",
10518
+ "revert",
10519
+ "revert-layer"
10520
+ ]);
10521
+ function namesAProperty(layer) {
10522
+ return tokens(layer).some((t) => /^[a-z][a-z0-9-]*$/.test(t) && !NOT_A_PROPERTY_NAME.has(t));
10523
+ }
10524
+ function triggers(prop, value, note, noteLc) {
10525
+ switch (prop) {
10526
+ case "margin": {
10527
+ const negative = noteLc.includes("negative");
10528
+ const auto = noteLc.includes("auto");
10529
+ if (!negative && !auto) return true;
10530
+ return negative && hasNegative(value) || auto && /\bauto\b/.test(value);
10531
+ }
10532
+ case "position": {
10533
+ const used = POSITION_KEYWORDS.find((k) => tokens(value).some((t) => unprefixed(t) === k));
10534
+ if (!used) return false;
10535
+ const m = note.match(/supports\s+.+?\s+but not\s+([^.]+)/i);
10536
+ if (m) return m[1].toLowerCase().includes(used);
10537
+ return true;
10538
+ }
10539
+ case "overflow": {
10540
+ if (!/\b(?:auto|scroll|overlay)\b/.test(value)) return false;
10541
+ if (!noteLc.includes("cannot scroll") && !noteLc.includes("overflow-block")) return true;
10542
+ return noteLc.includes("cannot scroll");
10543
+ }
10544
+ case "font-size": {
10545
+ if (noteLc.includes("percentage") || noteLc.includes("relative")) {
10546
+ return hasUnit(value, RELATIVE_FONT_UNITS) || /\d\s*%/.test(value) || tokens(value).some((t) => t === "smaller" || t === "larger");
10547
+ }
10548
+ if (noteLc.includes("`rem`")) return hasUnit(value, ["rem"]);
10549
+ return true;
10550
+ }
10551
+ case "display": {
10552
+ const only = note.match(/only supports\s+([^.]*)/i);
10553
+ if (only) {
10554
+ const allowed = quotedValues(only[1]).banned.map((q) => q.replace(/^display:-?/, "")).filter((q) => /^[a-z-]+$/.test(q));
10555
+ if (!allowed.length) return true;
10556
+ return !allowed.includes(dashed(value));
10557
+ }
10558
+ const { banned } = quotedValues(note);
10559
+ if (banned.length) {
10560
+ const v = dashed(value);
10561
+ return banned.includes(v) || banned.includes(unprefixed(v));
10562
+ }
10563
+ if (noteLc.includes("two-value syntax")) return tokens(value).length > 1;
10564
+ return true;
10565
+ }
10566
+ case "font-weight": {
10567
+ const nums = bareNumbers(value);
10568
+ if (!nums.length) return false;
10569
+ if (noteLc.includes("font weight")) return nums.some((n) => n !== 400 && n !== 700);
10570
+ if (noteLc.includes("only the following numeric values")) {
10571
+ return nums.some((n) => n % 100 !== 0 || n < 100 || n > 900);
10572
+ }
10573
+ return true;
10574
+ }
10575
+ case "border-radius": {
10576
+ if (noteLc.includes("slash")) return topLevelSplit(value, "/").length > 1;
10577
+ return true;
10578
+ }
10579
+ case "text-align": {
10580
+ const { supported, banned } = quotedValues(note);
10581
+ if (!banned.length) return true;
10582
+ return tokens(value).some(
10583
+ (t) => !supported.includes(t) && (banned.includes(t) || banned.includes(unprefixed(t)))
10584
+ );
10585
+ }
10586
+ case "background": {
10587
+ if (noteLc.includes("only `background-color`")) return !isColorOnly(value);
10588
+ if (noteLc.includes("multiple values")) {
10589
+ return topLevelSplit(value, ",").length > 1 || topLevelSplit(value, "/").length > 1;
10590
+ }
10591
+ return true;
10592
+ }
10593
+ case "letter-spacing": {
10594
+ const negative = noteLc.includes("negative");
10595
+ const em = noteLc.includes("`em`");
10596
+ if (!negative && !em) return true;
10597
+ return negative && hasNegative(value) || em && hasUnit(value, ["em"]);
10598
+ }
10599
+ case "transition": {
10600
+ if (noteLc.includes("`all`")) {
10601
+ if (["none", "inherit", "initial", "unset", "revert"].includes(value)) return false;
10602
+ return topLevelSplit(value, ",").some((layer) => !namesAProperty(layer));
10603
+ }
10604
+ return true;
10605
+ }
10606
+ default:
10607
+ return true;
10608
+ }
10609
+ }
10610
+ function caveatApplies(prop, values, notes) {
10611
+ if (!VALUE_CAVEAT_PROPS.has(prop)) return true;
10612
+ if (!(values == null ? void 0 : values.length)) return true;
10613
+ const note = (notes != null ? notes : []).join(" ");
10614
+ const noteLc = note.toLowerCase();
10615
+ return prepare(values).some((value) => triggers(prop, value, note, noteLc));
10616
+ }
10617
+
10381
10618
  // src/dark-mode-checker.ts
10382
10619
  import * as csstree4 from "css-tree";
10383
10620
 
@@ -10654,6 +10891,257 @@ function applyColorInversion($, mode) {
10654
10891
  });
10655
10892
  }
10656
10893
 
10894
+ // src/source-location.ts
10895
+ function toLoc(p) {
10896
+ return {
10897
+ line: p.startLine,
10898
+ column: p.startCol,
10899
+ endLine: p.endLine,
10900
+ endColumn: p.endCol,
10901
+ offset: p.startOffset,
10902
+ length: p.endOffset - p.startOffset
10903
+ };
10904
+ }
10905
+ function locOfElement(el) {
10906
+ var _a;
10907
+ const raw = el == null ? void 0 : el.sourceCodeLocation;
10908
+ if (!raw) return void 0;
10909
+ return toLoc((_a = raw.startTag) != null ? _a : raw);
10910
+ }
10911
+ function locOfAttr(el, attr) {
10912
+ var _a, _b, _c, _d;
10913
+ const raw = el == null ? void 0 : el.sourceCodeLocation;
10914
+ if (!raw) return void 0;
10915
+ const attrLoc = (_d = (_a = raw.attrs) == null ? void 0 : _a[attr]) != null ? _d : (_c = (_b = raw.startTag) == null ? void 0 : _b.attrs) == null ? void 0 : _c[attr];
10916
+ return attrLoc ? toLoc(attrLoc) : locOfElement(el);
10917
+ }
10918
+ function locInAttr(attrLoc, source, property, occurrence = 0) {
10919
+ if (!attrLoc || !source) return void 0;
10920
+ const raw = source.slice(attrLoc.offset, attrLoc.offset + attrLoc.length);
10921
+ const open = raw.search(/["']/);
10922
+ const close = raw.lastIndexOf(raw[open]);
10923
+ if (open === -1 || close <= open) return void 0;
10924
+ const found = declarationsIn(raw.slice(open + 1, close), property);
10925
+ const hit = found[occurrence];
10926
+ if (!hit) return void 0;
10927
+ const start = attrLoc.offset + open + 1 + hit.start;
10928
+ const end = attrLoc.offset + open + 1 + hit.end;
10929
+ const from = positionOf(source, start);
10930
+ const to = positionOf(source, end);
10931
+ return {
10932
+ line: from.line,
10933
+ column: from.column,
10934
+ endLine: to.line,
10935
+ endColumn: to.column,
10936
+ offset: start,
10937
+ length: end - start
10938
+ };
10939
+ }
10940
+ function declarationsIn(value, property) {
10941
+ const wanted = property.toLowerCase();
10942
+ const found = [];
10943
+ let depth = 0;
10944
+ let start = 0;
10945
+ const consider = (from, to) => {
10946
+ const text = value.slice(from, to);
10947
+ const colon = text.indexOf(":");
10948
+ if (colon === -1) return;
10949
+ if (text.slice(0, colon).trim().toLowerCase() !== wanted) return;
10950
+ const lead = text.length - text.trimStart().length;
10951
+ const trail = text.length - text.trimEnd().length;
10952
+ if (from + lead < to - trail) found.push({ start: from + lead, end: to - trail });
10953
+ };
10954
+ for (let i = 0; i < value.length; i++) {
10955
+ const c = value[i];
10956
+ if (c === "(") depth++;
10957
+ else if (c === ")") depth = Math.max(0, depth - 1);
10958
+ else if (c === ";" && depth === 0) {
10959
+ consider(start, i);
10960
+ start = i + 1;
10961
+ }
10962
+ }
10963
+ consider(start, value.length);
10964
+ return found;
10965
+ }
10966
+ function locOfFirst($, selector) {
10967
+ const el = $(selector).first()[0];
10968
+ return el ? locOfElement(el) : void 0;
10969
+ }
10970
+ function cssBlockAnchor(styleEl, cssText, source) {
10971
+ var _a;
10972
+ const children = styleEl == null ? void 0 : styleEl.children;
10973
+ if (!children || children.length !== 1) return void 0;
10974
+ const loc = (_a = children[0]) == null ? void 0 : _a.sourceCodeLocation;
10975
+ if (!loc) return void 0;
10976
+ const mapper = source ? crMapper(source.slice(loc.startOffset, loc.endOffset), cssText) : null;
10977
+ const extraBefore = mapper ? (index) => mapper(index) - index : crOffsetter(cssText, loc.endOffset - loc.startOffset);
10978
+ return __spreadValues({ loc, extraBefore }, mapper ? { source } : {});
10979
+ }
10980
+ function crMapper(raw, decoded) {
10981
+ if (raw.length === decoded.length) return (index) => index;
10982
+ const points = [];
10983
+ const extras = [];
10984
+ let r = 0;
10985
+ let d = 0;
10986
+ let extra = 0;
10987
+ while (d < decoded.length) {
10988
+ if (r >= raw.length) return null;
10989
+ if (raw[r] === decoded[d]) {
10990
+ r++;
10991
+ d++;
10992
+ continue;
10993
+ }
10994
+ if (raw[r] === "\r" && decoded[d] === "\n") {
10995
+ const consumed = raw[r + 1] === "\n" ? 2 : 1;
10996
+ extra += consumed - 1;
10997
+ points.push(d);
10998
+ extras.push(extra);
10999
+ r += consumed;
11000
+ d += 1;
11001
+ continue;
11002
+ }
11003
+ return null;
11004
+ }
11005
+ if (r !== raw.length) return null;
11006
+ return (index) => index + lookup(points, extras, index);
11007
+ }
11008
+ function lookup(points, extras, index) {
11009
+ let lo = 0;
11010
+ let hi = points.length - 1;
11011
+ let found = 0;
11012
+ while (lo <= hi) {
11013
+ const mid = lo + hi >> 1;
11014
+ if (points[mid] < index) {
11015
+ found = extras[mid];
11016
+ lo = mid + 1;
11017
+ } else {
11018
+ hi = mid - 1;
11019
+ }
11020
+ }
11021
+ return found;
11022
+ }
11023
+ function findRawOffset(raw, decoded, index, token) {
11024
+ if (!token) return -1;
11025
+ let occurrence = 0;
11026
+ for (let at = decoded.indexOf(token); at !== -1 && at < index; at = decoded.indexOf(token, at + 1)) {
11027
+ occurrence++;
11028
+ }
11029
+ let found = -1;
11030
+ let from = 0;
11031
+ for (let i = 0; i <= occurrence; i++) {
11032
+ found = raw.indexOf(token, from);
11033
+ if (found === -1) return -1;
11034
+ from = found + 1;
11035
+ }
11036
+ return found;
11037
+ }
11038
+ function positionOf(source, offset) {
11039
+ const prefix = source.slice(0, offset);
11040
+ return { line: prefix.split("\n").length, column: offset - prefix.lastIndexOf("\n") };
11041
+ }
11042
+ function crOffsetter(text, rawLength) {
11043
+ const removed = rawLength - text.length;
11044
+ if (removed === 0) return () => 0;
11045
+ const newlines = countNewlines(text, text.length);
11046
+ if (removed !== newlines || newlines === 0) return null;
11047
+ return (index) => countNewlines(text, index);
11048
+ }
11049
+ function countNewlines(text, upTo) {
11050
+ let n = 0;
11051
+ for (let i = 0; i < upTo && i < text.length; i++) if (text.charCodeAt(i) === 10) n++;
11052
+ return n;
11053
+ }
11054
+ function locInCssBlock(anchor, cssLoc) {
11055
+ if (!anchor || !cssLoc) return void 0;
11056
+ const { loc: block, extraBefore } = anchor;
11057
+ if (!extraBefore) {
11058
+ return {
11059
+ line: block.startLine,
11060
+ column: block.startCol,
11061
+ endLine: block.startLine,
11062
+ endColumn: block.startCol,
11063
+ offset: block.startOffset,
11064
+ length: 0
11065
+ };
11066
+ }
11067
+ const line = block.startLine + cssLoc.start.line - 1;
11068
+ const column = cssLoc.start.line === 1 ? block.startCol + cssLoc.start.column - 1 : cssLoc.start.column;
11069
+ const endLine = block.startLine + cssLoc.end.line - 1;
11070
+ const endColumn = cssLoc.end.line === 1 ? block.startCol + cssLoc.end.column - 1 : cssLoc.end.column;
11071
+ const start = block.startOffset + cssLoc.start.offset + extraBefore(cssLoc.start.offset);
11072
+ const end = block.startOffset + cssLoc.end.offset + extraBefore(cssLoc.end.offset);
11073
+ if (anchor.source) {
11074
+ const from = positionOf(anchor.source, start);
11075
+ const to = positionOf(anchor.source, end);
11076
+ return {
11077
+ line: from.line,
11078
+ column: from.column,
11079
+ endLine: to.line,
11080
+ endColumn: to.column,
11081
+ offset: start,
11082
+ length: end - start
11083
+ };
11084
+ }
11085
+ return { line, column, endLine, endColumn, offset: start, length: end - start };
11086
+ }
11087
+ function locInTextNode(node, index, length, source) {
11088
+ var _a;
11089
+ const anchor = node == null ? void 0 : node.sourceCodeLocation;
11090
+ if (!anchor) return void 0;
11091
+ const data = (_a = node.data) != null ? _a : "";
11092
+ const rawLength = anchor.endOffset - anchor.startOffset;
11093
+ if (source) {
11094
+ const raw = source.slice(anchor.startOffset, anchor.endOffset);
11095
+ const token = data.slice(index, index + length);
11096
+ const at = findRawOffset(raw, data, index, token);
11097
+ if (at !== -1) {
11098
+ const start2 = anchor.startOffset + at;
11099
+ const from = positionOf(source, start2);
11100
+ const to = positionOf(source, start2 + token.length);
11101
+ return {
11102
+ line: from.line,
11103
+ column: from.column,
11104
+ endLine: to.line,
11105
+ endColumn: to.column,
11106
+ offset: start2,
11107
+ length: token.length
11108
+ };
11109
+ }
11110
+ }
11111
+ const extraBefore = crOffsetter(data, rawLength);
11112
+ if (!extraBefore) {
11113
+ const clamped = Math.min(length, rawLength);
11114
+ return {
11115
+ line: anchor.startLine,
11116
+ column: anchor.startCol,
11117
+ endLine: anchor.startLine,
11118
+ endColumn: anchor.startCol + clamped,
11119
+ offset: anchor.startOffset,
11120
+ length: clamped
11121
+ };
11122
+ }
11123
+ const start = positionAt(data, index, anchor);
11124
+ const end = positionAt(data, index + length, anchor);
11125
+ const startOffset = anchor.startOffset + index + extraBefore(index);
11126
+ const endOffset = anchor.startOffset + index + length + extraBefore(index + length);
11127
+ return {
11128
+ line: start.line,
11129
+ column: start.column,
11130
+ endLine: end.line,
11131
+ endColumn: end.column,
11132
+ offset: startOffset,
11133
+ length: endOffset - startOffset
11134
+ };
11135
+ }
11136
+ function positionAt(data, index, anchor) {
11137
+ const prefix = data.slice(0, index);
11138
+ const newlines = prefix.split("\n").length - 1;
11139
+ if (newlines === 0) {
11140
+ return { line: anchor.startLine, column: anchor.startCol + index };
11141
+ }
11142
+ return { line: anchor.startLine + newlines, column: index - prefix.lastIndexOf("\n") };
11143
+ }
11144
+
10657
11145
  // src/dark-mode-checker.ts
10658
11146
  var DARK_MEDIA_RE = /\(\s*prefers-color-scheme\s*:\s*dark\s*\)/i;
10659
11147
  var MAX_UNCOVERED_ELEMENTS = 3;
@@ -10740,7 +11228,7 @@ function describeSelector($, el) {
10740
11228
  if (cls) return `${tag}.${cls.split(/\s+/)[0]}`;
10741
11229
  return tag;
10742
11230
  }
10743
- function checkDarkModeFromDom($) {
11231
+ function checkDarkModeFromDom($, source) {
10744
11232
  var _a, _b;
10745
11233
  const darkBlock = collectDarkBlocks($);
10746
11234
  if (!darkBlock) return [];
@@ -10750,15 +11238,16 @@ function checkDarkModeFromDom($) {
10750
11238
  return name === "color-scheme" || name === "supported-color-schemes";
10751
11239
  });
10752
11240
  if (!hasOptIn) {
11241
+ const headLoc = locOfFirst($, "head");
10753
11242
  for (const clientId of PREFERS_COLOR_SCHEME_CLIENTS) {
10754
- warnings.push({
11243
+ warnings.push(__spreadValues({
10755
11244
  severity: "warning",
10756
11245
  client: clientId,
10757
11246
  property: "dark-mode-opt-in",
10758
11247
  message: `The email has @media (prefers-color-scheme: dark) styles but no dark-mode opt-in meta tag. ${(_b = (_a = getClient(clientId)) == null ? void 0 : _a.name) != null ? _b : clientId} may keep the email in light mode, so the dark styles never activate.`,
10759
11248
  suggestion: 'Add both opt-in tags to <head>: <meta name="color-scheme" content="light dark"> and <meta name="supported-color-schemes" content="light dark">.',
10760
11249
  fixType: "structural"
10761
- });
11250
+ }, headLoc ? { loc: headLoc, locs: [headLoc] } : {}));
10762
11251
  }
10763
11252
  }
10764
11253
  if (darkBlock.rules === 0) return warnings;
@@ -10766,7 +11255,7 @@ function checkDarkModeFromDom($) {
10766
11255
  const coveredByAny = matchedElements($, darkBlock.any);
10767
11256
  let uncovered = 0;
10768
11257
  $("[bgcolor], [style]").each((_, el) => {
10769
- var _a2;
11258
+ var _a2, _b2;
10770
11259
  if (uncovered >= MAX_UNCOVERED_ELEMENTS) return false;
10771
11260
  const $el = $(el);
10772
11261
  const style = parseInlineStyle($el.attr("style") || "");
@@ -10776,8 +11265,10 @@ function checkDarkModeFromDom($) {
10776
11265
  if (inline ? coveredByImportant.has(el) : coveredByAny.has(el)) return;
10777
11266
  uncovered++;
10778
11267
  const selector = describeSelector($, el);
11268
+ const attrLoc = locOfAttr(el, inline ? "style" : "bgcolor");
11269
+ const loc = inline ? (_b2 = locInAttr(attrLoc, source, style.get("background-color") !== void 0 ? "background-color" : "background")) != null ? _b2 : attrLoc : attrLoc;
10779
11270
  for (const clientId of PREFERS_COLOR_SCHEME_CLIENTS) {
10780
- warnings.push({
11271
+ warnings.push(__spreadValues({
10781
11272
  severity: "warning",
10782
11273
  client: clientId,
10783
11274
  property: "dark-mode-coverage",
@@ -10785,12 +11276,25 @@ function checkDarkModeFromDom($) {
10785
11276
  suggestion: inline ? `Override it inside @media (prefers-color-scheme: dark) with a dark background-color and !important (an inline style beats a plain rule), or move the colour onto a class the dark block already targets.` : `Override it inside @media (prefers-color-scheme: dark) with a dark background-color, or give the element a class the dark block already targets.`,
10786
11277
  fixType: "css",
10787
11278
  selector
10788
- });
11279
+ }, loc ? { loc, locs: [loc] } : {}));
10789
11280
  }
10790
11281
  });
10791
11282
  return warnings;
10792
11283
  }
10793
11284
 
11285
+ // src/parse-html.ts
11286
+ import * as cheerio4 from "cheerio";
11287
+ function loadHtml(html, options) {
11288
+ return (options == null ? void 0 : options.positions) ? cheerio4.load(html, { sourceCodeLocationInfo: true }) : cheerio4.load(html);
11289
+ }
11290
+ function fromHtml(html, empty, fn, options) {
11291
+ if (!html || !html.trim()) return empty;
11292
+ if (html.length > MAX_HTML_SIZE) {
11293
+ throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
11294
+ }
11295
+ return fn(loadHtml(html, options), html);
11296
+ }
11297
+
10794
11298
  // src/analyze.ts
10795
11299
  var HTML_ELEMENT_SELECTORS = {
10796
11300
  "<style>": "style",
@@ -10843,14 +11347,25 @@ var CSS_FUNCTION_DETECTORS = CSS_FUNCTION_FEATURES.map((fn) => ({
10843
11347
  pattern: `${fn}(`
10844
11348
  // require opening paren — matches "min(" but not "Minion"
10845
11349
  }));
10846
- function analyzeEmailFromDom($, framework) {
11350
+ function analyzeEmailFromDom($, framework, source) {
10847
11351
  const warnings = [];
10848
- const seenWarnings = /* @__PURE__ */ new Set();
11352
+ const seenWarnings = /* @__PURE__ */ new Map();
10849
11353
  function addWarning(w) {
10850
11354
  const key = `${w.client}:${w.property}:${w.severity}:${w.selector || ""}`;
10851
- if (!seenWarnings.has(key)) {
10852
- seenWarnings.add(key);
11355
+ const existing = seenWarnings.get(key);
11356
+ if (!existing) {
11357
+ seenWarnings.set(key, w);
10853
11358
  warnings.push(w);
11359
+ return;
11360
+ }
11361
+ if (!existing.locs || !w.locs) return;
11362
+ for (const loc of w.locs) {
11363
+ if (existing.locs.some((l) => l.offset === loc.offset)) continue;
11364
+ if (existing.locs.length >= MAX_WARNING_LOCATIONS) {
11365
+ existing.locsTruncated = true;
11366
+ break;
11367
+ }
11368
+ existing.locs.push(loc);
10854
11369
  }
10855
11370
  }
10856
11371
  function describeSelector2(el) {
@@ -10868,10 +11383,16 @@ function analyzeEmailFromDom($, framework) {
10868
11383
  for (const feature of HTML_ELEMENT_FEATURES) {
10869
11384
  const selector = HTML_ELEMENT_SELECTORS[feature];
10870
11385
  if (!selector) continue;
10871
- if ($(selector).length === 0) continue;
11386
+ const matches = $(selector);
11387
+ if (matches.length === 0) continue;
10872
11388
  const supportData = CSS_SUPPORT[feature];
10873
11389
  if (!supportData) continue;
10874
11390
  const baseSeverity = HTML_ELEMENT_SEVERITY[feature] || "warning";
11391
+ const found = matches.toArray().map((m) => locOfElement(m)).filter((l) => l !== void 0);
11392
+ const featureOccurrences = found.length ? __spreadValues({
11393
+ locs: found.slice(0, MAX_WARNING_LOCATIONS)
11394
+ }, found.length > MAX_WARNING_LOCATIONS ? { truncated: true } : {}) : void 0;
11395
+ const featureLoc = featureOccurrences == null ? void 0 : featureOccurrences.locs[0];
10875
11396
  for (const client of EMAIL_CLIENTS) {
10876
11397
  const support = supportData[client.id];
10877
11398
  if (support === "unsupported") {
@@ -10879,7 +11400,7 @@ function analyzeEmailFromDom($, framework) {
10879
11400
  const message = msgFn ? msgFn(client.name) : `${client.name} does not support ${feature}.`;
10880
11401
  const sug = getSuggestion(feature, client.id, framework);
10881
11402
  const fix = getCodeFix(feature, client.id, framework);
10882
- addWarning(__spreadValues({
11403
+ addWarning(__spreadValues(__spreadValues({
10883
11404
  severity: baseSeverity,
10884
11405
  client: client.id,
10885
11406
  property: feature,
@@ -10887,11 +11408,11 @@ function analyzeEmailFromDom($, framework) {
10887
11408
  suggestion: sug.text,
10888
11409
  fix,
10889
11410
  fixType: getFixType(feature)
10890
- }, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(feature, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
11411
+ }, featureOccurrences ? occurrenceFields(featureOccurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(feature, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
10891
11412
  } else if (support === "partial" && feature === "<style>") {
10892
11413
  const sug = getSuggestion("<style>:partial", client.id, framework);
10893
11414
  const fix = getCodeFix("<style>", client.id, framework);
10894
- addWarning(__spreadValues({
11415
+ addWarning(__spreadValues(__spreadValues({
10895
11416
  severity: "warning",
10896
11417
  client: client.id,
10897
11418
  property: "<style>",
@@ -10899,55 +11420,97 @@ function analyzeEmailFromDom($, framework) {
10899
11420
  suggestion: sug.text,
10900
11421
  fix,
10901
11422
  fixType: getFixType("<style>")
10902
- }, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<style>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
11423
+ }, featureOccurrences ? occurrenceFields(featureOccurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<style>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
10903
11424
  }
10904
11425
  }
10905
11426
  }
10906
11427
  const parsedAtRules = /* @__PURE__ */ new Set();
11428
+ const selectorLocs = /* @__PURE__ */ new Map();
10907
11429
  const parsedProperties = /* @__PURE__ */ new Set();
10908
11430
  const propertyLines = /* @__PURE__ */ new Map();
11431
+ const propertyLocs = /* @__PURE__ */ new Map();
10909
11432
  const propertyValues = /* @__PURE__ */ new Map();
10910
11433
  const detectedCssFunctions = /* @__PURE__ */ new Set();
10911
11434
  const detectedPseudoClasses = /* @__PURE__ */ new Set();
10912
11435
  const detectedPseudoElements = /* @__PURE__ */ new Set();
11436
+ let blockAnchor;
11437
+ function recordSelectorLoc(key, cssLoc) {
11438
+ if (!cssLoc) return;
11439
+ const loc = locInCssBlock(blockAnchor, cssLoc);
11440
+ if (!loc) return;
11441
+ const seen = selectorLocs.get(key);
11442
+ if (!seen) {
11443
+ selectorLocs.set(key, { locs: [loc] });
11444
+ return;
11445
+ }
11446
+ if (seen.locs.some((l) => l.offset === loc.offset)) return;
11447
+ if (seen.locs.length >= MAX_WARNING_LOCATIONS) {
11448
+ seen.truncated = true;
11449
+ return;
11450
+ }
11451
+ seen.locs.push(loc);
11452
+ }
11453
+ function recordLoc(key, cssLoc, value) {
11454
+ const loc = locInCssBlock(blockAnchor, cssLoc);
11455
+ if (!loc) return;
11456
+ const seen = propertyLocs.get(key);
11457
+ if (!seen) {
11458
+ propertyLocs.set(key, __spreadValues({ locs: [loc] }, value !== void 0 ? { values: [value] } : {}));
11459
+ return;
11460
+ }
11461
+ if (seen.locs.some((l) => l.offset === loc.offset)) return;
11462
+ if (seen.locs.length >= MAX_WARNING_LOCATIONS) {
11463
+ seen.truncated = true;
11464
+ return;
11465
+ }
11466
+ seen.locs.push(loc);
11467
+ if (seen.values && value !== void 0) seen.values.push(value);
11468
+ }
10913
11469
  $("style").each((_, el) => {
10914
11470
  const cssText = $(el).text();
11471
+ blockAnchor = cssBlockAnchor(el, cssText, source);
10915
11472
  try {
10916
11473
  const ast = csstree5.parse(cssText, { parseCustomProperty: true, positions: true });
10917
11474
  csstree5.walk(ast, {
10918
11475
  enter(node) {
10919
11476
  if (node.type === "Atrule") {
10920
11477
  parsedAtRules.add(`@${node.name}`);
11478
+ recordSelectorLoc(`@${node.name}`, node.loc);
10921
11479
  }
10922
11480
  if (node.type === "PseudoClassSelector") {
10923
11481
  detectedPseudoClasses.add(`:${node.name}`);
11482
+ recordSelectorLoc(`:${node.name}`, node.loc);
10924
11483
  }
10925
11484
  if (node.type === "PseudoElementSelector") {
10926
11485
  detectedPseudoElements.add(`::${node.name}`);
11486
+ recordSelectorLoc(`::${node.name}`, node.loc);
10927
11487
  }
10928
11488
  if (node.type === "Declaration") {
10929
11489
  const prop = node.property.toLowerCase();
10930
11490
  parsedProperties.add(prop);
10931
- if (node.loc && !propertyLines.has(prop)) {
10932
- propertyLines.set(prop, node.loc.start.line);
10933
- }
10934
11491
  const valueStr = csstree5.generate(node.value);
10935
11492
  const seenValues = propertyValues.get(prop);
10936
11493
  if (seenValues) seenValues.push(valueStr);
10937
11494
  else propertyValues.set(prop, [valueStr]);
11495
+ if (node.loc) {
11496
+ if (!propertyLines.has(prop)) propertyLines.set(prop, node.loc.start.line);
11497
+ recordLoc(prop, node.loc, valueStr);
11498
+ }
10938
11499
  for (const det of COMPOUND_DETECTORS) {
10939
- if (prop === det.property && valueStr.includes(det.valueIncludes)) {
11500
+ if (prop === det.property && valueStr.toLowerCase().includes(det.valueIncludes)) {
10940
11501
  parsedProperties.add(det.key);
10941
- if (node.loc && !propertyLines.has(det.key)) {
10942
- propertyLines.set(det.key, node.loc.start.line);
11502
+ if (node.loc) {
11503
+ if (!propertyLines.has(det.key)) propertyLines.set(det.key, node.loc.start.line);
11504
+ recordLoc(det.key, node.loc);
10943
11505
  }
10944
11506
  }
10945
11507
  }
10946
11508
  for (const fn of CSS_FUNCTION_DETECTORS) {
10947
11509
  if (valueStr.includes(fn.pattern)) {
10948
11510
  detectedCssFunctions.add(fn.key);
10949
- if (node.loc && !propertyLines.has(fn.key)) {
10950
- propertyLines.set(fn.key, node.loc.start.line);
11511
+ if (node.loc) {
11512
+ if (!propertyLines.has(fn.key)) propertyLines.set(fn.key, node.loc.start.line);
11513
+ recordLoc(fn.key, node.loc);
10951
11514
  }
10952
11515
  }
10953
11516
  }
@@ -10959,33 +11522,69 @@ function analyzeEmailFromDom($, framework) {
10959
11522
  });
10960
11523
  for (const atRule of AT_RULE_FEATURES) {
10961
11524
  if (!parsedAtRules.has(atRule)) continue;
10962
- checkPropertySupport(atRule, addWarning, framework);
11525
+ checkPropertySupport(atRule, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(atRule));
10963
11526
  }
10964
11527
  const cssPropertiesToCheck = Object.keys(CSS_SUPPORT).filter(
10965
11528
  (k) => !k.startsWith("<") && !k.startsWith("@")
10966
11529
  );
10967
11530
  $("[style]").each((_, el) => {
10968
- var _a;
10969
11531
  const style = $(el).attr("style") || "";
10970
11532
  const props = parseStyleProperties(style);
10971
11533
  const selector = describeSelector2(el);
11534
+ const attrLoc = locOfAttr(el, "style");
11535
+ const declarationLocs = (prop, occurrence = 0) => {
11536
+ var _a;
11537
+ return (_a = elementLocs(locInAttr(attrLoc, source, prop, occurrence))) != null ? _a : elementLocs(attrLoc);
11538
+ };
11539
+ const locs = elementLocs(attrLoc);
10972
11540
  for (const prop of props) {
10973
11541
  for (const det of COMPOUND_DETECTORS) {
10974
11542
  if (prop === det.property) {
10975
11543
  const value2 = getStyleValue(style, prop);
10976
- if (value2 == null ? void 0 : value2.includes(det.valueIncludes)) {
10977
- checkPropertySupport(det.key, addWarning, framework, selector);
11544
+ if (value2 == null ? void 0 : value2.toLowerCase().includes(det.valueIncludes)) {
11545
+ checkPropertySupport(
11546
+ det.key,
11547
+ addWarning,
11548
+ framework,
11549
+ selector,
11550
+ void 0,
11551
+ void 0,
11552
+ declarationLocs(prop)
11553
+ );
10978
11554
  }
10979
11555
  }
10980
11556
  }
10981
11557
  if (cssPropertiesToCheck.includes(prop)) {
10982
- checkPropertySupport(prop, addWarning, framework, selector, void 0, (_a = getStyleValue(style, prop)) != null ? _a : void 0);
11558
+ const declared = getStyleValues(style, prop);
11559
+ const placed = [];
11560
+ declared.forEach((value2, i) => {
11561
+ const at = locInAttr(attrLoc, source, prop, i);
11562
+ if (at) placed.push({ value: value2, loc: at });
11563
+ });
11564
+ const occurrences = placed.length === declared.length && placed.length > 0 ? { locs: placed.map((p) => p.loc), values: placed.map((p) => p.value) } : locs;
11565
+ checkPropertySupport(
11566
+ prop,
11567
+ addWarning,
11568
+ framework,
11569
+ selector,
11570
+ void 0,
11571
+ declared.length ? declared : void 0,
11572
+ occurrences
11573
+ );
10983
11574
  }
10984
11575
  const value = getStyleValue(style, prop);
10985
11576
  if (value) {
10986
11577
  for (const fn of CSS_FUNCTION_DETECTORS) {
10987
11578
  if (value.includes(fn.pattern)) {
10988
- checkPropertySupport(fn.key, addWarning, framework, selector);
11579
+ checkPropertySupport(
11580
+ fn.key,
11581
+ addWarning,
11582
+ framework,
11583
+ selector,
11584
+ void 0,
11585
+ void 0,
11586
+ declarationLocs(prop)
11587
+ );
10989
11588
  }
10990
11589
  }
10991
11590
  }
@@ -11001,87 +11600,66 @@ function analyzeEmailFromDom($, framework) {
11001
11600
  framework,
11002
11601
  void 0,
11003
11602
  propertyLines.get(prop),
11004
- values ? values.join(" ") : void 0
11603
+ values,
11604
+ propertyLocs.get(prop)
11005
11605
  );
11006
11606
  }
11007
11607
  for (const compound of COMPOUND_VALUE_FEATURES) {
11008
11608
  if (compound.startsWith(":") || compound.startsWith("::")) continue;
11009
11609
  if (parsedProperties.has(compound)) {
11010
- checkPropertySupport(compound, addWarning, framework, void 0, propertyLines.get(compound));
11610
+ checkPropertySupport(compound, addWarning, framework, void 0, propertyLines.get(compound), void 0, propertyLocs.get(compound));
11011
11611
  }
11012
11612
  }
11013
11613
  for (const pseudo of detectedPseudoClasses) {
11014
11614
  if (CSS_SUPPORT[pseudo]) {
11015
- checkPropertySupport(pseudo, addWarning, framework);
11615
+ checkPropertySupport(pseudo, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(pseudo));
11016
11616
  }
11017
11617
  }
11018
11618
  for (const pseudo of detectedPseudoElements) {
11019
11619
  if (CSS_SUPPORT[pseudo]) {
11020
- checkPropertySupport(pseudo, addWarning, framework);
11620
+ checkPropertySupport(pseudo, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(pseudo));
11021
11621
  }
11022
11622
  }
11023
11623
  for (const fn of detectedCssFunctions) {
11024
- checkPropertySupport(fn, addWarning, framework, void 0, propertyLines.get(fn));
11624
+ checkPropertySupport(fn, addWarning, framework, void 0, propertyLines.get(fn), void 0, propertyLocs.get(fn));
11025
11625
  }
11026
- for (const w of checkDarkModeFromDom($)) addWarning(w);
11626
+ for (const w of checkDarkModeFromDom($, source)) addWarning(w);
11027
11627
  const severityOrder = { error: 0, warning: 1, info: 2 };
11028
11628
  warnings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
11029
11629
  return warnings;
11030
11630
  }
11031
- function analyzeEmail(html, framework) {
11631
+ function analyzeEmail(html, framework, options) {
11032
11632
  if (!html || !html.trim()) {
11033
11633
  return [];
11034
11634
  }
11035
11635
  if (html.length > MAX_HTML_SIZE) {
11036
11636
  throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
11037
11637
  }
11038
- const $ = cheerio4.load(html);
11039
- return analyzeEmailFromDom($, framework);
11638
+ const $ = loadHtml(html, options);
11639
+ return analyzeEmailFromDom($, framework, (options == null ? void 0 : options.positions) ? html : void 0);
11040
11640
  }
11041
11641
  function getFixType(prop) {
11042
11642
  return STRUCTURAL_FIX_PROPERTIES.has(prop) ? "structural" : "css";
11043
11643
  }
11044
- var VALUE_CAVEAT_PROPS = /* @__PURE__ */ new Set(["margin", "position", "overflow"]);
11045
- var POSITION_KEYWORDS = ["relative", "absolute", "fixed", "sticky"];
11046
- function valueTriggersCaveat(prop, value, notes) {
11047
- const note = (notes != null ? notes : []).join(" ");
11048
- const noteLc = note.toLowerCase();
11049
- if (prop === "margin") {
11050
- if (/(?:^|[\s:(])-\.?\d/.test(value) && noteLc.includes("negative")) return true;
11051
- if (/\bauto\b/.test(value) && noteLc.includes("auto")) return true;
11052
- return false;
11053
- }
11054
- if (prop === "position") {
11055
- const used = POSITION_KEYWORDS.find((k) => new RegExp(`\\b${k}\\b`).test(value));
11056
- if (!used) return false;
11057
- const m = note.match(/supports\s+.+?\s+but not\s+([^.]+)/i);
11058
- if (m) return m[1].toLowerCase().includes(used);
11059
- return used === "fixed" || used === "sticky";
11060
- }
11061
- if (prop === "overflow") {
11062
- if (!/\b(?:auto|scroll)\b/.test(value)) return false;
11063
- return noteLc.includes("cannot scroll");
11064
- }
11065
- return true;
11066
- }
11067
11644
  function noteSuffix(notes) {
11068
11645
  if (!(notes == null ? void 0 : notes.length)) return "";
11069
11646
  const cleaned = notes.map((n) => n.replace(/^(?:Partial|Buggy|Not supported)\.\s*/i, "").trim()).filter(Boolean);
11070
11647
  return cleaned.length ? ` ${cleaned.join(" ")}` : "";
11071
11648
  }
11072
- function checkPropertySupport(prop, addWarning, framework, selector, line, value) {
11073
- var _a;
11649
+ function checkPropertySupport(prop, addWarning, framework, selector, line, values, occurrences) {
11650
+ var _a, _b, _c, _d, _e, _f;
11651
+ const loc = occurrences == null ? void 0 : occurrences.locs[0];
11652
+ const reportedLine = (_a = loc == null ? void 0 : loc.line) != null ? _a : line;
11074
11653
  const supportData = CSS_SUPPORT[prop];
11075
11654
  if (!supportData) return;
11076
11655
  const fixType = getFixType(prop);
11077
- const valueGated = VALUE_CAVEAT_PROPS.has(prop);
11078
11656
  for (const client of EMAIL_CLIENTS) {
11079
11657
  const support = supportData[client.id] || "unknown";
11080
- const notes = (_a = CSS_SUPPORT_NOTES[prop]) == null ? void 0 : _a[client.id];
11658
+ const notes = (_b = CSS_SUPPORT_NOTES[prop]) == null ? void 0 : _b[client.id];
11081
11659
  if (support === "unsupported") {
11082
11660
  const sug = getSuggestion(prop, client.id, framework);
11083
11661
  const fix = getCodeFix(prop, client.id, framework);
11084
- addWarning(__spreadValues(__spreadValues(__spreadValues({
11662
+ addWarning(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
11085
11663
  severity: "warning",
11086
11664
  client: client.id,
11087
11665
  property: prop,
@@ -11089,12 +11667,13 @@ function checkPropertySupport(prop, addWarning, framework, selector, line, value
11089
11667
  suggestion: sug.text,
11090
11668
  fix,
11091
11669
  fixType
11092
- }, selector ? { selector } : {}), line !== void 0 ? { line } : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
11670
+ }, selector ? { selector } : {}), reportedLine !== void 0 ? { line: reportedLine } : {}), occurrences ? occurrenceFields(occurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
11093
11671
  } else if (support === "partial") {
11094
- if (valueGated && value !== void 0 && !valueTriggersCaveat(prop, value, notes)) continue;
11672
+ if (!caveatApplies(prop, values, notes)) continue;
11673
+ const hits = triggeringOccurrences(prop, occurrences, notes);
11095
11674
  const sug = getSuggestion(prop, client.id, framework);
11096
11675
  const fix = getCodeFix(prop, client.id, framework);
11097
- addWarning(__spreadValues(__spreadValues(__spreadValues({
11676
+ addWarning(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
11098
11677
  severity: "info",
11099
11678
  client: client.id,
11100
11679
  property: prop,
@@ -11102,7 +11681,7 @@ function checkPropertySupport(prop, addWarning, framework, selector, line, value
11102
11681
  suggestion: sug.text,
11103
11682
  fix,
11104
11683
  fixType
11105
- }, selector ? { selector } : {}), line !== void 0 ? { line } : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
11684
+ }, selector ? { selector } : {}), ((_d = (_c = hits == null ? void 0 : hits.locs[0]) == null ? void 0 : _c.line) != null ? _d : reportedLine) !== void 0 ? { line: (_f = (_e = hits == null ? void 0 : hits.locs[0]) == null ? void 0 : _e.line) != null ? _f : reportedLine } : {}), hits ? occurrenceFields(hits) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
11106
11685
  }
11107
11686
  }
11108
11687
  }
@@ -11121,6 +11700,19 @@ function generateCompatibilityScore(warnings) {
11121
11700
  }
11122
11701
  return result;
11123
11702
  }
11703
+ function occurrenceFields({ locs, truncated }) {
11704
+ return __spreadValues({ loc: locs[0], locs: [...locs] }, truncated ? { locsTruncated: true } : {});
11705
+ }
11706
+ function triggeringOccurrences(prop, occurrences, notes) {
11707
+ const values = occurrences == null ? void 0 : occurrences.values;
11708
+ if (!occurrences || !values) return occurrences;
11709
+ const locs = occurrences.locs.filter((_, i) => caveatApplies(prop, [values[i]], notes));
11710
+ if (!locs.length || locs.length === occurrences.locs.length) return occurrences;
11711
+ return __spreadValues({ locs }, occurrences.truncated ? { truncated: true } : {});
11712
+ }
11713
+ function elementLocs(loc) {
11714
+ return loc ? { locs: [loc] } : void 0;
11715
+ }
11124
11716
  function warningsForClient(warnings, clientId) {
11125
11717
  return warnings.filter((w) => w.client === clientId);
11126
11718
  }
@@ -11461,16 +12053,6 @@ function extractCode(response) {
11461
12053
  return response.trim();
11462
12054
  }
11463
12055
 
11464
- // src/parse-html.ts
11465
- import * as cheerio5 from "cheerio";
11466
- function fromHtml(html, empty, fn) {
11467
- if (!html || !html.trim()) return empty;
11468
- if (html.length > MAX_HTML_SIZE) {
11469
- throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
11470
- }
11471
- return fn(cheerio5.load(html), html);
11472
- }
11473
-
11474
12056
  // src/spam-scorer.ts
11475
12057
  var SPAM_TRIGGER_PHRASES = [
11476
12058
  "act now",
@@ -11939,6 +12521,8 @@ function validateLinksFromDom($) {
11939
12521
  const href = $(el).attr("href") || "";
11940
12522
  const text = $(el).text().trim();
11941
12523
  const category = classifyHref(href);
12524
+ const elLoc = locOfElement(el);
12525
+ const hrefLoc = href ? locOfAttr(el, "href") : elLoc;
11942
12526
  switch (category) {
11943
12527
  case "https":
11944
12528
  breakdown.https++;
@@ -11969,95 +12553,95 @@ function validateLinksFromDom($) {
11969
12553
  hrefCounts.set(href, (hrefCounts.get(href) || 0) + 1);
11970
12554
  }
11971
12555
  if (!href || !href.trim()) {
11972
- issues.push({
12556
+ issues.push(__spreadValues({
11973
12557
  severity: "error",
11974
12558
  rule: "empty-href",
11975
12559
  message: "Link has no href attribute",
11976
12560
  text: text.slice(0, 80) || "(no text)"
11977
- });
12561
+ }, elLoc ? { loc: elLoc } : {}));
11978
12562
  return;
11979
12563
  }
11980
12564
  if (category === "javascript" && !isPlaceholderHref(href)) {
11981
- issues.push({
12565
+ issues.push(__spreadValues({
11982
12566
  severity: "error",
11983
12567
  rule: "javascript-href",
11984
12568
  message: "Link uses javascript: protocol",
11985
12569
  href: href.slice(0, 100),
11986
12570
  text: text.slice(0, 80) || "(no text)"
11987
- });
12571
+ }, hrefLoc ? { loc: hrefLoc } : {}));
11988
12572
  return;
11989
12573
  }
11990
12574
  if (isPlaceholderHref(href)) {
11991
- issues.push({
12575
+ issues.push(__spreadValues({
11992
12576
  severity: "warning",
11993
12577
  rule: "placeholder-href",
11994
12578
  message: "Link has a placeholder href (# or javascript:void)",
11995
12579
  href,
11996
12580
  text: text.slice(0, 80) || "(no text)"
11997
- });
12581
+ }, hrefLoc ? { loc: hrefLoc } : {}));
11998
12582
  return;
11999
12583
  }
12000
12584
  if (category === "http") {
12001
- issues.push({
12585
+ issues.push(__spreadValues({
12002
12586
  severity: "warning",
12003
12587
  rule: "insecure-link",
12004
12588
  message: "Link uses HTTP instead of HTTPS",
12005
12589
  href: href.slice(0, 120),
12006
12590
  text: text.slice(0, 80) || "(no text)"
12007
- });
12591
+ }, hrefLoc ? { loc: hrefLoc } : {}));
12008
12592
  }
12009
12593
  if (category === "protocol-relative") {
12010
- issues.push({
12594
+ issues.push(__spreadValues({
12011
12595
  severity: "warning",
12012
12596
  rule: "protocol-relative",
12013
12597
  message: "Protocol-relative URL may break in email clients \u2014 use https:// explicitly",
12014
12598
  href: href.slice(0, 120),
12015
12599
  text: text.slice(0, 80) || "(no text)"
12016
- });
12600
+ }, hrefLoc ? { loc: hrefLoc } : {}));
12017
12601
  }
12018
12602
  if (text && GENERIC_LINK_TEXT.has(text.toLowerCase())) {
12019
- issues.push({
12603
+ issues.push(__spreadValues({
12020
12604
  severity: "warning",
12021
12605
  rule: "generic-link-text",
12022
12606
  message: `Link text "${text}" is vague \u2014 use descriptive text for accessibility and engagement`,
12023
12607
  href: href.slice(0, 120),
12024
12608
  text
12025
- });
12609
+ }, elLoc ? { loc: elLoc } : {}));
12026
12610
  }
12027
12611
  if (!text && !$(el).attr("aria-label") && !$(el).find("img[alt]").length) {
12028
- issues.push({
12612
+ issues.push(__spreadValues({
12029
12613
  severity: "error",
12030
12614
  rule: "empty-link-text",
12031
12615
  message: "Link has no visible text or aria-label",
12032
12616
  href: href.slice(0, 120)
12033
- });
12617
+ }, elLoc ? { loc: elLoc } : {}));
12034
12618
  }
12035
12619
  if (category === "mailto" && href.trim().toLowerCase() === "mailto:") {
12036
- issues.push({
12620
+ issues.push(__spreadValues({
12037
12621
  severity: "error",
12038
12622
  rule: "empty-mailto",
12039
12623
  message: "mailto: link has no email address",
12040
12624
  href,
12041
12625
  text: text.slice(0, 80) || "(no text)"
12042
- });
12626
+ }, hrefLoc ? { loc: hrefLoc } : {}));
12043
12627
  }
12044
12628
  if (category === "tel" && href.trim().toLowerCase() === "tel:") {
12045
- issues.push({
12629
+ issues.push(__spreadValues({
12046
12630
  severity: "error",
12047
12631
  rule: "empty-tel",
12048
12632
  message: "tel: link has no phone number",
12049
12633
  href,
12050
12634
  text: text.slice(0, 80) || "(no text)"
12051
- });
12635
+ }, hrefLoc ? { loc: hrefLoc } : {}));
12052
12636
  }
12053
12637
  if (href.length > 2e3) {
12054
- issues.push({
12638
+ issues.push(__spreadValues({
12055
12639
  severity: "info",
12056
12640
  rule: "long-url",
12057
12641
  message: "URL exceeds 2000 characters \u2014 may be truncated by some email clients",
12058
12642
  href: href.slice(0, 120) + "...",
12059
12643
  text: text.slice(0, 80) || "(no text)"
12060
- });
12644
+ }, hrefLoc ? { loc: hrefLoc } : {}));
12061
12645
  }
12062
12646
  });
12063
12647
  links.each((_, el) => {
@@ -12066,14 +12650,15 @@ function validateLinksFromDom($) {
12066
12650
  if (trimmed.startsWith("#") && trimmed.length > 1) {
12067
12651
  const targetId = trimmed.slice(1);
12068
12652
  const target = $(`[id="${targetId}"]`);
12653
+ const anchorLoc = locOfAttr(el, "href");
12069
12654
  if (target.length === 0) {
12070
- issues.push({
12655
+ issues.push(__spreadValues({
12071
12656
  severity: "error",
12072
12657
  rule: "broken-anchor",
12073
12658
  message: `Anchor link "${trimmed}" points to an element that does not exist`,
12074
12659
  href: trimmed,
12075
12660
  text: $(el).text().trim().slice(0, 80) || "(no text)"
12076
- });
12661
+ }, anchorLoc ? { loc: anchorLoc } : {}));
12077
12662
  }
12078
12663
  }
12079
12664
  });
@@ -12089,8 +12674,8 @@ function validateLinksFromDom($) {
12089
12674
  }
12090
12675
  return { totalLinks, issues, breakdown };
12091
12676
  }
12092
- function validateLinks(html) {
12093
- return fromHtml(html, EMPTY_LINKS, validateLinksFromDom);
12677
+ function validateLinks(html, options) {
12678
+ return fromHtml(html, EMPTY_LINKS, validateLinksFromDom, options);
12094
12679
  }
12095
12680
 
12096
12681
  // src/accessibility-checker.ts
@@ -12115,24 +12700,31 @@ function describeElement($, el) {
12115
12700
  function checkLangAttribute($) {
12116
12701
  const lang = $("html").attr("lang");
12117
12702
  if (!lang || !lang.trim()) {
12118
- return {
12703
+ const loc = locOfFirst($, "html");
12704
+ return __spreadProps(__spreadValues({
12119
12705
  severity: "error",
12120
12706
  rule: "missing-lang",
12121
- message: "Missing lang attribute on <html> element",
12707
+ message: "Missing lang attribute on <html> element"
12708
+ }, loc ? { loc } : {}), {
12122
12709
  details: 'Screen readers use the lang attribute to determine pronunciation. Add lang="en" (or appropriate language code).'
12123
- };
12710
+ });
12124
12711
  }
12125
12712
  return null;
12126
12713
  }
12714
+ function titleLoc($) {
12715
+ return $("title").length ? locOfFirst($, "title") : locOfFirst($, "head");
12716
+ }
12127
12717
  function checkTitle($) {
12128
12718
  const title = $("title").text().trim();
12129
12719
  if (!title) {
12130
- return {
12720
+ const loc = titleLoc($);
12721
+ return __spreadProps(__spreadValues({
12131
12722
  severity: "warning",
12132
12723
  rule: "missing-title",
12133
- message: "Missing or empty <title> element",
12724
+ message: "Missing or empty <title> element"
12725
+ }, loc ? { loc } : {}), {
12134
12726
  details: "The <title> helps screen readers identify the email content."
12135
- };
12727
+ });
12136
12728
  }
12137
12729
  return null;
12138
12730
  }
@@ -12142,34 +12734,38 @@ function checkImageAlt($) {
12142
12734
  const alt = $(el).attr("alt");
12143
12735
  const src = $(el).attr("src") || "";
12144
12736
  const role = $(el).attr("role");
12737
+ const elLoc = locOfElement(el);
12145
12738
  if (role === "presentation" || role === "none") return;
12146
12739
  if (alt === void 0) {
12147
- issues.push({
12740
+ issues.push(__spreadProps(__spreadValues({
12148
12741
  severity: "error",
12149
12742
  rule: "img-missing-alt",
12150
12743
  message: "Image missing alt attribute",
12151
- element: describeElement($, el),
12744
+ element: describeElement($, el)
12745
+ }, elLoc ? { loc: elLoc } : {}), {
12152
12746
  details: 'Every image must have an alt attribute. Use alt="" for decorative images.'
12153
- });
12747
+ }));
12154
12748
  } else if (alt.trim() === "") {
12155
12749
  const isLikelyContent = !src.includes("spacer") && !src.includes("pixel") && !src.includes("tracking") && !src.includes("1x1") && !src.includes("transparent");
12156
12750
  if (isLikelyContent && ($(el).attr("width") || "0") !== "1") {
12157
- issues.push({
12751
+ issues.push(__spreadProps(__spreadValues({
12158
12752
  severity: "info",
12159
12753
  rule: "img-empty-alt",
12160
12754
  message: "Image has empty alt text \u2014 verify it is decorative",
12161
- element: describeElement($, el),
12755
+ element: describeElement($, el)
12756
+ }, locOfAttr(el, "alt") ? { loc: locOfAttr(el, "alt") } : {}), {
12162
12757
  details: "Empty alt is correct for decorative images, but content images need descriptive alt text."
12163
- });
12758
+ }));
12164
12759
  }
12165
12760
  } else if (/\.(png|jpg|jpeg|gif|svg|webp|bmp)$/i.test(alt)) {
12166
- issues.push({
12761
+ issues.push(__spreadProps(__spreadValues({
12167
12762
  severity: "error",
12168
12763
  rule: "img-filename-alt",
12169
12764
  message: "Image alt text is a filename, not a description",
12170
- element: describeElement($, el),
12765
+ element: describeElement($, el)
12766
+ }, locOfAttr(el, "alt") ? { loc: locOfAttr(el, "alt") } : {}), {
12171
12767
  details: `Alt "${alt}" should describe the image content, not the file name.`
12172
- });
12768
+ }));
12173
12769
  }
12174
12770
  });
12175
12771
  return issues;
@@ -12177,28 +12773,31 @@ function checkImageAlt($) {
12177
12773
  function checkLinkAccessibility($) {
12178
12774
  const issues = [];
12179
12775
  $("a").each((_, el) => {
12776
+ const elLoc = locOfElement(el);
12180
12777
  const text = $(el).text().trim().toLowerCase();
12181
12778
  const ariaLabel = $(el).attr("aria-label");
12182
12779
  const title = $(el).attr("title");
12183
12780
  const imgAlt = $(el).find("img").attr("alt");
12184
12781
  if (!text && !ariaLabel && !title && !imgAlt) {
12185
- issues.push({
12782
+ issues.push(__spreadProps(__spreadValues({
12186
12783
  severity: "error",
12187
12784
  rule: "link-no-accessible-name",
12188
12785
  message: "Link has no accessible name",
12189
- element: describeElement($, el),
12786
+ element: describeElement($, el)
12787
+ }, elLoc ? { loc: elLoc } : {}), {
12190
12788
  details: "Links need visible text, aria-label, or an image with alt text."
12191
- });
12789
+ }));
12192
12790
  return;
12193
12791
  }
12194
12792
  if (text && GENERIC_LINK_TEXT.has(text) && !ariaLabel) {
12195
- issues.push({
12793
+ issues.push(__spreadProps(__spreadValues({
12196
12794
  severity: "warning",
12197
12795
  rule: "link-generic-text",
12198
12796
  message: `Link text "${$(el).text().trim()}" is not descriptive`,
12199
- element: describeElement($, el),
12797
+ element: describeElement($, el)
12798
+ }, elLoc ? { loc: elLoc } : {}), {
12200
12799
  details: "Screen readers often list links out of context. Use text that describes the destination."
12201
- });
12800
+ }));
12202
12801
  }
12203
12802
  });
12204
12803
  return issues;
@@ -12208,18 +12807,20 @@ function checkTableAccessibility($) {
12208
12807
  $("table").each((_, el) => {
12209
12808
  if ($(el).parents('table[role="presentation"], table[role="none"]').length > 0) return;
12210
12809
  const role = $(el).attr("role");
12810
+ const tableLoc = locOfElement(el);
12211
12811
  const hasHeaders = $(el).find("th").length > 0;
12212
12812
  const looksLikeLayout = !hasHeaders;
12213
12813
  if (looksLikeLayout && role !== "presentation" && role !== "none") {
12214
12814
  const nestedTables = $(el).find("table").length;
12215
12815
  if (nestedTables > 0 || $(el).find("td").length > 2) {
12216
- issues.push({
12816
+ issues.push(__spreadProps(__spreadValues({
12217
12817
  severity: "info",
12218
12818
  rule: "table-missing-role",
12219
- message: 'Layout table missing role="presentation"',
12819
+ message: 'Layout table missing role="presentation"'
12820
+ }, tableLoc ? { loc: tableLoc } : {}), {
12220
12821
  element: `<table> with ${$(el).find("td").length} cells`,
12221
12822
  details: `Add role="presentation" to tables used for layout so screen readers don't announce them as data tables.`
12222
- });
12823
+ }));
12223
12824
  }
12224
12825
  }
12225
12826
  });
@@ -12230,6 +12831,7 @@ function checkTextSizeAndContrast($) {
12230
12831
  let smallTextCount = 0;
12231
12832
  $("[style]").each((_, el) => {
12232
12833
  const style = $(el).attr("style") || "";
12834
+ const styleLoc = locOfAttr(el, "style");
12233
12835
  const fontSizeMatch = style.match(/font-size\s*:\s*(\d+(?:\.\d+)?)(px|pt)/i);
12234
12836
  if (fontSizeMatch) {
12235
12837
  const size = parseFloat(fontSizeMatch[1]);
@@ -12238,13 +12840,14 @@ function checkTextSizeAndContrast($) {
12238
12840
  if (pxSize < 9 && pxSize > 0) {
12239
12841
  smallTextCount++;
12240
12842
  if (smallTextCount <= 3) {
12241
- issues.push({
12843
+ issues.push(__spreadProps(__spreadValues({
12242
12844
  severity: "warning",
12243
12845
  rule: "small-text",
12244
12846
  message: `Very small text (${fontSizeMatch[0].trim()})`,
12245
- element: describeElement($, el),
12847
+ element: describeElement($, el)
12848
+ }, styleLoc ? { loc: styleLoc } : {}), {
12246
12849
  details: "Text smaller than 9px is difficult to read, especially on mobile devices."
12247
- });
12850
+ }));
12248
12851
  }
12249
12852
  }
12250
12853
  }
@@ -12289,21 +12892,23 @@ function checkTextSizeAndContrast($) {
12289
12892
  }
12290
12893
  const grade = wcagGrade(ratio);
12291
12894
  if (grade === "Fail") {
12292
- issues.push({
12895
+ issues.push(__spreadProps(__spreadValues({
12293
12896
  severity: "error",
12294
12897
  rule: "low-contrast",
12295
12898
  message: `Low contrast ratio ${ratio.toFixed(1)}:1 \u2014 fails WCAG minimum`,
12296
- element: describeElement($, el),
12899
+ element: describeElement($, el)
12900
+ }, styleLoc ? { loc: styleLoc } : {}), {
12297
12901
  details: `Foreground ${colorValue} on background needs at least ${isLargeText ? "3:1" : "4.5:1"} contrast ratio.`
12298
- });
12902
+ }));
12299
12903
  } else if (!isLargeText && grade === "AA Large") {
12300
- issues.push({
12904
+ issues.push(__spreadProps(__spreadValues({
12301
12905
  severity: "warning",
12302
12906
  rule: "low-contrast",
12303
12907
  message: `Low contrast ratio ${ratio.toFixed(1)}:1 \u2014 fails WCAG AA for normal text`,
12304
- element: describeElement($, el),
12908
+ element: describeElement($, el)
12909
+ }, styleLoc ? { loc: styleLoc } : {}), {
12305
12910
  details: `Foreground ${colorValue} on background needs at least 4.5:1 for normal-sized text.`
12306
- });
12911
+ }));
12307
12912
  }
12308
12913
  }
12309
12914
  }
@@ -12326,29 +12931,32 @@ function checkCharsetDeclaration($) {
12326
12931
  const content = httpEquiv.attr("content") || "";
12327
12932
  if (/charset\s*=/i.test(content)) return null;
12328
12933
  }
12329
- return {
12934
+ const loc = locOfFirst($, "head");
12935
+ return __spreadProps(__spreadValues({
12330
12936
  severity: "warning",
12331
12937
  rule: "missing-charset",
12332
- message: "Missing charset declaration",
12938
+ message: "Missing charset declaration"
12939
+ }, loc ? { loc } : {}), {
12333
12940
  details: 'Add <meta charset="utf-8"> in <head> to prevent encoding issues across email clients.'
12334
- };
12941
+ });
12335
12942
  }
12336
12943
  function checkSemanticStructure($) {
12337
12944
  const issues = [];
12338
12945
  const headings = [];
12339
12946
  $("h1, h2, h3, h4, h5, h6").each((_, el) => {
12340
12947
  const level = parseInt(el.tagName.replace(/h/i, ""), 10);
12341
- headings.push({ level, text: $(el).text().trim().slice(0, 60) });
12948
+ headings.push({ level, text: $(el).text().trim().slice(0, 60), loc: locOfElement(el) });
12342
12949
  });
12343
12950
  for (let i = 1; i < headings.length; i++) {
12344
12951
  const gap = headings[i].level - headings[i - 1].level;
12345
12952
  if (gap > 1) {
12346
- issues.push({
12953
+ issues.push(__spreadProps(__spreadValues({
12347
12954
  severity: "info",
12348
12955
  rule: "heading-skip",
12349
- message: `Heading level skipped: h${headings[i - 1].level} to h${headings[i].level}`,
12956
+ message: `Heading level skipped: h${headings[i - 1].level} to h${headings[i].level}`
12957
+ }, headings[i].loc ? { loc: headings[i].loc } : {}), {
12350
12958
  details: "Skipped heading levels can confuse screen readers. Use sequential heading levels."
12351
- });
12959
+ }));
12352
12960
  break;
12353
12961
  }
12354
12962
  }
@@ -12389,8 +12997,8 @@ function checkAccessibilityFromDom($) {
12389
12997
  const score = Math.max(0, 100 - penalty);
12390
12998
  return { score, issues };
12391
12999
  }
12392
- function checkAccessibility(html) {
12393
- return fromHtml(html, EMPTY_ACCESSIBILITY, checkAccessibilityFromDom);
13000
+ function checkAccessibility(html, options) {
13001
+ return fromHtml(html, EMPTY_ACCESSIBILITY, checkAccessibilityFromDom, options);
12394
13002
  }
12395
13003
 
12396
13004
  // src/image-analyzer.ts
@@ -12437,6 +13045,8 @@ function analyzeImagesFromDom($) {
12437
13045
  const height = (_c = img.attr("height")) != null ? _c : null;
12438
13046
  const style = (img.attr("style") || "").toLowerCase();
12439
13047
  const imgIssues = [];
13048
+ const elLoc = locOfElement(el);
13049
+ const srcLoc = src ? locOfAttr(el, "src") : elLoc;
12440
13050
  const tracking = isTrackingPixel(img);
12441
13051
  let dataUriBytes = 0;
12442
13052
  if (src.startsWith("data:")) {
@@ -12460,59 +13070,59 @@ function analyzeImagesFromDom($) {
12460
13070
  const hasStyleHeight = /height\s*:/.test(style);
12461
13071
  if (!hasStyleWidth && !hasStyleHeight) {
12462
13072
  imgIssues.push("missing-dimensions");
12463
- issues.push({
13073
+ issues.push(__spreadValues({
12464
13074
  rule: "missing-dimensions",
12465
13075
  severity: "warning",
12466
13076
  message: "Image missing width/height attributes \u2014 causes layout shifts and Outlook rendering issues.",
12467
13077
  src: truncateSrc(src)
12468
- });
13078
+ }, elLoc ? { loc: elLoc } : {}));
12469
13079
  }
12470
13080
  }
12471
13081
  if (dataUriBytes > DATA_URI_WARN_BYTES) {
12472
13082
  const kb = Math.round(dataUriBytes / 1024);
12473
13083
  imgIssues.push("large-data-uri");
12474
- issues.push({
13084
+ issues.push(__spreadValues({
12475
13085
  rule: "large-data-uri",
12476
13086
  severity: "warning",
12477
13087
  message: `Data URI is ${kb}KB \u2014 consider hosting the image externally to reduce email size.`,
12478
13088
  src: truncateSrc(src)
12479
- });
13089
+ }, srcLoc ? { loc: srcLoc } : {}));
12480
13090
  }
12481
13091
  if (alt === null) {
12482
13092
  imgIssues.push("missing-alt");
12483
- issues.push({
13093
+ issues.push(__spreadValues({
12484
13094
  rule: "missing-alt",
12485
13095
  severity: "warning",
12486
13096
  message: "Image missing alt attribute \u2014 hurts deliverability and accessibility.",
12487
13097
  src: truncateSrc(src)
12488
- });
13098
+ }, elLoc ? { loc: elLoc } : {}));
12489
13099
  }
12490
13100
  if (src.toLowerCase().endsWith(".webp") || src.includes("image/webp")) {
12491
13101
  imgIssues.push("webp-format");
12492
- issues.push({
13102
+ issues.push(__spreadValues({
12493
13103
  rule: "webp-format",
12494
13104
  severity: "info",
12495
13105
  message: "WebP format detected \u2014 not supported by all email clients. Consider PNG or JPEG.",
12496
13106
  src: truncateSrc(src)
12497
- });
13107
+ }, srcLoc ? { loc: srcLoc } : {}));
12498
13108
  }
12499
13109
  if (src.toLowerCase().endsWith(".svg") || src.includes("image/svg")) {
12500
13110
  imgIssues.push("svg-format");
12501
- issues.push({
13111
+ issues.push(__spreadValues({
12502
13112
  rule: "svg-format",
12503
13113
  severity: "info",
12504
13114
  message: "SVG format detected \u2014 not supported by most email clients. Use PNG instead.",
12505
13115
  src: truncateSrc(src)
12506
- });
13116
+ }, srcLoc ? { loc: srcLoc } : {}));
12507
13117
  }
12508
13118
  if (!style.includes("display:block") && !style.includes("display: block")) {
12509
13119
  imgIssues.push("missing-display-block");
12510
- issues.push({
13120
+ issues.push(__spreadValues({
12511
13121
  rule: "missing-display-block",
12512
13122
  severity: "info",
12513
13123
  message: "Image without display:block \u2014 may cause unwanted gaps in Outlook.",
12514
13124
  src: truncateSrc(src)
12515
- });
13125
+ }, elLoc ? { loc: elLoc } : {}));
12516
13126
  }
12517
13127
  images.push({
12518
13128
  src: truncateSrc(src),
@@ -12550,8 +13160,8 @@ function analyzeImagesFromDom($) {
12550
13160
  }
12551
13161
  return { total: images.length, totalDataUriBytes, issues, images };
12552
13162
  }
12553
- function analyzeImages(html) {
12554
- return fromHtml(html, EMPTY_IMAGES, analyzeImagesFromDom);
13163
+ function analyzeImages(html, options) {
13164
+ return fromHtml(html, EMPTY_IMAGES, analyzeImagesFromDom, options);
12555
13165
  }
12556
13166
 
12557
13167
  // src/inbox-preview.ts
@@ -12769,11 +13379,57 @@ function checkSize(html) {
12769
13379
  return fromHtml(html, EMPTY_SIZE, checkSizeFromDom);
12770
13380
  }
12771
13381
 
13382
+ // src/dom-text.ts
13383
+ function visibleTextNodes($) {
13384
+ var _a, _b, _c, _d;
13385
+ const nodes = [];
13386
+ const stack = [...(_b = (_a = $.root()[0]) == null ? void 0 : _a.children) != null ? _b : []].reverse();
13387
+ while (stack.length > 0) {
13388
+ const node = stack.pop();
13389
+ const tag = (_c = node.tagName) == null ? void 0 : _c.toLowerCase();
13390
+ if (tag === "style" || tag === "script" || tag === "head") continue;
13391
+ if (node.type === "text") {
13392
+ nodes.push(node);
13393
+ continue;
13394
+ }
13395
+ const children = (_d = node.children) != null ? _d : [];
13396
+ for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]);
13397
+ }
13398
+ return nodes;
13399
+ }
13400
+
12772
13401
  // src/template-checker.ts
12773
- function checkTemplateVariablesFromDom($) {
13402
+ function checkTemplateVariablesFromDom($, source) {
13403
+ var _a;
12774
13404
  const issues = [];
12775
13405
  const seen = /* @__PURE__ */ new Set();
12776
- const textContent = extractTextContent($);
13406
+ const textNodes = visibleTextNodes($);
13407
+ const positioned = textNodes.some((n) => n.sourceCodeLocation);
13408
+ for (const node of positioned ? textNodes : []) {
13409
+ const data = (_a = node.data) != null ? _a : "";
13410
+ for (const [pattern, label] of TEMPLATE_VARIABLE_PATTERNS) {
13411
+ pattern.lastIndex = 0;
13412
+ let match;
13413
+ while ((match = pattern.exec(data)) !== null) {
13414
+ const variable = match[0];
13415
+ const key = `text:${variable}`;
13416
+ if (seen.has(key)) continue;
13417
+ seen.add(key);
13418
+ const loc = locInTextNode(node, match.index, variable.length, source);
13419
+ issues.push(__spreadValues({
13420
+ rule: "unresolved-variable",
13421
+ severity: "error",
13422
+ message: `Unresolved ${label} variable "${variable}" found in text content.`,
13423
+ variable,
13424
+ location: "text"
13425
+ }, loc ? { loc } : {}));
13426
+ }
13427
+ }
13428
+ }
13429
+ const textContent = textNodes.map((n) => {
13430
+ var _a2;
13431
+ return (_a2 = n.data) != null ? _a2 : "";
13432
+ }).join("");
12777
13433
  for (const [pattern, label] of TEMPLATE_VARIABLE_PATTERNS) {
12778
13434
  pattern.lastIndex = 0;
12779
13435
  let match;
@@ -12806,13 +13462,14 @@ function checkTemplateVariablesFromDom($) {
12806
13462
  const key = `attr:${attr}:${variable}`;
12807
13463
  if (seen.has(key)) continue;
12808
13464
  seen.add(key);
12809
- issues.push({
13465
+ const loc = locOfAttr(el, attr);
13466
+ issues.push(__spreadValues({
12810
13467
  rule: "unresolved-variable",
12811
13468
  severity: "error",
12812
13469
  message: `Unresolved ${label} variable "${variable}" found in ${attr} attribute.`,
12813
13470
  variable,
12814
13471
  location: "attribute"
12815
- });
13472
+ }, loc ? { loc } : {}));
12816
13473
  }
12817
13474
  }
12818
13475
  }
@@ -12820,13 +13477,13 @@ function checkTemplateVariablesFromDom($) {
12820
13477
  }
12821
13478
  return { unresolvedCount: issues.length, issues };
12822
13479
  }
12823
- function extractTextContent($) {
12824
- const clone = $.root().clone();
12825
- clone.find("style, script, head").remove();
12826
- return clone.text();
12827
- }
12828
- function checkTemplateVariables(html) {
12829
- return fromHtml(html, EMPTY_TEMPLATE, checkTemplateVariablesFromDom);
13480
+ function checkTemplateVariables(html, options) {
13481
+ return fromHtml(
13482
+ html,
13483
+ EMPTY_TEMPLATE,
13484
+ ($, h) => checkTemplateVariablesFromDom($, (options == null ? void 0 : options.positions) ? h : void 0),
13485
+ options
13486
+ );
12830
13487
  }
12831
13488
 
12832
13489
  // src/overflow-checker.ts
@@ -12842,32 +13499,71 @@ function fixedPxWidth($el) {
12842
13499
  function isFluid(style) {
12843
13500
  return /max-width\s*:\s*100%/i.test(style) || /width\s*:\s*100%/i.test(style);
12844
13501
  }
12845
- function addWidthIssue(width, label, issues, seen) {
13502
+ function addWidthIssue(width, label, issues, seen, loc) {
12846
13503
  const key = `w:${label}:${width}`;
12847
- if (seen.has(key)) return;
12848
- seen.add(key);
12849
- issues.push({
13504
+ const existing = seen.get(key);
13505
+ if (existing) {
13506
+ addOccurrence(existing, loc);
13507
+ return;
13508
+ }
13509
+ const issue = __spreadValues({
12850
13510
  rule: "fixed-width-overflow",
12851
13511
  severity: "warning",
12852
13512
  message: `${label} has a fixed width of ${width}px, wider than the ${EMAIL_MAX_WIDTH}px email frame \u2014 it will force horizontal scrolling, especially on mobile.`,
12853
13513
  detail: `Use width:100% with max-width:${EMAIL_MAX_WIDTH}px instead of a fixed width beyond the frame.`
12854
- });
13514
+ }, loc ? { loc, locs: [loc] } : {});
13515
+ seen.set(key, issue);
13516
+ issues.push(issue);
13517
+ }
13518
+ function locateInNodes(nodes, starts, index, length, source) {
13519
+ var _a;
13520
+ let lo = 0;
13521
+ let hi = starts.length - 1;
13522
+ let found = -1;
13523
+ while (lo <= hi) {
13524
+ const mid = lo + hi >> 1;
13525
+ if (starts[mid] <= index) {
13526
+ found = mid;
13527
+ lo = mid + 1;
13528
+ } else {
13529
+ hi = mid - 1;
13530
+ }
13531
+ }
13532
+ if (found === -1) return void 0;
13533
+ const node = nodes[found];
13534
+ const within = index - starts[found];
13535
+ const available = ((_a = node.data) != null ? _a : "").length - within;
13536
+ return locInTextNode(node, within, Math.min(length, available), source);
12855
13537
  }
12856
- function checkOverflowFromDom($) {
13538
+ function addOccurrence(issue, loc) {
13539
+ if (!loc || !issue.locs) return;
13540
+ if (issue.locs.some((l) => l.offset === loc.offset)) return;
13541
+ if (issue.locs.length >= MAX_WARNING_LOCATIONS) {
13542
+ issue.locsTruncated = true;
13543
+ return;
13544
+ }
13545
+ issue.locs.push(loc);
13546
+ }
13547
+ function checkOverflowFromDom($, source) {
13548
+ var _a;
12857
13549
  const issues = [];
12858
- const seen = /* @__PURE__ */ new Set();
13550
+ const seen = /* @__PURE__ */ new Map();
13551
+ const tokensSeen = /* @__PURE__ */ new Set();
12859
13552
  $("[width], [style*='width']").each((_, el) => {
12860
13553
  const $el = $(el);
12861
13554
  const width = fixedPxWidth($el);
12862
13555
  if (width === null || width <= EMAIL_MAX_WIDTH) return;
12863
13556
  if (isFluid($el.attr("style") || "")) return;
12864
13557
  const tag = (el.tagName || "element").toLowerCase();
12865
- addWidthIssue(width, `<${tag}>`, issues, seen);
13558
+ const fromStyle = /(?:^|[;\s])width\s*:\s*\d+px/i.test($el.attr("style") || "");
13559
+ addWidthIssue(width, `<${tag}>`, issues, seen, locOfAttr(el, fromStyle ? "style" : "width"));
12866
13560
  });
12867
13561
  $("style").each((_, el) => {
13562
+ const cssText = $(el).text();
13563
+ const anchor = cssBlockAnchor(el, cssText, source);
12868
13564
  let ast;
12869
13565
  try {
12870
- ast = csstree6.parse($(el).text());
13566
+ ast = csstree6.parse(cssText, { positions: true });
12871
13567
  } catch (e) {
12872
13568
  return;
12873
13569
  }
@@ -12877,13 +13573,17 @@ function checkOverflowFromDom($) {
12877
13573
  if (node.type !== "Rule") return;
12878
13574
  let widthPx = null;
12879
13575
  let fluid = false;
13576
+ let widthLoc;
12880
13577
  node.block.children.forEach((child) => {
12881
13578
  if (child.type !== "Declaration") return;
12882
13579
  const prop = child.property.toLowerCase();
12883
13580
  const val = csstree6.generate(child.value);
12884
13581
  if (prop === "width") {
12885
13582
  const m = val.match(/^(\d+)px$/);
12886
- if (m) widthPx = parseInt(m[1], 10);
13583
+ if (m) {
13584
+ widthPx = parseInt(m[1], 10);
13585
+ widthLoc = locInCssBlock(anchor, child.loc);
13586
+ }
12887
13587
  if (/\b100%/.test(val)) fluid = true;
12888
13588
  } else if (prop === "max-width" && /\b100%/.test(val)) {
12889
13589
  fluid = true;
@@ -12891,36 +13591,46 @@ function checkOverflowFromDom($) {
12891
13591
  });
12892
13592
  if (widthPx !== null && widthPx > EMAIL_MAX_WIDTH && !fluid) {
12893
13593
  const selector = csstree6.generate(node.prelude).trim().slice(0, 40);
12894
- addWidthIssue(widthPx, selector || "rule", issues, seen);
13594
+ addWidthIssue(widthPx, selector || "rule", issues, seen, widthLoc);
12895
13595
  }
12896
13596
  }
12897
13597
  });
12898
13598
  });
12899
13599
  const usesWrapGuard = /overflow-wrap|word-break|word-wrap/i.test($.html());
12900
13600
  if (!usesWrapGuard) {
12901
- const $body = $("body");
13601
+ const nodes = visibleTextNodes($);
13602
+ const starts = [];
12902
13603
  let text = "";
12903
- if ($body.length) {
12904
- const clone = $body.clone();
12905
- clone.find("style, script").remove();
12906
- text = clone.text();
12907
- }
12908
- for (const token of text.split(/\s+/)) {
12909
- if (token.length <= UNBREAKABLE_STRING_LENGTH || seen.has(token)) continue;
12910
- seen.add(token);
13604
+ for (const node of nodes) {
13605
+ starts.push(text.length);
13606
+ text += (_a = node.data) != null ? _a : "";
13607
+ }
13608
+ let at = 0;
13609
+ for (const token of text.split(/(\s+)/)) {
13610
+ const start = at;
13611
+ at += token.length;
13612
+ if (/^\s*$/.test(token)) continue;
13613
+ if (token.length <= UNBREAKABLE_STRING_LENGTH || tokensSeen.has(token)) continue;
13614
+ tokensSeen.add(token);
12911
13615
  const preview = token.length > 50 ? `${token.slice(0, 50)}\u2026` : token;
12912
- issues.push({
13616
+ const loc = locateInNodes(nodes, starts, start, token.length, source);
13617
+ issues.push(__spreadValues({
12913
13618
  rule: "unbreakable-string",
12914
13619
  severity: "warning",
12915
13620
  message: `A ${token.length}-character unbroken string ("${preview}") can't wrap and will force horizontal scrolling on narrow screens.`,
12916
13621
  detail: `Add overflow-wrap: anywhere (or word-break: break-word) to its container.`
12917
- });
13622
+ }, loc ? { loc, locs: [loc] } : {}));
12918
13623
  }
12919
13624
  }
12920
13625
  return { hasOverflow: issues.length > 0, issues };
12921
13626
  }
12922
- function checkOverflow(html) {
12923
- return fromHtml(html, EMPTY_OVERFLOW, checkOverflowFromDom);
13627
+ function checkOverflow(html, options) {
13628
+ return fromHtml(
13629
+ html,
13630
+ EMPTY_OVERFLOW,
13631
+ ($, h) => checkOverflowFromDom($, (options == null ? void 0 : options.positions) ? h : void 0),
13632
+ options
13633
+ );
12924
13634
  }
12925
13635
 
12926
13636
  // src/visual-checker.ts
@@ -12933,8 +13643,8 @@ function isSolidColor(value) {
12933
13643
  return c !== null && c.a !== 0;
12934
13644
  }
12935
13645
  function firstColor(value) {
12936
- const tokens = value.match(/#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\)|\b[a-zA-Z]{3,}\b/g) || [];
12937
- for (const t of tokens) {
13646
+ const tokens2 = value.match(/#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\)|\b[a-zA-Z]{3,}\b/g) || [];
13647
+ for (const t of tokens2) {
12938
13648
  const lc = t.toLowerCase();
12939
13649
  if (lc === "transparent") continue;
12940
13650
  if (/^(?:linear|radial|conic|gradient|deg|turn|rad|grad|to|at|from|in|circle|ellipse|closest|farthest|side|corner|url)$/.test(lc)) continue;
@@ -12971,8 +13681,17 @@ function hasFontFallback(value) {
12971
13681
  return WEB_SAFE_FONTS.has(t) || GENERIC_FONT_FAMILIES.has(t) || t.startsWith("-apple-system") || t === "blinkmacsystemfont";
12972
13682
  });
12973
13683
  }
12974
- function inspectDeclarations(style, issues, seen) {
12975
- var _a, _b;
13684
+ function addOccurrence2(issue, loc) {
13685
+ if (!loc || !issue.locs) return;
13686
+ if (issue.locs.some((l) => l.offset === loc.offset)) return;
13687
+ if (issue.locs.length >= MAX_WARNING_LOCATIONS) {
13688
+ issue.locsTruncated = true;
13689
+ return;
13690
+ }
13691
+ issue.locs.push(loc);
13692
+ }
13693
+ function inspectDeclarations(style, issues, seen, locs) {
13694
+ var _a, _b, _c;
12976
13695
  const combined = `${(_a = style.get("background-image")) != null ? _a : ""} ${(_b = style.get("background")) != null ? _b : ""}`;
12977
13696
  const isGradient = GRADIENT_RE.test(combined);
12978
13697
  const isImage = isGradient || /url\(/i.test(combined);
@@ -12980,30 +13699,40 @@ function inspectDeclarations(style, issues, seen) {
12980
13699
  const stop = isGradient ? firstColor(combined) : null;
12981
13700
  const fix = stop ? `background-color: ${stop};` : `background-color: <solid colour matching the image>;`;
12982
13701
  const key = `bg:${fix}`;
12983
- if (!seen.has(key)) {
12984
- seen.add(key);
12985
- issues.push({
13702
+ const loc = (_c = locs == null ? void 0 : locs.get("background-image")) != null ? _c : locs == null ? void 0 : locs.get("background");
13703
+ const existing = seen.get(key);
13704
+ if (existing) {
13705
+ addOccurrence2(existing, loc);
13706
+ } else {
13707
+ const issue = __spreadValues({
12986
13708
  rule: "missing-background-fallback",
12987
13709
  severity: "warning",
12988
13710
  message: `${isGradient ? "Gradient" : "Background image"} has no background-color fallback \u2014 it renders as a blank area (and can hide overlaid text) in Outlook and other clients that drop image backgrounds.`,
12989
13711
  detail: isGradient ? `Add a solid fallback beneath the gradient using its first colour stop.` : `Add a solid background-color so the area still has colour when the image is dropped.`,
12990
13712
  fix
12991
- });
13713
+ }, loc ? { loc, locs: [loc] } : {});
13714
+ seen.set(key, issue);
13715
+ issues.push(issue);
12992
13716
  }
12993
13717
  }
12994
13718
  const font = style.get("font-family");
12995
13719
  if (font && !CSS_WIDE_KEYWORDS.has(font.trim().toLowerCase()) && !hasFontFallback(font)) {
12996
13720
  const fix = `font-family: ${font.trim()}, Arial, sans-serif;`;
12997
13721
  const key = `font:${font.trim().toLowerCase()}`;
12998
- if (!seen.has(key)) {
12999
- seen.add(key);
13000
- issues.push({
13722
+ const loc = locs == null ? void 0 : locs.get("font-family");
13723
+ const existing = seen.get(key);
13724
+ if (existing) {
13725
+ addOccurrence2(existing, loc);
13726
+ } else {
13727
+ const issue = __spreadValues({
13001
13728
  rule: "missing-font-fallback",
13002
13729
  severity: "warning",
13003
13730
  message: `font-family "${font.trim()}" has no web-safe fallback \u2014 clients that strip web fonts (Gmail, Outlook) fall back to Times New Roman.`,
13004
13731
  detail: `End the stack with a web-safe font and a generic family.`,
13005
13732
  fix
13006
- });
13733
+ }, loc ? { loc, locs: [loc] } : {});
13734
+ seen.set(key, issue);
13735
+ issues.push(issue);
13007
13736
  }
13008
13737
  }
13009
13738
  }
@@ -13016,30 +13745,47 @@ function ruleToMap(node) {
13016
13745
  });
13017
13746
  return map;
13018
13747
  }
13019
- function checkVisualFromDom($) {
13748
+ function checkVisualFromDom($, source) {
13020
13749
  const issues = [];
13021
- const seen = /* @__PURE__ */ new Set();
13750
+ const seen = /* @__PURE__ */ new Map();
13022
13751
  $("[style]").each((_, el) => {
13023
- inspectDeclarations(parseInlineStyle($(el).attr("style") || ""), issues, seen);
13752
+ const attrLoc = locOfAttr(el, "style");
13753
+ const style = parseInlineStyle($(el).attr("style") || "");
13754
+ const locs = attrLoc ? new Map([...style.keys()].map((prop) => [prop, attrLoc])) : void 0;
13755
+ inspectDeclarations(style, issues, seen, locs);
13024
13756
  });
13025
13757
  $("style").each((_, el) => {
13758
+ const cssText = $(el).text();
13759
+ const anchor = cssBlockAnchor(el, cssText, source);
13026
13760
  let ast;
13027
13761
  try {
13028
- ast = csstree7.parse($(el).text());
13762
+ ast = csstree7.parse(cssText, { positions: true });
13029
13763
  } catch (e) {
13030
13764
  return;
13031
13765
  }
13032
13766
  csstree7.walk(ast, {
13033
13767
  visit: "Rule",
13034
13768
  enter(node) {
13035
- if (node.type === "Rule") inspectDeclarations(ruleToMap(node), issues, seen);
13769
+ if (node.type !== "Rule") return;
13770
+ const locs = /* @__PURE__ */ new Map();
13771
+ node.block.children.forEach((child) => {
13772
+ if (child.type !== "Declaration") return;
13773
+ const loc = locInCssBlock(anchor, child.loc);
13774
+ if (loc) locs.set(child.property.toLowerCase(), loc);
13775
+ });
13776
+ inspectDeclarations(ruleToMap(node), issues, seen, locs);
13036
13777
  }
13037
13778
  });
13038
13779
  });
13039
13780
  return { issues };
13040
13781
  }
13041
- function checkVisual(html) {
13042
- return fromHtml(html, EMPTY_VISUAL, checkVisualFromDom);
13782
+ function checkVisual(html, options) {
13783
+ return fromHtml(
13784
+ html,
13785
+ EMPTY_VISUAL,
13786
+ ($, h) => checkVisualFromDom($, (options == null ? void 0 : options.positions) ? h : void 0),
13787
+ options
13788
+ );
13043
13789
  }
13044
13790
 
13045
13791
  // src/audit.ts
@@ -13058,7 +13804,8 @@ var EMPTY_AUDIT = {
13058
13804
  function runAudit($, html, framework, options) {
13059
13805
  var _a;
13060
13806
  const skip = new Set((_a = options == null ? void 0 : options.skip) != null ? _a : []);
13061
- const warnings = skip.has("compatibility") ? [] : analyzeEmailFromDom($, framework);
13807
+ const source = (options == null ? void 0 : options.positions) ? html : void 0;
13808
+ const warnings = skip.has("compatibility") ? [] : analyzeEmailFromDom($, framework, source);
13062
13809
  const scores = skip.has("compatibility") ? {} : generateCompatibilityScore(warnings);
13063
13810
  const spam = skip.has("spam") ? EMPTY_SPAM : analyzeSpamFromDom($, options == null ? void 0 : options.spam);
13064
13811
  const links = skip.has("links") ? EMPTY_LINKS : validateLinksFromDom($);
@@ -13066,19 +13813,19 @@ function runAudit($, html, framework, options) {
13066
13813
  const images = skip.has("images") ? EMPTY_IMAGES : analyzeImagesFromDom($);
13067
13814
  const inboxPreview = skip.has("inboxPreview") ? EMPTY_INBOX_PREVIEW : extractInboxPreviewFromDom($);
13068
13815
  const size = skip.has("size") ? EMPTY_SIZE : checkSizeFromDom($, html);
13069
- const templateVariables = skip.has("templateVariables") ? EMPTY_TEMPLATE : checkTemplateVariablesFromDom($);
13070
- const overflow = skip.has("overflow") ? EMPTY_OVERFLOW : checkOverflowFromDom($);
13071
- const visual = skip.has("visual") ? EMPTY_VISUAL : checkVisualFromDom($);
13816
+ const templateVariables = skip.has("templateVariables") ? EMPTY_TEMPLATE : checkTemplateVariablesFromDom($, source);
13817
+ const overflow = skip.has("overflow") ? EMPTY_OVERFLOW : checkOverflowFromDom($, source);
13818
+ const visual = skip.has("visual") ? EMPTY_VISUAL : checkVisualFromDom($, source);
13072
13819
  return { compatibility: { warnings, scores }, spam, links, accessibility, images, inboxPreview, size, templateVariables, overflow, visual };
13073
13820
  }
13074
13821
  function auditEmail(html, options) {
13075
- return fromHtml(html, EMPTY_AUDIT, ($, h) => runAudit($, h, options == null ? void 0 : options.framework, options));
13822
+ return fromHtml(html, EMPTY_AUDIT, ($, h) => runAudit($, h, options == null ? void 0 : options.framework, options), options);
13076
13823
  }
13077
13824
 
13078
13825
  // src/plain-text.ts
13079
- import * as cheerio6 from "cheerio";
13826
+ import * as cheerio5 from "cheerio";
13080
13827
  function toPlainText(html) {
13081
- const $ = cheerio6.load(html);
13828
+ const $ = cheerio5.load(html);
13082
13829
  $("style, script, head").remove();
13083
13830
  $("[data-skip-in-text='true']").remove();
13084
13831
  const lines = [];
@@ -13180,7 +13927,6 @@ function toPlainText(html) {
13180
13927
  }
13181
13928
 
13182
13929
  // src/session.ts
13183
- import * as cheerio7 from "cheerio";
13184
13930
  function createSession(html, options) {
13185
13931
  if (!html || !html.trim()) {
13186
13932
  const fw = options == null ? void 0 : options.framework;
@@ -13207,16 +13953,17 @@ function createSession(html, options) {
13207
13953
  if (html.length > MAX_HTML_SIZE) {
13208
13954
  throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
13209
13955
  }
13210
- const $ = cheerio7.load(html);
13956
+ const $ = loadHtml(html, options);
13211
13957
  const framework = options == null ? void 0 : options.framework;
13958
+ const source = (options == null ? void 0 : options.positions) ? html : void 0;
13212
13959
  return {
13213
13960
  html,
13214
13961
  framework,
13215
13962
  audit(opts) {
13216
- return runAudit($, html, framework, opts);
13963
+ return runAudit($, html, framework, __spreadProps(__spreadValues({}, opts), { positions: options == null ? void 0 : options.positions }));
13217
13964
  },
13218
13965
  analyze() {
13219
- return analyzeEmailFromDom($, framework);
13966
+ return analyzeEmailFromDom($, framework, source);
13220
13967
  },
13221
13968
  score(warnings) {
13222
13969
  return generateCompatibilityScore(warnings);
@@ -13240,13 +13987,13 @@ function createSession(html, options) {
13240
13987
  return checkSizeFromDom($, html);
13241
13988
  },
13242
13989
  checkTemplateVariables() {
13243
- return checkTemplateVariablesFromDom($);
13990
+ return checkTemplateVariablesFromDom($, source);
13244
13991
  },
13245
13992
  checkOverflow() {
13246
- return checkOverflowFromDom($);
13993
+ return checkOverflowFromDom($, source);
13247
13994
  },
13248
13995
  checkVisual() {
13249
- return checkVisualFromDom($);
13996
+ return checkVisualFromDom($, source);
13250
13997
  },
13251
13998
  // Transforms create isolated copies since they mutate the DOM
13252
13999
  transformForClient(clientId) {
@@ -13266,18 +14013,22 @@ export {
13266
14013
  COMPOUND_VALUE_FEATURES,
13267
14014
  CSS_FUNCTION_FEATURES,
13268
14015
  CSS_SUPPORT,
14016
+ CSS_SUPPORT_NOTES,
13269
14017
  CompileError,
13270
14018
  EMAIL_CLIENTS,
13271
14019
  EMPTY_DELIVERABILITY,
13272
14020
  GENERIC_LINK_TEXT,
13273
14021
  HTML_ELEMENT_FEATURES,
13274
14022
  MAX_HTML_SIZE,
14023
+ MAX_WARNING_LOCATIONS,
13275
14024
  STRUCTURAL_FIX_PROPERTIES,
14025
+ VALUE_CAVEAT_PROPS,
13276
14026
  alphaBlend,
13277
14027
  analyzeEmail,
13278
14028
  analyzeImages,
13279
14029
  analyzeSpam,
13280
14030
  auditEmail,
14031
+ caveatApplies,
13281
14032
  checkAccessibility,
13282
14033
  checkOverflow,
13283
14034
  checkSize,