/** * Markdown & Branded HTML Formatting Utilities for PostFundManagement (PFM) */ export const PFM_BRAND_LOGO_SVG = ` `; export const PFM_LOGO_DATA_URI = `data:image/svg+xml;utf8,${encodeURIComponent(PFM_BRAND_LOGO_SVG)}`; export interface MarkdownRenderOptions { applyBranding?: boolean; emailFormat?: boolean; subject?: string; tenantName?: string; } /** * Checks if the content string is formatted HTML or Markdown. */ export function isHtmlContent(content: string): boolean { if (!content) return false; const trimmed = content.trim(); return ( trimmed.startsWith('')) || /<(strong|b|em|i|u|h[1-6]|ul|ol|li|table|blockquote|div|p|img|hr|br)\b/i.test(trimmed) ); } /** * Extracts raw plain text from markdown or HTML for concise notification snippets. */ export function stripMarkdown(text: string): string { if (!text) return ''; return text .replace(/<[^>]*>/g, ' ') // Strip HTML tags .replace(/!\[.*?\]\(.*?\)/g, '') // Strip images .replace(/\[(.*?)\]\(.*?\)/g, '$1') // Extract link text .replace(/#{1,6}\s+/g, '') // Strip headers .replace(/(\*\*|__)(.*?)\1/g, '$2') // Strip bold .replace(/(\*|_)(.*?)\1/g, '$2') // Strip italic .replace(/~~(.*?)~~/g, '$1') // Strip strikethrough .replace(/`{1,3}.*?`{1,3}/gs, '') // Strip code blocks .replace(/^>\s+/gm, '') // Strip blockquotes .replace(/^[-*+]\s+/gm, '') // Strip list markers .replace(/^\d+\.\s+/gm, '') // Strip ordered list markers .replace(/\|/g, ' ') // Replace table pipes .replace(/\s+/g, ' ') .trim(); } /** * Parses markdown text into semantic HTML. */ export function parseMarkdownToHtml(markdown: string): string { if (!markdown) return ''; let html = markdown // Sanitize script tags .replace(/)<[^<]*)*<\/script>/gi, '') // Normalize line endings .replace(/\r\n/g, '\n'); // 1. Code blocks (```lang\ncode\n```) html = html.replace(/```([a-zA-Z0-9_-]*)\n([\s\S]*?)```/g, (_match, _lang, code) => { const escaped = code .replace(/&/g, '&') .replace(//g, '>'); return `
${escaped}
`; }); // 2. Inline code (`code`) html = html.replace(/`([^`]+)`/g, (_match, code) => { const escaped = code .replace(/&/g, '&') .replace(//g, '>'); return `${escaped}`; }); // 3. Tables (| A | B |\n|---|---|\n| C | D |) html = html.replace(/((?:\|[^\n]+\|\n?)+)/g, (match) => { const rows = match.trim().split('\n'); if (rows.length < 2) return match; const hasSeparator = rows[1] && rows[1].includes('---'); if (!hasSeparator) return match; const parseCells = (row: string) => row .split('|') .slice(1, -1) .map((c) => c.trim()); const headers = parseCells(rows[0]); const bodyRows = rows.slice(2); let tableHtml = `
`; tableHtml += ``; headers.forEach((h) => { tableHtml += ``; }); tableHtml += ``; bodyRows.forEach((row, i) => { const cells = parseCells(row); const bg = i % 2 === 0 ? '#0f172a' : '#1e293b/40'; tableHtml += ``; cells.forEach((c) => { tableHtml += ``; }); tableHtml += ``; }); tableHtml += `
${h}
${c}
`; return tableHtml; }); // 4. Headers (# through ####) html = html.replace(/^#### (.*?)$/gm, '

$1

'); html = html.replace(/^### (.*?)$/gm, '

$1

'); html = html.replace(/^## (.*?)$/gm, '

$1

'); html = html.replace(/^# (.*?)$/gm, '

$1

'); // 5. Blockquotes and GitHub alerts (> [!NOTE], > [!IMPORTANT], etc.) html = html.replace(/^>\s*\[!NOTE\]\s*(.*?)$/gim, '
NOTE: $1
'); html = html.replace(/^>\s*\[!IMPORTANT\]\s*(.*?)$/gim, '
IMPORTANT: $1
'); html = html.replace(/^>\s*\[!WARNING\]\s*(.*?)$/gim, '
WARNING: $1
'); html = html.replace(/^>\s*\[!TIP\]\s*(.*?)$/gim, '
TIP: $1
'); html = html.replace(/^>\s*(.*?)$/gm, '
$1
'); // 6. Horizontal Rules (---) html = html.replace(/^(?:---|\*\*\*|___)\s*$/gm, '
'); // 7. Bold & Italic html = html.replace(/\*\*\*(.*?)\*\*\*/g, '$1'); html = html.replace(/\*\*(.*?)\*\*/g, '$1'); html = html.replace(/__(.*?)__/g, '$1'); html = html.replace(/\*(.*?)\*/g, '$1'); html = html.replace(/_(.*?)_/g, '$1'); html = html.replace(/~~(.*?)~~/g, '$1'); // 8. Images and Links html = html.replace(/!\[(.*?)\]\((.*?)\)/g, '$1'); html = html.replace(/\[(.*?)\]\((.*?)\)/g, '$1'); // 9. Unordered Lists (- or *) html = html.replace(/^[-*+]\s+(.*?)$/gm, '
  • $1
  • '); // 10. Ordered Lists (1. 2.) html = html.replace(/^\d+\.\s+(.*?)$/gm, '
  • $1
  • '); // 11. Paragraphs const paragraphs = html.split(/\n{2,}/); html = paragraphs .map((p) => { const trimmed = p.trim(); if (!trimmed) return ''; if ( trimmed.startsWith('${trimmed.replace(/\n/g, '
    ')}

    `; }) .join('\n'); return html; } /** * Transforms Markdown into full branded executive HTML for email notifications. */ export function renderMarkdownToHtml( markdown: string, options?: MarkdownRenderOptions ): string { const trimmed = (markdown || '').trim(); if (trimmed.startsWith(')<[^<]*)*<\/script>/gi, '') : parseMarkdownToHtml(trimmed); if (!options?.applyBranding && !options?.emailFormat) { return contentHtml; } const subject = options?.subject || 'Statutory Platform Notification'; const tenantName = options?.tenantName || 'Post-Fund Management (PFM) Platform'; const dateFormatted = new Date().toLocaleDateString('en-ZA', { day: 'numeric', month: 'short', year: 'numeric', }); return ` ${subject}
    PFM Logo
    Post-Fund Management (PFM)
    ${tenantName}
    Official Notice

    ${subject}

    Dispatched on ${dateFormatted} • Electronic System Communication
    ${contentHtml}
    Post-Fund Management, Performance & Impact Governance Platform
    This message was transmitted securely under active multi-tenant encryption. Please do not reply directly to automated dispatch notices.
    © ${new Date().getFullYear()} PFM Governance Architecture. All rights reserved.
    `; }