@emailens/engine 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,34 +1,9 @@
1
- var __defProp = Object.defineProperty;
2
- var __defProps = Object.defineProperties;
3
- var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
- var __getOwnPropSymbols = Object.getOwnPropertySymbols;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __propIsEnum = Object.prototype.propertyIsEnumerable;
7
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
8
- var __spreadValues = (a, b) => {
9
- for (var prop in b || (b = {}))
10
- if (__hasOwnProp.call(b, prop))
11
- __defNormalProp(a, prop, b[prop]);
12
- if (__getOwnPropSymbols)
13
- for (var prop of __getOwnPropSymbols(b)) {
14
- if (__propIsEnum.call(b, prop))
15
- __defNormalProp(a, prop, b[prop]);
16
- }
17
- return a;
18
- };
19
- var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
20
- var __objRest = (source, exclude) => {
21
- var target = {};
22
- for (var prop in source)
23
- if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
24
- target[prop] = source[prop];
25
- if (source != null && __getOwnPropSymbols)
26
- for (var prop of __getOwnPropSymbols(source)) {
27
- if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
28
- target[prop] = source[prop];
29
- }
30
- return target;
31
- };
1
+ import {
2
+ CompileError,
3
+ __objRest,
4
+ __spreadProps,
5
+ __spreadValues
6
+ } from "./chunk-PFONR3YC.js";
32
7
 
33
8
  // src/clients.ts
34
9
  var EMAIL_CLIENTS = [
@@ -1060,8 +1035,8 @@ var STRUCTURAL_FIX_PROPERTIES = /* @__PURE__ */ new Set([
1060
1035
  "object-fit"
1061
1036
  ]);
1062
1037
 
1063
- // src/fix-snippets.ts
1064
- var FIX_DATABASE = {
1038
+ // src/fix-snippets/html-fixes.ts
1039
+ var HTML_FIX_DATABASE = {
1065
1040
  // ── border-radius (Outlook VML fallback) ──────────────────────────────
1066
1041
  "border-radius::outlook": {
1067
1042
  language: "html",
@@ -1565,7 +1540,7 @@ h1 {
1565
1540
  Preheader text
1566
1541
  </div>`
1567
1542
  },
1568
- // ── word-break → table cell wrapping ─────────────────────────────────────
1543
+ // ── word-break → table cell wrapping ──────────────────────────────────
1569
1544
  "word-break": {
1570
1545
  language: "html",
1571
1546
  description: "Wrap long text in a table cell to force line breaks without word-break",
@@ -1583,41 +1558,7 @@ h1 {
1583
1558
  </tr>
1584
1559
  </table>`
1585
1560
  },
1586
- "word-break::jsx": {
1587
- language: "jsx",
1588
- description: "Wrap long text in a table cell for Outlook-safe word breaking",
1589
- before: `<span style={{ wordBreak: "break-all" }}>{url}</span>`,
1590
- after: `{/* Table cells force text wrapping in Outlook and Yahoo */}
1591
- <table width="100%" cellPadding={0} cellSpacing={0}
1592
- role="presentation" style={{ borderCollapse: "collapse" }}>
1593
- <tr>
1594
- <td style={{
1595
- wordBreak: "break-all" as const,
1596
- overflowWrap: "break-word" as const,
1597
- wordWrap: "break-word" as const,
1598
- }}>
1599
- {url}
1600
- </td>
1601
- </tr>
1602
- </table>`
1603
- },
1604
- "word-break::mjml": {
1605
- language: "mjml",
1606
- description: "MJML renders text in table cells by default \u2014 word-break works via mj-text",
1607
- before: `<mj-text>
1608
- <span style="word-break: break-all;">Long URL here</span>
1609
- </mj-text>`,
1610
- after: `<!-- mj-text already renders inside a <td>, so add word-break
1611
- to the mj-text css-class or inline style -->
1612
- <mj-text css-class="break-words"
1613
- padding="0">
1614
- Long URL here
1615
- </mj-text>
1616
- <mj-style>
1617
- .break-words td { word-break: break-all; word-wrap: break-word; }
1618
- </mj-style>`
1619
- },
1620
- // ── overflow-wrap → table cell wrapping ────────────────────────────────
1561
+ // ── overflow-wrap → table cell wrapping ───────────────────────────────
1621
1562
  "overflow-wrap": {
1622
1563
  language: "html",
1623
1564
  description: "Use a table cell to force word wrapping without overflow-wrap",
@@ -1634,24 +1575,7 @@ h1 {
1634
1575
  </tr>
1635
1576
  </table>`
1636
1577
  },
1637
- "overflow-wrap::jsx": {
1638
- language: "jsx",
1639
- description: "Wrap text in a table cell for Outlook-safe overflow wrapping",
1640
- before: `<p style={{ overflowWrap: "break-word" }}>{longText}</p>`,
1641
- after: `<table width="100%" cellPadding={0} cellSpacing={0}
1642
- role="presentation" style={{ borderCollapse: "collapse" }}>
1643
- <tr>
1644
- <td style={{
1645
- overflowWrap: "break-word" as const,
1646
- wordWrap: "break-word" as const,
1647
- wordBreak: "break-all" as const,
1648
- }}>
1649
- {longText}
1650
- </td>
1651
- </tr>
1652
- </table>`
1653
- },
1654
- // ── text-shadow → border/font-weight alternative ───────────────────────
1578
+ // ── text-shadow → border/font-weight alternative ──────────────────────
1655
1579
  "text-shadow": {
1656
1580
  language: "css",
1657
1581
  description: "Use font-weight or border-bottom as alternatives to text-shadow",
@@ -1665,7 +1589,7 @@ h1 {
1665
1589
  letter-spacing: 0.5px;
1666
1590
  }`
1667
1591
  },
1668
- // ── border-spacing → cellspacing attribute ─────────────────────────────
1592
+ // ── border-spacing → cellspacing attribute ────────────────────────────
1669
1593
  "border-spacing": {
1670
1594
  language: "html",
1671
1595
  description: "Use the cellspacing HTML attribute instead of border-spacing CSS",
@@ -1676,7 +1600,7 @@ h1 {
1676
1600
  <tr><td>Cell</td></tr>
1677
1601
  </table>`
1678
1602
  },
1679
- // ── min-width → fixed width ────────────────────────────────────────────
1603
+ // ── min-width → fixed width ──────────────────────────────────────────
1680
1604
  "min-width": {
1681
1605
  language: "html",
1682
1606
  description: "Use a fixed width instead of min-width for Outlook compatibility",
@@ -1684,7 +1608,7 @@ h1 {
1684
1608
  after: `<!-- Outlook ignores min-width. Use a fixed width or a spacer. -->
1685
1609
  <td width="200" style="width: 200px;">Content</td>`
1686
1610
  },
1687
- // ── min-height → fixed height ──────────────────────────────────────────
1611
+ // ── min-height → fixed height ────────────────────────────────────────
1688
1612
  "min-height": {
1689
1613
  language: "html",
1690
1614
  description: "Use a fixed height or spacer instead of min-height",
@@ -1692,7 +1616,7 @@ h1 {
1692
1616
  after: `<!-- Outlook ignores min-height. Use height or a spacer image. -->
1693
1617
  <td height="100" style="height: 100px;">Content</td>`
1694
1618
  },
1695
- // ── max-height → fixed height ──────────────────────────────────────────
1619
+ // ── max-height → fixed height ────────────────────────────────────────
1696
1620
  "max-height": {
1697
1621
  language: "html",
1698
1622
  description: "Outlook ignores max-height \u2014 truncate content server-side",
@@ -1704,8 +1628,49 @@ h1 {
1704
1628
  Shortened content...
1705
1629
  <a href="https://example.com/full">Read more</a>
1706
1630
  </div>`
1631
+ }
1632
+ };
1633
+
1634
+ // src/fix-snippets/jsx-fixes.ts
1635
+ var JSX_FIX_DATABASE = {
1636
+ // ── word-break (JSX) ─────────────────────────────────────────────────
1637
+ "word-break::jsx": {
1638
+ language: "jsx",
1639
+ description: "Wrap long text in a table cell for Outlook-safe word breaking",
1640
+ before: `<span style={{ wordBreak: "break-all" }}>{url}</span>`,
1641
+ after: `{/* Table cells force text wrapping in Outlook and Yahoo */}
1642
+ <table width="100%" cellPadding={0} cellSpacing={0}
1643
+ role="presentation" style={{ borderCollapse: "collapse" }}>
1644
+ <tr>
1645
+ <td style={{
1646
+ wordBreak: "break-all" as const,
1647
+ overflowWrap: "break-word" as const,
1648
+ wordWrap: "break-word" as const,
1649
+ }}>
1650
+ {url}
1651
+ </td>
1652
+ </tr>
1653
+ </table>`
1654
+ },
1655
+ // ── overflow-wrap (JSX) ──────────────────────────────────────────────
1656
+ "overflow-wrap::jsx": {
1657
+ language: "jsx",
1658
+ description: "Wrap text in a table cell for Outlook-safe overflow wrapping",
1659
+ before: `<p style={{ overflowWrap: "break-word" }}>{longText}</p>`,
1660
+ after: `<table width="100%" cellPadding={0} cellSpacing={0}
1661
+ role="presentation" style={{ borderCollapse: "collapse" }}>
1662
+ <tr>
1663
+ <td style={{
1664
+ overflowWrap: "break-word" as const,
1665
+ wordWrap: "break-word" as const,
1666
+ wordBreak: "break-all" as const,
1667
+ }}>
1668
+ {longText}
1669
+ </td>
1670
+ </tr>
1671
+ </table>`
1707
1672
  },
1708
- // ── REACT EMAIL (jsx) framework-specific fixes ────────────────────────────
1673
+ // ── display:flex (Outlook JSX) ───────────────────────────────────────
1709
1674
  "display:flex::outlook::jsx": {
1710
1675
  language: "jsx",
1711
1676
  description: "Use React Email Row + Column components instead of flexbox (Outlook-safe)",
@@ -1724,6 +1689,7 @@ h1 {
1724
1689
  </Column>
1725
1690
  </Row>`
1726
1691
  },
1692
+ // ── display:grid (JSX) ──────────────────────────────────────────────
1727
1693
  "display:grid::jsx": {
1728
1694
  language: "jsx",
1729
1695
  description: "Replace CSS Grid with React Email Row + Column components",
@@ -1742,6 +1708,7 @@ h1 {
1742
1708
  </Column>
1743
1709
  </Row>`
1744
1710
  },
1711
+ // ── max-width (Outlook JSX) ─────────────────────────────────────────
1745
1712
  "max-width::outlook::jsx": {
1746
1713
  language: "jsx",
1747
1714
  description: "Use React Email Container component for Outlook-safe max-width centering",
@@ -1754,6 +1721,7 @@ h1 {
1754
1721
  Content here
1755
1722
  </Container>`
1756
1723
  },
1724
+ // ── @font-face (JSX) ────────────────────────────────────────────────
1757
1725
  "@font-face::jsx": {
1758
1726
  language: "jsx",
1759
1727
  description: "Use React Email Font component instead of @font-face in <style>",
@@ -1781,6 +1749,7 @@ h1 {
1781
1749
  </Head>
1782
1750
  <h1 style={{ fontFamily: "'CustomFont', Arial, sans-serif" }}>Hello</h1>`
1783
1751
  },
1752
+ // ── <svg> (JSX) ─────────────────────────────────────────────────────
1784
1753
  "<svg>::jsx": {
1785
1754
  language: "jsx",
1786
1755
  description: "Replace inline SVG with React Email Img component",
@@ -1797,6 +1766,7 @@ h1 {
1797
1766
  style={{ display: "block", border: "0" }}
1798
1767
  />`
1799
1768
  },
1769
+ // ── <video> (JSX) ───────────────────────────────────────────────────
1800
1770
  "<video>::jsx": {
1801
1771
  language: "jsx",
1802
1772
  description: "Replace video with a linked thumbnail using React Email Img + Link",
@@ -1814,6 +1784,7 @@ h1 {
1814
1784
  />
1815
1785
  </Link>`
1816
1786
  },
1787
+ // ── border-radius (Outlook JSX) ─────────────────────────────────────
1817
1788
  "border-radius::outlook::jsx": {
1818
1789
  language: "jsx",
1819
1790
  description: "Render rounded buttons with VML via JSX dangerouslySetInnerHTML (Outlook workaround)",
@@ -1824,31 +1795,9 @@ h1 {
1824
1795
  textDecoration: "none", display: "inline-block" }}>
1825
1796
  Click Here
1826
1797
  </a>`,
1827
- after: `{/* Use dangerouslySetInnerHTML to inject VML for Outlook rounded corners */}
1828
- <div
1829
- dangerouslySetInnerHTML={{
1830
- __html: \`
1831
- <!--[if mso]>
1832
- <v:roundrect xmlns:v="urn:schemas-microsoft-com:vml"
1833
- href="https://example.com"
1834
- style="height:44px; v-text-anchor:middle; width:200px;"
1835
- arcsize="14%" strokecolor="#6d28d9" fillcolor="#6d28d9">
1836
- <w:anchorlock/>
1837
- <center style="color:#fff; font-family:Arial,sans-serif;
1838
- font-size:14px; font-weight:bold;">Click Here</center>
1839
- </v:roundrect>
1840
- <![endif]-->
1841
- <!--[if !mso]><!-->
1842
- <a href="https://example.com"
1843
- style="background-color:#6d28d9; color:#fff; padding:12px 32px;
1844
- border-radius:6px; text-decoration:none; display:inline-block;">
1845
- Click Here
1846
- </a>
1847
- <!--<![endif]-->
1848
- \`,
1849
- }}
1850
- />`
1798
+ after: '{/* Use dangerouslySetInnerHTML to inject VML for Outlook rounded corners */}\n<div\n dangerouslySetInnerHTML={{\n __html: `\n<!--[if mso]>\n<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml"\n href="https://example.com"\n style="height:44px; v-text-anchor:middle; width:200px;"\n arcsize="14%" strokecolor="#6d28d9" fillcolor="#6d28d9">\n <w:anchorlock/>\n <center style="color:#fff; font-family:Arial,sans-serif;\n font-size:14px; font-weight:bold;">Click Here</center>\n</v:roundrect>\n<![endif]-->\n<!--[if !mso]><!-->\n<a href="https://example.com"\n style="background-color:#6d28d9; color:#fff; padding:12px 32px;\n border-radius:6px; text-decoration:none; display:inline-block;">\n Click Here\n</a>\n<!--<![endif]-->\n`,\n }}\n/>'
1851
1799
  },
1800
+ // ── gap (JSX) ───────────────────────────────────────────────────────
1852
1801
  "gap::jsx": {
1853
1802
  language: "jsx",
1854
1803
  description: "Use padding style prop on Column instead of gap (email-safe spacing)",
@@ -1866,6 +1815,7 @@ h1 {
1866
1815
  <Column>Item 3</Column>
1867
1816
  </Row>`
1868
1817
  },
1818
+ // ── <style> (Gmail JSX) ─────────────────────────────────────────────
1869
1819
  "<style>::gmail::jsx": {
1870
1820
  language: "jsx",
1871
1821
  description: "React Email inlines styles via style props \u2014 manual <style> blocks won't survive Gmail",
@@ -1885,6 +1835,7 @@ h1 {
1885
1835
  <h1 style={{ color: "#fff", fontSize: "24px", margin: 0 }}>Hello</h1>
1886
1836
  </div>`
1887
1837
  },
1838
+ // ── <link> (JSX) ────────────────────────────────────────────────────
1888
1839
  "<link>::jsx": {
1889
1840
  language: "jsx",
1890
1841
  description: "Use React Email Head component instead of <link> for stylesheet references",
@@ -1901,281 +1852,100 @@ h1 {
1901
1852
  {/* Inline your CSS here or use Font component for web fonts */}
1902
1853
  </Head>`
1903
1854
  },
1904
- // ── MJML framework-specific fixes ─────────────────────────────────────────
1905
- "@font-face::mjml": {
1906
- language: "mjml",
1907
- description: "Use mj-font in mj-head instead of @font-face",
1908
- before: `<mj-style>
1909
- @font-face {
1910
- font-family: 'CustomFont';
1911
- src: url('https://example.com/custom.woff2') format('woff2');
1912
- }
1913
- </mj-style>`,
1914
- after: `<mjml>
1915
- <mj-head>
1916
- <mj-font name="CustomFont"
1917
- href="https://fonts.googleapis.com/css2?family=CustomFont" />
1918
- <mj-attributes>
1919
- <mj-all font-family="CustomFont, Arial, Helvetica, sans-serif" />
1920
- </mj-attributes>
1921
- </mj-head>
1922
- </mjml>`
1855
+ // ── <form> (JSX) ───────────────────────────────────────────────────
1856
+ "<form>::jsx": {
1857
+ language: "jsx",
1858
+ description: "Replace embedded form with a React Email Button linking to a hosted form",
1859
+ before: `<form action="/subscribe" method="POST">
1860
+ <input type="email" placeholder="Email" />
1861
+ <button type="submit">Subscribe</button>
1862
+ </form>`,
1863
+ after: `import { Button } from "@react-email/components";
1864
+
1865
+ <Button
1866
+ href="https://example.com/subscribe"
1867
+ style={{
1868
+ backgroundColor: "#6d28d9",
1869
+ color: "#fff",
1870
+ padding: "12px 32px",
1871
+ borderRadius: "6px",
1872
+ textDecoration: "none",
1873
+ fontWeight: "bold",
1874
+ }}
1875
+ >
1876
+ Subscribe Now
1877
+ </Button>`
1923
1878
  },
1924
- "<style>::gmail::mjml": {
1925
- language: "mjml",
1926
- description: "Use mj-style inline='inline' to force style inlining for Gmail",
1927
- before: `<mj-head>
1928
- <mj-style>
1929
- .custom { color: #6d28d9; }
1930
- </mj-style>
1931
- </mj-head>`,
1932
- after: `<mj-head>
1933
- <!-- Use inline="inline" to force MJML to inline these styles.
1934
- Class-based styles in a plain mj-style block will be stripped by Gmail. -->
1935
- <mj-style inline="inline">
1936
- .custom { color: #6d28d9; }
1937
- </mj-style>
1938
- </mj-head>`
1879
+ // ── @media (JSX) ────────────────────────────────────────────────────
1880
+ "@media::jsx": {
1881
+ language: "jsx",
1882
+ description: "Design mobile-first \u2014 @media queries are stripped by many clients",
1883
+ before: `import { Head } from "@react-email/components";
1884
+
1885
+ <Head>
1886
+ <style>{\`
1887
+ @media (max-width: 600px) {
1888
+ .cols { display: block !important; }
1889
+ .col { width: 100% !important; }
1890
+ }
1891
+ \`}</style>
1892
+ </Head>
1893
+ <Row>
1894
+ <Column className="col" style={{ width: "50%" }}>Left</Column>
1895
+ <Column className="col" style={{ width: "50%" }}>Right</Column>
1896
+ </Row>`,
1897
+ after: `import { Container, Section, Text } from "@react-email/components";
1898
+
1899
+ {/* Single-column stacked layout works without @media.
1900
+ Stack content vertically so it reads well on all clients. */}
1901
+ <Container style={{ maxWidth: "600px" }}>
1902
+ <Section style={{ padding: "16px" }}>
1903
+ <Text>Left</Text>
1904
+ </Section>
1905
+ <Section style={{ padding: "16px" }}>
1906
+ <Text>Right</Text>
1907
+ </Section>
1908
+ </Container>`
1939
1909
  },
1940
- "border-radius::outlook::mjml": {
1941
- language: "mjml",
1942
- description: "MJML limitation: border-radius is unsupported in Outlook \u2014 MJML does not generate VML",
1943
- before: `<mj-button border-radius="6px" background-color="#6d28d9">
1944
- Click Here
1945
- </mj-button>`,
1946
- after: `<!-- Known MJML limitation: MJML does not generate VML for rounded corners.
1947
- Options: accept flat corners, use mj-raw for VML, or set border-radius="0". -->
1948
- <mj-button border-radius="0" background-color="#6d28d9">
1949
- Click Here
1950
- </mj-button>`
1910
+ // ── position (JSX) ─────────────────────────────────────────────────
1911
+ "position::jsx": {
1912
+ language: "jsx",
1913
+ description: "Use React Email Row and Column for layout instead of CSS position",
1914
+ before: `<div style={{ position: "relative" }}>
1915
+ <div style={{ position: "absolute", top: 0, right: 0 }}>
1916
+ Badge
1917
+ </div>
1918
+ <p>Content</p>
1919
+ </div>`,
1920
+ after: `import { Row, Column, Text } from "@react-email/components";
1921
+
1922
+ <Row>
1923
+ <Column style={{ verticalAlign: "top" }}>
1924
+ <Text>Content</Text>
1925
+ </Column>
1926
+ <Column style={{ width: "80px", verticalAlign: "top", textAlign: "right" }}>
1927
+ <Text>Badge</Text>
1928
+ </Column>
1929
+ </Row>`
1951
1930
  },
1952
- "background-image::outlook::mjml": {
1953
- language: "mjml",
1954
- description: "Use mj-section background-url for Outlook-compatible background images",
1955
- before: `<mj-section>
1956
- <mj-column>
1957
- <mj-image src="hero.jpg" />
1958
- </mj-column>
1959
- </mj-section>`,
1960
- after: `<!-- MJML generates VML-compatible markup automatically via background-url on mj-section. -->
1961
- <mj-section background-url="https://example.com/hero.jpg"
1962
- background-size="cover"
1963
- background-repeat="no-repeat"
1964
- background-color="#333333">
1965
- <mj-column>
1966
- <mj-text color="#ffffff">Your content here</mj-text>
1967
- </mj-column>
1968
- </mj-section>`
1969
- },
1970
- "display:flex::mjml": {
1971
- language: "mjml",
1972
- description: "Replace flexbox (from mj-raw or inline styles) with mj-section and mj-column",
1973
- before: `<mj-raw>
1974
- <div style="display: flex; gap: 16px;">
1975
- <div style="flex: 1;">Column 1</div>
1976
- <div style="flex: 1;">Column 2</div>
1977
- </div>
1978
- </mj-raw>`,
1979
- after: `<!-- Flexbox in MJML is not Outlook-compatible.
1980
- Use mj-section and mj-column \u2014 MJML compiles these to table-based layouts. -->
1981
- <mj-section>
1982
- <mj-column width="50%">
1983
- <mj-text>Column 1</mj-text>
1984
- </mj-column>
1985
- <mj-column width="50%">
1986
- <mj-text>Column 2</mj-text>
1987
- </mj-column>
1988
- </mj-section>`
1989
- },
1990
- "@media::mjml": {
1991
- language: "mjml",
1992
- description: "Use MJML responsive attributes and breakpoints instead of hand-written @media",
1993
- before: `<mj-style>
1994
- @media (max-width: 600px) {
1995
- .mobile-stack { display: block !important; width: 100% !important; }
1996
- }
1997
- </mj-style>`,
1998
- after: `<!-- MJML generates responsive @media queries automatically.
1999
- Use mj-breakpoint and mj-column widths to control responsive behavior. -->
2000
- <mj-head>
2001
- <mj-breakpoint width="600px" />
2002
- </mj-head>
2003
- <mj-body>
2004
- <mj-section>
2005
- <mj-column width="50%"><mj-text>Left</mj-text></mj-column>
2006
- <mj-column width="50%"><mj-text>Right</mj-text></mj-column>
2007
- </mj-section>
2008
- </mj-body>`
2009
- },
2010
- // ── MAIZZLE framework-specific fixes ──────────────────────────────────────
2011
- "display:flex::outlook::maizzle": {
2012
- language: "maizzle",
2013
- description: "Replace Tailwind flex classes with HTML table + MSO conditional comments",
2014
- before: `<div class="flex gap-4">
2015
- <div class="flex-1">Column 1</div>
2016
- <div class="flex-1">Column 2</div>
2017
- </div>`,
2018
- after: `<!--[if mso]>
2019
- <table role="presentation" width="100%" cellpadding="0"
2020
- cellspacing="0" border="0"><tr>
2021
- <td class="w-1/2" valign="top">Column 1</td>
2022
- <td class="w-1/2" valign="top">Column 2</td>
2023
- </tr></table>
2024
- <![endif]-->
2025
- <!--[if !mso]><!-->
2026
- <div class="flex gap-4">
2027
- <div class="flex-1">Column 1</div>
2028
- <div class="flex-1">Column 2</div>
2029
- </div>
2030
- <!--<![endif]-->`
2031
- },
2032
- "@font-face::maizzle": {
2033
- language: "maizzle",
2034
- description: 'Add fonts via the googleFonts key in config.js \u2014 Maizzle injects the Google Fonts link tag automatically. Set googleFonts: "Inter:ital,wght@0,400;0,700" in your environment config, then reference the font family in your template.',
2035
- before: `<style>
2036
- @font-face {
2037
- font-family: 'Inter';
2038
- src: url('https://fonts.gstatic.com/...') format('woff2');
2039
- }
2040
- </style>`,
2041
- after: `<!-- config.js: googleFonts: "Inter:ital,wght@0,400;0,700" -->
2042
- <p class="font-['Inter',Arial,sans-serif]">Hello</p>`
2043
- },
2044
- "<style>::gmail::maizzle": {
2045
- language: "maizzle",
2046
- description: "Maizzle automatically inlines CSS via juice during build (inlineCSS: true in config.js). Manual <style> blocks bypass juice and will be stripped by Gmail \u2014 prefer Tailwind utility classes instead.",
2047
- before: `<style>
2048
- .custom { color: #6d28d9; }
2049
- </style>
2050
- <div class="custom">Hello</div>`,
2051
- after: `<!-- Prefer Tailwind classes \u2014 Maizzle inlines them automatically during build -->
2052
- <div class="text-[#6d28d9]">Hello</div>`
2053
- },
2054
- "max-width::outlook::maizzle": {
2055
- language: "maizzle",
2056
- description: "Wrap max-width containers with MSO conditional table for Outlook",
2057
- before: `<div class="max-w-[600px] mx-auto">
2058
- Content here
2059
- </div>`,
2060
- after: `<!--[if mso]>
2061
- <table role="presentation" width="600" cellpadding="0"
2062
- cellspacing="0" border="0" align="center"><tr><td>
2063
- <![endif]-->
2064
- <div class="max-w-[600px] mx-auto">
2065
- Content here
2066
- </div>
2067
- <!--[if mso]>
2068
- </td></tr></table>
2069
- <![endif]-->`
2070
- },
2071
- "gap::maizzle": {
2072
- language: "maizzle",
2073
- description: "Use padding Tailwind classes on child elements instead of gap",
2074
- before: `<div class="flex gap-4">
2075
- <div>Item 1</div>
2076
- <div>Item 2</div>
2077
- <div>Item 3</div>
2078
- </div>`,
2079
- after: `<!-- gap is not supported in Outlook or many email clients.
2080
- Use padding classes on child elements instead. -->
2081
- <table role="presentation" cellpadding="0" cellspacing="0" border="0">
2082
- <tr>
2083
- <td class="pr-4">Item 1</td>
2084
- <td class="pr-4">Item 2</td>
2085
- <td>Item 3</td>
2086
- </tr>
2087
- </table>`
2088
- },
2089
- // ── JSX fixes for remaining common properties ─────────────────────────────
2090
- "<form>::jsx": {
2091
- language: "jsx",
2092
- description: "Replace embedded form with a React Email Button linking to a hosted form",
2093
- before: `<form action="/subscribe" method="POST">
2094
- <input type="email" placeholder="Email" />
2095
- <button type="submit">Subscribe</button>
2096
- </form>`,
2097
- after: `import { Button } from "@react-email/components";
2098
-
2099
- <Button
2100
- href="https://example.com/subscribe"
2101
- style={{
2102
- backgroundColor: "#6d28d9",
2103
- color: "#fff",
2104
- padding: "12px 32px",
2105
- borderRadius: "6px",
2106
- textDecoration: "none",
2107
- fontWeight: "bold",
2108
- }}
2109
- >
2110
- Subscribe Now
2111
- </Button>`
2112
- },
2113
- "@media::jsx": {
2114
- language: "jsx",
2115
- description: "Design mobile-first \u2014 @media queries are stripped by many clients",
2116
- before: `import { Head } from "@react-email/components";
2117
-
2118
- <Head>
2119
- <style>{\`
2120
- @media (max-width: 600px) {
2121
- .cols { display: block !important; }
2122
- .col { width: 100% !important; }
2123
- }
2124
- \`}</style>
2125
- </Head>
2126
- <Row>
2127
- <Column className="col" style={{ width: "50%" }}>Left</Column>
2128
- <Column className="col" style={{ width: "50%" }}>Right</Column>
2129
- </Row>`,
2130
- after: `import { Container, Section, Text } from "@react-email/components";
2131
-
2132
- {/* Single-column stacked layout works without @media.
2133
- Stack content vertically so it reads well on all clients. */}
2134
- <Container style={{ maxWidth: "600px" }}>
2135
- <Section style={{ padding: "16px" }}>
2136
- <Text>Left</Text>
2137
- </Section>
2138
- <Section style={{ padding: "16px" }}>
2139
- <Text>Right</Text>
2140
- </Section>
2141
- </Container>`
2142
- },
2143
- "position::jsx": {
2144
- language: "jsx",
2145
- description: "Use React Email Row and Column for layout instead of CSS position",
2146
- before: `<div style={{ position: "relative" }}>
2147
- <div style={{ position: "absolute", top: 0, right: 0 }}>
2148
- Badge
2149
- </div>
2150
- <p>Content</p>
2151
- </div>`,
2152
- after: `import { Row, Column, Text } from "@react-email/components";
2153
-
2154
- <Row>
2155
- <Column style={{ verticalAlign: "top" }}>
2156
- <Text>Content</Text>
2157
- </Column>
2158
- <Column style={{ width: "80px", verticalAlign: "top", textAlign: "right" }}>
2159
- <Text>Badge</Text>
2160
- </Column>
2161
- </Row>`
2162
- },
2163
- "float::jsx": {
2164
- language: "jsx",
2165
- description: "Use React Email Row and Column instead of float for side-by-side layout",
2166
- before: `<img src="photo.jpg" style={{ float: "left", marginRight: "16px" }} width={200} />
2167
- <p>Text wraps around the image.</p>`,
2168
- after: `import { Row, Column, Img, Text } from "@react-email/components";
2169
-
2170
- <Row>
2171
- <Column style={{ width: "200px", paddingRight: "16px", verticalAlign: "top" }}>
2172
- <Img src="photo.jpg" width={200} style={{ display: "block", border: "0" }} />
2173
- </Column>
2174
- <Column style={{ verticalAlign: "top" }}>
2175
- <Text>Text next to the image.</Text>
2176
- </Column>
2177
- </Row>`
1931
+ // ── float (JSX) ────────────────────────────────────────────────────
1932
+ "float::jsx": {
1933
+ language: "jsx",
1934
+ description: "Use React Email Row and Column instead of float for side-by-side layout",
1935
+ before: `<img src="photo.jpg" style={{ float: "left", marginRight: "16px" }} width={200} />
1936
+ <p>Text wraps around the image.</p>`,
1937
+ after: `import { Row, Column, Img, Text } from "@react-email/components";
1938
+
1939
+ <Row>
1940
+ <Column style={{ width: "200px", paddingRight: "16px", verticalAlign: "top" }}>
1941
+ <Img src="photo.jpg" width={200} style={{ display: "block", border: "0" }} />
1942
+ </Column>
1943
+ <Column style={{ verticalAlign: "top" }}>
1944
+ <Text>Text next to the image.</Text>
1945
+ </Column>
1946
+ </Row>`
2178
1947
  },
1948
+ // ── background-image (Outlook JSX) ──────────────────────────────────
2179
1949
  "background-image::outlook::jsx": {
2180
1950
  language: "jsx",
2181
1951
  description: "Use VML for Outlook background images in JSX via dangerouslySetInnerHTML",
@@ -2183,26 +1953,9 @@ h1 {
2183
1953
  backgroundSize: "cover", padding: "40px" }}>
2184
1954
  <h1 style={{ color: "#fff" }}>Hello World</h1>
2185
1955
  </td>`,
2186
- after: `<div
2187
- dangerouslySetInnerHTML={{
2188
- __html: \`
2189
- <!--[if gte mso 9]>
2190
- <v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true"
2191
- stroke="false" style="width:600px; height:300px;">
2192
- <v:fill type="frame" src="hero.jpg" />
2193
- <v:textbox inset="0,0,0,0">
2194
- <![endif]-->
2195
- <div style="background-image:url('hero.jpg'); background-size:cover; padding:40px;">
2196
- <h1 style="color:#fff;">Hello World</h1>
2197
- </div>
2198
- <!--[if gte mso 9]>
2199
- </v:textbox>
2200
- </v:rect>
2201
- <![endif]-->
2202
- \`,
2203
- }}
2204
- />`
1956
+ after: '<div\n dangerouslySetInnerHTML={{\n __html: `\n<!--[if gte mso 9]>\n<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true"\n stroke="false" style="width:600px; height:300px;">\n <v:fill type="frame" src="hero.jpg" />\n <v:textbox inset="0,0,0,0">\n<![endif]-->\n<div style="background-image:url(\'hero.jpg\'); background-size:cover; padding:40px;">\n <h1 style="color:#fff;">Hello World</h1>\n</div>\n<!--[if gte mso 9]>\n </v:textbox>\n</v:rect>\n<![endif]-->\n`,\n }}\n/>'
2205
1957
  },
1958
+ // ── opacity (JSX) ──────────────────────────────────────────────────
2206
1959
  "opacity::jsx": {
2207
1960
  language: "jsx",
2208
1961
  description: "Use solid colors instead of opacity in style objects",
@@ -2219,6 +1972,7 @@ h1 {
2219
1972
  Overlay content
2220
1973
  </div>`
2221
1974
  },
1975
+ // ── box-shadow (JSX) ──────────────────────────────────────────────
2222
1976
  "box-shadow::jsx": {
2223
1977
  language: "jsx",
2224
1978
  description: "Use border as a fallback for boxShadow in style objects",
@@ -2237,6 +1991,7 @@ h1 {
2237
1991
  Card content
2238
1992
  </Section>`
2239
1993
  },
1994
+ // ── linear-gradient (JSX) ─────────────────────────────────────────
2240
1995
  "linear-gradient::jsx": {
2241
1996
  language: "jsx",
2242
1997
  description: "Add a solid backgroundColor fallback before the gradient",
@@ -2257,6 +2012,7 @@ h1 {
2257
2012
  Content here
2258
2013
  </div>`
2259
2014
  },
2015
+ // ── transform (JSX) ──────────────────────────────────────────────
2260
2016
  "transform::jsx": {
2261
2017
  language: "jsx",
2262
2018
  description: "Pre-render transformed content as an image using React Email Img",
@@ -2274,6 +2030,7 @@ h1 {
2274
2030
  style={{ display: "block", border: "0" }}
2275
2031
  />`
2276
2032
  },
2033
+ // ── animation (JSX) ──────────────────────────────────────────────
2277
2034
  "animation::jsx": {
2278
2035
  language: "jsx",
2279
2036
  description: "Replace CSS animation with a React Email Img using an animated GIF",
@@ -2289,6 +2046,7 @@ h1 {
2289
2046
  style={{ display: "inline-block", border: "0" }}
2290
2047
  />`
2291
2048
  },
2049
+ // ── transition (JSX) ─────────────────────────────────────────────
2292
2050
  "transition::jsx": {
2293
2051
  language: "jsx",
2294
2052
  description: "Transitions don't work in email \u2014 style the default state well",
@@ -2318,6 +2076,7 @@ h1 {
2318
2076
  Click
2319
2077
  </Button>`
2320
2078
  },
2079
+ // ── overflow (JSX) ───────────────────────────────────────────────
2321
2080
  "overflow::jsx": {
2322
2081
  language: "jsx",
2323
2082
  description: "Content will always be visible \u2014 design for full content display",
@@ -2332,6 +2091,7 @@ h1 {
2332
2091
  <Link href="https://example.com/full">Read more</Link>
2333
2092
  </div>`
2334
2093
  },
2094
+ // ── visibility (JSX) ─────────────────────────────────────────────
2335
2095
  "visibility::jsx": {
2336
2096
  language: "jsx",
2337
2097
  description: "Use font-size/max-height trick instead of visibility:hidden",
@@ -2354,6 +2114,7 @@ h1 {
2354
2114
  Preheader text
2355
2115
  </div>`
2356
2116
  },
2117
+ // ── object-fit (JSX) ─────────────────────────────────────────────
2357
2118
  "object-fit::jsx": {
2358
2119
  language: "jsx",
2359
2120
  description: "Use React Email Img with explicit width/height instead of object-fit",
@@ -2372,6 +2133,7 @@ h1 {
2372
2133
  style={{ display: "block", border: "0" }}
2373
2134
  />`
2374
2135
  },
2136
+ // ── background-size (JSX) ────────────────────────────────────────
2375
2137
  "background-size::jsx": {
2376
2138
  language: "jsx",
2377
2139
  description: "Outlook ignores background-size \u2014 use sized images instead",
@@ -2391,6 +2153,7 @@ h1 {
2391
2153
  style={{ display: "block", width: "100%", border: "0" }}
2392
2154
  />`
2393
2155
  },
2156
+ // ── box-sizing (JSX) ─────────────────────────────────────────────
2394
2157
  "box-sizing::jsx": {
2395
2158
  language: "jsx",
2396
2159
  description: "Account for padding in width manually (no box-sizing support)",
@@ -2409,162 +2172,291 @@ h1 {
2409
2172
  </div>`
2410
2173
  }
2411
2174
  };
2412
- function getCodeFix(property, clientId, framework) {
2413
- const clientPrefix = getClientPrefix(clientId);
2414
- if (framework && clientPrefix) {
2415
- const tier1 = FIX_DATABASE[`${property}::${clientPrefix}::${framework}`];
2416
- if (tier1) return tier1;
2417
- }
2418
- if (framework) {
2419
- const tier2 = FIX_DATABASE[`${property}::${framework}`];
2420
- if (tier2) return tier2;
2421
- }
2422
- if (clientPrefix) {
2423
- const tier3 = FIX_DATABASE[`${property}::${clientPrefix}`];
2424
- if (tier3) return tier3;
2175
+
2176
+ // src/fix-snippets/mjml-fixes.ts
2177
+ var MJML_FIX_DATABASE = {
2178
+ // ── word-break (MJML) ────────────────────────────────────────────────
2179
+ "word-break::mjml": {
2180
+ language: "mjml",
2181
+ description: "MJML renders text in table cells by default \u2014 word-break works via mj-text",
2182
+ before: `<mj-text>
2183
+ <span style="word-break: break-all;">Long URL here</span>
2184
+ </mj-text>`,
2185
+ after: `<!-- mj-text already renders inside a <td>, so add word-break
2186
+ to the mj-text css-class or inline style -->
2187
+ <mj-text css-class="break-words"
2188
+ padding="0">
2189
+ Long URL here
2190
+ </mj-text>
2191
+ <mj-style>
2192
+ .break-words td { word-break: break-all; word-wrap: break-word; }
2193
+ </mj-style>`
2194
+ },
2195
+ // ── @font-face (MJML) ───────────────────────────────────────────────
2196
+ "@font-face::mjml": {
2197
+ language: "mjml",
2198
+ description: "Use mj-font in mj-head instead of @font-face",
2199
+ before: `<mj-style>
2200
+ @font-face {
2201
+ font-family: 'CustomFont';
2202
+ src: url('https://example.com/custom.woff2') format('woff2');
2425
2203
  }
2426
- return FIX_DATABASE[property];
2427
- }
2428
- function isCodeFixGenericFallback(property, clientId, framework) {
2429
- if (!framework) return false;
2430
- const clientPrefix = getClientPrefix(clientId);
2431
- if (clientPrefix && FIX_DATABASE[`${property}::${clientPrefix}::${framework}`]) return false;
2432
- if (FIX_DATABASE[`${property}::${framework}`]) return false;
2433
- return true;
2434
- }
2435
- function getClientPrefix(clientId) {
2436
- if (clientId.startsWith("outlook-windows")) return "outlook";
2437
- if (clientId.startsWith("outlook")) return null;
2438
- if (clientId.startsWith("gmail")) return "gmail";
2439
- if (clientId.startsWith("apple-mail")) return "apple";
2440
- if (clientId === "yahoo-mail") return "yahoo";
2441
- if (clientId === "samsung-mail") return "samsung";
2442
- return null;
2443
- }
2444
- var SUGGESTION_DATABASE = {
2204
+ </mj-style>`,
2205
+ after: `<mjml>
2206
+ <mj-head>
2207
+ <mj-font name="CustomFont"
2208
+ href="https://fonts.googleapis.com/css2?family=CustomFont" />
2209
+ <mj-attributes>
2210
+ <mj-all font-family="CustomFont, Arial, Helvetica, sans-serif" />
2211
+ </mj-attributes>
2212
+ </mj-head>
2213
+ </mjml>`
2214
+ },
2215
+ // ── <style> (Gmail MJML) ────────────────────────────────────────────
2216
+ "<style>::gmail::mjml": {
2217
+ language: "mjml",
2218
+ description: "Use mj-style inline='inline' to force style inlining for Gmail",
2219
+ before: `<mj-head>
2220
+ <mj-style>
2221
+ .custom { color: #6d28d9; }
2222
+ </mj-style>
2223
+ </mj-head>`,
2224
+ after: `<mj-head>
2225
+ <!-- Use inline="inline" to force MJML to inline these styles.
2226
+ Class-based styles in a plain mj-style block will be stripped by Gmail. -->
2227
+ <mj-style inline="inline">
2228
+ .custom { color: #6d28d9; }
2229
+ </mj-style>
2230
+ </mj-head>`
2231
+ },
2232
+ // ── border-radius (Outlook MJML) ────────────────────────────────────
2233
+ "border-radius::outlook::mjml": {
2234
+ language: "mjml",
2235
+ description: "MJML limitation: border-radius is unsupported in Outlook \u2014 MJML does not generate VML",
2236
+ before: `<mj-button border-radius="6px" background-color="#6d28d9">
2237
+ Click Here
2238
+ </mj-button>`,
2239
+ after: `<!-- Known MJML limitation: MJML does not generate VML for rounded corners.
2240
+ Options: accept flat corners, use mj-raw for VML, or set border-radius="0". -->
2241
+ <mj-button border-radius="0" background-color="#6d28d9">
2242
+ Click Here
2243
+ </mj-button>`
2244
+ },
2245
+ // ── background-image (Outlook MJML) ─────────────────────────────────
2246
+ "background-image::outlook::mjml": {
2247
+ language: "mjml",
2248
+ description: "Use mj-section background-url for Outlook-compatible background images",
2249
+ before: `<mj-section>
2250
+ <mj-column>
2251
+ <mj-image src="hero.jpg" />
2252
+ </mj-column>
2253
+ </mj-section>`,
2254
+ after: `<!-- MJML generates VML-compatible markup automatically via background-url on mj-section. -->
2255
+ <mj-section background-url="https://example.com/hero.jpg"
2256
+ background-size="cover"
2257
+ background-repeat="no-repeat"
2258
+ background-color="#333333">
2259
+ <mj-column>
2260
+ <mj-text color="#ffffff">Your content here</mj-text>
2261
+ </mj-column>
2262
+ </mj-section>`
2263
+ },
2264
+ // ── display:flex (MJML) ─────────────────────────────────────────────
2265
+ "display:flex::mjml": {
2266
+ language: "mjml",
2267
+ description: "Replace flexbox (from mj-raw or inline styles) with mj-section and mj-column",
2268
+ before: `<mj-raw>
2269
+ <div style="display: flex; gap: 16px;">
2270
+ <div style="flex: 1;">Column 1</div>
2271
+ <div style="flex: 1;">Column 2</div>
2272
+ </div>
2273
+ </mj-raw>`,
2274
+ after: `<!-- Flexbox in MJML is not Outlook-compatible.
2275
+ Use mj-section and mj-column \u2014 MJML compiles these to table-based layouts. -->
2276
+ <mj-section>
2277
+ <mj-column width="50%">
2278
+ <mj-text>Column 1</mj-text>
2279
+ </mj-column>
2280
+ <mj-column width="50%">
2281
+ <mj-text>Column 2</mj-text>
2282
+ </mj-column>
2283
+ </mj-section>`
2284
+ },
2285
+ // ── @media (MJML) ──────────────────────────────────────────────────
2286
+ "@media::mjml": {
2287
+ language: "mjml",
2288
+ description: "Use MJML responsive attributes and breakpoints instead of hand-written @media",
2289
+ before: `<mj-style>
2290
+ @media (max-width: 600px) {
2291
+ .mobile-stack { display: block !important; width: 100% !important; }
2292
+ }
2293
+ </mj-style>`,
2294
+ after: `<!-- MJML generates responsive @media queries automatically.
2295
+ Use mj-breakpoint and mj-column widths to control responsive behavior. -->
2296
+ <mj-head>
2297
+ <mj-breakpoint width="600px" />
2298
+ </mj-head>
2299
+ <mj-body>
2300
+ <mj-section>
2301
+ <mj-column width="50%"><mj-text>Left</mj-text></mj-column>
2302
+ <mj-column width="50%"><mj-text>Right</mj-text></mj-column>
2303
+ </mj-section>
2304
+ </mj-body>`
2305
+ }
2306
+ };
2307
+
2308
+ // src/fix-snippets/maizzle-fixes.ts
2309
+ var MAIZZLE_FIX_DATABASE = {
2310
+ // ── display:flex (Outlook Maizzle) ──────────────────────────────────
2311
+ "display:flex::outlook::maizzle": {
2312
+ language: "maizzle",
2313
+ description: "Replace Tailwind flex classes with HTML table + MSO conditional comments",
2314
+ before: `<div class="flex gap-4">
2315
+ <div class="flex-1">Column 1</div>
2316
+ <div class="flex-1">Column 2</div>
2317
+ </div>`,
2318
+ after: `<!--[if mso]>
2319
+ <table role="presentation" width="100%" cellpadding="0"
2320
+ cellspacing="0" border="0"><tr>
2321
+ <td class="w-1/2" valign="top">Column 1</td>
2322
+ <td class="w-1/2" valign="top">Column 2</td>
2323
+ </tr></table>
2324
+ <![endif]-->
2325
+ <!--[if !mso]><!-->
2326
+ <div class="flex gap-4">
2327
+ <div class="flex-1">Column 1</div>
2328
+ <div class="flex-1">Column 2</div>
2329
+ </div>
2330
+ <!--<![endif]-->`
2331
+ },
2332
+ // ── @font-face (Maizzle) ────────────────────────────────────────────
2333
+ "@font-face::maizzle": {
2334
+ language: "maizzle",
2335
+ description: 'Add fonts via the googleFonts key in config.js \u2014 Maizzle injects the Google Fonts link tag automatically. Set googleFonts: "Inter:ital,wght@0,400;0,700" in your environment config, then reference the font family in your template.',
2336
+ before: `<style>
2337
+ @font-face {
2338
+ font-family: 'Inter';
2339
+ src: url('https://fonts.gstatic.com/...') format('woff2');
2340
+ }
2341
+ </style>`,
2342
+ after: `<!-- config.js: googleFonts: "Inter:ital,wght@0,400;0,700" -->
2343
+ <p class="font-['Inter',Arial,sans-serif]">Hello</p>`
2344
+ },
2345
+ // ── <style> (Gmail Maizzle) ─────────────────────────────────────────
2346
+ "<style>::gmail::maizzle": {
2347
+ language: "maizzle",
2348
+ description: "Maizzle automatically inlines CSS via juice during build (inlineCSS: true in config.js). Manual <style> blocks bypass juice and will be stripped by Gmail \u2014 prefer Tailwind utility classes instead.",
2349
+ before: `<style>
2350
+ .custom { color: #6d28d9; }
2351
+ </style>
2352
+ <div class="custom">Hello</div>`,
2353
+ after: `<!-- Prefer Tailwind classes \u2014 Maizzle inlines them automatically during build -->
2354
+ <div class="text-[#6d28d9]">Hello</div>`
2355
+ },
2356
+ // ── max-width (Outlook Maizzle) ─────────────────────────────────────
2357
+ "max-width::outlook::maizzle": {
2358
+ language: "maizzle",
2359
+ description: "Wrap max-width containers with MSO conditional table for Outlook",
2360
+ before: `<div class="max-w-[600px] mx-auto">
2361
+ Content here
2362
+ </div>`,
2363
+ after: `<!--[if mso]>
2364
+ <table role="presentation" width="600" cellpadding="0"
2365
+ cellspacing="0" border="0" align="center"><tr><td>
2366
+ <![endif]-->
2367
+ <div class="max-w-[600px] mx-auto">
2368
+ Content here
2369
+ </div>
2370
+ <!--[if mso]>
2371
+ </td></tr></table>
2372
+ <![endif]-->`
2373
+ },
2374
+ // ── gap (Maizzle) ──────────────────────────────────────────────────
2375
+ "gap::maizzle": {
2376
+ language: "maizzle",
2377
+ description: "Use padding Tailwind classes on child elements instead of gap",
2378
+ before: `<div class="flex gap-4">
2379
+ <div>Item 1</div>
2380
+ <div>Item 2</div>
2381
+ <div>Item 3</div>
2382
+ </div>`,
2383
+ after: `<!-- gap is not supported in Outlook or many email clients.
2384
+ Use padding classes on child elements instead. -->
2385
+ <table role="presentation" cellpadding="0" cellspacing="0" border="0">
2386
+ <tr>
2387
+ <td class="pr-4">Item 1</td>
2388
+ <td class="pr-4">Item 2</td>
2389
+ <td>Item 3</td>
2390
+ </tr>
2391
+ </table>`
2392
+ }
2393
+ };
2394
+
2395
+ // src/fix-snippets/html-suggestions.ts
2396
+ var HTML_SUGGESTION_DATABASE = {
2445
2397
  // ── <style> ───────────────────────────────────────────────────────────
2446
2398
  "<style>": "Use a CSS inliner tool (like juice) to move styles to inline attributes.",
2447
2399
  "<style>:partial": "Use inline styles as the primary approach, with <style> in <head> as progressive enhancement.",
2448
- "<style>::jsx": "Move styles to inline style props \u2014 React Email components accept style objects directly.",
2449
- "<style>:partial::jsx": "Use inline style props on React Email components. Reserve <style> in <Head> for progressive enhancement only.",
2450
- "<style>::mjml": 'Use mj-style inline="inline" to force MJML to inline styles for Gmail compatibility.',
2451
- "<style>:partial::mjml": 'Use mj-style inline="inline" for critical styles; plain mj-style for progressive enhancement.',
2452
- "<style>::maizzle": "Prefer Tailwind utility classes \u2014 Maizzle inlines CSS via juice during build (inlineCSS: true in config.js).",
2453
- "<style>:partial::maizzle": "Use Tailwind utility classes for critical styles. Maizzle automatically inlines them at build time.",
2454
2400
  // ── <link> ────────────────────────────────────────────────────────────
2455
2401
  "<link>": "Inline all CSS directly in the HTML.",
2456
- "<link>::jsx": "Use the React Email <Head> component for font imports; place all other styles inline via style props.",
2457
- "<link>::mjml": "MJML does not support external stylesheets. Use mj-style or inline attributes.",
2458
- "<link>::maizzle": "External stylesheets are stripped. Use Tailwind CSS classes \u2014 Maizzle inlines them at build time.",
2459
2402
  // ── <svg> ─────────────────────────────────────────────────────────────
2460
2403
  "<svg>": "Convert SVGs to PNG/JPG images.",
2461
- "<svg>::jsx": "Replace inline SVG with the React Email <Img> component pointing to a hosted PNG.",
2462
- "<svg>::mjml": "Replace inline SVG with an mj-image component pointing to a hosted PNG.",
2463
- "<svg>::maizzle": "Replace inline SVG with an <img> tag pointing to a hosted PNG.",
2464
2404
  // ── <video> ───────────────────────────────────────────────────────────
2465
2405
  "<video>": "Use an animated GIF or a static image with a play button linking to the video.",
2466
- "<video>::jsx": "Replace <video> with a React Email <Link> wrapping an <Img> thumbnail.",
2467
- "<video>::mjml": "Replace <video> with an mj-image linking to a video thumbnail.",
2468
- "<video>::maizzle": "Replace <video> with a linked image thumbnail.",
2469
2406
  // ── <form> ────────────────────────────────────────────────────────────
2470
2407
  "<form>": "Use links to a web form instead of embedding forms in email.",
2471
- "<form>::jsx": "Replace the form with a React Email <Button> or <Link> component pointing to a hosted form page.",
2472
- "<form>::mjml": "Replace the form with an mj-button linking to a hosted form page.",
2473
- "<form>::maizzle": "Replace the form with a CTA link/button pointing to a hosted form page.",
2474
2408
  // ── @font-face ────────────────────────────────────────────────────────
2475
2409
  "@font-face": "Always include a web-safe font stack as fallback (e.g., Arial, Helvetica, sans-serif).",
2476
- "@font-face::jsx": "Use the React Email <Font> component in <Head> with a fallbackFontFamily prop.",
2477
- "@font-face::mjml": "Use mj-font in mj-head instead of @font-face in mj-style.",
2478
- "@font-face::maizzle": "Use the googleFonts key in config.js \u2014 Maizzle injects the Google Fonts link tag automatically.",
2479
2410
  // ── @media ────────────────────────────────────────────────────────────
2480
2411
  "@media": "Design emails mobile-first with a single-column layout that works without media queries.",
2481
- "@media::jsx": "Use a single-column layout with React Email <Container> and <Section>. Avoid relying on @media queries.",
2482
- "@media::mjml": "MJML generates responsive @media queries automatically. Use mj-breakpoint and mj-column widths.",
2483
- "@media::maizzle": "Use Tailwind responsive utility classes and Maizzle's breakpoints config instead of hand-written @media.",
2484
2412
  // ── display:flex ──────────────────────────────────────────────────────
2485
2413
  "display:flex": "Use <table> layouts for email client compatibility.",
2486
2414
  "display:flex::outlook": "Use <table> layouts with <!--[if mso]> conditional comments for Outlook's Word engine.",
2487
- "display:flex::jsx": "Use React Email <Row> and <Column> components instead of flexbox.",
2488
- "display:flex::mjml": "Use mj-section and mj-column \u2014 MJML compiles these to table-based layouts.",
2489
- "display:flex::maizzle": "Replace Tailwind flex classes with HTML table + MSO conditional comments for Outlook.",
2490
2415
  // ── display:grid ──────────────────────────────────────────────────────
2491
2416
  "display:grid": "Replace CSS Grid with table layout for email compatibility.",
2492
- "display:grid::jsx": "Use React Email <Row> and <Column> components instead of CSS Grid.",
2493
- "display:grid::mjml": "Use mj-section and mj-column for grid-like layouts.",
2494
- "display:grid::maizzle": "Replace Tailwind grid classes with HTML table layout for email compatibility.",
2495
2417
  // ── linear-gradient ───────────────────────────────────────────────────
2496
2418
  "linear-gradient": "Add a solid background-color fallback before the gradient.",
2497
- "linear-gradient::jsx": "Add a solid backgroundColor style prop as fallback before the gradient.",
2498
- "linear-gradient::mjml": "Add a background-color attribute on mj-section as a fallback.",
2499
- "linear-gradient::maizzle": "Add a bg-[color] Tailwind class as a fallback before the gradient.",
2500
2419
  // ── box-shadow ────────────────────────────────────────────────────────
2501
2420
  "box-shadow": "Use border styling as an alternative to box-shadow.",
2502
- "box-shadow::jsx": "Use a border style prop as an alternative to boxShadow.",
2503
- "box-shadow::mjml": "Use a border attribute on mj-section or mj-column as an alternative.",
2504
- "box-shadow::maizzle": "Use Tailwind border classes as an alternative to shadow classes.",
2505
2421
  // ── border-radius ─────────────────────────────────────────────────────
2506
2422
  "border-radius": "Use VML for rounded corners in Outlook, or accept square corners.",
2507
2423
  "border-radius::outlook": "Use VML (Vector Markup Language) for rounded buttons in Outlook.",
2508
- "border-radius::jsx": "Outlook ignores borderRadius. Use dangerouslySetInnerHTML with VML for rounded buttons if needed.",
2509
- "border-radius::mjml": 'MJML does not generate VML \u2014 border-radius will not render in Outlook. Set border-radius="0" or accept flat corners.',
2510
- "border-radius::maizzle": "Outlook ignores border-radius. Accept flat corners or use MSO conditional VML.",
2511
2424
  // ── max-width ─────────────────────────────────────────────────────────
2512
2425
  "max-width": "Use a fixed-width table wrapper for maximum compatibility.",
2513
2426
  "max-width::outlook": "Use a fixed width on table cells instead of max-width.",
2514
- "max-width::jsx": "Use the React Email <Container> component which handles max-width across clients.",
2515
- "max-width::mjml": "Set the width attribute on mj-body or mj-section for maximum compatibility.",
2516
- "max-width::maizzle": "Wrap max-w containers with MSO conditional table for Outlook.",
2517
2427
  // ── gap ───────────────────────────────────────────────────────────────
2518
2428
  "gap": "Use padding/margin on child elements instead of gap.",
2519
2429
  "gap::outlook": "Use cellpadding/cellspacing on tables, or padding on cells.",
2520
- "gap::jsx": "Use padding style props on <Column> components instead of gap.",
2521
- "gap::mjml": "Use padding attribute on mj-column or mj-text for spacing.",
2522
- "gap::maizzle": "Use Tailwind padding classes on child elements instead of gap.",
2523
2430
  // ── float ─────────────────────────────────────────────────────────────
2524
2431
  "float": "Use table cells with align attribute for side-by-side content.",
2525
2432
  "float::outlook": 'Use table cells with align="left" or align="right".',
2526
- "float::jsx": "Use React Email <Row> and <Column> components for side-by-side layout.",
2527
- "float::mjml": "Use mj-section with multiple mj-column elements for side-by-side layout.",
2528
- "float::maizzle": "Use HTML tables for side-by-side layout instead of Tailwind float classes.",
2529
2433
  // ── background-image ──────────────────────────────────────────────────
2530
2434
  "background-image": "Use VML for background images in clients that require it.",
2531
2435
  "background-image::outlook": "Use <!--[if gte mso 9]> with <v:background> VML for Outlook background images.",
2532
- "background-image::jsx": "Use VML via dangerouslySetInnerHTML for Outlook background images.",
2533
- "background-image::mjml": "Use background-url attribute on mj-section \u2014 MJML generates VML automatically.",
2534
- "background-image::maizzle": "Use MSO conditional VML for Outlook background images.",
2535
2436
  // ── position ──────────────────────────────────────────────────────────
2536
2437
  "position": "Use table-based positioning instead of CSS position.",
2537
- "position::jsx": "Use React Email <Row> and <Column> components for positioning.",
2538
- "position::mjml": "Use mj-section and mj-column for layout positioning.",
2539
- "position::maizzle": "Use HTML table layout instead of Tailwind position classes.",
2540
2438
  // ── opacity ───────────────────────────────────────────────────────────
2541
2439
  "opacity": "Use solid colors instead of opacity.",
2542
- "opacity::jsx": "Use solid colors. Opacity is not supported in many email clients.",
2543
- "opacity::mjml": "Use solid colors. Most email clients don't support opacity.",
2544
- "opacity::maizzle": "Use solid Tailwind color classes instead of opacity.",
2545
- // ── word-break ──────────────────────────────────────────────────────
2440
+ // ── word-break ────────────────────────────────────────────────────────
2546
2441
  "word-break": "Wrap long text in a <table><td> to force wrapping in clients that don't support word-break.",
2547
2442
  "word-break::outlook": "Outlook's Word engine ignores word-break. Place text inside a <td> with a constrained width \u2014 tables always wrap.",
2548
- "word-break::jsx": "Wrap long text in a <table><tr><td> element. Outlook ignores wordBreak but respects table cell widths.",
2549
- "word-break::mjml": "mj-text renders inside a <td>, which helps. Add word-wrap: break-word to the td via mj-style.",
2550
- // ── overflow-wrap ──────────────────────────────────────────────────
2443
+ // ── overflow-wrap ─────────────────────────────────────────────────────
2551
2444
  "overflow-wrap": "Wrap text in a <table><td> to force wrapping. overflow-wrap is ignored by Outlook and unreliable in Yahoo.",
2552
- "overflow-wrap::jsx": "Wrap text in a <table><tr><td> element. Outlook ignores overflowWrap but respects table cell widths.",
2553
- // ── white-space ────────────────────────────────────────────────────
2445
+ // ── white-space ───────────────────────────────────────────────────────
2554
2446
  "white-space": "Outlook only supports 'normal' and 'nowrap'. Use &nbsp; for non-breaking spaces.",
2555
- // ── text-overflow ──────────────────────────────────────────────────
2447
+ // ── text-overflow ─────────────────────────────────────────────────────
2556
2448
  "text-overflow": "text-overflow requires overflow:hidden which is stripped by Gmail. Truncate content server-side.",
2557
- // ── vertical-align ─────────────────────────────────────────────────
2449
+ // ── vertical-align ────────────────────────────────────────────────────
2558
2450
  "vertical-align": 'Use the valign HTML attribute on <td> elements for Outlook (e.g., valign="top").',
2559
- // ── border-spacing ─────────────────────────────────────────────────
2451
+ // ── border-spacing ────────────────────────────────────────────────────
2560
2452
  "border-spacing": 'Use the cellspacing HTML attribute instead (e.g., <table cellspacing="8">).',
2561
- // ── min-width / min-height ─────────────────────────────────────────
2453
+ // ── min-width / min-height ────────────────────────────────────────────
2562
2454
  "min-width": "Outlook ignores min-width. Use a fixed width attribute on <td> or <table>.",
2563
2455
  "min-height": "Outlook ignores min-height. Use a fixed height or a spacer image.",
2564
2456
  "max-height": "Outlook ignores max-height. Truncate content server-side or use a fixed height.",
2565
- // ── text-shadow ────────────────────────────────────────────────────
2457
+ // ── text-shadow ───────────────────────────────────────────────────────
2566
2458
  "text-shadow": "text-shadow is stripped by Gmail, Outlook, and Yahoo. Use font-weight for emphasis.",
2567
- // ── background-size / background-position ──────────────────────────
2459
+ // ── background-size / background-position ─────────────────────────────
2568
2460
  "background-size": "Not supported in many clients. Set image dimensions directly.",
2569
2461
  "background-position": "Not supported in many clients. Use VML for positioning.",
2570
2462
  // ── Additional properties covered by transform helpers ────────────────
@@ -2577,6 +2469,171 @@ var SUGGESTION_DATABASE = {
2577
2469
  "object-fit": "Use width/height attributes on <img> directly.",
2578
2470
  "display": "Use tables for layout in email clients."
2579
2471
  };
2472
+
2473
+ // src/fix-snippets/jsx-suggestions.ts
2474
+ var JSX_SUGGESTION_DATABASE = {
2475
+ // ── <style> ───────────────────────────────────────────────────────────
2476
+ "<style>::jsx": "Move styles to inline style props \u2014 React Email components accept style objects directly.",
2477
+ "<style>:partial::jsx": "Use inline style props on React Email components. Reserve <style> in <Head> for progressive enhancement only.",
2478
+ // ── <link> ────────────────────────────────────────────────────────────
2479
+ "<link>::jsx": "Use the React Email <Head> component for font imports; place all other styles inline via style props.",
2480
+ // ── <svg> ─────────────────────────────────────────────────────────────
2481
+ "<svg>::jsx": "Replace inline SVG with the React Email <Img> component pointing to a hosted PNG.",
2482
+ // ── <video> ───────────────────────────────────────────────────────────
2483
+ "<video>::jsx": "Replace <video> with a React Email <Link> wrapping an <Img> thumbnail.",
2484
+ // ── <form> ────────────────────────────────────────────────────────────
2485
+ "<form>::jsx": "Replace the form with a React Email <Button> or <Link> component pointing to a hosted form page.",
2486
+ // ── @font-face ────────────────────────────────────────────────────────
2487
+ "@font-face::jsx": "Use the React Email <Font> component in <Head> with a fallbackFontFamily prop.",
2488
+ // ── @media ────────────────────────────────────────────────────────────
2489
+ "@media::jsx": "Use a single-column layout with React Email <Container> and <Section>. Avoid relying on @media queries.",
2490
+ // ── display:flex ──────────────────────────────────────────────────────
2491
+ "display:flex::jsx": "Use React Email <Row> and <Column> components instead of flexbox.",
2492
+ // ── display:grid ──────────────────────────────────────────────────────
2493
+ "display:grid::jsx": "Use React Email <Row> and <Column> components instead of CSS Grid.",
2494
+ // ── linear-gradient ───────────────────────────────────────────────────
2495
+ "linear-gradient::jsx": "Add a solid backgroundColor style prop as fallback before the gradient.",
2496
+ // ── box-shadow ────────────────────────────────────────────────────────
2497
+ "box-shadow::jsx": "Use a border style prop as an alternative to boxShadow.",
2498
+ // ── border-radius ─────────────────────────────────────────────────────
2499
+ "border-radius::jsx": "Outlook ignores borderRadius. Use dangerouslySetInnerHTML with VML for rounded buttons if needed.",
2500
+ // ── max-width ─────────────────────────────────────────────────────────
2501
+ "max-width::jsx": "Use the React Email <Container> component which handles max-width across clients.",
2502
+ // ── gap ───────────────────────────────────────────────────────────────
2503
+ "gap::jsx": "Use padding style props on <Column> components instead of gap.",
2504
+ // ── float ─────────────────────────────────────────────────────────────
2505
+ "float::jsx": "Use React Email <Row> and <Column> components for side-by-side layout.",
2506
+ // ── background-image ──────────────────────────────────────────────────
2507
+ "background-image::jsx": "Use VML via dangerouslySetInnerHTML for Outlook background images.",
2508
+ // ── position ──────────────────────────────────────────────────────────
2509
+ "position::jsx": "Use React Email <Row> and <Column> components for positioning.",
2510
+ // ── opacity ───────────────────────────────────────────────────────────
2511
+ "opacity::jsx": "Use solid colors. Opacity is not supported in many email clients.",
2512
+ // ── word-break ────────────────────────────────────────────────────────
2513
+ "word-break::jsx": "Wrap long text in a <table><tr><td> element. Outlook ignores wordBreak but respects table cell widths.",
2514
+ // ── overflow-wrap ─────────────────────────────────────────────────────
2515
+ "overflow-wrap::jsx": "Wrap text in a <table><tr><td> element. Outlook ignores overflowWrap but respects table cell widths."
2516
+ };
2517
+
2518
+ // src/fix-snippets/mjml-suggestions.ts
2519
+ var MJML_SUGGESTION_DATABASE = {
2520
+ // ── <style> ───────────────────────────────────────────────────────────
2521
+ "<style>::mjml": 'Use mj-style inline="inline" to force MJML to inline styles for Gmail compatibility.',
2522
+ "<style>:partial::mjml": 'Use mj-style inline="inline" for critical styles; plain mj-style for progressive enhancement.',
2523
+ // ── <link> ────────────────────────────────────────────────────────────
2524
+ "<link>::mjml": "MJML does not support external stylesheets. Use mj-style or inline attributes.",
2525
+ // ── <svg> ─────────────────────────────────────────────────────────────
2526
+ "<svg>::mjml": "Replace inline SVG with an mj-image component pointing to a hosted PNG.",
2527
+ // ── <video> ───────────────────────────────────────────────────────────
2528
+ "<video>::mjml": "Replace <video> with an mj-image linking to a video thumbnail.",
2529
+ // ── <form> ────────────────────────────────────────────────────────────
2530
+ "<form>::mjml": "Replace the form with an mj-button linking to a hosted form page.",
2531
+ // ── @font-face ────────────────────────────────────────────────────────
2532
+ "@font-face::mjml": "Use mj-font in mj-head instead of @font-face in mj-style.",
2533
+ // ── @media ────────────────────────────────────────────────────────────
2534
+ "@media::mjml": "MJML generates responsive @media queries automatically. Use mj-breakpoint and mj-column widths.",
2535
+ // ── display:flex ──────────────────────────────────────────────────────
2536
+ "display:flex::mjml": "Use mj-section and mj-column \u2014 MJML compiles these to table-based layouts.",
2537
+ // ── display:grid ──────────────────────────────────────────────────────
2538
+ "display:grid::mjml": "Use mj-section and mj-column for grid-like layouts.",
2539
+ // ── linear-gradient ───────────────────────────────────────────────────
2540
+ "linear-gradient::mjml": "Add a background-color attribute on mj-section as a fallback.",
2541
+ // ── box-shadow ────────────────────────────────────────────────────────
2542
+ "box-shadow::mjml": "Use a border attribute on mj-section or mj-column as an alternative.",
2543
+ // ── border-radius ─────────────────────────────────────────────────────
2544
+ "border-radius::mjml": 'MJML does not generate VML \u2014 border-radius will not render in Outlook. Set border-radius="0" or accept flat corners.',
2545
+ // ── max-width ─────────────────────────────────────────────────────────
2546
+ "max-width::mjml": "Set the width attribute on mj-body or mj-section for maximum compatibility.",
2547
+ // ── gap ───────────────────────────────────────────────────────────────
2548
+ "gap::mjml": "Use padding attribute on mj-column or mj-text for spacing.",
2549
+ // ── float ─────────────────────────────────────────────────────────────
2550
+ "float::mjml": "Use mj-section with multiple mj-column elements for side-by-side layout.",
2551
+ // ── background-image ──────────────────────────────────────────────────
2552
+ "background-image::mjml": "Use background-url attribute on mj-section \u2014 MJML generates VML automatically.",
2553
+ // ── position ──────────────────────────────────────────────────────────
2554
+ "position::mjml": "Use mj-section and mj-column for layout positioning.",
2555
+ // ── opacity ───────────────────────────────────────────────────────────
2556
+ "opacity::mjml": "Use solid colors. Most email clients don't support opacity.",
2557
+ // ── word-break ────────────────────────────────────────────────────────
2558
+ "word-break::mjml": "mj-text renders inside a <td>, which helps. Add word-wrap: break-word to the td via mj-style."
2559
+ };
2560
+
2561
+ // src/fix-snippets/maizzle-suggestions.ts
2562
+ var MAIZZLE_SUGGESTION_DATABASE = {
2563
+ // ── <style> ───────────────────────────────────────────────────────────
2564
+ "<style>::maizzle": "Prefer Tailwind utility classes \u2014 Maizzle inlines CSS via juice during build (inlineCSS: true in config.js).",
2565
+ "<style>:partial::maizzle": "Use Tailwind utility classes for critical styles. Maizzle automatically inlines them at build time.",
2566
+ // ── <link> ────────────────────────────────────────────────────────────
2567
+ "<link>::maizzle": "External stylesheets are stripped. Use Tailwind CSS classes \u2014 Maizzle inlines them at build time.",
2568
+ // ── <svg> ─────────────────────────────────────────────────────────────
2569
+ "<svg>::maizzle": "Replace inline SVG with an <img> tag pointing to a hosted PNG.",
2570
+ // ── <video> ───────────────────────────────────────────────────────────
2571
+ "<video>::maizzle": "Replace <video> with a linked image thumbnail.",
2572
+ // ── <form> ────────────────────────────────────────────────────────────
2573
+ "<form>::maizzle": "Replace the form with a CTA link/button pointing to a hosted form page.",
2574
+ // ── @font-face ────────────────────────────────────────────────────────
2575
+ "@font-face::maizzle": "Use the googleFonts key in config.js \u2014 Maizzle injects the Google Fonts link tag automatically.",
2576
+ // ── @media ────────────────────────────────────────────────────────────
2577
+ "@media::maizzle": "Use Tailwind responsive utility classes and Maizzle's breakpoints config instead of hand-written @media.",
2578
+ // ── display:flex ──────────────────────────────────────────────────────
2579
+ "display:flex::maizzle": "Replace Tailwind flex classes with HTML table + MSO conditional comments for Outlook.",
2580
+ // ── display:grid ──────────────────────────────────────────────────────
2581
+ "display:grid::maizzle": "Replace Tailwind grid classes with HTML table layout for email compatibility.",
2582
+ // ── linear-gradient ───────────────────────────────────────────────────
2583
+ "linear-gradient::maizzle": "Add a bg-[color] Tailwind class as a fallback before the gradient.",
2584
+ // ── box-shadow ────────────────────────────────────────────────────────
2585
+ "box-shadow::maizzle": "Use Tailwind border classes as an alternative to shadow classes.",
2586
+ // ── border-radius ─────────────────────────────────────────────────────
2587
+ "border-radius::maizzle": "Outlook ignores border-radius. Accept flat corners or use MSO conditional VML.",
2588
+ // ── max-width ─────────────────────────────────────────────────────────
2589
+ "max-width::maizzle": "Wrap max-w containers with MSO conditional table for Outlook.",
2590
+ // ── gap ───────────────────────────────────────────────────────────────
2591
+ "gap::maizzle": "Use Tailwind padding classes on child elements instead of gap.",
2592
+ // ── float ─────────────────────────────────────────────────────────────
2593
+ "float::maizzle": "Use HTML tables for side-by-side layout instead of Tailwind float classes.",
2594
+ // ── background-image ──────────────────────────────────────────────────
2595
+ "background-image::maizzle": "Use MSO conditional VML for Outlook background images.",
2596
+ // ── position ──────────────────────────────────────────────────────────
2597
+ "position::maizzle": "Use HTML table layout instead of Tailwind position classes.",
2598
+ // ── opacity ───────────────────────────────────────────────────────────
2599
+ "opacity::maizzle": "Use solid Tailwind color classes instead of opacity."
2600
+ };
2601
+
2602
+ // src/fix-snippets/index.ts
2603
+ var FIX_DATABASE = __spreadValues(__spreadValues(__spreadValues(__spreadValues({}, HTML_FIX_DATABASE), JSX_FIX_DATABASE), MJML_FIX_DATABASE), MAIZZLE_FIX_DATABASE);
2604
+ var SUGGESTION_DATABASE = __spreadValues(__spreadValues(__spreadValues(__spreadValues({}, HTML_SUGGESTION_DATABASE), JSX_SUGGESTION_DATABASE), MJML_SUGGESTION_DATABASE), MAIZZLE_SUGGESTION_DATABASE);
2605
+ function getCodeFix(property, clientId, framework) {
2606
+ const clientPrefix = getClientPrefix(clientId);
2607
+ if (framework && clientPrefix) {
2608
+ const tier1 = FIX_DATABASE[`${property}::${clientPrefix}::${framework}`];
2609
+ if (tier1) return tier1;
2610
+ }
2611
+ if (framework) {
2612
+ const tier2 = FIX_DATABASE[`${property}::${framework}`];
2613
+ if (tier2) return tier2;
2614
+ }
2615
+ if (clientPrefix) {
2616
+ const tier3 = FIX_DATABASE[`${property}::${clientPrefix}`];
2617
+ if (tier3) return tier3;
2618
+ }
2619
+ return FIX_DATABASE[property];
2620
+ }
2621
+ function isCodeFixGenericFallback(property, clientId, framework) {
2622
+ if (!framework) return false;
2623
+ const clientPrefix = getClientPrefix(clientId);
2624
+ if (clientPrefix && FIX_DATABASE[`${property}::${clientPrefix}::${framework}`]) return false;
2625
+ if (FIX_DATABASE[`${property}::${framework}`]) return false;
2626
+ return true;
2627
+ }
2628
+ function getClientPrefix(clientId) {
2629
+ if (clientId.startsWith("outlook-windows")) return "outlook";
2630
+ if (clientId.startsWith("outlook")) return null;
2631
+ if (clientId.startsWith("gmail")) return "gmail";
2632
+ if (clientId.startsWith("apple-mail")) return "apple";
2633
+ if (clientId === "yahoo-mail") return "yahoo";
2634
+ if (clientId === "samsung-mail") return "samsung";
2635
+ return null;
2636
+ }
2580
2637
  function getSuggestion(property, clientId, framework) {
2581
2638
  const clientPrefix = getClientPrefix(clientId);
2582
2639
  if (framework && clientPrefix) {
@@ -2676,6 +2733,21 @@ function serializeStyle(map) {
2676
2733
  return parts.join("; ");
2677
2734
  }
2678
2735
 
2736
+ // src/constants.ts
2737
+ var MAX_HTML_SIZE = 2 * 1024 * 1024;
2738
+ var GENERIC_LINK_TEXT = /* @__PURE__ */ new Set([
2739
+ "click here",
2740
+ "here",
2741
+ "read more",
2742
+ "learn more",
2743
+ "more",
2744
+ "link",
2745
+ "this link",
2746
+ "click",
2747
+ "tap here",
2748
+ "this"
2749
+ ]);
2750
+
2679
2751
  // src/transform.ts
2680
2752
  function inlineStyles($) {
2681
2753
  const styleBlocks = [];
@@ -2720,8 +2792,38 @@ function makeWarning(base, prop, clientId, framework) {
2720
2792
  fixType: STRUCTURAL_FIX_PROPERTIES.has(prop) ? "structural" : "css"
2721
2793
  });
2722
2794
  }
2723
- function transformGmail(html, clientId, framework) {
2724
- const $ = cheerio.load(html);
2795
+ function detectAnimations($) {
2796
+ let found = false;
2797
+ $("[style]").each((_, el) => {
2798
+ const style = $(el).attr("style") || "";
2799
+ const props = parseInlineStyle(style);
2800
+ props.forEach((_2, prop) => {
2801
+ if (prop === "animation" || prop === "transition" || prop.startsWith("animation-") || prop.startsWith("transition-")) {
2802
+ found = true;
2803
+ }
2804
+ });
2805
+ });
2806
+ if (!found) {
2807
+ $("style").each((_, el) => {
2808
+ try {
2809
+ const ast = csstree.parse($(el).text());
2810
+ csstree.walk(ast, {
2811
+ enter(node) {
2812
+ if (node.type === "Declaration") {
2813
+ const prop = node.property.toLowerCase();
2814
+ if (prop === "animation" || prop === "transition" || prop.startsWith("animation-") || prop.startsWith("transition-")) {
2815
+ found = true;
2816
+ }
2817
+ }
2818
+ }
2819
+ });
2820
+ } catch (e) {
2821
+ }
2822
+ });
2823
+ }
2824
+ return found;
2825
+ }
2826
+ function gmailAdditionalChecks($, clientId, _html, framework) {
2725
2827
  const warnings = [];
2726
2828
  let hasAtFontFace = false;
2727
2829
  $("style").each((_, el) => {
@@ -2745,9 +2847,10 @@ function transformGmail(html, clientId, framework) {
2745
2847
  message: "Gmail does not support custom web fonts."
2746
2848
  }, "@font-face", clientId, framework));
2747
2849
  }
2748
- inlineStyles($);
2749
- $("style").remove();
2750
- $("link[rel='stylesheet']").remove();
2850
+ return warnings;
2851
+ }
2852
+ function gmailPostChecks($, clientId, _html, framework) {
2853
+ const warnings = [];
2751
2854
  $("*").contents().filter(function() {
2752
2855
  return this.type === "comment";
2753
2856
  }).each(function() {
@@ -2764,91 +2867,11 @@ function transformGmail(html, clientId, framework) {
2764
2867
  message: "Gmail partially supports <style> blocks (head only, 16KB limit). Inlining recommended for safety.",
2765
2868
  suggestion: styleSug.text
2766
2869
  });
2767
- $("[style]").each((_, el) => {
2768
- const style = $(el).attr("style") || "";
2769
- const props = parseInlineStyle(style);
2770
- const removed = [];
2771
- props.forEach((value, prop) => {
2772
- if (GMAIL_STRIPPED_PROPERTIES.has(prop)) {
2773
- removed.push(prop);
2774
- props.delete(prop);
2775
- }
2776
- if (prop === "display" && value.includes("grid")) {
2777
- removed.push(prop);
2778
- props.delete(prop);
2779
- }
2780
- if (prop === "background" && (value.includes("linear-gradient") || value.includes("radial-gradient"))) {
2781
- removed.push(prop);
2782
- props.delete(prop);
2783
- }
2784
- });
2785
- if (removed.length > 0) {
2786
- $(el).attr("style", serializeStyle(props));
2787
- for (const prop of removed) {
2788
- warnings.push(makeWarning({
2789
- severity: "warning",
2790
- client: clientId,
2791
- property: prop,
2792
- message: `Gmail strips "${prop}" from inline styles.`
2793
- }, prop, clientId, framework));
2794
- }
2795
- }
2796
- });
2797
- if ($("svg").length > 0) {
2798
- warnings.push(makeWarning({
2799
- severity: "error",
2800
- client: clientId,
2801
- property: "<svg>",
2802
- message: "Gmail does not support inline SVG elements."
2803
- }, "<svg>", clientId, framework));
2804
- $("svg").each((_, el) => {
2805
- $(el).replaceWith('<img alt="[SVG not supported]" />');
2806
- });
2807
- }
2808
- if ($("form").length > 0) {
2809
- warnings.push(makeWarning({
2810
- severity: "error",
2811
- client: clientId,
2812
- property: "<form>",
2813
- message: "Gmail strips all form elements."
2814
- }, "<form>", clientId, framework));
2815
- $("form").each((_, el) => {
2816
- $(el).replaceWith($(el).html() || "");
2817
- });
2818
- }
2819
- return { clientId, html: $.html(), warnings };
2870
+ return warnings;
2820
2871
  }
2821
- function transformOutlookWindows(html, clientId, framework) {
2822
- const $ = cheerio.load(html);
2872
+ function outlookWindowsAdditionalChecks($, clientId, _html, framework) {
2823
2873
  const warnings = [];
2824
- $("[style]").each((_, el) => {
2825
- const style = $(el).attr("style") || "";
2826
- const props = parseInlineStyle(style);
2827
- const removed = [];
2828
- props.forEach((value, prop) => {
2829
- if (OUTLOOK_WORD_UNSUPPORTED.has(prop)) {
2830
- removed.push(prop);
2831
- props.delete(prop);
2832
- }
2833
- if ((prop === "background" || prop === "background-image") && (value.includes("linear-gradient") || value.includes("radial-gradient"))) {
2834
- removed.push(prop);
2835
- props.delete(prop);
2836
- }
2837
- });
2838
- if (removed.length > 0) {
2839
- $(el).attr("style", serializeStyle(props));
2840
- for (const prop of removed) {
2841
- warnings.push(makeWarning({
2842
- severity: "warning",
2843
- client: clientId,
2844
- property: prop,
2845
- message: `Outlook Windows (Word engine) does not support "${prop}".`
2846
- }, prop, clientId, framework));
2847
- }
2848
- }
2849
- });
2850
- const borderRadiusElements = $("[style*='border-radius']");
2851
- if (borderRadiusElements.length > 0) {
2874
+ if ($("[style*='border-radius']").length > 0) {
2852
2875
  warnings.push(makeWarning({
2853
2876
  severity: "warning",
2854
2877
  client: clientId,
@@ -2856,8 +2879,7 @@ function transformOutlookWindows(html, clientId, framework) {
2856
2879
  message: "Outlook Windows ignores border-radius. Buttons and containers will have sharp corners."
2857
2880
  }, "border-radius", clientId, framework));
2858
2881
  }
2859
- const maxWidthElements = $("[style*='max-width']");
2860
- if (maxWidthElements.length > 0) {
2882
+ if ($("[style*='max-width']").length > 0) {
2861
2883
  warnings.push(makeWarning({
2862
2884
  severity: "warning",
2863
2885
  client: clientId,
@@ -2884,44 +2906,10 @@ function transformOutlookWindows(html, clientId, framework) {
2884
2906
  message: "Outlook Windows requires VML for background images."
2885
2907
  }, "background-image", clientId, framework));
2886
2908
  }
2887
- return { clientId, html: $.html(), warnings };
2888
- }
2889
- function transformOutlookWeb(html, clientId, framework) {
2890
- const $ = cheerio.load(html);
2891
- const warnings = [];
2892
- $("[style]").each((_, el) => {
2893
- const style = $(el).attr("style") || "";
2894
- const props = parseInlineStyle(style);
2895
- const removed = [];
2896
- const outlookWebUnsupported = /* @__PURE__ */ new Set([
2897
- "position",
2898
- "transform",
2899
- "animation",
2900
- "transition"
2901
- ]);
2902
- props.forEach((_2, prop) => {
2903
- if (outlookWebUnsupported.has(prop)) {
2904
- removed.push(prop);
2905
- props.delete(prop);
2906
- }
2907
- });
2908
- if (removed.length > 0) {
2909
- $(el).attr("style", serializeStyle(props));
2910
- for (const prop of removed) {
2911
- warnings.push(makeWarning({
2912
- severity: "warning",
2913
- client: clientId,
2914
- property: prop,
2915
- message: `Outlook 365 Web does not support "${prop}".`
2916
- }, prop, clientId, framework));
2917
- }
2918
- }
2919
- });
2920
- return { clientId, html: $.html(), warnings };
2909
+ return warnings;
2921
2910
  }
2922
- function transformAppleMail(html, clientId, _framework) {
2911
+ function appleMailAdditionalChecks($, clientId) {
2923
2912
  const warnings = [];
2924
- const $ = cheerio.load(html);
2925
2913
  const imgsWithTransparentBg = $("img").filter((_, el) => {
2926
2914
  const src = $(el).attr("src") || "";
2927
2915
  return src.endsWith(".png") || src.endsWith(".svg");
@@ -2935,10 +2923,9 @@ function transformAppleMail(html, clientId, _framework) {
2935
2923
  suggestion: "Add a white background or padding around images, or use dark-mode-friendly image variants."
2936
2924
  });
2937
2925
  }
2938
- return { clientId, html: $.html(), warnings };
2926
+ return warnings;
2939
2927
  }
2940
- function transformYahooMail(html, clientId, framework) {
2941
- const $ = cheerio.load(html);
2928
+ function yahooAdditionalChecks($, clientId, _html, framework) {
2942
2929
  const warnings = [];
2943
2930
  warnings.push({
2944
2931
  severity: "info",
@@ -2954,161 +2941,23 @@ function transformYahooMail(html, clientId, framework) {
2954
2941
  client: clientId,
2955
2942
  property: "background-image",
2956
2943
  message: "Yahoo Mail has inconsistent support for CSS background images."
2957
- }, "background-image", clientId, framework));
2958
- }
2959
- const yahooStripped = /* @__PURE__ */ new Set([
2960
- "position",
2961
- "box-shadow",
2962
- "transform",
2963
- "animation",
2964
- "transition",
2965
- "opacity"
2966
- ]);
2967
- $("[style]").each((_, el) => {
2968
- const style = $(el).attr("style") || "";
2969
- const props = parseInlineStyle(style);
2970
- const removed = [];
2971
- props.forEach((_2, prop) => {
2972
- if (yahooStripped.has(prop)) {
2973
- removed.push(prop);
2974
- props.delete(prop);
2975
- }
2976
- });
2977
- if (removed.length > 0) {
2978
- $(el).attr("style", serializeStyle(props));
2979
- for (const prop of removed) {
2980
- warnings.push(makeWarning({
2981
- severity: "warning",
2982
- client: clientId,
2983
- property: prop,
2984
- message: `Yahoo Mail strips "${prop}" from styles.`
2985
- }, prop, clientId, framework));
2986
- }
2987
- }
2988
- });
2989
- return { clientId, html: $.html(), warnings };
2990
- }
2991
- function transformSamsungMail(html, clientId, framework) {
2992
- const $ = cheerio.load(html);
2993
- const warnings = [];
2994
- const samsungPartial = /* @__PURE__ */ new Set([
2995
- "box-shadow",
2996
- "transform",
2997
- "animation",
2998
- "transition",
2999
- "opacity"
3000
- ]);
3001
- $("[style]").each((_, el) => {
3002
- const style = $(el).attr("style") || "";
3003
- const props = parseInlineStyle(style);
3004
- props.forEach((_2, prop) => {
3005
- if (samsungPartial.has(prop)) {
3006
- warnings.push(makeWarning({
3007
- severity: "info",
3008
- client: clientId,
3009
- property: prop,
3010
- message: `Samsung Mail has limited support for "${prop}".`
3011
- }, prop, clientId, framework));
3012
- }
3013
- });
3014
- });
3015
- return { clientId, html: $.html(), warnings };
3016
- }
3017
- function transformThunderbird(html, clientId, _framework) {
3018
- const $ = cheerio.load(html);
3019
- const warnings = [];
3020
- let hasAnimationOrTransition = false;
3021
- $("[style]").each((_, el) => {
3022
- const style = $(el).attr("style") || "";
3023
- const props = parseInlineStyle(style);
3024
- props.forEach((_2, prop) => {
3025
- if (prop === "animation" || prop === "transition" || prop.startsWith("animation-") || prop.startsWith("transition-")) {
3026
- hasAnimationOrTransition = true;
3027
- }
3028
- });
3029
- });
3030
- if (!hasAnimationOrTransition) {
3031
- $("style").each((_, el) => {
3032
- try {
3033
- const ast = csstree.parse($(el).text());
3034
- csstree.walk(ast, {
3035
- enter(node) {
3036
- if (node.type === "Declaration") {
3037
- const prop = node.property.toLowerCase();
3038
- if (prop === "animation" || prop === "transition" || prop.startsWith("animation-") || prop.startsWith("transition-")) {
3039
- hasAnimationOrTransition = true;
3040
- }
3041
- }
3042
- }
3043
- });
3044
- } catch (e) {
3045
- }
3046
- });
3047
- }
3048
- if (hasAnimationOrTransition) {
3049
- warnings.push({
3050
- severity: "info",
3051
- client: clientId,
3052
- property: "animation",
3053
- message: "Thunderbird does not support CSS animations or transitions."
3054
- });
3055
- }
3056
- return { clientId, html: $.html(), warnings };
3057
- }
3058
- function transformHeyMail(html, clientId, framework) {
3059
- const $ = cheerio.load(html);
3060
- const warnings = [];
3061
- const heyStripped = /* @__PURE__ */ new Set([
3062
- "transform",
3063
- "animation",
3064
- "transition"
3065
- ]);
3066
- $("[style]").each((_, el) => {
3067
- const style = $(el).attr("style") || "";
3068
- const props = parseInlineStyle(style);
3069
- const removed = [];
3070
- props.forEach((value, prop) => {
3071
- if (heyStripped.has(prop)) {
3072
- removed.push(prop);
3073
- props.delete(prop);
3074
- }
3075
- if (prop === "position" && (value.includes("fixed") || value.includes("sticky"))) {
3076
- removed.push(prop);
3077
- props.delete(prop);
3078
- }
3079
- });
3080
- if (removed.length > 0) {
3081
- $(el).attr("style", serializeStyle(props));
3082
- for (const prop of removed) {
3083
- warnings.push(makeWarning({
3084
- severity: "warning",
3085
- client: clientId,
3086
- property: prop,
3087
- message: `HEY Mail strips "${prop}" for security and rendering consistency.`
3088
- }, prop, clientId, framework));
3089
- }
3090
- }
3091
- });
3092
- if ($("form").length > 0) {
3093
- warnings.push(makeWarning({
3094
- severity: "error",
3095
- client: clientId,
3096
- property: "<form>",
3097
- message: "HEY Mail removes form elements for security."
3098
- }, "<form>", clientId, framework));
3099
- $("form").each((_, el) => {
3100
- $(el).replaceWith($(el).html() || "");
3101
- });
2944
+ }, "background-image", clientId, framework));
3102
2945
  }
3103
- if ($("link[rel='stylesheet']").length > 0) {
3104
- warnings.push(makeWarning({
3105
- severity: "error",
2946
+ return warnings;
2947
+ }
2948
+ function thunderbirdAdditionalChecks($, clientId) {
2949
+ if (detectAnimations($)) {
2950
+ return [{
2951
+ severity: "info",
3106
2952
  client: clientId,
3107
- property: "<link>",
3108
- message: "HEY Mail does not load external stylesheets."
3109
- }, "<link>", clientId, framework));
3110
- $("link[rel='stylesheet']").remove();
2953
+ property: "animation",
2954
+ message: "Thunderbird does not support CSS animations or transitions."
2955
+ }];
3111
2956
  }
2957
+ return [];
2958
+ }
2959
+ function heyAdditionalChecks(_$, clientId, html) {
2960
+ const warnings = [];
3112
2961
  if (!html.includes("prefers-color-scheme")) {
3113
2962
  warnings.push({
3114
2963
  severity: "info",
@@ -3118,96 +2967,270 @@ function transformHeyMail(html, clientId, framework) {
3118
2967
  suggestion: "Add a @media (prefers-color-scheme: dark) block to optimize for HEY's audience."
3119
2968
  });
3120
2969
  }
3121
- return { clientId, html: $.html(), warnings };
2970
+ return warnings;
2971
+ }
2972
+ function superhumanAdditionalChecks($, clientId, html) {
2973
+ const warnings = [];
2974
+ if (detectAnimations($)) {
2975
+ warnings.push({
2976
+ severity: "info",
2977
+ client: clientId,
2978
+ property: "animation",
2979
+ message: "Superhuman may honor OS-level 'reduce motion' preferences, disabling animations.",
2980
+ suggestion: "Use @media (prefers-reduced-motion: reduce) to provide static fallbacks."
2981
+ });
2982
+ }
2983
+ warnings.push({
2984
+ severity: "info",
2985
+ client: clientId,
2986
+ property: "<style>",
2987
+ message: "Superhuman uses Chromium rendering with excellent CSS support. Flexbox, Grid, CSS variables, and modern properties all work."
2988
+ });
2989
+ return warnings;
3122
2990
  }
3123
- function transformSuperhuman(html, clientId, framework) {
2991
+ var EMPTY_SET = /* @__PURE__ */ new Set();
2992
+ var CLIENT_CONFIGS = {
2993
+ "gmail-web": {
2994
+ id: "gmail-web",
2995
+ strippedProperties: GMAIL_STRIPPED_PROPERTIES,
2996
+ stripMode: "strip",
2997
+ valueStrips: [
2998
+ { prop: "display", pattern: /grid/ },
2999
+ { prop: "background", pattern: /linear-gradient|radial-gradient/ }
3000
+ ],
3001
+ inlineAndStripStyles: true,
3002
+ stripExternalStylesheets: true,
3003
+ stripForms: true,
3004
+ stripSvg: true,
3005
+ additionalChecks: gmailAdditionalChecks
3006
+ },
3007
+ "gmail-android": {
3008
+ id: "gmail-android",
3009
+ strippedProperties: GMAIL_STRIPPED_PROPERTIES,
3010
+ stripMode: "strip",
3011
+ valueStrips: [
3012
+ { prop: "display", pattern: /grid/ },
3013
+ { prop: "background", pattern: /linear-gradient|radial-gradient/ }
3014
+ ],
3015
+ inlineAndStripStyles: true,
3016
+ stripExternalStylesheets: true,
3017
+ stripForms: true,
3018
+ stripSvg: true,
3019
+ additionalChecks: gmailAdditionalChecks
3020
+ },
3021
+ "gmail-ios": {
3022
+ id: "gmail-ios",
3023
+ strippedProperties: GMAIL_STRIPPED_PROPERTIES,
3024
+ stripMode: "strip",
3025
+ valueStrips: [
3026
+ { prop: "display", pattern: /grid/ },
3027
+ { prop: "background", pattern: /linear-gradient|radial-gradient/ }
3028
+ ],
3029
+ inlineAndStripStyles: true,
3030
+ stripExternalStylesheets: true,
3031
+ stripForms: true,
3032
+ stripSvg: true,
3033
+ additionalChecks: gmailAdditionalChecks
3034
+ },
3035
+ "outlook-windows": {
3036
+ id: "outlook-windows",
3037
+ strippedProperties: OUTLOOK_WORD_UNSUPPORTED,
3038
+ stripMode: "strip",
3039
+ valueStrips: [
3040
+ { prop: "background", pattern: /linear-gradient|radial-gradient/ },
3041
+ { prop: "background-image", pattern: /linear-gradient|radial-gradient/ }
3042
+ ],
3043
+ inlineAndStripStyles: false,
3044
+ stripExternalStylesheets: false,
3045
+ stripForms: false,
3046
+ stripSvg: false,
3047
+ additionalChecks: outlookWindowsAdditionalChecks
3048
+ },
3049
+ "outlook-web": {
3050
+ id: "outlook-web",
3051
+ strippedProperties: /* @__PURE__ */ new Set(["position", "transform", "animation", "transition"]),
3052
+ stripMode: "strip",
3053
+ inlineAndStripStyles: false,
3054
+ stripExternalStylesheets: false,
3055
+ stripForms: false,
3056
+ stripSvg: false
3057
+ },
3058
+ "apple-mail-macos": {
3059
+ id: "apple-mail-macos",
3060
+ strippedProperties: EMPTY_SET,
3061
+ stripMode: "strip",
3062
+ inlineAndStripStyles: false,
3063
+ stripExternalStylesheets: false,
3064
+ stripForms: false,
3065
+ stripSvg: false,
3066
+ additionalChecks: appleMailAdditionalChecks
3067
+ },
3068
+ "apple-mail-ios": {
3069
+ id: "apple-mail-ios",
3070
+ strippedProperties: EMPTY_SET,
3071
+ stripMode: "strip",
3072
+ inlineAndStripStyles: false,
3073
+ stripExternalStylesheets: false,
3074
+ stripForms: false,
3075
+ stripSvg: false,
3076
+ additionalChecks: appleMailAdditionalChecks
3077
+ },
3078
+ "yahoo-mail": {
3079
+ id: "yahoo-mail",
3080
+ strippedProperties: /* @__PURE__ */ new Set(["position", "box-shadow", "transform", "animation", "transition", "opacity"]),
3081
+ stripMode: "strip",
3082
+ inlineAndStripStyles: false,
3083
+ stripExternalStylesheets: false,
3084
+ stripForms: false,
3085
+ stripSvg: false,
3086
+ additionalChecks: yahooAdditionalChecks
3087
+ },
3088
+ "samsung-mail": {
3089
+ id: "samsung-mail",
3090
+ strippedProperties: /* @__PURE__ */ new Set(["box-shadow", "transform", "animation", "transition", "opacity"]),
3091
+ stripMode: "info",
3092
+ inlineAndStripStyles: false,
3093
+ stripExternalStylesheets: false,
3094
+ stripForms: false,
3095
+ stripSvg: false
3096
+ },
3097
+ "thunderbird": {
3098
+ id: "thunderbird",
3099
+ strippedProperties: EMPTY_SET,
3100
+ stripMode: "strip",
3101
+ inlineAndStripStyles: false,
3102
+ stripExternalStylesheets: false,
3103
+ stripForms: false,
3104
+ stripSvg: false,
3105
+ additionalChecks: thunderbirdAdditionalChecks
3106
+ },
3107
+ "hey-mail": {
3108
+ id: "hey-mail",
3109
+ strippedProperties: /* @__PURE__ */ new Set(["transform", "animation", "transition"]),
3110
+ stripMode: "strip",
3111
+ valueStrips: [
3112
+ { prop: "position", pattern: /fixed|sticky/ }
3113
+ ],
3114
+ inlineAndStripStyles: false,
3115
+ stripExternalStylesheets: true,
3116
+ stripForms: true,
3117
+ stripSvg: false,
3118
+ additionalChecks: heyAdditionalChecks
3119
+ },
3120
+ "superhuman": {
3121
+ id: "superhuman",
3122
+ strippedProperties: EMPTY_SET,
3123
+ stripMode: "strip",
3124
+ inlineAndStripStyles: false,
3125
+ stripExternalStylesheets: true,
3126
+ stripForms: true,
3127
+ stripSvg: false,
3128
+ additionalChecks: superhumanAdditionalChecks
3129
+ }
3130
+ };
3131
+ function applyTransform(html, config, framework) {
3124
3132
  const $ = cheerio.load(html);
3125
3133
  const warnings = [];
3126
- if ($("form").length > 0) {
3134
+ const clientId = config.id;
3135
+ if (config.additionalChecks && config.inlineAndStripStyles) {
3136
+ warnings.push(...config.additionalChecks($, clientId, html, framework));
3137
+ }
3138
+ if (config.inlineAndStripStyles) {
3139
+ inlineStyles($);
3140
+ $("style").remove();
3141
+ }
3142
+ if (config.stripExternalStylesheets) {
3143
+ if ($("link[rel='stylesheet']").length > 0) {
3144
+ warnings.push(makeWarning({
3145
+ severity: "error",
3146
+ client: clientId,
3147
+ property: "<link>",
3148
+ message: `${clientId} does not load external stylesheets.`
3149
+ }, "<link>", clientId, framework));
3150
+ $("link[rel='stylesheet']").remove();
3151
+ }
3152
+ }
3153
+ if (config.stripForms && $("form").length > 0) {
3127
3154
  warnings.push(makeWarning({
3128
3155
  severity: "error",
3129
3156
  client: clientId,
3130
3157
  property: "<form>",
3131
- message: "Superhuman removes form elements."
3158
+ message: `${clientId} removes form elements.`
3132
3159
  }, "<form>", clientId, framework));
3133
3160
  $("form").each((_, el) => {
3134
3161
  $(el).replaceWith($(el).html() || "");
3135
3162
  });
3136
3163
  }
3137
- if ($("link[rel='stylesheet']").length > 0) {
3164
+ if (config.stripSvg && $("svg").length > 0) {
3138
3165
  warnings.push(makeWarning({
3139
3166
  severity: "error",
3140
3167
  client: clientId,
3141
- property: "<link>",
3142
- message: "Superhuman does not load external stylesheets."
3143
- }, "<link>", clientId, framework));
3144
- $("link[rel='stylesheet']").remove();
3145
- }
3146
- let hasAnimation = false;
3147
- $("[style]").each((_, el) => {
3148
- const style = $(el).attr("style") || "";
3149
- const props = parseInlineStyle(style);
3150
- props.forEach((_2, prop) => {
3151
- if (prop === "animation" || prop === "transition" || prop.startsWith("animation-") || prop.startsWith("transition-")) {
3152
- hasAnimation = true;
3153
- }
3168
+ property: "<svg>",
3169
+ message: `${clientId} does not support inline SVG elements.`
3170
+ }, "<svg>", clientId, framework));
3171
+ $("svg").each((_, el) => {
3172
+ $(el).replaceWith('<img alt="[SVG not supported]" />');
3154
3173
  });
3155
- });
3156
- if (!hasAnimation) {
3157
- $("style").each((_, el) => {
3158
- try {
3159
- const ast = csstree.parse($(el).text());
3160
- csstree.walk(ast, {
3161
- enter(node) {
3162
- if (node.type === "Declaration") {
3163
- const prop = node.property.toLowerCase();
3164
- if (prop === "animation" || prop === "transition" || prop.startsWith("animation-") || prop.startsWith("transition-")) {
3165
- hasAnimation = true;
3166
- }
3167
- }
3174
+ }
3175
+ if (config.strippedProperties.size > 0 || config.valueStrips && config.valueStrips.length > 0) {
3176
+ $("[style]").each((_, el) => {
3177
+ const style = $(el).attr("style") || "";
3178
+ const props = parseInlineStyle(style);
3179
+ const removed = [];
3180
+ props.forEach((value, prop) => {
3181
+ var _a;
3182
+ if (config.strippedProperties.has(prop)) {
3183
+ if (config.stripMode === "strip") {
3184
+ removed.push(prop);
3185
+ props.delete(prop);
3186
+ } else {
3187
+ warnings.push(makeWarning({
3188
+ severity: "info",
3189
+ client: clientId,
3190
+ property: prop,
3191
+ message: `${clientId} has limited support for "${prop}".`
3192
+ }, prop, clientId, framework));
3168
3193
  }
3169
- });
3170
- } catch (e) {
3194
+ return;
3195
+ }
3196
+ for (const vs of (_a = config.valueStrips) != null ? _a : []) {
3197
+ if (prop === vs.prop && vs.pattern.test(value)) {
3198
+ removed.push(prop);
3199
+ props.delete(prop);
3200
+ return;
3201
+ }
3202
+ }
3203
+ });
3204
+ if (removed.length > 0) {
3205
+ $(el).attr("style", serializeStyle(props));
3206
+ for (const prop of removed) {
3207
+ warnings.push(makeWarning({
3208
+ severity: "warning",
3209
+ client: clientId,
3210
+ property: prop,
3211
+ message: `${clientId} strips "${prop}" from styles.`
3212
+ }, prop, clientId, framework));
3213
+ }
3171
3214
  }
3172
3215
  });
3173
3216
  }
3174
- if (hasAnimation) {
3175
- warnings.push({
3176
- severity: "info",
3177
- client: clientId,
3178
- property: "animation",
3179
- message: "Superhuman may honor OS-level 'reduce motion' preferences, disabling animations.",
3180
- suggestion: "Use @media (prefers-reduced-motion: reduce) to provide static fallbacks."
3181
- });
3217
+ if (config.inlineAndStripStyles) {
3218
+ warnings.push(...gmailPostChecks($, clientId, html, framework));
3219
+ }
3220
+ if (config.additionalChecks && !config.inlineAndStripStyles) {
3221
+ warnings.push(...config.additionalChecks($, clientId, html, framework));
3182
3222
  }
3183
- warnings.push({
3184
- severity: "info",
3185
- client: clientId,
3186
- property: "<style>",
3187
- message: "Superhuman uses Chromium rendering with excellent CSS support. Flexbox, Grid, CSS variables, and modern properties all work."
3188
- });
3189
3223
  return { clientId, html: $.html(), warnings };
3190
3224
  }
3191
- var TRANSFORMERS = {
3192
- "gmail-web": transformGmail,
3193
- "gmail-android": transformGmail,
3194
- "gmail-ios": transformGmail,
3195
- "outlook-web": transformOutlookWeb,
3196
- "outlook-windows": transformOutlookWindows,
3197
- "apple-mail-macos": transformAppleMail,
3198
- "apple-mail-ios": transformAppleMail,
3199
- "yahoo-mail": transformYahooMail,
3200
- "samsung-mail": transformSamsungMail,
3201
- "thunderbird": transformThunderbird,
3202
- "hey-mail": transformHeyMail,
3203
- "superhuman": transformSuperhuman
3204
- };
3205
3225
  function transformForClient(html, clientId, framework) {
3206
3226
  if (!html || !html.trim()) {
3207
3227
  return { clientId, html: html || "", warnings: [] };
3208
3228
  }
3209
- const transformer = TRANSFORMERS[clientId];
3210
- if (!transformer) {
3229
+ if (html.length > MAX_HTML_SIZE) {
3230
+ throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
3231
+ }
3232
+ const config = CLIENT_CONFIGS[clientId];
3233
+ if (!config) {
3211
3234
  return {
3212
3235
  clientId,
3213
3236
  html,
@@ -3221,10 +3244,10 @@ function transformForClient(html, clientId, framework) {
3221
3244
  ]
3222
3245
  };
3223
3246
  }
3224
- return transformer(html, clientId, framework);
3247
+ return applyTransform(html, config, framework);
3225
3248
  }
3226
3249
  function transformForAllClients(html, framework) {
3227
- return Object.keys(TRANSFORMERS).map(
3250
+ return Object.keys(CLIENT_CONFIGS).map(
3228
3251
  (clientId) => transformForClient(html, clientId, framework)
3229
3252
  );
3230
3253
  }
@@ -3233,20 +3256,35 @@ function transformForAllClients(html, framework) {
3233
3256
  import * as cheerio2 from "cheerio";
3234
3257
  import * as csstree2 from "css-tree";
3235
3258
  function analyzeEmail(html, framework) {
3236
- var _a, _b, _c, _d, _e, _f, _g, _h;
3259
+ var _a, _b, _c, _d, _e, _f, _g;
3237
3260
  if (!html || !html.trim()) {
3238
3261
  return [];
3239
3262
  }
3263
+ if (html.length > MAX_HTML_SIZE) {
3264
+ throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
3265
+ }
3240
3266
  const $ = cheerio2.load(html);
3241
3267
  const warnings = [];
3242
3268
  const seenWarnings = /* @__PURE__ */ new Set();
3243
3269
  function addWarning(w) {
3244
- const key = `${w.client}:${w.property}:${w.severity}`;
3270
+ const key = `${w.client}:${w.property}:${w.severity}:${w.selector || ""}`;
3245
3271
  if (!seenWarnings.has(key)) {
3246
3272
  seenWarnings.add(key);
3247
3273
  warnings.push(w);
3248
3274
  }
3249
3275
  }
3276
+ function describeSelector(el) {
3277
+ var _a2;
3278
+ const $el = $(el);
3279
+ const tag = ((_a2 = el.tagName) == null ? void 0 : _a2.toLowerCase()) || "";
3280
+ const cls = $el.attr("class");
3281
+ const id = $el.attr("id");
3282
+ if (id) return `${tag}#${id}`;
3283
+ if (cls) return `${tag}.${cls.split(/\s+/)[0]}`;
3284
+ const href = $el.attr("href");
3285
+ if (href) return `${tag}[href]`;
3286
+ return tag;
3287
+ }
3250
3288
  if ($("style").length > 0) {
3251
3289
  for (const client of EMAIL_CLIENTS) {
3252
3290
  const support = (_a = CSS_SUPPORT["<style>"]) == null ? void 0 : _a[client.id];
@@ -3351,25 +3389,43 @@ function analyzeEmail(html, framework) {
3351
3389
  }
3352
3390
  const parsedAtRules = /* @__PURE__ */ new Set();
3353
3391
  const parsedProperties = /* @__PURE__ */ new Set();
3392
+ const propertyLines = /* @__PURE__ */ new Map();
3354
3393
  $("style").each((_, el) => {
3355
3394
  const cssText = $(el).text();
3356
3395
  try {
3357
- const ast = csstree2.parse(cssText, { parseCustomProperty: true });
3396
+ const ast = csstree2.parse(cssText, { parseCustomProperty: true, positions: true });
3358
3397
  csstree2.walk(ast, {
3359
3398
  enter(node) {
3360
3399
  if (node.type === "Atrule") {
3361
3400
  parsedAtRules.add(`@${node.name}`);
3362
3401
  }
3363
3402
  if (node.type === "Declaration") {
3364
- parsedProperties.add(node.property.toLowerCase());
3365
- if (node.property.toLowerCase() === "display") {
3403
+ const prop = node.property.toLowerCase();
3404
+ parsedProperties.add(prop);
3405
+ if (node.loc && !propertyLines.has(prop)) {
3406
+ propertyLines.set(prop, node.loc.start.line);
3407
+ }
3408
+ if (prop === "display") {
3366
3409
  const value = csstree2.generate(node.value);
3367
- if (value.includes("flex")) parsedProperties.add("display:flex");
3368
- if (value.includes("grid")) parsedProperties.add("display:grid");
3410
+ if (value.includes("flex")) {
3411
+ parsedProperties.add("display:flex");
3412
+ if (node.loc && !propertyLines.has("display:flex")) {
3413
+ propertyLines.set("display:flex", node.loc.start.line);
3414
+ }
3415
+ }
3416
+ if (value.includes("grid")) {
3417
+ parsedProperties.add("display:grid");
3418
+ if (node.loc && !propertyLines.has("display:grid")) {
3419
+ propertyLines.set("display:grid", node.loc.start.line);
3420
+ }
3421
+ }
3369
3422
  }
3370
3423
  const valueStr = csstree2.generate(node.value);
3371
3424
  if (valueStr.includes("linear-gradient") || valueStr.includes("radial-gradient")) {
3372
3425
  parsedProperties.add("linear-gradient");
3426
+ if (node.loc && !propertyLines.has("linear-gradient")) {
3427
+ propertyLines.set("linear-gradient", node.loc.start.line);
3428
+ }
3373
3429
  }
3374
3430
  }
3375
3431
  }
@@ -3419,43 +3475,33 @@ function analyzeEmail(html, framework) {
3419
3475
  $("[style]").each((_, el) => {
3420
3476
  const style = $(el).attr("style") || "";
3421
3477
  const props = parseStyleProperties(style);
3478
+ const selector = describeSelector(el);
3422
3479
  for (const prop of props) {
3423
3480
  if (prop === "display") {
3424
3481
  const value2 = getStyleValue(style, "display");
3425
3482
  if (value2 == null ? void 0 : value2.includes("flex")) {
3426
- checkPropertySupport("display:flex", addWarning, framework);
3483
+ checkPropertySupport("display:flex", addWarning, framework, selector);
3427
3484
  } else if (value2 == null ? void 0 : value2.includes("grid")) {
3428
- checkPropertySupport("display:grid", addWarning, framework);
3485
+ checkPropertySupport("display:grid", addWarning, framework, selector);
3429
3486
  }
3430
3487
  }
3431
3488
  if (cssPropertiesToCheck.includes(prop)) {
3432
- checkPropertySupport(prop, addWarning, framework);
3489
+ checkPropertySupport(prop, addWarning, framework, selector);
3433
3490
  }
3434
3491
  const value = getStyleValue(style, prop);
3435
3492
  if (value && (value.includes("linear-gradient") || value.includes("radial-gradient"))) {
3436
- checkPropertySupport("linear-gradient", addWarning, framework);
3493
+ checkPropertySupport("linear-gradient", addWarning, framework, selector);
3437
3494
  }
3438
3495
  }
3439
3496
  });
3440
3497
  for (const prop of parsedProperties) {
3441
3498
  if (prop.includes(":")) continue;
3442
3499
  if (!cssPropertiesToCheck.includes(prop)) continue;
3443
- for (const client of EMAIL_CLIENTS) {
3444
- const support = (_h = CSS_SUPPORT[prop]) == null ? void 0 : _h[client.id];
3445
- if (support === "unsupported") {
3446
- addWarning({
3447
- severity: "warning",
3448
- client: client.id,
3449
- property: prop,
3450
- message: `${client.name} does not support "${prop}" in <style> blocks.`,
3451
- fixType: getFixType(prop)
3452
- });
3453
- }
3454
- }
3500
+ checkPropertySupport(prop, addWarning, framework, void 0, propertyLines.get(prop));
3455
3501
  }
3456
3502
  for (const compound of ["display:flex", "display:grid", "linear-gradient"]) {
3457
3503
  if (parsedProperties.has(compound)) {
3458
- checkPropertySupport(compound, addWarning, framework);
3504
+ checkPropertySupport(compound, addWarning, framework, void 0, propertyLines.get(compound));
3459
3505
  }
3460
3506
  }
3461
3507
  const severityOrder = { error: 0, warning: 1, info: 2 };
@@ -3465,7 +3511,7 @@ function analyzeEmail(html, framework) {
3465
3511
  function getFixType(prop) {
3466
3512
  return STRUCTURAL_FIX_PROPERTIES.has(prop) ? "structural" : "css";
3467
3513
  }
3468
- function checkPropertySupport(prop, addWarning, framework) {
3514
+ function checkPropertySupport(prop, addWarning, framework, selector, line) {
3469
3515
  const supportData = CSS_SUPPORT[prop];
3470
3516
  if (!supportData) return;
3471
3517
  const fixType = getFixType(prop);
@@ -3474,7 +3520,7 @@ function checkPropertySupport(prop, addWarning, framework) {
3474
3520
  if (support === "unsupported") {
3475
3521
  const sug = getSuggestion(prop, client.id, framework);
3476
3522
  const fix = getCodeFix(prop, client.id, framework);
3477
- addWarning(__spreadValues({
3523
+ addWarning(__spreadValues(__spreadValues(__spreadValues({
3478
3524
  severity: "warning",
3479
3525
  client: client.id,
3480
3526
  property: prop,
@@ -3482,11 +3528,11 @@ function checkPropertySupport(prop, addWarning, framework) {
3482
3528
  suggestion: sug.text,
3483
3529
  fix,
3484
3530
  fixType
3485
- }, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
3531
+ }, selector ? { selector } : {}), line !== void 0 ? { line } : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
3486
3532
  } else if (support === "partial") {
3487
3533
  const sug = getSuggestion(prop, client.id, framework);
3488
3534
  const fix = getCodeFix(prop, client.id, framework);
3489
- addWarning(__spreadValues({
3535
+ addWarning(__spreadValues(__spreadValues(__spreadValues({
3490
3536
  severity: "info",
3491
3537
  client: client.id,
3492
3538
  property: prop,
@@ -3494,7 +3540,7 @@ function checkPropertySupport(prop, addWarning, framework) {
3494
3540
  suggestion: sug.text,
3495
3541
  fix,
3496
3542
  fixType
3497
- }, framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
3543
+ }, selector ? { selector } : {}), line !== void 0 ? { line } : {}), framework && (sug.isGenericFallback || fix && isCodeFixGenericFallback(prop, client.id, framework)) ? { fixIsGenericFallback: true } : {}));
3498
3544
  }
3499
3545
  }
3500
3546
  }
@@ -3510,13 +3556,260 @@ function generateCompatibilityScore(warnings) {
3510
3556
  }
3511
3557
  return result;
3512
3558
  }
3559
+ function warningsForClient(warnings, clientId) {
3560
+ return warnings.filter((w) => w.client === clientId);
3561
+ }
3562
+ function errorWarnings(warnings) {
3563
+ return warnings.filter((w) => w.severity === "error");
3564
+ }
3565
+ function structuralWarnings(warnings) {
3566
+ return warnings.filter((w) => w.fixType === "structural");
3567
+ }
3513
3568
 
3514
3569
  // src/dark-mode.ts
3515
3570
  import * as cheerio3 from "cheerio";
3571
+ import * as csstree3 from "css-tree";
3572
+
3573
+ // src/color-utils.ts
3574
+ var NAMED_COLORS = {
3575
+ aliceblue: [240, 248, 255],
3576
+ antiquewhite: [250, 235, 215],
3577
+ aqua: [0, 255, 255],
3578
+ aquamarine: [127, 255, 212],
3579
+ azure: [240, 255, 255],
3580
+ beige: [245, 245, 220],
3581
+ bisque: [255, 228, 196],
3582
+ black: [0, 0, 0],
3583
+ blanchedalmond: [255, 235, 205],
3584
+ blue: [0, 0, 255],
3585
+ blueviolet: [138, 43, 226],
3586
+ brown: [165, 42, 42],
3587
+ burlywood: [222, 184, 135],
3588
+ cadetblue: [95, 158, 160],
3589
+ chartreuse: [127, 255, 0],
3590
+ chocolate: [210, 105, 30],
3591
+ coral: [255, 127, 80],
3592
+ cornflowerblue: [100, 149, 237],
3593
+ cornsilk: [255, 248, 220],
3594
+ crimson: [220, 20, 60],
3595
+ cyan: [0, 255, 255],
3596
+ darkblue: [0, 0, 139],
3597
+ darkcyan: [0, 139, 139],
3598
+ darkgoldenrod: [184, 134, 11],
3599
+ darkgray: [169, 169, 169],
3600
+ darkgreen: [0, 100, 0],
3601
+ darkgrey: [169, 169, 169],
3602
+ darkkhaki: [189, 183, 107],
3603
+ darkmagenta: [139, 0, 139],
3604
+ darkolivegreen: [85, 107, 47],
3605
+ darkorange: [255, 140, 0],
3606
+ darkorchid: [153, 50, 204],
3607
+ darkred: [139, 0, 0],
3608
+ darksalmon: [233, 150, 122],
3609
+ darkseagreen: [143, 188, 143],
3610
+ darkslateblue: [72, 61, 139],
3611
+ darkslategray: [47, 79, 79],
3612
+ darkslategrey: [47, 79, 79],
3613
+ darkturquoise: [0, 206, 209],
3614
+ darkviolet: [148, 0, 211],
3615
+ deeppink: [255, 20, 147],
3616
+ deepskyblue: [0, 191, 255],
3617
+ dimgray: [105, 105, 105],
3618
+ dimgrey: [105, 105, 105],
3619
+ dodgerblue: [30, 144, 255],
3620
+ firebrick: [178, 34, 34],
3621
+ floralwhite: [255, 250, 240],
3622
+ forestgreen: [34, 139, 34],
3623
+ fuchsia: [255, 0, 255],
3624
+ gainsboro: [220, 220, 220],
3625
+ ghostwhite: [248, 248, 255],
3626
+ gold: [255, 215, 0],
3627
+ goldenrod: [218, 165, 32],
3628
+ gray: [128, 128, 128],
3629
+ green: [0, 128, 0],
3630
+ greenyellow: [173, 255, 47],
3631
+ grey: [128, 128, 128],
3632
+ honeydew: [240, 255, 240],
3633
+ hotpink: [255, 105, 180],
3634
+ indianred: [205, 92, 92],
3635
+ indigo: [75, 0, 130],
3636
+ ivory: [255, 255, 240],
3637
+ khaki: [240, 230, 140],
3638
+ lavender: [230, 230, 250],
3639
+ lavenderblush: [255, 240, 245],
3640
+ lawngreen: [124, 252, 0],
3641
+ lemonchiffon: [255, 250, 205],
3642
+ lightblue: [173, 216, 230],
3643
+ lightcoral: [240, 128, 128],
3644
+ lightcyan: [224, 255, 255],
3645
+ lightgoldenrodyellow: [250, 250, 210],
3646
+ lightgray: [211, 211, 211],
3647
+ lightgreen: [144, 238, 144],
3648
+ lightgrey: [211, 211, 211],
3649
+ lightpink: [255, 182, 193],
3650
+ lightsalmon: [255, 160, 122],
3651
+ lightseagreen: [32, 178, 170],
3652
+ lightskyblue: [135, 206, 250],
3653
+ lightslategray: [119, 136, 153],
3654
+ lightslategrey: [119, 136, 153],
3655
+ lightsteelblue: [176, 196, 222],
3656
+ lightyellow: [255, 255, 224],
3657
+ lime: [0, 255, 0],
3658
+ limegreen: [50, 205, 50],
3659
+ linen: [250, 240, 230],
3660
+ magenta: [255, 0, 255],
3661
+ maroon: [128, 0, 0],
3662
+ mediumaquamarine: [102, 205, 170],
3663
+ mediumblue: [0, 0, 205],
3664
+ mediumorchid: [186, 85, 211],
3665
+ mediumpurple: [147, 111, 219],
3666
+ mediumseagreen: [60, 179, 113],
3667
+ mediumslateblue: [123, 104, 238],
3668
+ mediumspringgreen: [0, 250, 154],
3669
+ mediumturquoise: [72, 209, 204],
3670
+ mediumvioletred: [199, 21, 133],
3671
+ midnightblue: [25, 25, 112],
3672
+ mintcream: [245, 255, 250],
3673
+ mistyrose: [255, 228, 225],
3674
+ moccasin: [255, 228, 181],
3675
+ navajowhite: [255, 222, 173],
3676
+ navy: [0, 0, 128],
3677
+ oldlace: [253, 245, 230],
3678
+ olive: [128, 128, 0],
3679
+ olivedrab: [107, 142, 35],
3680
+ orange: [255, 165, 0],
3681
+ orangered: [255, 69, 0],
3682
+ orchid: [218, 112, 214],
3683
+ palegoldenrod: [238, 232, 170],
3684
+ palegreen: [152, 251, 152],
3685
+ paleturquoise: [175, 238, 238],
3686
+ palevioletred: [219, 112, 147],
3687
+ papayawhip: [255, 239, 213],
3688
+ peachpuff: [255, 218, 185],
3689
+ peru: [205, 133, 63],
3690
+ pink: [255, 192, 203],
3691
+ plum: [221, 160, 221],
3692
+ powderblue: [176, 224, 230],
3693
+ purple: [128, 0, 128],
3694
+ rebeccapurple: [102, 51, 153],
3695
+ red: [255, 0, 0],
3696
+ rosybrown: [188, 143, 143],
3697
+ royalblue: [65, 105, 225],
3698
+ saddlebrown: [139, 69, 19],
3699
+ salmon: [250, 128, 114],
3700
+ sandybrown: [244, 164, 96],
3701
+ seagreen: [46, 139, 87],
3702
+ seashell: [255, 245, 238],
3703
+ sienna: [160, 82, 45],
3704
+ silver: [192, 192, 192],
3705
+ skyblue: [135, 206, 235],
3706
+ slateblue: [106, 90, 205],
3707
+ slategray: [112, 128, 144],
3708
+ slategrey: [112, 128, 144],
3709
+ snow: [255, 250, 250],
3710
+ springgreen: [0, 255, 127],
3711
+ steelblue: [70, 130, 180],
3712
+ tan: [210, 180, 140],
3713
+ teal: [0, 128, 128],
3714
+ thistle: [216, 191, 216],
3715
+ tomato: [255, 99, 71],
3716
+ turquoise: [64, 224, 208],
3717
+ violet: [238, 130, 238],
3718
+ wheat: [245, 222, 179],
3719
+ white: [255, 255, 255],
3720
+ whitesmoke: [245, 245, 245],
3721
+ yellow: [255, 255, 0],
3722
+ yellowgreen: [154, 205, 50]
3723
+ };
3724
+ function parseColor(value) {
3725
+ if (!value) return null;
3726
+ const v = value.trim().toLowerCase();
3727
+ if (v === "inherit" || v === "currentcolor" || v === "initial" || v === "unset" || v.startsWith("var(")) {
3728
+ return null;
3729
+ }
3730
+ if (v === "transparent") {
3731
+ return { r: 0, g: 0, b: 0, a: 0 };
3732
+ }
3733
+ const named = NAMED_COLORS[v];
3734
+ if (named) {
3735
+ return { r: named[0], g: named[1], b: named[2], a: 1 };
3736
+ }
3737
+ if (v.startsWith("#")) {
3738
+ const hex = v.slice(1);
3739
+ if (hex.length === 3) {
3740
+ return {
3741
+ r: parseInt(hex[0] + hex[0], 16),
3742
+ g: parseInt(hex[1] + hex[1], 16),
3743
+ b: parseInt(hex[2] + hex[2], 16),
3744
+ a: 1
3745
+ };
3746
+ }
3747
+ if (hex.length === 6) {
3748
+ return {
3749
+ r: parseInt(hex.slice(0, 2), 16),
3750
+ g: parseInt(hex.slice(2, 4), 16),
3751
+ b: parseInt(hex.slice(4, 6), 16),
3752
+ a: 1
3753
+ };
3754
+ }
3755
+ if (hex.length === 8) {
3756
+ return {
3757
+ r: parseInt(hex.slice(0, 2), 16),
3758
+ g: parseInt(hex.slice(2, 4), 16),
3759
+ b: parseInt(hex.slice(4, 6), 16),
3760
+ a: parseInt(hex.slice(6, 8), 16) / 255
3761
+ };
3762
+ }
3763
+ return null;
3764
+ }
3765
+ const rgbMatch = v.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)$/);
3766
+ if (rgbMatch) {
3767
+ return {
3768
+ r: Math.min(255, parseInt(rgbMatch[1], 10)),
3769
+ g: Math.min(255, parseInt(rgbMatch[2], 10)),
3770
+ b: Math.min(255, parseInt(rgbMatch[3], 10)),
3771
+ a: rgbMatch[4] !== void 0 ? Math.min(1, parseFloat(rgbMatch[4])) : 1
3772
+ };
3773
+ }
3774
+ return null;
3775
+ }
3776
+ function relativeLuminance(r, g, b) {
3777
+ const [rs, gs, bs] = [r, g, b].map((c) => {
3778
+ const s = c / 255;
3779
+ return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
3780
+ });
3781
+ return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
3782
+ }
3783
+ function contrastRatio(l1, l2) {
3784
+ const lighter = Math.max(l1, l2);
3785
+ const darker = Math.min(l1, l2);
3786
+ return (lighter + 0.05) / (darker + 0.05);
3787
+ }
3788
+ function wcagGrade(ratio) {
3789
+ if (ratio >= 7) return "AAA";
3790
+ if (ratio >= 4.5) return "AA";
3791
+ if (ratio >= 3) return "AA Large";
3792
+ return "Fail";
3793
+ }
3794
+ function alphaBlend(fg, bgR, bgG, bgB) {
3795
+ const a = fg.a;
3796
+ return [
3797
+ Math.round(fg.r * a + bgR * (1 - a)),
3798
+ Math.round(fg.g * a + bgG * (1 - a)),
3799
+ Math.round(fg.b * a + bgB * (1 - a))
3800
+ ];
3801
+ }
3802
+
3803
+ // src/dark-mode.ts
3804
+ var LIGHT_THRESHOLD = 0.7;
3805
+ var DARK_THRESHOLD = 0.15;
3516
3806
  function simulateDarkMode(html, clientId) {
3517
3807
  if (!html || !html.trim()) {
3518
3808
  return { html: html || "", warnings: [] };
3519
3809
  }
3810
+ if (html.length > MAX_HTML_SIZE) {
3811
+ throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
3812
+ }
3520
3813
  const $ = cheerio3.load(html);
3521
3814
  const warnings = [];
3522
3815
  $("img").each((_, el) => {
@@ -3591,24 +3884,115 @@ function simulateDarkMode(html, clientId) {
3591
3884
  $("body").css("color", "#e0e0e0");
3592
3885
  return { html: $.html(), warnings };
3593
3886
  }
3887
+ function invertColor(value, mode) {
3888
+ const parsed = parseColor(value);
3889
+ if (!parsed || parsed.a === 0) return null;
3890
+ const lum = relativeLuminance(parsed.r, parsed.g, parsed.b);
3891
+ if (mode === "full") {
3892
+ if (lum > LIGHT_THRESHOLD) {
3893
+ return "#1a1a1a";
3894
+ }
3895
+ if (lum < DARK_THRESHOLD) {
3896
+ return "#e0e0e0";
3897
+ }
3898
+ return null;
3899
+ } else {
3900
+ if (lum > 0.85) {
3901
+ return "#2d2d2d";
3902
+ }
3903
+ if (lum < 0.05) {
3904
+ return "#d4d4d4";
3905
+ }
3906
+ return null;
3907
+ }
3908
+ }
3909
+ var COLOR_PROPS = /* @__PURE__ */ new Set([
3910
+ "color",
3911
+ "background-color",
3912
+ "border-color",
3913
+ "border-top-color",
3914
+ "border-right-color",
3915
+ "border-bottom-color",
3916
+ "border-left-color",
3917
+ "outline-color"
3918
+ ]);
3919
+ function extractBackgroundColor(value) {
3920
+ const trimmed = value.trim();
3921
+ if (trimmed.startsWith("#") || trimmed.startsWith("rgb") || /^[a-z]+$/i.test(trimmed.split(/\s/)[0])) {
3922
+ const firstToken = trimmed.split(/\s/)[0];
3923
+ if (parseColor(firstToken)) return firstToken;
3924
+ }
3925
+ return null;
3926
+ }
3594
3927
  function applyColorInversion($, mode) {
3595
3928
  $("[style]").each((_, el) => {
3596
3929
  const style = $(el).attr("style") || "";
3597
- if (mode === "full") {
3598
- const updated = style.replace(/background-color:\s*(#fff|#ffffff|white|#fafafa|#f5f5f5|#f0f0f0|#fefefe)/gi, "background-color: #1a1a1a").replace(/background:\s*(#fff|#ffffff|white|#fafafa|#f5f5f5|#f0f0f0|#fefefe)/gi, "background: #1a1a1a").replace(/color:\s*(#000|#000000|black|#111|#222|#333)/gi, "color: #e0e0e0").replace(/color:\s*(#fff|#ffffff|white)/gi, "color: #e0e0e0").replace(
3599
- /border(?:-[a-z]+)?:\s*[^;]*(?:#000|#111|#222|#333|black)/gi,
3600
- (match) => match.replace(/#000|#111|#222|#333|black/gi, "#555")
3601
- );
3602
- $(el).attr("style", updated);
3603
- } else {
3604
- const updated = style.replace(/background-color:\s*(#fff|#ffffff|white)/gi, "background-color: #2d2d2d").replace(/background:\s*(#fff|#ffffff|white)/gi, "background: #2d2d2d").replace(/color:\s*(#000|#000000|black)/gi, "color: #d4d4d4");
3605
- $(el).attr("style", updated);
3930
+ const props = parseInlineStyle(style);
3931
+ let changed = false;
3932
+ props.forEach((value, prop) => {
3933
+ if (COLOR_PROPS.has(prop)) {
3934
+ const inverted = invertColor(value, mode);
3935
+ if (inverted) {
3936
+ props.set(prop, inverted);
3937
+ changed = true;
3938
+ }
3939
+ }
3940
+ if (prop === "background") {
3941
+ const bgColor = extractBackgroundColor(value);
3942
+ if (bgColor) {
3943
+ const inverted = invertColor(bgColor, mode);
3944
+ if (inverted) {
3945
+ props.set(prop, value.replace(bgColor, inverted));
3946
+ changed = true;
3947
+ }
3948
+ }
3949
+ }
3950
+ });
3951
+ if (changed) {
3952
+ $(el).attr("style", serializeStyle(props));
3953
+ }
3954
+ });
3955
+ $("style").each((_, el) => {
3956
+ const cssText = $(el).text();
3957
+ try {
3958
+ const ast = csstree3.parse(cssText, { parseCustomProperty: true });
3959
+ let modified = false;
3960
+ csstree3.walk(ast, {
3961
+ enter(node) {
3962
+ if (node.type !== "Declaration") return;
3963
+ const prop = node.property.toLowerCase();
3964
+ if (!COLOR_PROPS.has(prop) && prop !== "background") return;
3965
+ const valueStr = csstree3.generate(node.value);
3966
+ if (prop === "background") {
3967
+ const bgColor = extractBackgroundColor(valueStr);
3968
+ if (bgColor) {
3969
+ const inverted = invertColor(bgColor, mode);
3970
+ if (inverted) {
3971
+ const newValue = valueStr.replace(bgColor, inverted);
3972
+ node.value = csstree3.parse(newValue, { context: "value" });
3973
+ modified = true;
3974
+ }
3975
+ }
3976
+ } else {
3977
+ const inverted = invertColor(valueStr, mode);
3978
+ if (inverted) {
3979
+ node.value = csstree3.parse(inverted, { context: "value" });
3980
+ modified = true;
3981
+ }
3982
+ }
3983
+ }
3984
+ });
3985
+ if (modified) {
3986
+ $(el).text(csstree3.generate(ast));
3987
+ }
3988
+ } catch (e) {
3606
3989
  }
3607
3990
  });
3608
3991
  $("[bgcolor]").each((_, el) => {
3609
- const bgcolor = ($(el).attr("bgcolor") || "").toLowerCase();
3610
- if (bgcolor === "#ffffff" || bgcolor === "#fff" || bgcolor === "white" || bgcolor === "#fafafa" || bgcolor === "#f5f5f5") {
3611
- $(el).attr("bgcolor", mode === "full" ? "#1a1a1a" : "#2d2d2d");
3992
+ const bgcolor = $(el).attr("bgcolor") || "";
3993
+ const inverted = invertColor(bgcolor, mode);
3994
+ if (inverted) {
3995
+ $(el).attr("bgcolor", inverted);
3612
3996
  }
3613
3997
  });
3614
3998
  }
@@ -4254,6 +4638,9 @@ function analyzeSpam(html, options) {
4254
4638
  if (!html || !html.trim()) {
4255
4639
  return { score: 100, level: "low", issues: [] };
4256
4640
  }
4641
+ if (html.length > MAX_HTML_SIZE) {
4642
+ throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
4643
+ }
4257
4644
  const $ = cheerio4.load(html);
4258
4645
  const text = extractVisibleText($);
4259
4646
  const issues = [];
@@ -4295,18 +4682,6 @@ function analyzeSpam(html, options) {
4295
4682
 
4296
4683
  // src/link-validator.ts
4297
4684
  import * as cheerio5 from "cheerio";
4298
- var GENERIC_LINK_TEXT = /* @__PURE__ */ new Set([
4299
- "click here",
4300
- "here",
4301
- "read more",
4302
- "learn more",
4303
- "more",
4304
- "link",
4305
- "this link",
4306
- "click",
4307
- "tap here",
4308
- "this"
4309
- ]);
4310
4685
  function classifyHref(href) {
4311
4686
  if (!href || !href.trim()) return "empty";
4312
4687
  const h = href.trim().toLowerCase();
@@ -4328,12 +4703,15 @@ function validateLinks(html) {
4328
4703
  return {
4329
4704
  totalLinks: 0,
4330
4705
  issues: [],
4331
- breakdown: { https: 0, http: 0, mailto: 0, tel: 0, anchor: 0, other: 0 }
4706
+ breakdown: { https: 0, http: 0, mailto: 0, tel: 0, anchor: 0, javascript: 0, protocolRelative: 0, other: 0 }
4332
4707
  };
4333
4708
  }
4709
+ if (html.length > MAX_HTML_SIZE) {
4710
+ throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
4711
+ }
4334
4712
  const $ = cheerio5.load(html);
4335
4713
  const issues = [];
4336
- const breakdown = { https: 0, http: 0, mailto: 0, tel: 0, anchor: 0, other: 0 };
4714
+ const breakdown = { https: 0, http: 0, mailto: 0, tel: 0, anchor: 0, javascript: 0, protocolRelative: 0, other: 0 };
4337
4715
  const links = $("a");
4338
4716
  const totalLinks = links.length;
4339
4717
  if (totalLinks === 0) {
@@ -4344,6 +4722,7 @@ function validateLinks(html) {
4344
4722
  });
4345
4723
  return { totalLinks: 0, issues, breakdown };
4346
4724
  }
4725
+ const hrefCounts = /* @__PURE__ */ new Map();
4347
4726
  links.each((_, el) => {
4348
4727
  const href = $(el).attr("href") || "";
4349
4728
  const text = $(el).text().trim();
@@ -4364,10 +4743,19 @@ function validateLinks(html) {
4364
4743
  case "anchor":
4365
4744
  breakdown.anchor++;
4366
4745
  break;
4746
+ case "javascript":
4747
+ breakdown.javascript++;
4748
+ break;
4749
+ case "protocol-relative":
4750
+ breakdown.protocolRelative++;
4751
+ break;
4367
4752
  default:
4368
4753
  breakdown.other++;
4369
4754
  break;
4370
4755
  }
4756
+ if (href && href.trim()) {
4757
+ hrefCounts.set(href, (hrefCounts.get(href) || 0) + 1);
4758
+ }
4371
4759
  if (!href || !href.trim()) {
4372
4760
  issues.push({
4373
4761
  severity: "error",
@@ -4406,6 +4794,15 @@ function validateLinks(html) {
4406
4794
  text: text.slice(0, 80) || "(no text)"
4407
4795
  });
4408
4796
  }
4797
+ if (category === "protocol-relative") {
4798
+ issues.push({
4799
+ severity: "warning",
4800
+ rule: "protocol-relative",
4801
+ message: "Protocol-relative URL may break in email clients \u2014 use https:// explicitly",
4802
+ href: href.slice(0, 120),
4803
+ text: text.slice(0, 80) || "(no text)"
4804
+ });
4805
+ }
4409
4806
  if (text && GENERIC_LINK_TEXT.has(text.toLowerCase())) {
4410
4807
  issues.push({
4411
4808
  severity: "warning",
@@ -4432,6 +4829,15 @@ function validateLinks(html) {
4432
4829
  text: text.slice(0, 80) || "(no text)"
4433
4830
  });
4434
4831
  }
4832
+ if (category === "tel" && href.trim().toLowerCase() === "tel:") {
4833
+ issues.push({
4834
+ severity: "error",
4835
+ rule: "empty-tel",
4836
+ message: "tel: link has no phone number",
4837
+ href,
4838
+ text: text.slice(0, 80) || "(no text)"
4839
+ });
4840
+ }
4435
4841
  if (href.length > 2e3) {
4436
4842
  issues.push({
4437
4843
  severity: "info",
@@ -4442,23 +4848,28 @@ function validateLinks(html) {
4442
4848
  });
4443
4849
  }
4444
4850
  });
4851
+ for (const [href, count] of hrefCounts) {
4852
+ if (count > 5) {
4853
+ issues.push({
4854
+ severity: "info",
4855
+ rule: "duplicate-links",
4856
+ message: `URL appears ${count} times \u2014 consider consolidating`,
4857
+ href: href.slice(0, 120)
4858
+ });
4859
+ }
4860
+ }
4445
4861
  return { totalLinks, issues, breakdown };
4446
4862
  }
4447
4863
 
4448
4864
  // src/accessibility-checker.ts
4449
4865
  import * as cheerio6 from "cheerio";
4450
- var GENERIC_LINK_TEXT2 = /* @__PURE__ */ new Set([
4451
- "click here",
4452
- "here",
4453
- "read more",
4454
- "learn more",
4455
- "more",
4456
- "link",
4457
- "this link",
4458
- "click",
4459
- "tap here",
4460
- "this"
4461
- ]);
4866
+ var RULE_PENALTY_CAPS = {
4867
+ "img-missing-alt": 3,
4868
+ "link-generic-text": 3,
4869
+ "link-no-accessible-name": 3,
4870
+ "table-missing-role": 2,
4871
+ "low-contrast": 3
4872
+ };
4462
4873
  function describeElement($, el) {
4463
4874
  var _a;
4464
4875
  const tag = ((_a = el.tagName) == null ? void 0 : _a.toLowerCase()) || "unknown";
@@ -4549,7 +4960,7 @@ function checkLinkAccessibility($) {
4549
4960
  });
4550
4961
  return;
4551
4962
  }
4552
- if (text && GENERIC_LINK_TEXT2.has(text) && !ariaLabel) {
4963
+ if (text && GENERIC_LINK_TEXT.has(text) && !ariaLabel) {
4553
4964
  issues.push({
4554
4965
  severity: "warning",
4555
4966
  rule: "link-generic-text",
@@ -4564,6 +4975,7 @@ function checkLinkAccessibility($) {
4564
4975
  function checkTableAccessibility($) {
4565
4976
  const issues = [];
4566
4977
  $("table").each((_, el) => {
4978
+ if ($(el).parents('table[role="presentation"], table[role="none"]').length > 0) return;
4567
4979
  const role = $(el).attr("role");
4568
4980
  const hasHeaders = $(el).find("th").length > 0;
4569
4981
  const looksLikeLayout = !hasHeaders;
@@ -4582,7 +4994,7 @@ function checkTableAccessibility($) {
4582
4994
  });
4583
4995
  return issues;
4584
4996
  }
4585
- function checkColorContrast($) {
4997
+ function checkTextSizeAndContrast($) {
4586
4998
  const issues = [];
4587
4999
  let smallTextCount = 0;
4588
5000
  $("[style]").each((_, el) => {
@@ -4592,7 +5004,7 @@ function checkColorContrast($) {
4592
5004
  const size = parseFloat(fontSizeMatch[1]);
4593
5005
  const unit = fontSizeMatch[2].toLowerCase();
4594
5006
  const pxSize = unit === "pt" ? size * 1.333 : size;
4595
- if (pxSize < 10 && pxSize > 0) {
5007
+ if (pxSize < 9 && pxSize > 0) {
4596
5008
  smallTextCount++;
4597
5009
  if (smallTextCount <= 3) {
4598
5010
  issues.push({
@@ -4600,7 +5012,66 @@ function checkColorContrast($) {
4600
5012
  rule: "small-text",
4601
5013
  message: `Very small text (${fontSizeMatch[0].trim()})`,
4602
5014
  element: describeElement($, el),
4603
- details: "Text smaller than 10px is difficult to read, especially on mobile devices."
5015
+ details: "Text smaller than 9px is difficult to read, especially on mobile devices."
5016
+ });
5017
+ }
5018
+ }
5019
+ }
5020
+ const colorValue = getStyleValue(style, "color");
5021
+ if (colorValue) {
5022
+ const fg = parseColor(colorValue);
5023
+ if (fg) {
5024
+ let bgR = 255, bgG = 255, bgB = 255;
5025
+ let current = $(el);
5026
+ let foundBg = false;
5027
+ const elements = [current, ...$(el).parents("[style]").toArray().map((p) => $(p))];
5028
+ for (const ancestor of elements) {
5029
+ const ancestorStyle = (typeof ancestor === "function" ? ancestor : $(ancestor)).attr("style") || "";
5030
+ const bgValue = getStyleValue(ancestorStyle, "background-color");
5031
+ if (bgValue) {
5032
+ const bg = parseColor(bgValue);
5033
+ if (bg && bg.a > 0) {
5034
+ if (bg.a < 1) {
5035
+ [bgR, bgG, bgB] = alphaBlend(bg, 255, 255, 255);
5036
+ } else {
5037
+ bgR = bg.r;
5038
+ bgG = bg.g;
5039
+ bgB = bg.b;
5040
+ }
5041
+ foundBg = true;
5042
+ break;
5043
+ }
5044
+ }
5045
+ }
5046
+ const [fR, fG, fB] = fg.a < 1 ? alphaBlend(fg, bgR, bgG, bgB) : [fg.r, fg.g, fg.b];
5047
+ const fgLum = relativeLuminance(fR, fG, fB);
5048
+ const bgLum = relativeLuminance(bgR, bgG, bgB);
5049
+ const ratio = contrastRatio(fgLum, bgLum);
5050
+ let isLargeText = false;
5051
+ if (fontSizeMatch) {
5052
+ const size = parseFloat(fontSizeMatch[1]);
5053
+ const unit = fontSizeMatch[2].toLowerCase();
5054
+ const pxSize = unit === "pt" ? size * 1.333 : size;
5055
+ const fontWeight = getStyleValue(style, "font-weight");
5056
+ const isBold = fontWeight === "bold" || fontWeight === "bolder" || fontWeight && parseInt(fontWeight, 10) >= 700;
5057
+ isLargeText = pxSize >= 18 || pxSize >= 14 && !!isBold;
5058
+ }
5059
+ const grade = wcagGrade(ratio);
5060
+ if (grade === "Fail") {
5061
+ issues.push({
5062
+ severity: "error",
5063
+ rule: "low-contrast",
5064
+ message: `Low contrast ratio ${ratio.toFixed(1)}:1 \u2014 fails WCAG minimum`,
5065
+ element: describeElement($, el),
5066
+ details: `Foreground ${colorValue} on background needs at least ${isLargeText ? "3:1" : "4.5:1"} contrast ratio.`
5067
+ });
5068
+ } else if (!isLargeText && grade === "AA Large") {
5069
+ issues.push({
5070
+ severity: "warning",
5071
+ rule: "low-contrast",
5072
+ message: `Low contrast ratio ${ratio.toFixed(1)}:1 \u2014 fails WCAG AA for normal text`,
5073
+ element: describeElement($, el),
5074
+ details: `Foreground ${colorValue} on background needs at least 4.5:1 for normal-sized text.`
4604
5075
  });
4605
5076
  }
4606
5077
  }
@@ -4610,7 +5081,7 @@ function checkColorContrast($) {
4610
5081
  issues.push({
4611
5082
  severity: "warning",
4612
5083
  rule: "small-text-multiple",
4613
- message: `${smallTextCount} elements with text smaller than 10px`,
5084
+ message: `${smallTextCount} elements with text smaller than 9px`,
4614
5085
  details: "Consider using a minimum font size of 12-14px for readability."
4615
5086
  });
4616
5087
  }
@@ -4641,6 +5112,9 @@ function checkAccessibility(html) {
4641
5112
  if (!html || !html.trim()) {
4642
5113
  return { score: 100, issues: [] };
4643
5114
  }
5115
+ if (html.length > MAX_HTML_SIZE) {
5116
+ throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
5117
+ }
4644
5118
  const $ = cheerio6.load(html);
4645
5119
  const issues = [];
4646
5120
  const langIssue = checkLangAttribute($);
@@ -4650,10 +5124,15 @@ function checkAccessibility(html) {
4650
5124
  issues.push(...checkImageAlt($));
4651
5125
  issues.push(...checkLinkAccessibility($));
4652
5126
  issues.push(...checkTableAccessibility($));
4653
- issues.push(...checkColorContrast($));
5127
+ issues.push(...checkTextSizeAndContrast($));
4654
5128
  issues.push(...checkSemanticStructure($));
4655
5129
  let penalty = 0;
5130
+ const seenRules = /* @__PURE__ */ new Map();
4656
5131
  for (const issue of issues) {
5132
+ const count = (seenRules.get(issue.rule) || 0) + 1;
5133
+ seenRules.set(issue.rule, count);
5134
+ const cap = RULE_PENALTY_CAPS[issue.rule];
5135
+ if (cap !== void 0 && count > cap) continue;
4657
5136
  switch (issue.severity) {
4658
5137
  case "error":
4659
5138
  penalty += 12;
@@ -4706,6 +5185,9 @@ function analyzeImages(html) {
4706
5185
  if (!html || !html.trim()) {
4707
5186
  return { total: 0, totalDataUriBytes: 0, issues: [], images: [] };
4708
5187
  }
5188
+ if (html.length > MAX_HTML_SIZE) {
5189
+ throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
5190
+ }
4709
5191
  const $ = cheerio7.load(html);
4710
5192
  const issues = [];
4711
5193
  const images = [];
@@ -4832,15 +5314,56 @@ function analyzeImages(html) {
4832
5314
  }
4833
5315
  return { total: images.length, totalDataUriBytes, issues, images };
4834
5316
  }
5317
+
5318
+ // src/audit.ts
5319
+ var EMPTY_SPAM = { score: 100, level: "low", issues: [] };
5320
+ var EMPTY_LINKS = {
5321
+ totalLinks: 0,
5322
+ issues: [],
5323
+ breakdown: { https: 0, http: 0, mailto: 0, tel: 0, anchor: 0, javascript: 0, protocolRelative: 0, other: 0 }
5324
+ };
5325
+ var EMPTY_ACCESSIBILITY = { score: 100, issues: [] };
5326
+ var EMPTY_IMAGES = { total: 0, totalDataUriBytes: 0, issues: [], images: [] };
5327
+ function auditEmail(html, options) {
5328
+ var _a;
5329
+ if (!html || !html.trim()) {
5330
+ return {
5331
+ compatibility: { warnings: [], scores: {} },
5332
+ spam: EMPTY_SPAM,
5333
+ links: EMPTY_LINKS,
5334
+ accessibility: EMPTY_ACCESSIBILITY,
5335
+ images: EMPTY_IMAGES
5336
+ };
5337
+ }
5338
+ if (html.length > MAX_HTML_SIZE) {
5339
+ throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
5340
+ }
5341
+ const framework = options == null ? void 0 : options.framework;
5342
+ const skip = new Set((_a = options == null ? void 0 : options.skip) != null ? _a : []);
5343
+ const warnings = skip.has("compatibility") ? [] : analyzeEmail(html, framework);
5344
+ const scores = skip.has("compatibility") ? {} : generateCompatibilityScore(warnings);
5345
+ const spam = skip.has("spam") ? EMPTY_SPAM : analyzeSpam(html, options == null ? void 0 : options.spam);
5346
+ const links = skip.has("links") ? EMPTY_LINKS : validateLinks(html);
5347
+ const accessibility = skip.has("accessibility") ? EMPTY_ACCESSIBILITY : checkAccessibility(html);
5348
+ const images = skip.has("images") ? EMPTY_IMAGES : analyzeImages(html);
5349
+ return { compatibility: { warnings, scores }, spam, links, accessibility, images };
5350
+ }
4835
5351
  export {
4836
5352
  AI_FIX_SYSTEM_PROMPT,
5353
+ CompileError,
4837
5354
  EMAIL_CLIENTS,
5355
+ GENERIC_LINK_TEXT,
5356
+ MAX_HTML_SIZE,
4838
5357
  STRUCTURAL_FIX_PROPERTIES,
5358
+ alphaBlend,
4839
5359
  analyzeEmail,
4840
5360
  analyzeImages,
4841
5361
  analyzeSpam,
5362
+ auditEmail,
4842
5363
  checkAccessibility,
5364
+ contrastRatio,
4843
5365
  diffResults,
5366
+ errorWarnings,
4844
5367
  estimateAiFixTokens,
4845
5368
  generateAiFix,
4846
5369
  generateCompatibilityScore,
@@ -4849,9 +5372,14 @@ export {
4849
5372
  getCodeFix,
4850
5373
  getSuggestion,
4851
5374
  heuristicTokenCount,
5375
+ parseColor,
5376
+ relativeLuminance,
4852
5377
  simulateDarkMode,
5378
+ structuralWarnings,
4853
5379
  transformForAllClients,
4854
5380
  transformForClient,
4855
- validateLinks
5381
+ validateLinks,
5382
+ warningsForClient,
5383
+ wcagGrade
4856
5384
  };
4857
5385
  //# sourceMappingURL=index.js.map