@emailens/engine 0.10.0 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -64,18 +64,22 @@ __export(index_exports, {
64
64
  COMPOUND_VALUE_FEATURES: () => COMPOUND_VALUE_FEATURES,
65
65
  CSS_FUNCTION_FEATURES: () => CSS_FUNCTION_FEATURES,
66
66
  CSS_SUPPORT: () => CSS_SUPPORT,
67
+ CSS_SUPPORT_NOTES: () => CSS_SUPPORT_NOTES,
67
68
  CompileError: () => CompileError,
68
69
  EMAIL_CLIENTS: () => EMAIL_CLIENTS,
69
70
  EMPTY_DELIVERABILITY: () => EMPTY_DELIVERABILITY,
70
71
  GENERIC_LINK_TEXT: () => GENERIC_LINK_TEXT,
71
72
  HTML_ELEMENT_FEATURES: () => HTML_ELEMENT_FEATURES,
72
73
  MAX_HTML_SIZE: () => MAX_HTML_SIZE,
74
+ MAX_WARNING_LOCATIONS: () => MAX_WARNING_LOCATIONS,
73
75
  STRUCTURAL_FIX_PROPERTIES: () => STRUCTURAL_FIX_PROPERTIES,
76
+ VALUE_CAVEAT_PROPS: () => VALUE_CAVEAT_PROPS,
74
77
  alphaBlend: () => alphaBlend,
75
78
  analyzeEmail: () => analyzeEmail,
76
79
  analyzeImages: () => analyzeImages,
77
80
  analyzeSpam: () => analyzeSpam,
78
81
  auditEmail: () => auditEmail,
82
+ caveatApplies: () => caveatApplies,
79
83
  checkAccessibility: () => checkAccessibility,
80
84
  checkOverflow: () => checkOverflow,
81
85
  checkSize: () => checkSize,
@@ -158,7 +162,11 @@ var EMAIL_CLIENTS = [
158
162
  engine: "Microsoft Word",
159
163
  darkModeSupport: true,
160
164
  icon: "monitor",
161
- deprecated: "2026-10"
165
+ // End of support ~Q2 2029 (Microsoft: "supported until at least 2029").
166
+ // The April 2026 date some sources cite is the opt-out phase, when classic
167
+ // stops being the Windows default, not end of support. October 2026 was
168
+ // wrong: the nearest real Oct date is Oct 2025, for legacy Outlook for Mac.
169
+ deprecated: "2029-06"
162
170
  },
163
171
  {
164
172
  id: "outlook-ios",
@@ -8991,6 +8999,16 @@ function getStyleValue(style, property) {
8991
8999
  }
8992
9000
  return null;
8993
9001
  }
9002
+ function getStyleValues(style, property) {
9003
+ const values = [];
9004
+ for (const part of splitStyleDeclarations(style)) {
9005
+ const colonIndex = part.indexOf(":");
9006
+ if (colonIndex === -1) continue;
9007
+ if (part.slice(0, colonIndex).trim().toLowerCase() !== property) continue;
9008
+ values.push(part.slice(colonIndex + 1).trim());
9009
+ }
9010
+ return values;
9011
+ }
8994
9012
  function parseInlineStyle(style) {
8995
9013
  const map = /* @__PURE__ */ new Map();
8996
9014
  const declarations = splitStyleDeclarations(style);
@@ -9741,6 +9759,7 @@ function downlevelCSS(html) {
9741
9759
 
9742
9760
  // src/constants.ts
9743
9761
  var MAX_HTML_SIZE = 2 * 1024 * 1024;
9762
+ var MAX_WARNING_LOCATIONS = 100;
9744
9763
  var GENERIC_LINK_TEXT = /* @__PURE__ */ new Set([
9745
9764
  "click here",
9746
9765
  "here",
@@ -10477,360 +10496,256 @@ function transformForAllClients(html, framework) {
10477
10496
  }
10478
10497
 
10479
10498
  // src/analyze.ts
10480
- var cheerio3 = __toESM(require("cheerio"), 1);
10481
- var csstree3 = __toESM(require("css-tree"), 1);
10482
- var HTML_ELEMENT_SELECTORS = {
10483
- "<style>": "style",
10484
- "<link>": "link[rel='stylesheet']",
10485
- "<svg>": "svg",
10486
- "<video>": "video",
10487
- "<form>": "form, input, button[type='submit']",
10488
- "<audio>": "audio",
10489
- "<picture>": "picture",
10490
- "<dialog>": "dialog",
10491
- "<meter>": "meter",
10492
- "<progress>": "progress",
10493
- "<select>": "select",
10494
- "<textarea>": "textarea",
10495
- "<marquee>": "marquee",
10496
- "<object>": "object",
10497
- "<base>": "base"
10498
- };
10499
- var HTML_ELEMENT_SEVERITY = {
10500
- "<style>": "error",
10501
- "<link>": "error",
10502
- "<svg>": "error",
10503
- "<form>": "error",
10504
- "<video>": "warning",
10505
- "<audio>": "warning",
10506
- "<picture>": "warning",
10507
- "<dialog>": "warning",
10508
- "<marquee>": "warning",
10509
- "<meter>": "warning",
10510
- "<progress>": "warning",
10511
- "<select>": "warning",
10512
- "<textarea>": "warning",
10513
- "<object>": "warning",
10514
- "<base>": "warning"
10515
- };
10516
- var HTML_ELEMENT_MESSAGES = {
10517
- "<style>": (n) => `${n} strips <style> blocks. Styles must be inlined.`,
10518
- "<link>": (n) => `${n} does not support external stylesheets.`,
10519
- "<svg>": (n) => `${n} does not support inline SVG.`,
10520
- "<video>": (n) => `${n} does not support <video> elements.`,
10521
- "<form>": (n) => `${n} strips form elements.`
10522
- };
10523
- var COMPOUND_DETECTORS = [
10524
- { key: "display:flex", property: "display", valueIncludes: "flex" },
10525
- { key: "display:grid", property: "display", valueIncludes: "grid" },
10526
- { key: "display:none", property: "display", valueIncludes: "none" }
10527
- ];
10528
- var CSS_FUNCTION_DETECTORS = CSS_FUNCTION_FEATURES.map((fn) => ({
10529
- key: fn,
10530
- pattern: `${fn}(`
10531
- // require opening paren — matches "min(" but not "Minion"
10532
- }));
10533
- function analyzeEmailFromDom($, framework) {
10534
- const warnings = [];
10535
- const seenWarnings = /* @__PURE__ */ new Set();
10536
- function addWarning(w) {
10537
- const key = `${w.client}:${w.property}:${w.severity}:${w.selector || ""}`;
10538
- if (!seenWarnings.has(key)) {
10539
- seenWarnings.add(key);
10540
- warnings.push(w);
10499
+ var csstree5 = __toESM(require("css-tree"), 1);
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;
10541
10538
  }
10542
10539
  }
10543
- function describeSelector(el) {
10544
- var _a;
10545
- const $el = $(el);
10546
- const tag = ((_a = el.tagName) == null ? void 0 : _a.toLowerCase()) || "";
10547
- const cls = $el.attr("class");
10548
- const id = $el.attr("id");
10549
- if (id) return `${tag}#${id}`;
10550
- if (cls) return `${tag}.${cls.split(/\s+/)[0]}`;
10551
- const href = $el.attr("href");
10552
- if (href) return `${tag}[href]`;
10553
- return tag;
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()));
10554
10568
  }
10555
- for (const feature of HTML_ELEMENT_FEATURES) {
10556
- const selector = HTML_ELEMENT_SELECTORS[feature];
10557
- if (!selector) continue;
10558
- if ($(selector).length === 0) continue;
10559
- const supportData = CSS_SUPPORT[feature];
10560
- if (!supportData) continue;
10561
- const baseSeverity = HTML_ELEMENT_SEVERITY[feature] || "warning";
10562
- for (const client of EMAIL_CLIENTS) {
10563
- const support = supportData[client.id];
10564
- if (support === "unsupported") {
10565
- const msgFn = HTML_ELEMENT_MESSAGES[feature];
10566
- const message = msgFn ? msgFn(client.name) : `${client.name} does not support ${feature}.`;
10567
- const sug = getSuggestion(feature, client.id, framework);
10568
- const fix = getCodeFix(feature, client.id, framework);
10569
- addWarning(__spreadValues({
10570
- severity: baseSeverity,
10571
- client: client.id,
10572
- property: feature,
10573
- message,
10574
- suggestion: sug.text,
10575
- fix,
10576
- fixType: getFixType(feature)
10577
- }, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(feature, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
10578
- } else if (support === "partial" && feature === "<style>") {
10579
- const sug = getSuggestion("<style>:partial", client.id, framework);
10580
- const fix = getCodeFix("<style>", client.id, framework);
10581
- addWarning(__spreadValues({
10582
- severity: "warning",
10583
- client: client.id,
10584
- property: "<style>",
10585
- message: `${client.name} has partial <style> support (head only, with limitations). Inline styles recommended.`,
10586
- suggestion: sug.text,
10587
- fix,
10588
- fixType: getFixType("<style>")
10589
- }, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<style>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
10590
- }
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);
10591
10637
  }
10592
- }
10593
- const parsedAtRules = /* @__PURE__ */ new Set();
10594
- const parsedProperties = /* @__PURE__ */ new Set();
10595
- const propertyLines = /* @__PURE__ */ new Map();
10596
- const propertyValues = /* @__PURE__ */ new Map();
10597
- const detectedCssFunctions = /* @__PURE__ */ new Set();
10598
- const detectedPseudoClasses = /* @__PURE__ */ new Set();
10599
- const detectedPseudoElements = /* @__PURE__ */ new Set();
10600
- $("style").each((_, el) => {
10601
- const cssText = $(el).text();
10602
- try {
10603
- const ast = csstree3.parse(cssText, { parseCustomProperty: true, positions: true });
10604
- csstree3.walk(ast, {
10605
- enter(node) {
10606
- if (node.type === "Atrule") {
10607
- parsedAtRules.add(`@${node.name}`);
10608
- }
10609
- if (node.type === "PseudoClassSelector") {
10610
- detectedPseudoClasses.add(`:${node.name}`);
10611
- }
10612
- if (node.type === "PseudoElementSelector") {
10613
- detectedPseudoElements.add(`::${node.name}`);
10614
- }
10615
- if (node.type === "Declaration") {
10616
- const prop = node.property.toLowerCase();
10617
- parsedProperties.add(prop);
10618
- if (node.loc && !propertyLines.has(prop)) {
10619
- propertyLines.set(prop, node.loc.start.line);
10620
- }
10621
- const valueStr = csstree3.generate(node.value);
10622
- const seenValues = propertyValues.get(prop);
10623
- if (seenValues) seenValues.push(valueStr);
10624
- else propertyValues.set(prop, [valueStr]);
10625
- for (const det of COMPOUND_DETECTORS) {
10626
- if (prop === det.property && valueStr.includes(det.valueIncludes)) {
10627
- parsedProperties.add(det.key);
10628
- if (node.loc && !propertyLines.has(det.key)) {
10629
- propertyLines.set(det.key, node.loc.start.line);
10630
- }
10631
- }
10632
- }
10633
- for (const fn of CSS_FUNCTION_DETECTORS) {
10634
- if (valueStr.includes(fn.pattern)) {
10635
- detectedCssFunctions.add(fn.key);
10636
- if (node.loc && !propertyLines.has(fn.key)) {
10637
- propertyLines.set(fn.key, node.loc.start.line);
10638
- }
10639
- }
10640
- }
10641
- }
10642
- }
10643
- });
10644
- } catch (e) {
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;
10645
10644
  }
10646
- });
10647
- for (const atRule of AT_RULE_FEATURES) {
10648
- if (!parsedAtRules.has(atRule)) continue;
10649
- checkPropertySupport(atRule, addWarning, framework);
10650
- }
10651
- const cssPropertiesToCheck = Object.keys(CSS_SUPPORT).filter(
10652
- (k) => !k.startsWith("<") && !k.startsWith("@")
10653
- );
10654
- $("[style]").each((_, el) => {
10655
- var _a;
10656
- const style = $(el).attr("style") || "";
10657
- const props = parseStyleProperties(style);
10658
- const selector = describeSelector(el);
10659
- for (const prop of props) {
10660
- for (const det of COMPOUND_DETECTORS) {
10661
- if (prop === det.property) {
10662
- const value2 = getStyleValue(style, prop);
10663
- if (value2 == null ? void 0 : value2.includes(det.valueIncludes)) {
10664
- checkPropertySupport(det.key, addWarning, framework, selector);
10665
- }
10666
- }
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");
10667
10653
  }
10668
- if (cssPropertiesToCheck.includes(prop)) {
10669
- checkPropertySupport(prop, addWarning, framework, selector, void 0, (_a = getStyleValue(style, prop)) != null ? _a : void 0);
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));
10670
10663
  }
10671
- const value = getStyleValue(style, prop);
10672
- if (value) {
10673
- for (const fn of CSS_FUNCTION_DETECTORS) {
10674
- if (value.includes(fn.pattern)) {
10675
- checkPropertySupport(fn.key, addWarning, framework, selector);
10676
- }
10677
- }
10664
+ const { banned } = quotedValues(note);
10665
+ if (banned.length) {
10666
+ const v = dashed(value);
10667
+ return banned.includes(v) || banned.includes(unprefixed(v));
10678
10668
  }
10669
+ if (noteLc.includes("two-value syntax")) return tokens(value).length > 1;
10670
+ return true;
10679
10671
  }
10680
- });
10681
- for (const prop of parsedProperties) {
10682
- if (prop.includes(":")) continue;
10683
- if (!cssPropertiesToCheck.includes(prop)) continue;
10684
- const values = propertyValues.get(prop);
10685
- checkPropertySupport(
10686
- prop,
10687
- addWarning,
10688
- framework,
10689
- void 0,
10690
- propertyLines.get(prop),
10691
- values ? values.join(" ") : void 0
10692
- );
10693
- }
10694
- for (const compound of COMPOUND_VALUE_FEATURES) {
10695
- if (compound.startsWith(":") || compound.startsWith("::")) continue;
10696
- if (parsedProperties.has(compound)) {
10697
- checkPropertySupport(compound, addWarning, framework, void 0, propertyLines.get(compound));
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;
10698
10680
  }
10699
- }
10700
- for (const pseudo of detectedPseudoClasses) {
10701
- if (CSS_SUPPORT[pseudo]) {
10702
- checkPropertySupport(pseudo, addWarning, framework);
10681
+ case "border-radius": {
10682
+ if (noteLc.includes("slash")) return topLevelSplit(value, "/").length > 1;
10683
+ return true;
10703
10684
  }
10704
- }
10705
- for (const pseudo of detectedPseudoElements) {
10706
- if (CSS_SUPPORT[pseudo]) {
10707
- checkPropertySupport(pseudo, addWarning, framework);
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
+ );
10708
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;
10709
10714
  }
10710
- for (const fn of detectedCssFunctions) {
10711
- checkPropertySupport(fn, addWarning, framework, void 0, propertyLines.get(fn));
10712
- }
10713
- const severityOrder = { error: 0, warning: 1, info: 2 };
10714
- warnings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
10715
- return warnings;
10716
10715
  }
10717
- function analyzeEmail(html, framework) {
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
+
10724
+ // src/dark-mode-checker.ts
10725
+ var csstree4 = __toESM(require("css-tree"), 1);
10726
+
10727
+ // src/dark-mode.ts
10728
+ var cheerio3 = __toESM(require("cheerio"), 1);
10729
+ var csstree3 = __toESM(require("css-tree"), 1);
10730
+ var LIGHT_THRESHOLD = 0.7;
10731
+ var DARK_THRESHOLD = 0.15;
10732
+ var PREFERS_COLOR_SCHEME_CLIENTS = [
10733
+ "apple-mail-macos",
10734
+ "apple-mail-ios",
10735
+ "samsung-mail",
10736
+ "thunderbird",
10737
+ "hey-mail",
10738
+ "superhuman"
10739
+ ];
10740
+ function simulateDarkMode(html, clientId) {
10741
+ var _a, _b;
10718
10742
  if (!html || !html.trim()) {
10719
- return [];
10743
+ return { html: html || "", warnings: [] };
10720
10744
  }
10721
10745
  if (html.length > MAX_HTML_SIZE) {
10722
10746
  throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
10723
10747
  }
10724
10748
  const $ = cheerio3.load(html);
10725
- return analyzeEmailFromDom($, framework);
10726
- }
10727
- function getFixType(prop) {
10728
- return STRUCTURAL_FIX_PROPERTIES.has(prop) ? "structural" : "css";
10729
- }
10730
- var VALUE_CAVEAT_PROPS = /* @__PURE__ */ new Set(["margin", "position", "overflow"]);
10731
- var POSITION_KEYWORDS = ["relative", "absolute", "fixed", "sticky"];
10732
- function valueTriggersCaveat(prop, value, notes) {
10733
- const note = (notes != null ? notes : []).join(" ");
10734
- const noteLc = note.toLowerCase();
10735
- if (prop === "margin") {
10736
- if (/(?:^|[\s:(])-\.?\d/.test(value) && noteLc.includes("negative")) return true;
10737
- if (/\bauto\b/.test(value) && noteLc.includes("auto")) return true;
10738
- return false;
10739
- }
10740
- if (prop === "position") {
10741
- const used = POSITION_KEYWORDS.find((k) => new RegExp(`\\b${k}\\b`).test(value));
10742
- if (!used) return false;
10743
- const m = note.match(/supports\s+.+?\s+but not\s+([^.]+)/i);
10744
- if (m) return m[1].toLowerCase().includes(used);
10745
- return used === "fixed" || used === "sticky";
10746
- }
10747
- if (prop === "overflow") {
10748
- if (!/\b(?:auto|scroll)\b/.test(value)) return false;
10749
- return noteLc.includes("cannot scroll");
10750
- }
10751
- return true;
10752
- }
10753
- function noteSuffix(notes) {
10754
- if (!(notes == null ? void 0 : notes.length)) return "";
10755
- const cleaned = notes.map((n) => n.replace(/^(?:Partial|Buggy|Not supported)\.\s*/i, "").trim()).filter(Boolean);
10756
- return cleaned.length ? ` ${cleaned.join(" ")}` : "";
10757
- }
10758
- function checkPropertySupport(prop, addWarning, framework, selector, line, value) {
10759
- var _a;
10760
- const supportData = CSS_SUPPORT[prop];
10761
- if (!supportData) return;
10762
- const fixType = getFixType(prop);
10763
- const valueGated = VALUE_CAVEAT_PROPS.has(prop);
10764
- for (const client of EMAIL_CLIENTS) {
10765
- const support = supportData[client.id] || "unknown";
10766
- const notes = (_a = CSS_SUPPORT_NOTES[prop]) == null ? void 0 : _a[client.id];
10767
- if (support === "unsupported") {
10768
- const sug = getSuggestion(prop, client.id, framework);
10769
- const fix = getCodeFix(prop, client.id, framework);
10770
- addWarning(__spreadValues(__spreadValues(__spreadValues({
10771
- severity: "warning",
10772
- client: client.id,
10773
- property: prop,
10774
- message: `${client.name} does not support "${prop}".${noteSuffix(notes)}`,
10775
- suggestion: sug.text,
10776
- fix,
10777
- fixType
10778
- }, selector ? { selector } : {}), line !== void 0 ? { line } : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
10779
- } else if (support === "partial") {
10780
- if (valueGated && value !== void 0 && !valueTriggersCaveat(prop, value, notes)) continue;
10781
- const sug = getSuggestion(prop, client.id, framework);
10782
- const fix = getCodeFix(prop, client.id, framework);
10783
- addWarning(__spreadValues(__spreadValues(__spreadValues({
10784
- severity: "info",
10785
- client: client.id,
10786
- property: prop,
10787
- message: `${client.name} has partial support for "${prop}".${noteSuffix(notes)}`,
10788
- suggestion: sug.text,
10789
- fix,
10790
- fixType
10791
- }, selector ? { selector } : {}), line !== void 0 ? { line } : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
10792
- }
10793
- }
10794
- }
10795
- function generateCompatibilityScore(warnings) {
10796
- const result = {};
10797
- for (const client of EMAIL_CLIENTS) {
10798
- const clientWarnings = warnings.filter((w) => w.client === client.id);
10799
- const errorProps = new Set(clientWarnings.filter((w) => w.severity === "error").map((w) => w.property));
10800
- const warnProps = new Set(clientWarnings.filter((w) => w.severity === "warning").map((w) => w.property));
10801
- const infoProps = new Set(clientWarnings.filter((w) => w.severity === "info").map((w) => w.property));
10802
- const errors = errorProps.size;
10803
- const warns = warnProps.size;
10804
- const info = infoProps.size;
10805
- const score = Math.max(0, Math.min(100, 100 - errors * 10 - warns * 3));
10806
- result[client.id] = { score, errors, warnings: warns, info };
10807
- }
10808
- return result;
10809
- }
10810
- function warningsForClient(warnings, clientId) {
10811
- return warnings.filter((w) => w.client === clientId);
10812
- }
10813
- function errorWarnings(warnings) {
10814
- return warnings.filter((w) => w.severity === "error");
10815
- }
10816
- function structuralWarnings(warnings) {
10817
- return warnings.filter((w) => w.fixType === "structural");
10818
- }
10819
-
10820
- // src/dark-mode.ts
10821
- var cheerio4 = __toESM(require("cheerio"), 1);
10822
- var csstree4 = __toESM(require("css-tree"), 1);
10823
- var LIGHT_THRESHOLD = 0.7;
10824
- var DARK_THRESHOLD = 0.15;
10825
- function simulateDarkMode(html, clientId) {
10826
- var _a, _b;
10827
- if (!html || !html.trim()) {
10828
- return { html: html || "", warnings: [] };
10829
- }
10830
- if (html.length > MAX_HTML_SIZE) {
10831
- throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
10832
- }
10833
- const $ = cheerio4.load(html);
10834
10749
  const warnings = [];
10835
10750
  $("img").each((_, el) => {
10836
10751
  const src = $(el).attr("src") || "";
@@ -11040,46 +10955,802 @@ function applyColorInversion($, mode) {
11040
10955
  $("style").each((_, el) => {
11041
10956
  const cssText = $(el).text();
11042
10957
  try {
11043
- const ast = csstree4.parse(cssText, { parseCustomProperty: true });
10958
+ const ast = csstree3.parse(cssText, { parseCustomProperty: true });
11044
10959
  let modified = false;
11045
- csstree4.walk(ast, {
10960
+ csstree3.walk(ast, {
11046
10961
  enter(node) {
11047
10962
  if (node.type !== "Declaration") return;
11048
10963
  const prop = node.property.toLowerCase();
11049
10964
  if (!COLOR_PROPS.has(prop) && prop !== "background") return;
11050
- const valueStr = csstree4.generate(node.value);
10965
+ const valueStr = csstree3.generate(node.value);
11051
10966
  if (prop === "background") {
11052
10967
  const bgColor = extractBackgroundColor(valueStr);
11053
10968
  if (bgColor) {
11054
10969
  const inverted = invertColor(bgColor, mode);
11055
10970
  if (inverted) {
11056
10971
  const newValue = valueStr.replace(bgColor, inverted);
11057
- node.value = csstree4.parse(newValue, { context: "value" });
10972
+ node.value = csstree3.parse(newValue, { context: "value" });
11058
10973
  modified = true;
11059
10974
  }
11060
10975
  }
11061
10976
  } else {
11062
10977
  const inverted = invertColor(valueStr, mode);
11063
10978
  if (inverted) {
11064
- node.value = csstree4.parse(inverted, { context: "value" });
10979
+ node.value = csstree3.parse(inverted, { context: "value" });
11065
10980
  modified = true;
11066
10981
  }
11067
10982
  }
11068
10983
  }
11069
- });
11070
- if (modified) {
11071
- $(el).text(csstree4.generate(ast));
10984
+ });
10985
+ if (modified) {
10986
+ $(el).text(csstree3.generate(ast));
10987
+ }
10988
+ } catch (e) {
10989
+ }
10990
+ });
10991
+ $("[bgcolor]").each((_, el) => {
10992
+ const bgcolor = $(el).attr("bgcolor") || "";
10993
+ const inverted = invertColor(bgcolor, mode);
10994
+ if (inverted) {
10995
+ $(el).attr("bgcolor", inverted);
10996
+ }
10997
+ });
10998
+ }
10999
+
11000
+ // src/source-location.ts
11001
+ function toLoc(p) {
11002
+ return {
11003
+ line: p.startLine,
11004
+ column: p.startCol,
11005
+ endLine: p.endLine,
11006
+ endColumn: p.endCol,
11007
+ offset: p.startOffset,
11008
+ length: p.endOffset - p.startOffset
11009
+ };
11010
+ }
11011
+ function locOfElement(el) {
11012
+ var _a;
11013
+ const raw = el == null ? void 0 : el.sourceCodeLocation;
11014
+ if (!raw) return void 0;
11015
+ return toLoc((_a = raw.startTag) != null ? _a : raw);
11016
+ }
11017
+ function locOfAttr(el, attr) {
11018
+ var _a, _b, _c, _d;
11019
+ const raw = el == null ? void 0 : el.sourceCodeLocation;
11020
+ if (!raw) return void 0;
11021
+ const attrLoc = (_d = (_a = raw.attrs) == null ? void 0 : _a[attr]) != null ? _d : (_c = (_b = raw.startTag) == null ? void 0 : _b.attrs) == null ? void 0 : _c[attr];
11022
+ return attrLoc ? toLoc(attrLoc) : locOfElement(el);
11023
+ }
11024
+ function locOfFirst($, selector) {
11025
+ const el = $(selector).first()[0];
11026
+ return el ? locOfElement(el) : void 0;
11027
+ }
11028
+ function cssBlockAnchor(styleEl, cssText, source) {
11029
+ var _a;
11030
+ const children = styleEl == null ? void 0 : styleEl.children;
11031
+ if (!children || children.length !== 1) return void 0;
11032
+ const loc = (_a = children[0]) == null ? void 0 : _a.sourceCodeLocation;
11033
+ if (!loc) return void 0;
11034
+ const mapper = source ? crMapper(source.slice(loc.startOffset, loc.endOffset), cssText) : null;
11035
+ const extraBefore = mapper ? (index) => mapper(index) - index : crOffsetter(cssText, loc.endOffset - loc.startOffset);
11036
+ return __spreadValues({ loc, extraBefore }, mapper ? { source } : {});
11037
+ }
11038
+ function crMapper(raw, decoded) {
11039
+ if (raw.length === decoded.length) return (index) => index;
11040
+ const points = [];
11041
+ const extras = [];
11042
+ let r = 0;
11043
+ let d = 0;
11044
+ let extra = 0;
11045
+ while (d < decoded.length) {
11046
+ if (r >= raw.length) return null;
11047
+ if (raw[r] === decoded[d]) {
11048
+ r++;
11049
+ d++;
11050
+ continue;
11051
+ }
11052
+ if (raw[r] === "\r" && decoded[d] === "\n") {
11053
+ const consumed = raw[r + 1] === "\n" ? 2 : 1;
11054
+ extra += consumed - 1;
11055
+ points.push(d);
11056
+ extras.push(extra);
11057
+ r += consumed;
11058
+ d += 1;
11059
+ continue;
11060
+ }
11061
+ return null;
11062
+ }
11063
+ if (r !== raw.length) return null;
11064
+ return (index) => index + lookup(points, extras, index);
11065
+ }
11066
+ function lookup(points, extras, index) {
11067
+ let lo = 0;
11068
+ let hi = points.length - 1;
11069
+ let found = 0;
11070
+ while (lo <= hi) {
11071
+ const mid = lo + hi >> 1;
11072
+ if (points[mid] < index) {
11073
+ found = extras[mid];
11074
+ lo = mid + 1;
11075
+ } else {
11076
+ hi = mid - 1;
11077
+ }
11078
+ }
11079
+ return found;
11080
+ }
11081
+ function findRawOffset(raw, decoded, index, token) {
11082
+ if (!token) return -1;
11083
+ let occurrence = 0;
11084
+ for (let at = decoded.indexOf(token); at !== -1 && at < index; at = decoded.indexOf(token, at + 1)) {
11085
+ occurrence++;
11086
+ }
11087
+ let found = -1;
11088
+ let from = 0;
11089
+ for (let i = 0; i <= occurrence; i++) {
11090
+ found = raw.indexOf(token, from);
11091
+ if (found === -1) return -1;
11092
+ from = found + 1;
11093
+ }
11094
+ return found;
11095
+ }
11096
+ function positionOf(source, offset) {
11097
+ const prefix = source.slice(0, offset);
11098
+ return { line: prefix.split("\n").length, column: offset - prefix.lastIndexOf("\n") };
11099
+ }
11100
+ function crOffsetter(text, rawLength) {
11101
+ const removed = rawLength - text.length;
11102
+ if (removed === 0) return () => 0;
11103
+ const newlines = countNewlines(text, text.length);
11104
+ if (removed !== newlines || newlines === 0) return null;
11105
+ return (index) => countNewlines(text, index);
11106
+ }
11107
+ function countNewlines(text, upTo) {
11108
+ let n = 0;
11109
+ for (let i = 0; i < upTo && i < text.length; i++) if (text.charCodeAt(i) === 10) n++;
11110
+ return n;
11111
+ }
11112
+ function locInCssBlock(anchor, cssLoc) {
11113
+ if (!anchor || !cssLoc) return void 0;
11114
+ const { loc: block, extraBefore } = anchor;
11115
+ if (!extraBefore) {
11116
+ return {
11117
+ line: block.startLine,
11118
+ column: block.startCol,
11119
+ endLine: block.startLine,
11120
+ endColumn: block.startCol,
11121
+ offset: block.startOffset,
11122
+ length: 0
11123
+ };
11124
+ }
11125
+ const line = block.startLine + cssLoc.start.line - 1;
11126
+ const column = cssLoc.start.line === 1 ? block.startCol + cssLoc.start.column - 1 : cssLoc.start.column;
11127
+ const endLine = block.startLine + cssLoc.end.line - 1;
11128
+ const endColumn = cssLoc.end.line === 1 ? block.startCol + cssLoc.end.column - 1 : cssLoc.end.column;
11129
+ const start = block.startOffset + cssLoc.start.offset + extraBefore(cssLoc.start.offset);
11130
+ const end = block.startOffset + cssLoc.end.offset + extraBefore(cssLoc.end.offset);
11131
+ if (anchor.source) {
11132
+ const from = positionOf(anchor.source, start);
11133
+ const to = positionOf(anchor.source, end);
11134
+ return {
11135
+ line: from.line,
11136
+ column: from.column,
11137
+ endLine: to.line,
11138
+ endColumn: to.column,
11139
+ offset: start,
11140
+ length: end - start
11141
+ };
11142
+ }
11143
+ return { line, column, endLine, endColumn, offset: start, length: end - start };
11144
+ }
11145
+ function locInTextNode(node, index, length, source) {
11146
+ var _a;
11147
+ const anchor = node == null ? void 0 : node.sourceCodeLocation;
11148
+ if (!anchor) return void 0;
11149
+ const data = (_a = node.data) != null ? _a : "";
11150
+ const rawLength = anchor.endOffset - anchor.startOffset;
11151
+ if (source) {
11152
+ const raw = source.slice(anchor.startOffset, anchor.endOffset);
11153
+ const token = data.slice(index, index + length);
11154
+ const at = findRawOffset(raw, data, index, token);
11155
+ if (at !== -1) {
11156
+ const start2 = anchor.startOffset + at;
11157
+ const from = positionOf(source, start2);
11158
+ const to = positionOf(source, start2 + token.length);
11159
+ return {
11160
+ line: from.line,
11161
+ column: from.column,
11162
+ endLine: to.line,
11163
+ endColumn: to.column,
11164
+ offset: start2,
11165
+ length: token.length
11166
+ };
11167
+ }
11168
+ }
11169
+ const extraBefore = crOffsetter(data, rawLength);
11170
+ if (!extraBefore) {
11171
+ const clamped = Math.min(length, rawLength);
11172
+ return {
11173
+ line: anchor.startLine,
11174
+ column: anchor.startCol,
11175
+ endLine: anchor.startLine,
11176
+ endColumn: anchor.startCol + clamped,
11177
+ offset: anchor.startOffset,
11178
+ length: clamped
11179
+ };
11180
+ }
11181
+ const start = positionAt(data, index, anchor);
11182
+ const end = positionAt(data, index + length, anchor);
11183
+ const startOffset = anchor.startOffset + index + extraBefore(index);
11184
+ const endOffset = anchor.startOffset + index + length + extraBefore(index + length);
11185
+ return {
11186
+ line: start.line,
11187
+ column: start.column,
11188
+ endLine: end.line,
11189
+ endColumn: end.column,
11190
+ offset: startOffset,
11191
+ length: endOffset - startOffset
11192
+ };
11193
+ }
11194
+ function positionAt(data, index, anchor) {
11195
+ const prefix = data.slice(0, index);
11196
+ const newlines = prefix.split("\n").length - 1;
11197
+ if (newlines === 0) {
11198
+ return { line: anchor.startLine, column: anchor.startCol + index };
11199
+ }
11200
+ return { line: anchor.startLine + newlines, column: index - prefix.lastIndexOf("\n") };
11201
+ }
11202
+
11203
+ // src/dark-mode-checker.ts
11204
+ var DARK_MEDIA_RE = /\(\s*prefers-color-scheme\s*:\s*dark\s*\)/i;
11205
+ var MAX_UNCOVERED_ELEMENTS = 3;
11206
+ function backgroundShorthandColor(value) {
11207
+ if (!value) return null;
11208
+ const trimmed = value.trim();
11209
+ if (parseColor(trimmed)) return trimmed;
11210
+ for (const token of trimmed.split(/\s+/)) {
11211
+ if (token.includes("(")) continue;
11212
+ if (parseColor(token)) return token;
11213
+ }
11214
+ return null;
11215
+ }
11216
+ function isLight(value) {
11217
+ const c = parseColor(value);
11218
+ if (!c || c.a < 0.5) return false;
11219
+ return relativeLuminance(c.r, c.g, c.b) > LIGHT_THRESHOLD;
11220
+ }
11221
+ function collectDarkBlocks($) {
11222
+ const block = { any: [], important: [], rules: 0 };
11223
+ let found = false;
11224
+ $("style").each((_, el) => {
11225
+ const cssText = $(el).text();
11226
+ if (!DARK_MEDIA_RE.test(cssText)) return;
11227
+ let ast;
11228
+ try {
11229
+ ast = csstree4.parse(cssText);
11230
+ } catch (e) {
11231
+ found = true;
11232
+ return;
11233
+ }
11234
+ csstree4.walk(ast, {
11235
+ visit: "Atrule",
11236
+ enter(node) {
11237
+ if (node.type !== "Atrule" || node.name.toLowerCase() !== "media") return;
11238
+ if (!node.prelude || !DARK_MEDIA_RE.test(csstree4.generate(node.prelude))) return;
11239
+ found = true;
11240
+ if (!node.block) return;
11241
+ csstree4.walk(node.block, {
11242
+ visit: "Rule",
11243
+ enter(rule) {
11244
+ if (rule.type !== "Rule") return;
11245
+ block.rules++;
11246
+ let setsBackground = false;
11247
+ let important = false;
11248
+ rule.block.children.forEach((child) => {
11249
+ if (child.type !== "Declaration") return;
11250
+ const prop = child.property.toLowerCase();
11251
+ if (prop !== "background" && prop !== "background-color") return;
11252
+ setsBackground = true;
11253
+ if (child.important) important = true;
11254
+ });
11255
+ if (!setsBackground) return;
11256
+ const selector = csstree4.generate(rule.prelude).trim();
11257
+ if (!selector) return;
11258
+ block.any.push(selector);
11259
+ if (important) block.important.push(selector);
11260
+ }
11261
+ });
11262
+ }
11263
+ });
11264
+ });
11265
+ return found ? block : null;
11266
+ }
11267
+ function matchedElements($, selectors) {
11268
+ const matched = /* @__PURE__ */ new Set();
11269
+ for (const selector of selectors) {
11270
+ try {
11271
+ $(selector).each((_, el) => {
11272
+ matched.add(el);
11273
+ });
11274
+ } catch (e) {
11275
+ }
11276
+ }
11277
+ return matched;
11278
+ }
11279
+ function describeSelector($, el) {
11280
+ var _a;
11281
+ const $el = $(el);
11282
+ const tag = ((_a = el.tagName) == null ? void 0 : _a.toLowerCase()) || "element";
11283
+ const id = $el.attr("id");
11284
+ if (id) return `${tag}#${id}`;
11285
+ const cls = $el.attr("class");
11286
+ if (cls) return `${tag}.${cls.split(/\s+/)[0]}`;
11287
+ return tag;
11288
+ }
11289
+ function checkDarkModeFromDom($) {
11290
+ var _a, _b;
11291
+ const darkBlock = collectDarkBlocks($);
11292
+ if (!darkBlock) return [];
11293
+ const warnings = [];
11294
+ const hasOptIn = $("meta").toArray().some((el) => {
11295
+ const name = ($(el).attr("name") || "").trim().toLowerCase();
11296
+ return name === "color-scheme" || name === "supported-color-schemes";
11297
+ });
11298
+ if (!hasOptIn) {
11299
+ const headLoc = locOfFirst($, "head");
11300
+ for (const clientId of PREFERS_COLOR_SCHEME_CLIENTS) {
11301
+ warnings.push(__spreadValues({
11302
+ severity: "warning",
11303
+ client: clientId,
11304
+ property: "dark-mode-opt-in",
11305
+ message: `The email has @media (prefers-color-scheme: dark) styles but no dark-mode opt-in meta tag. ${(_b = (_a = getClient(clientId)) == null ? void 0 : _a.name) != null ? _b : clientId} may keep the email in light mode, so the dark styles never activate.`,
11306
+ suggestion: 'Add both opt-in tags to <head>: <meta name="color-scheme" content="light dark"> and <meta name="supported-color-schemes" content="light dark">.',
11307
+ fixType: "structural"
11308
+ }, headLoc ? { loc: headLoc, locs: [headLoc] } : {}));
11309
+ }
11310
+ }
11311
+ if (darkBlock.rules === 0) return warnings;
11312
+ const coveredByImportant = matchedElements($, darkBlock.important);
11313
+ const coveredByAny = matchedElements($, darkBlock.any);
11314
+ let uncovered = 0;
11315
+ $("[bgcolor], [style]").each((_, el) => {
11316
+ var _a2;
11317
+ if (uncovered >= MAX_UNCOVERED_ELEMENTS) return false;
11318
+ const $el = $(el);
11319
+ const style = parseInlineStyle($el.attr("style") || "");
11320
+ const inline = (_a2 = style.get("background-color")) != null ? _a2 : backgroundShorthandColor(style.get("background"));
11321
+ const color = inline != null ? inline : $el.attr("bgcolor");
11322
+ if (!color || !isLight(color)) return;
11323
+ if (inline ? coveredByImportant.has(el) : coveredByAny.has(el)) return;
11324
+ uncovered++;
11325
+ const selector = describeSelector($, el);
11326
+ const loc = locOfAttr(el, inline ? "style" : "bgcolor");
11327
+ for (const clientId of PREFERS_COLOR_SCHEME_CLIENTS) {
11328
+ warnings.push(__spreadValues({
11329
+ severity: "warning",
11330
+ client: clientId,
11331
+ property: "dark-mode-coverage",
11332
+ message: `<${selector}> keeps its hardcoded light background (${color}) in dark mode \u2014 the dark block never overrides it. The rest of the email inverts around it, producing a half-inverted render (e.g. light text left on a still-white background).`,
11333
+ suggestion: inline ? `Override it inside @media (prefers-color-scheme: dark) with a dark background-color and !important (an inline style beats a plain rule), or move the colour onto a class the dark block already targets.` : `Override it inside @media (prefers-color-scheme: dark) with a dark background-color, or give the element a class the dark block already targets.`,
11334
+ fixType: "css",
11335
+ selector
11336
+ }, loc ? { loc, locs: [loc] } : {}));
11337
+ }
11338
+ });
11339
+ return warnings;
11340
+ }
11341
+
11342
+ // src/parse-html.ts
11343
+ var cheerio4 = __toESM(require("cheerio"), 1);
11344
+ function loadHtml(html, options) {
11345
+ return (options == null ? void 0 : options.positions) ? cheerio4.load(html, { sourceCodeLocationInfo: true }) : cheerio4.load(html);
11346
+ }
11347
+ function fromHtml(html, empty, fn, options) {
11348
+ if (!html || !html.trim()) return empty;
11349
+ if (html.length > MAX_HTML_SIZE) {
11350
+ throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
11351
+ }
11352
+ return fn(loadHtml(html, options), html);
11353
+ }
11354
+
11355
+ // src/analyze.ts
11356
+ var HTML_ELEMENT_SELECTORS = {
11357
+ "<style>": "style",
11358
+ "<link>": "link[rel='stylesheet']",
11359
+ "<svg>": "svg",
11360
+ "<video>": "video",
11361
+ "<form>": "form, input, button[type='submit']",
11362
+ "<audio>": "audio",
11363
+ "<picture>": "picture",
11364
+ "<dialog>": "dialog",
11365
+ "<meter>": "meter",
11366
+ "<progress>": "progress",
11367
+ "<select>": "select",
11368
+ "<textarea>": "textarea",
11369
+ "<marquee>": "marquee",
11370
+ "<object>": "object",
11371
+ "<base>": "base"
11372
+ };
11373
+ var HTML_ELEMENT_SEVERITY = {
11374
+ "<style>": "error",
11375
+ "<link>": "error",
11376
+ "<svg>": "error",
11377
+ "<form>": "error",
11378
+ "<video>": "warning",
11379
+ "<audio>": "warning",
11380
+ "<picture>": "warning",
11381
+ "<dialog>": "warning",
11382
+ "<marquee>": "warning",
11383
+ "<meter>": "warning",
11384
+ "<progress>": "warning",
11385
+ "<select>": "warning",
11386
+ "<textarea>": "warning",
11387
+ "<object>": "warning",
11388
+ "<base>": "warning"
11389
+ };
11390
+ var HTML_ELEMENT_MESSAGES = {
11391
+ "<style>": (n) => `${n} strips <style> blocks. Styles must be inlined.`,
11392
+ "<link>": (n) => `${n} does not support external stylesheets.`,
11393
+ "<svg>": (n) => `${n} does not support inline SVG.`,
11394
+ "<video>": (n) => `${n} does not support <video> elements.`,
11395
+ "<form>": (n) => `${n} strips form elements.`
11396
+ };
11397
+ var COMPOUND_DETECTORS = [
11398
+ { key: "display:flex", property: "display", valueIncludes: "flex" },
11399
+ { key: "display:grid", property: "display", valueIncludes: "grid" },
11400
+ { key: "display:none", property: "display", valueIncludes: "none" }
11401
+ ];
11402
+ var CSS_FUNCTION_DETECTORS = CSS_FUNCTION_FEATURES.map((fn) => ({
11403
+ key: fn,
11404
+ pattern: `${fn}(`
11405
+ // require opening paren — matches "min(" but not "Minion"
11406
+ }));
11407
+ function analyzeEmailFromDom($, framework, source) {
11408
+ const warnings = [];
11409
+ const seenWarnings = /* @__PURE__ */ new Map();
11410
+ function addWarning(w) {
11411
+ const key = `${w.client}:${w.property}:${w.severity}:${w.selector || ""}`;
11412
+ const existing = seenWarnings.get(key);
11413
+ if (!existing) {
11414
+ seenWarnings.set(key, w);
11415
+ warnings.push(w);
11416
+ return;
11417
+ }
11418
+ if (!existing.locs || !w.locs) return;
11419
+ for (const loc of w.locs) {
11420
+ if (existing.locs.some((l) => l.offset === loc.offset)) continue;
11421
+ if (existing.locs.length >= MAX_WARNING_LOCATIONS) {
11422
+ existing.locsTruncated = true;
11423
+ break;
11424
+ }
11425
+ existing.locs.push(loc);
11426
+ }
11427
+ }
11428
+ function describeSelector2(el) {
11429
+ var _a;
11430
+ const $el = $(el);
11431
+ const tag = ((_a = el.tagName) == null ? void 0 : _a.toLowerCase()) || "";
11432
+ const cls = $el.attr("class");
11433
+ const id = $el.attr("id");
11434
+ if (id) return `${tag}#${id}`;
11435
+ if (cls) return `${tag}.${cls.split(/\s+/)[0]}`;
11436
+ const href = $el.attr("href");
11437
+ if (href) return `${tag}[href]`;
11438
+ return tag;
11439
+ }
11440
+ for (const feature of HTML_ELEMENT_FEATURES) {
11441
+ const selector = HTML_ELEMENT_SELECTORS[feature];
11442
+ if (!selector) continue;
11443
+ const matches = $(selector);
11444
+ if (matches.length === 0) continue;
11445
+ const supportData = CSS_SUPPORT[feature];
11446
+ if (!supportData) continue;
11447
+ const baseSeverity = HTML_ELEMENT_SEVERITY[feature] || "warning";
11448
+ const found = matches.toArray().map((m) => locOfElement(m)).filter((l) => l !== void 0);
11449
+ const featureOccurrences = found.length ? __spreadValues({
11450
+ locs: found.slice(0, MAX_WARNING_LOCATIONS)
11451
+ }, found.length > MAX_WARNING_LOCATIONS ? { truncated: true } : {}) : void 0;
11452
+ const featureLoc = featureOccurrences == null ? void 0 : featureOccurrences.locs[0];
11453
+ for (const client of EMAIL_CLIENTS) {
11454
+ const support = supportData[client.id];
11455
+ if (support === "unsupported") {
11456
+ const msgFn = HTML_ELEMENT_MESSAGES[feature];
11457
+ const message = msgFn ? msgFn(client.name) : `${client.name} does not support ${feature}.`;
11458
+ const sug = getSuggestion(feature, client.id, framework);
11459
+ const fix = getCodeFix(feature, client.id, framework);
11460
+ addWarning(__spreadValues(__spreadValues({
11461
+ severity: baseSeverity,
11462
+ client: client.id,
11463
+ property: feature,
11464
+ message,
11465
+ suggestion: sug.text,
11466
+ fix,
11467
+ fixType: getFixType(feature)
11468
+ }, featureOccurrences ? occurrenceFields(featureOccurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(feature, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
11469
+ } else if (support === "partial" && feature === "<style>") {
11470
+ const sug = getSuggestion("<style>:partial", client.id, framework);
11471
+ const fix = getCodeFix("<style>", client.id, framework);
11472
+ addWarning(__spreadValues(__spreadValues({
11473
+ severity: "warning",
11474
+ client: client.id,
11475
+ property: "<style>",
11476
+ message: `${client.name} has partial <style> support (head only, with limitations). Inline styles recommended.`,
11477
+ suggestion: sug.text,
11478
+ fix,
11479
+ fixType: getFixType("<style>")
11480
+ }, featureOccurrences ? occurrenceFields(featureOccurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<style>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
11481
+ }
11482
+ }
11483
+ }
11484
+ const parsedAtRules = /* @__PURE__ */ new Set();
11485
+ const selectorLocs = /* @__PURE__ */ new Map();
11486
+ const parsedProperties = /* @__PURE__ */ new Set();
11487
+ const propertyLines = /* @__PURE__ */ new Map();
11488
+ const propertyLocs = /* @__PURE__ */ new Map();
11489
+ const propertyValues = /* @__PURE__ */ new Map();
11490
+ const detectedCssFunctions = /* @__PURE__ */ new Set();
11491
+ const detectedPseudoClasses = /* @__PURE__ */ new Set();
11492
+ const detectedPseudoElements = /* @__PURE__ */ new Set();
11493
+ let blockAnchor;
11494
+ function recordSelectorLoc(key, cssLoc) {
11495
+ if (!cssLoc) return;
11496
+ const loc = locInCssBlock(blockAnchor, cssLoc);
11497
+ if (!loc) return;
11498
+ const seen = selectorLocs.get(key);
11499
+ if (!seen) {
11500
+ selectorLocs.set(key, { locs: [loc] });
11501
+ return;
11502
+ }
11503
+ if (seen.locs.some((l) => l.offset === loc.offset)) return;
11504
+ if (seen.locs.length >= MAX_WARNING_LOCATIONS) {
11505
+ seen.truncated = true;
11506
+ return;
11507
+ }
11508
+ seen.locs.push(loc);
11509
+ }
11510
+ function recordLoc(key, cssLoc, value) {
11511
+ const loc = locInCssBlock(blockAnchor, cssLoc);
11512
+ if (!loc) return;
11513
+ const seen = propertyLocs.get(key);
11514
+ if (!seen) {
11515
+ propertyLocs.set(key, __spreadValues({ locs: [loc] }, value !== void 0 ? { values: [value] } : {}));
11516
+ return;
11517
+ }
11518
+ if (seen.locs.some((l) => l.offset === loc.offset)) return;
11519
+ if (seen.locs.length >= MAX_WARNING_LOCATIONS) {
11520
+ seen.truncated = true;
11521
+ return;
11522
+ }
11523
+ seen.locs.push(loc);
11524
+ if (seen.values && value !== void 0) seen.values.push(value);
11525
+ }
11526
+ $("style").each((_, el) => {
11527
+ const cssText = $(el).text();
11528
+ blockAnchor = cssBlockAnchor(el, cssText, source);
11529
+ try {
11530
+ const ast = csstree5.parse(cssText, { parseCustomProperty: true, positions: true });
11531
+ csstree5.walk(ast, {
11532
+ enter(node) {
11533
+ if (node.type === "Atrule") {
11534
+ parsedAtRules.add(`@${node.name}`);
11535
+ recordSelectorLoc(`@${node.name}`, node.loc);
11536
+ }
11537
+ if (node.type === "PseudoClassSelector") {
11538
+ detectedPseudoClasses.add(`:${node.name}`);
11539
+ recordSelectorLoc(`:${node.name}`, node.loc);
11540
+ }
11541
+ if (node.type === "PseudoElementSelector") {
11542
+ detectedPseudoElements.add(`::${node.name}`);
11543
+ recordSelectorLoc(`::${node.name}`, node.loc);
11544
+ }
11545
+ if (node.type === "Declaration") {
11546
+ const prop = node.property.toLowerCase();
11547
+ parsedProperties.add(prop);
11548
+ const valueStr = csstree5.generate(node.value);
11549
+ const seenValues = propertyValues.get(prop);
11550
+ if (seenValues) seenValues.push(valueStr);
11551
+ else propertyValues.set(prop, [valueStr]);
11552
+ if (node.loc) {
11553
+ if (!propertyLines.has(prop)) propertyLines.set(prop, node.loc.start.line);
11554
+ recordLoc(prop, node.loc, valueStr);
11555
+ }
11556
+ for (const det of COMPOUND_DETECTORS) {
11557
+ if (prop === det.property && valueStr.toLowerCase().includes(det.valueIncludes)) {
11558
+ parsedProperties.add(det.key);
11559
+ if (node.loc) {
11560
+ if (!propertyLines.has(det.key)) propertyLines.set(det.key, node.loc.start.line);
11561
+ recordLoc(det.key, node.loc);
11562
+ }
11563
+ }
11564
+ }
11565
+ for (const fn of CSS_FUNCTION_DETECTORS) {
11566
+ if (valueStr.includes(fn.pattern)) {
11567
+ detectedCssFunctions.add(fn.key);
11568
+ if (node.loc) {
11569
+ if (!propertyLines.has(fn.key)) propertyLines.set(fn.key, node.loc.start.line);
11570
+ recordLoc(fn.key, node.loc);
11571
+ }
11572
+ }
11573
+ }
11574
+ }
11575
+ }
11576
+ });
11577
+ } catch (e) {
11578
+ }
11579
+ });
11580
+ for (const atRule of AT_RULE_FEATURES) {
11581
+ if (!parsedAtRules.has(atRule)) continue;
11582
+ checkPropertySupport(atRule, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(atRule));
11583
+ }
11584
+ const cssPropertiesToCheck = Object.keys(CSS_SUPPORT).filter(
11585
+ (k) => !k.startsWith("<") && !k.startsWith("@")
11586
+ );
11587
+ $("[style]").each((_, el) => {
11588
+ const style = $(el).attr("style") || "";
11589
+ const props = parseStyleProperties(style);
11590
+ const selector = describeSelector2(el);
11591
+ const locs = elementLocs(locOfAttr(el, "style"));
11592
+ for (const prop of props) {
11593
+ for (const det of COMPOUND_DETECTORS) {
11594
+ if (prop === det.property) {
11595
+ const value2 = getStyleValue(style, prop);
11596
+ if (value2 == null ? void 0 : value2.toLowerCase().includes(det.valueIncludes)) {
11597
+ checkPropertySupport(det.key, addWarning, framework, selector, void 0, void 0, locs);
11598
+ }
11599
+ }
11600
+ }
11601
+ if (cssPropertiesToCheck.includes(prop)) {
11602
+ const declared = getStyleValues(style, prop);
11603
+ checkPropertySupport(
11604
+ prop,
11605
+ addWarning,
11606
+ framework,
11607
+ selector,
11608
+ void 0,
11609
+ declared.length ? declared : void 0,
11610
+ locs
11611
+ );
11612
+ }
11613
+ const value = getStyleValue(style, prop);
11614
+ if (value) {
11615
+ for (const fn of CSS_FUNCTION_DETECTORS) {
11616
+ if (value.includes(fn.pattern)) {
11617
+ checkPropertySupport(fn.key, addWarning, framework, selector, void 0, void 0, locs);
11618
+ }
11619
+ }
11072
11620
  }
11073
- } catch (e) {
11074
11621
  }
11075
11622
  });
11076
- $("[bgcolor]").each((_, el) => {
11077
- const bgcolor = $(el).attr("bgcolor") || "";
11078
- const inverted = invertColor(bgcolor, mode);
11079
- if (inverted) {
11080
- $(el).attr("bgcolor", inverted);
11623
+ for (const prop of parsedProperties) {
11624
+ if (prop.includes(":")) continue;
11625
+ if (!cssPropertiesToCheck.includes(prop)) continue;
11626
+ const values = propertyValues.get(prop);
11627
+ checkPropertySupport(
11628
+ prop,
11629
+ addWarning,
11630
+ framework,
11631
+ void 0,
11632
+ propertyLines.get(prop),
11633
+ values,
11634
+ propertyLocs.get(prop)
11635
+ );
11636
+ }
11637
+ for (const compound of COMPOUND_VALUE_FEATURES) {
11638
+ if (compound.startsWith(":") || compound.startsWith("::")) continue;
11639
+ if (parsedProperties.has(compound)) {
11640
+ checkPropertySupport(compound, addWarning, framework, void 0, propertyLines.get(compound), void 0, propertyLocs.get(compound));
11081
11641
  }
11082
- });
11642
+ }
11643
+ for (const pseudo of detectedPseudoClasses) {
11644
+ if (CSS_SUPPORT[pseudo]) {
11645
+ checkPropertySupport(pseudo, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(pseudo));
11646
+ }
11647
+ }
11648
+ for (const pseudo of detectedPseudoElements) {
11649
+ if (CSS_SUPPORT[pseudo]) {
11650
+ checkPropertySupport(pseudo, addWarning, framework, void 0, void 0, void 0, selectorLocs.get(pseudo));
11651
+ }
11652
+ }
11653
+ for (const fn of detectedCssFunctions) {
11654
+ checkPropertySupport(fn, addWarning, framework, void 0, propertyLines.get(fn), void 0, propertyLocs.get(fn));
11655
+ }
11656
+ for (const w of checkDarkModeFromDom($)) addWarning(w);
11657
+ const severityOrder = { error: 0, warning: 1, info: 2 };
11658
+ warnings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
11659
+ return warnings;
11660
+ }
11661
+ function analyzeEmail(html, framework, options) {
11662
+ if (!html || !html.trim()) {
11663
+ return [];
11664
+ }
11665
+ if (html.length > MAX_HTML_SIZE) {
11666
+ throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
11667
+ }
11668
+ const $ = loadHtml(html, options);
11669
+ return analyzeEmailFromDom($, framework, (options == null ? void 0 : options.positions) ? html : void 0);
11670
+ }
11671
+ function getFixType(prop) {
11672
+ return STRUCTURAL_FIX_PROPERTIES.has(prop) ? "structural" : "css";
11673
+ }
11674
+ function noteSuffix(notes) {
11675
+ if (!(notes == null ? void 0 : notes.length)) return "";
11676
+ const cleaned = notes.map((n) => n.replace(/^(?:Partial|Buggy|Not supported)\.\s*/i, "").trim()).filter(Boolean);
11677
+ return cleaned.length ? ` ${cleaned.join(" ")}` : "";
11678
+ }
11679
+ function checkPropertySupport(prop, addWarning, framework, selector, line, values, occurrences) {
11680
+ var _a, _b, _c, _d, _e, _f;
11681
+ const loc = occurrences == null ? void 0 : occurrences.locs[0];
11682
+ const reportedLine = (_a = loc == null ? void 0 : loc.line) != null ? _a : line;
11683
+ const supportData = CSS_SUPPORT[prop];
11684
+ if (!supportData) return;
11685
+ const fixType = getFixType(prop);
11686
+ for (const client of EMAIL_CLIENTS) {
11687
+ const support = supportData[client.id] || "unknown";
11688
+ const notes = (_b = CSS_SUPPORT_NOTES[prop]) == null ? void 0 : _b[client.id];
11689
+ if (support === "unsupported") {
11690
+ const sug = getSuggestion(prop, client.id, framework);
11691
+ const fix = getCodeFix(prop, client.id, framework);
11692
+ addWarning(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
11693
+ severity: "warning",
11694
+ client: client.id,
11695
+ property: prop,
11696
+ message: `${client.name} does not support "${prop}".${noteSuffix(notes)}`,
11697
+ suggestion: sug.text,
11698
+ fix,
11699
+ fixType
11700
+ }, selector ? { selector } : {}), reportedLine !== void 0 ? { line: reportedLine } : {}), occurrences ? occurrenceFields(occurrences) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
11701
+ } else if (support === "partial") {
11702
+ if (!caveatApplies(prop, values, notes)) continue;
11703
+ const hits = triggeringOccurrences(prop, occurrences, notes);
11704
+ const sug = getSuggestion(prop, client.id, framework);
11705
+ const fix = getCodeFix(prop, client.id, framework);
11706
+ addWarning(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
11707
+ severity: "info",
11708
+ client: client.id,
11709
+ property: prop,
11710
+ message: `${client.name} has partial support for "${prop}".${noteSuffix(notes)}`,
11711
+ suggestion: sug.text,
11712
+ fix,
11713
+ fixType
11714
+ }, selector ? { selector } : {}), ((_d = (_c = hits == null ? void 0 : hits.locs[0]) == null ? void 0 : _c.line) != null ? _d : reportedLine) !== void 0 ? { line: (_f = (_e = hits == null ? void 0 : hits.locs[0]) == null ? void 0 : _e.line) != null ? _f : reportedLine } : {}), hits ? occurrenceFields(hits) : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
11715
+ }
11716
+ }
11717
+ }
11718
+ function generateCompatibilityScore(warnings) {
11719
+ const result = {};
11720
+ for (const client of EMAIL_CLIENTS) {
11721
+ const clientWarnings = warnings.filter((w) => w.client === client.id);
11722
+ const errorProps = new Set(clientWarnings.filter((w) => w.severity === "error").map((w) => w.property));
11723
+ const warnProps = new Set(clientWarnings.filter((w) => w.severity === "warning").map((w) => w.property));
11724
+ const infoProps = new Set(clientWarnings.filter((w) => w.severity === "info").map((w) => w.property));
11725
+ const errors = errorProps.size;
11726
+ const warns = warnProps.size;
11727
+ const info = infoProps.size;
11728
+ const score = Math.max(0, Math.min(100, 100 - errors * 10 - warns * 3));
11729
+ result[client.id] = { score, errors, warnings: warns, info };
11730
+ }
11731
+ return result;
11732
+ }
11733
+ function occurrenceFields({ locs, truncated }) {
11734
+ return __spreadValues({ loc: locs[0], locs: [...locs] }, truncated ? { locsTruncated: true } : {});
11735
+ }
11736
+ function triggeringOccurrences(prop, occurrences, notes) {
11737
+ const values = occurrences == null ? void 0 : occurrences.values;
11738
+ if (!occurrences || !values) return occurrences;
11739
+ const locs = occurrences.locs.filter((_, i) => caveatApplies(prop, [values[i]], notes));
11740
+ if (!locs.length || locs.length === occurrences.locs.length) return occurrences;
11741
+ return __spreadValues({ locs }, occurrences.truncated ? { truncated: true } : {});
11742
+ }
11743
+ function elementLocs(loc) {
11744
+ return loc ? { locs: [loc] } : void 0;
11745
+ }
11746
+ function warningsForClient(warnings, clientId) {
11747
+ return warnings.filter((w) => w.client === clientId);
11748
+ }
11749
+ function errorWarnings(warnings) {
11750
+ return warnings.filter((w) => w.severity === "error");
11751
+ }
11752
+ function structuralWarnings(warnings) {
11753
+ return warnings.filter((w) => w.fixType === "structural");
11083
11754
  }
11084
11755
 
11085
11756
  // src/diff.ts
@@ -11412,16 +12083,6 @@ function extractCode(response) {
11412
12083
  return response.trim();
11413
12084
  }
11414
12085
 
11415
- // src/parse-html.ts
11416
- var cheerio5 = __toESM(require("cheerio"), 1);
11417
- function fromHtml(html, empty, fn) {
11418
- if (!html || !html.trim()) return empty;
11419
- if (html.length > MAX_HTML_SIZE) {
11420
- throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
11421
- }
11422
- return fn(cheerio5.load(html), html);
11423
- }
11424
-
11425
12086
  // src/spam-scorer.ts
11426
12087
  var SPAM_TRIGGER_PHRASES = [
11427
12088
  "act now",
@@ -11890,6 +12551,8 @@ function validateLinksFromDom($) {
11890
12551
  const href = $(el).attr("href") || "";
11891
12552
  const text = $(el).text().trim();
11892
12553
  const category = classifyHref(href);
12554
+ const elLoc = locOfElement(el);
12555
+ const hrefLoc = href ? locOfAttr(el, "href") : elLoc;
11893
12556
  switch (category) {
11894
12557
  case "https":
11895
12558
  breakdown.https++;
@@ -11920,95 +12583,95 @@ function validateLinksFromDom($) {
11920
12583
  hrefCounts.set(href, (hrefCounts.get(href) || 0) + 1);
11921
12584
  }
11922
12585
  if (!href || !href.trim()) {
11923
- issues.push({
12586
+ issues.push(__spreadValues({
11924
12587
  severity: "error",
11925
12588
  rule: "empty-href",
11926
12589
  message: "Link has no href attribute",
11927
12590
  text: text.slice(0, 80) || "(no text)"
11928
- });
12591
+ }, elLoc ? { loc: elLoc } : {}));
11929
12592
  return;
11930
12593
  }
11931
12594
  if (category === "javascript" && !isPlaceholderHref(href)) {
11932
- issues.push({
12595
+ issues.push(__spreadValues({
11933
12596
  severity: "error",
11934
12597
  rule: "javascript-href",
11935
12598
  message: "Link uses javascript: protocol",
11936
12599
  href: href.slice(0, 100),
11937
12600
  text: text.slice(0, 80) || "(no text)"
11938
- });
12601
+ }, hrefLoc ? { loc: hrefLoc } : {}));
11939
12602
  return;
11940
12603
  }
11941
12604
  if (isPlaceholderHref(href)) {
11942
- issues.push({
12605
+ issues.push(__spreadValues({
11943
12606
  severity: "warning",
11944
12607
  rule: "placeholder-href",
11945
12608
  message: "Link has a placeholder href (# or javascript:void)",
11946
12609
  href,
11947
12610
  text: text.slice(0, 80) || "(no text)"
11948
- });
12611
+ }, hrefLoc ? { loc: hrefLoc } : {}));
11949
12612
  return;
11950
12613
  }
11951
12614
  if (category === "http") {
11952
- issues.push({
12615
+ issues.push(__spreadValues({
11953
12616
  severity: "warning",
11954
12617
  rule: "insecure-link",
11955
12618
  message: "Link uses HTTP instead of HTTPS",
11956
12619
  href: href.slice(0, 120),
11957
12620
  text: text.slice(0, 80) || "(no text)"
11958
- });
12621
+ }, hrefLoc ? { loc: hrefLoc } : {}));
11959
12622
  }
11960
12623
  if (category === "protocol-relative") {
11961
- issues.push({
12624
+ issues.push(__spreadValues({
11962
12625
  severity: "warning",
11963
12626
  rule: "protocol-relative",
11964
12627
  message: "Protocol-relative URL may break in email clients \u2014 use https:// explicitly",
11965
12628
  href: href.slice(0, 120),
11966
12629
  text: text.slice(0, 80) || "(no text)"
11967
- });
12630
+ }, hrefLoc ? { loc: hrefLoc } : {}));
11968
12631
  }
11969
12632
  if (text && GENERIC_LINK_TEXT.has(text.toLowerCase())) {
11970
- issues.push({
12633
+ issues.push(__spreadValues({
11971
12634
  severity: "warning",
11972
12635
  rule: "generic-link-text",
11973
12636
  message: `Link text "${text}" is vague \u2014 use descriptive text for accessibility and engagement`,
11974
12637
  href: href.slice(0, 120),
11975
12638
  text
11976
- });
12639
+ }, elLoc ? { loc: elLoc } : {}));
11977
12640
  }
11978
12641
  if (!text && !$(el).attr("aria-label") && !$(el).find("img[alt]").length) {
11979
- issues.push({
12642
+ issues.push(__spreadValues({
11980
12643
  severity: "error",
11981
12644
  rule: "empty-link-text",
11982
12645
  message: "Link has no visible text or aria-label",
11983
12646
  href: href.slice(0, 120)
11984
- });
12647
+ }, elLoc ? { loc: elLoc } : {}));
11985
12648
  }
11986
12649
  if (category === "mailto" && href.trim().toLowerCase() === "mailto:") {
11987
- issues.push({
12650
+ issues.push(__spreadValues({
11988
12651
  severity: "error",
11989
12652
  rule: "empty-mailto",
11990
12653
  message: "mailto: link has no email address",
11991
12654
  href,
11992
12655
  text: text.slice(0, 80) || "(no text)"
11993
- });
12656
+ }, hrefLoc ? { loc: hrefLoc } : {}));
11994
12657
  }
11995
12658
  if (category === "tel" && href.trim().toLowerCase() === "tel:") {
11996
- issues.push({
12659
+ issues.push(__spreadValues({
11997
12660
  severity: "error",
11998
12661
  rule: "empty-tel",
11999
12662
  message: "tel: link has no phone number",
12000
12663
  href,
12001
12664
  text: text.slice(0, 80) || "(no text)"
12002
- });
12665
+ }, hrefLoc ? { loc: hrefLoc } : {}));
12003
12666
  }
12004
12667
  if (href.length > 2e3) {
12005
- issues.push({
12668
+ issues.push(__spreadValues({
12006
12669
  severity: "info",
12007
12670
  rule: "long-url",
12008
12671
  message: "URL exceeds 2000 characters \u2014 may be truncated by some email clients",
12009
12672
  href: href.slice(0, 120) + "...",
12010
12673
  text: text.slice(0, 80) || "(no text)"
12011
- });
12674
+ }, hrefLoc ? { loc: hrefLoc } : {}));
12012
12675
  }
12013
12676
  });
12014
12677
  links.each((_, el) => {
@@ -12017,14 +12680,15 @@ function validateLinksFromDom($) {
12017
12680
  if (trimmed.startsWith("#") && trimmed.length > 1) {
12018
12681
  const targetId = trimmed.slice(1);
12019
12682
  const target = $(`[id="${targetId}"]`);
12683
+ const anchorLoc = locOfAttr(el, "href");
12020
12684
  if (target.length === 0) {
12021
- issues.push({
12685
+ issues.push(__spreadValues({
12022
12686
  severity: "error",
12023
12687
  rule: "broken-anchor",
12024
12688
  message: `Anchor link "${trimmed}" points to an element that does not exist`,
12025
12689
  href: trimmed,
12026
12690
  text: $(el).text().trim().slice(0, 80) || "(no text)"
12027
- });
12691
+ }, anchorLoc ? { loc: anchorLoc } : {}));
12028
12692
  }
12029
12693
  }
12030
12694
  });
@@ -12040,8 +12704,8 @@ function validateLinksFromDom($) {
12040
12704
  }
12041
12705
  return { totalLinks, issues, breakdown };
12042
12706
  }
12043
- function validateLinks(html) {
12044
- return fromHtml(html, EMPTY_LINKS, validateLinksFromDom);
12707
+ function validateLinks(html, options) {
12708
+ return fromHtml(html, EMPTY_LINKS, validateLinksFromDom, options);
12045
12709
  }
12046
12710
 
12047
12711
  // src/accessibility-checker.ts
@@ -12066,24 +12730,31 @@ function describeElement($, el) {
12066
12730
  function checkLangAttribute($) {
12067
12731
  const lang = $("html").attr("lang");
12068
12732
  if (!lang || !lang.trim()) {
12069
- return {
12733
+ const loc = locOfFirst($, "html");
12734
+ return __spreadProps(__spreadValues({
12070
12735
  severity: "error",
12071
12736
  rule: "missing-lang",
12072
- message: "Missing lang attribute on <html> element",
12737
+ message: "Missing lang attribute on <html> element"
12738
+ }, loc ? { loc } : {}), {
12073
12739
  details: 'Screen readers use the lang attribute to determine pronunciation. Add lang="en" (or appropriate language code).'
12074
- };
12740
+ });
12075
12741
  }
12076
12742
  return null;
12077
12743
  }
12744
+ function titleLoc($) {
12745
+ return $("title").length ? locOfFirst($, "title") : locOfFirst($, "head");
12746
+ }
12078
12747
  function checkTitle($) {
12079
12748
  const title = $("title").text().trim();
12080
12749
  if (!title) {
12081
- return {
12750
+ const loc = titleLoc($);
12751
+ return __spreadProps(__spreadValues({
12082
12752
  severity: "warning",
12083
12753
  rule: "missing-title",
12084
- message: "Missing or empty <title> element",
12754
+ message: "Missing or empty <title> element"
12755
+ }, loc ? { loc } : {}), {
12085
12756
  details: "The <title> helps screen readers identify the email content."
12086
- };
12757
+ });
12087
12758
  }
12088
12759
  return null;
12089
12760
  }
@@ -12093,34 +12764,38 @@ function checkImageAlt($) {
12093
12764
  const alt = $(el).attr("alt");
12094
12765
  const src = $(el).attr("src") || "";
12095
12766
  const role = $(el).attr("role");
12767
+ const elLoc = locOfElement(el);
12096
12768
  if (role === "presentation" || role === "none") return;
12097
12769
  if (alt === void 0) {
12098
- issues.push({
12770
+ issues.push(__spreadProps(__spreadValues({
12099
12771
  severity: "error",
12100
12772
  rule: "img-missing-alt",
12101
12773
  message: "Image missing alt attribute",
12102
- element: describeElement($, el),
12774
+ element: describeElement($, el)
12775
+ }, elLoc ? { loc: elLoc } : {}), {
12103
12776
  details: 'Every image must have an alt attribute. Use alt="" for decorative images.'
12104
- });
12777
+ }));
12105
12778
  } else if (alt.trim() === "") {
12106
12779
  const isLikelyContent = !src.includes("spacer") && !src.includes("pixel") && !src.includes("tracking") && !src.includes("1x1") && !src.includes("transparent");
12107
12780
  if (isLikelyContent && ($(el).attr("width") || "0") !== "1") {
12108
- issues.push({
12781
+ issues.push(__spreadProps(__spreadValues({
12109
12782
  severity: "info",
12110
12783
  rule: "img-empty-alt",
12111
12784
  message: "Image has empty alt text \u2014 verify it is decorative",
12112
- element: describeElement($, el),
12785
+ element: describeElement($, el)
12786
+ }, locOfAttr(el, "alt") ? { loc: locOfAttr(el, "alt") } : {}), {
12113
12787
  details: "Empty alt is correct for decorative images, but content images need descriptive alt text."
12114
- });
12788
+ }));
12115
12789
  }
12116
12790
  } else if (/\.(png|jpg|jpeg|gif|svg|webp|bmp)$/i.test(alt)) {
12117
- issues.push({
12791
+ issues.push(__spreadProps(__spreadValues({
12118
12792
  severity: "error",
12119
12793
  rule: "img-filename-alt",
12120
12794
  message: "Image alt text is a filename, not a description",
12121
- element: describeElement($, el),
12795
+ element: describeElement($, el)
12796
+ }, locOfAttr(el, "alt") ? { loc: locOfAttr(el, "alt") } : {}), {
12122
12797
  details: `Alt "${alt}" should describe the image content, not the file name.`
12123
- });
12798
+ }));
12124
12799
  }
12125
12800
  });
12126
12801
  return issues;
@@ -12128,28 +12803,31 @@ function checkImageAlt($) {
12128
12803
  function checkLinkAccessibility($) {
12129
12804
  const issues = [];
12130
12805
  $("a").each((_, el) => {
12806
+ const elLoc = locOfElement(el);
12131
12807
  const text = $(el).text().trim().toLowerCase();
12132
12808
  const ariaLabel = $(el).attr("aria-label");
12133
12809
  const title = $(el).attr("title");
12134
12810
  const imgAlt = $(el).find("img").attr("alt");
12135
12811
  if (!text && !ariaLabel && !title && !imgAlt) {
12136
- issues.push({
12812
+ issues.push(__spreadProps(__spreadValues({
12137
12813
  severity: "error",
12138
12814
  rule: "link-no-accessible-name",
12139
12815
  message: "Link has no accessible name",
12140
- element: describeElement($, el),
12816
+ element: describeElement($, el)
12817
+ }, elLoc ? { loc: elLoc } : {}), {
12141
12818
  details: "Links need visible text, aria-label, or an image with alt text."
12142
- });
12819
+ }));
12143
12820
  return;
12144
12821
  }
12145
12822
  if (text && GENERIC_LINK_TEXT.has(text) && !ariaLabel) {
12146
- issues.push({
12823
+ issues.push(__spreadProps(__spreadValues({
12147
12824
  severity: "warning",
12148
12825
  rule: "link-generic-text",
12149
12826
  message: `Link text "${$(el).text().trim()}" is not descriptive`,
12150
- element: describeElement($, el),
12827
+ element: describeElement($, el)
12828
+ }, elLoc ? { loc: elLoc } : {}), {
12151
12829
  details: "Screen readers often list links out of context. Use text that describes the destination."
12152
- });
12830
+ }));
12153
12831
  }
12154
12832
  });
12155
12833
  return issues;
@@ -12159,18 +12837,20 @@ function checkTableAccessibility($) {
12159
12837
  $("table").each((_, el) => {
12160
12838
  if ($(el).parents('table[role="presentation"], table[role="none"]').length > 0) return;
12161
12839
  const role = $(el).attr("role");
12840
+ const tableLoc = locOfElement(el);
12162
12841
  const hasHeaders = $(el).find("th").length > 0;
12163
12842
  const looksLikeLayout = !hasHeaders;
12164
12843
  if (looksLikeLayout && role !== "presentation" && role !== "none") {
12165
12844
  const nestedTables = $(el).find("table").length;
12166
12845
  if (nestedTables > 0 || $(el).find("td").length > 2) {
12167
- issues.push({
12846
+ issues.push(__spreadProps(__spreadValues({
12168
12847
  severity: "info",
12169
12848
  rule: "table-missing-role",
12170
- message: 'Layout table missing role="presentation"',
12849
+ message: 'Layout table missing role="presentation"'
12850
+ }, tableLoc ? { loc: tableLoc } : {}), {
12171
12851
  element: `<table> with ${$(el).find("td").length} cells`,
12172
12852
  details: `Add role="presentation" to tables used for layout so screen readers don't announce them as data tables.`
12173
- });
12853
+ }));
12174
12854
  }
12175
12855
  }
12176
12856
  });
@@ -12181,6 +12861,7 @@ function checkTextSizeAndContrast($) {
12181
12861
  let smallTextCount = 0;
12182
12862
  $("[style]").each((_, el) => {
12183
12863
  const style = $(el).attr("style") || "";
12864
+ const styleLoc = locOfAttr(el, "style");
12184
12865
  const fontSizeMatch = style.match(/font-size\s*:\s*(\d+(?:\.\d+)?)(px|pt)/i);
12185
12866
  if (fontSizeMatch) {
12186
12867
  const size = parseFloat(fontSizeMatch[1]);
@@ -12189,13 +12870,14 @@ function checkTextSizeAndContrast($) {
12189
12870
  if (pxSize < 9 && pxSize > 0) {
12190
12871
  smallTextCount++;
12191
12872
  if (smallTextCount <= 3) {
12192
- issues.push({
12873
+ issues.push(__spreadProps(__spreadValues({
12193
12874
  severity: "warning",
12194
12875
  rule: "small-text",
12195
12876
  message: `Very small text (${fontSizeMatch[0].trim()})`,
12196
- element: describeElement($, el),
12877
+ element: describeElement($, el)
12878
+ }, styleLoc ? { loc: styleLoc } : {}), {
12197
12879
  details: "Text smaller than 9px is difficult to read, especially on mobile devices."
12198
- });
12880
+ }));
12199
12881
  }
12200
12882
  }
12201
12883
  }
@@ -12240,21 +12922,23 @@ function checkTextSizeAndContrast($) {
12240
12922
  }
12241
12923
  const grade = wcagGrade(ratio);
12242
12924
  if (grade === "Fail") {
12243
- issues.push({
12925
+ issues.push(__spreadProps(__spreadValues({
12244
12926
  severity: "error",
12245
12927
  rule: "low-contrast",
12246
12928
  message: `Low contrast ratio ${ratio.toFixed(1)}:1 \u2014 fails WCAG minimum`,
12247
- element: describeElement($, el),
12929
+ element: describeElement($, el)
12930
+ }, styleLoc ? { loc: styleLoc } : {}), {
12248
12931
  details: `Foreground ${colorValue} on background needs at least ${isLargeText ? "3:1" : "4.5:1"} contrast ratio.`
12249
- });
12932
+ }));
12250
12933
  } else if (!isLargeText && grade === "AA Large") {
12251
- issues.push({
12934
+ issues.push(__spreadProps(__spreadValues({
12252
12935
  severity: "warning",
12253
12936
  rule: "low-contrast",
12254
12937
  message: `Low contrast ratio ${ratio.toFixed(1)}:1 \u2014 fails WCAG AA for normal text`,
12255
- element: describeElement($, el),
12938
+ element: describeElement($, el)
12939
+ }, styleLoc ? { loc: styleLoc } : {}), {
12256
12940
  details: `Foreground ${colorValue} on background needs at least 4.5:1 for normal-sized text.`
12257
- });
12941
+ }));
12258
12942
  }
12259
12943
  }
12260
12944
  }
@@ -12277,29 +12961,32 @@ function checkCharsetDeclaration($) {
12277
12961
  const content = httpEquiv.attr("content") || "";
12278
12962
  if (/charset\s*=/i.test(content)) return null;
12279
12963
  }
12280
- return {
12964
+ const loc = locOfFirst($, "head");
12965
+ return __spreadProps(__spreadValues({
12281
12966
  severity: "warning",
12282
12967
  rule: "missing-charset",
12283
- message: "Missing charset declaration",
12968
+ message: "Missing charset declaration"
12969
+ }, loc ? { loc } : {}), {
12284
12970
  details: 'Add <meta charset="utf-8"> in <head> to prevent encoding issues across email clients.'
12285
- };
12971
+ });
12286
12972
  }
12287
12973
  function checkSemanticStructure($) {
12288
12974
  const issues = [];
12289
12975
  const headings = [];
12290
12976
  $("h1, h2, h3, h4, h5, h6").each((_, el) => {
12291
12977
  const level = parseInt(el.tagName.replace(/h/i, ""), 10);
12292
- headings.push({ level, text: $(el).text().trim().slice(0, 60) });
12978
+ headings.push({ level, text: $(el).text().trim().slice(0, 60), loc: locOfElement(el) });
12293
12979
  });
12294
12980
  for (let i = 1; i < headings.length; i++) {
12295
12981
  const gap = headings[i].level - headings[i - 1].level;
12296
12982
  if (gap > 1) {
12297
- issues.push({
12983
+ issues.push(__spreadProps(__spreadValues({
12298
12984
  severity: "info",
12299
12985
  rule: "heading-skip",
12300
- message: `Heading level skipped: h${headings[i - 1].level} to h${headings[i].level}`,
12986
+ message: `Heading level skipped: h${headings[i - 1].level} to h${headings[i].level}`
12987
+ }, headings[i].loc ? { loc: headings[i].loc } : {}), {
12301
12988
  details: "Skipped heading levels can confuse screen readers. Use sequential heading levels."
12302
- });
12989
+ }));
12303
12990
  break;
12304
12991
  }
12305
12992
  }
@@ -12340,8 +13027,8 @@ function checkAccessibilityFromDom($) {
12340
13027
  const score = Math.max(0, 100 - penalty);
12341
13028
  return { score, issues };
12342
13029
  }
12343
- function checkAccessibility(html) {
12344
- return fromHtml(html, EMPTY_ACCESSIBILITY, checkAccessibilityFromDom);
13030
+ function checkAccessibility(html, options) {
13031
+ return fromHtml(html, EMPTY_ACCESSIBILITY, checkAccessibilityFromDom, options);
12345
13032
  }
12346
13033
 
12347
13034
  // src/image-analyzer.ts
@@ -12388,6 +13075,8 @@ function analyzeImagesFromDom($) {
12388
13075
  const height = (_c = img.attr("height")) != null ? _c : null;
12389
13076
  const style = (img.attr("style") || "").toLowerCase();
12390
13077
  const imgIssues = [];
13078
+ const elLoc = locOfElement(el);
13079
+ const srcLoc = src ? locOfAttr(el, "src") : elLoc;
12391
13080
  const tracking = isTrackingPixel(img);
12392
13081
  let dataUriBytes = 0;
12393
13082
  if (src.startsWith("data:")) {
@@ -12411,59 +13100,59 @@ function analyzeImagesFromDom($) {
12411
13100
  const hasStyleHeight = /height\s*:/.test(style);
12412
13101
  if (!hasStyleWidth && !hasStyleHeight) {
12413
13102
  imgIssues.push("missing-dimensions");
12414
- issues.push({
13103
+ issues.push(__spreadValues({
12415
13104
  rule: "missing-dimensions",
12416
13105
  severity: "warning",
12417
13106
  message: "Image missing width/height attributes \u2014 causes layout shifts and Outlook rendering issues.",
12418
13107
  src: truncateSrc(src)
12419
- });
13108
+ }, elLoc ? { loc: elLoc } : {}));
12420
13109
  }
12421
13110
  }
12422
13111
  if (dataUriBytes > DATA_URI_WARN_BYTES) {
12423
13112
  const kb = Math.round(dataUriBytes / 1024);
12424
13113
  imgIssues.push("large-data-uri");
12425
- issues.push({
13114
+ issues.push(__spreadValues({
12426
13115
  rule: "large-data-uri",
12427
13116
  severity: "warning",
12428
13117
  message: `Data URI is ${kb}KB \u2014 consider hosting the image externally to reduce email size.`,
12429
13118
  src: truncateSrc(src)
12430
- });
13119
+ }, srcLoc ? { loc: srcLoc } : {}));
12431
13120
  }
12432
13121
  if (alt === null) {
12433
13122
  imgIssues.push("missing-alt");
12434
- issues.push({
13123
+ issues.push(__spreadValues({
12435
13124
  rule: "missing-alt",
12436
13125
  severity: "warning",
12437
13126
  message: "Image missing alt attribute \u2014 hurts deliverability and accessibility.",
12438
13127
  src: truncateSrc(src)
12439
- });
13128
+ }, elLoc ? { loc: elLoc } : {}));
12440
13129
  }
12441
13130
  if (src.toLowerCase().endsWith(".webp") || src.includes("image/webp")) {
12442
13131
  imgIssues.push("webp-format");
12443
- issues.push({
13132
+ issues.push(__spreadValues({
12444
13133
  rule: "webp-format",
12445
13134
  severity: "info",
12446
13135
  message: "WebP format detected \u2014 not supported by all email clients. Consider PNG or JPEG.",
12447
13136
  src: truncateSrc(src)
12448
- });
13137
+ }, srcLoc ? { loc: srcLoc } : {}));
12449
13138
  }
12450
13139
  if (src.toLowerCase().endsWith(".svg") || src.includes("image/svg")) {
12451
13140
  imgIssues.push("svg-format");
12452
- issues.push({
13141
+ issues.push(__spreadValues({
12453
13142
  rule: "svg-format",
12454
13143
  severity: "info",
12455
13144
  message: "SVG format detected \u2014 not supported by most email clients. Use PNG instead.",
12456
13145
  src: truncateSrc(src)
12457
- });
13146
+ }, srcLoc ? { loc: srcLoc } : {}));
12458
13147
  }
12459
13148
  if (!style.includes("display:block") && !style.includes("display: block")) {
12460
13149
  imgIssues.push("missing-display-block");
12461
- issues.push({
13150
+ issues.push(__spreadValues({
12462
13151
  rule: "missing-display-block",
12463
13152
  severity: "info",
12464
13153
  message: "Image without display:block \u2014 may cause unwanted gaps in Outlook.",
12465
13154
  src: truncateSrc(src)
12466
- });
13155
+ }, elLoc ? { loc: elLoc } : {}));
12467
13156
  }
12468
13157
  images.push({
12469
13158
  src: truncateSrc(src),
@@ -12501,8 +13190,8 @@ function analyzeImagesFromDom($) {
12501
13190
  }
12502
13191
  return { total: images.length, totalDataUriBytes, issues, images };
12503
13192
  }
12504
- function analyzeImages(html) {
12505
- return fromHtml(html, EMPTY_IMAGES, analyzeImagesFromDom);
13193
+ function analyzeImages(html, options) {
13194
+ return fromHtml(html, EMPTY_IMAGES, analyzeImagesFromDom, options);
12506
13195
  }
12507
13196
 
12508
13197
  // src/inbox-preview.ts
@@ -12720,11 +13409,57 @@ function checkSize(html) {
12720
13409
  return fromHtml(html, EMPTY_SIZE, checkSizeFromDom);
12721
13410
  }
12722
13411
 
13412
+ // src/dom-text.ts
13413
+ function visibleTextNodes($) {
13414
+ var _a, _b, _c, _d;
13415
+ const nodes = [];
13416
+ const stack = [...(_b = (_a = $.root()[0]) == null ? void 0 : _a.children) != null ? _b : []].reverse();
13417
+ while (stack.length > 0) {
13418
+ const node = stack.pop();
13419
+ const tag = (_c = node.tagName) == null ? void 0 : _c.toLowerCase();
13420
+ if (tag === "style" || tag === "script" || tag === "head") continue;
13421
+ if (node.type === "text") {
13422
+ nodes.push(node);
13423
+ continue;
13424
+ }
13425
+ const children = (_d = node.children) != null ? _d : [];
13426
+ for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]);
13427
+ }
13428
+ return nodes;
13429
+ }
13430
+
12723
13431
  // src/template-checker.ts
12724
- function checkTemplateVariablesFromDom($) {
13432
+ function checkTemplateVariablesFromDom($, source) {
13433
+ var _a;
12725
13434
  const issues = [];
12726
13435
  const seen = /* @__PURE__ */ new Set();
12727
- const textContent = extractTextContent($);
13436
+ const textNodes = visibleTextNodes($);
13437
+ const positioned = textNodes.some((n) => n.sourceCodeLocation);
13438
+ for (const node of positioned ? textNodes : []) {
13439
+ const data = (_a = node.data) != null ? _a : "";
13440
+ for (const [pattern, label] of TEMPLATE_VARIABLE_PATTERNS) {
13441
+ pattern.lastIndex = 0;
13442
+ let match;
13443
+ while ((match = pattern.exec(data)) !== null) {
13444
+ const variable = match[0];
13445
+ const key = `text:${variable}`;
13446
+ if (seen.has(key)) continue;
13447
+ seen.add(key);
13448
+ const loc = locInTextNode(node, match.index, variable.length, source);
13449
+ issues.push(__spreadValues({
13450
+ rule: "unresolved-variable",
13451
+ severity: "error",
13452
+ message: `Unresolved ${label} variable "${variable}" found in text content.`,
13453
+ variable,
13454
+ location: "text"
13455
+ }, loc ? { loc } : {}));
13456
+ }
13457
+ }
13458
+ }
13459
+ const textContent = textNodes.map((n) => {
13460
+ var _a2;
13461
+ return (_a2 = n.data) != null ? _a2 : "";
13462
+ }).join("");
12728
13463
  for (const [pattern, label] of TEMPLATE_VARIABLE_PATTERNS) {
12729
13464
  pattern.lastIndex = 0;
12730
13465
  let match;
@@ -12757,13 +13492,14 @@ function checkTemplateVariablesFromDom($) {
12757
13492
  const key = `attr:${attr}:${variable}`;
12758
13493
  if (seen.has(key)) continue;
12759
13494
  seen.add(key);
12760
- issues.push({
13495
+ const loc = locOfAttr(el, attr);
13496
+ issues.push(__spreadValues({
12761
13497
  rule: "unresolved-variable",
12762
13498
  severity: "error",
12763
13499
  message: `Unresolved ${label} variable "${variable}" found in ${attr} attribute.`,
12764
13500
  variable,
12765
13501
  location: "attribute"
12766
- });
13502
+ }, loc ? { loc } : {}));
12767
13503
  }
12768
13504
  }
12769
13505
  }
@@ -12771,17 +13507,17 @@ function checkTemplateVariablesFromDom($) {
12771
13507
  }
12772
13508
  return { unresolvedCount: issues.length, issues };
12773
13509
  }
12774
- function extractTextContent($) {
12775
- const clone = $.root().clone();
12776
- clone.find("style, script, head").remove();
12777
- return clone.text();
12778
- }
12779
- function checkTemplateVariables(html) {
12780
- return fromHtml(html, EMPTY_TEMPLATE, checkTemplateVariablesFromDom);
13510
+ function checkTemplateVariables(html, options) {
13511
+ return fromHtml(
13512
+ html,
13513
+ EMPTY_TEMPLATE,
13514
+ ($, h) => checkTemplateVariablesFromDom($, (options == null ? void 0 : options.positions) ? h : void 0),
13515
+ options
13516
+ );
12781
13517
  }
12782
13518
 
12783
13519
  // src/overflow-checker.ts
12784
- var csstree5 = __toESM(require("css-tree"), 1);
13520
+ var csstree6 = __toESM(require("css-tree"), 1);
12785
13521
  function fixedPxWidth($el) {
12786
13522
  const style = $el.attr("style") || "";
12787
13523
  const styleMatch = style.match(/(?:^|[;\s])width\s*:\s*(\d+)px/i);
@@ -12793,89 +13529,142 @@ function fixedPxWidth($el) {
12793
13529
  function isFluid(style) {
12794
13530
  return /max-width\s*:\s*100%/i.test(style) || /width\s*:\s*100%/i.test(style);
12795
13531
  }
12796
- function addWidthIssue(width, label, issues, seen) {
13532
+ function addWidthIssue(width, label, issues, seen, loc) {
12797
13533
  const key = `w:${label}:${width}`;
12798
- if (seen.has(key)) return;
12799
- seen.add(key);
12800
- issues.push({
13534
+ const existing = seen.get(key);
13535
+ if (existing) {
13536
+ addOccurrence(existing, loc);
13537
+ return;
13538
+ }
13539
+ const issue = __spreadValues({
12801
13540
  rule: "fixed-width-overflow",
12802
13541
  severity: "warning",
12803
13542
  message: `${label} has a fixed width of ${width}px, wider than the ${EMAIL_MAX_WIDTH}px email frame \u2014 it will force horizontal scrolling, especially on mobile.`,
12804
13543
  detail: `Use width:100% with max-width:${EMAIL_MAX_WIDTH}px instead of a fixed width beyond the frame.`
12805
- });
13544
+ }, loc ? { loc, locs: [loc] } : {});
13545
+ seen.set(key, issue);
13546
+ issues.push(issue);
13547
+ }
13548
+ function locateInNodes(nodes, starts, index, length, source) {
13549
+ var _a;
13550
+ let lo = 0;
13551
+ let hi = starts.length - 1;
13552
+ let found = -1;
13553
+ while (lo <= hi) {
13554
+ const mid = lo + hi >> 1;
13555
+ if (starts[mid] <= index) {
13556
+ found = mid;
13557
+ lo = mid + 1;
13558
+ } else {
13559
+ hi = mid - 1;
13560
+ }
13561
+ }
13562
+ if (found === -1) return void 0;
13563
+ const node = nodes[found];
13564
+ const within = index - starts[found];
13565
+ const available = ((_a = node.data) != null ? _a : "").length - within;
13566
+ return locInTextNode(node, within, Math.min(length, available), source);
13567
+ }
13568
+ function addOccurrence(issue, loc) {
13569
+ if (!loc || !issue.locs) return;
13570
+ if (issue.locs.some((l) => l.offset === loc.offset)) return;
13571
+ if (issue.locs.length >= MAX_WARNING_LOCATIONS) {
13572
+ issue.locsTruncated = true;
13573
+ return;
13574
+ }
13575
+ issue.locs.push(loc);
12806
13576
  }
12807
- function checkOverflowFromDom($) {
13577
+ function checkOverflowFromDom($, source) {
13578
+ var _a;
12808
13579
  const issues = [];
12809
- const seen = /* @__PURE__ */ new Set();
13580
+ const seen = /* @__PURE__ */ new Map();
13581
+ const tokensSeen = /* @__PURE__ */ new Set();
12810
13582
  $("[width], [style*='width']").each((_, el) => {
12811
13583
  const $el = $(el);
12812
13584
  const width = fixedPxWidth($el);
12813
13585
  if (width === null || width <= EMAIL_MAX_WIDTH) return;
12814
13586
  if (isFluid($el.attr("style") || "")) return;
12815
13587
  const tag = (el.tagName || "element").toLowerCase();
12816
- addWidthIssue(width, `<${tag}>`, issues, seen);
13588
+ const fromStyle = /(?:^|[;\s])width\s*:\s*\d+px/i.test($el.attr("style") || "");
13589
+ addWidthIssue(width, `<${tag}>`, issues, seen, locOfAttr(el, fromStyle ? "style" : "width"));
12817
13590
  });
12818
13591
  $("style").each((_, el) => {
13592
+ const cssText = $(el).text();
13593
+ const anchor = cssBlockAnchor(el, cssText, source);
12819
13594
  let ast;
12820
13595
  try {
12821
- ast = csstree5.parse($(el).text());
13596
+ ast = csstree6.parse(cssText, { positions: true });
12822
13597
  } catch (e) {
12823
13598
  return;
12824
13599
  }
12825
- csstree5.walk(ast, {
13600
+ csstree6.walk(ast, {
12826
13601
  visit: "Rule",
12827
13602
  enter(node) {
12828
13603
  if (node.type !== "Rule") return;
12829
13604
  let widthPx = null;
12830
13605
  let fluid = false;
13606
+ let widthLoc;
12831
13607
  node.block.children.forEach((child) => {
12832
13608
  if (child.type !== "Declaration") return;
12833
13609
  const prop = child.property.toLowerCase();
12834
- const val = csstree5.generate(child.value);
13610
+ const val = csstree6.generate(child.value);
12835
13611
  if (prop === "width") {
12836
13612
  const m = val.match(/^(\d+)px$/);
12837
- if (m) widthPx = parseInt(m[1], 10);
13613
+ if (m) {
13614
+ widthPx = parseInt(m[1], 10);
13615
+ widthLoc = locInCssBlock(anchor, child.loc);
13616
+ }
12838
13617
  if (/\b100%/.test(val)) fluid = true;
12839
13618
  } else if (prop === "max-width" && /\b100%/.test(val)) {
12840
13619
  fluid = true;
12841
13620
  }
12842
13621
  });
12843
13622
  if (widthPx !== null && widthPx > EMAIL_MAX_WIDTH && !fluid) {
12844
- const selector = csstree5.generate(node.prelude).trim().slice(0, 40);
12845
- addWidthIssue(widthPx, selector || "rule", issues, seen);
13623
+ const selector = csstree6.generate(node.prelude).trim().slice(0, 40);
13624
+ addWidthIssue(widthPx, selector || "rule", issues, seen, widthLoc);
12846
13625
  }
12847
13626
  }
12848
13627
  });
12849
13628
  });
12850
13629
  const usesWrapGuard = /overflow-wrap|word-break|word-wrap/i.test($.html());
12851
13630
  if (!usesWrapGuard) {
12852
- const $body = $("body");
13631
+ const nodes = visibleTextNodes($);
13632
+ const starts = [];
12853
13633
  let text = "";
12854
- if ($body.length) {
12855
- const clone = $body.clone();
12856
- clone.find("style, script").remove();
12857
- text = clone.text();
12858
- }
12859
- for (const token of text.split(/\s+/)) {
12860
- if (token.length <= UNBREAKABLE_STRING_LENGTH || seen.has(token)) continue;
12861
- seen.add(token);
13634
+ for (const node of nodes) {
13635
+ starts.push(text.length);
13636
+ text += (_a = node.data) != null ? _a : "";
13637
+ }
13638
+ let at = 0;
13639
+ for (const token of text.split(/(\s+)/)) {
13640
+ const start = at;
13641
+ at += token.length;
13642
+ if (/^\s*$/.test(token)) continue;
13643
+ if (token.length <= UNBREAKABLE_STRING_LENGTH || tokensSeen.has(token)) continue;
13644
+ tokensSeen.add(token);
12862
13645
  const preview = token.length > 50 ? `${token.slice(0, 50)}\u2026` : token;
12863
- issues.push({
13646
+ const loc = locateInNodes(nodes, starts, start, token.length, source);
13647
+ issues.push(__spreadValues({
12864
13648
  rule: "unbreakable-string",
12865
13649
  severity: "warning",
12866
13650
  message: `A ${token.length}-character unbroken string ("${preview}") can't wrap and will force horizontal scrolling on narrow screens.`,
12867
13651
  detail: `Add overflow-wrap: anywhere (or word-break: break-word) to its container.`
12868
- });
13652
+ }, loc ? { loc, locs: [loc] } : {}));
12869
13653
  }
12870
13654
  }
12871
13655
  return { hasOverflow: issues.length > 0, issues };
12872
13656
  }
12873
- function checkOverflow(html) {
12874
- return fromHtml(html, EMPTY_OVERFLOW, checkOverflowFromDom);
13657
+ function checkOverflow(html, options) {
13658
+ return fromHtml(
13659
+ html,
13660
+ EMPTY_OVERFLOW,
13661
+ ($, h) => checkOverflowFromDom($, (options == null ? void 0 : options.positions) ? h : void 0),
13662
+ options
13663
+ );
12875
13664
  }
12876
13665
 
12877
13666
  // src/visual-checker.ts
12878
- var csstree6 = __toESM(require("css-tree"), 1);
13667
+ var csstree7 = __toESM(require("css-tree"), 1);
12879
13668
  var CSS_WIDE_KEYWORDS = /* @__PURE__ */ new Set(["inherit", "initial", "unset", "revert", "revert-layer"]);
12880
13669
  var GRADIENT_RE = /(?:linear|radial|conic)-gradient\(/i;
12881
13670
  function isSolidColor(value) {
@@ -12884,8 +13673,8 @@ function isSolidColor(value) {
12884
13673
  return c !== null && c.a !== 0;
12885
13674
  }
12886
13675
  function firstColor(value) {
12887
- const tokens = value.match(/#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\)|\b[a-zA-Z]{3,}\b/g) || [];
12888
- for (const t of tokens) {
13676
+ const tokens2 = value.match(/#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\)|\b[a-zA-Z]{3,}\b/g) || [];
13677
+ for (const t of tokens2) {
12889
13678
  const lc = t.toLowerCase();
12890
13679
  if (lc === "transparent") continue;
12891
13680
  if (/^(?:linear|radial|conic|gradient|deg|turn|rad|grad|to|at|from|in|circle|ellipse|closest|farthest|side|corner|url)$/.test(lc)) continue;
@@ -12922,8 +13711,17 @@ function hasFontFallback(value) {
12922
13711
  return WEB_SAFE_FONTS.has(t) || GENERIC_FONT_FAMILIES.has(t) || t.startsWith("-apple-system") || t === "blinkmacsystemfont";
12923
13712
  });
12924
13713
  }
12925
- function inspectDeclarations(style, issues, seen) {
12926
- var _a, _b;
13714
+ function addOccurrence2(issue, loc) {
13715
+ if (!loc || !issue.locs) return;
13716
+ if (issue.locs.some((l) => l.offset === loc.offset)) return;
13717
+ if (issue.locs.length >= MAX_WARNING_LOCATIONS) {
13718
+ issue.locsTruncated = true;
13719
+ return;
13720
+ }
13721
+ issue.locs.push(loc);
13722
+ }
13723
+ function inspectDeclarations(style, issues, seen, locs) {
13724
+ var _a, _b, _c;
12927
13725
  const combined = `${(_a = style.get("background-image")) != null ? _a : ""} ${(_b = style.get("background")) != null ? _b : ""}`;
12928
13726
  const isGradient = GRADIENT_RE.test(combined);
12929
13727
  const isImage = isGradient || /url\(/i.test(combined);
@@ -12931,30 +13729,40 @@ function inspectDeclarations(style, issues, seen) {
12931
13729
  const stop = isGradient ? firstColor(combined) : null;
12932
13730
  const fix = stop ? `background-color: ${stop};` : `background-color: <solid colour matching the image>;`;
12933
13731
  const key = `bg:${fix}`;
12934
- if (!seen.has(key)) {
12935
- seen.add(key);
12936
- issues.push({
13732
+ const loc = (_c = locs == null ? void 0 : locs.get("background-image")) != null ? _c : locs == null ? void 0 : locs.get("background");
13733
+ const existing = seen.get(key);
13734
+ if (existing) {
13735
+ addOccurrence2(existing, loc);
13736
+ } else {
13737
+ const issue = __spreadValues({
12937
13738
  rule: "missing-background-fallback",
12938
13739
  severity: "warning",
12939
13740
  message: `${isGradient ? "Gradient" : "Background image"} has no background-color fallback \u2014 it renders as a blank area (and can hide overlaid text) in Outlook and other clients that drop image backgrounds.`,
12940
13741
  detail: isGradient ? `Add a solid fallback beneath the gradient using its first colour stop.` : `Add a solid background-color so the area still has colour when the image is dropped.`,
12941
13742
  fix
12942
- });
13743
+ }, loc ? { loc, locs: [loc] } : {});
13744
+ seen.set(key, issue);
13745
+ issues.push(issue);
12943
13746
  }
12944
13747
  }
12945
13748
  const font = style.get("font-family");
12946
13749
  if (font && !CSS_WIDE_KEYWORDS.has(font.trim().toLowerCase()) && !hasFontFallback(font)) {
12947
13750
  const fix = `font-family: ${font.trim()}, Arial, sans-serif;`;
12948
13751
  const key = `font:${font.trim().toLowerCase()}`;
12949
- if (!seen.has(key)) {
12950
- seen.add(key);
12951
- issues.push({
13752
+ const loc = locs == null ? void 0 : locs.get("font-family");
13753
+ const existing = seen.get(key);
13754
+ if (existing) {
13755
+ addOccurrence2(existing, loc);
13756
+ } else {
13757
+ const issue = __spreadValues({
12952
13758
  rule: "missing-font-fallback",
12953
13759
  severity: "warning",
12954
13760
  message: `font-family "${font.trim()}" has no web-safe fallback \u2014 clients that strip web fonts (Gmail, Outlook) fall back to Times New Roman.`,
12955
13761
  detail: `End the stack with a web-safe font and a generic family.`,
12956
13762
  fix
12957
- });
13763
+ }, loc ? { loc, locs: [loc] } : {});
13764
+ seen.set(key, issue);
13765
+ issues.push(issue);
12958
13766
  }
12959
13767
  }
12960
13768
  }
@@ -12962,35 +13770,52 @@ function ruleToMap(node) {
12962
13770
  const map = /* @__PURE__ */ new Map();
12963
13771
  node.block.children.forEach((child) => {
12964
13772
  if (child.type === "Declaration") {
12965
- map.set(child.property.toLowerCase(), csstree6.generate(child.value));
13773
+ map.set(child.property.toLowerCase(), csstree7.generate(child.value));
12966
13774
  }
12967
13775
  });
12968
13776
  return map;
12969
13777
  }
12970
- function checkVisualFromDom($) {
13778
+ function checkVisualFromDom($, source) {
12971
13779
  const issues = [];
12972
- const seen = /* @__PURE__ */ new Set();
13780
+ const seen = /* @__PURE__ */ new Map();
12973
13781
  $("[style]").each((_, el) => {
12974
- inspectDeclarations(parseInlineStyle($(el).attr("style") || ""), issues, seen);
13782
+ const attrLoc = locOfAttr(el, "style");
13783
+ const style = parseInlineStyle($(el).attr("style") || "");
13784
+ const locs = attrLoc ? new Map([...style.keys()].map((prop) => [prop, attrLoc])) : void 0;
13785
+ inspectDeclarations(style, issues, seen, locs);
12975
13786
  });
12976
13787
  $("style").each((_, el) => {
13788
+ const cssText = $(el).text();
13789
+ const anchor = cssBlockAnchor(el, cssText, source);
12977
13790
  let ast;
12978
13791
  try {
12979
- ast = csstree6.parse($(el).text());
13792
+ ast = csstree7.parse(cssText, { positions: true });
12980
13793
  } catch (e) {
12981
13794
  return;
12982
13795
  }
12983
- csstree6.walk(ast, {
13796
+ csstree7.walk(ast, {
12984
13797
  visit: "Rule",
12985
13798
  enter(node) {
12986
- if (node.type === "Rule") inspectDeclarations(ruleToMap(node), issues, seen);
13799
+ if (node.type !== "Rule") return;
13800
+ const locs = /* @__PURE__ */ new Map();
13801
+ node.block.children.forEach((child) => {
13802
+ if (child.type !== "Declaration") return;
13803
+ const loc = locInCssBlock(anchor, child.loc);
13804
+ if (loc) locs.set(child.property.toLowerCase(), loc);
13805
+ });
13806
+ inspectDeclarations(ruleToMap(node), issues, seen, locs);
12987
13807
  }
12988
13808
  });
12989
13809
  });
12990
13810
  return { issues };
12991
13811
  }
12992
- function checkVisual(html) {
12993
- return fromHtml(html, EMPTY_VISUAL, checkVisualFromDom);
13812
+ function checkVisual(html, options) {
13813
+ return fromHtml(
13814
+ html,
13815
+ EMPTY_VISUAL,
13816
+ ($, h) => checkVisualFromDom($, (options == null ? void 0 : options.positions) ? h : void 0),
13817
+ options
13818
+ );
12994
13819
  }
12995
13820
 
12996
13821
  // src/audit.ts
@@ -13009,7 +13834,8 @@ var EMPTY_AUDIT = {
13009
13834
  function runAudit($, html, framework, options) {
13010
13835
  var _a;
13011
13836
  const skip = new Set((_a = options == null ? void 0 : options.skip) != null ? _a : []);
13012
- const warnings = skip.has("compatibility") ? [] : analyzeEmailFromDom($, framework);
13837
+ const source = (options == null ? void 0 : options.positions) ? html : void 0;
13838
+ const warnings = skip.has("compatibility") ? [] : analyzeEmailFromDom($, framework, source);
13013
13839
  const scores = skip.has("compatibility") ? {} : generateCompatibilityScore(warnings);
13014
13840
  const spam = skip.has("spam") ? EMPTY_SPAM : analyzeSpamFromDom($, options == null ? void 0 : options.spam);
13015
13841
  const links = skip.has("links") ? EMPTY_LINKS : validateLinksFromDom($);
@@ -13017,19 +13843,19 @@ function runAudit($, html, framework, options) {
13017
13843
  const images = skip.has("images") ? EMPTY_IMAGES : analyzeImagesFromDom($);
13018
13844
  const inboxPreview = skip.has("inboxPreview") ? EMPTY_INBOX_PREVIEW : extractInboxPreviewFromDom($);
13019
13845
  const size = skip.has("size") ? EMPTY_SIZE : checkSizeFromDom($, html);
13020
- const templateVariables = skip.has("templateVariables") ? EMPTY_TEMPLATE : checkTemplateVariablesFromDom($);
13021
- const overflow = skip.has("overflow") ? EMPTY_OVERFLOW : checkOverflowFromDom($);
13022
- const visual = skip.has("visual") ? EMPTY_VISUAL : checkVisualFromDom($);
13846
+ const templateVariables = skip.has("templateVariables") ? EMPTY_TEMPLATE : checkTemplateVariablesFromDom($, source);
13847
+ const overflow = skip.has("overflow") ? EMPTY_OVERFLOW : checkOverflowFromDom($, source);
13848
+ const visual = skip.has("visual") ? EMPTY_VISUAL : checkVisualFromDom($, source);
13023
13849
  return { compatibility: { warnings, scores }, spam, links, accessibility, images, inboxPreview, size, templateVariables, overflow, visual };
13024
13850
  }
13025
13851
  function auditEmail(html, options) {
13026
- return fromHtml(html, EMPTY_AUDIT, ($, h) => runAudit($, h, options == null ? void 0 : options.framework, options));
13852
+ return fromHtml(html, EMPTY_AUDIT, ($, h) => runAudit($, h, options == null ? void 0 : options.framework, options), options);
13027
13853
  }
13028
13854
 
13029
13855
  // src/plain-text.ts
13030
- var cheerio6 = __toESM(require("cheerio"), 1);
13856
+ var cheerio5 = __toESM(require("cheerio"), 1);
13031
13857
  function toPlainText(html) {
13032
- const $ = cheerio6.load(html);
13858
+ const $ = cheerio5.load(html);
13033
13859
  $("style, script, head").remove();
13034
13860
  $("[data-skip-in-text='true']").remove();
13035
13861
  const lines = [];
@@ -13064,7 +13890,7 @@ function toPlainText(html) {
13064
13890
  if (trimmed) lines.push(trimmed);
13065
13891
  currentLine = "";
13066
13892
  }
13067
- function walk7(node) {
13893
+ function walk8(node) {
13068
13894
  var _a;
13069
13895
  if (node.type === "text") {
13070
13896
  const text = node.data.replace(/\s+/g, " ");
@@ -13113,7 +13939,7 @@ function toPlainText(html) {
13113
13939
  currentLine = "- ";
13114
13940
  }
13115
13941
  for (const child of el.children) {
13116
- walk7(child);
13942
+ walk8(child);
13117
13943
  }
13118
13944
  if (isBlock) flushLine();
13119
13945
  }
@@ -13121,7 +13947,7 @@ function toPlainText(html) {
13121
13947
  const root = body.length ? body[0] : $.root()[0];
13122
13948
  if (root && "children" in root) {
13123
13949
  for (const child of root.children) {
13124
- walk7(child);
13950
+ walk8(child);
13125
13951
  }
13126
13952
  }
13127
13953
  flushLine();
@@ -13131,7 +13957,6 @@ function toPlainText(html) {
13131
13957
  }
13132
13958
 
13133
13959
  // src/session.ts
13134
- var cheerio7 = __toESM(require("cheerio"), 1);
13135
13960
  function createSession(html, options) {
13136
13961
  if (!html || !html.trim()) {
13137
13962
  const fw = options == null ? void 0 : options.framework;
@@ -13158,16 +13983,17 @@ function createSession(html, options) {
13158
13983
  if (html.length > MAX_HTML_SIZE) {
13159
13984
  throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
13160
13985
  }
13161
- const $ = cheerio7.load(html);
13986
+ const $ = loadHtml(html, options);
13162
13987
  const framework = options == null ? void 0 : options.framework;
13988
+ const source = (options == null ? void 0 : options.positions) ? html : void 0;
13163
13989
  return {
13164
13990
  html,
13165
13991
  framework,
13166
13992
  audit(opts) {
13167
- return runAudit($, html, framework, opts);
13993
+ return runAudit($, html, framework, __spreadProps(__spreadValues({}, opts), { positions: options == null ? void 0 : options.positions }));
13168
13994
  },
13169
13995
  analyze() {
13170
- return analyzeEmailFromDom($, framework);
13996
+ return analyzeEmailFromDom($, framework, source);
13171
13997
  },
13172
13998
  score(warnings) {
13173
13999
  return generateCompatibilityScore(warnings);
@@ -13191,13 +14017,13 @@ function createSession(html, options) {
13191
14017
  return checkSizeFromDom($, html);
13192
14018
  },
13193
14019
  checkTemplateVariables() {
13194
- return checkTemplateVariablesFromDom($);
14020
+ return checkTemplateVariablesFromDom($, source);
13195
14021
  },
13196
14022
  checkOverflow() {
13197
- return checkOverflowFromDom($);
14023
+ return checkOverflowFromDom($, source);
13198
14024
  },
13199
14025
  checkVisual() {
13200
- return checkVisualFromDom($);
14026
+ return checkVisualFromDom($, source);
13201
14027
  },
13202
14028
  // Transforms create isolated copies since they mutate the DOM
13203
14029
  transformForClient(clientId) {
@@ -13228,18 +14054,22 @@ var CompileError = class extends Error {
13228
14054
  COMPOUND_VALUE_FEATURES,
13229
14055
  CSS_FUNCTION_FEATURES,
13230
14056
  CSS_SUPPORT,
14057
+ CSS_SUPPORT_NOTES,
13231
14058
  CompileError,
13232
14059
  EMAIL_CLIENTS,
13233
14060
  EMPTY_DELIVERABILITY,
13234
14061
  GENERIC_LINK_TEXT,
13235
14062
  HTML_ELEMENT_FEATURES,
13236
14063
  MAX_HTML_SIZE,
14064
+ MAX_WARNING_LOCATIONS,
13237
14065
  STRUCTURAL_FIX_PROPERTIES,
14066
+ VALUE_CAVEAT_PROPS,
13238
14067
  alphaBlend,
13239
14068
  analyzeEmail,
13240
14069
  analyzeImages,
13241
14070
  analyzeSpam,
13242
14071
  auditEmail,
14072
+ caveatApplies,
13243
14073
  checkAccessibility,
13244
14074
  checkOverflow,
13245
14075
  checkSize,