@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.cjs CHANGED
@@ -82,9 +82,11 @@ __export(index_exports, {
82
82
  contrastRatio: () => contrastRatio,
83
83
  createSession: () => createSession,
84
84
  diffResults: () => diffResults,
85
+ downlevelCSS: () => downlevelCSS,
85
86
  errorWarnings: () => errorWarnings,
86
87
  estimateAiFixTokens: () => estimateAiFixTokens,
87
88
  extractInboxPreview: () => extractInboxPreview,
89
+ formatRgb: () => formatRgb,
88
90
  generateAiFix: () => generateAiFix,
89
91
  generateCompatibilityScore: () => generateCompatibilityScore,
90
92
  generateFixPrompt: () => generateFixPrompt,
@@ -96,6 +98,7 @@ __export(index_exports, {
96
98
  relativeLuminance: () => relativeLuminance,
97
99
  simulateDarkMode: () => simulateDarkMode,
98
100
  structuralWarnings: () => structuralWarnings,
101
+ toPlainText: () => toPlainText,
99
102
  transformForAllClients: () => transformForAllClients,
100
103
  transformForClient: () => transformForClient,
101
104
  validateLinks: () => validateLinks,
@@ -217,8 +220,8 @@ function getClient(id) {
217
220
  }
218
221
 
219
222
  // src/transform.ts
220
- var cheerio = __toESM(require("cheerio"), 1);
221
- var csstree = __toESM(require("css-tree"), 1);
223
+ var cheerio2 = __toESM(require("cheerio"), 1);
224
+ var csstree2 = __toESM(require("css-tree"), 1);
222
225
 
223
226
  // src/rules/css-support.ts
224
227
  var CSS_SUPPORT = {
@@ -4743,6 +4746,48 @@ h1 {
4743
4746
  Shortened content...
4744
4747
  <a href="https://example.com/full">Read more</a>
4745
4748
  </div>`
4749
+ },
4750
+ // ── padding (Outlook MSO font-width trick) ─────────────────────────────
4751
+ "padding::outlook": {
4752
+ language: "html",
4753
+ description: "Use MSO font-width trick for reliable button padding in Outlook",
4754
+ before: `<a href="https://example.com"
4755
+ style="background-color: #6d28d9; color: #fff;
4756
+ padding: 12px 32px; display: inline-block;
4757
+ text-decoration: none;">
4758
+ Click Here
4759
+ </a>`,
4760
+ after: `<!--[if mso]>
4761
+ <table role="presentation" cellpadding="0" cellspacing="0" border="0"
4762
+ style="border-collapse: collapse;">
4763
+ <tr>
4764
+ <td style="background-color: #6d28d9; padding: 0;
4765
+ mso-padding-alt: 12px 32px;">
4766
+ <!--[if mso]>
4767
+ <i style="letter-spacing: 32px; mso-font-width: -100%;
4768
+ mso-text-raise: 18pt;" hidden>&#8202;</i>
4769
+ <![endif]-->
4770
+ <a href="https://example.com"
4771
+ style="background-color: #6d28d9; color: #fff;
4772
+ mso-text-raise: 9pt; text-decoration: none;
4773
+ font-family: Arial, sans-serif; font-size: 14px;
4774
+ font-weight: bold;">Click Here</a>
4775
+ <!--[if mso]>
4776
+ <i style="letter-spacing: 32px; mso-font-width: -100%;"
4777
+ hidden>&#8202;</i>
4778
+ <![endif]-->
4779
+ </td>
4780
+ </tr>
4781
+ </table>
4782
+ <![endif]-->
4783
+ <!--[if !mso]><!-->
4784
+ <a href="https://example.com"
4785
+ style="background-color: #6d28d9; color: #fff;
4786
+ padding: 12px 32px; display: inline-block;
4787
+ text-decoration: none;">
4788
+ Click Here
4789
+ </a>
4790
+ <!--<![endif]-->`
4746
4791
  }
4747
4792
  };
4748
4793
 
@@ -5285,6 +5330,35 @@ var JSX_FIX_DATABASE = {
5285
5330
  Content \u2014 padding on inner element
5286
5331
  </div>
5287
5332
  </div>`
5333
+ },
5334
+ // ── padding (Outlook MSO font-width trick, JSX) ───────────────────────
5335
+ "padding::outlook::jsx": {
5336
+ language: "jsx",
5337
+ description: "Use React Email Button component which handles MSO padding automatically",
5338
+ before: `<a
5339
+ href="https://example.com"
5340
+ style={{ backgroundColor: "#6d28d9", color: "#fff",
5341
+ padding: "12px 32px", display: "inline-block",
5342
+ textDecoration: "none" }}>
5343
+ Click Here
5344
+ </a>`,
5345
+ after: `import { Button } from "@react-email/components";
5346
+
5347
+ {/* The Button component automatically handles MSO padding
5348
+ via the mso-font-width + &#8202; technique */}
5349
+ <Button
5350
+ href="https://example.com"
5351
+ style={{
5352
+ backgroundColor: "#6d28d9",
5353
+ color: "#fff",
5354
+ padding: "12px 32px",
5355
+ borderRadius: "6px",
5356
+ textDecoration: "none",
5357
+ fontWeight: "bold",
5358
+ }}
5359
+ >
5360
+ Click Here
5361
+ </Button>`
5288
5362
  }
5289
5363
  };
5290
5364
 
@@ -5582,7 +5656,9 @@ var HTML_SUGGESTION_DATABASE = {
5582
5656
  "transition": "CSS transitions are not supported in email.",
5583
5657
  "box-sizing": "Account for padding in your width calculations (use padding on a nested element).",
5584
5658
  "object-fit": "Use width/height attributes on <img> directly.",
5585
- "display": "Use tables for layout in email clients."
5659
+ "display": "Use tables for layout in email clients.",
5660
+ // ── padding ─────────────────────────────────────────────────────────────
5661
+ "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."
5586
5662
  };
5587
5663
 
5588
5664
  // src/fix-snippets/jsx-suggestions.ts
@@ -5627,7 +5703,9 @@ var JSX_SUGGESTION_DATABASE = {
5627
5703
  // ── word-break ────────────────────────────────────────────────────────
5628
5704
  "word-break::jsx": "Wrap long text in a <table><tr><td> element. Outlook ignores wordBreak but respects table cell widths.",
5629
5705
  // ── overflow-wrap ─────────────────────────────────────────────────────
5630
- "overflow-wrap::jsx": "Wrap text in a <table><tr><td> element. Outlook ignores overflowWrap but respects table cell widths."
5706
+ "overflow-wrap::jsx": "Wrap text in a <table><tr><td> element. Outlook ignores overflowWrap but respects table cell widths.",
5707
+ // ── padding ─────────────────────────────────────────────────────────────
5708
+ "padding::outlook::jsx": "Use React Email's <Button> component which automatically generates MSO-compatible padding using the mso-font-width trick."
5631
5709
  };
5632
5710
 
5633
5711
  // src/fix-snippets/mjml-suggestions.ts
@@ -5849,1103 +5927,1647 @@ function serializeStyle(map) {
5849
5927
  return parts.join("; ");
5850
5928
  }
5851
5929
 
5852
- // src/constants.ts
5853
- var MAX_HTML_SIZE = 2 * 1024 * 1024;
5854
- var GENERIC_LINK_TEXT = /* @__PURE__ */ new Set([
5855
- "click here",
5856
- "here",
5857
- "read more",
5858
- "learn more",
5859
- "more",
5860
- "link",
5861
- "this link",
5862
- "click",
5863
- "tap here",
5864
- "this"
5865
- ]);
5866
- var GMAIL_CLIP_THRESHOLD = 102 * 1024;
5867
- var GMAIL_CLIP_WARNING_THRESHOLD = 90 * 1024;
5868
- var CLIENT_DISPLAY_LIMITS = [
5869
- { client: "Gmail (Web)", subjectLimit: 70, preheaderLimit: 90 },
5870
- { client: "Gmail (Mobile)", subjectLimit: 40, preheaderLimit: 45 },
5871
- { client: "Outlook (Web)", subjectLimit: 60, preheaderLimit: 90 },
5872
- { client: "Outlook (Desktop)", subjectLimit: 55, preheaderLimit: 35 },
5873
- { client: "Apple Mail (macOS)", subjectLimit: 78, preheaderLimit: 140 },
5874
- { client: "Apple Mail (iOS)", subjectLimit: 41, preheaderLimit: 90 },
5875
- { client: "Yahoo Mail", subjectLimit: 46, preheaderLimit: 100 },
5876
- { client: "Samsung Email", subjectLimit: 40, preheaderLimit: 70 }
5877
- ];
5878
- var TEMPLATE_VARIABLE_PATTERNS = [
5879
- [/\{\{[\s\S]*?\}\}/g, "Handlebars/Mustache"],
5880
- // {{var}}
5881
- [/\$\{[^}]+\}/g, "ES template literal"],
5882
- // ${var}
5883
- [/<%=?\s*[^%]+%>/g, "ERB/EJS"],
5884
- // <% %> / <%= %>
5885
- [/\*\|[A-Z_][A-Z0-9_]*\|\*/g, "Mailchimp merge tag"],
5886
- // *|TAG|*
5887
- [/%%[A-Za-z_][A-Za-z0-9_]*%%/g, "Salesforce AMPscript"],
5888
- // %%tag%%
5889
- [/\{[A-Za-z_][A-Za-z0-9_.]{2,}\}/g, "Single-brace merge field"]
5890
- // {merge_field} (3+ char names)
5891
- ];
5892
- var EMPTY_SPAM = { score: 100, level: "low", issues: [] };
5893
- var EMPTY_LINKS = {
5894
- totalLinks: 0,
5895
- issues: [],
5896
- breakdown: { https: 0, http: 0, mailto: 0, tel: 0, anchor: 0, javascript: 0, protocolRelative: 0, other: 0 }
5897
- };
5898
- var EMPTY_ACCESSIBILITY = { score: 100, issues: [] };
5899
- var EMPTY_IMAGES = { total: 0, totalDataUriBytes: 0, issues: [], images: [] };
5900
- var EMPTY_INBOX_PREVIEW = { subject: null, preheader: null, subjectLength: 0, preheaderLength: 0, truncation: [], issues: [] };
5901
- var EMPTY_SIZE = { htmlBytes: 0, humanSize: "0 B", clipped: false, issues: [] };
5902
- var EMPTY_TEMPLATE = { unresolvedCount: 0, issues: [] };
5903
- var EMPTY_DELIVERABILITY = { domain: "", checks: [], score: 0, issues: [] };
5930
+ // src/downlevel.ts
5931
+ var cheerio = __toESM(require("cheerio"), 1);
5932
+ var csstree = __toESM(require("css-tree"), 1);
5904
5933
 
5905
- // src/transform.ts
5906
- function inlineStyles($) {
5907
- const styleBlocks = [];
5908
- $("style").each((_, el) => {
5909
- styleBlocks.push($(el).text());
5910
- });
5911
- if (styleBlocks.length === 0) return;
5912
- for (const block of styleBlocks) {
5913
- let ast;
5914
- try {
5915
- ast = csstree.parse(block, { parseCustomProperty: true });
5916
- } catch (e) {
5917
- continue;
5918
- }
5919
- csstree.walk(ast, {
5920
- visit: "Rule",
5921
- enter(node) {
5922
- if (node.type !== "Rule" || node.prelude.type !== "SelectorList") return;
5923
- const declarations = csstree.generate(node.block);
5924
- const declText = declarations.slice(1, -1).trim();
5925
- if (!declText) return;
5926
- const selectorText = csstree.generate(node.prelude);
5927
- if (selectorText.includes(":hover") || selectorText.includes(":focus") || selectorText.includes(":active") || selectorText.includes("::")) {
5928
- return;
5929
- }
5930
- try {
5931
- $(selectorText).each((_, el) => {
5932
- const existing = $(el).attr("style") || "";
5933
- $(el).attr("style", existing ? `${existing}; ${declText}` : declText);
5934
- });
5935
- } catch (e) {
5936
- }
5937
- }
5938
- });
5939
- }
5940
- }
5941
- function makeWarning(base, prop, clientId, framework) {
5942
- const sug = getSuggestion(prop, clientId, framework);
5943
- const fix = getCodeFix(prop, clientId, framework);
5944
- const isFallback = framework && ((sug == null ? void 0 : sug.isGenericFallback) || fix && isCodeFixGenericFallback(prop, clientId, framework));
5945
- return __spreadProps(__spreadValues(__spreadValues(__spreadValues(__spreadValues({}, base), sug ? { suggestion: sug.text } : {}), fix ? { fix } : {}), isFallback ? { fixIsGenericFallback: true } : {}), {
5946
- fixType: STRUCTURAL_FIX_PROPERTIES.has(prop) ? "structural" : "css"
5947
- });
5948
- }
5949
- function detectAnimations($) {
5950
- let found = false;
5951
- $("[style]").each((_, el) => {
5952
- const style = $(el).attr("style") || "";
5953
- const props = parseInlineStyle(style);
5954
- props.forEach((_2, prop) => {
5955
- if (prop === "animation" || prop === "transition" || prop.startsWith("animation-") || prop.startsWith("transition-")) {
5956
- found = true;
5957
- }
5958
- });
5959
- });
5960
- if (!found) {
5961
- $("style").each((_, el) => {
5962
- try {
5963
- const ast = csstree.parse($(el).text());
5964
- csstree.walk(ast, {
5965
- enter(node) {
5966
- if (node.type === "Declaration") {
5967
- const prop = node.property.toLowerCase();
5968
- if (prop === "animation" || prop === "transition" || prop.startsWith("animation-") || prop.startsWith("transition-")) {
5969
- found = true;
5970
- }
5971
- }
5972
- }
5973
- });
5974
- } catch (e) {
5975
- }
5976
- });
5977
- }
5978
- return found;
5979
- }
5980
- function gmailAdditionalChecks($, clientId, _html, framework) {
5981
- const warnings = [];
5982
- let hasAtFontFace = false;
5983
- $("style").each((_, el) => {
5984
- try {
5985
- const ast = csstree.parse($(el).text());
5986
- csstree.walk(ast, {
5987
- enter(node) {
5988
- if (node.type === "Atrule" && node.name === "font-face") {
5989
- hasAtFontFace = true;
5990
- }
5991
- }
5992
- });
5993
- } catch (e) {
5994
- }
5995
- });
5996
- if (hasAtFontFace) {
5997
- warnings.push(makeWarning({
5998
- severity: "warning",
5999
- client: clientId,
6000
- property: "@font-face",
6001
- message: "Gmail does not support custom web fonts."
6002
- }, "@font-face", clientId, framework));
6003
- }
6004
- return warnings;
6005
- }
6006
- function gmailPostChecks($, clientId, _html, framework) {
6007
- const warnings = [];
6008
- $("*").contents().filter(function() {
6009
- return this.type === "comment";
6010
- }).each(function() {
6011
- const commentText = this.data || "";
6012
- if (commentText.includes("<style") || commentText.includes("[if mso]") || commentText.includes("[if gte mso")) {
6013
- $(this).remove();
6014
- }
6015
- });
6016
- const styleSug = getSuggestion("<style>:partial", clientId, framework);
6017
- warnings.push({
6018
- severity: "info",
6019
- client: clientId,
6020
- property: "<style>",
6021
- message: "Gmail partially supports <style> blocks (head only, 16KB limit). Inlining recommended for safety.",
6022
- suggestion: styleSug.text
6023
- });
6024
- return warnings;
6025
- }
6026
- function outlookWindowsAdditionalChecks($, clientId, _html, framework) {
6027
- const warnings = [];
6028
- if ($("[style*='border-radius']").length > 0) {
6029
- warnings.push(makeWarning({
6030
- severity: "warning",
6031
- client: clientId,
6032
- property: "border-radius",
6033
- message: "Outlook Classic ignores border-radius. Buttons and containers will have sharp corners."
6034
- }, "border-radius", clientId, framework));
6035
- }
6036
- if ($("[style*='max-width']").length > 0) {
6037
- warnings.push(makeWarning({
6038
- severity: "warning",
6039
- client: clientId,
6040
- property: "max-width",
6041
- message: "Outlook Classic ignores max-width."
6042
- }, "max-width", clientId, framework));
6043
- }
6044
- const hasDivLayout = $("div[style*='display']").length > 0 || $("div[style*='flex']").length > 0 || $("div[style*='grid']").length > 0;
6045
- if (hasDivLayout) {
6046
- warnings.push(makeWarning({
6047
- severity: "error",
6048
- client: clientId,
6049
- property: "display:flex",
6050
- message: "Outlook Classic uses Microsoft Word for rendering. Flexbox and Grid layouts will break."
6051
- }, "display:flex", clientId, framework));
6052
- }
6053
- if ($("[style*='background-image']").length > 0 || $("[style*='background:']").filter(
6054
- (_, el) => ($(el).attr("style") || "").includes("url(")
6055
- ).length > 0) {
6056
- warnings.push(makeWarning({
6057
- severity: "warning",
6058
- client: clientId,
6059
- property: "background-image",
6060
- message: "Outlook Classic requires VML for background images."
6061
- }, "background-image", clientId, framework));
6062
- }
6063
- return warnings;
6064
- }
6065
- function appleMailAdditionalChecks($, clientId) {
6066
- const warnings = [];
6067
- const imgsWithTransparentBg = $("img").filter((_, el) => {
6068
- const src = $(el).attr("src") || "";
6069
- return src.endsWith(".png") || src.endsWith(".svg");
6070
- });
6071
- if (imgsWithTransparentBg.length > 0) {
6072
- warnings.push({
6073
- severity: "info",
6074
- client: clientId,
6075
- property: "dark-mode",
6076
- message: "PNG/SVG images with transparent backgrounds may become invisible in Apple Mail dark mode.",
6077
- suggestion: "Add a white background or padding around images, or use dark-mode-friendly image variants."
6078
- });
5934
+ // src/color-utils.ts
5935
+ var NAMED_COLORS = {
5936
+ aliceblue: [240, 248, 255],
5937
+ antiquewhite: [250, 235, 215],
5938
+ aqua: [0, 255, 255],
5939
+ aquamarine: [127, 255, 212],
5940
+ azure: [240, 255, 255],
5941
+ beige: [245, 245, 220],
5942
+ bisque: [255, 228, 196],
5943
+ black: [0, 0, 0],
5944
+ blanchedalmond: [255, 235, 205],
5945
+ blue: [0, 0, 255],
5946
+ blueviolet: [138, 43, 226],
5947
+ brown: [165, 42, 42],
5948
+ burlywood: [222, 184, 135],
5949
+ cadetblue: [95, 158, 160],
5950
+ chartreuse: [127, 255, 0],
5951
+ chocolate: [210, 105, 30],
5952
+ coral: [255, 127, 80],
5953
+ cornflowerblue: [100, 149, 237],
5954
+ cornsilk: [255, 248, 220],
5955
+ crimson: [220, 20, 60],
5956
+ cyan: [0, 255, 255],
5957
+ darkblue: [0, 0, 139],
5958
+ darkcyan: [0, 139, 139],
5959
+ darkgoldenrod: [184, 134, 11],
5960
+ darkgray: [169, 169, 169],
5961
+ darkgreen: [0, 100, 0],
5962
+ darkgrey: [169, 169, 169],
5963
+ darkkhaki: [189, 183, 107],
5964
+ darkmagenta: [139, 0, 139],
5965
+ darkolivegreen: [85, 107, 47],
5966
+ darkorange: [255, 140, 0],
5967
+ darkorchid: [153, 50, 204],
5968
+ darkred: [139, 0, 0],
5969
+ darksalmon: [233, 150, 122],
5970
+ darkseagreen: [143, 188, 143],
5971
+ darkslateblue: [72, 61, 139],
5972
+ darkslategray: [47, 79, 79],
5973
+ darkslategrey: [47, 79, 79],
5974
+ darkturquoise: [0, 206, 209],
5975
+ darkviolet: [148, 0, 211],
5976
+ deeppink: [255, 20, 147],
5977
+ deepskyblue: [0, 191, 255],
5978
+ dimgray: [105, 105, 105],
5979
+ dimgrey: [105, 105, 105],
5980
+ dodgerblue: [30, 144, 255],
5981
+ firebrick: [178, 34, 34],
5982
+ floralwhite: [255, 250, 240],
5983
+ forestgreen: [34, 139, 34],
5984
+ fuchsia: [255, 0, 255],
5985
+ gainsboro: [220, 220, 220],
5986
+ ghostwhite: [248, 248, 255],
5987
+ gold: [255, 215, 0],
5988
+ goldenrod: [218, 165, 32],
5989
+ gray: [128, 128, 128],
5990
+ green: [0, 128, 0],
5991
+ greenyellow: [173, 255, 47],
5992
+ grey: [128, 128, 128],
5993
+ honeydew: [240, 255, 240],
5994
+ hotpink: [255, 105, 180],
5995
+ indianred: [205, 92, 92],
5996
+ indigo: [75, 0, 130],
5997
+ ivory: [255, 255, 240],
5998
+ khaki: [240, 230, 140],
5999
+ lavender: [230, 230, 250],
6000
+ lavenderblush: [255, 240, 245],
6001
+ lawngreen: [124, 252, 0],
6002
+ lemonchiffon: [255, 250, 205],
6003
+ lightblue: [173, 216, 230],
6004
+ lightcoral: [240, 128, 128],
6005
+ lightcyan: [224, 255, 255],
6006
+ lightgoldenrodyellow: [250, 250, 210],
6007
+ lightgray: [211, 211, 211],
6008
+ lightgreen: [144, 238, 144],
6009
+ lightgrey: [211, 211, 211],
6010
+ lightpink: [255, 182, 193],
6011
+ lightsalmon: [255, 160, 122],
6012
+ lightseagreen: [32, 178, 170],
6013
+ lightskyblue: [135, 206, 250],
6014
+ lightslategray: [119, 136, 153],
6015
+ lightslategrey: [119, 136, 153],
6016
+ lightsteelblue: [176, 196, 222],
6017
+ lightyellow: [255, 255, 224],
6018
+ lime: [0, 255, 0],
6019
+ limegreen: [50, 205, 50],
6020
+ linen: [250, 240, 230],
6021
+ magenta: [255, 0, 255],
6022
+ maroon: [128, 0, 0],
6023
+ mediumaquamarine: [102, 205, 170],
6024
+ mediumblue: [0, 0, 205],
6025
+ mediumorchid: [186, 85, 211],
6026
+ mediumpurple: [147, 111, 219],
6027
+ mediumseagreen: [60, 179, 113],
6028
+ mediumslateblue: [123, 104, 238],
6029
+ mediumspringgreen: [0, 250, 154],
6030
+ mediumturquoise: [72, 209, 204],
6031
+ mediumvioletred: [199, 21, 133],
6032
+ midnightblue: [25, 25, 112],
6033
+ mintcream: [245, 255, 250],
6034
+ mistyrose: [255, 228, 225],
6035
+ moccasin: [255, 228, 181],
6036
+ navajowhite: [255, 222, 173],
6037
+ navy: [0, 0, 128],
6038
+ oldlace: [253, 245, 230],
6039
+ olive: [128, 128, 0],
6040
+ olivedrab: [107, 142, 35],
6041
+ orange: [255, 165, 0],
6042
+ orangered: [255, 69, 0],
6043
+ orchid: [218, 112, 214],
6044
+ palegoldenrod: [238, 232, 170],
6045
+ palegreen: [152, 251, 152],
6046
+ paleturquoise: [175, 238, 238],
6047
+ palevioletred: [219, 112, 147],
6048
+ papayawhip: [255, 239, 213],
6049
+ peachpuff: [255, 218, 185],
6050
+ peru: [205, 133, 63],
6051
+ pink: [255, 192, 203],
6052
+ plum: [221, 160, 221],
6053
+ powderblue: [176, 224, 230],
6054
+ purple: [128, 0, 128],
6055
+ rebeccapurple: [102, 51, 153],
6056
+ red: [255, 0, 0],
6057
+ rosybrown: [188, 143, 143],
6058
+ royalblue: [65, 105, 225],
6059
+ saddlebrown: [139, 69, 19],
6060
+ salmon: [250, 128, 114],
6061
+ sandybrown: [244, 164, 96],
6062
+ seagreen: [46, 139, 87],
6063
+ seashell: [255, 245, 238],
6064
+ sienna: [160, 82, 45],
6065
+ silver: [192, 192, 192],
6066
+ skyblue: [135, 206, 235],
6067
+ slateblue: [106, 90, 205],
6068
+ slategray: [112, 128, 144],
6069
+ slategrey: [112, 128, 144],
6070
+ snow: [255, 250, 250],
6071
+ springgreen: [0, 255, 127],
6072
+ steelblue: [70, 130, 180],
6073
+ tan: [210, 180, 140],
6074
+ teal: [0, 128, 128],
6075
+ thistle: [216, 191, 216],
6076
+ tomato: [255, 99, 71],
6077
+ turquoise: [64, 224, 208],
6078
+ violet: [238, 130, 238],
6079
+ wheat: [245, 222, 179],
6080
+ white: [255, 255, 255],
6081
+ whitesmoke: [245, 245, 245],
6082
+ yellow: [255, 255, 0],
6083
+ yellowgreen: [154, 205, 50]
6084
+ };
6085
+ function parseAlpha(raw) {
6086
+ if (raw === void 0) return 1;
6087
+ const s = raw.trim();
6088
+ if (s.endsWith("%")) {
6089
+ return Math.min(1, Math.max(0, parseFloat(s) / 100));
6079
6090
  }
6080
- return warnings;
6091
+ return Math.min(1, Math.max(0, parseFloat(s)));
6081
6092
  }
6082
- function yahooAdditionalChecks($, clientId, _html, framework) {
6083
- const warnings = [];
6084
- warnings.push({
6085
- severity: "info",
6086
- client: clientId,
6087
- property: "class",
6088
- message: "Yahoo Mail rewrites CSS class names with a prefix. Class-based selectors in <style> blocks will still work but the names change."
6089
- });
6090
- if ($("[style*='background']").filter(
6091
- (_, el) => ($(el).attr("style") || "").includes("url(")
6092
- ).length > 0) {
6093
- warnings.push(makeWarning({
6094
- severity: "warning",
6095
- client: clientId,
6096
- property: "background-image",
6097
- message: "Yahoo Mail has inconsistent support for CSS background images."
6098
- }, "background-image", clientId, framework));
6099
- }
6100
- return warnings;
6093
+ function clamp255(n) {
6094
+ return Math.round(Math.min(255, Math.max(0, n)));
6101
6095
  }
6102
- function thunderbirdAdditionalChecks($, clientId) {
6103
- if (detectAnimations($)) {
6104
- return [{
6105
- severity: "info",
6106
- client: clientId,
6107
- property: "animation",
6108
- message: "Thunderbird does not support CSS animations or transitions."
6109
- }];
6096
+ function hslToRgb(h, s, l) {
6097
+ const c = (1 - Math.abs(2 * l - 1)) * s;
6098
+ const hp = h / 60;
6099
+ const x = c * (1 - Math.abs(hp % 2 - 1));
6100
+ const m = l - c / 2;
6101
+ let r1 = 0, g1 = 0, b1 = 0;
6102
+ if (hp >= 0 && hp < 1) {
6103
+ r1 = c;
6104
+ g1 = x;
6105
+ b1 = 0;
6106
+ } else if (hp >= 1 && hp < 2) {
6107
+ r1 = x;
6108
+ g1 = c;
6109
+ b1 = 0;
6110
+ } else if (hp >= 2 && hp < 3) {
6111
+ r1 = 0;
6112
+ g1 = c;
6113
+ b1 = x;
6114
+ } else if (hp >= 3 && hp < 4) {
6115
+ r1 = 0;
6116
+ g1 = x;
6117
+ b1 = c;
6118
+ } else if (hp >= 4 && hp < 5) {
6119
+ r1 = x;
6120
+ g1 = 0;
6121
+ b1 = c;
6122
+ } else {
6123
+ r1 = c;
6124
+ g1 = 0;
6125
+ b1 = x;
6110
6126
  }
6111
- return [];
6127
+ return [
6128
+ clamp255((r1 + m) * 255),
6129
+ clamp255((g1 + m) * 255),
6130
+ clamp255((b1 + m) * 255)
6131
+ ];
6112
6132
  }
6113
- function heyAdditionalChecks(_$, clientId, html) {
6114
- const warnings = [];
6115
- if (!html.includes("prefers-color-scheme")) {
6116
- warnings.push({
6117
- severity: "info",
6118
- client: clientId,
6119
- property: "dark-mode",
6120
- message: "HEY Mail supports @media (prefers-color-scheme: dark). Consider adding dark mode styles.",
6121
- suggestion: "Add a @media (prefers-color-scheme: dark) block to optimize for HEY's audience."
6122
- });
6133
+ function hwbToRgb(h, w, b) {
6134
+ if (w + b >= 1) {
6135
+ const gray = clamp255(w / (w + b) * 255);
6136
+ return [gray, gray, gray];
6123
6137
  }
6124
- return warnings;
6138
+ const [r0, g0, b0] = hslToRgb(h, 1, 0.5);
6139
+ return [
6140
+ clamp255(r0 / 255 * (1 - w - b) * 255 + w * 255),
6141
+ clamp255(g0 / 255 * (1 - w - b) * 255 + w * 255),
6142
+ clamp255(b0 / 255 * (1 - w - b) * 255 + w * 255)
6143
+ ];
6125
6144
  }
6126
- function superhumanAdditionalChecks($, clientId, html) {
6127
- const warnings = [];
6128
- if (detectAnimations($)) {
6129
- warnings.push({
6130
- severity: "info",
6131
- client: clientId,
6132
- property: "animation",
6133
- message: "Superhuman may honor OS-level 'reduce motion' preferences, disabling animations.",
6134
- suggestion: "Use @media (prefers-reduced-motion: reduce) to provide static fallbacks."
6135
- });
6136
- }
6137
- warnings.push({
6138
- severity: "info",
6139
- client: clientId,
6140
- property: "<style>",
6141
- message: "Superhuman uses Chromium rendering with excellent CSS support. Flexbox, Grid, CSS variables, and modern properties all work."
6142
- });
6143
- return warnings;
6145
+ function linearToGamma(c) {
6146
+ if (c <= 31308e-7) return 12.92 * c;
6147
+ return 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
6144
6148
  }
6145
- var EMPTY_SET = /* @__PURE__ */ new Set();
6146
- var CLIENT_CONFIGS = {
6147
- "gmail-web": {
6148
- id: "gmail-web",
6149
- strippedProperties: GMAIL_STRIPPED_PROPERTIES,
6150
- stripMode: "strip",
6151
- valueStrips: [
6152
- { prop: "display", pattern: /grid/ },
6153
- { prop: "background", pattern: /linear-gradient|radial-gradient/ }
6154
- ],
6155
- inlineAndStripStyles: true,
6156
- stripExternalStylesheets: true,
6157
- stripForms: true,
6158
- stripSvg: true,
6159
- additionalChecks: gmailAdditionalChecks
6160
- },
6161
- "gmail-android": {
6162
- id: "gmail-android",
6163
- strippedProperties: GMAIL_STRIPPED_PROPERTIES,
6164
- stripMode: "strip",
6165
- valueStrips: [
6166
- { prop: "display", pattern: /grid/ },
6167
- { prop: "background", pattern: /linear-gradient|radial-gradient/ }
6168
- ],
6169
- inlineAndStripStyles: true,
6170
- stripExternalStylesheets: true,
6171
- stripForms: true,
6172
- stripSvg: true,
6173
- additionalChecks: gmailAdditionalChecks
6174
- },
6175
- "gmail-ios": {
6176
- id: "gmail-ios",
6177
- strippedProperties: GMAIL_STRIPPED_PROPERTIES,
6178
- stripMode: "strip",
6179
- valueStrips: [
6180
- { prop: "display", pattern: /grid/ },
6181
- { prop: "background", pattern: /linear-gradient|radial-gradient/ }
6182
- ],
6183
- inlineAndStripStyles: true,
6184
- stripExternalStylesheets: true,
6185
- stripForms: true,
6186
- stripSvg: true,
6187
- additionalChecks: gmailAdditionalChecks
6188
- },
6189
- "outlook-windows": {
6190
- id: "outlook-windows",
6191
- strippedProperties: /* @__PURE__ */ new Set(["position", "transform", "animation", "transition"]),
6192
- stripMode: "strip",
6193
- inlineAndStripStyles: false,
6194
- stripExternalStylesheets: false,
6195
- stripForms: false,
6196
- stripSvg: false
6197
- },
6198
- "outlook-windows-legacy": {
6199
- id: "outlook-windows-legacy",
6200
- strippedProperties: OUTLOOK_WORD_UNSUPPORTED,
6201
- stripMode: "strip",
6202
- valueStrips: [
6203
- { prop: "background", pattern: /linear-gradient|radial-gradient/ },
6204
- { prop: "background-image", pattern: /linear-gradient|radial-gradient/ }
6205
- ],
6206
- inlineAndStripStyles: false,
6207
- stripExternalStylesheets: false,
6208
- stripForms: false,
6209
- stripSvg: false,
6210
- additionalChecks: outlookWindowsAdditionalChecks
6211
- },
6212
- "outlook-web": {
6213
- id: "outlook-web",
6214
- strippedProperties: /* @__PURE__ */ new Set(["position", "transform", "animation", "transition"]),
6215
- stripMode: "strip",
6216
- inlineAndStripStyles: false,
6217
- stripExternalStylesheets: false,
6218
- stripForms: false,
6219
- stripSvg: false
6220
- },
6221
- "apple-mail-macos": {
6222
- id: "apple-mail-macos",
6223
- strippedProperties: EMPTY_SET,
6224
- stripMode: "strip",
6225
- inlineAndStripStyles: false,
6226
- stripExternalStylesheets: false,
6227
- stripForms: false,
6228
- stripSvg: false,
6229
- additionalChecks: appleMailAdditionalChecks
6230
- },
6231
- "apple-mail-ios": {
6232
- id: "apple-mail-ios",
6233
- strippedProperties: EMPTY_SET,
6234
- stripMode: "strip",
6235
- inlineAndStripStyles: false,
6236
- stripExternalStylesheets: false,
6237
- stripForms: false,
6238
- stripSvg: false,
6239
- additionalChecks: appleMailAdditionalChecks
6240
- },
6241
- "yahoo-mail": {
6242
- id: "yahoo-mail",
6243
- strippedProperties: /* @__PURE__ */ new Set(["position", "box-shadow", "transform", "animation", "transition", "opacity"]),
6244
- stripMode: "strip",
6245
- inlineAndStripStyles: false,
6246
- stripExternalStylesheets: false,
6247
- stripForms: false,
6248
- stripSvg: false,
6249
- additionalChecks: yahooAdditionalChecks
6250
- },
6251
- "samsung-mail": {
6252
- id: "samsung-mail",
6253
- strippedProperties: /* @__PURE__ */ new Set(["box-shadow", "transform", "animation", "transition", "opacity"]),
6254
- stripMode: "info",
6255
- inlineAndStripStyles: false,
6256
- stripExternalStylesheets: false,
6257
- stripForms: false,
6258
- stripSvg: false
6259
- },
6260
- "thunderbird": {
6261
- id: "thunderbird",
6262
- strippedProperties: EMPTY_SET,
6263
- stripMode: "strip",
6264
- inlineAndStripStyles: false,
6265
- stripExternalStylesheets: false,
6266
- stripForms: false,
6267
- stripSvg: false,
6268
- additionalChecks: thunderbirdAdditionalChecks
6269
- },
6270
- "hey-mail": {
6271
- id: "hey-mail",
6272
- strippedProperties: /* @__PURE__ */ new Set(["transform", "animation", "transition"]),
6273
- stripMode: "strip",
6274
- valueStrips: [
6275
- { prop: "position", pattern: /fixed|sticky/ }
6276
- ],
6277
- inlineAndStripStyles: false,
6278
- stripExternalStylesheets: true,
6279
- stripForms: true,
6280
- stripSvg: false,
6281
- additionalChecks: heyAdditionalChecks
6282
- },
6283
- "superhuman": {
6284
- id: "superhuman",
6285
- strippedProperties: EMPTY_SET,
6286
- stripMode: "strip",
6287
- inlineAndStripStyles: false,
6288
- stripExternalStylesheets: true,
6289
- stripForms: true,
6290
- stripSvg: false,
6291
- additionalChecks: superhumanAdditionalChecks
6149
+ function oklchToRgb(L, C, H) {
6150
+ const hRad = H * Math.PI / 180;
6151
+ const a = C * Math.cos(hRad);
6152
+ const b = C * Math.sin(hRad);
6153
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
6154
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
6155
+ const s_ = L - 0.0894841775 * a - 1.291485548 * b;
6156
+ const l = l_ * l_ * l_;
6157
+ const m = m_ * m_ * m_;
6158
+ const s = s_ * s_ * s_;
6159
+ const rLin = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
6160
+ const gLin = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
6161
+ const bLin = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s;
6162
+ return [
6163
+ clamp255(linearToGamma(rLin) * 255),
6164
+ clamp255(linearToGamma(gLin) * 255),
6165
+ clamp255(linearToGamma(bLin) * 255)
6166
+ ];
6167
+ }
6168
+ function parseColor(value) {
6169
+ if (!value) return null;
6170
+ const v = value.trim().toLowerCase();
6171
+ if (v === "inherit" || v === "currentcolor" || v === "initial" || v === "unset" || v.startsWith("var(")) {
6172
+ return null;
6292
6173
  }
6293
- };
6294
- function applyTransform(html, config, framework) {
6295
- const $ = cheerio.load(html);
6296
- const warnings = [];
6297
- const clientId = config.id;
6298
- if (config.additionalChecks && config.inlineAndStripStyles) {
6299
- warnings.push(...config.additionalChecks($, clientId, html, framework));
6174
+ if (v === "transparent") {
6175
+ return { r: 0, g: 0, b: 0, a: 0 };
6300
6176
  }
6301
- if (config.inlineAndStripStyles) {
6302
- inlineStyles($);
6303
- $("style").remove();
6177
+ const named = NAMED_COLORS[v];
6178
+ if (named) {
6179
+ return { r: named[0], g: named[1], b: named[2], a: 1 };
6304
6180
  }
6305
- if (config.stripExternalStylesheets) {
6306
- if ($("link[rel='stylesheet']").length > 0) {
6307
- warnings.push(makeWarning({
6308
- severity: "error",
6309
- client: clientId,
6310
- property: "<link>",
6311
- message: `${clientId} does not load external stylesheets.`
6312
- }, "<link>", clientId, framework));
6313
- $("link[rel='stylesheet']").remove();
6181
+ if (v.startsWith("#")) {
6182
+ const hex = v.slice(1);
6183
+ if (hex.length === 3) {
6184
+ return {
6185
+ r: parseInt(hex[0] + hex[0], 16),
6186
+ g: parseInt(hex[1] + hex[1], 16),
6187
+ b: parseInt(hex[2] + hex[2], 16),
6188
+ a: 1
6189
+ };
6314
6190
  }
6191
+ if (hex.length === 4) {
6192
+ return {
6193
+ r: parseInt(hex[0] + hex[0], 16),
6194
+ g: parseInt(hex[1] + hex[1], 16),
6195
+ b: parseInt(hex[2] + hex[2], 16),
6196
+ a: parseInt(hex[3] + hex[3], 16) / 255
6197
+ };
6198
+ }
6199
+ if (hex.length === 6) {
6200
+ return {
6201
+ r: parseInt(hex.slice(0, 2), 16),
6202
+ g: parseInt(hex.slice(2, 4), 16),
6203
+ b: parseInt(hex.slice(4, 6), 16),
6204
+ a: 1
6205
+ };
6206
+ }
6207
+ if (hex.length === 8) {
6208
+ return {
6209
+ r: parseInt(hex.slice(0, 2), 16),
6210
+ g: parseInt(hex.slice(2, 4), 16),
6211
+ b: parseInt(hex.slice(4, 6), 16),
6212
+ a: parseInt(hex.slice(6, 8), 16) / 255
6213
+ };
6214
+ }
6215
+ return null;
6315
6216
  }
6316
- if (config.stripForms && $("form").length > 0) {
6317
- warnings.push(makeWarning({
6318
- severity: "error",
6319
- client: clientId,
6320
- property: "<form>",
6321
- message: `${clientId} removes form elements.`
6322
- }, "<form>", clientId, framework));
6323
- $("form").each((_, el) => {
6324
- $(el).replaceWith($(el).html() || "");
6325
- });
6326
- }
6327
- if (config.stripSvg && $("svg").length > 0) {
6328
- warnings.push(makeWarning({
6329
- severity: "error",
6330
- client: clientId,
6331
- property: "<svg>",
6332
- message: `${clientId} does not support inline SVG elements.`
6333
- }, "<svg>", clientId, framework));
6334
- $("svg").each((_, el) => {
6335
- $(el).replaceWith('<img alt="[SVG not supported]" />');
6336
- });
6337
- }
6338
- if (config.strippedProperties.size > 0 || config.valueStrips && config.valueStrips.length > 0) {
6339
- $("[style]").each((_, el) => {
6340
- const style = $(el).attr("style") || "";
6341
- const props = parseInlineStyle(style);
6342
- const removed = [];
6343
- props.forEach((value, prop) => {
6344
- var _a;
6345
- if (config.strippedProperties.has(prop)) {
6346
- if (config.stripMode === "strip") {
6347
- removed.push(prop);
6348
- props.delete(prop);
6349
- } else {
6350
- warnings.push(makeWarning({
6351
- severity: "info",
6352
- client: clientId,
6353
- property: prop,
6354
- message: `${clientId} has limited support for "${prop}".`
6355
- }, prop, clientId, framework));
6356
- }
6357
- return;
6358
- }
6359
- for (const vs of (_a = config.valueStrips) != null ? _a : []) {
6360
- if (prop === vs.prop && vs.pattern.test(value)) {
6361
- removed.push(prop);
6362
- props.delete(prop);
6363
- return;
6364
- }
6365
- }
6366
- });
6367
- if (removed.length > 0) {
6368
- $(el).attr("style", serializeStyle(props));
6369
- for (const prop of removed) {
6370
- warnings.push(makeWarning({
6371
- severity: "warning",
6372
- client: clientId,
6373
- property: prop,
6374
- message: `${clientId} strips "${prop}" from styles.`
6375
- }, prop, clientId, framework));
6376
- }
6377
- }
6378
- });
6217
+ const rgbComma = v.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+%?))?\s*\)$/);
6218
+ if (rgbComma) {
6219
+ return {
6220
+ r: Math.min(255, parseInt(rgbComma[1], 10)),
6221
+ g: Math.min(255, parseInt(rgbComma[2], 10)),
6222
+ b: Math.min(255, parseInt(rgbComma[3], 10)),
6223
+ a: rgbComma[4] !== void 0 ? parseAlpha(rgbComma[4]) : 1
6224
+ };
6379
6225
  }
6380
- if (config.inlineAndStripStyles) {
6381
- warnings.push(...gmailPostChecks($, clientId, html, framework));
6226
+ const rgbSpace = v.match(/^rgba?\(\s*(\d+)\s+(\d+)\s+(\d+)(?:\s*\/\s*([\d.]+%?))?\s*\)$/);
6227
+ if (rgbSpace) {
6228
+ return {
6229
+ r: Math.min(255, parseInt(rgbSpace[1], 10)),
6230
+ g: Math.min(255, parseInt(rgbSpace[2], 10)),
6231
+ b: Math.min(255, parseInt(rgbSpace[3], 10)),
6232
+ a: rgbSpace[4] !== void 0 ? parseAlpha(rgbSpace[4]) : 1
6233
+ };
6382
6234
  }
6383
- if (config.additionalChecks && !config.inlineAndStripStyles) {
6384
- warnings.push(...config.additionalChecks($, clientId, html, framework));
6235
+ const hslComma = v.match(/^hsla?\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%(?:\s*,\s*([\d.]+%?))?\s*\)$/);
6236
+ if (hslComma) {
6237
+ const h = (parseFloat(hslComma[1]) % 360 + 360) % 360;
6238
+ const s = Math.min(100, parseFloat(hslComma[2])) / 100;
6239
+ const l = Math.min(100, parseFloat(hslComma[3])) / 100;
6240
+ const [r, g, b] = hslToRgb(h, s, l);
6241
+ return { r, g, b, a: hslComma[4] !== void 0 ? parseAlpha(hslComma[4]) : 1 };
6242
+ }
6243
+ const hslSpace = v.match(/^hsla?\(\s*([\d.]+)\s+([\d.]+)%\s+([\d.]+)%(?:\s*\/\s*([\d.]+%?))?\s*\)$/);
6244
+ if (hslSpace) {
6245
+ const h = (parseFloat(hslSpace[1]) % 360 + 360) % 360;
6246
+ const s = Math.min(100, parseFloat(hslSpace[2])) / 100;
6247
+ const l = Math.min(100, parseFloat(hslSpace[3])) / 100;
6248
+ const [r, g, b] = hslToRgb(h, s, l);
6249
+ return { r, g, b, a: hslSpace[4] !== void 0 ? parseAlpha(hslSpace[4]) : 1 };
6250
+ }
6251
+ const hwbMatch = v.match(/^hwb\(\s*([\d.]+)\s+([\d.]+)%\s+([\d.]+)%(?:\s*\/\s*([\d.]+%?))?\s*\)$/);
6252
+ if (hwbMatch) {
6253
+ const h = (parseFloat(hwbMatch[1]) % 360 + 360) % 360;
6254
+ const w = Math.min(100, parseFloat(hwbMatch[2])) / 100;
6255
+ const bk = Math.min(100, parseFloat(hwbMatch[3])) / 100;
6256
+ const [r, g, b] = hwbToRgb(h, w, bk);
6257
+ return { r, g, b, a: hwbMatch[4] !== void 0 ? parseAlpha(hwbMatch[4]) : 1 };
6258
+ }
6259
+ const oklchMatch = v.match(/^oklch\(\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*([\d.]+%?))?\s*\)$/);
6260
+ if (oklchMatch) {
6261
+ const L = Math.min(1, Math.max(0, parseFloat(oklchMatch[1])));
6262
+ const C = Math.max(0, parseFloat(oklchMatch[2]));
6263
+ const H = (parseFloat(oklchMatch[3]) % 360 + 360) % 360;
6264
+ const [r, g, b] = oklchToRgb(L, C, H);
6265
+ return { r, g, b, a: oklchMatch[4] !== void 0 ? parseAlpha(oklchMatch[4]) : 1 };
6385
6266
  }
6386
- return { clientId, html: $.html(), warnings };
6267
+ return null;
6387
6268
  }
6388
- function transformForClient(html, clientId, framework) {
6389
- if (!html || !html.trim()) {
6390
- return { clientId, html: html || "", warnings: [] };
6391
- }
6392
- if (html.length > MAX_HTML_SIZE) {
6393
- throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
6394
- }
6395
- const config = CLIENT_CONFIGS[clientId];
6396
- if (!config) {
6397
- return {
6398
- clientId,
6399
- html,
6400
- warnings: [
6401
- {
6402
- severity: "info",
6403
- client: clientId,
6404
- property: "unknown",
6405
- message: `No transformation rules available for client "${clientId}".`
6406
- }
6407
- ]
6408
- };
6409
- }
6410
- return applyTransform(html, config, framework);
6269
+ function formatRgb(rgba) {
6270
+ const r = clamp255(rgba.r);
6271
+ const g = clamp255(rgba.g);
6272
+ const b = clamp255(rgba.b);
6273
+ if (rgba.a < 1) {
6274
+ return `rgba(${r}, ${g}, ${b}, ${rgba.a})`;
6275
+ }
6276
+ return `rgb(${r}, ${g}, ${b})`;
6411
6277
  }
6412
- function transformForAllClients(html, framework) {
6413
- return Object.keys(CLIENT_CONFIGS).map(
6414
- (clientId) => transformForClient(html, clientId, framework)
6415
- );
6278
+ function relativeLuminance(r, g, b) {
6279
+ const [rs, gs, bs] = [r, g, b].map((c) => {
6280
+ const s = c / 255;
6281
+ return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
6282
+ });
6283
+ return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
6284
+ }
6285
+ function contrastRatio(l1, l2) {
6286
+ const lighter = Math.max(l1, l2);
6287
+ const darker = Math.min(l1, l2);
6288
+ return (lighter + 0.05) / (darker + 0.05);
6289
+ }
6290
+ function wcagGrade(ratio) {
6291
+ if (ratio >= 7) return "AAA";
6292
+ if (ratio >= 4.5) return "AA";
6293
+ if (ratio >= 3) return "AA Large";
6294
+ return "Fail";
6295
+ }
6296
+ function alphaBlend(fg, bgR, bgG, bgB) {
6297
+ const a = fg.a;
6298
+ return [
6299
+ Math.round(fg.r * a + bgR * (1 - a)),
6300
+ Math.round(fg.g * a + bgG * (1 - a)),
6301
+ Math.round(fg.b * a + bgB * (1 - a))
6302
+ ];
6416
6303
  }
6417
6304
 
6418
- // src/analyze.ts
6419
- var cheerio2 = __toESM(require("cheerio"), 1);
6420
- var csstree2 = __toESM(require("css-tree"), 1);
6421
- var HTML_ELEMENT_SELECTORS = {
6422
- "<style>": "style",
6423
- "<link>": "link[rel='stylesheet']",
6424
- "<svg>": "svg",
6425
- "<video>": "video",
6426
- "<form>": "form, input, button[type='submit']",
6427
- "<audio>": "audio",
6428
- "<picture>": "picture",
6429
- "<dialog>": "dialog",
6430
- "<meter>": "meter",
6431
- "<progress>": "progress",
6432
- "<select>": "select",
6433
- "<textarea>": "textarea",
6434
- "<marquee>": "marquee",
6435
- "<object>": "object",
6436
- "<base>": "base"
6437
- };
6438
- var HTML_ELEMENT_SEVERITY = {
6439
- "<style>": "error",
6440
- "<link>": "error",
6441
- "<svg>": "error",
6442
- "<form>": "error",
6443
- "<video>": "warning",
6444
- "<audio>": "warning",
6445
- "<picture>": "warning",
6446
- "<dialog>": "warning",
6447
- "<marquee>": "warning",
6448
- "<meter>": "warning",
6449
- "<progress>": "warning",
6450
- "<select>": "warning",
6451
- "<textarea>": "warning",
6452
- "<object>": "warning",
6453
- "<base>": "warning"
6454
- };
6455
- var HTML_ELEMENT_MESSAGES = {
6456
- "<style>": (n) => `${n} strips <style> blocks. Styles must be inlined.`,
6457
- "<link>": (n) => `${n} does not support external stylesheets.`,
6458
- "<svg>": (n) => `${n} does not support inline SVG.`,
6459
- "<video>": (n) => `${n} does not support <video> elements.`,
6460
- "<form>": (n) => `${n} strips form elements.`
6305
+ // src/downlevel.ts
6306
+ var LOGICAL_PROPERTY_MAP = {
6307
+ "padding-inline": ["padding-left", "padding-right"],
6308
+ "padding-block": ["padding-top", "padding-bottom"],
6309
+ "margin-inline": ["margin-left", "margin-right"],
6310
+ "margin-block": ["margin-top", "margin-bottom"],
6311
+ "inset-inline": ["left", "right"],
6312
+ "inset-block": ["top", "bottom"],
6313
+ "border-inline": ["border-left", "border-right"],
6314
+ "border-block": ["border-top", "border-bottom"]
6461
6315
  };
6462
- var COMPOUND_DETECTORS = [
6463
- { key: "display:flex", property: "display", valueIncludes: "flex" },
6464
- { key: "display:grid", property: "display", valueIncludes: "grid" },
6465
- { key: "display:none", property: "display", valueIncludes: "none" }
6466
- ];
6467
- var CSS_FUNCTION_DETECTORS = CSS_FUNCTION_FEATURES.map((fn) => ({
6468
- key: fn,
6469
- pattern: `${fn}(`
6470
- // require opening paren — matches "min(" but not "Minion"
6471
- }));
6472
- function analyzeEmailFromDom($, framework) {
6473
- const warnings = [];
6474
- const seenWarnings = /* @__PURE__ */ new Set();
6475
- function addWarning(w) {
6476
- const key = `${w.client}:${w.property}:${w.severity}:${w.selector || ""}`;
6477
- if (!seenWarnings.has(key)) {
6478
- seenWarnings.add(key);
6479
- warnings.push(w);
6316
+ var MODERN_COLOR_RE = /\b(oklch|hsl|hsla|hwb|rgba?)\s*\(/g;
6317
+ function extractFunctionCall(str, fnStart, openParenIndex) {
6318
+ let depth = 0;
6319
+ for (let i = openParenIndex; i < str.length; i++) {
6320
+ if (str[i] === "(") depth++;
6321
+ else if (str[i] === ")") {
6322
+ depth--;
6323
+ if (depth === 0) {
6324
+ return str.slice(fnStart, i + 1);
6325
+ }
6480
6326
  }
6481
6327
  }
6482
- function describeSelector(el) {
6483
- var _a;
6484
- const $el = $(el);
6485
- const tag = ((_a = el.tagName) == null ? void 0 : _a.toLowerCase()) || "";
6486
- const cls = $el.attr("class");
6487
- const id = $el.attr("id");
6488
- if (id) return `${tag}#${id}`;
6489
- if (cls) return `${tag}.${cls.split(/\s+/)[0]}`;
6490
- const href = $el.attr("href");
6491
- if (href) return `${tag}[href]`;
6492
- return tag;
6328
+ return null;
6329
+ }
6330
+ function isSpaceSyntaxRgb(args) {
6331
+ const inner = args.trim();
6332
+ return !inner.includes(",") && /^\d/.test(inner);
6333
+ }
6334
+ function convertModernColors(value) {
6335
+ let result = value;
6336
+ let match;
6337
+ MODERN_COLOR_RE.lastIndex = 0;
6338
+ const replacements = [];
6339
+ while ((match = MODERN_COLOR_RE.exec(result)) !== null) {
6340
+ const fnName = match[1].toLowerCase();
6341
+ const fnStart = match.index;
6342
+ const openParen = fnStart + match[0].length - 1;
6343
+ const fullCall = extractFunctionCall(result, fnStart, openParen);
6344
+ if (!fullCall) continue;
6345
+ if (fnName === "rgb" || fnName === "rgba") {
6346
+ const argsStr = fullCall.slice(fullCall.indexOf("(") + 1, -1);
6347
+ if (!isSpaceSyntaxRgb(argsStr)) continue;
6348
+ }
6349
+ const parsed = parseColor(fullCall);
6350
+ if (parsed) {
6351
+ replacements.push({
6352
+ start: fnStart,
6353
+ end: fnStart + fullCall.length,
6354
+ replacement: formatRgb(parsed)
6355
+ });
6356
+ }
6493
6357
  }
6494
- for (const feature of HTML_ELEMENT_FEATURES) {
6495
- const selector = HTML_ELEMENT_SELECTORS[feature];
6496
- if (!selector) continue;
6497
- if ($(selector).length === 0) continue;
6498
- const supportData = CSS_SUPPORT[feature];
6499
- if (!supportData) continue;
6500
- const baseSeverity = HTML_ELEMENT_SEVERITY[feature] || "warning";
6501
- for (const client of EMAIL_CLIENTS) {
6502
- const support = supportData[client.id];
6503
- if (support === "unsupported") {
6504
- const msgFn = HTML_ELEMENT_MESSAGES[feature];
6505
- const message = msgFn ? msgFn(client.name) : `${client.name} does not support ${feature}.`;
6506
- const sug = getSuggestion(feature, client.id, framework);
6507
- const fix = getCodeFix(feature, client.id, framework);
6508
- addWarning(__spreadValues({
6509
- severity: baseSeverity,
6510
- client: client.id,
6511
- property: feature,
6512
- message,
6513
- suggestion: sug.text,
6514
- fix,
6515
- fixType: getFixType(feature)
6516
- }, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(feature, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
6517
- } else if (support === "partial" && feature === "<style>") {
6518
- const sug = getSuggestion("<style>:partial", client.id, framework);
6519
- const fix = getCodeFix("<style>", client.id, framework);
6520
- addWarning(__spreadValues({
6521
- severity: "warning",
6522
- client: client.id,
6523
- property: "<style>",
6524
- message: `${client.name} has partial <style> support (head only, with limitations). Inline styles recommended.`,
6525
- suggestion: sug.text,
6526
- fix,
6527
- fixType: getFixType("<style>")
6528
- }, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<style>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
6358
+ for (let i = replacements.length - 1; i >= 0; i--) {
6359
+ const r = replacements[i];
6360
+ result = result.slice(0, r.start) + r.replacement + result.slice(r.end);
6361
+ }
6362
+ return result;
6363
+ }
6364
+ function convertLogicalProperty(prop, value) {
6365
+ const mapping = LOGICAL_PROPERTY_MAP[prop];
6366
+ if (!mapping) return null;
6367
+ const parts = value.trim().split(/\s+/);
6368
+ const [first, second] = mapping;
6369
+ if (parts.length >= 2) {
6370
+ return [
6371
+ [first, parts[0]],
6372
+ [second, parts[1]]
6373
+ ];
6374
+ }
6375
+ return [
6376
+ [first, parts[0]],
6377
+ [second, parts[0]]
6378
+ ];
6379
+ }
6380
+ var CALC_INFINITY_RE = /calc\(\s*infinity\s*\*\s*1(px|em|rem|%|vh|vw)\s*\)/gi;
6381
+ var CALC_INFINITY_SIMPLE_RE = /calc\(\s*infinity\s*\)/gi;
6382
+ function resolveCalcInfinity(value) {
6383
+ let result = value.replace(CALC_INFINITY_RE, (_match, unit) => {
6384
+ return `9999${unit}`;
6385
+ });
6386
+ result = result.replace(CALC_INFINITY_SIMPLE_RE, "9999px");
6387
+ return result;
6388
+ }
6389
+ function unnestCSSRules(css) {
6390
+ let result = "";
6391
+ let i = 0;
6392
+ while (i < css.length) {
6393
+ const atMediaInside = findNestedAtRule(css, i);
6394
+ if (atMediaInside) {
6395
+ result += css.slice(i, atMediaInside.outerStart);
6396
+ if (atMediaInside.preamble) {
6397
+ result += `${atMediaInside.selector}{${atMediaInside.preamble}}`;
6529
6398
  }
6399
+ result += `${atMediaInside.atRule}{${atMediaInside.selector}{${atMediaInside.body}}}`;
6400
+ i = atMediaInside.outerEnd;
6401
+ } else {
6402
+ result += css.slice(i);
6403
+ break;
6530
6404
  }
6531
6405
  }
6532
- const parsedAtRules = /* @__PURE__ */ new Set();
6533
- const parsedProperties = /* @__PURE__ */ new Set();
6534
- const propertyLines = /* @__PURE__ */ new Map();
6535
- const detectedCssFunctions = /* @__PURE__ */ new Set();
6536
- const detectedPseudoClasses = /* @__PURE__ */ new Set();
6537
- const detectedPseudoElements = /* @__PURE__ */ new Set();
6538
- $("style").each((_, el) => {
6539
- const cssText = $(el).text();
6540
- try {
6541
- const ast = csstree2.parse(cssText, { parseCustomProperty: true, positions: true });
6542
- csstree2.walk(ast, {
6543
- enter(node) {
6544
- if (node.type === "Atrule") {
6545
- parsedAtRules.add(`@${node.name}`);
6546
- }
6547
- if (node.type === "PseudoClassSelector") {
6548
- detectedPseudoClasses.add(`:${node.name}`);
6549
- }
6550
- if (node.type === "PseudoElementSelector") {
6551
- detectedPseudoElements.add(`::${node.name}`);
6406
+ return result;
6407
+ }
6408
+ function findNestedAtRule(css, startPos) {
6409
+ for (let i = startPos; i < css.length; i++) {
6410
+ if (css[i] === "{") {
6411
+ const selector = css.slice(startPos, i).trim();
6412
+ if (!selector || selector.startsWith("@")) {
6413
+ const closeIdx2 = findMatchingBrace(css, i);
6414
+ if (closeIdx2 === -1) return null;
6415
+ const rest = findNestedAtRule(css, closeIdx2 + 1);
6416
+ if (rest) return rest;
6417
+ return null;
6418
+ }
6419
+ const innerStart = i + 1;
6420
+ const closeIdx = findMatchingBrace(css, i);
6421
+ if (closeIdx === -1) return null;
6422
+ const innerContent = css.slice(innerStart, closeIdx).trim();
6423
+ const atMatch = innerContent.match(/@(media|supports)\s/);
6424
+ if (atMatch && atMatch.index !== void 0) {
6425
+ const atPos = atMatch.index;
6426
+ const atBraceIdx = innerContent.indexOf("{", atPos);
6427
+ if (atBraceIdx === -1) continue;
6428
+ const atRule = innerContent.slice(atPos, atBraceIdx).trim();
6429
+ const atCloseIdx = findMatchingBrace(innerContent, atBraceIdx);
6430
+ if (atCloseIdx === -1) continue;
6431
+ const body = innerContent.slice(atBraceIdx + 1, atCloseIdx).trim();
6432
+ const beforeAt = innerContent.slice(0, atPos).trim();
6433
+ return __spreadValues({
6434
+ outerStart: findSelectorStart(css, i, startPos),
6435
+ outerEnd: closeIdx + 1,
6436
+ selector,
6437
+ atRule,
6438
+ body
6439
+ }, beforeAt ? { preamble: beforeAt } : {});
6440
+ }
6441
+ }
6442
+ }
6443
+ return null;
6444
+ }
6445
+ function findSelectorStart(css, braceIdx, minPos) {
6446
+ let i = braceIdx - 1;
6447
+ while (i >= minPos && css[i] !== "}" && css[i] !== ";") {
6448
+ i--;
6449
+ }
6450
+ return i + 1;
6451
+ }
6452
+ function findMatchingBrace(css, openIdx) {
6453
+ let depth = 0;
6454
+ for (let i = openIdx; i < css.length; i++) {
6455
+ if (css[i] === "{") depth++;
6456
+ else if (css[i] === "}") {
6457
+ depth--;
6458
+ if (depth === 0) return i;
6459
+ }
6460
+ }
6461
+ return -1;
6462
+ }
6463
+ function convertRangeMediaQueries(css) {
6464
+ return css.replace(
6465
+ /\(\s*width\s*(>=?|<=?)\s*([\d.]+\w*)\s*\)/g,
6466
+ (_match, op, value) => {
6467
+ if (op === ">=" || op === ">") {
6468
+ return `(min-width: ${value})`;
6469
+ }
6470
+ return `(max-width: ${value})`;
6471
+ }
6472
+ );
6473
+ }
6474
+ var UNIVERSAL_SELECTORS = /* @__PURE__ */ new Set([":root", "*", "html"]);
6475
+ function selectorsCouldMatch(defSelector, useSelector) {
6476
+ if (defSelector === useSelector) return true;
6477
+ if (UNIVERSAL_SELECTORS.has(defSelector)) return true;
6478
+ return false;
6479
+ }
6480
+ function resolveCSSVariables(ast) {
6481
+ const varDefs = [];
6482
+ csstree.walk(ast, {
6483
+ visit: "Rule",
6484
+ enter(rule) {
6485
+ if (rule.type !== "Rule" || !rule.prelude || !rule.block) return;
6486
+ const selector = csstree.generate(rule.prelude);
6487
+ rule.block.children.forEach((node) => {
6488
+ if (node.type === "Declaration" && node.property.startsWith("--")) {
6489
+ varDefs.push({
6490
+ selector,
6491
+ varName: node.property,
6492
+ value: csstree.generate(node.value)
6493
+ });
6494
+ }
6495
+ });
6496
+ }
6497
+ });
6498
+ if (varDefs.length === 0) return;
6499
+ csstree.walk(ast, {
6500
+ visit: "Rule",
6501
+ enter(rule) {
6502
+ if (rule.type !== "Rule" || !rule.prelude || !rule.block) return;
6503
+ const useSelector = csstree.generate(rule.prelude);
6504
+ csstree.walk(rule.block, {
6505
+ visit: "Function",
6506
+ enter(node, item, list) {
6507
+ if (node.name !== "var" || !list) return;
6508
+ const args = [];
6509
+ if (node.children) {
6510
+ node.children.forEach((child) => {
6511
+ args.push(csstree.generate(child));
6512
+ });
6552
6513
  }
6553
- if (node.type === "Declaration") {
6554
- const prop = node.property.toLowerCase();
6555
- parsedProperties.add(prop);
6556
- if (node.loc && !propertyLines.has(prop)) {
6557
- propertyLines.set(prop, node.loc.start.line);
6514
+ const fullArgs = args.join("");
6515
+ const commaIdx = fullArgs.indexOf(",");
6516
+ const varName = commaIdx !== -1 ? fullArgs.slice(0, commaIdx).trim() : fullArgs.trim();
6517
+ const fallback = commaIdx !== -1 ? fullArgs.slice(commaIdx + 1).trim() : null;
6518
+ let resolved = null;
6519
+ for (const def of varDefs) {
6520
+ if (def.varName === varName && def.selector === useSelector) {
6521
+ resolved = def.value;
6522
+ break;
6558
6523
  }
6559
- const valueStr = csstree2.generate(node.value);
6560
- for (const det of COMPOUND_DETECTORS) {
6561
- if (prop === det.property && valueStr.includes(det.valueIncludes)) {
6562
- parsedProperties.add(det.key);
6563
- if (node.loc && !propertyLines.has(det.key)) {
6564
- propertyLines.set(det.key, node.loc.start.line);
6565
- }
6524
+ }
6525
+ if (!resolved) {
6526
+ for (const def of varDefs) {
6527
+ if (def.varName === varName && selectorsCouldMatch(def.selector, useSelector)) {
6528
+ resolved = def.value;
6529
+ break;
6566
6530
  }
6567
6531
  }
6568
- for (const fn of CSS_FUNCTION_DETECTORS) {
6569
- if (valueStr.includes(fn.pattern)) {
6570
- detectedCssFunctions.add(fn.key);
6571
- if (node.loc && !propertyLines.has(fn.key)) {
6572
- propertyLines.set(fn.key, node.loc.start.line);
6573
- }
6574
- }
6532
+ }
6533
+ if (!resolved && fallback) {
6534
+ resolved = fallback;
6535
+ }
6536
+ if (resolved && item) {
6537
+ const replacement = csstree.parse(resolved, {
6538
+ context: "value"
6539
+ });
6540
+ if (replacement.children) {
6541
+ replacement.children.forEach((child) => {
6542
+ list.insertData(child, item);
6543
+ });
6544
+ list.remove(item);
6575
6545
  }
6576
6546
  }
6577
6547
  }
6578
6548
  });
6579
- } catch (e) {
6580
6549
  }
6581
6550
  });
6582
- for (const atRule of AT_RULE_FEATURES) {
6583
- if (!parsedAtRules.has(atRule)) continue;
6584
- checkPropertySupport(atRule, addWarning, framework);
6585
- }
6586
- const cssPropertiesToCheck = Object.keys(CSS_SUPPORT).filter(
6587
- (k) => !k.startsWith("<") && !k.startsWith("@")
6588
- );
6589
- $("[style]").each((_, el) => {
6590
- const style = $(el).attr("style") || "";
6591
- const props = parseStyleProperties(style);
6592
- const selector = describeSelector(el);
6593
- for (const prop of props) {
6594
- for (const det of COMPOUND_DETECTORS) {
6595
- if (prop === det.property) {
6596
- const value2 = getStyleValue(style, prop);
6597
- if (value2 == null ? void 0 : value2.includes(det.valueIncludes)) {
6598
- checkPropertySupport(det.key, addWarning, framework, selector);
6551
+ }
6552
+ function sanitizeDeclarations(ast) {
6553
+ csstree.walk(ast, {
6554
+ visit: "Declaration",
6555
+ enter(decl) {
6556
+ if (decl.property.startsWith("--")) return;
6557
+ if (decl.value.type === "Value") {
6558
+ const raw = csstree.generate(decl.value);
6559
+ let converted = convertModernColors(raw);
6560
+ converted = resolveCalcInfinity(converted);
6561
+ if (converted !== raw) {
6562
+ try {
6563
+ const newValue = csstree.parse(converted, {
6564
+ context: "value"
6565
+ });
6566
+ decl.value = newValue;
6567
+ } catch (e) {
6599
6568
  }
6600
6569
  }
6601
6570
  }
6602
- if (cssPropertiesToCheck.includes(prop)) {
6603
- checkPropertySupport(prop, addWarning, framework, selector);
6571
+ }
6572
+ });
6573
+ csstree.walk(ast, {
6574
+ visit: "Rule",
6575
+ enter(rule) {
6576
+ if (rule.type !== "Rule" || !rule.block) return;
6577
+ const toInsert = [];
6578
+ rule.block.children.forEach(function(node, item) {
6579
+ if (node.type !== "Declaration") return;
6580
+ const mapping = LOGICAL_PROPERTY_MAP[node.property];
6581
+ if (!mapping) return;
6582
+ const valueStr = csstree.generate(node.value);
6583
+ const parts = valueStr.trim().split(/\s+/);
6584
+ const [first, second] = mapping;
6585
+ const val1 = parts[0];
6586
+ const val2 = parts.length >= 2 ? parts[1] : parts[0];
6587
+ toInsert.push(
6588
+ { property: first, value: val1, important: !!node.important, item },
6589
+ { property: second, value: val2, important: !!node.important, item }
6590
+ );
6591
+ });
6592
+ for (const { property, value, important, item } of toInsert) {
6593
+ try {
6594
+ const newDecl = {
6595
+ type: "Declaration",
6596
+ important,
6597
+ property,
6598
+ value: csstree.parse(value, { context: "value" })
6599
+ };
6600
+ rule.block.children.insertData(newDecl, item);
6601
+ } catch (e) {
6602
+ }
6604
6603
  }
6605
- const value = getStyleValue(style, prop);
6606
- if (value) {
6607
- for (const fn of CSS_FUNCTION_DETECTORS) {
6608
- if (value.includes(fn.pattern)) {
6609
- checkPropertySupport(fn.key, addWarning, framework, selector);
6610
- }
6604
+ const removed = /* @__PURE__ */ new Set();
6605
+ for (const { item } of toInsert) {
6606
+ if (!removed.has(item)) {
6607
+ removed.add(item);
6608
+ rule.block.children.remove(item);
6611
6609
  }
6612
6610
  }
6613
6611
  }
6614
6612
  });
6615
- for (const prop of parsedProperties) {
6616
- if (prop.includes(":")) continue;
6617
- if (!cssPropertiesToCheck.includes(prop)) continue;
6618
- checkPropertySupport(prop, addWarning, framework, void 0, propertyLines.get(prop));
6619
- }
6620
- for (const compound of COMPOUND_VALUE_FEATURES) {
6621
- if (compound.startsWith(":") || compound.startsWith("::")) continue;
6622
- if (parsedProperties.has(compound)) {
6623
- checkPropertySupport(compound, addWarning, framework, void 0, propertyLines.get(compound));
6613
+ }
6614
+ function downlevelCSS(html) {
6615
+ const $ = cheerio.load(html);
6616
+ $("style").each((_, el) => {
6617
+ let css = $(el).text();
6618
+ css = unnestCSSRules(css);
6619
+ css = convertRangeMediaQueries(css);
6620
+ try {
6621
+ const ast = csstree.parse(css, { parseCustomProperty: true });
6622
+ resolveCSSVariables(ast);
6623
+ sanitizeDeclarations(ast);
6624
+ $(el).text(csstree.generate(ast));
6625
+ } catch (e) {
6626
+ $(el).text(css);
6624
6627
  }
6625
- }
6626
- for (const pseudo of detectedPseudoClasses) {
6627
- if (CSS_SUPPORT[pseudo]) {
6628
- checkPropertySupport(pseudo, addWarning, framework);
6628
+ });
6629
+ $("[style]").each((_, el) => {
6630
+ const style = $(el).attr("style") || "";
6631
+ const props = parseInlineStyle(style);
6632
+ let changed = false;
6633
+ const newProps = /* @__PURE__ */ new Map();
6634
+ props.forEach((value, prop) => {
6635
+ const converted = convertModernColors(value);
6636
+ if (converted !== value) changed = true;
6637
+ const physical = convertLogicalProperty(prop, converted);
6638
+ if (physical) {
6639
+ changed = true;
6640
+ for (const [p, v] of physical) {
6641
+ newProps.set(p, v);
6642
+ }
6643
+ } else {
6644
+ const calcFixed = resolveCalcInfinity(converted);
6645
+ if (calcFixed !== converted) changed = true;
6646
+ newProps.set(prop, calcFixed);
6647
+ }
6648
+ });
6649
+ if (changed) {
6650
+ $(el).attr("style", serializeStyle(newProps));
6629
6651
  }
6630
- }
6631
- for (const pseudo of detectedPseudoElements) {
6632
- if (CSS_SUPPORT[pseudo]) {
6633
- checkPropertySupport(pseudo, addWarning, framework);
6652
+ });
6653
+ return $.html();
6654
+ }
6655
+
6656
+ // src/constants.ts
6657
+ var MAX_HTML_SIZE = 2 * 1024 * 1024;
6658
+ var GENERIC_LINK_TEXT = /* @__PURE__ */ new Set([
6659
+ "click here",
6660
+ "here",
6661
+ "read more",
6662
+ "learn more",
6663
+ "more",
6664
+ "link",
6665
+ "this link",
6666
+ "click",
6667
+ "tap here",
6668
+ "this"
6669
+ ]);
6670
+ var GMAIL_CLIP_THRESHOLD = 102 * 1024;
6671
+ var GMAIL_CLIP_WARNING_THRESHOLD = 90 * 1024;
6672
+ var CLIENT_DISPLAY_LIMITS = [
6673
+ { client: "Gmail (Web)", subjectLimit: 70, preheaderLimit: 90 },
6674
+ { client: "Gmail (Mobile)", subjectLimit: 40, preheaderLimit: 45 },
6675
+ { client: "Outlook (Web)", subjectLimit: 60, preheaderLimit: 90 },
6676
+ { client: "Outlook (Desktop)", subjectLimit: 55, preheaderLimit: 35 },
6677
+ { client: "Apple Mail (macOS)", subjectLimit: 78, preheaderLimit: 140 },
6678
+ { client: "Apple Mail (iOS)", subjectLimit: 41, preheaderLimit: 90 },
6679
+ { client: "Yahoo Mail", subjectLimit: 46, preheaderLimit: 100 },
6680
+ { client: "Samsung Email", subjectLimit: 40, preheaderLimit: 70 }
6681
+ ];
6682
+ var TEMPLATE_VARIABLE_PATTERNS = [
6683
+ [/\{\{[\s\S]*?\}\}/g, "Handlebars/Mustache"],
6684
+ // {{var}}
6685
+ [/\$\{[^}]+\}/g, "ES template literal"],
6686
+ // ${var}
6687
+ [/<%=?\s*[^%]+%>/g, "ERB/EJS"],
6688
+ // <% %> / <%= %>
6689
+ [/\*\|[A-Z_][A-Z0-9_]*\|\*/g, "Mailchimp merge tag"],
6690
+ // *|TAG|*
6691
+ [/%%[A-Za-z_][A-Za-z0-9_]*%%/g, "Salesforce AMPscript"],
6692
+ // %%tag%%
6693
+ [/\{[A-Za-z_][A-Za-z0-9_.]{2,}\}/g, "Single-brace merge field"]
6694
+ // {merge_field} (3+ char names)
6695
+ ];
6696
+ var EMPTY_SPAM = { score: 100, level: "low", issues: [] };
6697
+ var EMPTY_LINKS = {
6698
+ totalLinks: 0,
6699
+ issues: [],
6700
+ breakdown: { https: 0, http: 0, mailto: 0, tel: 0, anchor: 0, javascript: 0, protocolRelative: 0, other: 0 }
6701
+ };
6702
+ var EMPTY_ACCESSIBILITY = { score: 100, issues: [] };
6703
+ var EMPTY_IMAGES = { total: 0, totalDataUriBytes: 0, issues: [], images: [] };
6704
+ var EMPTY_INBOX_PREVIEW = { subject: null, preheader: null, subjectLength: 0, preheaderLength: 0, truncation: [], issues: [] };
6705
+ var EMPTY_SIZE = { htmlBytes: 0, humanSize: "0 B", clipped: false, issues: [] };
6706
+ var EMPTY_TEMPLATE = { unresolvedCount: 0, issues: [] };
6707
+ var EMPTY_DELIVERABILITY = { domain: "", checks: [], score: 0, issues: [] };
6708
+
6709
+ // src/transform.ts
6710
+ function hasPseudoSelector(selector) {
6711
+ return new RegExp("(?<![[\\w])::?[a-z][\\w-]*(?:\\(|(?=[^(]))", "i").test(selector);
6712
+ }
6713
+ function addImportantToDeclarations(node) {
6714
+ csstree2.walk(node, {
6715
+ visit: "Declaration",
6716
+ enter(decl) {
6717
+ if (decl.type !== "Declaration") return;
6718
+ if (decl.important) return;
6719
+ decl.important = true;
6634
6720
  }
6635
- }
6636
- for (const fn of detectedCssFunctions) {
6637
- checkPropertySupport(fn, addWarning, framework, void 0, propertyLines.get(fn));
6638
- }
6639
- const severityOrder = { error: 0, warning: 1, info: 2 };
6640
- warnings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
6641
- return warnings;
6721
+ });
6642
6722
  }
6643
- function analyzeEmail(html, framework) {
6644
- if (!html || !html.trim()) {
6645
- return [];
6646
- }
6647
- if (html.length > MAX_HTML_SIZE) {
6648
- throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
6723
+ function inlineStyles($) {
6724
+ const styleBlocks = [];
6725
+ $("style").each((_, el) => {
6726
+ styleBlocks.push($(el).text());
6727
+ });
6728
+ if (styleBlocks.length === 0) return "";
6729
+ const preserved = [];
6730
+ for (const block of styleBlocks) {
6731
+ let ast;
6732
+ try {
6733
+ ast = csstree2.parse(block, { parseCustomProperty: true });
6734
+ } catch (e) {
6735
+ continue;
6736
+ }
6737
+ csstree2.walk(ast, {
6738
+ visit: "Atrule",
6739
+ enter(node) {
6740
+ if (node.type !== "Atrule") return;
6741
+ const name = node.name.toLowerCase();
6742
+ if (name === "media" || name === "supports" || name === "keyframes" || name === "-webkit-keyframes" || name === "font-face") {
6743
+ if (name === "media" || name === "supports") {
6744
+ addImportantToDeclarations(node);
6745
+ }
6746
+ preserved.push(csstree2.generate(node));
6747
+ return this.skip;
6748
+ }
6749
+ }
6750
+ });
6751
+ csstree2.walk(ast, {
6752
+ visit: "Rule",
6753
+ enter(node) {
6754
+ if (node.type !== "Rule" || node.prelude.type !== "SelectorList") return;
6755
+ const declarations = csstree2.generate(node.block);
6756
+ const declText = declarations.slice(1, -1).trim();
6757
+ if (!declText) return;
6758
+ const selectorText = csstree2.generate(node.prelude);
6759
+ if (hasPseudoSelector(selectorText)) {
6760
+ preserved.push(csstree2.generate(node));
6761
+ return;
6762
+ }
6763
+ try {
6764
+ $(selectorText).each((_, el) => {
6765
+ const existing = $(el).attr("style") || "";
6766
+ $(el).attr("style", existing ? `${existing}; ${declText}` : declText);
6767
+ });
6768
+ } catch (e) {
6769
+ }
6770
+ }
6771
+ });
6649
6772
  }
6650
- const $ = cheerio2.load(html);
6651
- return analyzeEmailFromDom($, framework);
6773
+ return preserved.join("\n");
6652
6774
  }
6653
- function getFixType(prop) {
6654
- return STRUCTURAL_FIX_PROPERTIES.has(prop) ? "structural" : "css";
6775
+ function makeWarning(base, prop, clientId, framework) {
6776
+ const sug = getSuggestion(prop, clientId, framework);
6777
+ const fix = getCodeFix(prop, clientId, framework);
6778
+ const isFallback = framework && ((sug == null ? void 0 : sug.isGenericFallback) || fix && isCodeFixGenericFallback(prop, clientId, framework));
6779
+ return __spreadProps(__spreadValues(__spreadValues(__spreadValues(__spreadValues({}, base), sug ? { suggestion: sug.text } : {}), fix ? { fix } : {}), isFallback ? { fixIsGenericFallback: true } : {}), {
6780
+ fixType: STRUCTURAL_FIX_PROPERTIES.has(prop) ? "structural" : "css"
6781
+ });
6655
6782
  }
6656
- function checkPropertySupport(prop, addWarning, framework, selector, line) {
6657
- const supportData = CSS_SUPPORT[prop];
6658
- if (!supportData) return;
6659
- const fixType = getFixType(prop);
6660
- for (const client of EMAIL_CLIENTS) {
6661
- const support = supportData[client.id] || "unknown";
6662
- if (support === "unsupported") {
6663
- const sug = getSuggestion(prop, client.id, framework);
6664
- const fix = getCodeFix(prop, client.id, framework);
6665
- addWarning(__spreadValues(__spreadValues(__spreadValues({
6666
- severity: "warning",
6667
- client: client.id,
6668
- property: prop,
6669
- message: `${client.name} does not support "${prop}".`,
6670
- suggestion: sug.text,
6671
- fix,
6672
- fixType
6673
- }, selector ? { selector } : {}), line !== void 0 ? { line } : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
6674
- } else if (support === "partial") {
6675
- const sug = getSuggestion(prop, client.id, framework);
6676
- const fix = getCodeFix(prop, client.id, framework);
6677
- addWarning(__spreadValues(__spreadValues(__spreadValues({
6678
- severity: "info",
6679
- client: client.id,
6680
- property: prop,
6681
- message: `${client.name} has partial support for "${prop}".`,
6682
- suggestion: sug.text,
6683
- fix,
6684
- fixType
6685
- }, selector ? { selector } : {}), line !== void 0 ? { line } : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
6686
- }
6783
+ function detectAnimations($) {
6784
+ let found = false;
6785
+ $("[style]").each((_, el) => {
6786
+ const style = $(el).attr("style") || "";
6787
+ const props = parseInlineStyle(style);
6788
+ props.forEach((_2, prop) => {
6789
+ if (prop === "animation" || prop === "transition" || prop.startsWith("animation-") || prop.startsWith("transition-")) {
6790
+ found = true;
6791
+ }
6792
+ });
6793
+ });
6794
+ if (!found) {
6795
+ $("style").each((_, el) => {
6796
+ try {
6797
+ const ast = csstree2.parse($(el).text());
6798
+ csstree2.walk(ast, {
6799
+ enter(node) {
6800
+ if (node.type === "Declaration") {
6801
+ const prop = node.property.toLowerCase();
6802
+ if (prop === "animation" || prop === "transition" || prop.startsWith("animation-") || prop.startsWith("transition-")) {
6803
+ found = true;
6804
+ }
6805
+ }
6806
+ }
6807
+ });
6808
+ } catch (e) {
6809
+ }
6810
+ });
6687
6811
  }
6812
+ return found;
6688
6813
  }
6689
- function generateCompatibilityScore(warnings) {
6690
- const result = {};
6691
- for (const client of EMAIL_CLIENTS) {
6692
- const clientWarnings = warnings.filter((w) => w.client === client.id);
6693
- const errorProps = new Set(clientWarnings.filter((w) => w.severity === "error").map((w) => w.property));
6694
- const warnProps = new Set(clientWarnings.filter((w) => w.severity === "warning").map((w) => w.property));
6695
- const infoProps = new Set(clientWarnings.filter((w) => w.severity === "info").map((w) => w.property));
6696
- const errors = errorProps.size;
6697
- const warns = warnProps.size;
6698
- const info = infoProps.size;
6699
- const score = Math.max(0, Math.min(100, 100 - errors * 10 - warns * 3));
6700
- result[client.id] = { score, errors, warnings: warns, info };
6814
+ function gmailAdditionalChecks($, clientId, _html, framework) {
6815
+ const warnings = [];
6816
+ let hasAtFontFace = false;
6817
+ $("style").each((_, el) => {
6818
+ try {
6819
+ const ast = csstree2.parse($(el).text());
6820
+ csstree2.walk(ast, {
6821
+ enter(node) {
6822
+ if (node.type === "Atrule" && node.name === "font-face") {
6823
+ hasAtFontFace = true;
6824
+ }
6825
+ }
6826
+ });
6827
+ } catch (e) {
6828
+ }
6829
+ });
6830
+ if (hasAtFontFace) {
6831
+ warnings.push(makeWarning({
6832
+ severity: "warning",
6833
+ client: clientId,
6834
+ property: "@font-face",
6835
+ message: "Gmail does not support custom web fonts."
6836
+ }, "@font-face", clientId, framework));
6701
6837
  }
6702
- return result;
6838
+ return warnings;
6703
6839
  }
6704
- function warningsForClient(warnings, clientId) {
6705
- return warnings.filter((w) => w.client === clientId);
6840
+ function gmailPostChecks($, clientId, _html, framework) {
6841
+ const warnings = [];
6842
+ $("*").contents().filter(function() {
6843
+ return this.type === "comment";
6844
+ }).each(function() {
6845
+ const commentText = this.data || "";
6846
+ if (commentText.includes("<style") || commentText.includes("[if mso]") || commentText.includes("[if gte mso")) {
6847
+ $(this).remove();
6848
+ }
6849
+ });
6850
+ const styleSug = getSuggestion("<style>:partial", clientId, framework);
6851
+ warnings.push({
6852
+ severity: "info",
6853
+ client: clientId,
6854
+ property: "<style>",
6855
+ message: "Gmail partially supports <style> blocks (head only, 16KB limit). Inlining recommended for safety.",
6856
+ suggestion: styleSug.text
6857
+ });
6858
+ return warnings;
6706
6859
  }
6707
- function errorWarnings(warnings) {
6708
- return warnings.filter((w) => w.severity === "error");
6860
+ function outlookWindowsAdditionalChecks($, clientId, _html, framework) {
6861
+ const warnings = [];
6862
+ if ($("[style*='border-radius']").length > 0) {
6863
+ warnings.push(makeWarning({
6864
+ severity: "warning",
6865
+ client: clientId,
6866
+ property: "border-radius",
6867
+ message: "Outlook Classic ignores border-radius. Buttons and containers will have sharp corners."
6868
+ }, "border-radius", clientId, framework));
6869
+ }
6870
+ if ($("[style*='max-width']").length > 0) {
6871
+ warnings.push(makeWarning({
6872
+ severity: "warning",
6873
+ client: clientId,
6874
+ property: "max-width",
6875
+ message: "Outlook Classic ignores max-width."
6876
+ }, "max-width", clientId, framework));
6877
+ }
6878
+ const hasDivLayout = $("div[style*='display']").length > 0 || $("div[style*='flex']").length > 0 || $("div[style*='grid']").length > 0;
6879
+ if (hasDivLayout) {
6880
+ warnings.push(makeWarning({
6881
+ severity: "error",
6882
+ client: clientId,
6883
+ property: "display:flex",
6884
+ message: "Outlook Classic uses Microsoft Word for rendering. Flexbox and Grid layouts will break."
6885
+ }, "display:flex", clientId, framework));
6886
+ }
6887
+ if ($("[style*='background-image']").length > 0 || $("[style*='background:']").filter(
6888
+ (_, el) => ($(el).attr("style") || "").includes("url(")
6889
+ ).length > 0) {
6890
+ warnings.push(makeWarning({
6891
+ severity: "warning",
6892
+ client: clientId,
6893
+ property: "background-image",
6894
+ message: "Outlook Classic requires VML for background images."
6895
+ }, "background-image", clientId, framework));
6896
+ }
6897
+ return warnings;
6709
6898
  }
6710
- function structuralWarnings(warnings) {
6711
- return warnings.filter((w) => w.fixType === "structural");
6899
+ function appleMailAdditionalChecks($, clientId) {
6900
+ const warnings = [];
6901
+ const imgsWithTransparentBg = $("img").filter((_, el) => {
6902
+ const src = $(el).attr("src") || "";
6903
+ return src.endsWith(".png") || src.endsWith(".svg");
6904
+ });
6905
+ if (imgsWithTransparentBg.length > 0) {
6906
+ warnings.push({
6907
+ severity: "info",
6908
+ client: clientId,
6909
+ property: "dark-mode",
6910
+ message: "PNG/SVG images with transparent backgrounds may become invisible in Apple Mail dark mode.",
6911
+ suggestion: "Add a white background or padding around images, or use dark-mode-friendly image variants."
6912
+ });
6913
+ }
6914
+ return warnings;
6712
6915
  }
6713
-
6714
- // src/dark-mode.ts
6715
- var cheerio3 = __toESM(require("cheerio"), 1);
6716
- var csstree3 = __toESM(require("css-tree"), 1);
6717
-
6718
- // src/color-utils.ts
6719
- var NAMED_COLORS = {
6720
- aliceblue: [240, 248, 255],
6721
- antiquewhite: [250, 235, 215],
6722
- aqua: [0, 255, 255],
6723
- aquamarine: [127, 255, 212],
6724
- azure: [240, 255, 255],
6725
- beige: [245, 245, 220],
6726
- bisque: [255, 228, 196],
6727
- black: [0, 0, 0],
6728
- blanchedalmond: [255, 235, 205],
6729
- blue: [0, 0, 255],
6730
- blueviolet: [138, 43, 226],
6731
- brown: [165, 42, 42],
6732
- burlywood: [222, 184, 135],
6733
- cadetblue: [95, 158, 160],
6734
- chartreuse: [127, 255, 0],
6735
- chocolate: [210, 105, 30],
6736
- coral: [255, 127, 80],
6737
- cornflowerblue: [100, 149, 237],
6738
- cornsilk: [255, 248, 220],
6739
- crimson: [220, 20, 60],
6740
- cyan: [0, 255, 255],
6741
- darkblue: [0, 0, 139],
6742
- darkcyan: [0, 139, 139],
6743
- darkgoldenrod: [184, 134, 11],
6744
- darkgray: [169, 169, 169],
6745
- darkgreen: [0, 100, 0],
6746
- darkgrey: [169, 169, 169],
6747
- darkkhaki: [189, 183, 107],
6748
- darkmagenta: [139, 0, 139],
6749
- darkolivegreen: [85, 107, 47],
6750
- darkorange: [255, 140, 0],
6751
- darkorchid: [153, 50, 204],
6752
- darkred: [139, 0, 0],
6753
- darksalmon: [233, 150, 122],
6754
- darkseagreen: [143, 188, 143],
6755
- darkslateblue: [72, 61, 139],
6756
- darkslategray: [47, 79, 79],
6757
- darkslategrey: [47, 79, 79],
6758
- darkturquoise: [0, 206, 209],
6759
- darkviolet: [148, 0, 211],
6760
- deeppink: [255, 20, 147],
6761
- deepskyblue: [0, 191, 255],
6762
- dimgray: [105, 105, 105],
6763
- dimgrey: [105, 105, 105],
6764
- dodgerblue: [30, 144, 255],
6765
- firebrick: [178, 34, 34],
6766
- floralwhite: [255, 250, 240],
6767
- forestgreen: [34, 139, 34],
6768
- fuchsia: [255, 0, 255],
6769
- gainsboro: [220, 220, 220],
6770
- ghostwhite: [248, 248, 255],
6771
- gold: [255, 215, 0],
6772
- goldenrod: [218, 165, 32],
6773
- gray: [128, 128, 128],
6774
- green: [0, 128, 0],
6775
- greenyellow: [173, 255, 47],
6776
- grey: [128, 128, 128],
6777
- honeydew: [240, 255, 240],
6778
- hotpink: [255, 105, 180],
6779
- indianred: [205, 92, 92],
6780
- indigo: [75, 0, 130],
6781
- ivory: [255, 255, 240],
6782
- khaki: [240, 230, 140],
6783
- lavender: [230, 230, 250],
6784
- lavenderblush: [255, 240, 245],
6785
- lawngreen: [124, 252, 0],
6786
- lemonchiffon: [255, 250, 205],
6787
- lightblue: [173, 216, 230],
6788
- lightcoral: [240, 128, 128],
6789
- lightcyan: [224, 255, 255],
6790
- lightgoldenrodyellow: [250, 250, 210],
6791
- lightgray: [211, 211, 211],
6792
- lightgreen: [144, 238, 144],
6793
- lightgrey: [211, 211, 211],
6794
- lightpink: [255, 182, 193],
6795
- lightsalmon: [255, 160, 122],
6796
- lightseagreen: [32, 178, 170],
6797
- lightskyblue: [135, 206, 250],
6798
- lightslategray: [119, 136, 153],
6799
- lightslategrey: [119, 136, 153],
6800
- lightsteelblue: [176, 196, 222],
6801
- lightyellow: [255, 255, 224],
6802
- lime: [0, 255, 0],
6803
- limegreen: [50, 205, 50],
6804
- linen: [250, 240, 230],
6805
- magenta: [255, 0, 255],
6806
- maroon: [128, 0, 0],
6807
- mediumaquamarine: [102, 205, 170],
6808
- mediumblue: [0, 0, 205],
6809
- mediumorchid: [186, 85, 211],
6810
- mediumpurple: [147, 111, 219],
6811
- mediumseagreen: [60, 179, 113],
6812
- mediumslateblue: [123, 104, 238],
6813
- mediumspringgreen: [0, 250, 154],
6814
- mediumturquoise: [72, 209, 204],
6815
- mediumvioletred: [199, 21, 133],
6816
- midnightblue: [25, 25, 112],
6817
- mintcream: [245, 255, 250],
6818
- mistyrose: [255, 228, 225],
6819
- moccasin: [255, 228, 181],
6820
- navajowhite: [255, 222, 173],
6821
- navy: [0, 0, 128],
6822
- oldlace: [253, 245, 230],
6823
- olive: [128, 128, 0],
6824
- olivedrab: [107, 142, 35],
6825
- orange: [255, 165, 0],
6826
- orangered: [255, 69, 0],
6827
- orchid: [218, 112, 214],
6828
- palegoldenrod: [238, 232, 170],
6829
- palegreen: [152, 251, 152],
6830
- paleturquoise: [175, 238, 238],
6831
- palevioletred: [219, 112, 147],
6832
- papayawhip: [255, 239, 213],
6833
- peachpuff: [255, 218, 185],
6834
- peru: [205, 133, 63],
6835
- pink: [255, 192, 203],
6836
- plum: [221, 160, 221],
6837
- powderblue: [176, 224, 230],
6838
- purple: [128, 0, 128],
6839
- rebeccapurple: [102, 51, 153],
6840
- red: [255, 0, 0],
6841
- rosybrown: [188, 143, 143],
6842
- royalblue: [65, 105, 225],
6843
- saddlebrown: [139, 69, 19],
6844
- salmon: [250, 128, 114],
6845
- sandybrown: [244, 164, 96],
6846
- seagreen: [46, 139, 87],
6847
- seashell: [255, 245, 238],
6848
- sienna: [160, 82, 45],
6849
- silver: [192, 192, 192],
6850
- skyblue: [135, 206, 235],
6851
- slateblue: [106, 90, 205],
6852
- slategray: [112, 128, 144],
6853
- slategrey: [112, 128, 144],
6854
- snow: [255, 250, 250],
6855
- springgreen: [0, 255, 127],
6856
- steelblue: [70, 130, 180],
6857
- tan: [210, 180, 140],
6858
- teal: [0, 128, 128],
6859
- thistle: [216, 191, 216],
6860
- tomato: [255, 99, 71],
6861
- turquoise: [64, 224, 208],
6862
- violet: [238, 130, 238],
6863
- wheat: [245, 222, 179],
6864
- white: [255, 255, 255],
6865
- whitesmoke: [245, 245, 245],
6866
- yellow: [255, 255, 0],
6867
- yellowgreen: [154, 205, 50]
6868
- };
6869
- function parseColor(value) {
6870
- if (!value) return null;
6871
- const v = value.trim().toLowerCase();
6872
- if (v === "inherit" || v === "currentcolor" || v === "initial" || v === "unset" || v.startsWith("var(")) {
6873
- return null;
6916
+ function yahooAdditionalChecks($, clientId, _html, framework) {
6917
+ const warnings = [];
6918
+ warnings.push({
6919
+ severity: "info",
6920
+ client: clientId,
6921
+ property: "class",
6922
+ message: "Yahoo Mail rewrites CSS class names with a prefix. Class-based selectors in <style> blocks will still work but the names change."
6923
+ });
6924
+ if ($("[style*='background']").filter(
6925
+ (_, el) => ($(el).attr("style") || "").includes("url(")
6926
+ ).length > 0) {
6927
+ warnings.push(makeWarning({
6928
+ severity: "warning",
6929
+ client: clientId,
6930
+ property: "background-image",
6931
+ message: "Yahoo Mail has inconsistent support for CSS background images."
6932
+ }, "background-image", clientId, framework));
6874
6933
  }
6875
- if (v === "transparent") {
6876
- return { r: 0, g: 0, b: 0, a: 0 };
6934
+ return warnings;
6935
+ }
6936
+ function thunderbirdAdditionalChecks($, clientId) {
6937
+ if (detectAnimations($)) {
6938
+ return [{
6939
+ severity: "info",
6940
+ client: clientId,
6941
+ property: "animation",
6942
+ message: "Thunderbird does not support CSS animations or transitions."
6943
+ }];
6944
+ }
6945
+ return [];
6946
+ }
6947
+ function heyAdditionalChecks(_$, clientId, html) {
6948
+ const warnings = [];
6949
+ if (!html.includes("prefers-color-scheme")) {
6950
+ warnings.push({
6951
+ severity: "info",
6952
+ client: clientId,
6953
+ property: "dark-mode",
6954
+ message: "HEY Mail supports @media (prefers-color-scheme: dark). Consider adding dark mode styles.",
6955
+ suggestion: "Add a @media (prefers-color-scheme: dark) block to optimize for HEY's audience."
6956
+ });
6957
+ }
6958
+ return warnings;
6959
+ }
6960
+ function superhumanAdditionalChecks($, clientId, html) {
6961
+ const warnings = [];
6962
+ if (detectAnimations($)) {
6963
+ warnings.push({
6964
+ severity: "info",
6965
+ client: clientId,
6966
+ property: "animation",
6967
+ message: "Superhuman may honor OS-level 'reduce motion' preferences, disabling animations.",
6968
+ suggestion: "Use @media (prefers-reduced-motion: reduce) to provide static fallbacks."
6969
+ });
6970
+ }
6971
+ warnings.push({
6972
+ severity: "info",
6973
+ client: clientId,
6974
+ property: "<style>",
6975
+ message: "Superhuman uses Chromium rendering with excellent CSS support. Flexbox, Grid, CSS variables, and modern properties all work."
6976
+ });
6977
+ return warnings;
6978
+ }
6979
+ var EMPTY_SET = /* @__PURE__ */ new Set();
6980
+ var CLIENT_CONFIGS = {
6981
+ "gmail-web": {
6982
+ id: "gmail-web",
6983
+ strippedProperties: GMAIL_STRIPPED_PROPERTIES,
6984
+ stripMode: "strip",
6985
+ valueStrips: [
6986
+ { prop: "display", pattern: /grid/ },
6987
+ { prop: "background", pattern: /linear-gradient|radial-gradient/ }
6988
+ ],
6989
+ inlineAndStripStyles: true,
6990
+ stripExternalStylesheets: true,
6991
+ stripForms: true,
6992
+ stripSvg: true,
6993
+ additionalChecks: gmailAdditionalChecks
6994
+ },
6995
+ "gmail-android": {
6996
+ id: "gmail-android",
6997
+ strippedProperties: GMAIL_STRIPPED_PROPERTIES,
6998
+ stripMode: "strip",
6999
+ valueStrips: [
7000
+ { prop: "display", pattern: /grid/ },
7001
+ { prop: "background", pattern: /linear-gradient|radial-gradient/ }
7002
+ ],
7003
+ inlineAndStripStyles: true,
7004
+ stripExternalStylesheets: true,
7005
+ stripForms: true,
7006
+ stripSvg: true,
7007
+ additionalChecks: gmailAdditionalChecks
7008
+ },
7009
+ "gmail-ios": {
7010
+ id: "gmail-ios",
7011
+ strippedProperties: GMAIL_STRIPPED_PROPERTIES,
7012
+ stripMode: "strip",
7013
+ valueStrips: [
7014
+ { prop: "display", pattern: /grid/ },
7015
+ { prop: "background", pattern: /linear-gradient|radial-gradient/ }
7016
+ ],
7017
+ inlineAndStripStyles: true,
7018
+ stripExternalStylesheets: true,
7019
+ stripForms: true,
7020
+ stripSvg: true,
7021
+ additionalChecks: gmailAdditionalChecks
7022
+ },
7023
+ "outlook-windows": {
7024
+ id: "outlook-windows",
7025
+ strippedProperties: /* @__PURE__ */ new Set(["position", "transform", "animation", "transition"]),
7026
+ stripMode: "strip",
7027
+ inlineAndStripStyles: false,
7028
+ stripExternalStylesheets: false,
7029
+ stripForms: false,
7030
+ stripSvg: false
7031
+ },
7032
+ "outlook-windows-legacy": {
7033
+ id: "outlook-windows-legacy",
7034
+ strippedProperties: OUTLOOK_WORD_UNSUPPORTED,
7035
+ stripMode: "strip",
7036
+ valueStrips: [
7037
+ { prop: "background", pattern: /linear-gradient|radial-gradient/ },
7038
+ { prop: "background-image", pattern: /linear-gradient|radial-gradient/ }
7039
+ ],
7040
+ inlineAndStripStyles: false,
7041
+ stripExternalStylesheets: false,
7042
+ stripForms: false,
7043
+ stripSvg: false,
7044
+ additionalChecks: outlookWindowsAdditionalChecks
7045
+ },
7046
+ "outlook-web": {
7047
+ id: "outlook-web",
7048
+ strippedProperties: /* @__PURE__ */ new Set(["position", "transform", "animation", "transition"]),
7049
+ stripMode: "strip",
7050
+ inlineAndStripStyles: false,
7051
+ stripExternalStylesheets: false,
7052
+ stripForms: false,
7053
+ stripSvg: false
7054
+ },
7055
+ "apple-mail-macos": {
7056
+ id: "apple-mail-macos",
7057
+ strippedProperties: EMPTY_SET,
7058
+ stripMode: "strip",
7059
+ inlineAndStripStyles: false,
7060
+ stripExternalStylesheets: false,
7061
+ stripForms: false,
7062
+ stripSvg: false,
7063
+ additionalChecks: appleMailAdditionalChecks
7064
+ },
7065
+ "apple-mail-ios": {
7066
+ id: "apple-mail-ios",
7067
+ strippedProperties: EMPTY_SET,
7068
+ stripMode: "strip",
7069
+ inlineAndStripStyles: false,
7070
+ stripExternalStylesheets: false,
7071
+ stripForms: false,
7072
+ stripSvg: false,
7073
+ additionalChecks: appleMailAdditionalChecks
7074
+ },
7075
+ "yahoo-mail": {
7076
+ id: "yahoo-mail",
7077
+ strippedProperties: /* @__PURE__ */ new Set(["position", "box-shadow", "transform", "animation", "transition", "opacity"]),
7078
+ stripMode: "strip",
7079
+ inlineAndStripStyles: false,
7080
+ stripExternalStylesheets: false,
7081
+ stripForms: false,
7082
+ stripSvg: false,
7083
+ additionalChecks: yahooAdditionalChecks
7084
+ },
7085
+ "samsung-mail": {
7086
+ id: "samsung-mail",
7087
+ strippedProperties: /* @__PURE__ */ new Set(["box-shadow", "transform", "animation", "transition", "opacity"]),
7088
+ stripMode: "info",
7089
+ inlineAndStripStyles: false,
7090
+ stripExternalStylesheets: false,
7091
+ stripForms: false,
7092
+ stripSvg: false
7093
+ },
7094
+ "thunderbird": {
7095
+ id: "thunderbird",
7096
+ strippedProperties: EMPTY_SET,
7097
+ stripMode: "strip",
7098
+ inlineAndStripStyles: false,
7099
+ stripExternalStylesheets: false,
7100
+ stripForms: false,
7101
+ stripSvg: false,
7102
+ additionalChecks: thunderbirdAdditionalChecks
7103
+ },
7104
+ "hey-mail": {
7105
+ id: "hey-mail",
7106
+ strippedProperties: /* @__PURE__ */ new Set(["transform", "animation", "transition"]),
7107
+ stripMode: "strip",
7108
+ valueStrips: [
7109
+ { prop: "position", pattern: /fixed|sticky/ }
7110
+ ],
7111
+ inlineAndStripStyles: false,
7112
+ stripExternalStylesheets: true,
7113
+ stripForms: true,
7114
+ stripSvg: false,
7115
+ additionalChecks: heyAdditionalChecks
7116
+ },
7117
+ "superhuman": {
7118
+ id: "superhuman",
7119
+ strippedProperties: EMPTY_SET,
7120
+ stripMode: "strip",
7121
+ inlineAndStripStyles: false,
7122
+ stripExternalStylesheets: true,
7123
+ stripForms: true,
7124
+ stripSvg: false,
7125
+ additionalChecks: superhumanAdditionalChecks
7126
+ }
7127
+ };
7128
+ function applyTransform(html, config, framework) {
7129
+ const $ = cheerio2.load(html);
7130
+ const warnings = [];
7131
+ const clientId = config.id;
7132
+ if (config.additionalChecks && config.inlineAndStripStyles) {
7133
+ warnings.push(...config.additionalChecks($, clientId, html, framework));
7134
+ }
7135
+ if (config.inlineAndStripStyles) {
7136
+ const preserved = inlineStyles($);
7137
+ $("style").remove();
7138
+ if (preserved.trim()) {
7139
+ const head = $("head");
7140
+ if (head.length > 0) {
7141
+ head.append(`<style>${preserved}</style>`);
7142
+ } else {
7143
+ $("body").prepend(`<style>${preserved}</style>`);
7144
+ }
7145
+ }
7146
+ }
7147
+ if (config.stripExternalStylesheets) {
7148
+ if ($("link[rel='stylesheet']").length > 0) {
7149
+ warnings.push(makeWarning({
7150
+ severity: "error",
7151
+ client: clientId,
7152
+ property: "<link>",
7153
+ message: `${clientId} does not load external stylesheets.`
7154
+ }, "<link>", clientId, framework));
7155
+ $("link[rel='stylesheet']").remove();
7156
+ }
7157
+ }
7158
+ if (config.stripForms && $("form").length > 0) {
7159
+ warnings.push(makeWarning({
7160
+ severity: "error",
7161
+ client: clientId,
7162
+ property: "<form>",
7163
+ message: `${clientId} removes form elements.`
7164
+ }, "<form>", clientId, framework));
7165
+ $("form").each((_, el) => {
7166
+ $(el).replaceWith($(el).html() || "");
7167
+ });
7168
+ }
7169
+ if (config.stripSvg && $("svg").length > 0) {
7170
+ warnings.push(makeWarning({
7171
+ severity: "error",
7172
+ client: clientId,
7173
+ property: "<svg>",
7174
+ message: `${clientId} does not support inline SVG elements.`
7175
+ }, "<svg>", clientId, framework));
7176
+ $("svg").each((_, el) => {
7177
+ $(el).replaceWith('<img alt="[SVG not supported]" />');
7178
+ });
7179
+ }
7180
+ if (config.strippedProperties.size > 0 || config.valueStrips && config.valueStrips.length > 0) {
7181
+ $("[style]").each((_, el) => {
7182
+ const style = $(el).attr("style") || "";
7183
+ const props = parseInlineStyle(style);
7184
+ const removed = [];
7185
+ props.forEach((value, prop) => {
7186
+ var _a;
7187
+ if (config.strippedProperties.has(prop)) {
7188
+ if (config.stripMode === "strip") {
7189
+ removed.push(prop);
7190
+ props.delete(prop);
7191
+ } else {
7192
+ warnings.push(makeWarning({
7193
+ severity: "info",
7194
+ client: clientId,
7195
+ property: prop,
7196
+ message: `${clientId} has limited support for "${prop}".`
7197
+ }, prop, clientId, framework));
7198
+ }
7199
+ return;
7200
+ }
7201
+ for (const vs of (_a = config.valueStrips) != null ? _a : []) {
7202
+ if (prop === vs.prop && vs.pattern.test(value)) {
7203
+ removed.push(prop);
7204
+ props.delete(prop);
7205
+ return;
7206
+ }
7207
+ }
7208
+ });
7209
+ if (removed.length > 0) {
7210
+ $(el).attr("style", serializeStyle(props));
7211
+ for (const prop of removed) {
7212
+ warnings.push(makeWarning({
7213
+ severity: "warning",
7214
+ client: clientId,
7215
+ property: prop,
7216
+ message: `${clientId} strips "${prop}" from styles.`
7217
+ }, prop, clientId, framework));
7218
+ }
7219
+ }
7220
+ });
7221
+ }
7222
+ if (config.inlineAndStripStyles) {
7223
+ warnings.push(...gmailPostChecks($, clientId, html, framework));
7224
+ }
7225
+ if (config.additionalChecks && !config.inlineAndStripStyles) {
7226
+ warnings.push(...config.additionalChecks($, clientId, html, framework));
7227
+ }
7228
+ return { clientId, html: $.html(), warnings };
7229
+ }
7230
+ function transformForClient(html, clientId, framework) {
7231
+ if (!html || !html.trim()) {
7232
+ return { clientId, html: html || "", warnings: [] };
7233
+ }
7234
+ if (html.length > MAX_HTML_SIZE) {
7235
+ throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
7236
+ }
7237
+ const config = CLIENT_CONFIGS[clientId];
7238
+ if (!config) {
7239
+ return {
7240
+ clientId,
7241
+ html,
7242
+ warnings: [
7243
+ {
7244
+ severity: "info",
7245
+ client: clientId,
7246
+ property: "unknown",
7247
+ message: `No transformation rules available for client "${clientId}".`
7248
+ }
7249
+ ]
7250
+ };
7251
+ }
7252
+ const downleveled = downlevelCSS(html);
7253
+ return applyTransform(downleveled, config, framework);
7254
+ }
7255
+ function transformForAllClients(html, framework) {
7256
+ if (!html || !html.trim()) {
7257
+ return Object.keys(CLIENT_CONFIGS).map((clientId) => ({
7258
+ clientId,
7259
+ html: html || "",
7260
+ warnings: []
7261
+ }));
7262
+ }
7263
+ if (html.length > MAX_HTML_SIZE) {
7264
+ throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
7265
+ }
7266
+ const downleveled = downlevelCSS(html);
7267
+ return Object.keys(CLIENT_CONFIGS).map(
7268
+ (clientId) => applyTransform(downleveled, CLIENT_CONFIGS[clientId], framework)
7269
+ );
7270
+ }
7271
+
7272
+ // src/analyze.ts
7273
+ var cheerio3 = __toESM(require("cheerio"), 1);
7274
+ var csstree3 = __toESM(require("css-tree"), 1);
7275
+ var HTML_ELEMENT_SELECTORS = {
7276
+ "<style>": "style",
7277
+ "<link>": "link[rel='stylesheet']",
7278
+ "<svg>": "svg",
7279
+ "<video>": "video",
7280
+ "<form>": "form, input, button[type='submit']",
7281
+ "<audio>": "audio",
7282
+ "<picture>": "picture",
7283
+ "<dialog>": "dialog",
7284
+ "<meter>": "meter",
7285
+ "<progress>": "progress",
7286
+ "<select>": "select",
7287
+ "<textarea>": "textarea",
7288
+ "<marquee>": "marquee",
7289
+ "<object>": "object",
7290
+ "<base>": "base"
7291
+ };
7292
+ var HTML_ELEMENT_SEVERITY = {
7293
+ "<style>": "error",
7294
+ "<link>": "error",
7295
+ "<svg>": "error",
7296
+ "<form>": "error",
7297
+ "<video>": "warning",
7298
+ "<audio>": "warning",
7299
+ "<picture>": "warning",
7300
+ "<dialog>": "warning",
7301
+ "<marquee>": "warning",
7302
+ "<meter>": "warning",
7303
+ "<progress>": "warning",
7304
+ "<select>": "warning",
7305
+ "<textarea>": "warning",
7306
+ "<object>": "warning",
7307
+ "<base>": "warning"
7308
+ };
7309
+ var HTML_ELEMENT_MESSAGES = {
7310
+ "<style>": (n) => `${n} strips <style> blocks. Styles must be inlined.`,
7311
+ "<link>": (n) => `${n} does not support external stylesheets.`,
7312
+ "<svg>": (n) => `${n} does not support inline SVG.`,
7313
+ "<video>": (n) => `${n} does not support <video> elements.`,
7314
+ "<form>": (n) => `${n} strips form elements.`
7315
+ };
7316
+ var COMPOUND_DETECTORS = [
7317
+ { key: "display:flex", property: "display", valueIncludes: "flex" },
7318
+ { key: "display:grid", property: "display", valueIncludes: "grid" },
7319
+ { key: "display:none", property: "display", valueIncludes: "none" }
7320
+ ];
7321
+ var CSS_FUNCTION_DETECTORS = CSS_FUNCTION_FEATURES.map((fn) => ({
7322
+ key: fn,
7323
+ pattern: `${fn}(`
7324
+ // require opening paren — matches "min(" but not "Minion"
7325
+ }));
7326
+ function analyzeEmailFromDom($, framework) {
7327
+ const warnings = [];
7328
+ const seenWarnings = /* @__PURE__ */ new Set();
7329
+ function addWarning(w) {
7330
+ const key = `${w.client}:${w.property}:${w.severity}:${w.selector || ""}`;
7331
+ if (!seenWarnings.has(key)) {
7332
+ seenWarnings.add(key);
7333
+ warnings.push(w);
7334
+ }
7335
+ }
7336
+ function describeSelector(el) {
7337
+ var _a;
7338
+ const $el = $(el);
7339
+ const tag = ((_a = el.tagName) == null ? void 0 : _a.toLowerCase()) || "";
7340
+ const cls = $el.attr("class");
7341
+ const id = $el.attr("id");
7342
+ if (id) return `${tag}#${id}`;
7343
+ if (cls) return `${tag}.${cls.split(/\s+/)[0]}`;
7344
+ const href = $el.attr("href");
7345
+ if (href) return `${tag}[href]`;
7346
+ return tag;
7347
+ }
7348
+ for (const feature of HTML_ELEMENT_FEATURES) {
7349
+ const selector = HTML_ELEMENT_SELECTORS[feature];
7350
+ if (!selector) continue;
7351
+ if ($(selector).length === 0) continue;
7352
+ const supportData = CSS_SUPPORT[feature];
7353
+ if (!supportData) continue;
7354
+ const baseSeverity = HTML_ELEMENT_SEVERITY[feature] || "warning";
7355
+ for (const client of EMAIL_CLIENTS) {
7356
+ const support = supportData[client.id];
7357
+ if (support === "unsupported") {
7358
+ const msgFn = HTML_ELEMENT_MESSAGES[feature];
7359
+ const message = msgFn ? msgFn(client.name) : `${client.name} does not support ${feature}.`;
7360
+ const sug = getSuggestion(feature, client.id, framework);
7361
+ const fix = getCodeFix(feature, client.id, framework);
7362
+ addWarning(__spreadValues({
7363
+ severity: baseSeverity,
7364
+ client: client.id,
7365
+ property: feature,
7366
+ message,
7367
+ suggestion: sug.text,
7368
+ fix,
7369
+ fixType: getFixType(feature)
7370
+ }, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(feature, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
7371
+ } else if (support === "partial" && feature === "<style>") {
7372
+ const sug = getSuggestion("<style>:partial", client.id, framework);
7373
+ const fix = getCodeFix("<style>", client.id, framework);
7374
+ addWarning(__spreadValues({
7375
+ severity: "warning",
7376
+ client: client.id,
7377
+ property: "<style>",
7378
+ message: `${client.name} has partial <style> support (head only, with limitations). Inline styles recommended.`,
7379
+ suggestion: sug.text,
7380
+ fix,
7381
+ fixType: getFixType("<style>")
7382
+ }, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback("<style>", client.id, framework)) ? { fixIsGenericFallback: true } : {}));
7383
+ }
7384
+ }
7385
+ }
7386
+ const parsedAtRules = /* @__PURE__ */ new Set();
7387
+ const parsedProperties = /* @__PURE__ */ new Set();
7388
+ const propertyLines = /* @__PURE__ */ new Map();
7389
+ const detectedCssFunctions = /* @__PURE__ */ new Set();
7390
+ const detectedPseudoClasses = /* @__PURE__ */ new Set();
7391
+ const detectedPseudoElements = /* @__PURE__ */ new Set();
7392
+ $("style").each((_, el) => {
7393
+ const cssText = $(el).text();
7394
+ try {
7395
+ const ast = csstree3.parse(cssText, { parseCustomProperty: true, positions: true });
7396
+ csstree3.walk(ast, {
7397
+ enter(node) {
7398
+ if (node.type === "Atrule") {
7399
+ parsedAtRules.add(`@${node.name}`);
7400
+ }
7401
+ if (node.type === "PseudoClassSelector") {
7402
+ detectedPseudoClasses.add(`:${node.name}`);
7403
+ }
7404
+ if (node.type === "PseudoElementSelector") {
7405
+ detectedPseudoElements.add(`::${node.name}`);
7406
+ }
7407
+ if (node.type === "Declaration") {
7408
+ const prop = node.property.toLowerCase();
7409
+ parsedProperties.add(prop);
7410
+ if (node.loc && !propertyLines.has(prop)) {
7411
+ propertyLines.set(prop, node.loc.start.line);
7412
+ }
7413
+ const valueStr = csstree3.generate(node.value);
7414
+ for (const det of COMPOUND_DETECTORS) {
7415
+ if (prop === det.property && valueStr.includes(det.valueIncludes)) {
7416
+ parsedProperties.add(det.key);
7417
+ if (node.loc && !propertyLines.has(det.key)) {
7418
+ propertyLines.set(det.key, node.loc.start.line);
7419
+ }
7420
+ }
7421
+ }
7422
+ for (const fn of CSS_FUNCTION_DETECTORS) {
7423
+ if (valueStr.includes(fn.pattern)) {
7424
+ detectedCssFunctions.add(fn.key);
7425
+ if (node.loc && !propertyLines.has(fn.key)) {
7426
+ propertyLines.set(fn.key, node.loc.start.line);
7427
+ }
7428
+ }
7429
+ }
7430
+ }
7431
+ }
7432
+ });
7433
+ } catch (e) {
7434
+ }
7435
+ });
7436
+ for (const atRule of AT_RULE_FEATURES) {
7437
+ if (!parsedAtRules.has(atRule)) continue;
7438
+ checkPropertySupport(atRule, addWarning, framework);
6877
7439
  }
6878
- const named = NAMED_COLORS[v];
6879
- if (named) {
6880
- return { r: named[0], g: named[1], b: named[2], a: 1 };
7440
+ const cssPropertiesToCheck = Object.keys(CSS_SUPPORT).filter(
7441
+ (k) => !k.startsWith("<") && !k.startsWith("@")
7442
+ );
7443
+ $("[style]").each((_, el) => {
7444
+ const style = $(el).attr("style") || "";
7445
+ const props = parseStyleProperties(style);
7446
+ const selector = describeSelector(el);
7447
+ for (const prop of props) {
7448
+ for (const det of COMPOUND_DETECTORS) {
7449
+ if (prop === det.property) {
7450
+ const value2 = getStyleValue(style, prop);
7451
+ if (value2 == null ? void 0 : value2.includes(det.valueIncludes)) {
7452
+ checkPropertySupport(det.key, addWarning, framework, selector);
7453
+ }
7454
+ }
7455
+ }
7456
+ if (cssPropertiesToCheck.includes(prop)) {
7457
+ checkPropertySupport(prop, addWarning, framework, selector);
7458
+ }
7459
+ const value = getStyleValue(style, prop);
7460
+ if (value) {
7461
+ for (const fn of CSS_FUNCTION_DETECTORS) {
7462
+ if (value.includes(fn.pattern)) {
7463
+ checkPropertySupport(fn.key, addWarning, framework, selector);
7464
+ }
7465
+ }
7466
+ }
7467
+ }
7468
+ });
7469
+ for (const prop of parsedProperties) {
7470
+ if (prop.includes(":")) continue;
7471
+ if (!cssPropertiesToCheck.includes(prop)) continue;
7472
+ checkPropertySupport(prop, addWarning, framework, void 0, propertyLines.get(prop));
6881
7473
  }
6882
- if (v.startsWith("#")) {
6883
- const hex = v.slice(1);
6884
- if (hex.length === 3) {
6885
- return {
6886
- r: parseInt(hex[0] + hex[0], 16),
6887
- g: parseInt(hex[1] + hex[1], 16),
6888
- b: parseInt(hex[2] + hex[2], 16),
6889
- a: 1
6890
- };
7474
+ for (const compound of COMPOUND_VALUE_FEATURES) {
7475
+ if (compound.startsWith(":") || compound.startsWith("::")) continue;
7476
+ if (parsedProperties.has(compound)) {
7477
+ checkPropertySupport(compound, addWarning, framework, void 0, propertyLines.get(compound));
6891
7478
  }
6892
- if (hex.length === 6) {
6893
- return {
6894
- r: parseInt(hex.slice(0, 2), 16),
6895
- g: parseInt(hex.slice(2, 4), 16),
6896
- b: parseInt(hex.slice(4, 6), 16),
6897
- a: 1
6898
- };
7479
+ }
7480
+ for (const pseudo of detectedPseudoClasses) {
7481
+ if (CSS_SUPPORT[pseudo]) {
7482
+ checkPropertySupport(pseudo, addWarning, framework);
6899
7483
  }
6900
- if (hex.length === 8) {
6901
- return {
6902
- r: parseInt(hex.slice(0, 2), 16),
6903
- g: parseInt(hex.slice(2, 4), 16),
6904
- b: parseInt(hex.slice(4, 6), 16),
6905
- a: parseInt(hex.slice(6, 8), 16) / 255
6906
- };
7484
+ }
7485
+ for (const pseudo of detectedPseudoElements) {
7486
+ if (CSS_SUPPORT[pseudo]) {
7487
+ checkPropertySupport(pseudo, addWarning, framework);
6907
7488
  }
6908
- return null;
6909
7489
  }
6910
- const rgbMatch = v.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)$/);
6911
- if (rgbMatch) {
6912
- return {
6913
- r: Math.min(255, parseInt(rgbMatch[1], 10)),
6914
- g: Math.min(255, parseInt(rgbMatch[2], 10)),
6915
- b: Math.min(255, parseInt(rgbMatch[3], 10)),
6916
- a: rgbMatch[4] !== void 0 ? Math.min(1, parseFloat(rgbMatch[4])) : 1
6917
- };
7490
+ for (const fn of detectedCssFunctions) {
7491
+ checkPropertySupport(fn, addWarning, framework, void 0, propertyLines.get(fn));
6918
7492
  }
6919
- return null;
7493
+ const severityOrder = { error: 0, warning: 1, info: 2 };
7494
+ warnings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
7495
+ return warnings;
6920
7496
  }
6921
- function relativeLuminance(r, g, b) {
6922
- const [rs, gs, bs] = [r, g, b].map((c) => {
6923
- const s = c / 255;
6924
- return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
6925
- });
6926
- return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
7497
+ function analyzeEmail(html, framework) {
7498
+ if (!html || !html.trim()) {
7499
+ return [];
7500
+ }
7501
+ if (html.length > MAX_HTML_SIZE) {
7502
+ throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
7503
+ }
7504
+ const $ = cheerio3.load(html);
7505
+ return analyzeEmailFromDom($, framework);
6927
7506
  }
6928
- function contrastRatio(l1, l2) {
6929
- const lighter = Math.max(l1, l2);
6930
- const darker = Math.min(l1, l2);
6931
- return (lighter + 0.05) / (darker + 0.05);
7507
+ function getFixType(prop) {
7508
+ return STRUCTURAL_FIX_PROPERTIES.has(prop) ? "structural" : "css";
6932
7509
  }
6933
- function wcagGrade(ratio) {
6934
- if (ratio >= 7) return "AAA";
6935
- if (ratio >= 4.5) return "AA";
6936
- if (ratio >= 3) return "AA Large";
6937
- return "Fail";
7510
+ function checkPropertySupport(prop, addWarning, framework, selector, line) {
7511
+ const supportData = CSS_SUPPORT[prop];
7512
+ if (!supportData) return;
7513
+ const fixType = getFixType(prop);
7514
+ for (const client of EMAIL_CLIENTS) {
7515
+ const support = supportData[client.id] || "unknown";
7516
+ if (support === "unsupported") {
7517
+ const sug = getSuggestion(prop, client.id, framework);
7518
+ const fix = getCodeFix(prop, client.id, framework);
7519
+ addWarning(__spreadValues(__spreadValues(__spreadValues({
7520
+ severity: "warning",
7521
+ client: client.id,
7522
+ property: prop,
7523
+ message: `${client.name} does not support "${prop}".`,
7524
+ suggestion: sug.text,
7525
+ fix,
7526
+ fixType
7527
+ }, selector ? { selector } : {}), line !== void 0 ? { line } : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
7528
+ } else if (support === "partial") {
7529
+ const sug = getSuggestion(prop, client.id, framework);
7530
+ const fix = getCodeFix(prop, client.id, framework);
7531
+ addWarning(__spreadValues(__spreadValues(__spreadValues({
7532
+ severity: "info",
7533
+ client: client.id,
7534
+ property: prop,
7535
+ message: `${client.name} has partial support for "${prop}".`,
7536
+ suggestion: sug.text,
7537
+ fix,
7538
+ fixType
7539
+ }, selector ? { selector } : {}), line !== void 0 ? { line } : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
7540
+ }
7541
+ }
6938
7542
  }
6939
- function alphaBlend(fg, bgR, bgG, bgB) {
6940
- const a = fg.a;
6941
- return [
6942
- Math.round(fg.r * a + bgR * (1 - a)),
6943
- Math.round(fg.g * a + bgG * (1 - a)),
6944
- Math.round(fg.b * a + bgB * (1 - a))
6945
- ];
7543
+ function generateCompatibilityScore(warnings) {
7544
+ const result = {};
7545
+ for (const client of EMAIL_CLIENTS) {
7546
+ const clientWarnings = warnings.filter((w) => w.client === client.id);
7547
+ const errorProps = new Set(clientWarnings.filter((w) => w.severity === "error").map((w) => w.property));
7548
+ const warnProps = new Set(clientWarnings.filter((w) => w.severity === "warning").map((w) => w.property));
7549
+ const infoProps = new Set(clientWarnings.filter((w) => w.severity === "info").map((w) => w.property));
7550
+ const errors = errorProps.size;
7551
+ const warns = warnProps.size;
7552
+ const info = infoProps.size;
7553
+ const score = Math.max(0, Math.min(100, 100 - errors * 10 - warns * 3));
7554
+ result[client.id] = { score, errors, warnings: warns, info };
7555
+ }
7556
+ return result;
7557
+ }
7558
+ function warningsForClient(warnings, clientId) {
7559
+ return warnings.filter((w) => w.client === clientId);
7560
+ }
7561
+ function errorWarnings(warnings) {
7562
+ return warnings.filter((w) => w.severity === "error");
7563
+ }
7564
+ function structuralWarnings(warnings) {
7565
+ return warnings.filter((w) => w.fixType === "structural");
6946
7566
  }
6947
7567
 
6948
7568
  // src/dark-mode.ts
7569
+ var cheerio4 = __toESM(require("cheerio"), 1);
7570
+ var csstree4 = __toESM(require("css-tree"), 1);
6949
7571
  var LIGHT_THRESHOLD = 0.7;
6950
7572
  var DARK_THRESHOLD = 0.15;
6951
7573
  function simulateDarkMode(html, clientId) {
@@ -6955,7 +7577,7 @@ function simulateDarkMode(html, clientId) {
6955
7577
  if (html.length > MAX_HTML_SIZE) {
6956
7578
  throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
6957
7579
  }
6958
- const $ = cheerio3.load(html);
7580
+ const $ = cheerio4.load(html);
6959
7581
  const warnings = [];
6960
7582
  $("img").each((_, el) => {
6961
7583
  const src = $(el).attr("src") || "";
@@ -7152,35 +7774,35 @@ function applyColorInversion($, mode) {
7152
7774
  $("style").each((_, el) => {
7153
7775
  const cssText = $(el).text();
7154
7776
  try {
7155
- const ast = csstree3.parse(cssText, { parseCustomProperty: true });
7777
+ const ast = csstree4.parse(cssText, { parseCustomProperty: true });
7156
7778
  let modified = false;
7157
- csstree3.walk(ast, {
7779
+ csstree4.walk(ast, {
7158
7780
  enter(node) {
7159
7781
  if (node.type !== "Declaration") return;
7160
7782
  const prop = node.property.toLowerCase();
7161
7783
  if (!COLOR_PROPS.has(prop) && prop !== "background") return;
7162
- const valueStr = csstree3.generate(node.value);
7784
+ const valueStr = csstree4.generate(node.value);
7163
7785
  if (prop === "background") {
7164
7786
  const bgColor = extractBackgroundColor(valueStr);
7165
7787
  if (bgColor) {
7166
7788
  const inverted = invertColor(bgColor, mode);
7167
7789
  if (inverted) {
7168
7790
  const newValue = valueStr.replace(bgColor, inverted);
7169
- node.value = csstree3.parse(newValue, { context: "value" });
7791
+ node.value = csstree4.parse(newValue, { context: "value" });
7170
7792
  modified = true;
7171
7793
  }
7172
7794
  }
7173
7795
  } else {
7174
7796
  const inverted = invertColor(valueStr, mode);
7175
7797
  if (inverted) {
7176
- node.value = csstree3.parse(inverted, { context: "value" });
7798
+ node.value = csstree4.parse(inverted, { context: "value" });
7177
7799
  modified = true;
7178
7800
  }
7179
7801
  }
7180
7802
  }
7181
7803
  });
7182
7804
  if (modified) {
7183
- $(el).text(csstree3.generate(ast));
7805
+ $(el).text(csstree4.generate(ast));
7184
7806
  }
7185
7807
  } catch (e) {
7186
7808
  }
@@ -7492,7 +8114,7 @@ function extractCode(response) {
7492
8114
  }
7493
8115
 
7494
8116
  // src/spam-scorer.ts
7495
- var cheerio4 = __toESM(require("cheerio"), 1);
8117
+ var cheerio5 = __toESM(require("cheerio"), 1);
7496
8118
  var SPAM_TRIGGER_PHRASES = [
7497
8119
  "act now",
7498
8120
  "limited time",
@@ -7923,12 +8545,12 @@ function analyzeSpam(html, options) {
7923
8545
  if (html.length > MAX_HTML_SIZE) {
7924
8546
  throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
7925
8547
  }
7926
- const $ = cheerio4.load(html);
8548
+ const $ = cheerio5.load(html);
7927
8549
  return analyzeSpamFromDom($, options);
7928
8550
  }
7929
8551
 
7930
8552
  // src/link-validator.ts
7931
- var cheerio5 = __toESM(require("cheerio"), 1);
8553
+ var cheerio6 = __toESM(require("cheerio"), 1);
7932
8554
  function classifyHref(href) {
7933
8555
  if (!href || !href.trim()) return "empty";
7934
8556
  const h = href.trim().toLowerCase();
@@ -8124,12 +8746,12 @@ function validateLinks(html) {
8124
8746
  if (html.length > MAX_HTML_SIZE) {
8125
8747
  throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
8126
8748
  }
8127
- const $ = cheerio5.load(html);
8749
+ const $ = cheerio6.load(html);
8128
8750
  return validateLinksFromDom($);
8129
8751
  }
8130
8752
 
8131
8753
  // src/accessibility-checker.ts
8132
- var cheerio6 = __toESM(require("cheerio"), 1);
8754
+ var cheerio7 = __toESM(require("cheerio"), 1);
8133
8755
  var RULE_PENALTY_CAPS = {
8134
8756
  "img-missing-alt": 3,
8135
8757
  "link-generic-text": 3,
@@ -8432,12 +9054,12 @@ function checkAccessibility(html) {
8432
9054
  if (html.length > MAX_HTML_SIZE) {
8433
9055
  throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
8434
9056
  }
8435
- const $ = cheerio6.load(html);
9057
+ const $ = cheerio7.load(html);
8436
9058
  return checkAccessibilityFromDom($);
8437
9059
  }
8438
9060
 
8439
9061
  // src/image-analyzer.ts
8440
- var cheerio7 = __toESM(require("cheerio"), 1);
9062
+ var cheerio8 = __toESM(require("cheerio"), 1);
8441
9063
  var DATA_URI_WARN_BYTES = 100 * 1024;
8442
9064
  var TOTAL_DATA_URI_WARN_BYTES = 500 * 1024;
8443
9065
  var HIGH_IMAGE_COUNT = 10;
@@ -8601,12 +9223,12 @@ function analyzeImages(html) {
8601
9223
  if (html.length > MAX_HTML_SIZE) {
8602
9224
  throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
8603
9225
  }
8604
- const $ = cheerio7.load(html);
9226
+ const $ = cheerio8.load(html);
8605
9227
  return analyzeImagesFromDom($);
8606
9228
  }
8607
9229
 
8608
9230
  // src/inbox-preview.ts
8609
- var cheerio8 = __toESM(require("cheerio"), 1);
9231
+ var cheerio9 = __toESM(require("cheerio"), 1);
8610
9232
  var MAX_SUBJECT_LENGTH = 60;
8611
9233
  var MAX_PREHEADER_LENGTH = 100;
8612
9234
  var MIN_PREHEADER_LENGTH = 30;
@@ -8626,7 +9248,7 @@ function extractInboxPreview(html) {
8626
9248
  if (html.length > MAX_HTML_SIZE) {
8627
9249
  throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
8628
9250
  }
8629
- const $ = cheerio8.load(html);
9251
+ const $ = cheerio9.load(html);
8630
9252
  return extractInboxPreviewFromDom($);
8631
9253
  }
8632
9254
  function extractInboxPreviewFromDom($) {
@@ -8794,7 +9416,7 @@ function getFirstVisibleText($, body) {
8794
9416
  }
8795
9417
 
8796
9418
  // src/size-checker.ts
8797
- var cheerio9 = __toESM(require("cheerio"), 1);
9419
+ var cheerio10 = __toESM(require("cheerio"), 1);
8798
9420
  function humanizeBytes(bytes) {
8799
9421
  if (bytes < 1024) return `${bytes} B`;
8800
9422
  const kb = bytes / 1024;
@@ -8832,12 +9454,12 @@ function checkSize(html) {
8832
9454
  if (html.length > MAX_HTML_SIZE) {
8833
9455
  throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
8834
9456
  }
8835
- const $ = cheerio9.load(html);
9457
+ const $ = cheerio10.load(html);
8836
9458
  return checkSizeFromDom($, html);
8837
9459
  }
8838
9460
 
8839
9461
  // src/template-checker.ts
8840
- var cheerio10 = __toESM(require("cheerio"), 1);
9462
+ var cheerio11 = __toESM(require("cheerio"), 1);
8841
9463
  function checkTemplateVariablesFromDom($) {
8842
9464
  const issues = [];
8843
9465
  const seen = /* @__PURE__ */ new Set();
@@ -8900,12 +9522,12 @@ function checkTemplateVariables(html) {
8900
9522
  if (html.length > MAX_HTML_SIZE) {
8901
9523
  throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
8902
9524
  }
8903
- const $ = cheerio10.load(html);
9525
+ const $ = cheerio11.load(html);
8904
9526
  return checkTemplateVariablesFromDom($);
8905
9527
  }
8906
9528
 
8907
9529
  // src/audit.ts
8908
- var cheerio11 = __toESM(require("cheerio"), 1);
9530
+ var cheerio12 = __toESM(require("cheerio"), 1);
8909
9531
  function auditEmail(html, options) {
8910
9532
  var _a;
8911
9533
  if (!html || !html.trim()) {
@@ -8925,7 +9547,7 @@ function auditEmail(html, options) {
8925
9547
  }
8926
9548
  const framework = options == null ? void 0 : options.framework;
8927
9549
  const skip = new Set((_a = options == null ? void 0 : options.skip) != null ? _a : []);
8928
- const $ = cheerio11.load(html);
9550
+ const $ = cheerio12.load(html);
8929
9551
  const warnings = skip.has("compatibility") ? [] : analyzeEmailFromDom($, framework);
8930
9552
  const scores = skip.has("compatibility") ? {} : generateCompatibilityScore(warnings);
8931
9553
  const spam = skip.has("spam") ? EMPTY_SPAM : analyzeSpamFromDom($, options == null ? void 0 : options.spam);
@@ -8938,8 +9560,53 @@ function auditEmail(html, options) {
8938
9560
  return { compatibility: { warnings, scores }, spam, links, accessibility, images, inboxPreview, size, templateVariables };
8939
9561
  }
8940
9562
 
9563
+ // src/plain-text.ts
9564
+ var cheerio13 = __toESM(require("cheerio"), 1);
9565
+ function toPlainText(html) {
9566
+ const $ = cheerio13.load(html);
9567
+ $("style, script").remove();
9568
+ $("[data-skip-in-text='true']").remove();
9569
+ $("hr").replaceWith("\n---\n");
9570
+ $("br").replaceWith("\n");
9571
+ $("img").each((_, el) => {
9572
+ const alt = $(el).attr("alt");
9573
+ $(el).replaceWith(alt ? alt : "");
9574
+ });
9575
+ $("a").each((_, el) => {
9576
+ const $el = $(el);
9577
+ const href = $el.attr("href") || "";
9578
+ const text2 = $el.text().trim();
9579
+ if (!href || href === text2) {
9580
+ $el.replaceWith(text2 || href);
9581
+ } else if (!text2) {
9582
+ $el.replaceWith(href);
9583
+ } else {
9584
+ $el.replaceWith(`${text2} (${href})`);
9585
+ }
9586
+ });
9587
+ $("li").each((_, el) => {
9588
+ const $el = $(el);
9589
+ const text2 = $el.text().trim();
9590
+ $el.replaceWith(`
9591
+ - ${text2}
9592
+ `);
9593
+ });
9594
+ const blockTags = "p, div, h1, h2, h3, h4, h5, h6, tr, blockquote, ul, ol";
9595
+ $(blockTags).each((_, el) => {
9596
+ const $el = $(el);
9597
+ $el.prepend("\n\n");
9598
+ $el.append("\n\n");
9599
+ });
9600
+ let text = $("body").text();
9601
+ if (!text.trim()) {
9602
+ text = $.root().text();
9603
+ }
9604
+ text = text.replace(/\n{3,}/g, "\n\n");
9605
+ return text.trim();
9606
+ }
9607
+
8941
9608
  // src/session.ts
8942
- var cheerio12 = __toESM(require("cheerio"), 1);
9609
+ var cheerio14 = __toESM(require("cheerio"), 1);
8943
9610
  function createSession(html, options) {
8944
9611
  if (!html || !html.trim()) {
8945
9612
  const fw = options == null ? void 0 : options.framework;
@@ -8973,7 +9640,7 @@ function createSession(html, options) {
8973
9640
  if (html.length > MAX_HTML_SIZE) {
8974
9641
  throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
8975
9642
  }
8976
- const $ = cheerio12.load(html);
9643
+ const $ = cheerio14.load(html);
8977
9644
  const framework = options == null ? void 0 : options.framework;
8978
9645
  return {
8979
9646
  html,
@@ -9066,9 +9733,11 @@ var CompileError = class extends Error {
9066
9733
  contrastRatio,
9067
9734
  createSession,
9068
9735
  diffResults,
9736
+ downlevelCSS,
9069
9737
  errorWarnings,
9070
9738
  estimateAiFixTokens,
9071
9739
  extractInboxPreview,
9740
+ formatRgb,
9072
9741
  generateAiFix,
9073
9742
  generateCompatibilityScore,
9074
9743
  generateFixPrompt,
@@ -9080,6 +9749,7 @@ var CompileError = class extends Error {
9080
9749
  relativeLuminance,
9081
9750
  simulateDarkMode,
9082
9751
  structuralWarnings,
9752
+ toPlainText,
9083
9753
  transformForAllClients,
9084
9754
  transformForClient,
9085
9755
  validateLinks,