@emabuild/email-renderer 0.0.3 → 0.0.4

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 ADDED
@@ -0,0 +1,77 @@
1
+ # @emabuild/email-renderer
2
+
3
+ Standalone email HTML renderer for the [@emabuild](https://www.npmjs.com/package/@emabuild/core) email editor. Converts design JSON into cross-client email HTML.
4
+
5
+ Works in both browser and Node.js — use it server-side to generate email HTML without the editor UI.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @emabuild/email-renderer
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```typescript
16
+ import { renderDesignToHtml } from '@emabuild/email-renderer';
17
+
18
+ // Tool renderers map: tool name → HTML render function
19
+ const toolRenderers = new Map();
20
+
21
+ toolRenderers.set('text', (values, ctx) => {
22
+ return `<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
23
+ <tr><td style="padding:${values.containerPadding || '10px'};">
24
+ ${values.text}
25
+ </td></tr>
26
+ </table>`;
27
+ });
28
+
29
+ // Render design to HTML
30
+ const result = renderDesignToHtml(designJson, toolRenderers, {
31
+ mergeTags: { first_name: 'John' },
32
+ });
33
+
34
+ console.log(result.html); // Full HTML document
35
+ console.log(result.design); // Design JSON (for saving)
36
+ console.log(result.chunks); // { body, css, fonts[], js }
37
+ ```
38
+
39
+ ## Export Options
40
+
41
+ ```typescript
42
+ renderDesignToHtml(design, toolRenderers, {
43
+ minify: true, // Minify HTML output
44
+ inlineStyles: true, // Inline CSS into style attributes
45
+ cleanup: true, // Remove unused CSS
46
+ mergeTags: { // Replace {{tag}} with values
47
+ first_name: 'John',
48
+ },
49
+ });
50
+ ```
51
+
52
+ ## Email Client Support
53
+
54
+ The generated HTML uses fluid hybrid design with MSO conditional comments:
55
+
56
+ - Gmail (Web, iOS, Android)
57
+ - Outlook (2016, 2019, 365, Outlook.com)
58
+ - Apple Mail (macOS, iOS)
59
+ - Yahoo Mail, Thunderbird, Samsung Mail
60
+
61
+ ## Exported Functions
62
+
63
+ | Function | Description |
64
+ |----------|-------------|
65
+ | `renderDesignToHtml()` | Main export: design JSON → full HTML document |
66
+ | `wrapInDocumentShell()` | Wrap body HTML in email-safe DOCTYPE/head/body |
67
+ | `renderRow()` | Render a single row with fluid hybrid columns |
68
+ | `getResponsiveCss()` | Generate responsive CSS media queries |
69
+
70
+ ## Related Packages
71
+
72
+ - [`@emabuild/core`](https://www.npmjs.com/package/@emabuild/core) — Full drag & drop editor Web Component
73
+ - [`@emabuild/types`](https://www.npmjs.com/package/@emabuild/types) — TypeScript type definitions
74
+
75
+ ## License
76
+
77
+ MIT
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/render.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 const preheader = preheaderText\n ? `<div style=\"display:none;font-size:1px;color:${bgColor};line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;\">${preheaderText}${'&zwnj;&nbsp;'.repeat(80)}</div>`\n : '';\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; }\n table, tr, td { vertical-align: top; border-collapse: collapse; }\n p { margin: 0; }\n .ie-container table, .mso-container table { table-layout: fixed; }\n * { line-height: inherit; }\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\" style=\"border-collapse:collapse;table-layout:fixed;border-spacing:0;mso-table-lspace:0pt;mso-table-rspace:0pt;vertical-align:top;min-width:320px;margin:0 auto;background-color:${bgColor};width:100%;\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\">\n <tbody>\n <tr style=\"vertical-align:top;\">\n <td style=\"word-break:break-word;border-collapse:collapse !important;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 bgImage = row.values.backgroundImage?.url\n ? `background-image:url('${row.values.backgroundImage.url}');background-repeat:${row.values.backgroundImage.repeat ? 'repeat' : 'no-repeat'};background-position:center top;background-size:${row.values.backgroundImage.cover ? 'cover' : 'auto'};`\n : '';\n\n const columnsHtml = row.columns.map((col, i) => {\n const colWidthPx = Math.round((row.cells[i] / totalCells) * contentWidth);\n return renderColumn(col, colWidthPx, colsBgColor, bodyValues, toolRenderers);\n });\n\n // For multi-column layouts, use MSO ghost tables\n const needsGhostTable = row.columns.length > 1;\n\n let innerHtml: string;\n if (needsGhostTable) {\n const ghostCols = 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 `<!--[if (mso)|(IE)]><td align=\"center\" width=\"${colWidthPx}\" style=\"width:${colWidthPx}px;padding:0px;border:none;\" valign=\"top\"><![endif]-->\n${colHtml}\n<!--[if (mso)|(IE)]></td><![endif]-->`;\n });\n\n innerHtml = `<!--[if (mso)|(IE)]><table role=\"presentation\" width=\"${contentWidth}\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr>${ghostCols.join('\\n')}</tr></table><![endif]-->\n\n<!--[if !mso]><!-->\n<div style=\"max-width:${contentWidth}px;margin:0 auto;\">\n${columnsHtml.join('\\n')}\n</div>\n<!--<![endif]-->`;\n } else {\n innerHtml = columnsHtml.join('\\n');\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 return `<div class=\"u_row${hideDesktop}${hideMobile}\" style=\"padding:${padding};${bgStyle}${bgImage}\">\n <div style=\"margin:0 auto;min-width:320px;max-width:${contentWidth}px;overflow-wrap:break-word;word-wrap:break-word;word-break:break-word;background-color:transparent;\">\n <div style=\"border-collapse:collapse;display:table;width:100%;height:100%;background-color:transparent;\">\n ${innerHtml}\n </div>\n </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 return renderer(content.values, ctx);\n })\n .join('\\n');\n\n return `<div class=\"u_column\" style=\"max-width:${widthPx}px;min-width:${Math.min(widthPx, 320)}px;display:table-cell;vertical-align:top;\">\n <div style=\"height:100%;width:100% !important;border-radius:${borderRadius};-webkit-border-radius:${borderRadius};${bgStyle}\">\n <div style=\"box-sizing:border-box;height:100%;padding:${padding};border:none;border-radius:${borderRadius};-webkit-border-radius:${borderRadius};\">\n ${contentsHtml || '<!--[if (!mso)&(!IE)]><!--><div style=\"height:0;min-height:1px;font-size:0;\">&nbsp;</div><!--<![endif]-->'}\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: table-cell; }\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 min-width: 320px !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","import type { UnlayerDesign, 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';\n\ntype ContentRenderer = (values: Record<string, unknown>, ctx: any) => string;\n\nexport function renderDesignToHtml(\n design: UnlayerDesign,\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 // Build full document\n let fullHtml = wrapInDocumentShell(rowsHtml, 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"],"names":[],"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/render.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 const preheader = preheaderText\n ? `<div style=\"display:none;font-size:1px;color:${bgColor};line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;\">${preheaderText}${'&zwnj;&nbsp;'.repeat(80)}</div>`\n : '';\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; }\n table, tr, td { vertical-align: top; border-collapse: collapse; }\n p { margin: 0; }\n .ie-container table, .mso-container table { table-layout: fixed; }\n * { line-height: inherit; }\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\" style=\"border-collapse:collapse;table-layout:fixed;border-spacing:0;mso-table-lspace:0pt;mso-table-rspace:0pt;vertical-align:top;min-width:320px;margin:0 auto;background-color:${bgColor};width:100%;\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\">\n <tbody>\n <tr style=\"vertical-align:top;\">\n <td style=\"word-break:break-word;border-collapse:collapse !important;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 bgImage = row.values.backgroundImage?.url\n ? `background-image:url('${row.values.backgroundImage.url}');background-repeat:${row.values.backgroundImage.repeat ? 'repeat' : 'no-repeat'};background-position:center top;background-size:${row.values.backgroundImage.cover ? 'cover' : 'auto'};`\n : '';\n\n const columnsHtml = row.columns.map((col, i) => {\n const colWidthPx = Math.round((row.cells[i] / totalCells) * contentWidth);\n return renderColumn(col, colWidthPx, colsBgColor, bodyValues, toolRenderers);\n });\n\n // For multi-column layouts, use MSO ghost tables\n const needsGhostTable = row.columns.length > 1;\n\n let innerHtml: string;\n if (needsGhostTable) {\n const ghostCols = 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 `<!--[if (mso)|(IE)]><td align=\"center\" width=\"${colWidthPx}\" style=\"width:${colWidthPx}px;padding:0px;border:none;\" valign=\"top\"><![endif]-->\n${colHtml}\n<!--[if (mso)|(IE)]></td><![endif]-->`;\n });\n\n innerHtml = `<!--[if (mso)|(IE)]><table role=\"presentation\" width=\"${contentWidth}\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr>${ghostCols.join('\\n')}</tr></table><![endif]-->\n\n<!--[if !mso]><!-->\n<div style=\"max-width:${contentWidth}px;margin:0 auto;\">\n${columnsHtml.join('\\n')}\n</div>\n<!--<![endif]-->`;\n } else {\n innerHtml = columnsHtml.join('\\n');\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 return `<div class=\"u_row${hideDesktop}${hideMobile}\" style=\"padding:${padding};${bgStyle}${bgImage}\">\n <div style=\"margin:0 auto;min-width:320px;max-width:${contentWidth}px;overflow-wrap:break-word;word-wrap:break-word;word-break:break-word;background-color:transparent;\">\n <div style=\"border-collapse:collapse;display:table;width:100%;height:100%;background-color:transparent;\">\n ${innerHtml}\n </div>\n </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 return renderer(content.values, ctx);\n })\n .join('\\n');\n\n return `<div class=\"u_column\" style=\"max-width:${widthPx}px;min-width:${Math.min(widthPx, 320)}px;display:table-cell;vertical-align:top;\">\n <div style=\"height:100%;width:100% !important;border-radius:${borderRadius};-webkit-border-radius:${borderRadius};${bgStyle}\">\n <div style=\"box-sizing:border-box;height:100%;padding:${padding};border:none;border-radius:${borderRadius};-webkit-border-radius:${borderRadius};\">\n ${contentsHtml || '<!--[if (!mso)&(!IE)]><!--><div style=\"height:0;min-height:1px;font-size:0;\">&nbsp;</div><!--<![endif]-->'}\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: table-cell; }\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 min-width: 320px !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","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';\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 // Build full document\n let fullHtml = wrapInDocumentShell(rowsHtml, 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"],"names":[],"mappings":"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"}
package/dist/render.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { UnlayerDesign, ExportResult, ExportOptions } from '@emabuild/types';
1
+ import { EmailDesign, ExportResult, ExportOptions } from '@emabuild/types';
2
2
  type ContentRenderer = (values: Record<string, unknown>, ctx: any) => string;
3
- export declare function renderDesignToHtml(design: UnlayerDesign, toolRenderers: Map<string, ContentRenderer>, options?: ExportOptions): ExportResult;
3
+ export declare function renderDesignToHtml(design: EmailDesign, toolRenderers: Map<string, ContentRenderer>, options?: ExportOptions): ExportResult;
4
4
  export {};
5
5
  //# sourceMappingURL=render.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAc,MAAM,iBAAiB,CAAC;AAK9F,KAAK,eAAe,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,GAAG,KAAK,MAAM,CAAC;AAE7E,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,aAAa,EACrB,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,EAC3C,OAAO,CAAC,EAAE,aAAa,GACtB,YAAY,CA0Cd"}
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAc,MAAM,iBAAiB,CAAC;AAK5F,KAAK,eAAe,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,GAAG,KAAK,MAAM,CAAC;AAE7E,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,WAAW,EACnB,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,EAC3C,OAAO,CAAC,EAAE,aAAa,GACtB,YAAY,CA0Cd"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@emabuild/email-renderer",
3
- "version": "0.0.3",
4
- "description": "Email HTML renderer — converts Unlayer-compatible design JSON to cross-client email HTML",
3
+ "version": "0.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",
7
7
  "module": "./dist/index.js",
@@ -18,7 +18,7 @@
18
18
  "README.md"
19
19
  ],
20
20
  "dependencies": {
21
- "@emabuild/types": "0.0.3"
21
+ "@emabuild/types": "0.0.4"
22
22
  },
23
23
  "devDependencies": {
24
24
  "vite": "^6.2.0",
@@ -27,6 +27,15 @@
27
27
  "publishConfig": {
28
28
  "access": "public"
29
29
  },
30
+ "keywords": [
31
+ "email",
32
+ "html-email",
33
+ "email-renderer",
34
+ "email-template",
35
+ "responsive-email",
36
+ "outlook",
37
+ "gmail"
38
+ ],
30
39
  "license": "MIT",
31
40
  "sideEffects": false,
32
41
  "scripts": {