import { PageLayout, LayoutContainer, WidgetInstance, AnyContainer, FlexibleContainer, ColumnDef } from '@/modules/layout/LayoutManager'; import { widgetRegistry } from './widgetRegistry'; import { template } from '@/lib/variables'; import { marked } from 'marked'; export const generateEmailHtml = async (layout: PageLayout, rootTemplateUrl: string): Promise => { try { // 1. Fetch Root Template const rootRes = await fetch(rootTemplateUrl); if (!rootRes.ok) throw new Error(`Failed to load root template: ${rootTemplateUrl}`); let rootHtml = await rootRes.text(); // 2. Resolve Root Template Variables (e.g. ${title}) // Map layout properties to variables. We map 'name' to 'title' for common usage. const rootVars: Record = { ...layout, title: layout.name, SOURCE: '${SOURCE}' }; rootHtml = template(rootHtml, rootVars, false); // 3. Generate Content Body const contentHtml = await generateContainerHtml(layout.containers); // 4. Inject Content if (rootHtml.includes('${SOURCE}')) { return rootHtml.replace('${SOURCE}', contentHtml); } else { return rootHtml.replace('', `${contentHtml}`); } } catch (error) { console.error("Email generation failed:", error); throw error; } }; const generateContainerHtml = async (containers: AnyContainer[]): Promise => { let html = ''; // Sort containers const sortedContainers = [...containers].sort((a, b) => (a.order || 0) - (b.order || 0)); for (const container of sortedContainers) { if (container.type === 'flex-container') { html += await generateFlexContainerHtml(container as FlexibleContainer); continue; } const gap = container.gap || 0; // Container Table html += ``; html += ``; html += `
`; html += ``; if (container.columns === 1) { // Stacked vertical html += ``; } else { // Grid Layout const colWidth = Math.floor(100 / container.columns); const sortedWidgets = [...container.widgets].sort((a, b) => (a.order || 0) - (b.order || 0)); for (let i = 0; i < sortedWidgets.length; i += container.columns) { const rowWidgets = sortedWidgets.slice(i, i + container.columns); if (i > 0) html += ``; for (const widget of rowWidgets) { html += ``; } // Filler cells if (rowWidgets.length < container.columns) { for (let j = rowWidgets.length; j < container.columns; j++) { html += ``; } } } } html += `
`; const sortedWidgets = [...container.widgets].sort((a, b) => (a.order || 0) - (b.order || 0)); for (const widget of sortedWidgets) { html += await generateWidgetHtml(widget); } if (container.children?.length > 0) { html += await generateContainerHtml(container.children); } html += `
`; html += await generateWidgetHtml(widget); html += ` 
`; html += `
`; } return html; }; const generateWidgetHtml = async (widget: WidgetInstance): Promise => { const def = widgetRegistry.get(widget.widgetId); if (!def) return ``; const templateUrl = def.metadata.defaultProps?.__templateUrl; if (templateUrl) { try { const res = await fetch(templateUrl); if (res.ok) { const content = await res.text(); // Process props for markdown conversion const processedProps = { ...(widget.props || {}) }; if (def.metadata.configSchema) { for (const [key, schema] of Object.entries(def.metadata.configSchema)) { if ((schema as any).type === 'markdown' && typeof processedProps[key] === 'string') { try { processedProps[key] = await marked.parse(processedProps[key]); } catch (e) { console.warn(`Failed to parse markdown for widget ${widget.widgetId} prop ${key}`, e); } } } } // Perform Substitution using helper return template(content, processedProps, false); } } catch (e) { console.error(`Failed to fetch template for ${widget.widgetId}`, e); } } return ``; }; // ─── Flex Container Email Rendering ───────────────────────────────────── function columnsToPercentages(columns: ColumnDef[]): number[] { const totalFr = columns.reduce((sum, c) => { if (c.unit === 'fr') return sum + c.width; return sum; }, 0); if (totalFr === 0) { // Fallback: equal distribution return columns.map(() => Math.floor(100 / columns.length)); } return columns.map(c => { if (c.unit === '%') return c.width; if (c.unit === 'fr') return Math.round((c.width / totalFr) * 100); // px/rem: just use equal fallback return Math.floor(100 / columns.length); }); } const generateFlexContainerHtml = async (container: FlexibleContainer): Promise => { const gap = container.gap || 0; let html = ''; // Group widgets by cell const widgetsByCell = new Map(); const sortedWidgets = [...container.widgets].sort((a, b) => (a.order || 0) - (b.order || 0)); for (const w of sortedWidgets) { const key = `${(w as any).rowId || ''}:${(w as any).column ?? 0}`; const arr = widgetsByCell.get(key) || []; arr.push(w); widgetsByCell.set(key, arr); } for (const row of container.rows) { const percentages = columnsToPercentages(row.columns); const colGap = row.gap ?? gap; const halfGap = Math.round(colGap / 2); const colCount = row.columns.length; // Outer row wrapper html += ``; html += ``; html += `
`; // MSO conditional for Outlook html += ``; for (let colIdx = 0; colIdx < colCount; colIdx++) { const pct = percentages[colIdx]; const cellKey = `${row.id}:${colIdx}`; const cellWidgets = widgetsByCell.get(cellKey) || []; const padLeft = colIdx > 0 ? halfGap : 0; const padRight = colIdx < colCount - 1 ? halfGap : 0; const padStyle = `padding-left:${padLeft}px; padding-right:${padRight}px;`; // MSO td html += ``; // Modern column using inline-block html += `
`; for (const widget of cellWidgets) { html += await generateWidgetHtml(widget); } if (cellWidgets.length === 0) { html += ' '; } html += `
`; html += ``; } html += ``; html += `
`; } return html; }; export { columnsToPercentages };