@emailens/engine 0.8.5 → 0.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -120,8 +120,8 @@ function getClient(id) {
120
120
  }
121
121
 
122
122
  // src/transform.ts
123
- import * as cheerio from "cheerio";
124
- import * as csstree from "css-tree";
123
+ import * as cheerio2 from "cheerio";
124
+ import * as csstree2 from "css-tree";
125
125
 
126
126
  // src/rules/css-support.ts
127
127
  var CSS_SUPPORT = {
@@ -4646,6 +4646,48 @@ h1 {
4646
4646
  Shortened content...
4647
4647
  <a href="https://example.com/full">Read more</a>
4648
4648
  </div>`
4649
+ },
4650
+ // ── padding (Outlook MSO font-width trick) ─────────────────────────────
4651
+ "padding::outlook": {
4652
+ language: "html",
4653
+ description: "Use MSO font-width trick for reliable button padding in Outlook",
4654
+ before: `<a href="https://example.com"
4655
+ style="background-color: #6d28d9; color: #fff;
4656
+ padding: 12px 32px; display: inline-block;
4657
+ text-decoration: none;">
4658
+ Click Here
4659
+ </a>`,
4660
+ after: `<!--[if mso]>
4661
+ <table role="presentation" cellpadding="0" cellspacing="0" border="0"
4662
+ style="border-collapse: collapse;">
4663
+ <tr>
4664
+ <td style="background-color: #6d28d9; padding: 0;
4665
+ mso-padding-alt: 12px 32px;">
4666
+ <!--[if mso]>
4667
+ <i style="letter-spacing: 32px; mso-font-width: -100%;
4668
+ mso-text-raise: 18pt;" hidden>&#8202;</i>
4669
+ <![endif]-->
4670
+ <a href="https://example.com"
4671
+ style="background-color: #6d28d9; color: #fff;
4672
+ mso-text-raise: 9pt; text-decoration: none;
4673
+ font-family: Arial, sans-serif; font-size: 14px;
4674
+ font-weight: bold;">Click Here</a>
4675
+ <!--[if mso]>
4676
+ <i style="letter-spacing: 32px; mso-font-width: -100%;"
4677
+ hidden>&#8202;</i>
4678
+ <![endif]-->
4679
+ </td>
4680
+ </tr>
4681
+ </table>
4682
+ <![endif]-->
4683
+ <!--[if !mso]><!-->
4684
+ <a href="https://example.com"
4685
+ style="background-color: #6d28d9; color: #fff;
4686
+ padding: 12px 32px; display: inline-block;
4687
+ text-decoration: none;">
4688
+ Click Here
4689
+ </a>
4690
+ <!--<![endif]-->`
4649
4691
  }
4650
4692
  };
4651
4693
 
@@ -5188,6 +5230,35 @@ var JSX_FIX_DATABASE = {
5188
5230
  Content \u2014 padding on inner element
5189
5231
  </div>
5190
5232
  </div>`
5233
+ },
5234
+ // ── padding (Outlook MSO font-width trick, JSX) ───────────────────────
5235
+ "padding::outlook::jsx": {
5236
+ language: "jsx",
5237
+ description: "Use React Email Button component which handles MSO padding automatically",
5238
+ before: `<a
5239
+ href="https://example.com"
5240
+ style={{ backgroundColor: "#6d28d9", color: "#fff",
5241
+ padding: "12px 32px", display: "inline-block",
5242
+ textDecoration: "none" }}>
5243
+ Click Here
5244
+ </a>`,
5245
+ after: `import { Button } from "@react-email/components";
5246
+
5247
+ {/* The Button component automatically handles MSO padding
5248
+ via the mso-font-width + &#8202; technique */}
5249
+ <Button
5250
+ href="https://example.com"
5251
+ style={{
5252
+ backgroundColor: "#6d28d9",
5253
+ color: "#fff",
5254
+ padding: "12px 32px",
5255
+ borderRadius: "6px",
5256
+ textDecoration: "none",
5257
+ fontWeight: "bold",
5258
+ }}
5259
+ >
5260
+ Click Here
5261
+ </Button>`
5191
5262
  }
5192
5263
  };
5193
5264
 
@@ -5485,7 +5556,9 @@ var HTML_SUGGESTION_DATABASE = {
5485
5556
  "transition": "CSS transitions are not supported in email.",
5486
5557
  "box-sizing": "Account for padding in your width calculations (use padding on a nested element).",
5487
5558
  "object-fit": "Use width/height attributes on <img> directly.",
5488
- "display": "Use tables for layout in email clients."
5559
+ "display": "Use tables for layout in email clients.",
5560
+ // ── padding ─────────────────────────────────────────────────────────────
5561
+ "padding::outlook": "Outlook ignores padding on <a> tags. Use the MSO font-width trick with &#8202; hair spaces, or use VML. React Email's <Button> handles this automatically."
5489
5562
  };
5490
5563
 
5491
5564
  // src/fix-snippets/jsx-suggestions.ts
@@ -5530,7 +5603,9 @@ var JSX_SUGGESTION_DATABASE = {
5530
5603
  // ── word-break ────────────────────────────────────────────────────────
5531
5604
  "word-break::jsx": "Wrap long text in a <table><tr><td> element. Outlook ignores wordBreak but respects table cell widths.",
5532
5605
  // ── overflow-wrap ─────────────────────────────────────────────────────
5533
- "overflow-wrap::jsx": "Wrap text in a <table><tr><td> element. Outlook ignores overflowWrap but respects table cell widths."
5606
+ "overflow-wrap::jsx": "Wrap text in a <table><tr><td> element. Outlook ignores overflowWrap but respects table cell widths.",
5607
+ // ── padding ─────────────────────────────────────────────────────────────
5608
+ "padding::outlook::jsx": "Use React Email's <Button> component which automatically generates MSO-compatible padding using the mso-font-width trick."
5534
5609
  };
5535
5610
 
5536
5611
  // src/fix-snippets/mjml-suggestions.ts
@@ -5752,1103 +5827,1647 @@ function serializeStyle(map) {
5752
5827
  return parts.join("; ");
5753
5828
  }
5754
5829
 
5755
- // src/constants.ts
5756
- var MAX_HTML_SIZE = 2 * 1024 * 1024;
5757
- var GENERIC_LINK_TEXT = /* @__PURE__ */ new Set([
5758
- "click here",
5759
- "here",
5760
- "read more",
5761
- "learn more",
5762
- "more",
5763
- "link",
5764
- "this link",
5765
- "click",
5766
- "tap here",
5767
- "this"
5768
- ]);
5769
- var GMAIL_CLIP_THRESHOLD = 102 * 1024;
5770
- var GMAIL_CLIP_WARNING_THRESHOLD = 90 * 1024;
5771
- var CLIENT_DISPLAY_LIMITS = [
5772
- { client: "Gmail (Web)", subjectLimit: 70, preheaderLimit: 90 },
5773
- { client: "Gmail (Mobile)", subjectLimit: 40, preheaderLimit: 45 },
5774
- { client: "Outlook (Web)", subjectLimit: 60, preheaderLimit: 90 },
5775
- { client: "Outlook (Desktop)", subjectLimit: 55, preheaderLimit: 35 },
5776
- { client: "Apple Mail (macOS)", subjectLimit: 78, preheaderLimit: 140 },
5777
- { client: "Apple Mail (iOS)", subjectLimit: 41, preheaderLimit: 90 },
5778
- { client: "Yahoo Mail", subjectLimit: 46, preheaderLimit: 100 },
5779
- { client: "Samsung Email", subjectLimit: 40, preheaderLimit: 70 }
5780
- ];
5781
- var TEMPLATE_VARIABLE_PATTERNS = [
5782
- [/\{\{[\s\S]*?\}\}/g, "Handlebars/Mustache"],
5783
- // {{var}}
5784
- [/\$\{[^}]+\}/g, "ES template literal"],
5785
- // ${var}
5786
- [/<%=?\s*[^%]+%>/g, "ERB/EJS"],
5787
- // <% %> / <%= %>
5788
- [/\*\|[A-Z_][A-Z0-9_]*\|\*/g, "Mailchimp merge tag"],
5789
- // *|TAG|*
5790
- [/%%[A-Za-z_][A-Za-z0-9_]*%%/g, "Salesforce AMPscript"],
5791
- // %%tag%%
5792
- [/\{[A-Za-z_][A-Za-z0-9_.]{2,}\}/g, "Single-brace merge field"]
5793
- // {merge_field} (3+ char names)
5794
- ];
5795
- var EMPTY_SPAM = { score: 100, level: "low", issues: [] };
5796
- var EMPTY_LINKS = {
5797
- totalLinks: 0,
5798
- issues: [],
5799
- breakdown: { https: 0, http: 0, mailto: 0, tel: 0, anchor: 0, javascript: 0, protocolRelative: 0, other: 0 }
5800
- };
5801
- var EMPTY_ACCESSIBILITY = { score: 100, issues: [] };
5802
- var EMPTY_IMAGES = { total: 0, totalDataUriBytes: 0, issues: [], images: [] };
5803
- var EMPTY_INBOX_PREVIEW = { subject: null, preheader: null, subjectLength: 0, preheaderLength: 0, truncation: [], issues: [] };
5804
- var EMPTY_SIZE = { htmlBytes: 0, humanSize: "0 B", clipped: false, issues: [] };
5805
- var EMPTY_TEMPLATE = { unresolvedCount: 0, issues: [] };
5806
- var EMPTY_DELIVERABILITY = { domain: "", checks: [], score: 0, issues: [] };
5830
+ // src/downlevel.ts
5831
+ import * as cheerio from "cheerio";
5832
+ import * as csstree from "css-tree";
5807
5833
 
5808
- // src/transform.ts
5809
- function inlineStyles($) {
5810
- const styleBlocks = [];
5811
- $("style").each((_, el) => {
5812
- styleBlocks.push($(el).text());
5813
- });
5814
- if (styleBlocks.length === 0) return;
5815
- for (const block of styleBlocks) {
5816
- let ast;
5817
- try {
5818
- ast = csstree.parse(block, { parseCustomProperty: true });
5819
- } catch (e) {
5820
- continue;
5821
- }
5822
- csstree.walk(ast, {
5823
- visit: "Rule",
5824
- enter(node) {
5825
- if (node.type !== "Rule" || node.prelude.type !== "SelectorList") return;
5826
- const declarations = csstree.generate(node.block);
5827
- const declText = declarations.slice(1, -1).trim();
5828
- if (!declText) return;
5829
- const selectorText = csstree.generate(node.prelude);
5830
- if (selectorText.includes(":hover") || selectorText.includes(":focus") || selectorText.includes(":active") || selectorText.includes("::")) {
5831
- return;
5832
- }
5833
- try {
5834
- $(selectorText).each((_, el) => {
5835
- const existing = $(el).attr("style") || "";
5836
- $(el).attr("style", existing ? `${existing}; ${declText}` : declText);
5837
- });
5838
- } catch (e) {
5839
- }
5840
- }
5841
- });
5842
- }
5843
- }
5844
- function makeWarning(base, prop, clientId, framework) {
5845
- const sug = getSuggestion(prop, clientId, framework);
5846
- const fix = getCodeFix(prop, clientId, framework);
5847
- const isFallback = framework && ((sug == null ? void 0 : sug.isGenericFallback) || fix && isCodeFixGenericFallback(prop, clientId, framework));
5848
- return __spreadProps(__spreadValues(__spreadValues(__spreadValues(__spreadValues({}, base), sug ? { suggestion: sug.text } : {}), fix ? { fix } : {}), isFallback ? { fixIsGenericFallback: true } : {}), {
5849
- fixType: STRUCTURAL_FIX_PROPERTIES.has(prop) ? "structural" : "css"
5850
- });
5851
- }
5852
- function detectAnimations($) {
5853
- let found = false;
5854
- $("[style]").each((_, el) => {
5855
- const style = $(el).attr("style") || "";
5856
- const props = parseInlineStyle(style);
5857
- props.forEach((_2, prop) => {
5858
- if (prop === "animation" || prop === "transition" || prop.startsWith("animation-") || prop.startsWith("transition-")) {
5859
- found = true;
5860
- }
5861
- });
5862
- });
5863
- if (!found) {
5864
- $("style").each((_, el) => {
5865
- try {
5866
- const ast = csstree.parse($(el).text());
5867
- csstree.walk(ast, {
5868
- enter(node) {
5869
- if (node.type === "Declaration") {
5870
- const prop = node.property.toLowerCase();
5871
- if (prop === "animation" || prop === "transition" || prop.startsWith("animation-") || prop.startsWith("transition-")) {
5872
- found = true;
5873
- }
5874
- }
5875
- }
5876
- });
5877
- } catch (e) {
5878
- }
5879
- });
5880
- }
5881
- return found;
5882
- }
5883
- function gmailAdditionalChecks($, clientId, _html, framework) {
5884
- const warnings = [];
5885
- let hasAtFontFace = false;
5886
- $("style").each((_, el) => {
5887
- try {
5888
- const ast = csstree.parse($(el).text());
5889
- csstree.walk(ast, {
5890
- enter(node) {
5891
- if (node.type === "Atrule" && node.name === "font-face") {
5892
- hasAtFontFace = true;
5893
- }
5894
- }
5895
- });
5896
- } catch (e) {
5897
- }
5898
- });
5899
- if (hasAtFontFace) {
5900
- warnings.push(makeWarning({
5901
- severity: "warning",
5902
- client: clientId,
5903
- property: "@font-face",
5904
- message: "Gmail does not support custom web fonts."
5905
- }, "@font-face", clientId, framework));
5906
- }
5907
- return warnings;
5908
- }
5909
- function gmailPostChecks($, clientId, _html, framework) {
5910
- const warnings = [];
5911
- $("*").contents().filter(function() {
5912
- return this.type === "comment";
5913
- }).each(function() {
5914
- const commentText = this.data || "";
5915
- if (commentText.includes("<style") || commentText.includes("[if mso]") || commentText.includes("[if gte mso")) {
5916
- $(this).remove();
5917
- }
5918
- });
5919
- const styleSug = getSuggestion("<style>:partial", clientId, framework);
5920
- warnings.push({
5921
- severity: "info",
5922
- client: clientId,
5923
- property: "<style>",
5924
- message: "Gmail partially supports <style> blocks (head only, 16KB limit). Inlining recommended for safety.",
5925
- suggestion: styleSug.text
5926
- });
5927
- return warnings;
5928
- }
5929
- function outlookWindowsAdditionalChecks($, clientId, _html, framework) {
5930
- const warnings = [];
5931
- if ($("[style*='border-radius']").length > 0) {
5932
- warnings.push(makeWarning({
5933
- severity: "warning",
5934
- client: clientId,
5935
- property: "border-radius",
5936
- message: "Outlook Classic ignores border-radius. Buttons and containers will have sharp corners."
5937
- }, "border-radius", clientId, framework));
5938
- }
5939
- if ($("[style*='max-width']").length > 0) {
5940
- warnings.push(makeWarning({
5941
- severity: "warning",
5942
- client: clientId,
5943
- property: "max-width",
5944
- message: "Outlook Classic ignores max-width."
5945
- }, "max-width", clientId, framework));
5946
- }
5947
- const hasDivLayout = $("div[style*='display']").length > 0 || $("div[style*='flex']").length > 0 || $("div[style*='grid']").length > 0;
5948
- if (hasDivLayout) {
5949
- warnings.push(makeWarning({
5950
- severity: "error",
5951
- client: clientId,
5952
- property: "display:flex",
5953
- message: "Outlook Classic uses Microsoft Word for rendering. Flexbox and Grid layouts will break."
5954
- }, "display:flex", clientId, framework));
5955
- }
5956
- if ($("[style*='background-image']").length > 0 || $("[style*='background:']").filter(
5957
- (_, el) => ($(el).attr("style") || "").includes("url(")
5958
- ).length > 0) {
5959
- warnings.push(makeWarning({
5960
- severity: "warning",
5961
- client: clientId,
5962
- property: "background-image",
5963
- message: "Outlook Classic requires VML for background images."
5964
- }, "background-image", clientId, framework));
5965
- }
5966
- return warnings;
5967
- }
5968
- function appleMailAdditionalChecks($, clientId) {
5969
- const warnings = [];
5970
- const imgsWithTransparentBg = $("img").filter((_, el) => {
5971
- const src = $(el).attr("src") || "";
5972
- return src.endsWith(".png") || src.endsWith(".svg");
5973
- });
5974
- if (imgsWithTransparentBg.length > 0) {
5975
- warnings.push({
5976
- severity: "info",
5977
- client: clientId,
5978
- property: "dark-mode",
5979
- message: "PNG/SVG images with transparent backgrounds may become invisible in Apple Mail dark mode.",
5980
- suggestion: "Add a white background or padding around images, or use dark-mode-friendly image variants."
5981
- });
5834
+ // src/color-utils.ts
5835
+ var NAMED_COLORS = {
5836
+ aliceblue: [240, 248, 255],
5837
+ antiquewhite: [250, 235, 215],
5838
+ aqua: [0, 255, 255],
5839
+ aquamarine: [127, 255, 212],
5840
+ azure: [240, 255, 255],
5841
+ beige: [245, 245, 220],
5842
+ bisque: [255, 228, 196],
5843
+ black: [0, 0, 0],
5844
+ blanchedalmond: [255, 235, 205],
5845
+ blue: [0, 0, 255],
5846
+ blueviolet: [138, 43, 226],
5847
+ brown: [165, 42, 42],
5848
+ burlywood: [222, 184, 135],
5849
+ cadetblue: [95, 158, 160],
5850
+ chartreuse: [127, 255, 0],
5851
+ chocolate: [210, 105, 30],
5852
+ coral: [255, 127, 80],
5853
+ cornflowerblue: [100, 149, 237],
5854
+ cornsilk: [255, 248, 220],
5855
+ crimson: [220, 20, 60],
5856
+ cyan: [0, 255, 255],
5857
+ darkblue: [0, 0, 139],
5858
+ darkcyan: [0, 139, 139],
5859
+ darkgoldenrod: [184, 134, 11],
5860
+ darkgray: [169, 169, 169],
5861
+ darkgreen: [0, 100, 0],
5862
+ darkgrey: [169, 169, 169],
5863
+ darkkhaki: [189, 183, 107],
5864
+ darkmagenta: [139, 0, 139],
5865
+ darkolivegreen: [85, 107, 47],
5866
+ darkorange: [255, 140, 0],
5867
+ darkorchid: [153, 50, 204],
5868
+ darkred: [139, 0, 0],
5869
+ darksalmon: [233, 150, 122],
5870
+ darkseagreen: [143, 188, 143],
5871
+ darkslateblue: [72, 61, 139],
5872
+ darkslategray: [47, 79, 79],
5873
+ darkslategrey: [47, 79, 79],
5874
+ darkturquoise: [0, 206, 209],
5875
+ darkviolet: [148, 0, 211],
5876
+ deeppink: [255, 20, 147],
5877
+ deepskyblue: [0, 191, 255],
5878
+ dimgray: [105, 105, 105],
5879
+ dimgrey: [105, 105, 105],
5880
+ dodgerblue: [30, 144, 255],
5881
+ firebrick: [178, 34, 34],
5882
+ floralwhite: [255, 250, 240],
5883
+ forestgreen: [34, 139, 34],
5884
+ fuchsia: [255, 0, 255],
5885
+ gainsboro: [220, 220, 220],
5886
+ ghostwhite: [248, 248, 255],
5887
+ gold: [255, 215, 0],
5888
+ goldenrod: [218, 165, 32],
5889
+ gray: [128, 128, 128],
5890
+ green: [0, 128, 0],
5891
+ greenyellow: [173, 255, 47],
5892
+ grey: [128, 128, 128],
5893
+ honeydew: [240, 255, 240],
5894
+ hotpink: [255, 105, 180],
5895
+ indianred: [205, 92, 92],
5896
+ indigo: [75, 0, 130],
5897
+ ivory: [255, 255, 240],
5898
+ khaki: [240, 230, 140],
5899
+ lavender: [230, 230, 250],
5900
+ lavenderblush: [255, 240, 245],
5901
+ lawngreen: [124, 252, 0],
5902
+ lemonchiffon: [255, 250, 205],
5903
+ lightblue: [173, 216, 230],
5904
+ lightcoral: [240, 128, 128],
5905
+ lightcyan: [224, 255, 255],
5906
+ lightgoldenrodyellow: [250, 250, 210],
5907
+ lightgray: [211, 211, 211],
5908
+ lightgreen: [144, 238, 144],
5909
+ lightgrey: [211, 211, 211],
5910
+ lightpink: [255, 182, 193],
5911
+ lightsalmon: [255, 160, 122],
5912
+ lightseagreen: [32, 178, 170],
5913
+ lightskyblue: [135, 206, 250],
5914
+ lightslategray: [119, 136, 153],
5915
+ lightslategrey: [119, 136, 153],
5916
+ lightsteelblue: [176, 196, 222],
5917
+ lightyellow: [255, 255, 224],
5918
+ lime: [0, 255, 0],
5919
+ limegreen: [50, 205, 50],
5920
+ linen: [250, 240, 230],
5921
+ magenta: [255, 0, 255],
5922
+ maroon: [128, 0, 0],
5923
+ mediumaquamarine: [102, 205, 170],
5924
+ mediumblue: [0, 0, 205],
5925
+ mediumorchid: [186, 85, 211],
5926
+ mediumpurple: [147, 111, 219],
5927
+ mediumseagreen: [60, 179, 113],
5928
+ mediumslateblue: [123, 104, 238],
5929
+ mediumspringgreen: [0, 250, 154],
5930
+ mediumturquoise: [72, 209, 204],
5931
+ mediumvioletred: [199, 21, 133],
5932
+ midnightblue: [25, 25, 112],
5933
+ mintcream: [245, 255, 250],
5934
+ mistyrose: [255, 228, 225],
5935
+ moccasin: [255, 228, 181],
5936
+ navajowhite: [255, 222, 173],
5937
+ navy: [0, 0, 128],
5938
+ oldlace: [253, 245, 230],
5939
+ olive: [128, 128, 0],
5940
+ olivedrab: [107, 142, 35],
5941
+ orange: [255, 165, 0],
5942
+ orangered: [255, 69, 0],
5943
+ orchid: [218, 112, 214],
5944
+ palegoldenrod: [238, 232, 170],
5945
+ palegreen: [152, 251, 152],
5946
+ paleturquoise: [175, 238, 238],
5947
+ palevioletred: [219, 112, 147],
5948
+ papayawhip: [255, 239, 213],
5949
+ peachpuff: [255, 218, 185],
5950
+ peru: [205, 133, 63],
5951
+ pink: [255, 192, 203],
5952
+ plum: [221, 160, 221],
5953
+ powderblue: [176, 224, 230],
5954
+ purple: [128, 0, 128],
5955
+ rebeccapurple: [102, 51, 153],
5956
+ red: [255, 0, 0],
5957
+ rosybrown: [188, 143, 143],
5958
+ royalblue: [65, 105, 225],
5959
+ saddlebrown: [139, 69, 19],
5960
+ salmon: [250, 128, 114],
5961
+ sandybrown: [244, 164, 96],
5962
+ seagreen: [46, 139, 87],
5963
+ seashell: [255, 245, 238],
5964
+ sienna: [160, 82, 45],
5965
+ silver: [192, 192, 192],
5966
+ skyblue: [135, 206, 235],
5967
+ slateblue: [106, 90, 205],
5968
+ slategray: [112, 128, 144],
5969
+ slategrey: [112, 128, 144],
5970
+ snow: [255, 250, 250],
5971
+ springgreen: [0, 255, 127],
5972
+ steelblue: [70, 130, 180],
5973
+ tan: [210, 180, 140],
5974
+ teal: [0, 128, 128],
5975
+ thistle: [216, 191, 216],
5976
+ tomato: [255, 99, 71],
5977
+ turquoise: [64, 224, 208],
5978
+ violet: [238, 130, 238],
5979
+ wheat: [245, 222, 179],
5980
+ white: [255, 255, 255],
5981
+ whitesmoke: [245, 245, 245],
5982
+ yellow: [255, 255, 0],
5983
+ yellowgreen: [154, 205, 50]
5984
+ };
5985
+ function parseAlpha(raw) {
5986
+ if (raw === void 0) return 1;
5987
+ const s = raw.trim();
5988
+ if (s.endsWith("%")) {
5989
+ return Math.min(1, Math.max(0, parseFloat(s) / 100));
5982
5990
  }
5983
- return warnings;
5991
+ return Math.min(1, Math.max(0, parseFloat(s)));
5984
5992
  }
5985
- function yahooAdditionalChecks($, clientId, _html, framework) {
5986
- const warnings = [];
5987
- warnings.push({
5988
- severity: "info",
5989
- client: clientId,
5990
- property: "class",
5991
- message: "Yahoo Mail rewrites CSS class names with a prefix. Class-based selectors in <style> blocks will still work but the names change."
5992
- });
5993
- if ($("[style*='background']").filter(
5994
- (_, el) => ($(el).attr("style") || "").includes("url(")
5995
- ).length > 0) {
5996
- warnings.push(makeWarning({
5997
- severity: "warning",
5998
- client: clientId,
5999
- property: "background-image",
6000
- message: "Yahoo Mail has inconsistent support for CSS background images."
6001
- }, "background-image", clientId, framework));
6002
- }
6003
- return warnings;
5993
+ function clamp255(n) {
5994
+ return Math.round(Math.min(255, Math.max(0, n)));
6004
5995
  }
6005
- function thunderbirdAdditionalChecks($, clientId) {
6006
- if (detectAnimations($)) {
6007
- return [{
6008
- severity: "info",
6009
- client: clientId,
6010
- property: "animation",
6011
- message: "Thunderbird does not support CSS animations or transitions."
6012
- }];
5996
+ function hslToRgb(h, s, l) {
5997
+ const c = (1 - Math.abs(2 * l - 1)) * s;
5998
+ const hp = h / 60;
5999
+ const x = c * (1 - Math.abs(hp % 2 - 1));
6000
+ const m = l - c / 2;
6001
+ let r1 = 0, g1 = 0, b1 = 0;
6002
+ if (hp >= 0 && hp < 1) {
6003
+ r1 = c;
6004
+ g1 = x;
6005
+ b1 = 0;
6006
+ } else if (hp >= 1 && hp < 2) {
6007
+ r1 = x;
6008
+ g1 = c;
6009
+ b1 = 0;
6010
+ } else if (hp >= 2 && hp < 3) {
6011
+ r1 = 0;
6012
+ g1 = c;
6013
+ b1 = x;
6014
+ } else if (hp >= 3 && hp < 4) {
6015
+ r1 = 0;
6016
+ g1 = x;
6017
+ b1 = c;
6018
+ } else if (hp >= 4 && hp < 5) {
6019
+ r1 = x;
6020
+ g1 = 0;
6021
+ b1 = c;
6022
+ } else {
6023
+ r1 = c;
6024
+ g1 = 0;
6025
+ b1 = x;
6013
6026
  }
6014
- return [];
6027
+ return [
6028
+ clamp255((r1 + m) * 255),
6029
+ clamp255((g1 + m) * 255),
6030
+ clamp255((b1 + m) * 255)
6031
+ ];
6015
6032
  }
6016
- function heyAdditionalChecks(_$, clientId, html) {
6017
- const warnings = [];
6018
- if (!html.includes("prefers-color-scheme")) {
6019
- warnings.push({
6020
- severity: "info",
6021
- client: clientId,
6022
- property: "dark-mode",
6023
- message: "HEY Mail supports @media (prefers-color-scheme: dark). Consider adding dark mode styles.",
6024
- suggestion: "Add a @media (prefers-color-scheme: dark) block to optimize for HEY's audience."
6025
- });
6033
+ function hwbToRgb(h, w, b) {
6034
+ if (w + b >= 1) {
6035
+ const gray = clamp255(w / (w + b) * 255);
6036
+ return [gray, gray, gray];
6026
6037
  }
6027
- return warnings;
6038
+ const [r0, g0, b0] = hslToRgb(h, 1, 0.5);
6039
+ return [
6040
+ clamp255(r0 / 255 * (1 - w - b) * 255 + w * 255),
6041
+ clamp255(g0 / 255 * (1 - w - b) * 255 + w * 255),
6042
+ clamp255(b0 / 255 * (1 - w - b) * 255 + w * 255)
6043
+ ];
6028
6044
  }
6029
- function superhumanAdditionalChecks($, clientId, html) {
6030
- const warnings = [];
6031
- if (detectAnimations($)) {
6032
- warnings.push({
6033
- severity: "info",
6034
- client: clientId,
6035
- property: "animation",
6036
- message: "Superhuman may honor OS-level 'reduce motion' preferences, disabling animations.",
6037
- suggestion: "Use @media (prefers-reduced-motion: reduce) to provide static fallbacks."
6038
- });
6039
- }
6040
- warnings.push({
6041
- severity: "info",
6042
- client: clientId,
6043
- property: "<style>",
6044
- message: "Superhuman uses Chromium rendering with excellent CSS support. Flexbox, Grid, CSS variables, and modern properties all work."
6045
- });
6046
- return warnings;
6045
+ function linearToGamma(c) {
6046
+ if (c <= 31308e-7) return 12.92 * c;
6047
+ return 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
6047
6048
  }
6048
- var EMPTY_SET = /* @__PURE__ */ new Set();
6049
- var CLIENT_CONFIGS = {
6050
- "gmail-web": {
6051
- id: "gmail-web",
6052
- strippedProperties: GMAIL_STRIPPED_PROPERTIES,
6053
- stripMode: "strip",
6054
- valueStrips: [
6055
- { prop: "display", pattern: /grid/ },
6056
- { prop: "background", pattern: /linear-gradient|radial-gradient/ }
6057
- ],
6058
- inlineAndStripStyles: true,
6059
- stripExternalStylesheets: true,
6060
- stripForms: true,
6061
- stripSvg: true,
6062
- additionalChecks: gmailAdditionalChecks
6063
- },
6064
- "gmail-android": {
6065
- id: "gmail-android",
6066
- strippedProperties: GMAIL_STRIPPED_PROPERTIES,
6067
- stripMode: "strip",
6068
- valueStrips: [
6069
- { prop: "display", pattern: /grid/ },
6070
- { prop: "background", pattern: /linear-gradient|radial-gradient/ }
6071
- ],
6072
- inlineAndStripStyles: true,
6073
- stripExternalStylesheets: true,
6074
- stripForms: true,
6075
- stripSvg: true,
6076
- additionalChecks: gmailAdditionalChecks
6077
- },
6078
- "gmail-ios": {
6079
- id: "gmail-ios",
6080
- strippedProperties: GMAIL_STRIPPED_PROPERTIES,
6081
- stripMode: "strip",
6082
- valueStrips: [
6083
- { prop: "display", pattern: /grid/ },
6084
- { prop: "background", pattern: /linear-gradient|radial-gradient/ }
6085
- ],
6086
- inlineAndStripStyles: true,
6087
- stripExternalStylesheets: true,
6088
- stripForms: true,
6089
- stripSvg: true,
6090
- additionalChecks: gmailAdditionalChecks
6091
- },
6092
- "outlook-windows": {
6093
- id: "outlook-windows",
6094
- strippedProperties: /* @__PURE__ */ new Set(["position", "transform", "animation", "transition"]),
6095
- stripMode: "strip",
6096
- inlineAndStripStyles: false,
6097
- stripExternalStylesheets: false,
6098
- stripForms: false,
6099
- stripSvg: false
6100
- },
6101
- "outlook-windows-legacy": {
6102
- id: "outlook-windows-legacy",
6103
- strippedProperties: OUTLOOK_WORD_UNSUPPORTED,
6104
- stripMode: "strip",
6105
- valueStrips: [
6106
- { prop: "background", pattern: /linear-gradient|radial-gradient/ },
6107
- { prop: "background-image", pattern: /linear-gradient|radial-gradient/ }
6108
- ],
6109
- inlineAndStripStyles: false,
6110
- stripExternalStylesheets: false,
6111
- stripForms: false,
6112
- stripSvg: false,
6113
- additionalChecks: outlookWindowsAdditionalChecks
6114
- },
6115
- "outlook-web": {
6116
- id: "outlook-web",
6117
- strippedProperties: /* @__PURE__ */ new Set(["position", "transform", "animation", "transition"]),
6118
- stripMode: "strip",
6119
- inlineAndStripStyles: false,
6120
- stripExternalStylesheets: false,
6121
- stripForms: false,
6122
- stripSvg: false
6123
- },
6124
- "apple-mail-macos": {
6125
- id: "apple-mail-macos",
6126
- strippedProperties: EMPTY_SET,
6127
- stripMode: "strip",
6128
- inlineAndStripStyles: false,
6129
- stripExternalStylesheets: false,
6130
- stripForms: false,
6131
- stripSvg: false,
6132
- additionalChecks: appleMailAdditionalChecks
6133
- },
6134
- "apple-mail-ios": {
6135
- id: "apple-mail-ios",
6136
- strippedProperties: EMPTY_SET,
6137
- stripMode: "strip",
6138
- inlineAndStripStyles: false,
6139
- stripExternalStylesheets: false,
6140
- stripForms: false,
6141
- stripSvg: false,
6142
- additionalChecks: appleMailAdditionalChecks
6143
- },
6144
- "yahoo-mail": {
6145
- id: "yahoo-mail",
6146
- strippedProperties: /* @__PURE__ */ new Set(["position", "box-shadow", "transform", "animation", "transition", "opacity"]),
6147
- stripMode: "strip",
6148
- inlineAndStripStyles: false,
6149
- stripExternalStylesheets: false,
6150
- stripForms: false,
6151
- stripSvg: false,
6152
- additionalChecks: yahooAdditionalChecks
6153
- },
6154
- "samsung-mail": {
6155
- id: "samsung-mail",
6156
- strippedProperties: /* @__PURE__ */ new Set(["box-shadow", "transform", "animation", "transition", "opacity"]),
6157
- stripMode: "info",
6158
- inlineAndStripStyles: false,
6159
- stripExternalStylesheets: false,
6160
- stripForms: false,
6161
- stripSvg: false
6162
- },
6163
- "thunderbird": {
6164
- id: "thunderbird",
6165
- strippedProperties: EMPTY_SET,
6166
- stripMode: "strip",
6167
- inlineAndStripStyles: false,
6168
- stripExternalStylesheets: false,
6169
- stripForms: false,
6170
- stripSvg: false,
6171
- additionalChecks: thunderbirdAdditionalChecks
6172
- },
6173
- "hey-mail": {
6174
- id: "hey-mail",
6175
- strippedProperties: /* @__PURE__ */ new Set(["transform", "animation", "transition"]),
6176
- stripMode: "strip",
6177
- valueStrips: [
6178
- { prop: "position", pattern: /fixed|sticky/ }
6179
- ],
6180
- inlineAndStripStyles: false,
6181
- stripExternalStylesheets: true,
6182
- stripForms: true,
6183
- stripSvg: false,
6184
- additionalChecks: heyAdditionalChecks
6185
- },
6186
- "superhuman": {
6187
- id: "superhuman",
6188
- strippedProperties: EMPTY_SET,
6189
- stripMode: "strip",
6190
- inlineAndStripStyles: false,
6191
- stripExternalStylesheets: true,
6192
- stripForms: true,
6193
- stripSvg: false,
6194
- additionalChecks: superhumanAdditionalChecks
6049
+ function oklchToRgb(L, C, H) {
6050
+ const hRad = H * Math.PI / 180;
6051
+ const a = C * Math.cos(hRad);
6052
+ const b = C * Math.sin(hRad);
6053
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
6054
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
6055
+ const s_ = L - 0.0894841775 * a - 1.291485548 * b;
6056
+ const l = l_ * l_ * l_;
6057
+ const m = m_ * m_ * m_;
6058
+ const s = s_ * s_ * s_;
6059
+ const rLin = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
6060
+ const gLin = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
6061
+ const bLin = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s;
6062
+ return [
6063
+ clamp255(linearToGamma(rLin) * 255),
6064
+ clamp255(linearToGamma(gLin) * 255),
6065
+ clamp255(linearToGamma(bLin) * 255)
6066
+ ];
6067
+ }
6068
+ function parseColor(value) {
6069
+ if (!value) return null;
6070
+ const v = value.trim().toLowerCase();
6071
+ if (v === "inherit" || v === "currentcolor" || v === "initial" || v === "unset" || v.startsWith("var(")) {
6072
+ return null;
6195
6073
  }
6196
- };
6197
- function applyTransform(html, config, framework) {
6198
- const $ = cheerio.load(html);
6199
- const warnings = [];
6200
- const clientId = config.id;
6201
- if (config.additionalChecks && config.inlineAndStripStyles) {
6202
- warnings.push(...config.additionalChecks($, clientId, html, framework));
6074
+ if (v === "transparent") {
6075
+ return { r: 0, g: 0, b: 0, a: 0 };
6203
6076
  }
6204
- if (config.inlineAndStripStyles) {
6205
- inlineStyles($);
6206
- $("style").remove();
6077
+ const named = NAMED_COLORS[v];
6078
+ if (named) {
6079
+ return { r: named[0], g: named[1], b: named[2], a: 1 };
6207
6080
  }
6208
- if (config.stripExternalStylesheets) {
6209
- if ($("link[rel='stylesheet']").length > 0) {
6210
- warnings.push(makeWarning({
6211
- severity: "error",
6212
- client: clientId,
6213
- property: "<link>",
6214
- message: `${clientId} does not load external stylesheets.`
6215
- }, "<link>", clientId, framework));
6216
- $("link[rel='stylesheet']").remove();
6081
+ if (v.startsWith("#")) {
6082
+ const hex = v.slice(1);
6083
+ if (hex.length === 3) {
6084
+ return {
6085
+ r: parseInt(hex[0] + hex[0], 16),
6086
+ g: parseInt(hex[1] + hex[1], 16),
6087
+ b: parseInt(hex[2] + hex[2], 16),
6088
+ a: 1
6089
+ };
6217
6090
  }
6091
+ if (hex.length === 4) {
6092
+ return {
6093
+ r: parseInt(hex[0] + hex[0], 16),
6094
+ g: parseInt(hex[1] + hex[1], 16),
6095
+ b: parseInt(hex[2] + hex[2], 16),
6096
+ a: parseInt(hex[3] + hex[3], 16) / 255
6097
+ };
6098
+ }
6099
+ if (hex.length === 6) {
6100
+ return {
6101
+ r: parseInt(hex.slice(0, 2), 16),
6102
+ g: parseInt(hex.slice(2, 4), 16),
6103
+ b: parseInt(hex.slice(4, 6), 16),
6104
+ a: 1
6105
+ };
6106
+ }
6107
+ if (hex.length === 8) {
6108
+ return {
6109
+ r: parseInt(hex.slice(0, 2), 16),
6110
+ g: parseInt(hex.slice(2, 4), 16),
6111
+ b: parseInt(hex.slice(4, 6), 16),
6112
+ a: parseInt(hex.slice(6, 8), 16) / 255
6113
+ };
6114
+ }
6115
+ return null;
6218
6116
  }
6219
- if (config.stripForms && $("form").length > 0) {
6220
- warnings.push(makeWarning({
6221
- severity: "error",
6222
- client: clientId,
6223
- property: "<form>",
6224
- message: `${clientId} removes form elements.`
6225
- }, "<form>", clientId, framework));
6226
- $("form").each((_, el) => {
6227
- $(el).replaceWith($(el).html() || "");
6228
- });
6229
- }
6230
- if (config.stripSvg && $("svg").length > 0) {
6231
- warnings.push(makeWarning({
6232
- severity: "error",
6233
- client: clientId,
6234
- property: "<svg>",
6235
- message: `${clientId} does not support inline SVG elements.`
6236
- }, "<svg>", clientId, framework));
6237
- $("svg").each((_, el) => {
6238
- $(el).replaceWith('<img alt="[SVG not supported]" />');
6239
- });
6240
- }
6241
- if (config.strippedProperties.size > 0 || config.valueStrips && config.valueStrips.length > 0) {
6242
- $("[style]").each((_, el) => {
6243
- const style = $(el).attr("style") || "";
6244
- const props = parseInlineStyle(style);
6245
- const removed = [];
6246
- props.forEach((value, prop) => {
6247
- var _a;
6248
- if (config.strippedProperties.has(prop)) {
6249
- if (config.stripMode === "strip") {
6250
- removed.push(prop);
6251
- props.delete(prop);
6252
- } else {
6253
- warnings.push(makeWarning({
6254
- severity: "info",
6255
- client: clientId,
6256
- property: prop,
6257
- message: `${clientId} has limited support for "${prop}".`
6258
- }, prop, clientId, framework));
6259
- }
6260
- return;
6261
- }
6262
- for (const vs of (_a = config.valueStrips) != null ? _a : []) {
6263
- if (prop === vs.prop && vs.pattern.test(value)) {
6264
- removed.push(prop);
6265
- props.delete(prop);
6266
- return;
6267
- }
6268
- }
6269
- });
6270
- if (removed.length > 0) {
6271
- $(el).attr("style", serializeStyle(props));
6272
- for (const prop of removed) {
6273
- warnings.push(makeWarning({
6274
- severity: "warning",
6275
- client: clientId,
6276
- property: prop,
6277
- message: `${clientId} strips "${prop}" from styles.`
6278
- }, prop, clientId, framework));
6279
- }
6280
- }
6281
- });
6117
+ const rgbComma = v.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+%?))?\s*\)$/);
6118
+ if (rgbComma) {
6119
+ return {
6120
+ r: Math.min(255, parseInt(rgbComma[1], 10)),
6121
+ g: Math.min(255, parseInt(rgbComma[2], 10)),
6122
+ b: Math.min(255, parseInt(rgbComma[3], 10)),
6123
+ a: rgbComma[4] !== void 0 ? parseAlpha(rgbComma[4]) : 1
6124
+ };
6282
6125
  }
6283
- if (config.inlineAndStripStyles) {
6284
- warnings.push(...gmailPostChecks($, clientId, html, framework));
6126
+ const rgbSpace = v.match(/^rgba?\(\s*(\d+)\s+(\d+)\s+(\d+)(?:\s*\/\s*([\d.]+%?))?\s*\)$/);
6127
+ if (rgbSpace) {
6128
+ return {
6129
+ r: Math.min(255, parseInt(rgbSpace[1], 10)),
6130
+ g: Math.min(255, parseInt(rgbSpace[2], 10)),
6131
+ b: Math.min(255, parseInt(rgbSpace[3], 10)),
6132
+ a: rgbSpace[4] !== void 0 ? parseAlpha(rgbSpace[4]) : 1
6133
+ };
6285
6134
  }
6286
- if (config.additionalChecks && !config.inlineAndStripStyles) {
6287
- warnings.push(...config.additionalChecks($, clientId, html, framework));
6135
+ const hslComma = v.match(/^hsla?\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%(?:\s*,\s*([\d.]+%?))?\s*\)$/);
6136
+ if (hslComma) {
6137
+ const h = (parseFloat(hslComma[1]) % 360 + 360) % 360;
6138
+ const s = Math.min(100, parseFloat(hslComma[2])) / 100;
6139
+ const l = Math.min(100, parseFloat(hslComma[3])) / 100;
6140
+ const [r, g, b] = hslToRgb(h, s, l);
6141
+ return { r, g, b, a: hslComma[4] !== void 0 ? parseAlpha(hslComma[4]) : 1 };
6142
+ }
6143
+ const hslSpace = v.match(/^hsla?\(\s*([\d.]+)\s+([\d.]+)%\s+([\d.]+)%(?:\s*\/\s*([\d.]+%?))?\s*\)$/);
6144
+ if (hslSpace) {
6145
+ const h = (parseFloat(hslSpace[1]) % 360 + 360) % 360;
6146
+ const s = Math.min(100, parseFloat(hslSpace[2])) / 100;
6147
+ const l = Math.min(100, parseFloat(hslSpace[3])) / 100;
6148
+ const [r, g, b] = hslToRgb(h, s, l);
6149
+ return { r, g, b, a: hslSpace[4] !== void 0 ? parseAlpha(hslSpace[4]) : 1 };
6150
+ }
6151
+ const hwbMatch = v.match(/^hwb\(\s*([\d.]+)\s+([\d.]+)%\s+([\d.]+)%(?:\s*\/\s*([\d.]+%?))?\s*\)$/);
6152
+ if (hwbMatch) {
6153
+ const h = (parseFloat(hwbMatch[1]) % 360 + 360) % 360;
6154
+ const w = Math.min(100, parseFloat(hwbMatch[2])) / 100;
6155
+ const bk = Math.min(100, parseFloat(hwbMatch[3])) / 100;
6156
+ const [r, g, b] = hwbToRgb(h, w, bk);
6157
+ return { r, g, b, a: hwbMatch[4] !== void 0 ? parseAlpha(hwbMatch[4]) : 1 };
6158
+ }
6159
+ const oklchMatch = v.match(/^oklch\(\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*([\d.]+%?))?\s*\)$/);
6160
+ if (oklchMatch) {
6161
+ const L = Math.min(1, Math.max(0, parseFloat(oklchMatch[1])));
6162
+ const C = Math.max(0, parseFloat(oklchMatch[2]));
6163
+ const H = (parseFloat(oklchMatch[3]) % 360 + 360) % 360;
6164
+ const [r, g, b] = oklchToRgb(L, C, H);
6165
+ return { r, g, b, a: oklchMatch[4] !== void 0 ? parseAlpha(oklchMatch[4]) : 1 };
6288
6166
  }
6289
- return { clientId, html: $.html(), warnings };
6167
+ return null;
6290
6168
  }
6291
- function transformForClient(html, clientId, framework) {
6292
- if (!html || !html.trim()) {
6293
- return { clientId, html: html || "", warnings: [] };
6294
- }
6295
- if (html.length > MAX_HTML_SIZE) {
6296
- throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
6297
- }
6298
- const config = CLIENT_CONFIGS[clientId];
6299
- if (!config) {
6300
- return {
6301
- clientId,
6302
- html,
6303
- warnings: [
6304
- {
6305
- severity: "info",
6306
- client: clientId,
6307
- property: "unknown",
6308
- message: `No transformation rules available for client "${clientId}".`
6309
- }
6310
- ]
6311
- };
6312
- }
6313
- return applyTransform(html, config, framework);
6169
+ function formatRgb(rgba) {
6170
+ const r = clamp255(rgba.r);
6171
+ const g = clamp255(rgba.g);
6172
+ const b = clamp255(rgba.b);
6173
+ if (rgba.a < 1) {
6174
+ return `rgba(${r}, ${g}, ${b}, ${rgba.a})`;
6175
+ }
6176
+ return `rgb(${r}, ${g}, ${b})`;
6314
6177
  }
6315
- function transformForAllClients(html, framework) {
6316
- return Object.keys(CLIENT_CONFIGS).map(
6317
- (clientId) => transformForClient(html, clientId, framework)
6318
- );
6178
+ function relativeLuminance(r, g, b) {
6179
+ const [rs, gs, bs] = [r, g, b].map((c) => {
6180
+ const s = c / 255;
6181
+ return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
6182
+ });
6183
+ return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
6184
+ }
6185
+ function contrastRatio(l1, l2) {
6186
+ const lighter = Math.max(l1, l2);
6187
+ const darker = Math.min(l1, l2);
6188
+ return (lighter + 0.05) / (darker + 0.05);
6189
+ }
6190
+ function wcagGrade(ratio) {
6191
+ if (ratio >= 7) return "AAA";
6192
+ if (ratio >= 4.5) return "AA";
6193
+ if (ratio >= 3) return "AA Large";
6194
+ return "Fail";
6195
+ }
6196
+ function alphaBlend(fg, bgR, bgG, bgB) {
6197
+ const a = fg.a;
6198
+ return [
6199
+ Math.round(fg.r * a + bgR * (1 - a)),
6200
+ Math.round(fg.g * a + bgG * (1 - a)),
6201
+ Math.round(fg.b * a + bgB * (1 - a))
6202
+ ];
6319
6203
  }
6320
6204
 
6321
- // src/analyze.ts
6322
- import * as cheerio2 from "cheerio";
6323
- import * as csstree2 from "css-tree";
6324
- var HTML_ELEMENT_SELECTORS = {
6325
- "<style>": "style",
6326
- "<link>": "link[rel='stylesheet']",
6327
- "<svg>": "svg",
6328
- "<video>": "video",
6329
- "<form>": "form, input, button[type='submit']",
6330
- "<audio>": "audio",
6331
- "<picture>": "picture",
6332
- "<dialog>": "dialog",
6333
- "<meter>": "meter",
6334
- "<progress>": "progress",
6335
- "<select>": "select",
6336
- "<textarea>": "textarea",
6337
- "<marquee>": "marquee",
6338
- "<object>": "object",
6339
- "<base>": "base"
6340
- };
6341
- var HTML_ELEMENT_SEVERITY = {
6342
- "<style>": "error",
6343
- "<link>": "error",
6344
- "<svg>": "error",
6345
- "<form>": "error",
6346
- "<video>": "warning",
6347
- "<audio>": "warning",
6348
- "<picture>": "warning",
6349
- "<dialog>": "warning",
6350
- "<marquee>": "warning",
6351
- "<meter>": "warning",
6352
- "<progress>": "warning",
6353
- "<select>": "warning",
6354
- "<textarea>": "warning",
6355
- "<object>": "warning",
6356
- "<base>": "warning"
6357
- };
6358
- var HTML_ELEMENT_MESSAGES = {
6359
- "<style>": (n) => `${n} strips <style> blocks. Styles must be inlined.`,
6360
- "<link>": (n) => `${n} does not support external stylesheets.`,
6361
- "<svg>": (n) => `${n} does not support inline SVG.`,
6362
- "<video>": (n) => `${n} does not support <video> elements.`,
6363
- "<form>": (n) => `${n} strips form elements.`
6205
+ // src/downlevel.ts
6206
+ var LOGICAL_PROPERTY_MAP = {
6207
+ "padding-inline": ["padding-left", "padding-right"],
6208
+ "padding-block": ["padding-top", "padding-bottom"],
6209
+ "margin-inline": ["margin-left", "margin-right"],
6210
+ "margin-block": ["margin-top", "margin-bottom"],
6211
+ "inset-inline": ["left", "right"],
6212
+ "inset-block": ["top", "bottom"],
6213
+ "border-inline": ["border-left", "border-right"],
6214
+ "border-block": ["border-top", "border-bottom"]
6364
6215
  };
6365
- var COMPOUND_DETECTORS = [
6366
- { key: "display:flex", property: "display", valueIncludes: "flex" },
6367
- { key: "display:grid", property: "display", valueIncludes: "grid" },
6368
- { key: "display:none", property: "display", valueIncludes: "none" }
6369
- ];
6370
- var CSS_FUNCTION_DETECTORS = CSS_FUNCTION_FEATURES.map((fn) => ({
6371
- key: fn,
6372
- pattern: `${fn}(`
6373
- // require opening paren — matches "min(" but not "Minion"
6374
- }));
6375
- function analyzeEmailFromDom($, framework) {
6376
- const warnings = [];
6377
- const seenWarnings = /* @__PURE__ */ new Set();
6378
- function addWarning(w) {
6379
- const key = `${w.client}:${w.property}:${w.severity}:${w.selector || ""}`;
6380
- if (!seenWarnings.has(key)) {
6381
- seenWarnings.add(key);
6382
- warnings.push(w);
6216
+ var MODERN_COLOR_RE = /\b(oklch|hsl|hsla|hwb|rgba?)\s*\(/g;
6217
+ function extractFunctionCall(str, fnStart, openParenIndex) {
6218
+ let depth = 0;
6219
+ for (let i = openParenIndex; i < str.length; i++) {
6220
+ if (str[i] === "(") depth++;
6221
+ else if (str[i] === ")") {
6222
+ depth--;
6223
+ if (depth === 0) {
6224
+ return str.slice(fnStart, i + 1);
6225
+ }
6383
6226
  }
6384
6227
  }
6385
- function describeSelector(el) {
6386
- var _a;
6387
- const $el = $(el);
6388
- const tag = ((_a = el.tagName) == null ? void 0 : _a.toLowerCase()) || "";
6389
- const cls = $el.attr("class");
6390
- const id = $el.attr("id");
6391
- if (id) return `${tag}#${id}`;
6392
- if (cls) return `${tag}.${cls.split(/\s+/)[0]}`;
6393
- const href = $el.attr("href");
6394
- if (href) return `${tag}[href]`;
6395
- return tag;
6228
+ return null;
6229
+ }
6230
+ function isSpaceSyntaxRgb(args) {
6231
+ const inner = args.trim();
6232
+ return !inner.includes(",") && /^\d/.test(inner);
6233
+ }
6234
+ function convertModernColors(value) {
6235
+ let result = value;
6236
+ let match;
6237
+ MODERN_COLOR_RE.lastIndex = 0;
6238
+ const replacements = [];
6239
+ while ((match = MODERN_COLOR_RE.exec(result)) !== null) {
6240
+ const fnName = match[1].toLowerCase();
6241
+ const fnStart = match.index;
6242
+ const openParen = fnStart + match[0].length - 1;
6243
+ const fullCall = extractFunctionCall(result, fnStart, openParen);
6244
+ if (!fullCall) continue;
6245
+ if (fnName === "rgb" || fnName === "rgba") {
6246
+ const argsStr = fullCall.slice(fullCall.indexOf("(") + 1, -1);
6247
+ if (!isSpaceSyntaxRgb(argsStr)) continue;
6248
+ }
6249
+ const parsed = parseColor(fullCall);
6250
+ if (parsed) {
6251
+ replacements.push({
6252
+ start: fnStart,
6253
+ end: fnStart + fullCall.length,
6254
+ replacement: formatRgb(parsed)
6255
+ });
6256
+ }
6396
6257
  }
6397
- for (const feature of HTML_ELEMENT_FEATURES) {
6398
- const selector = HTML_ELEMENT_SELECTORS[feature];
6399
- if (!selector) continue;
6400
- if ($(selector).length === 0) continue;
6401
- const supportData = CSS_SUPPORT[feature];
6402
- if (!supportData) continue;
6403
- const baseSeverity = HTML_ELEMENT_SEVERITY[feature] || "warning";
6404
- for (const client of EMAIL_CLIENTS) {
6405
- const support = supportData[client.id];
6406
- if (support === "unsupported") {
6407
- const msgFn = HTML_ELEMENT_MESSAGES[feature];
6408
- const message = msgFn ? msgFn(client.name) : `${client.name} does not support ${feature}.`;
6409
- const sug = getSuggestion(feature, client.id, framework);
6410
- const fix = getCodeFix(feature, client.id, framework);
6411
- addWarning(__spreadValues({
6412
- severity: baseSeverity,
6413
- client: client.id,
6414
- property: feature,
6415
- message,
6416
- suggestion: sug.text,
6417
- fix,
6418
- fixType: getFixType(feature)
6419
- }, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(feature, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
6420
- } else if (support === "partial" && feature === "<style>") {
6421
- const sug = getSuggestion("<style>:partial", client.id, framework);
6422
- const fix = getCodeFix("<style>", client.id, framework);
6423
- addWarning(__spreadValues({
6424
- severity: "warning",
6425
- client: client.id,
6426
- property: "<style>",
6427
- message: `${client.name} has partial <style> support (head only, with limitations). Inline styles recommended.`,
6428
- suggestion: sug.text,
6429
- fix,
6430
- fixType: getFixType("<style>")
6431
- }, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<style>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
6258
+ for (let i = replacements.length - 1; i >= 0; i--) {
6259
+ const r = replacements[i];
6260
+ result = result.slice(0, r.start) + r.replacement + result.slice(r.end);
6261
+ }
6262
+ return result;
6263
+ }
6264
+ function convertLogicalProperty(prop, value) {
6265
+ const mapping = LOGICAL_PROPERTY_MAP[prop];
6266
+ if (!mapping) return null;
6267
+ const parts = value.trim().split(/\s+/);
6268
+ const [first, second] = mapping;
6269
+ if (parts.length >= 2) {
6270
+ return [
6271
+ [first, parts[0]],
6272
+ [second, parts[1]]
6273
+ ];
6274
+ }
6275
+ return [
6276
+ [first, parts[0]],
6277
+ [second, parts[0]]
6278
+ ];
6279
+ }
6280
+ var CALC_INFINITY_RE = /calc\(\s*infinity\s*\*\s*1(px|em|rem|%|vh|vw)\s*\)/gi;
6281
+ var CALC_INFINITY_SIMPLE_RE = /calc\(\s*infinity\s*\)/gi;
6282
+ function resolveCalcInfinity(value) {
6283
+ let result = value.replace(CALC_INFINITY_RE, (_match, unit) => {
6284
+ return `9999${unit}`;
6285
+ });
6286
+ result = result.replace(CALC_INFINITY_SIMPLE_RE, "9999px");
6287
+ return result;
6288
+ }
6289
+ function unnestCSSRules(css) {
6290
+ let result = "";
6291
+ let i = 0;
6292
+ while (i < css.length) {
6293
+ const atMediaInside = findNestedAtRule(css, i);
6294
+ if (atMediaInside) {
6295
+ result += css.slice(i, atMediaInside.outerStart);
6296
+ if (atMediaInside.preamble) {
6297
+ result += `${atMediaInside.selector}{${atMediaInside.preamble}}`;
6432
6298
  }
6299
+ result += `${atMediaInside.atRule}{${atMediaInside.selector}{${atMediaInside.body}}}`;
6300
+ i = atMediaInside.outerEnd;
6301
+ } else {
6302
+ result += css.slice(i);
6303
+ break;
6433
6304
  }
6434
6305
  }
6435
- const parsedAtRules = /* @__PURE__ */ new Set();
6436
- const parsedProperties = /* @__PURE__ */ new Set();
6437
- const propertyLines = /* @__PURE__ */ new Map();
6438
- const detectedCssFunctions = /* @__PURE__ */ new Set();
6439
- const detectedPseudoClasses = /* @__PURE__ */ new Set();
6440
- const detectedPseudoElements = /* @__PURE__ */ new Set();
6441
- $("style").each((_, el) => {
6442
- const cssText = $(el).text();
6443
- try {
6444
- const ast = csstree2.parse(cssText, { parseCustomProperty: true, positions: true });
6445
- csstree2.walk(ast, {
6446
- enter(node) {
6447
- if (node.type === "Atrule") {
6448
- parsedAtRules.add(`@${node.name}`);
6449
- }
6450
- if (node.type === "PseudoClassSelector") {
6451
- detectedPseudoClasses.add(`:${node.name}`);
6452
- }
6453
- if (node.type === "PseudoElementSelector") {
6454
- detectedPseudoElements.add(`::${node.name}`);
6306
+ return result;
6307
+ }
6308
+ function findNestedAtRule(css, startPos) {
6309
+ for (let i = startPos; i < css.length; i++) {
6310
+ if (css[i] === "{") {
6311
+ const selector = css.slice(startPos, i).trim();
6312
+ if (!selector || selector.startsWith("@")) {
6313
+ const closeIdx2 = findMatchingBrace(css, i);
6314
+ if (closeIdx2 === -1) return null;
6315
+ const rest = findNestedAtRule(css, closeIdx2 + 1);
6316
+ if (rest) return rest;
6317
+ return null;
6318
+ }
6319
+ const innerStart = i + 1;
6320
+ const closeIdx = findMatchingBrace(css, i);
6321
+ if (closeIdx === -1) return null;
6322
+ const innerContent = css.slice(innerStart, closeIdx).trim();
6323
+ const atMatch = innerContent.match(/@(media|supports)\s/);
6324
+ if (atMatch && atMatch.index !== void 0) {
6325
+ const atPos = atMatch.index;
6326
+ const atBraceIdx = innerContent.indexOf("{", atPos);
6327
+ if (atBraceIdx === -1) continue;
6328
+ const atRule = innerContent.slice(atPos, atBraceIdx).trim();
6329
+ const atCloseIdx = findMatchingBrace(innerContent, atBraceIdx);
6330
+ if (atCloseIdx === -1) continue;
6331
+ const body = innerContent.slice(atBraceIdx + 1, atCloseIdx).trim();
6332
+ const beforeAt = innerContent.slice(0, atPos).trim();
6333
+ return __spreadValues({
6334
+ outerStart: findSelectorStart(css, i, startPos),
6335
+ outerEnd: closeIdx + 1,
6336
+ selector,
6337
+ atRule,
6338
+ body
6339
+ }, beforeAt ? { preamble: beforeAt } : {});
6340
+ }
6341
+ }
6342
+ }
6343
+ return null;
6344
+ }
6345
+ function findSelectorStart(css, braceIdx, minPos) {
6346
+ let i = braceIdx - 1;
6347
+ while (i >= minPos && css[i] !== "}" && css[i] !== ";") {
6348
+ i--;
6349
+ }
6350
+ return i + 1;
6351
+ }
6352
+ function findMatchingBrace(css, openIdx) {
6353
+ let depth = 0;
6354
+ for (let i = openIdx; i < css.length; i++) {
6355
+ if (css[i] === "{") depth++;
6356
+ else if (css[i] === "}") {
6357
+ depth--;
6358
+ if (depth === 0) return i;
6359
+ }
6360
+ }
6361
+ return -1;
6362
+ }
6363
+ function convertRangeMediaQueries(css) {
6364
+ return css.replace(
6365
+ /\(\s*width\s*(>=?|<=?)\s*([\d.]+\w*)\s*\)/g,
6366
+ (_match, op, value) => {
6367
+ if (op === ">=" || op === ">") {
6368
+ return `(min-width: ${value})`;
6369
+ }
6370
+ return `(max-width: ${value})`;
6371
+ }
6372
+ );
6373
+ }
6374
+ var UNIVERSAL_SELECTORS = /* @__PURE__ */ new Set([":root", "*", "html"]);
6375
+ function selectorsCouldMatch(defSelector, useSelector) {
6376
+ if (defSelector === useSelector) return true;
6377
+ if (UNIVERSAL_SELECTORS.has(defSelector)) return true;
6378
+ return false;
6379
+ }
6380
+ function resolveCSSVariables(ast) {
6381
+ const varDefs = [];
6382
+ csstree.walk(ast, {
6383
+ visit: "Rule",
6384
+ enter(rule) {
6385
+ if (rule.type !== "Rule" || !rule.prelude || !rule.block) return;
6386
+ const selector = csstree.generate(rule.prelude);
6387
+ rule.block.children.forEach((node) => {
6388
+ if (node.type === "Declaration" && node.property.startsWith("--")) {
6389
+ varDefs.push({
6390
+ selector,
6391
+ varName: node.property,
6392
+ value: csstree.generate(node.value)
6393
+ });
6394
+ }
6395
+ });
6396
+ }
6397
+ });
6398
+ if (varDefs.length === 0) return;
6399
+ csstree.walk(ast, {
6400
+ visit: "Rule",
6401
+ enter(rule) {
6402
+ if (rule.type !== "Rule" || !rule.prelude || !rule.block) return;
6403
+ const useSelector = csstree.generate(rule.prelude);
6404
+ csstree.walk(rule.block, {
6405
+ visit: "Function",
6406
+ enter(node, item, list) {
6407
+ if (node.name !== "var" || !list) return;
6408
+ const args = [];
6409
+ if (node.children) {
6410
+ node.children.forEach((child) => {
6411
+ args.push(csstree.generate(child));
6412
+ });
6455
6413
  }
6456
- if (node.type === "Declaration") {
6457
- const prop = node.property.toLowerCase();
6458
- parsedProperties.add(prop);
6459
- if (node.loc && !propertyLines.has(prop)) {
6460
- propertyLines.set(prop, node.loc.start.line);
6414
+ const fullArgs = args.join("");
6415
+ const commaIdx = fullArgs.indexOf(",");
6416
+ const varName = commaIdx !== -1 ? fullArgs.slice(0, commaIdx).trim() : fullArgs.trim();
6417
+ const fallback = commaIdx !== -1 ? fullArgs.slice(commaIdx + 1).trim() : null;
6418
+ let resolved = null;
6419
+ for (const def of varDefs) {
6420
+ if (def.varName === varName && def.selector === useSelector) {
6421
+ resolved = def.value;
6422
+ break;
6461
6423
  }
6462
- const valueStr = csstree2.generate(node.value);
6463
- for (const det of COMPOUND_DETECTORS) {
6464
- if (prop === det.property && valueStr.includes(det.valueIncludes)) {
6465
- parsedProperties.add(det.key);
6466
- if (node.loc && !propertyLines.has(det.key)) {
6467
- propertyLines.set(det.key, node.loc.start.line);
6468
- }
6424
+ }
6425
+ if (!resolved) {
6426
+ for (const def of varDefs) {
6427
+ if (def.varName === varName && selectorsCouldMatch(def.selector, useSelector)) {
6428
+ resolved = def.value;
6429
+ break;
6469
6430
  }
6470
6431
  }
6471
- for (const fn of CSS_FUNCTION_DETECTORS) {
6472
- if (valueStr.includes(fn.pattern)) {
6473
- detectedCssFunctions.add(fn.key);
6474
- if (node.loc && !propertyLines.has(fn.key)) {
6475
- propertyLines.set(fn.key, node.loc.start.line);
6476
- }
6477
- }
6432
+ }
6433
+ if (!resolved && fallback) {
6434
+ resolved = fallback;
6435
+ }
6436
+ if (resolved && item) {
6437
+ const replacement = csstree.parse(resolved, {
6438
+ context: "value"
6439
+ });
6440
+ if (replacement.children) {
6441
+ replacement.children.forEach((child) => {
6442
+ list.insertData(child, item);
6443
+ });
6444
+ list.remove(item);
6478
6445
  }
6479
6446
  }
6480
6447
  }
6481
6448
  });
6482
- } catch (e) {
6483
6449
  }
6484
6450
  });
6485
- for (const atRule of AT_RULE_FEATURES) {
6486
- if (!parsedAtRules.has(atRule)) continue;
6487
- checkPropertySupport(atRule, addWarning, framework);
6488
- }
6489
- const cssPropertiesToCheck = Object.keys(CSS_SUPPORT).filter(
6490
- (k) => !k.startsWith("<") && !k.startsWith("@")
6491
- );
6492
- $("[style]").each((_, el) => {
6493
- const style = $(el).attr("style") || "";
6494
- const props = parseStyleProperties(style);
6495
- const selector = describeSelector(el);
6496
- for (const prop of props) {
6497
- for (const det of COMPOUND_DETECTORS) {
6498
- if (prop === det.property) {
6499
- const value2 = getStyleValue(style, prop);
6500
- if (value2 == null ? void 0 : value2.includes(det.valueIncludes)) {
6501
- checkPropertySupport(det.key, addWarning, framework, selector);
6451
+ }
6452
+ function sanitizeDeclarations(ast) {
6453
+ csstree.walk(ast, {
6454
+ visit: "Declaration",
6455
+ enter(decl) {
6456
+ if (decl.property.startsWith("--")) return;
6457
+ if (decl.value.type === "Value") {
6458
+ const raw = csstree.generate(decl.value);
6459
+ let converted = convertModernColors(raw);
6460
+ converted = resolveCalcInfinity(converted);
6461
+ if (converted !== raw) {
6462
+ try {
6463
+ const newValue = csstree.parse(converted, {
6464
+ context: "value"
6465
+ });
6466
+ decl.value = newValue;
6467
+ } catch (e) {
6502
6468
  }
6503
6469
  }
6504
6470
  }
6505
- if (cssPropertiesToCheck.includes(prop)) {
6506
- checkPropertySupport(prop, addWarning, framework, selector);
6471
+ }
6472
+ });
6473
+ csstree.walk(ast, {
6474
+ visit: "Rule",
6475
+ enter(rule) {
6476
+ if (rule.type !== "Rule" || !rule.block) return;
6477
+ const toInsert = [];
6478
+ rule.block.children.forEach(function(node, item) {
6479
+ if (node.type !== "Declaration") return;
6480
+ const mapping = LOGICAL_PROPERTY_MAP[node.property];
6481
+ if (!mapping) return;
6482
+ const valueStr = csstree.generate(node.value);
6483
+ const parts = valueStr.trim().split(/\s+/);
6484
+ const [first, second] = mapping;
6485
+ const val1 = parts[0];
6486
+ const val2 = parts.length >= 2 ? parts[1] : parts[0];
6487
+ toInsert.push(
6488
+ { property: first, value: val1, important: !!node.important, item },
6489
+ { property: second, value: val2, important: !!node.important, item }
6490
+ );
6491
+ });
6492
+ for (const { property, value, important, item } of toInsert) {
6493
+ try {
6494
+ const newDecl = {
6495
+ type: "Declaration",
6496
+ important,
6497
+ property,
6498
+ value: csstree.parse(value, { context: "value" })
6499
+ };
6500
+ rule.block.children.insertData(newDecl, item);
6501
+ } catch (e) {
6502
+ }
6507
6503
  }
6508
- const value = getStyleValue(style, prop);
6509
- if (value) {
6510
- for (const fn of CSS_FUNCTION_DETECTORS) {
6511
- if (value.includes(fn.pattern)) {
6512
- checkPropertySupport(fn.key, addWarning, framework, selector);
6513
- }
6504
+ const removed = /* @__PURE__ */ new Set();
6505
+ for (const { item } of toInsert) {
6506
+ if (!removed.has(item)) {
6507
+ removed.add(item);
6508
+ rule.block.children.remove(item);
6514
6509
  }
6515
6510
  }
6516
6511
  }
6517
6512
  });
6518
- for (const prop of parsedProperties) {
6519
- if (prop.includes(":")) continue;
6520
- if (!cssPropertiesToCheck.includes(prop)) continue;
6521
- checkPropertySupport(prop, addWarning, framework, void 0, propertyLines.get(prop));
6522
- }
6523
- for (const compound of COMPOUND_VALUE_FEATURES) {
6524
- if (compound.startsWith(":") || compound.startsWith("::")) continue;
6525
- if (parsedProperties.has(compound)) {
6526
- checkPropertySupport(compound, addWarning, framework, void 0, propertyLines.get(compound));
6513
+ }
6514
+ function downlevelCSS(html) {
6515
+ const $ = cheerio.load(html);
6516
+ $("style").each((_, el) => {
6517
+ let css = $(el).text();
6518
+ css = unnestCSSRules(css);
6519
+ css = convertRangeMediaQueries(css);
6520
+ try {
6521
+ const ast = csstree.parse(css, { parseCustomProperty: true });
6522
+ resolveCSSVariables(ast);
6523
+ sanitizeDeclarations(ast);
6524
+ $(el).text(csstree.generate(ast));
6525
+ } catch (e) {
6526
+ $(el).text(css);
6527
6527
  }
6528
- }
6529
- for (const pseudo of detectedPseudoClasses) {
6530
- if (CSS_SUPPORT[pseudo]) {
6531
- checkPropertySupport(pseudo, addWarning, framework);
6528
+ });
6529
+ $("[style]").each((_, el) => {
6530
+ const style = $(el).attr("style") || "";
6531
+ const props = parseInlineStyle(style);
6532
+ let changed = false;
6533
+ const newProps = /* @__PURE__ */ new Map();
6534
+ props.forEach((value, prop) => {
6535
+ const converted = convertModernColors(value);
6536
+ if (converted !== value) changed = true;
6537
+ const physical = convertLogicalProperty(prop, converted);
6538
+ if (physical) {
6539
+ changed = true;
6540
+ for (const [p, v] of physical) {
6541
+ newProps.set(p, v);
6542
+ }
6543
+ } else {
6544
+ const calcFixed = resolveCalcInfinity(converted);
6545
+ if (calcFixed !== converted) changed = true;
6546
+ newProps.set(prop, calcFixed);
6547
+ }
6548
+ });
6549
+ if (changed) {
6550
+ $(el).attr("style", serializeStyle(newProps));
6532
6551
  }
6533
- }
6534
- for (const pseudo of detectedPseudoElements) {
6535
- if (CSS_SUPPORT[pseudo]) {
6536
- checkPropertySupport(pseudo, addWarning, framework);
6552
+ });
6553
+ return $.html();
6554
+ }
6555
+
6556
+ // src/constants.ts
6557
+ var MAX_HTML_SIZE = 2 * 1024 * 1024;
6558
+ var GENERIC_LINK_TEXT = /* @__PURE__ */ new Set([
6559
+ "click here",
6560
+ "here",
6561
+ "read more",
6562
+ "learn more",
6563
+ "more",
6564
+ "link",
6565
+ "this link",
6566
+ "click",
6567
+ "tap here",
6568
+ "this"
6569
+ ]);
6570
+ var GMAIL_CLIP_THRESHOLD = 102 * 1024;
6571
+ var GMAIL_CLIP_WARNING_THRESHOLD = 90 * 1024;
6572
+ var CLIENT_DISPLAY_LIMITS = [
6573
+ { client: "Gmail (Web)", subjectLimit: 70, preheaderLimit: 90 },
6574
+ { client: "Gmail (Mobile)", subjectLimit: 40, preheaderLimit: 45 },
6575
+ { client: "Outlook (Web)", subjectLimit: 60, preheaderLimit: 90 },
6576
+ { client: "Outlook (Desktop)", subjectLimit: 55, preheaderLimit: 35 },
6577
+ { client: "Apple Mail (macOS)", subjectLimit: 78, preheaderLimit: 140 },
6578
+ { client: "Apple Mail (iOS)", subjectLimit: 41, preheaderLimit: 90 },
6579
+ { client: "Yahoo Mail", subjectLimit: 46, preheaderLimit: 100 },
6580
+ { client: "Samsung Email", subjectLimit: 40, preheaderLimit: 70 }
6581
+ ];
6582
+ var TEMPLATE_VARIABLE_PATTERNS = [
6583
+ [/\{\{[\s\S]*?\}\}/g, "Handlebars/Mustache"],
6584
+ // {{var}}
6585
+ [/\$\{[^}]+\}/g, "ES template literal"],
6586
+ // ${var}
6587
+ [/<%=?\s*[^%]+%>/g, "ERB/EJS"],
6588
+ // <% %> / <%= %>
6589
+ [/\*\|[A-Z_][A-Z0-9_]*\|\*/g, "Mailchimp merge tag"],
6590
+ // *|TAG|*
6591
+ [/%%[A-Za-z_][A-Za-z0-9_]*%%/g, "Salesforce AMPscript"],
6592
+ // %%tag%%
6593
+ [/\{[A-Za-z_][A-Za-z0-9_.]{2,}\}/g, "Single-brace merge field"]
6594
+ // {merge_field} (3+ char names)
6595
+ ];
6596
+ var EMPTY_SPAM = { score: 100, level: "low", issues: [] };
6597
+ var EMPTY_LINKS = {
6598
+ totalLinks: 0,
6599
+ issues: [],
6600
+ breakdown: { https: 0, http: 0, mailto: 0, tel: 0, anchor: 0, javascript: 0, protocolRelative: 0, other: 0 }
6601
+ };
6602
+ var EMPTY_ACCESSIBILITY = { score: 100, issues: [] };
6603
+ var EMPTY_IMAGES = { total: 0, totalDataUriBytes: 0, issues: [], images: [] };
6604
+ var EMPTY_INBOX_PREVIEW = { subject: null, preheader: null, subjectLength: 0, preheaderLength: 0, truncation: [], issues: [] };
6605
+ var EMPTY_SIZE = { htmlBytes: 0, humanSize: "0 B", clipped: false, issues: [] };
6606
+ var EMPTY_TEMPLATE = { unresolvedCount: 0, issues: [] };
6607
+ var EMPTY_DELIVERABILITY = { domain: "", checks: [], score: 0, issues: [] };
6608
+
6609
+ // src/transform.ts
6610
+ function hasPseudoSelector(selector) {
6611
+ return new RegExp("(?<![[\\w])::?[a-z][\\w-]*(?:\\(|(?=[^(]))", "i").test(selector);
6612
+ }
6613
+ function addImportantToDeclarations(node) {
6614
+ csstree2.walk(node, {
6615
+ visit: "Declaration",
6616
+ enter(decl) {
6617
+ if (decl.type !== "Declaration") return;
6618
+ if (decl.important) return;
6619
+ decl.important = true;
6537
6620
  }
6538
- }
6539
- for (const fn of detectedCssFunctions) {
6540
- checkPropertySupport(fn, addWarning, framework, void 0, propertyLines.get(fn));
6541
- }
6542
- const severityOrder = { error: 0, warning: 1, info: 2 };
6543
- warnings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
6544
- return warnings;
6621
+ });
6545
6622
  }
6546
- function analyzeEmail(html, framework) {
6547
- if (!html || !html.trim()) {
6548
- return [];
6549
- }
6550
- if (html.length > MAX_HTML_SIZE) {
6551
- throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
6623
+ function inlineStyles($) {
6624
+ const styleBlocks = [];
6625
+ $("style").each((_, el) => {
6626
+ styleBlocks.push($(el).text());
6627
+ });
6628
+ if (styleBlocks.length === 0) return "";
6629
+ const preserved = [];
6630
+ for (const block of styleBlocks) {
6631
+ let ast;
6632
+ try {
6633
+ ast = csstree2.parse(block, { parseCustomProperty: true });
6634
+ } catch (e) {
6635
+ continue;
6636
+ }
6637
+ csstree2.walk(ast, {
6638
+ visit: "Atrule",
6639
+ enter(node) {
6640
+ if (node.type !== "Atrule") return;
6641
+ const name = node.name.toLowerCase();
6642
+ if (name === "media" || name === "supports" || name === "keyframes" || name === "-webkit-keyframes" || name === "font-face") {
6643
+ if (name === "media" || name === "supports") {
6644
+ addImportantToDeclarations(node);
6645
+ }
6646
+ preserved.push(csstree2.generate(node));
6647
+ return this.skip;
6648
+ }
6649
+ }
6650
+ });
6651
+ csstree2.walk(ast, {
6652
+ visit: "Rule",
6653
+ enter(node) {
6654
+ if (node.type !== "Rule" || node.prelude.type !== "SelectorList") return;
6655
+ const declarations = csstree2.generate(node.block);
6656
+ const declText = declarations.slice(1, -1).trim();
6657
+ if (!declText) return;
6658
+ const selectorText = csstree2.generate(node.prelude);
6659
+ if (hasPseudoSelector(selectorText)) {
6660
+ preserved.push(csstree2.generate(node));
6661
+ return;
6662
+ }
6663
+ try {
6664
+ $(selectorText).each((_, el) => {
6665
+ const existing = $(el).attr("style") || "";
6666
+ $(el).attr("style", existing ? `${existing}; ${declText}` : declText);
6667
+ });
6668
+ } catch (e) {
6669
+ }
6670
+ }
6671
+ });
6552
6672
  }
6553
- const $ = cheerio2.load(html);
6554
- return analyzeEmailFromDom($, framework);
6673
+ return preserved.join("\n");
6555
6674
  }
6556
- function getFixType(prop) {
6557
- return STRUCTURAL_FIX_PROPERTIES.has(prop) ? "structural" : "css";
6675
+ function makeWarning(base, prop, clientId, framework) {
6676
+ const sug = getSuggestion(prop, clientId, framework);
6677
+ const fix = getCodeFix(prop, clientId, framework);
6678
+ const isFallback = framework && ((sug == null ? void 0 : sug.isGenericFallback) || fix && isCodeFixGenericFallback(prop, clientId, framework));
6679
+ return __spreadProps(__spreadValues(__spreadValues(__spreadValues(__spreadValues({}, base), sug ? { suggestion: sug.text } : {}), fix ? { fix } : {}), isFallback ? { fixIsGenericFallback: true } : {}), {
6680
+ fixType: STRUCTURAL_FIX_PROPERTIES.has(prop) ? "structural" : "css"
6681
+ });
6558
6682
  }
6559
- function checkPropertySupport(prop, addWarning, framework, selector, line) {
6560
- const supportData = CSS_SUPPORT[prop];
6561
- if (!supportData) return;
6562
- const fixType = getFixType(prop);
6563
- for (const client of EMAIL_CLIENTS) {
6564
- const support = supportData[client.id] || "unknown";
6565
- if (support === "unsupported") {
6566
- const sug = getSuggestion(prop, client.id, framework);
6567
- const fix = getCodeFix(prop, client.id, framework);
6568
- addWarning(__spreadValues(__spreadValues(__spreadValues({
6569
- severity: "warning",
6570
- client: client.id,
6571
- property: prop,
6572
- message: `${client.name} does not support "${prop}".`,
6573
- suggestion: sug.text,
6574
- fix,
6575
- fixType
6576
- }, selector ? { selector } : {}), line !== void 0 ? { line } : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
6577
- } else if (support === "partial") {
6578
- const sug = getSuggestion(prop, client.id, framework);
6579
- const fix = getCodeFix(prop, client.id, framework);
6580
- addWarning(__spreadValues(__spreadValues(__spreadValues({
6581
- severity: "info",
6582
- client: client.id,
6583
- property: prop,
6584
- message: `${client.name} has partial support for "${prop}".`,
6585
- suggestion: sug.text,
6586
- fix,
6587
- fixType
6588
- }, selector ? { selector } : {}), line !== void 0 ? { line } : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
6589
- }
6683
+ function detectAnimations($) {
6684
+ let found = false;
6685
+ $("[style]").each((_, el) => {
6686
+ const style = $(el).attr("style") || "";
6687
+ const props = parseInlineStyle(style);
6688
+ props.forEach((_2, prop) => {
6689
+ if (prop === "animation" || prop === "transition" || prop.startsWith("animation-") || prop.startsWith("transition-")) {
6690
+ found = true;
6691
+ }
6692
+ });
6693
+ });
6694
+ if (!found) {
6695
+ $("style").each((_, el) => {
6696
+ try {
6697
+ const ast = csstree2.parse($(el).text());
6698
+ csstree2.walk(ast, {
6699
+ enter(node) {
6700
+ if (node.type === "Declaration") {
6701
+ const prop = node.property.toLowerCase();
6702
+ if (prop === "animation" || prop === "transition" || prop.startsWith("animation-") || prop.startsWith("transition-")) {
6703
+ found = true;
6704
+ }
6705
+ }
6706
+ }
6707
+ });
6708
+ } catch (e) {
6709
+ }
6710
+ });
6590
6711
  }
6712
+ return found;
6591
6713
  }
6592
- function generateCompatibilityScore(warnings) {
6593
- const result = {};
6594
- for (const client of EMAIL_CLIENTS) {
6595
- const clientWarnings = warnings.filter((w) => w.client === client.id);
6596
- const errorProps = new Set(clientWarnings.filter((w) => w.severity === "error").map((w) => w.property));
6597
- const warnProps = new Set(clientWarnings.filter((w) => w.severity === "warning").map((w) => w.property));
6598
- const infoProps = new Set(clientWarnings.filter((w) => w.severity === "info").map((w) => w.property));
6599
- const errors = errorProps.size;
6600
- const warns = warnProps.size;
6601
- const info = infoProps.size;
6602
- const score = Math.max(0, Math.min(100, 100 - errors * 10 - warns * 3));
6603
- result[client.id] = { score, errors, warnings: warns, info };
6714
+ function gmailAdditionalChecks($, clientId, _html, framework) {
6715
+ const warnings = [];
6716
+ let hasAtFontFace = false;
6717
+ $("style").each((_, el) => {
6718
+ try {
6719
+ const ast = csstree2.parse($(el).text());
6720
+ csstree2.walk(ast, {
6721
+ enter(node) {
6722
+ if (node.type === "Atrule" && node.name === "font-face") {
6723
+ hasAtFontFace = true;
6724
+ }
6725
+ }
6726
+ });
6727
+ } catch (e) {
6728
+ }
6729
+ });
6730
+ if (hasAtFontFace) {
6731
+ warnings.push(makeWarning({
6732
+ severity: "warning",
6733
+ client: clientId,
6734
+ property: "@font-face",
6735
+ message: "Gmail does not support custom web fonts."
6736
+ }, "@font-face", clientId, framework));
6604
6737
  }
6605
- return result;
6738
+ return warnings;
6606
6739
  }
6607
- function warningsForClient(warnings, clientId) {
6608
- return warnings.filter((w) => w.client === clientId);
6740
+ function gmailPostChecks($, clientId, _html, framework) {
6741
+ const warnings = [];
6742
+ $("*").contents().filter(function() {
6743
+ return this.type === "comment";
6744
+ }).each(function() {
6745
+ const commentText = this.data || "";
6746
+ if (commentText.includes("<style") || commentText.includes("[if mso]") || commentText.includes("[if gte mso")) {
6747
+ $(this).remove();
6748
+ }
6749
+ });
6750
+ const styleSug = getSuggestion("<style>:partial", clientId, framework);
6751
+ warnings.push({
6752
+ severity: "info",
6753
+ client: clientId,
6754
+ property: "<style>",
6755
+ message: "Gmail partially supports <style> blocks (head only, 16KB limit). Inlining recommended for safety.",
6756
+ suggestion: styleSug.text
6757
+ });
6758
+ return warnings;
6609
6759
  }
6610
- function errorWarnings(warnings) {
6611
- return warnings.filter((w) => w.severity === "error");
6760
+ function outlookWindowsAdditionalChecks($, clientId, _html, framework) {
6761
+ const warnings = [];
6762
+ if ($("[style*='border-radius']").length > 0) {
6763
+ warnings.push(makeWarning({
6764
+ severity: "warning",
6765
+ client: clientId,
6766
+ property: "border-radius",
6767
+ message: "Outlook Classic ignores border-radius. Buttons and containers will have sharp corners."
6768
+ }, "border-radius", clientId, framework));
6769
+ }
6770
+ if ($("[style*='max-width']").length > 0) {
6771
+ warnings.push(makeWarning({
6772
+ severity: "warning",
6773
+ client: clientId,
6774
+ property: "max-width",
6775
+ message: "Outlook Classic ignores max-width."
6776
+ }, "max-width", clientId, framework));
6777
+ }
6778
+ const hasDivLayout = $("div[style*='display']").length > 0 || $("div[style*='flex']").length > 0 || $("div[style*='grid']").length > 0;
6779
+ if (hasDivLayout) {
6780
+ warnings.push(makeWarning({
6781
+ severity: "error",
6782
+ client: clientId,
6783
+ property: "display:flex",
6784
+ message: "Outlook Classic uses Microsoft Word for rendering. Flexbox and Grid layouts will break."
6785
+ }, "display:flex", clientId, framework));
6786
+ }
6787
+ if ($("[style*='background-image']").length > 0 || $("[style*='background:']").filter(
6788
+ (_, el) => ($(el).attr("style") || "").includes("url(")
6789
+ ).length > 0) {
6790
+ warnings.push(makeWarning({
6791
+ severity: "warning",
6792
+ client: clientId,
6793
+ property: "background-image",
6794
+ message: "Outlook Classic requires VML for background images."
6795
+ }, "background-image", clientId, framework));
6796
+ }
6797
+ return warnings;
6612
6798
  }
6613
- function structuralWarnings(warnings) {
6614
- return warnings.filter((w) => w.fixType === "structural");
6799
+ function appleMailAdditionalChecks($, clientId) {
6800
+ const warnings = [];
6801
+ const imgsWithTransparentBg = $("img").filter((_, el) => {
6802
+ const src = $(el).attr("src") || "";
6803
+ return src.endsWith(".png") || src.endsWith(".svg");
6804
+ });
6805
+ if (imgsWithTransparentBg.length > 0) {
6806
+ warnings.push({
6807
+ severity: "info",
6808
+ client: clientId,
6809
+ property: "dark-mode",
6810
+ message: "PNG/SVG images with transparent backgrounds may become invisible in Apple Mail dark mode.",
6811
+ suggestion: "Add a white background or padding around images, or use dark-mode-friendly image variants."
6812
+ });
6813
+ }
6814
+ return warnings;
6615
6815
  }
6616
-
6617
- // src/dark-mode.ts
6618
- import * as cheerio3 from "cheerio";
6619
- import * as csstree3 from "css-tree";
6620
-
6621
- // src/color-utils.ts
6622
- var NAMED_COLORS = {
6623
- aliceblue: [240, 248, 255],
6624
- antiquewhite: [250, 235, 215],
6625
- aqua: [0, 255, 255],
6626
- aquamarine: [127, 255, 212],
6627
- azure: [240, 255, 255],
6628
- beige: [245, 245, 220],
6629
- bisque: [255, 228, 196],
6630
- black: [0, 0, 0],
6631
- blanchedalmond: [255, 235, 205],
6632
- blue: [0, 0, 255],
6633
- blueviolet: [138, 43, 226],
6634
- brown: [165, 42, 42],
6635
- burlywood: [222, 184, 135],
6636
- cadetblue: [95, 158, 160],
6637
- chartreuse: [127, 255, 0],
6638
- chocolate: [210, 105, 30],
6639
- coral: [255, 127, 80],
6640
- cornflowerblue: [100, 149, 237],
6641
- cornsilk: [255, 248, 220],
6642
- crimson: [220, 20, 60],
6643
- cyan: [0, 255, 255],
6644
- darkblue: [0, 0, 139],
6645
- darkcyan: [0, 139, 139],
6646
- darkgoldenrod: [184, 134, 11],
6647
- darkgray: [169, 169, 169],
6648
- darkgreen: [0, 100, 0],
6649
- darkgrey: [169, 169, 169],
6650
- darkkhaki: [189, 183, 107],
6651
- darkmagenta: [139, 0, 139],
6652
- darkolivegreen: [85, 107, 47],
6653
- darkorange: [255, 140, 0],
6654
- darkorchid: [153, 50, 204],
6655
- darkred: [139, 0, 0],
6656
- darksalmon: [233, 150, 122],
6657
- darkseagreen: [143, 188, 143],
6658
- darkslateblue: [72, 61, 139],
6659
- darkslategray: [47, 79, 79],
6660
- darkslategrey: [47, 79, 79],
6661
- darkturquoise: [0, 206, 209],
6662
- darkviolet: [148, 0, 211],
6663
- deeppink: [255, 20, 147],
6664
- deepskyblue: [0, 191, 255],
6665
- dimgray: [105, 105, 105],
6666
- dimgrey: [105, 105, 105],
6667
- dodgerblue: [30, 144, 255],
6668
- firebrick: [178, 34, 34],
6669
- floralwhite: [255, 250, 240],
6670
- forestgreen: [34, 139, 34],
6671
- fuchsia: [255, 0, 255],
6672
- gainsboro: [220, 220, 220],
6673
- ghostwhite: [248, 248, 255],
6674
- gold: [255, 215, 0],
6675
- goldenrod: [218, 165, 32],
6676
- gray: [128, 128, 128],
6677
- green: [0, 128, 0],
6678
- greenyellow: [173, 255, 47],
6679
- grey: [128, 128, 128],
6680
- honeydew: [240, 255, 240],
6681
- hotpink: [255, 105, 180],
6682
- indianred: [205, 92, 92],
6683
- indigo: [75, 0, 130],
6684
- ivory: [255, 255, 240],
6685
- khaki: [240, 230, 140],
6686
- lavender: [230, 230, 250],
6687
- lavenderblush: [255, 240, 245],
6688
- lawngreen: [124, 252, 0],
6689
- lemonchiffon: [255, 250, 205],
6690
- lightblue: [173, 216, 230],
6691
- lightcoral: [240, 128, 128],
6692
- lightcyan: [224, 255, 255],
6693
- lightgoldenrodyellow: [250, 250, 210],
6694
- lightgray: [211, 211, 211],
6695
- lightgreen: [144, 238, 144],
6696
- lightgrey: [211, 211, 211],
6697
- lightpink: [255, 182, 193],
6698
- lightsalmon: [255, 160, 122],
6699
- lightseagreen: [32, 178, 170],
6700
- lightskyblue: [135, 206, 250],
6701
- lightslategray: [119, 136, 153],
6702
- lightslategrey: [119, 136, 153],
6703
- lightsteelblue: [176, 196, 222],
6704
- lightyellow: [255, 255, 224],
6705
- lime: [0, 255, 0],
6706
- limegreen: [50, 205, 50],
6707
- linen: [250, 240, 230],
6708
- magenta: [255, 0, 255],
6709
- maroon: [128, 0, 0],
6710
- mediumaquamarine: [102, 205, 170],
6711
- mediumblue: [0, 0, 205],
6712
- mediumorchid: [186, 85, 211],
6713
- mediumpurple: [147, 111, 219],
6714
- mediumseagreen: [60, 179, 113],
6715
- mediumslateblue: [123, 104, 238],
6716
- mediumspringgreen: [0, 250, 154],
6717
- mediumturquoise: [72, 209, 204],
6718
- mediumvioletred: [199, 21, 133],
6719
- midnightblue: [25, 25, 112],
6720
- mintcream: [245, 255, 250],
6721
- mistyrose: [255, 228, 225],
6722
- moccasin: [255, 228, 181],
6723
- navajowhite: [255, 222, 173],
6724
- navy: [0, 0, 128],
6725
- oldlace: [253, 245, 230],
6726
- olive: [128, 128, 0],
6727
- olivedrab: [107, 142, 35],
6728
- orange: [255, 165, 0],
6729
- orangered: [255, 69, 0],
6730
- orchid: [218, 112, 214],
6731
- palegoldenrod: [238, 232, 170],
6732
- palegreen: [152, 251, 152],
6733
- paleturquoise: [175, 238, 238],
6734
- palevioletred: [219, 112, 147],
6735
- papayawhip: [255, 239, 213],
6736
- peachpuff: [255, 218, 185],
6737
- peru: [205, 133, 63],
6738
- pink: [255, 192, 203],
6739
- plum: [221, 160, 221],
6740
- powderblue: [176, 224, 230],
6741
- purple: [128, 0, 128],
6742
- rebeccapurple: [102, 51, 153],
6743
- red: [255, 0, 0],
6744
- rosybrown: [188, 143, 143],
6745
- royalblue: [65, 105, 225],
6746
- saddlebrown: [139, 69, 19],
6747
- salmon: [250, 128, 114],
6748
- sandybrown: [244, 164, 96],
6749
- seagreen: [46, 139, 87],
6750
- seashell: [255, 245, 238],
6751
- sienna: [160, 82, 45],
6752
- silver: [192, 192, 192],
6753
- skyblue: [135, 206, 235],
6754
- slateblue: [106, 90, 205],
6755
- slategray: [112, 128, 144],
6756
- slategrey: [112, 128, 144],
6757
- snow: [255, 250, 250],
6758
- springgreen: [0, 255, 127],
6759
- steelblue: [70, 130, 180],
6760
- tan: [210, 180, 140],
6761
- teal: [0, 128, 128],
6762
- thistle: [216, 191, 216],
6763
- tomato: [255, 99, 71],
6764
- turquoise: [64, 224, 208],
6765
- violet: [238, 130, 238],
6766
- wheat: [245, 222, 179],
6767
- white: [255, 255, 255],
6768
- whitesmoke: [245, 245, 245],
6769
- yellow: [255, 255, 0],
6770
- yellowgreen: [154, 205, 50]
6771
- };
6772
- function parseColor(value) {
6773
- if (!value) return null;
6774
- const v = value.trim().toLowerCase();
6775
- if (v === "inherit" || v === "currentcolor" || v === "initial" || v === "unset" || v.startsWith("var(")) {
6776
- return null;
6816
+ function yahooAdditionalChecks($, clientId, _html, framework) {
6817
+ const warnings = [];
6818
+ warnings.push({
6819
+ severity: "info",
6820
+ client: clientId,
6821
+ property: "class",
6822
+ message: "Yahoo Mail rewrites CSS class names with a prefix. Class-based selectors in <style> blocks will still work but the names change."
6823
+ });
6824
+ if ($("[style*='background']").filter(
6825
+ (_, el) => ($(el).attr("style") || "").includes("url(")
6826
+ ).length > 0) {
6827
+ warnings.push(makeWarning({
6828
+ severity: "warning",
6829
+ client: clientId,
6830
+ property: "background-image",
6831
+ message: "Yahoo Mail has inconsistent support for CSS background images."
6832
+ }, "background-image", clientId, framework));
6777
6833
  }
6778
- if (v === "transparent") {
6779
- return { r: 0, g: 0, b: 0, a: 0 };
6834
+ return warnings;
6835
+ }
6836
+ function thunderbirdAdditionalChecks($, clientId) {
6837
+ if (detectAnimations($)) {
6838
+ return [{
6839
+ severity: "info",
6840
+ client: clientId,
6841
+ property: "animation",
6842
+ message: "Thunderbird does not support CSS animations or transitions."
6843
+ }];
6844
+ }
6845
+ return [];
6846
+ }
6847
+ function heyAdditionalChecks(_$, clientId, html) {
6848
+ const warnings = [];
6849
+ if (!html.includes("prefers-color-scheme")) {
6850
+ warnings.push({
6851
+ severity: "info",
6852
+ client: clientId,
6853
+ property: "dark-mode",
6854
+ message: "HEY Mail supports @media (prefers-color-scheme: dark). Consider adding dark mode styles.",
6855
+ suggestion: "Add a @media (prefers-color-scheme: dark) block to optimize for HEY's audience."
6856
+ });
6857
+ }
6858
+ return warnings;
6859
+ }
6860
+ function superhumanAdditionalChecks($, clientId, html) {
6861
+ const warnings = [];
6862
+ if (detectAnimations($)) {
6863
+ warnings.push({
6864
+ severity: "info",
6865
+ client: clientId,
6866
+ property: "animation",
6867
+ message: "Superhuman may honor OS-level 'reduce motion' preferences, disabling animations.",
6868
+ suggestion: "Use @media (prefers-reduced-motion: reduce) to provide static fallbacks."
6869
+ });
6870
+ }
6871
+ warnings.push({
6872
+ severity: "info",
6873
+ client: clientId,
6874
+ property: "<style>",
6875
+ message: "Superhuman uses Chromium rendering with excellent CSS support. Flexbox, Grid, CSS variables, and modern properties all work."
6876
+ });
6877
+ return warnings;
6878
+ }
6879
+ var EMPTY_SET = /* @__PURE__ */ new Set();
6880
+ var CLIENT_CONFIGS = {
6881
+ "gmail-web": {
6882
+ id: "gmail-web",
6883
+ strippedProperties: GMAIL_STRIPPED_PROPERTIES,
6884
+ stripMode: "strip",
6885
+ valueStrips: [
6886
+ { prop: "display", pattern: /grid/ },
6887
+ { prop: "background", pattern: /linear-gradient|radial-gradient/ }
6888
+ ],
6889
+ inlineAndStripStyles: true,
6890
+ stripExternalStylesheets: true,
6891
+ stripForms: true,
6892
+ stripSvg: true,
6893
+ additionalChecks: gmailAdditionalChecks
6894
+ },
6895
+ "gmail-android": {
6896
+ id: "gmail-android",
6897
+ strippedProperties: GMAIL_STRIPPED_PROPERTIES,
6898
+ stripMode: "strip",
6899
+ valueStrips: [
6900
+ { prop: "display", pattern: /grid/ },
6901
+ { prop: "background", pattern: /linear-gradient|radial-gradient/ }
6902
+ ],
6903
+ inlineAndStripStyles: true,
6904
+ stripExternalStylesheets: true,
6905
+ stripForms: true,
6906
+ stripSvg: true,
6907
+ additionalChecks: gmailAdditionalChecks
6908
+ },
6909
+ "gmail-ios": {
6910
+ id: "gmail-ios",
6911
+ strippedProperties: GMAIL_STRIPPED_PROPERTIES,
6912
+ stripMode: "strip",
6913
+ valueStrips: [
6914
+ { prop: "display", pattern: /grid/ },
6915
+ { prop: "background", pattern: /linear-gradient|radial-gradient/ }
6916
+ ],
6917
+ inlineAndStripStyles: true,
6918
+ stripExternalStylesheets: true,
6919
+ stripForms: true,
6920
+ stripSvg: true,
6921
+ additionalChecks: gmailAdditionalChecks
6922
+ },
6923
+ "outlook-windows": {
6924
+ id: "outlook-windows",
6925
+ strippedProperties: /* @__PURE__ */ new Set(["position", "transform", "animation", "transition"]),
6926
+ stripMode: "strip",
6927
+ inlineAndStripStyles: false,
6928
+ stripExternalStylesheets: false,
6929
+ stripForms: false,
6930
+ stripSvg: false
6931
+ },
6932
+ "outlook-windows-legacy": {
6933
+ id: "outlook-windows-legacy",
6934
+ strippedProperties: OUTLOOK_WORD_UNSUPPORTED,
6935
+ stripMode: "strip",
6936
+ valueStrips: [
6937
+ { prop: "background", pattern: /linear-gradient|radial-gradient/ },
6938
+ { prop: "background-image", pattern: /linear-gradient|radial-gradient/ }
6939
+ ],
6940
+ inlineAndStripStyles: false,
6941
+ stripExternalStylesheets: false,
6942
+ stripForms: false,
6943
+ stripSvg: false,
6944
+ additionalChecks: outlookWindowsAdditionalChecks
6945
+ },
6946
+ "outlook-web": {
6947
+ id: "outlook-web",
6948
+ strippedProperties: /* @__PURE__ */ new Set(["position", "transform", "animation", "transition"]),
6949
+ stripMode: "strip",
6950
+ inlineAndStripStyles: false,
6951
+ stripExternalStylesheets: false,
6952
+ stripForms: false,
6953
+ stripSvg: false
6954
+ },
6955
+ "apple-mail-macos": {
6956
+ id: "apple-mail-macos",
6957
+ strippedProperties: EMPTY_SET,
6958
+ stripMode: "strip",
6959
+ inlineAndStripStyles: false,
6960
+ stripExternalStylesheets: false,
6961
+ stripForms: false,
6962
+ stripSvg: false,
6963
+ additionalChecks: appleMailAdditionalChecks
6964
+ },
6965
+ "apple-mail-ios": {
6966
+ id: "apple-mail-ios",
6967
+ strippedProperties: EMPTY_SET,
6968
+ stripMode: "strip",
6969
+ inlineAndStripStyles: false,
6970
+ stripExternalStylesheets: false,
6971
+ stripForms: false,
6972
+ stripSvg: false,
6973
+ additionalChecks: appleMailAdditionalChecks
6974
+ },
6975
+ "yahoo-mail": {
6976
+ id: "yahoo-mail",
6977
+ strippedProperties: /* @__PURE__ */ new Set(["position", "box-shadow", "transform", "animation", "transition", "opacity"]),
6978
+ stripMode: "strip",
6979
+ inlineAndStripStyles: false,
6980
+ stripExternalStylesheets: false,
6981
+ stripForms: false,
6982
+ stripSvg: false,
6983
+ additionalChecks: yahooAdditionalChecks
6984
+ },
6985
+ "samsung-mail": {
6986
+ id: "samsung-mail",
6987
+ strippedProperties: /* @__PURE__ */ new Set(["box-shadow", "transform", "animation", "transition", "opacity"]),
6988
+ stripMode: "info",
6989
+ inlineAndStripStyles: false,
6990
+ stripExternalStylesheets: false,
6991
+ stripForms: false,
6992
+ stripSvg: false
6993
+ },
6994
+ "thunderbird": {
6995
+ id: "thunderbird",
6996
+ strippedProperties: EMPTY_SET,
6997
+ stripMode: "strip",
6998
+ inlineAndStripStyles: false,
6999
+ stripExternalStylesheets: false,
7000
+ stripForms: false,
7001
+ stripSvg: false,
7002
+ additionalChecks: thunderbirdAdditionalChecks
7003
+ },
7004
+ "hey-mail": {
7005
+ id: "hey-mail",
7006
+ strippedProperties: /* @__PURE__ */ new Set(["transform", "animation", "transition"]),
7007
+ stripMode: "strip",
7008
+ valueStrips: [
7009
+ { prop: "position", pattern: /fixed|sticky/ }
7010
+ ],
7011
+ inlineAndStripStyles: false,
7012
+ stripExternalStylesheets: true,
7013
+ stripForms: true,
7014
+ stripSvg: false,
7015
+ additionalChecks: heyAdditionalChecks
7016
+ },
7017
+ "superhuman": {
7018
+ id: "superhuman",
7019
+ strippedProperties: EMPTY_SET,
7020
+ stripMode: "strip",
7021
+ inlineAndStripStyles: false,
7022
+ stripExternalStylesheets: true,
7023
+ stripForms: true,
7024
+ stripSvg: false,
7025
+ additionalChecks: superhumanAdditionalChecks
7026
+ }
7027
+ };
7028
+ function applyTransform(html, config, framework) {
7029
+ const $ = cheerio2.load(html);
7030
+ const warnings = [];
7031
+ const clientId = config.id;
7032
+ if (config.additionalChecks && config.inlineAndStripStyles) {
7033
+ warnings.push(...config.additionalChecks($, clientId, html, framework));
7034
+ }
7035
+ if (config.inlineAndStripStyles) {
7036
+ const preserved = inlineStyles($);
7037
+ $("style").remove();
7038
+ if (preserved.trim()) {
7039
+ const head = $("head");
7040
+ if (head.length > 0) {
7041
+ head.append(`<style>${preserved}</style>`);
7042
+ } else {
7043
+ $("body").prepend(`<style>${preserved}</style>`);
7044
+ }
7045
+ }
7046
+ }
7047
+ if (config.stripExternalStylesheets) {
7048
+ if ($("link[rel='stylesheet']").length > 0) {
7049
+ warnings.push(makeWarning({
7050
+ severity: "error",
7051
+ client: clientId,
7052
+ property: "<link>",
7053
+ message: `${clientId} does not load external stylesheets.`
7054
+ }, "<link>", clientId, framework));
7055
+ $("link[rel='stylesheet']").remove();
7056
+ }
7057
+ }
7058
+ if (config.stripForms && $("form").length > 0) {
7059
+ warnings.push(makeWarning({
7060
+ severity: "error",
7061
+ client: clientId,
7062
+ property: "<form>",
7063
+ message: `${clientId} removes form elements.`
7064
+ }, "<form>", clientId, framework));
7065
+ $("form").each((_, el) => {
7066
+ $(el).replaceWith($(el).html() || "");
7067
+ });
7068
+ }
7069
+ if (config.stripSvg && $("svg").length > 0) {
7070
+ warnings.push(makeWarning({
7071
+ severity: "error",
7072
+ client: clientId,
7073
+ property: "<svg>",
7074
+ message: `${clientId} does not support inline SVG elements.`
7075
+ }, "<svg>", clientId, framework));
7076
+ $("svg").each((_, el) => {
7077
+ $(el).replaceWith('<img alt="[SVG not supported]" />');
7078
+ });
7079
+ }
7080
+ if (config.strippedProperties.size > 0 || config.valueStrips && config.valueStrips.length > 0) {
7081
+ $("[style]").each((_, el) => {
7082
+ const style = $(el).attr("style") || "";
7083
+ const props = parseInlineStyle(style);
7084
+ const removed = [];
7085
+ props.forEach((value, prop) => {
7086
+ var _a;
7087
+ if (config.strippedProperties.has(prop)) {
7088
+ if (config.stripMode === "strip") {
7089
+ removed.push(prop);
7090
+ props.delete(prop);
7091
+ } else {
7092
+ warnings.push(makeWarning({
7093
+ severity: "info",
7094
+ client: clientId,
7095
+ property: prop,
7096
+ message: `${clientId} has limited support for "${prop}".`
7097
+ }, prop, clientId, framework));
7098
+ }
7099
+ return;
7100
+ }
7101
+ for (const vs of (_a = config.valueStrips) != null ? _a : []) {
7102
+ if (prop === vs.prop && vs.pattern.test(value)) {
7103
+ removed.push(prop);
7104
+ props.delete(prop);
7105
+ return;
7106
+ }
7107
+ }
7108
+ });
7109
+ if (removed.length > 0) {
7110
+ $(el).attr("style", serializeStyle(props));
7111
+ for (const prop of removed) {
7112
+ warnings.push(makeWarning({
7113
+ severity: "warning",
7114
+ client: clientId,
7115
+ property: prop,
7116
+ message: `${clientId} strips "${prop}" from styles.`
7117
+ }, prop, clientId, framework));
7118
+ }
7119
+ }
7120
+ });
7121
+ }
7122
+ if (config.inlineAndStripStyles) {
7123
+ warnings.push(...gmailPostChecks($, clientId, html, framework));
7124
+ }
7125
+ if (config.additionalChecks && !config.inlineAndStripStyles) {
7126
+ warnings.push(...config.additionalChecks($, clientId, html, framework));
7127
+ }
7128
+ return { clientId, html: $.html(), warnings };
7129
+ }
7130
+ function transformForClient(html, clientId, framework) {
7131
+ if (!html || !html.trim()) {
7132
+ return { clientId, html: html || "", warnings: [] };
7133
+ }
7134
+ if (html.length > MAX_HTML_SIZE) {
7135
+ throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
7136
+ }
7137
+ const config = CLIENT_CONFIGS[clientId];
7138
+ if (!config) {
7139
+ return {
7140
+ clientId,
7141
+ html,
7142
+ warnings: [
7143
+ {
7144
+ severity: "info",
7145
+ client: clientId,
7146
+ property: "unknown",
7147
+ message: `No transformation rules available for client "${clientId}".`
7148
+ }
7149
+ ]
7150
+ };
7151
+ }
7152
+ const downleveled = downlevelCSS(html);
7153
+ return applyTransform(downleveled, config, framework);
7154
+ }
7155
+ function transformForAllClients(html, framework) {
7156
+ if (!html || !html.trim()) {
7157
+ return Object.keys(CLIENT_CONFIGS).map((clientId) => ({
7158
+ clientId,
7159
+ html: html || "",
7160
+ warnings: []
7161
+ }));
7162
+ }
7163
+ if (html.length > MAX_HTML_SIZE) {
7164
+ throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
7165
+ }
7166
+ const downleveled = downlevelCSS(html);
7167
+ return Object.keys(CLIENT_CONFIGS).map(
7168
+ (clientId) => applyTransform(downleveled, CLIENT_CONFIGS[clientId], framework)
7169
+ );
7170
+ }
7171
+
7172
+ // src/analyze.ts
7173
+ import * as cheerio3 from "cheerio";
7174
+ import * as csstree3 from "css-tree";
7175
+ var HTML_ELEMENT_SELECTORS = {
7176
+ "<style>": "style",
7177
+ "<link>": "link[rel='stylesheet']",
7178
+ "<svg>": "svg",
7179
+ "<video>": "video",
7180
+ "<form>": "form, input, button[type='submit']",
7181
+ "<audio>": "audio",
7182
+ "<picture>": "picture",
7183
+ "<dialog>": "dialog",
7184
+ "<meter>": "meter",
7185
+ "<progress>": "progress",
7186
+ "<select>": "select",
7187
+ "<textarea>": "textarea",
7188
+ "<marquee>": "marquee",
7189
+ "<object>": "object",
7190
+ "<base>": "base"
7191
+ };
7192
+ var HTML_ELEMENT_SEVERITY = {
7193
+ "<style>": "error",
7194
+ "<link>": "error",
7195
+ "<svg>": "error",
7196
+ "<form>": "error",
7197
+ "<video>": "warning",
7198
+ "<audio>": "warning",
7199
+ "<picture>": "warning",
7200
+ "<dialog>": "warning",
7201
+ "<marquee>": "warning",
7202
+ "<meter>": "warning",
7203
+ "<progress>": "warning",
7204
+ "<select>": "warning",
7205
+ "<textarea>": "warning",
7206
+ "<object>": "warning",
7207
+ "<base>": "warning"
7208
+ };
7209
+ var HTML_ELEMENT_MESSAGES = {
7210
+ "<style>": (n) => `${n} strips <style> blocks. Styles must be inlined.`,
7211
+ "<link>": (n) => `${n} does not support external stylesheets.`,
7212
+ "<svg>": (n) => `${n} does not support inline SVG.`,
7213
+ "<video>": (n) => `${n} does not support <video> elements.`,
7214
+ "<form>": (n) => `${n} strips form elements.`
7215
+ };
7216
+ var COMPOUND_DETECTORS = [
7217
+ { key: "display:flex", property: "display", valueIncludes: "flex" },
7218
+ { key: "display:grid", property: "display", valueIncludes: "grid" },
7219
+ { key: "display:none", property: "display", valueIncludes: "none" }
7220
+ ];
7221
+ var CSS_FUNCTION_DETECTORS = CSS_FUNCTION_FEATURES.map((fn) => ({
7222
+ key: fn,
7223
+ pattern: `${fn}(`
7224
+ // require opening paren — matches "min(" but not "Minion"
7225
+ }));
7226
+ function analyzeEmailFromDom($, framework) {
7227
+ const warnings = [];
7228
+ const seenWarnings = /* @__PURE__ */ new Set();
7229
+ function addWarning(w) {
7230
+ const key = `${w.client}:${w.property}:${w.severity}:${w.selector || ""}`;
7231
+ if (!seenWarnings.has(key)) {
7232
+ seenWarnings.add(key);
7233
+ warnings.push(w);
7234
+ }
7235
+ }
7236
+ function describeSelector(el) {
7237
+ var _a;
7238
+ const $el = $(el);
7239
+ const tag = ((_a = el.tagName) == null ? void 0 : _a.toLowerCase()) || "";
7240
+ const cls = $el.attr("class");
7241
+ const id = $el.attr("id");
7242
+ if (id) return `${tag}#${id}`;
7243
+ if (cls) return `${tag}.${cls.split(/\s+/)[0]}`;
7244
+ const href = $el.attr("href");
7245
+ if (href) return `${tag}[href]`;
7246
+ return tag;
7247
+ }
7248
+ for (const feature of HTML_ELEMENT_FEATURES) {
7249
+ const selector = HTML_ELEMENT_SELECTORS[feature];
7250
+ if (!selector) continue;
7251
+ if ($(selector).length === 0) continue;
7252
+ const supportData = CSS_SUPPORT[feature];
7253
+ if (!supportData) continue;
7254
+ const baseSeverity = HTML_ELEMENT_SEVERITY[feature] || "warning";
7255
+ for (const client of EMAIL_CLIENTS) {
7256
+ const support = supportData[client.id];
7257
+ if (support === "unsupported") {
7258
+ const msgFn = HTML_ELEMENT_MESSAGES[feature];
7259
+ const message = msgFn ? msgFn(client.name) : `${client.name} does not support ${feature}.`;
7260
+ const sug = getSuggestion(feature, client.id, framework);
7261
+ const fix = getCodeFix(feature, client.id, framework);
7262
+ addWarning(__spreadValues({
7263
+ severity: baseSeverity,
7264
+ client: client.id,
7265
+ property: feature,
7266
+ message,
7267
+ suggestion: sug.text,
7268
+ fix,
7269
+ fixType: getFixType(feature)
7270
+ }, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(feature, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
7271
+ } else if (support === "partial" && feature === "<style>") {
7272
+ const sug = getSuggestion("<style>:partial", client.id, framework);
7273
+ const fix = getCodeFix("<style>", client.id, framework);
7274
+ addWarning(__spreadValues({
7275
+ severity: "warning",
7276
+ client: client.id,
7277
+ property: "<style>",
7278
+ message: `${client.name} has partial <style> support (head only, with limitations). Inline styles recommended.`,
7279
+ suggestion: sug.text,
7280
+ fix,
7281
+ fixType: getFixType("<style>")
7282
+ }, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<style>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
7283
+ }
7284
+ }
7285
+ }
7286
+ const parsedAtRules = /* @__PURE__ */ new Set();
7287
+ const parsedProperties = /* @__PURE__ */ new Set();
7288
+ const propertyLines = /* @__PURE__ */ new Map();
7289
+ const detectedCssFunctions = /* @__PURE__ */ new Set();
7290
+ const detectedPseudoClasses = /* @__PURE__ */ new Set();
7291
+ const detectedPseudoElements = /* @__PURE__ */ new Set();
7292
+ $("style").each((_, el) => {
7293
+ const cssText = $(el).text();
7294
+ try {
7295
+ const ast = csstree3.parse(cssText, { parseCustomProperty: true, positions: true });
7296
+ csstree3.walk(ast, {
7297
+ enter(node) {
7298
+ if (node.type === "Atrule") {
7299
+ parsedAtRules.add(`@${node.name}`);
7300
+ }
7301
+ if (node.type === "PseudoClassSelector") {
7302
+ detectedPseudoClasses.add(`:${node.name}`);
7303
+ }
7304
+ if (node.type === "PseudoElementSelector") {
7305
+ detectedPseudoElements.add(`::${node.name}`);
7306
+ }
7307
+ if (node.type === "Declaration") {
7308
+ const prop = node.property.toLowerCase();
7309
+ parsedProperties.add(prop);
7310
+ if (node.loc && !propertyLines.has(prop)) {
7311
+ propertyLines.set(prop, node.loc.start.line);
7312
+ }
7313
+ const valueStr = csstree3.generate(node.value);
7314
+ for (const det of COMPOUND_DETECTORS) {
7315
+ if (prop === det.property && valueStr.includes(det.valueIncludes)) {
7316
+ parsedProperties.add(det.key);
7317
+ if (node.loc && !propertyLines.has(det.key)) {
7318
+ propertyLines.set(det.key, node.loc.start.line);
7319
+ }
7320
+ }
7321
+ }
7322
+ for (const fn of CSS_FUNCTION_DETECTORS) {
7323
+ if (valueStr.includes(fn.pattern)) {
7324
+ detectedCssFunctions.add(fn.key);
7325
+ if (node.loc && !propertyLines.has(fn.key)) {
7326
+ propertyLines.set(fn.key, node.loc.start.line);
7327
+ }
7328
+ }
7329
+ }
7330
+ }
7331
+ }
7332
+ });
7333
+ } catch (e) {
7334
+ }
7335
+ });
7336
+ for (const atRule of AT_RULE_FEATURES) {
7337
+ if (!parsedAtRules.has(atRule)) continue;
7338
+ checkPropertySupport(atRule, addWarning, framework);
6780
7339
  }
6781
- const named = NAMED_COLORS[v];
6782
- if (named) {
6783
- return { r: named[0], g: named[1], b: named[2], a: 1 };
7340
+ const cssPropertiesToCheck = Object.keys(CSS_SUPPORT).filter(
7341
+ (k) => !k.startsWith("<") && !k.startsWith("@")
7342
+ );
7343
+ $("[style]").each((_, el) => {
7344
+ const style = $(el).attr("style") || "";
7345
+ const props = parseStyleProperties(style);
7346
+ const selector = describeSelector(el);
7347
+ for (const prop of props) {
7348
+ for (const det of COMPOUND_DETECTORS) {
7349
+ if (prop === det.property) {
7350
+ const value2 = getStyleValue(style, prop);
7351
+ if (value2 == null ? void 0 : value2.includes(det.valueIncludes)) {
7352
+ checkPropertySupport(det.key, addWarning, framework, selector);
7353
+ }
7354
+ }
7355
+ }
7356
+ if (cssPropertiesToCheck.includes(prop)) {
7357
+ checkPropertySupport(prop, addWarning, framework, selector);
7358
+ }
7359
+ const value = getStyleValue(style, prop);
7360
+ if (value) {
7361
+ for (const fn of CSS_FUNCTION_DETECTORS) {
7362
+ if (value.includes(fn.pattern)) {
7363
+ checkPropertySupport(fn.key, addWarning, framework, selector);
7364
+ }
7365
+ }
7366
+ }
7367
+ }
7368
+ });
7369
+ for (const prop of parsedProperties) {
7370
+ if (prop.includes(":")) continue;
7371
+ if (!cssPropertiesToCheck.includes(prop)) continue;
7372
+ checkPropertySupport(prop, addWarning, framework, void 0, propertyLines.get(prop));
6784
7373
  }
6785
- if (v.startsWith("#")) {
6786
- const hex = v.slice(1);
6787
- if (hex.length === 3) {
6788
- return {
6789
- r: parseInt(hex[0] + hex[0], 16),
6790
- g: parseInt(hex[1] + hex[1], 16),
6791
- b: parseInt(hex[2] + hex[2], 16),
6792
- a: 1
6793
- };
7374
+ for (const compound of COMPOUND_VALUE_FEATURES) {
7375
+ if (compound.startsWith(":") || compound.startsWith("::")) continue;
7376
+ if (parsedProperties.has(compound)) {
7377
+ checkPropertySupport(compound, addWarning, framework, void 0, propertyLines.get(compound));
6794
7378
  }
6795
- if (hex.length === 6) {
6796
- return {
6797
- r: parseInt(hex.slice(0, 2), 16),
6798
- g: parseInt(hex.slice(2, 4), 16),
6799
- b: parseInt(hex.slice(4, 6), 16),
6800
- a: 1
6801
- };
7379
+ }
7380
+ for (const pseudo of detectedPseudoClasses) {
7381
+ if (CSS_SUPPORT[pseudo]) {
7382
+ checkPropertySupport(pseudo, addWarning, framework);
6802
7383
  }
6803
- if (hex.length === 8) {
6804
- return {
6805
- r: parseInt(hex.slice(0, 2), 16),
6806
- g: parseInt(hex.slice(2, 4), 16),
6807
- b: parseInt(hex.slice(4, 6), 16),
6808
- a: parseInt(hex.slice(6, 8), 16) / 255
6809
- };
7384
+ }
7385
+ for (const pseudo of detectedPseudoElements) {
7386
+ if (CSS_SUPPORT[pseudo]) {
7387
+ checkPropertySupport(pseudo, addWarning, framework);
6810
7388
  }
6811
- return null;
6812
7389
  }
6813
- const rgbMatch = v.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)$/);
6814
- if (rgbMatch) {
6815
- return {
6816
- r: Math.min(255, parseInt(rgbMatch[1], 10)),
6817
- g: Math.min(255, parseInt(rgbMatch[2], 10)),
6818
- b: Math.min(255, parseInt(rgbMatch[3], 10)),
6819
- a: rgbMatch[4] !== void 0 ? Math.min(1, parseFloat(rgbMatch[4])) : 1
6820
- };
7390
+ for (const fn of detectedCssFunctions) {
7391
+ checkPropertySupport(fn, addWarning, framework, void 0, propertyLines.get(fn));
6821
7392
  }
6822
- return null;
7393
+ const severityOrder = { error: 0, warning: 1, info: 2 };
7394
+ warnings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
7395
+ return warnings;
6823
7396
  }
6824
- function relativeLuminance(r, g, b) {
6825
- const [rs, gs, bs] = [r, g, b].map((c) => {
6826
- const s = c / 255;
6827
- return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
6828
- });
6829
- return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
7397
+ function analyzeEmail(html, framework) {
7398
+ if (!html || !html.trim()) {
7399
+ return [];
7400
+ }
7401
+ if (html.length > MAX_HTML_SIZE) {
7402
+ throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
7403
+ }
7404
+ const $ = cheerio3.load(html);
7405
+ return analyzeEmailFromDom($, framework);
6830
7406
  }
6831
- function contrastRatio(l1, l2) {
6832
- const lighter = Math.max(l1, l2);
6833
- const darker = Math.min(l1, l2);
6834
- return (lighter + 0.05) / (darker + 0.05);
7407
+ function getFixType(prop) {
7408
+ return STRUCTURAL_FIX_PROPERTIES.has(prop) ? "structural" : "css";
6835
7409
  }
6836
- function wcagGrade(ratio) {
6837
- if (ratio >= 7) return "AAA";
6838
- if (ratio >= 4.5) return "AA";
6839
- if (ratio >= 3) return "AA Large";
6840
- return "Fail";
7410
+ function checkPropertySupport(prop, addWarning, framework, selector, line) {
7411
+ const supportData = CSS_SUPPORT[prop];
7412
+ if (!supportData) return;
7413
+ const fixType = getFixType(prop);
7414
+ for (const client of EMAIL_CLIENTS) {
7415
+ const support = supportData[client.id] || "unknown";
7416
+ if (support === "unsupported") {
7417
+ const sug = getSuggestion(prop, client.id, framework);
7418
+ const fix = getCodeFix(prop, client.id, framework);
7419
+ addWarning(__spreadValues(__spreadValues(__spreadValues({
7420
+ severity: "warning",
7421
+ client: client.id,
7422
+ property: prop,
7423
+ message: `${client.name} does not support "${prop}".`,
7424
+ suggestion: sug.text,
7425
+ fix,
7426
+ fixType
7427
+ }, selector ? { selector } : {}), line !== void 0 ? { line } : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
7428
+ } else if (support === "partial") {
7429
+ const sug = getSuggestion(prop, client.id, framework);
7430
+ const fix = getCodeFix(prop, client.id, framework);
7431
+ addWarning(__spreadValues(__spreadValues(__spreadValues({
7432
+ severity: "info",
7433
+ client: client.id,
7434
+ property: prop,
7435
+ message: `${client.name} has partial support for "${prop}".`,
7436
+ suggestion: sug.text,
7437
+ fix,
7438
+ fixType
7439
+ }, selector ? { selector } : {}), line !== void 0 ? { line } : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
7440
+ }
7441
+ }
6841
7442
  }
6842
- function alphaBlend(fg, bgR, bgG, bgB) {
6843
- const a = fg.a;
6844
- return [
6845
- Math.round(fg.r * a + bgR * (1 - a)),
6846
- Math.round(fg.g * a + bgG * (1 - a)),
6847
- Math.round(fg.b * a + bgB * (1 - a))
6848
- ];
7443
+ function generateCompatibilityScore(warnings) {
7444
+ const result = {};
7445
+ for (const client of EMAIL_CLIENTS) {
7446
+ const clientWarnings = warnings.filter((w) => w.client === client.id);
7447
+ const errorProps = new Set(clientWarnings.filter((w) => w.severity === "error").map((w) => w.property));
7448
+ const warnProps = new Set(clientWarnings.filter((w) => w.severity === "warning").map((w) => w.property));
7449
+ const infoProps = new Set(clientWarnings.filter((w) => w.severity === "info").map((w) => w.property));
7450
+ const errors = errorProps.size;
7451
+ const warns = warnProps.size;
7452
+ const info = infoProps.size;
7453
+ const score = Math.max(0, Math.min(100, 100 - errors * 10 - warns * 3));
7454
+ result[client.id] = { score, errors, warnings: warns, info };
7455
+ }
7456
+ return result;
7457
+ }
7458
+ function warningsForClient(warnings, clientId) {
7459
+ return warnings.filter((w) => w.client === clientId);
7460
+ }
7461
+ function errorWarnings(warnings) {
7462
+ return warnings.filter((w) => w.severity === "error");
7463
+ }
7464
+ function structuralWarnings(warnings) {
7465
+ return warnings.filter((w) => w.fixType === "structural");
6849
7466
  }
6850
7467
 
6851
7468
  // src/dark-mode.ts
7469
+ import * as cheerio4 from "cheerio";
7470
+ import * as csstree4 from "css-tree";
6852
7471
  var LIGHT_THRESHOLD = 0.7;
6853
7472
  var DARK_THRESHOLD = 0.15;
6854
7473
  function simulateDarkMode(html, clientId) {
@@ -6858,7 +7477,7 @@ function simulateDarkMode(html, clientId) {
6858
7477
  if (html.length > MAX_HTML_SIZE) {
6859
7478
  throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
6860
7479
  }
6861
- const $ = cheerio3.load(html);
7480
+ const $ = cheerio4.load(html);
6862
7481
  const warnings = [];
6863
7482
  $("img").each((_, el) => {
6864
7483
  const src = $(el).attr("src") || "";
@@ -7055,35 +7674,35 @@ function applyColorInversion($, mode) {
7055
7674
  $("style").each((_, el) => {
7056
7675
  const cssText = $(el).text();
7057
7676
  try {
7058
- const ast = csstree3.parse(cssText, { parseCustomProperty: true });
7677
+ const ast = csstree4.parse(cssText, { parseCustomProperty: true });
7059
7678
  let modified = false;
7060
- csstree3.walk(ast, {
7679
+ csstree4.walk(ast, {
7061
7680
  enter(node) {
7062
7681
  if (node.type !== "Declaration") return;
7063
7682
  const prop = node.property.toLowerCase();
7064
7683
  if (!COLOR_PROPS.has(prop) && prop !== "background") return;
7065
- const valueStr = csstree3.generate(node.value);
7684
+ const valueStr = csstree4.generate(node.value);
7066
7685
  if (prop === "background") {
7067
7686
  const bgColor = extractBackgroundColor(valueStr);
7068
7687
  if (bgColor) {
7069
7688
  const inverted = invertColor(bgColor, mode);
7070
7689
  if (inverted) {
7071
7690
  const newValue = valueStr.replace(bgColor, inverted);
7072
- node.value = csstree3.parse(newValue, { context: "value" });
7691
+ node.value = csstree4.parse(newValue, { context: "value" });
7073
7692
  modified = true;
7074
7693
  }
7075
7694
  }
7076
7695
  } else {
7077
7696
  const inverted = invertColor(valueStr, mode);
7078
7697
  if (inverted) {
7079
- node.value = csstree3.parse(inverted, { context: "value" });
7698
+ node.value = csstree4.parse(inverted, { context: "value" });
7080
7699
  modified = true;
7081
7700
  }
7082
7701
  }
7083
7702
  }
7084
7703
  });
7085
7704
  if (modified) {
7086
- $(el).text(csstree3.generate(ast));
7705
+ $(el).text(csstree4.generate(ast));
7087
7706
  }
7088
7707
  } catch (e) {
7089
7708
  }
@@ -7395,7 +8014,7 @@ function extractCode(response) {
7395
8014
  }
7396
8015
 
7397
8016
  // src/spam-scorer.ts
7398
- import * as cheerio4 from "cheerio";
8017
+ import * as cheerio5 from "cheerio";
7399
8018
  var SPAM_TRIGGER_PHRASES = [
7400
8019
  "act now",
7401
8020
  "limited time",
@@ -7826,12 +8445,12 @@ function analyzeSpam(html, options) {
7826
8445
  if (html.length > MAX_HTML_SIZE) {
7827
8446
  throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
7828
8447
  }
7829
- const $ = cheerio4.load(html);
8448
+ const $ = cheerio5.load(html);
7830
8449
  return analyzeSpamFromDom($, options);
7831
8450
  }
7832
8451
 
7833
8452
  // src/link-validator.ts
7834
- import * as cheerio5 from "cheerio";
8453
+ import * as cheerio6 from "cheerio";
7835
8454
  function classifyHref(href) {
7836
8455
  if (!href || !href.trim()) return "empty";
7837
8456
  const h = href.trim().toLowerCase();
@@ -8027,12 +8646,12 @@ function validateLinks(html) {
8027
8646
  if (html.length > MAX_HTML_SIZE) {
8028
8647
  throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
8029
8648
  }
8030
- const $ = cheerio5.load(html);
8649
+ const $ = cheerio6.load(html);
8031
8650
  return validateLinksFromDom($);
8032
8651
  }
8033
8652
 
8034
8653
  // src/accessibility-checker.ts
8035
- import * as cheerio6 from "cheerio";
8654
+ import * as cheerio7 from "cheerio";
8036
8655
  var RULE_PENALTY_CAPS = {
8037
8656
  "img-missing-alt": 3,
8038
8657
  "link-generic-text": 3,
@@ -8335,12 +8954,12 @@ function checkAccessibility(html) {
8335
8954
  if (html.length > MAX_HTML_SIZE) {
8336
8955
  throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
8337
8956
  }
8338
- const $ = cheerio6.load(html);
8957
+ const $ = cheerio7.load(html);
8339
8958
  return checkAccessibilityFromDom($);
8340
8959
  }
8341
8960
 
8342
8961
  // src/image-analyzer.ts
8343
- import * as cheerio7 from "cheerio";
8962
+ import * as cheerio8 from "cheerio";
8344
8963
  var DATA_URI_WARN_BYTES = 100 * 1024;
8345
8964
  var TOTAL_DATA_URI_WARN_BYTES = 500 * 1024;
8346
8965
  var HIGH_IMAGE_COUNT = 10;
@@ -8504,12 +9123,12 @@ function analyzeImages(html) {
8504
9123
  if (html.length > MAX_HTML_SIZE) {
8505
9124
  throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
8506
9125
  }
8507
- const $ = cheerio7.load(html);
9126
+ const $ = cheerio8.load(html);
8508
9127
  return analyzeImagesFromDom($);
8509
9128
  }
8510
9129
 
8511
9130
  // src/inbox-preview.ts
8512
- import * as cheerio8 from "cheerio";
9131
+ import * as cheerio9 from "cheerio";
8513
9132
  var MAX_SUBJECT_LENGTH = 60;
8514
9133
  var MAX_PREHEADER_LENGTH = 100;
8515
9134
  var MIN_PREHEADER_LENGTH = 30;
@@ -8529,7 +9148,7 @@ function extractInboxPreview(html) {
8529
9148
  if (html.length > MAX_HTML_SIZE) {
8530
9149
  throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
8531
9150
  }
8532
- const $ = cheerio8.load(html);
9151
+ const $ = cheerio9.load(html);
8533
9152
  return extractInboxPreviewFromDom($);
8534
9153
  }
8535
9154
  function extractInboxPreviewFromDom($) {
@@ -8697,7 +9316,7 @@ function getFirstVisibleText($, body) {
8697
9316
  }
8698
9317
 
8699
9318
  // src/size-checker.ts
8700
- import * as cheerio9 from "cheerio";
9319
+ import * as cheerio10 from "cheerio";
8701
9320
  function humanizeBytes(bytes) {
8702
9321
  if (bytes < 1024) return `${bytes} B`;
8703
9322
  const kb = bytes / 1024;
@@ -8735,12 +9354,12 @@ function checkSize(html) {
8735
9354
  if (html.length > MAX_HTML_SIZE) {
8736
9355
  throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
8737
9356
  }
8738
- const $ = cheerio9.load(html);
9357
+ const $ = cheerio10.load(html);
8739
9358
  return checkSizeFromDom($, html);
8740
9359
  }
8741
9360
 
8742
9361
  // src/template-checker.ts
8743
- import * as cheerio10 from "cheerio";
9362
+ import * as cheerio11 from "cheerio";
8744
9363
  function checkTemplateVariablesFromDom($) {
8745
9364
  const issues = [];
8746
9365
  const seen = /* @__PURE__ */ new Set();
@@ -8803,12 +9422,12 @@ function checkTemplateVariables(html) {
8803
9422
  if (html.length > MAX_HTML_SIZE) {
8804
9423
  throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
8805
9424
  }
8806
- const $ = cheerio10.load(html);
9425
+ const $ = cheerio11.load(html);
8807
9426
  return checkTemplateVariablesFromDom($);
8808
9427
  }
8809
9428
 
8810
9429
  // src/audit.ts
8811
- import * as cheerio11 from "cheerio";
9430
+ import * as cheerio12 from "cheerio";
8812
9431
  function auditEmail(html, options) {
8813
9432
  var _a;
8814
9433
  if (!html || !html.trim()) {
@@ -8828,7 +9447,7 @@ function auditEmail(html, options) {
8828
9447
  }
8829
9448
  const framework = options == null ? void 0 : options.framework;
8830
9449
  const skip = new Set((_a = options == null ? void 0 : options.skip) != null ? _a : []);
8831
- const $ = cheerio11.load(html);
9450
+ const $ = cheerio12.load(html);
8832
9451
  const warnings = skip.has("compatibility") ? [] : analyzeEmailFromDom($, framework);
8833
9452
  const scores = skip.has("compatibility") ? {} : generateCompatibilityScore(warnings);
8834
9453
  const spam = skip.has("spam") ? EMPTY_SPAM : analyzeSpamFromDom($, options == null ? void 0 : options.spam);
@@ -8841,8 +9460,53 @@ function auditEmail(html, options) {
8841
9460
  return { compatibility: { warnings, scores }, spam, links, accessibility, images, inboxPreview, size, templateVariables };
8842
9461
  }
8843
9462
 
9463
+ // src/plain-text.ts
9464
+ import * as cheerio13 from "cheerio";
9465
+ function toPlainText(html) {
9466
+ const $ = cheerio13.load(html);
9467
+ $("style, script").remove();
9468
+ $("[data-skip-in-text='true']").remove();
9469
+ $("hr").replaceWith("\n---\n");
9470
+ $("br").replaceWith("\n");
9471
+ $("img").each((_, el) => {
9472
+ const alt = $(el).attr("alt");
9473
+ $(el).replaceWith(alt ? alt : "");
9474
+ });
9475
+ $("a").each((_, el) => {
9476
+ const $el = $(el);
9477
+ const href = $el.attr("href") || "";
9478
+ const text2 = $el.text().trim();
9479
+ if (!href || href === text2) {
9480
+ $el.replaceWith(text2 || href);
9481
+ } else if (!text2) {
9482
+ $el.replaceWith(href);
9483
+ } else {
9484
+ $el.replaceWith(`${text2} (${href})`);
9485
+ }
9486
+ });
9487
+ $("li").each((_, el) => {
9488
+ const $el = $(el);
9489
+ const text2 = $el.text().trim();
9490
+ $el.replaceWith(`
9491
+ - ${text2}
9492
+ `);
9493
+ });
9494
+ const blockTags = "p, div, h1, h2, h3, h4, h5, h6, tr, blockquote, ul, ol";
9495
+ $(blockTags).each((_, el) => {
9496
+ const $el = $(el);
9497
+ $el.prepend("\n\n");
9498
+ $el.append("\n\n");
9499
+ });
9500
+ let text = $("body").text();
9501
+ if (!text.trim()) {
9502
+ text = $.root().text();
9503
+ }
9504
+ text = text.replace(/\n{3,}/g, "\n\n");
9505
+ return text.trim();
9506
+ }
9507
+
8844
9508
  // src/session.ts
8845
- import * as cheerio12 from "cheerio";
9509
+ import * as cheerio14 from "cheerio";
8846
9510
  function createSession(html, options) {
8847
9511
  if (!html || !html.trim()) {
8848
9512
  const fw = options == null ? void 0 : options.framework;
@@ -8876,7 +9540,7 @@ function createSession(html, options) {
8876
9540
  if (html.length > MAX_HTML_SIZE) {
8877
9541
  throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
8878
9542
  }
8879
- const $ = cheerio12.load(html);
9543
+ const $ = cheerio14.load(html);
8880
9544
  const framework = options == null ? void 0 : options.framework;
8881
9545
  return {
8882
9546
  html,
@@ -8958,9 +9622,11 @@ export {
8958
9622
  contrastRatio,
8959
9623
  createSession,
8960
9624
  diffResults,
9625
+ downlevelCSS,
8961
9626
  errorWarnings,
8962
9627
  estimateAiFixTokens,
8963
9628
  extractInboxPreview,
9629
+ formatRgb,
8964
9630
  generateAiFix,
8965
9631
  generateCompatibilityScore,
8966
9632
  generateFixPrompt,
@@ -8972,6 +9638,7 @@ export {
8972
9638
  relativeLuminance,
8973
9639
  simulateDarkMode,
8974
9640
  structuralWarnings,
9641
+ toPlainText,
8975
9642
  transformForAllClients,
8976
9643
  transformForClient,
8977
9644
  validateLinks,