@emailens/engine 0.3.1 → 0.5.1
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/README.md +255 -246
- package/dist/chunk-PFONR3YC.js +56 -0
- package/dist/chunk-PFONR3YC.js.map +1 -0
- package/dist/chunk-PX25W7YG.js +331 -0
- package/dist/chunk-PX25W7YG.js.map +1 -0
- package/dist/chunk-SZ5O5PDZ.js +78 -0
- package/dist/chunk-SZ5O5PDZ.js.map +1 -0
- package/dist/chunk-W4SPWESS.js +64 -0
- package/dist/chunk-W4SPWESS.js.map +1 -0
- package/dist/compile/index.cjs +590 -0
- package/dist/compile/index.cjs.map +1 -0
- package/dist/compile/index.d.cts +47 -0
- package/dist/compile/index.d.ts +47 -0
- package/dist/compile/index.js +59 -0
- package/dist/compile/index.js.map +1 -0
- package/dist/index.cjs +5485 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +273 -0
- package/dist/index.d.ts +84 -231
- package/dist/index.js +1455 -927
- package/dist/index.js.map +1 -1
- package/dist/maizzle-YDSYDVSM.js +8 -0
- package/dist/maizzle-YDSYDVSM.js.map +1 -0
- package/dist/mjml-IYGC6AOM.js +8 -0
- package/dist/mjml-IYGC6AOM.js.map +1 -0
- package/dist/react-email-BQljgXbo.d.cts +289 -0
- package/dist/react-email-BQljgXbo.d.ts +289 -0
- package/dist/react-email-QRL5KZ4Y.js +8 -0
- package/dist/react-email-QRL5KZ4Y.js.map +1 -0
- package/package.json +97 -60
package/dist/index.js
CHANGED
|
@@ -1,34 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// ──
|
|
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:
|
|
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
|
-
// ──
|
|
1905
|
-
"
|
|
1906
|
-
language: "
|
|
1907
|
-
description: "
|
|
1908
|
-
before: `<
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
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
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
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
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
<
|
|
1949
|
-
|
|
1950
|
-
|
|
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
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
<
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
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:
|
|
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
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
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
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
}
|
|
2444
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2553
|
-
// ── white-space ────────────────────────────────────────────────────
|
|
2445
|
+
// ── white-space ───────────────────────────────────────────────────────
|
|
2554
2446
|
"white-space": "Outlook only supports 'normal' and 'nowrap'. Use 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
|
|
2724
|
-
|
|
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
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
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
|
-
|
|
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
|
|
2822
|
-
const $ = cheerio.load(html);
|
|
2872
|
+
function outlookWindowsAdditionalChecks($, clientId, _html, framework) {
|
|
2823
2873
|
const warnings = [];
|
|
2824
|
-
$("[style]").
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
2926
|
+
return warnings;
|
|
2939
2927
|
}
|
|
2940
|
-
function
|
|
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
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
2946
|
+
return warnings;
|
|
2947
|
+
}
|
|
2948
|
+
function thunderbirdAdditionalChecks($, clientId) {
|
|
2949
|
+
if (detectAnimations($)) {
|
|
2950
|
+
return [{
|
|
2951
|
+
severity: "info",
|
|
3106
2952
|
client: clientId,
|
|
3107
|
-
property: "
|
|
3108
|
-
message: "
|
|
3109
|
-
}
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 ($("
|
|
3164
|
+
if (config.stripSvg && $("svg").length > 0) {
|
|
3138
3165
|
warnings.push(makeWarning({
|
|
3139
3166
|
severity: "error",
|
|
3140
3167
|
client: clientId,
|
|
3141
|
-
property: "<
|
|
3142
|
-
message:
|
|
3143
|
-
}, "<
|
|
3144
|
-
$("
|
|
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 (
|
|
3157
|
-
$("style").each((_, el) => {
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
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
|
-
|
|
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 (
|
|
3175
|
-
warnings.push(
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
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
|
-
|
|
3210
|
-
|
|
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
|
|
3247
|
+
return applyTransform(html, config, framework);
|
|
3225
3248
|
}
|
|
3226
3249
|
function transformForAllClients(html, framework) {
|
|
3227
|
-
return Object.keys(
|
|
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
|
|
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
|
-
|
|
3365
|
-
|
|
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"))
|
|
3368
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
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 =
|
|
3610
|
-
|
|
3611
|
-
|
|
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
|
|
4451
|
-
"
|
|
4452
|
-
"
|
|
4453
|
-
"
|
|
4454
|
-
"
|
|
4455
|
-
"
|
|
4456
|
-
|
|
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 &&
|
|
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
|
|
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 <
|
|
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
|
|
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
|
|
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(...
|
|
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
|