@emabuild/email-renderer 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- function w(e, n, i) {
2
- const t = i.backgroundColor || "#e7e7e7", c = i.contentWidth || "600px", p = i.fontFamily?.value || "arial,helvetica,sans-serif", u = i.textColor || "#000000", o = i.preheaderText || "" || "&zwnj;", s = `<div style="display:none;font-size:1px;color:${t};line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;">${o}${"&zwnj;&nbsp;".repeat(80)}</div>`;
1
+ function w(e, i, n) {
2
+ const t = n.backgroundColor || "#e7e7e7", c = n.contentWidth || "600px", p = n.fontFamily?.value || "arial,helvetica,sans-serif", u = n.textColor || "#000000", o = n.preheaderText || "" || "&zwnj;", s = `<div style="display:none;font-size:1px;color:${t};line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;">${o}${"&zwnj;&nbsp;".repeat(80)}</div>`;
3
3
  return `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
4
4
  <html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
5
5
  <head>
@@ -23,7 +23,7 @@ function w(e, n, i) {
23
23
  <![endif]-->
24
24
  <!--[if !mso]><!-->
25
25
  <style type="text/css">
26
- ${n}
26
+ ${i}
27
27
  </style>
28
28
  <!--<![endif]-->
29
29
  <style type="text/css">
@@ -49,8 +49,8 @@ function w(e, n, i) {
49
49
  </body>
50
50
  </html>`;
51
51
  }
52
- function x(e, n, i) {
53
- const t = parseInt(n.contentWidth || "600"), c = e.values.backgroundColor || "", p = e.values.columnsBackgroundColor || "", u = e.values.padding || "0px", g = e.cells.reduce((h, y) => h + y, 0), o = c ? `background-color:${c};` : "", s = e.values.backgroundImage;
52
+ function x(e, i, n) {
53
+ const t = parseInt(i.contentWidth || "600"), c = e.values.backgroundColor || "", p = e.values.columnsBackgroundColor || "", u = e.values.padding || "0px", g = e.cells.reduce((h, y) => h + y, 0), o = c ? `background-color:${c};` : "", s = e.values.backgroundImage;
54
54
  let a = "";
55
55
  if (s?.url) {
56
56
  const h = s.repeat === !0 || s.repeat === "repeat" ? "repeat" : "no-repeat", y = s.cover === !0 ? "cover" : s.fullWidth === !0 ? "100% auto" : "auto", f = s.center !== !1 ? "center top" : "left top";
@@ -58,7 +58,7 @@ function x(e, n, i) {
58
58
  }
59
59
  const r = e.columns.length > 1, d = e.columns.map((h, y) => {
60
60
  const f = Math.round(e.cells[y] / g * t);
61
- return { colHtml: k(h, f, p, n, i), colWidthPx: f };
61
+ return { colHtml: k(h, f, p, i, n), colWidthPx: f };
62
62
  });
63
63
  let m;
64
64
  if (r) {
@@ -73,12 +73,12 @@ function x(e, n, i) {
73
73
  <div style="margin:0 auto;max-width:${t}px;${r ? "font-size:0;" : ""}text-align:center;">${m}</div>
74
74
  </div>`;
75
75
  }
76
- function k(e, n, i, t, c) {
77
- const p = e.values.backgroundColor || i || "", u = e.values.padding || "0px", g = e.values.borderRadius || "0px", o = p ? `background-color:${p};` : "", s = e.contents.map((a) => {
76
+ function k(e, i, n, t, c) {
77
+ const p = e.values.backgroundColor || n || "", u = e.values.padding || "0px", g = e.values.borderRadius || "0px", o = p ? `background-color:${p};` : "", s = e.contents.map((a) => {
78
78
  const r = c.get(a.type);
79
79
  if (!r) return `<!-- unknown tool: ${a.type} -->`;
80
80
  const d = {
81
- columnWidth: n,
81
+ columnWidth: i,
82
82
  displayMode: "email",
83
83
  contentWidth: parseInt(t.contentWidth || "600"),
84
84
  bodyValues: t
@@ -88,7 +88,7 @@ function k(e, n, i, t, c) {
88
88
  return (b || v) && (m = `<div class="${[b && "u_hide_desktop", v && "u_hide_mobile"].filter(Boolean).join(" ")}">${m}</div>`), m;
89
89
  }).join(`
90
90
  `);
91
- return `<div class="u_column" style="display:inline-block;vertical-align:top;width:${n}px;max-width:${n}px;font-size:14px;text-align:left;">
91
+ return `<div class="u_column" style="display:inline-block;vertical-align:top;width:${i}px;max-width:${i}px;font-size:14px;text-align:left;">
92
92
  <div style="width:100%;${o}${g !== "0px" ? `border-radius:${g};` : ""}">
93
93
  <div style="padding:${u};">
94
94
  ${s || "&nbsp;"}
@@ -147,7 +147,6 @@ function $(e) {
147
147
  }
148
148
  const _ = [
149
149
  "box-sizing",
150
- "float",
151
150
  "overflow-wrap",
152
151
  "word-break",
153
152
  "word-wrap",
@@ -156,7 +155,6 @@ const _ = [
156
155
  "transition",
157
156
  "animation",
158
157
  "transform",
159
- "position",
160
158
  "z-index",
161
159
  "display:\\s*flex",
162
160
  "display:\\s*grid",
@@ -166,17 +164,17 @@ const _ = [
166
164
  "gi"
167
165
  ), E = /var\(--[^)]*\)/gi;
168
166
  function I(e) {
169
- return e.replace(/style="([^"]*)"/gi, (n, i) => {
170
- let t = i;
167
+ return e.replace(/style="([^"]*)"/gi, (i, n) => {
168
+ let t = n;
171
169
  return t = t.replace(C, ""), t = t.replace(E, "inherit"), t = t.replace(/;\s*;/g, ";").replace(/^\s*;\s*/, "").replace(/;\s*$/, "").trim(), t ? `style="${t}"` : "";
172
170
  });
173
171
  }
174
- function M(e, n, i) {
175
- const t = e.body.values, c = parseInt(t.contentWidth || "600"), p = e.body.rows.map((d) => x(d, t, n)).join(`
172
+ function M(e, i, n) {
173
+ const t = e.body.values, c = parseInt(t.contentWidth || "600"), p = e.body.rows.map((d) => x(d, t, i)).join(`
176
174
  `), u = $(c), g = I(p);
177
175
  let o = w(g, u, t);
178
- if (i?.mergeTags)
179
- for (const [d, m] of Object.entries(i.mergeTags))
176
+ if (n?.mergeTags)
177
+ for (const [d, m] of Object.entries(n.mergeTags))
180
178
  o = o.replaceAll(`{{${d}}}`, m);
181
179
  const s = o.match(/<body[^>]*>([\s\S]*)<\/body>/i), a = o.match(/<style[^>]*>([\s\S]*?)<\/style>/gi), r = [];
182
180
  return t.fontFamily?.url && r.push(t.fontFamily.url), {
@@ -192,28 +190,28 @@ function M(e, n, i) {
192
190
  };
193
191
  }
194
192
  function P(e) {
195
- const n = [], i = [];
196
- e.includes("<!DOCTYPE") || n.push({ rule: "doctype", message: "Missing <!DOCTYPE> declaration. Required for consistent rendering.", severity: "error" }), (!e.includes("<meta") || !e.includes("charset")) && n.push({ rule: "charset", message: 'Missing charset meta tag. Add <meta charset="UTF-8">.', severity: "error" }), e.includes("viewport") || i.push({ rule: "viewport", message: "Missing viewport meta tag. Needed for responsive rendering on mobile.", severity: "warning" });
193
+ const i = [], n = [];
194
+ e.includes("<!DOCTYPE") || i.push({ rule: "doctype", message: "Missing <!DOCTYPE> declaration. Required for consistent rendering.", severity: "error" }), (!e.includes("<meta") || !e.includes("charset")) && i.push({ rule: "charset", message: 'Missing charset meta tag. Add <meta charset="UTF-8">.', severity: "error" }), e.includes("viewport") || n.push({ rule: "viewport", message: "Missing viewport meta tag. Needed for responsive rendering on mobile.", severity: "warning" });
197
195
  const t = (e.match(/<table/gi) || []).length, c = (e.match(/role="presentation"/gi) || []).length;
198
- t > 0 && c < t * 0.5 && i.push({ rule: "table-role", message: `Only ${c}/${t} tables have role="presentation". Screen readers may interpret layout tables as data tables.`, severity: "warning" });
196
+ t > 0 && c < t * 0.5 && n.push({ rule: "table-role", message: `Only ${c}/${t} tables have role="presentation". Screen readers may interpret layout tables as data tables.`, severity: "warning" });
199
197
  const p = e.match(/<img[^>]*>/gi) || [];
200
198
  for (const l of p)
201
- l.includes("alt=") || n.push({ rule: "img-alt", message: "Image missing alt attribute. Required for accessibility and when images are blocked.", severity: "error" }), (!l.includes("style=") || !l.includes("display")) && i.push({ rule: "img-display", message: "Image without display:block may have a bottom gap in some email clients.", severity: "warning" }), (l.includes('width="100%"') || !l.includes("width=") && !l.includes("width:")) && i.push({ rule: "img-width", message: "Image without explicit pixel width. Some email clients ignore percentage widths.", severity: "warning" });
199
+ l.includes("alt=") || i.push({ rule: "img-alt", message: "Image missing alt attribute. Required for accessibility and when images are blocked.", severity: "error" }), (!l.includes("style=") || !l.includes("display")) && n.push({ rule: "img-display", message: "Image without display:block may have a bottom gap in some email clients.", severity: "warning" }), (l.includes('width="100%"') || !l.includes("width=") && !l.includes("width:")) && n.push({ rule: "img-width", message: "Image without explicit pixel width. Some email clients ignore percentage widths.", severity: "warning" });
202
200
  const u = e.match(/<style[^>]*>[\s\S]*?<\/style>/gi) || [], g = e.includes('style="');
203
- u.length > 0 && !e.includes("@media") && i.push({ rule: "no-media-queries", message: "Style block found but no @media queries. Consider adding responsive breakpoints.", severity: "warning" }), !g && t > 0 && n.push({ rule: "inline-styles", message: "No inline styles detected. Gmail and many clients strip <style> tags — inline styles are essential.", severity: "error" });
201
+ u.length > 0 && !e.includes("@media") && n.push({ rule: "no-media-queries", message: "Style block found but no @media queries. Consider adding responsive breakpoints.", severity: "warning" }), !g && t > 0 && i.push({ rule: "inline-styles", message: "No inline styles detected. Gmail and many clients strip <style> tags — inline styles are essential.", severity: "error" });
204
202
  const o = ["position:fixed", "position:absolute", "display:flex", "display:grid", "gap:", "transform:", "animation:"];
205
203
  for (const l of o)
206
- e.includes('style="') && e.match(new RegExp(`style="[^"]*${l.replace(":", ":\\s*")}`, "i")) && i.push({ rule: "unsupported-css", message: `CSS property "${l.replace(":", "")}" not supported in most email clients.`, severity: "warning" });
207
- e.includes("<!--[if") || i.push({ rule: "mso-conditionals", message: "No MSO conditional comments found. Outlook may not render multi-column layouts correctly.", severity: "warning" }), !e.includes("xmlns:v") && !e.includes("xmlns:o") && i.push({ rule: "mso-namespace", message: "Missing Microsoft Office XML namespaces. May affect Outlook rendering.", severity: "warning" });
204
+ e.includes('style="') && e.match(new RegExp(`style="[^"]*${l.replace(":", ":\\s*")}`, "i")) && n.push({ rule: "unsupported-css", message: `CSS property "${l.replace(":", "")}" not supported in most email clients.`, severity: "warning" });
205
+ e.includes("<!--[if") || n.push({ rule: "mso-conditionals", message: "No MSO conditional comments found. Outlook may not render multi-column layouts correctly.", severity: "warning" }), !e.includes("xmlns:v") && !e.includes("xmlns:o") && n.push({ rule: "mso-namespace", message: "Missing Microsoft Office XML namespaces. May affect Outlook rendering.", severity: "warning" });
208
206
  const a = (e.match(/<a[^>]*>/gi) || []).filter((l) => !l.includes("style="));
209
- a.length > 0 && i.push({ rule: "link-color", message: `${a.length} link(s) without inline styles. Email clients override unstyled link colors.`, severity: "warning" }), !e.includes("color-scheme") && !e.includes("prefers-color-scheme") && i.push({ rule: "dark-mode", message: "No dark mode support detected. Consider adding color-scheme meta and prefers-color-scheme media query.", severity: "warning" });
207
+ a.length > 0 && n.push({ rule: "link-color", message: `${a.length} link(s) without inline styles. Email clients override unstyled link colors.`, severity: "warning" }), !e.includes("color-scheme") && !e.includes("prefers-color-scheme") && n.push({ rule: "dark-mode", message: "No dark mode support detected. Consider adding color-scheme meta and prefers-color-scheme media query.", severity: "warning" });
210
208
  const r = Math.round(e.length / 1024);
211
- r > 102 ? n.push({ rule: "size-limit", message: `Email HTML is ${r}KB. Gmail clips emails larger than 102KB.`, severity: "error" }) : r > 80 && i.push({ rule: "size-warning", message: `Email HTML is ${r}KB. Getting close to Gmail's 102KB clip limit.`, severity: "warning" }), !e.includes("display:none") && !e.includes("max-height:0") && i.push({ rule: "preheader", message: "No preheader text detected. Preheader text improves open rates in inbox previews.", severity: "warning" });
212
- const d = 100, m = n.length * 15, b = i.length * 5, v = Math.max(0, d - m - b);
209
+ r > 102 ? i.push({ rule: "size-limit", message: `Email HTML is ${r}KB. Gmail clips emails larger than 102KB.`, severity: "error" }) : r > 80 && n.push({ rule: "size-warning", message: `Email HTML is ${r}KB. Getting close to Gmail's 102KB clip limit.`, severity: "warning" }), !e.includes("display:none") && !e.includes("max-height:0") && n.push({ rule: "preheader", message: "No preheader text detected. Preheader text improves open rates in inbox previews.", severity: "warning" });
210
+ const d = 100, m = i.length * 15, b = n.length * 5, v = Math.max(0, d - m - b);
213
211
  return {
214
- valid: n.length === 0,
215
- errors: n,
216
- warnings: i,
212
+ valid: i.length === 0,
213
+ errors: i,
214
+ warnings: n,
217
215
  score: v
218
216
  };
219
217
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/layout/document-shell.ts","../src/layout/fluid-hybrid.ts","../src/utils/responsive-css.ts","../src/utils/html-sanitizer.ts","../src/render.ts","../src/utils/html-validator.ts"],"sourcesContent":["import type { BodyValues } from '@emabuild/types';\n\nexport function wrapInDocumentShell(bodyHtml: string, cssBlock: string, bodyValues: BodyValues): string {\n const bgColor = bodyValues.backgroundColor || '#e7e7e7';\n const contentWidth = bodyValues.contentWidth || '600px';\n const fontFamily = bodyValues.fontFamily?.value || 'arial,helvetica,sans-serif';\n const textColor = bodyValues.textColor || '#000000';\n const preheaderText = bodyValues.preheaderText || '';\n\n // Always include preheader wrapper — even if empty, the hidden div\n // prevents email clients from using the first visible text as preview\n const preheaderContent = preheaderText || '&zwnj;';\n const preheader = `<div style=\"display:none;font-size:1px;color:${bgColor};line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;\">${preheaderContent}${'&zwnj;&nbsp;'.repeat(80)}</div>`;\n\n return `<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n<head>\n <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n <meta name=\"x-apple-disable-message-reformatting\">\n <meta name=\"format-detection\" content=\"telephone=no,address=no,email=no,date=no,url=no\">\n <meta name=\"color-scheme\" content=\"light dark\">\n <meta name=\"supported-color-schemes\" content=\"light dark\">\n <title></title>\n <!--[if mso]>\n <noscript><xml>\n <o:OfficeDocumentSettings>\n <o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch>\n </o:OfficeDocumentSettings>\n </xml></noscript>\n <style type=\"text/css\">\n table, td, th { font-family: ${fontFamily} !important; }\n </style>\n <![endif]-->\n <!--[if !mso]><!-->\n <style type=\"text/css\">\n ${cssBlock}\n </style>\n <!--<![endif]-->\n <style type=\"text/css\">\n body { margin: 0; padding: 0; word-break: normal; }\n table, tr, td { vertical-align: top; border-collapse: collapse; }\n p { margin: 0; }\n a[x-apple-data-detectors='true'] { color: inherit !important; text-decoration: none !important; }\n </style>\n</head>\n<body class=\"clean-body u_body\" style=\"margin:0;padding:0;-webkit-text-size-adjust:100%;background-color:${bgColor};color:${textColor};\">\n ${preheader}\n <table id=\"u_body\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0;margin:0 auto;background-color:${bgColor};width:100%;\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\">\n <tbody>\n <tr>\n <td style=\"vertical-align:top;\">\n <!--[if (mso)|(IE)]><table width=\"${parseInt(contentWidth)}\" align=\"center\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr><td><![endif]-->\n ${bodyHtml}\n <!--[if (mso)|(IE)]></td></tr></table><![endif]-->\n </td>\n </tr>\n </tbody>\n </table>\n</body>\n</html>`;\n}\n","import type { DesignRow, DesignColumn, BodyValues } from '@emabuild/types';\n\ntype ContentRenderer = (values: Record<string, unknown>, ctx: any) => string;\n\nexport function renderRow(\n row: DesignRow,\n bodyValues: BodyValues,\n toolRenderers: Map<string, ContentRenderer>,\n): string {\n const contentWidth = parseInt(bodyValues.contentWidth || '600');\n const bgColor = row.values.backgroundColor || '';\n const colsBgColor = row.values.columnsBackgroundColor || '';\n const padding = row.values.padding || '0px';\n const totalCells = row.cells.reduce((a, b) => a + b, 0);\n\n const bgStyle = bgColor ? `background-color:${bgColor};` : '';\n const bgImg = row.values.backgroundImage;\n let bgImage = '';\n if (bgImg?.url) {\n // repeat can be: true, false, \"repeat\", \"no-repeat\", or a CSS value\n const repeat = bgImg.repeat === true || bgImg.repeat === 'repeat' ? 'repeat' : 'no-repeat';\n const size = bgImg.cover === true ? 'cover' : bgImg.fullWidth === true ? '100% auto' : 'auto';\n const position = bgImg.center !== false ? 'center top' : 'left top';\n bgImage = `background-image:url('${bgImg.url}');background-repeat:${repeat};background-position:${position};background-size:${size};`;\n }\n\n // Render columns once, wrap with MSO ghost table conditionals for multi-column\n const needsGhostTable = row.columns.length > 1;\n\n const columnEntries = row.columns.map((col, i) => {\n const colWidthPx = Math.round((row.cells[i] / totalCells) * contentWidth);\n const colHtml = renderColumn(col, colWidthPx, colsBgColor, bodyValues, toolRenderers);\n return { colHtml, colWidthPx };\n });\n\n let innerHtml: string;\n if (needsGhostTable) {\n // Wrap each column with MSO conditional <td> for Outlook.\n // For modern clients: inline-block columns inside a font-size:0 parent\n // (eliminates whitespace gaps between inline-block elements).\n const wrappedCols = columnEntries.map(({ colHtml, colWidthPx }) =>\n `<!--[if (mso)|(IE)]><td align=\"center\" width=\"${colWidthPx}\" style=\"width:${colWidthPx}px;padding:0px;\" valign=\"top\"><![endif]-->${colHtml}<!--[if (mso)|(IE)]></td><![endif]-->`\n );\n\n innerHtml = `<!--[if (mso)|(IE)]><table role=\"presentation\" width=\"${contentWidth}\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr><![endif]-->${wrappedCols.join('')}<!--[if (mso)|(IE)]></tr></table><![endif]-->`;\n } else {\n innerHtml = columnEntries.map(({ colHtml }) => colHtml).join('');\n }\n\n // Visibility classes\n const hideDesktop = row.values.hideDesktop ? ' u_hide_desktop' : '';\n const hideMobile = row.values.hideMobile ? ' u_hide_mobile' : '';\n\n // font-size:0 on the wrapper eliminates whitespace between inline-block columns\n const fsZero = needsGhostTable ? 'font-size:0;' : '';\n\n return `<div class=\"u_row${hideDesktop}${hideMobile}\" style=\"padding:${padding};${bgStyle}${bgImage}\">\n <div style=\"margin:0 auto;max-width:${contentWidth}px;${fsZero}text-align:center;\">${innerHtml}</div>\n</div>`;\n}\n\nfunction renderColumn(\n col: DesignColumn,\n widthPx: number,\n colsBgColor: string,\n bodyValues: BodyValues,\n toolRenderers: Map<string, ContentRenderer>,\n): string {\n const bgColor = col.values.backgroundColor || colsBgColor || '';\n const padding = col.values.padding || '0px';\n const borderRadius = col.values.borderRadius || '0px';\n const bgStyle = bgColor ? `background-color:${bgColor};` : '';\n\n const contentsHtml = col.contents\n .map((content) => {\n const renderer = toolRenderers.get(content.type);\n if (!renderer) return `<!-- unknown tool: ${content.type} -->`;\n const ctx = {\n columnWidth: widthPx,\n displayMode: 'email',\n contentWidth: parseInt(bodyValues.contentWidth || '600'),\n bodyValues,\n };\n let html = renderer(content.values, ctx);\n\n // Wrap with visibility classes if hideDesktop/hideMobile is set\n const hideDesktop = !!content.values.hideDesktop;\n const hideMobile = !!content.values.hideMobile;\n if (hideDesktop || hideMobile) {\n const cls = [hideDesktop && 'u_hide_desktop', hideMobile && 'u_hide_mobile'].filter(Boolean).join(' ');\n html = `<div class=\"${cls}\">${html}</div>`;\n }\n\n return html;\n })\n .join('\\n');\n\n return `<div class=\"u_column\" style=\"display:inline-block;vertical-align:top;width:${widthPx}px;max-width:${widthPx}px;font-size:14px;text-align:left;\">\n <div style=\"width:100%;${bgStyle}${borderRadius !== '0px' ? `border-radius:${borderRadius};` : ''}\">\n <div style=\"padding:${padding};\">\n ${contentsHtml || '&nbsp;'}\n </div>\n </div>\n</div>`;\n}\n","export function getResponsiveCss(contentWidth: number): string {\n return `\n@media only screen and (min-width: ${contentWidth + 20}px) {\n .u_row .u_column { display: inline-block !important; }\n}\n\n@media only screen and (max-width: ${contentWidth + 20}px) {\n .u_row .u_column {\n display: block !important;\n width: 100% !important;\n max-width: 100% !important;\n }\n .u_row {\n width: 100% !important;\n }\n}\n\n@media only screen and (max-width: 620px) {\n .u_row-container {\n max-width: 100% !important;\n padding-left: 0 !important;\n padding-right: 0 !important;\n }\n}\n\n@media (prefers-color-scheme: dark) {\n /* Dark mode overrides — add per-client rules as needed */\n}\n\n/* Outlook dark mode */\n[data-ogsb] body,\n[data-ogsb] table,\n[data-ogsb] td {\n /* Preserve original colors */\n}\n\n.u_hide_desktop { display: block !important; }\n.u_hide_mobile { display: block !important; }\n\n@media only screen and (max-width: ${contentWidth + 20}px) {\n .u_hide_desktop { display: block !important; }\n .u_hide_mobile { display: none !important; }\n}\n\n@media only screen and (min-width: ${contentWidth + 21}px) {\n .u_hide_desktop { display: none !important; }\n .u_hide_mobile { display: block !important; }\n}`;\n}\n","/**\n * @module html-sanitizer\n *\n * Sanitizes user-generated HTML content for email compatibility.\n * Removes CSS properties that are not supported by email clients\n * and replaces CSS variables with fallback values.\n */\n\n/** CSS properties to strip from inline styles */\nconst UNSUPPORTED_PROPS = [\n 'box-sizing',\n 'float',\n 'overflow-wrap',\n 'word-break',\n 'word-wrap',\n 'outline',\n 'cursor',\n 'transition',\n 'animation',\n 'transform',\n 'position',\n 'z-index',\n 'display:\\\\s*flex',\n 'display:\\\\s*grid',\n 'gap',\n];\n\n/** Build a single regex that matches any unsupported property in a style attribute */\nconst PROP_REGEX = new RegExp(\n `(?:;\\\\s*|^\\\\s*)(${UNSUPPORTED_PROPS.join('|')})\\\\s*:[^;]*;?`,\n 'gi',\n);\n\n/** Replace CSS var() with empty string (email clients don't support CSS variables) */\nconst VAR_REGEX = /var\\(--[^)]*\\)/gi;\n\n/**\n * Clean inline styles in HTML content to remove email-unsupported CSS.\n * Processes `style=\"...\"` attributes and removes problematic properties.\n *\n * @param html - Raw HTML content (e.g. from a text/heading block)\n * @returns Cleaned HTML with email-safe inline styles\n */\nexport function sanitizeEmailHtml(html: string): string {\n // Process each style=\"...\" attribute\n return html.replace(/style=\"([^\"]*)\"/gi, (match, styleContent: string) => {\n let cleaned = styleContent;\n\n // Remove unsupported properties\n cleaned = cleaned.replace(PROP_REGEX, '');\n\n // Replace CSS variables with empty/inherit\n cleaned = cleaned.replace(VAR_REGEX, 'inherit');\n\n // Clean up multiple semicolons and whitespace\n cleaned = cleaned.replace(/;\\s*;/g, ';').replace(/^\\s*;\\s*/, '').replace(/;\\s*$/, '').trim();\n\n return cleaned ? `style=\"${cleaned}\"` : '';\n });\n}\n","import type { EmailDesign, ExportResult, ExportOptions, BodyValues } from '@emabuild/types';\nimport { wrapInDocumentShell } from './layout/document-shell.js';\nimport { renderRow } from './layout/fluid-hybrid.js';\nimport { getResponsiveCss } from './utils/responsive-css.js';\nimport { sanitizeEmailHtml } from './utils/html-sanitizer.js';\n\ntype ContentRenderer = (values: Record<string, unknown>, ctx: any) => string;\n\nexport function renderDesignToHtml(\n design: EmailDesign,\n toolRenderers: Map<string, ContentRenderer>,\n options?: ExportOptions,\n): ExportResult {\n const bodyValues = design.body.values;\n const contentWidth = parseInt(bodyValues.contentWidth || '600');\n\n // Render all rows\n const rowsHtml = design.body.rows\n .map((row) => renderRow(row, bodyValues, toolRenderers))\n .join('\\n');\n\n // Build responsive CSS\n const cssBlock = getResponsiveCss(contentWidth);\n\n // Sanitize user content (remove email-unsupported CSS)\n const cleanRowsHtml = sanitizeEmailHtml(rowsHtml);\n\n // Build full document\n let fullHtml = wrapInDocumentShell(cleanRowsHtml, cssBlock, bodyValues);\n\n // Process merge tags if provided\n if (options?.mergeTags) {\n for (const [tag, value] of Object.entries(options.mergeTags)) {\n fullHtml = fullHtml.replaceAll(`{{${tag}}}`, value);\n }\n }\n\n // Extract chunks\n const bodyMatch = fullHtml.match(/<body[^>]*>([\\s\\S]*)<\\/body>/i);\n const cssMatch = fullHtml.match(/<style[^>]*>([\\s\\S]*?)<\\/style>/gi);\n const fontsUsed: string[] = [];\n\n // Collect Google Fonts\n if (bodyValues.fontFamily?.url) {\n fontsUsed.push(bodyValues.fontFamily.url);\n }\n\n return {\n design: structuredClone(design),\n html: fullHtml,\n chunks: {\n body: bodyMatch?.[1] ?? rowsHtml,\n css: cssMatch?.map((s) => s.replace(/<\\/?style[^>]*>/gi, '')).join('\\n') ?? cssBlock,\n fonts: fontsUsed,\n js: '',\n },\n };\n}\n","/**\n * @module html-validator\n *\n * Validates exported email HTML against common email client compatibility rules.\n * Returns a list of warnings and errors that may cause rendering issues.\n */\n\nexport interface ValidationResult {\n valid: boolean;\n errors: ValidationIssue[];\n warnings: ValidationIssue[];\n score: number; // 0-100\n}\n\nexport interface ValidationIssue {\n rule: string;\n message: string;\n severity: 'error' | 'warning';\n line?: number;\n}\n\n/** Validate email HTML for cross-client compatibility */\nexport function validateEmailHtml(html: string): ValidationResult {\n const errors: ValidationIssue[] = [];\n const warnings: ValidationIssue[] = [];\n\n // ── Structure checks ──────────────────────────────────\n\n if (!html.includes('<!DOCTYPE')) {\n errors.push({ rule: 'doctype', message: 'Missing <!DOCTYPE> declaration. Required for consistent rendering.', severity: 'error' });\n }\n\n if (!html.includes('<meta') || !html.includes('charset')) {\n errors.push({ rule: 'charset', message: 'Missing charset meta tag. Add <meta charset=\"UTF-8\">.', severity: 'error' });\n }\n\n if (!html.includes('viewport')) {\n warnings.push({ rule: 'viewport', message: 'Missing viewport meta tag. Needed for responsive rendering on mobile.', severity: 'warning' });\n }\n\n // ── Table layout checks ───────────────────────────────\n\n const tableCount = (html.match(/<table/gi) || []).length;\n const presentationCount = (html.match(/role=\"presentation\"/gi) || []).length;\n if (tableCount > 0 && presentationCount < tableCount * 0.5) {\n warnings.push({ rule: 'table-role', message: `Only ${presentationCount}/${tableCount} tables have role=\"presentation\". Screen readers may interpret layout tables as data tables.`, severity: 'warning' });\n }\n\n // ── Image checks ──────────────────────────────────────\n\n const imgTags = html.match(/<img[^>]*>/gi) || [];\n for (const img of imgTags) {\n if (!img.includes('alt=')) {\n errors.push({ rule: 'img-alt', message: 'Image missing alt attribute. Required for accessibility and when images are blocked.', severity: 'error' });\n }\n if (!img.includes('style=') || !img.includes('display')) {\n warnings.push({ rule: 'img-display', message: 'Image without display:block may have a bottom gap in some email clients.', severity: 'warning' });\n }\n if (img.includes('width=\"100%\"') || (!img.includes('width=') && !img.includes('width:'))) {\n warnings.push({ rule: 'img-width', message: 'Image without explicit pixel width. Some email clients ignore percentage widths.', severity: 'warning' });\n }\n }\n\n // ── CSS checks ────────────────────────────────────────\n\n const styleBlocks = html.match(/<style[^>]*>[\\s\\S]*?<\\/style>/gi) || [];\n const hasInlineStyles = html.includes('style=\"');\n\n if (styleBlocks.length > 0 && !html.includes('@media')) {\n warnings.push({ rule: 'no-media-queries', message: 'Style block found but no @media queries. Consider adding responsive breakpoints.', severity: 'warning' });\n }\n\n if (!hasInlineStyles && tableCount > 0) {\n errors.push({ rule: 'inline-styles', message: 'No inline styles detected. Gmail and many clients strip <style> tags — inline styles are essential.', severity: 'error' });\n }\n\n // Check for unsupported CSS properties\n const unsupportedProps = ['position:fixed', 'position:absolute', 'display:flex', 'display:grid', 'gap:', 'transform:', 'animation:'];\n for (const prop of unsupportedProps) {\n if (html.includes(`style=\"`) && html.match(new RegExp(`style=\"[^\"]*${prop.replace(':', ':\\\\s*')}`, 'i'))) {\n warnings.push({ rule: 'unsupported-css', message: `CSS property \"${prop.replace(':', '')}\" not supported in most email clients.`, severity: 'warning' });\n }\n }\n\n // ── Outlook / MSO checks ──────────────────────────────\n\n if (!html.includes('<!--[if')) {\n warnings.push({ rule: 'mso-conditionals', message: 'No MSO conditional comments found. Outlook may not render multi-column layouts correctly.', severity: 'warning' });\n }\n\n if (!html.includes('xmlns:v') && !html.includes('xmlns:o')) {\n warnings.push({ rule: 'mso-namespace', message: 'Missing Microsoft Office XML namespaces. May affect Outlook rendering.', severity: 'warning' });\n }\n\n // ── Link checks ───────────────────────────────────────\n\n const links = html.match(/<a[^>]*>/gi) || [];\n const unstyledLinks = links.filter((link) => !link.includes('style='));\n if (unstyledLinks.length > 0) {\n warnings.push({ rule: 'link-color', message: `${unstyledLinks.length} link(s) without inline styles. Email clients override unstyled link colors.`, severity: 'warning' });\n }\n\n // ── Dark mode checks ──────────────────────────────────\n\n if (!html.includes('color-scheme') && !html.includes('prefers-color-scheme')) {\n warnings.push({ rule: 'dark-mode', message: 'No dark mode support detected. Consider adding color-scheme meta and prefers-color-scheme media query.', severity: 'warning' });\n }\n\n // ── Size checks ───────────────────────────────────────\n\n const sizeKB = Math.round(html.length / 1024);\n if (sizeKB > 102) {\n errors.push({ rule: 'size-limit', message: `Email HTML is ${sizeKB}KB. Gmail clips emails larger than 102KB.`, severity: 'error' });\n } else if (sizeKB > 80) {\n warnings.push({ rule: 'size-warning', message: `Email HTML is ${sizeKB}KB. Getting close to Gmail's 102KB clip limit.`, severity: 'warning' });\n }\n\n // ── Preheader check ───────────────────────────────────\n\n if (!html.includes('display:none') && !html.includes('max-height:0')) {\n warnings.push({ rule: 'preheader', message: 'No preheader text detected. Preheader text improves open rates in inbox previews.', severity: 'warning' });\n }\n\n // ── Score calculation ─────────────────────────────────\n\n const maxScore = 100;\n const errorPenalty = errors.length * 15;\n const warningPenalty = warnings.length * 5;\n const score = Math.max(0, maxScore - errorPenalty - warningPenalty);\n\n return {\n valid: errors.length === 0,\n errors,\n warnings,\n score,\n };\n}\n"],"names":["wrapInDocumentShell","bodyHtml","cssBlock","bodyValues","bgColor","contentWidth","fontFamily","textColor","preheaderContent","preheader","renderRow","row","toolRenderers","colsBgColor","padding","totalCells","a","b","bgStyle","bgImg","bgImage","repeat","size","position","needsGhostTable","columnEntries","col","i","colWidthPx","renderColumn","innerHtml","wrappedCols","colHtml","hideDesktop","hideMobile","widthPx","borderRadius","contentsHtml","content","renderer","ctx","html","getResponsiveCss","UNSUPPORTED_PROPS","PROP_REGEX","VAR_REGEX","sanitizeEmailHtml","match","styleContent","cleaned","renderDesignToHtml","design","options","rowsHtml","cleanRowsHtml","fullHtml","tag","value","bodyMatch","cssMatch","fontsUsed","s","validateEmailHtml","errors","warnings","tableCount","presentationCount","imgTags","img","styleBlocks","hasInlineStyles","unsupportedProps","prop","unstyledLinks","link","sizeKB","maxScore","errorPenalty","warningPenalty","score"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../src/layout/document-shell.ts","../src/layout/fluid-hybrid.ts","../src/utils/responsive-css.ts","../src/utils/html-sanitizer.ts","../src/render.ts","../src/utils/html-validator.ts"],"sourcesContent":["import type { BodyValues } from '@emabuild/types';\n\nexport function wrapInDocumentShell(bodyHtml: string, cssBlock: string, bodyValues: BodyValues): string {\n const bgColor = bodyValues.backgroundColor || '#e7e7e7';\n const contentWidth = bodyValues.contentWidth || '600px';\n const fontFamily = bodyValues.fontFamily?.value || 'arial,helvetica,sans-serif';\n const textColor = bodyValues.textColor || '#000000';\n const preheaderText = bodyValues.preheaderText || '';\n\n // Always include preheader wrapper — even if empty, the hidden div\n // prevents email clients from using the first visible text as preview\n const preheaderContent = preheaderText || '&zwnj;';\n const preheader = `<div style=\"display:none;font-size:1px;color:${bgColor};line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;\">${preheaderContent}${'&zwnj;&nbsp;'.repeat(80)}</div>`;\n\n return `<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n<head>\n <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n <meta name=\"x-apple-disable-message-reformatting\">\n <meta name=\"format-detection\" content=\"telephone=no,address=no,email=no,date=no,url=no\">\n <meta name=\"color-scheme\" content=\"light dark\">\n <meta name=\"supported-color-schemes\" content=\"light dark\">\n <title></title>\n <!--[if mso]>\n <noscript><xml>\n <o:OfficeDocumentSettings>\n <o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch>\n </o:OfficeDocumentSettings>\n </xml></noscript>\n <style type=\"text/css\">\n table, td, th { font-family: ${fontFamily} !important; }\n </style>\n <![endif]-->\n <!--[if !mso]><!-->\n <style type=\"text/css\">\n ${cssBlock}\n </style>\n <!--<![endif]-->\n <style type=\"text/css\">\n body { margin: 0; padding: 0; word-break: normal; }\n table, tr, td { vertical-align: top; border-collapse: collapse; }\n p { margin: 0; }\n a[x-apple-data-detectors='true'] { color: inherit !important; text-decoration: none !important; }\n </style>\n</head>\n<body class=\"clean-body u_body\" style=\"margin:0;padding:0;-webkit-text-size-adjust:100%;background-color:${bgColor};color:${textColor};\">\n ${preheader}\n <table id=\"u_body\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0;margin:0 auto;background-color:${bgColor};width:100%;\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\">\n <tbody>\n <tr>\n <td style=\"vertical-align:top;\">\n <!--[if (mso)|(IE)]><table width=\"${parseInt(contentWidth)}\" align=\"center\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr><td><![endif]-->\n ${bodyHtml}\n <!--[if (mso)|(IE)]></td></tr></table><![endif]-->\n </td>\n </tr>\n </tbody>\n </table>\n</body>\n</html>`;\n}\n","import type { DesignRow, DesignColumn, BodyValues } from '@emabuild/types';\n\ntype ContentRenderer = (values: Record<string, unknown>, ctx: any) => string;\n\nexport function renderRow(\n row: DesignRow,\n bodyValues: BodyValues,\n toolRenderers: Map<string, ContentRenderer>,\n): string {\n const contentWidth = parseInt(bodyValues.contentWidth || '600');\n const bgColor = row.values.backgroundColor || '';\n const colsBgColor = row.values.columnsBackgroundColor || '';\n const padding = row.values.padding || '0px';\n const totalCells = row.cells.reduce((a, b) => a + b, 0);\n\n const bgStyle = bgColor ? `background-color:${bgColor};` : '';\n const bgImg = row.values.backgroundImage;\n let bgImage = '';\n if (bgImg?.url) {\n // repeat can be: true, false, \"repeat\", \"no-repeat\", or a CSS value\n const repeat = bgImg.repeat === true || bgImg.repeat === 'repeat' ? 'repeat' : 'no-repeat';\n const size = bgImg.cover === true ? 'cover' : bgImg.fullWidth === true ? '100% auto' : 'auto';\n const position = bgImg.center !== false ? 'center top' : 'left top';\n bgImage = `background-image:url('${bgImg.url}');background-repeat:${repeat};background-position:${position};background-size:${size};`;\n }\n\n // Render columns once, wrap with MSO ghost table conditionals for multi-column\n const needsGhostTable = row.columns.length > 1;\n\n const columnEntries = row.columns.map((col, i) => {\n const colWidthPx = Math.round((row.cells[i] / totalCells) * contentWidth);\n const colHtml = renderColumn(col, colWidthPx, colsBgColor, bodyValues, toolRenderers);\n return { colHtml, colWidthPx };\n });\n\n let innerHtml: string;\n if (needsGhostTable) {\n // Wrap each column with MSO conditional <td> for Outlook.\n // For modern clients: inline-block columns inside a font-size:0 parent\n // (eliminates whitespace gaps between inline-block elements).\n const wrappedCols = columnEntries.map(({ colHtml, colWidthPx }) =>\n `<!--[if (mso)|(IE)]><td align=\"center\" width=\"${colWidthPx}\" style=\"width:${colWidthPx}px;padding:0px;\" valign=\"top\"><![endif]-->${colHtml}<!--[if (mso)|(IE)]></td><![endif]-->`\n );\n\n innerHtml = `<!--[if (mso)|(IE)]><table role=\"presentation\" width=\"${contentWidth}\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr><![endif]-->${wrappedCols.join('')}<!--[if (mso)|(IE)]></tr></table><![endif]-->`;\n } else {\n innerHtml = columnEntries.map(({ colHtml }) => colHtml).join('');\n }\n\n // Visibility classes\n const hideDesktop = row.values.hideDesktop ? ' u_hide_desktop' : '';\n const hideMobile = row.values.hideMobile ? ' u_hide_mobile' : '';\n\n // font-size:0 on the wrapper eliminates whitespace between inline-block columns\n const fsZero = needsGhostTable ? 'font-size:0;' : '';\n\n return `<div class=\"u_row${hideDesktop}${hideMobile}\" style=\"padding:${padding};${bgStyle}${bgImage}\">\n <div style=\"margin:0 auto;max-width:${contentWidth}px;${fsZero}text-align:center;\">${innerHtml}</div>\n</div>`;\n}\n\nfunction renderColumn(\n col: DesignColumn,\n widthPx: number,\n colsBgColor: string,\n bodyValues: BodyValues,\n toolRenderers: Map<string, ContentRenderer>,\n): string {\n const bgColor = col.values.backgroundColor || colsBgColor || '';\n const padding = col.values.padding || '0px';\n const borderRadius = col.values.borderRadius || '0px';\n const bgStyle = bgColor ? `background-color:${bgColor};` : '';\n\n const contentsHtml = col.contents\n .map((content) => {\n const renderer = toolRenderers.get(content.type);\n if (!renderer) return `<!-- unknown tool: ${content.type} -->`;\n const ctx = {\n columnWidth: widthPx,\n displayMode: 'email',\n contentWidth: parseInt(bodyValues.contentWidth || '600'),\n bodyValues,\n };\n let html = renderer(content.values, ctx);\n\n // Wrap with visibility classes if hideDesktop/hideMobile is set\n const hideDesktop = !!content.values.hideDesktop;\n const hideMobile = !!content.values.hideMobile;\n if (hideDesktop || hideMobile) {\n const cls = [hideDesktop && 'u_hide_desktop', hideMobile && 'u_hide_mobile'].filter(Boolean).join(' ');\n html = `<div class=\"${cls}\">${html}</div>`;\n }\n\n return html;\n })\n .join('\\n');\n\n return `<div class=\"u_column\" style=\"display:inline-block;vertical-align:top;width:${widthPx}px;max-width:${widthPx}px;font-size:14px;text-align:left;\">\n <div style=\"width:100%;${bgStyle}${borderRadius !== '0px' ? `border-radius:${borderRadius};` : ''}\">\n <div style=\"padding:${padding};\">\n ${contentsHtml || '&nbsp;'}\n </div>\n </div>\n</div>`;\n}\n","export function getResponsiveCss(contentWidth: number): string {\n return `\n@media only screen and (min-width: ${contentWidth + 20}px) {\n .u_row .u_column { display: inline-block !important; }\n}\n\n@media only screen and (max-width: ${contentWidth + 20}px) {\n .u_row .u_column {\n display: block !important;\n width: 100% !important;\n max-width: 100% !important;\n }\n .u_row {\n width: 100% !important;\n }\n}\n\n@media only screen and (max-width: 620px) {\n .u_row-container {\n max-width: 100% !important;\n padding-left: 0 !important;\n padding-right: 0 !important;\n }\n}\n\n@media (prefers-color-scheme: dark) {\n /* Dark mode overrides — add per-client rules as needed */\n}\n\n/* Outlook dark mode */\n[data-ogsb] body,\n[data-ogsb] table,\n[data-ogsb] td {\n /* Preserve original colors */\n}\n\n.u_hide_desktop { display: block !important; }\n.u_hide_mobile { display: block !important; }\n\n@media only screen and (max-width: ${contentWidth + 20}px) {\n .u_hide_desktop { display: block !important; }\n .u_hide_mobile { display: none !important; }\n}\n\n@media only screen and (min-width: ${contentWidth + 21}px) {\n .u_hide_desktop { display: none !important; }\n .u_hide_mobile { display: block !important; }\n}`;\n}\n","/**\n * @module html-sanitizer\n *\n * Sanitizes user-generated HTML content for email compatibility.\n * Removes CSS properties that are not supported by email clients\n * and replaces CSS variables with fallback values.\n */\n\n/** CSS properties to strip from inline styles.\n * Only truly unsupported/harmful properties — keep harmless ones\n * like float:none, display:inline that are part of user content. */\nconst UNSUPPORTED_PROPS = [\n 'box-sizing',\n 'overflow-wrap',\n 'word-break',\n 'word-wrap',\n 'outline',\n 'cursor',\n 'transition',\n 'animation',\n 'transform',\n 'z-index',\n 'display:\\\\s*flex',\n 'display:\\\\s*grid',\n 'gap',\n];\n\n/** Build a single regex that matches any unsupported property in a style attribute */\nconst PROP_REGEX = new RegExp(\n `(?:;\\\\s*|^\\\\s*)(${UNSUPPORTED_PROPS.join('|')})\\\\s*:[^;]*;?`,\n 'gi',\n);\n\n/** Replace CSS var() with empty string (email clients don't support CSS variables) */\nconst VAR_REGEX = /var\\(--[^)]*\\)/gi;\n\n/**\n * Clean inline styles in HTML content to remove email-unsupported CSS.\n * Processes `style=\"...\"` attributes and removes problematic properties.\n *\n * @param html - Raw HTML content (e.g. from a text/heading block)\n * @returns Cleaned HTML with email-safe inline styles\n */\nexport function sanitizeEmailHtml(html: string): string {\n // Process each style=\"...\" attribute\n return html.replace(/style=\"([^\"]*)\"/gi, (match, styleContent: string) => {\n let cleaned = styleContent;\n\n // Remove unsupported properties\n cleaned = cleaned.replace(PROP_REGEX, '');\n\n // Replace CSS variables with empty/inherit\n cleaned = cleaned.replace(VAR_REGEX, 'inherit');\n\n // Clean up multiple semicolons and whitespace\n cleaned = cleaned.replace(/;\\s*;/g, ';').replace(/^\\s*;\\s*/, '').replace(/;\\s*$/, '').trim();\n\n return cleaned ? `style=\"${cleaned}\"` : '';\n });\n}\n","import type { EmailDesign, ExportResult, ExportOptions, BodyValues } from '@emabuild/types';\nimport { wrapInDocumentShell } from './layout/document-shell.js';\nimport { renderRow } from './layout/fluid-hybrid.js';\nimport { getResponsiveCss } from './utils/responsive-css.js';\nimport { sanitizeEmailHtml } from './utils/html-sanitizer.js';\n\ntype ContentRenderer = (values: Record<string, unknown>, ctx: any) => string;\n\nexport function renderDesignToHtml(\n design: EmailDesign,\n toolRenderers: Map<string, ContentRenderer>,\n options?: ExportOptions,\n): ExportResult {\n const bodyValues = design.body.values;\n const contentWidth = parseInt(bodyValues.contentWidth || '600');\n\n // Render all rows\n const rowsHtml = design.body.rows\n .map((row) => renderRow(row, bodyValues, toolRenderers))\n .join('\\n');\n\n // Build responsive CSS\n const cssBlock = getResponsiveCss(contentWidth);\n\n // Sanitize user content (remove email-unsupported CSS)\n const cleanRowsHtml = sanitizeEmailHtml(rowsHtml);\n\n // Build full document\n let fullHtml = wrapInDocumentShell(cleanRowsHtml, cssBlock, bodyValues);\n\n // Process merge tags if provided\n if (options?.mergeTags) {\n for (const [tag, value] of Object.entries(options.mergeTags)) {\n fullHtml = fullHtml.replaceAll(`{{${tag}}}`, value);\n }\n }\n\n // Extract chunks\n const bodyMatch = fullHtml.match(/<body[^>]*>([\\s\\S]*)<\\/body>/i);\n const cssMatch = fullHtml.match(/<style[^>]*>([\\s\\S]*?)<\\/style>/gi);\n const fontsUsed: string[] = [];\n\n // Collect Google Fonts\n if (bodyValues.fontFamily?.url) {\n fontsUsed.push(bodyValues.fontFamily.url);\n }\n\n return {\n design: structuredClone(design),\n html: fullHtml,\n chunks: {\n body: bodyMatch?.[1] ?? rowsHtml,\n css: cssMatch?.map((s) => s.replace(/<\\/?style[^>]*>/gi, '')).join('\\n') ?? cssBlock,\n fonts: fontsUsed,\n js: '',\n },\n };\n}\n","/**\n * @module html-validator\n *\n * Validates exported email HTML against common email client compatibility rules.\n * Returns a list of warnings and errors that may cause rendering issues.\n */\n\nexport interface ValidationResult {\n valid: boolean;\n errors: ValidationIssue[];\n warnings: ValidationIssue[];\n score: number; // 0-100\n}\n\nexport interface ValidationIssue {\n rule: string;\n message: string;\n severity: 'error' | 'warning';\n line?: number;\n}\n\n/** Validate email HTML for cross-client compatibility */\nexport function validateEmailHtml(html: string): ValidationResult {\n const errors: ValidationIssue[] = [];\n const warnings: ValidationIssue[] = [];\n\n // ── Structure checks ──────────────────────────────────\n\n if (!html.includes('<!DOCTYPE')) {\n errors.push({ rule: 'doctype', message: 'Missing <!DOCTYPE> declaration. Required for consistent rendering.', severity: 'error' });\n }\n\n if (!html.includes('<meta') || !html.includes('charset')) {\n errors.push({ rule: 'charset', message: 'Missing charset meta tag. Add <meta charset=\"UTF-8\">.', severity: 'error' });\n }\n\n if (!html.includes('viewport')) {\n warnings.push({ rule: 'viewport', message: 'Missing viewport meta tag. Needed for responsive rendering on mobile.', severity: 'warning' });\n }\n\n // ── Table layout checks ───────────────────────────────\n\n const tableCount = (html.match(/<table/gi) || []).length;\n const presentationCount = (html.match(/role=\"presentation\"/gi) || []).length;\n if (tableCount > 0 && presentationCount < tableCount * 0.5) {\n warnings.push({ rule: 'table-role', message: `Only ${presentationCount}/${tableCount} tables have role=\"presentation\". Screen readers may interpret layout tables as data tables.`, severity: 'warning' });\n }\n\n // ── Image checks ──────────────────────────────────────\n\n const imgTags = html.match(/<img[^>]*>/gi) || [];\n for (const img of imgTags) {\n if (!img.includes('alt=')) {\n errors.push({ rule: 'img-alt', message: 'Image missing alt attribute. Required for accessibility and when images are blocked.', severity: 'error' });\n }\n if (!img.includes('style=') || !img.includes('display')) {\n warnings.push({ rule: 'img-display', message: 'Image without display:block may have a bottom gap in some email clients.', severity: 'warning' });\n }\n if (img.includes('width=\"100%\"') || (!img.includes('width=') && !img.includes('width:'))) {\n warnings.push({ rule: 'img-width', message: 'Image without explicit pixel width. Some email clients ignore percentage widths.', severity: 'warning' });\n }\n }\n\n // ── CSS checks ────────────────────────────────────────\n\n const styleBlocks = html.match(/<style[^>]*>[\\s\\S]*?<\\/style>/gi) || [];\n const hasInlineStyles = html.includes('style=\"');\n\n if (styleBlocks.length > 0 && !html.includes('@media')) {\n warnings.push({ rule: 'no-media-queries', message: 'Style block found but no @media queries. Consider adding responsive breakpoints.', severity: 'warning' });\n }\n\n if (!hasInlineStyles && tableCount > 0) {\n errors.push({ rule: 'inline-styles', message: 'No inline styles detected. Gmail and many clients strip <style> tags — inline styles are essential.', severity: 'error' });\n }\n\n // Check for unsupported CSS properties\n const unsupportedProps = ['position:fixed', 'position:absolute', 'display:flex', 'display:grid', 'gap:', 'transform:', 'animation:'];\n for (const prop of unsupportedProps) {\n if (html.includes(`style=\"`) && html.match(new RegExp(`style=\"[^\"]*${prop.replace(':', ':\\\\s*')}`, 'i'))) {\n warnings.push({ rule: 'unsupported-css', message: `CSS property \"${prop.replace(':', '')}\" not supported in most email clients.`, severity: 'warning' });\n }\n }\n\n // ── Outlook / MSO checks ──────────────────────────────\n\n if (!html.includes('<!--[if')) {\n warnings.push({ rule: 'mso-conditionals', message: 'No MSO conditional comments found. Outlook may not render multi-column layouts correctly.', severity: 'warning' });\n }\n\n if (!html.includes('xmlns:v') && !html.includes('xmlns:o')) {\n warnings.push({ rule: 'mso-namespace', message: 'Missing Microsoft Office XML namespaces. May affect Outlook rendering.', severity: 'warning' });\n }\n\n // ── Link checks ───────────────────────────────────────\n\n const links = html.match(/<a[^>]*>/gi) || [];\n const unstyledLinks = links.filter((link) => !link.includes('style='));\n if (unstyledLinks.length > 0) {\n warnings.push({ rule: 'link-color', message: `${unstyledLinks.length} link(s) without inline styles. Email clients override unstyled link colors.`, severity: 'warning' });\n }\n\n // ── Dark mode checks ──────────────────────────────────\n\n if (!html.includes('color-scheme') && !html.includes('prefers-color-scheme')) {\n warnings.push({ rule: 'dark-mode', message: 'No dark mode support detected. Consider adding color-scheme meta and prefers-color-scheme media query.', severity: 'warning' });\n }\n\n // ── Size checks ───────────────────────────────────────\n\n const sizeKB = Math.round(html.length / 1024);\n if (sizeKB > 102) {\n errors.push({ rule: 'size-limit', message: `Email HTML is ${sizeKB}KB. Gmail clips emails larger than 102KB.`, severity: 'error' });\n } else if (sizeKB > 80) {\n warnings.push({ rule: 'size-warning', message: `Email HTML is ${sizeKB}KB. Getting close to Gmail's 102KB clip limit.`, severity: 'warning' });\n }\n\n // ── Preheader check ───────────────────────────────────\n\n if (!html.includes('display:none') && !html.includes('max-height:0')) {\n warnings.push({ rule: 'preheader', message: 'No preheader text detected. Preheader text improves open rates in inbox previews.', severity: 'warning' });\n }\n\n // ── Score calculation ─────────────────────────────────\n\n const maxScore = 100;\n const errorPenalty = errors.length * 15;\n const warningPenalty = warnings.length * 5;\n const score = Math.max(0, maxScore - errorPenalty - warningPenalty);\n\n return {\n valid: errors.length === 0,\n errors,\n warnings,\n score,\n };\n}\n"],"names":["wrapInDocumentShell","bodyHtml","cssBlock","bodyValues","bgColor","contentWidth","fontFamily","textColor","preheaderContent","preheader","renderRow","row","toolRenderers","colsBgColor","padding","totalCells","a","b","bgStyle","bgImg","bgImage","repeat","size","position","needsGhostTable","columnEntries","col","i","colWidthPx","renderColumn","innerHtml","wrappedCols","colHtml","hideDesktop","hideMobile","widthPx","borderRadius","contentsHtml","content","renderer","ctx","html","getResponsiveCss","UNSUPPORTED_PROPS","PROP_REGEX","VAR_REGEX","sanitizeEmailHtml","match","styleContent","cleaned","renderDesignToHtml","design","options","rowsHtml","cleanRowsHtml","fullHtml","tag","value","bodyMatch","cssMatch","fontsUsed","s","validateEmailHtml","errors","warnings","tableCount","presentationCount","imgTags","img","styleBlocks","hasInlineStyles","unsupportedProps","prop","unstyledLinks","link","sizeKB","maxScore","errorPenalty","warningPenalty","score"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emabuild/email-renderer",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "Email HTML renderer — converts design JSON to cross-client email HTML",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -17,7 +17,7 @@
17
17
  "README.md"
18
18
  ],
19
19
  "dependencies": {
20
- "@emabuild/types": "0.1.4"
20
+ "@emabuild/types": "0.2.0"
21
21
  },
22
22
  "devDependencies": {
23
23
  "vite": "^6.2.0",