import mjml2html from 'mjml' function getPath(data, path) { return String(path || '').split('.').filter(Boolean).reduce((value, key) => value?.[key], data) } export function escapeHtml(value) { return String(value ?? '') .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", ''') } export function interpolate(input, data) { return String(input ?? '').replace(/{{\s*([\w.[\]-]+)\s*}}/g, (_, path) => { const normalized = path.replace(/\[(\d+)\]/g, '.$1') const value = getPath(data, normalized) return value == null ? '' : String(value) }) } function safeUrl(input, data) { const url = interpolate(input, data).trim() return /^(https?:|mailto:)/i.test(url) ? url : '#' } function safeHex(value, fallback) { const color = String(value || '').trim() return /^#[0-9a-f]{3}([0-9a-f]{3})?$/i.test(color) ? color : fallback } function safeNumber(value, fallback, min, max) { const number = Number(value) if (!Number.isFinite(number)) return fallback return Math.min(max, Math.max(min, number)) } function safeAlign(value) { return ['left', 'center', 'right'].includes(value) ? value : 'left' } function safeFont(value) { const font = String(value || '') const allowed = ['Arial', 'Helvetica', 'Verdana', 'Georgia', 'Times New Roman', 'system-ui'] return allowed.includes(font) ? font : 'Arial' } function themeOf(template) { const theme = template?.theme || {} return { backgroundColor: safeHex(theme.backgroundColor, '#f8fafc'), surfaceColor: safeHex(theme.surfaceColor, '#ffffff'), textColor: safeHex(theme.textColor, '#172033'), mutedColor: safeHex(theme.mutedColor, '#64748b'), accentColor: safeHex(theme.accentColor, '#111827'), buttonTextColor: safeHex(theme.buttonTextColor, '#ffffff'), borderColor: safeHex(theme.borderColor, '#dbe2ea'), borderRadius: safeNumber(theme.borderRadius, 12, 0, 32), pagePadding: safeNumber(theme.pagePadding, 18, 0, 40), fontFamily: safeFont(theme.fontFamily), pageBackgroundImage: safeUrl(theme.pageBackgroundImage, {}), borderImage: safeUrl(theme.borderImage, {}) } } function reportBlockStyle(block, theme) { const style = block?.style || {} const rules = [ `background:${safeHex(style.backgroundColor, 'transparent')}`, `color:${safeHex(style.textColor, theme.textColor)}`, `border-color:${safeHex(style.borderColor, theme.borderColor)}`, `border-width:${safeNumber(style.borderWidth, 0, 0, 8)}px`, 'border-style:solid', `border-radius:${safeNumber(style.borderRadius, theme.borderRadius, 0, 32)}px`, `padding:${safeNumber(style.padding, 0, 0, 48)}px`, `text-align:${safeAlign(style.align)}` ] return rules.join(';') } function wrapReportBlock(block, theme, html) { return `
| ${escapeHtml(c.label)} | `).join('')}
|---|
| ${escapeHtml(getPath(row, c.key))} | `).join('')}
${escapeHtml(interpolate(p.text, data))}
` break case 'kpi': html = `${escapeHtml(interpolate(p.text, data))}
` break case 'image': html = `