/** * Markdown & Branded HTML Formatting Utilities for PostFundManagement (PFM) */ 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) ); } export function stripMarkdown(text: string): string { if (!text) return ''; return text .replace(/<[^>]*>/g, ' ') .replace(/!\[.*?\]\(.*?\)/g, '') .replace(/\[(.*?)\]\(.*?\)/g, '$1') .replace(/#{1,6}\s+/g, '') .replace(/(\*\*|__)(.*?)\1/g, '$2') .replace(/(\*|_)(.*?)\1/g, '$2') .replace(/~~(.*?)~~/g, '$1') .replace(/`{1,3}.*?`{1,3}/gs, '') .replace(/^>\s+/gm, '') .replace(/^[-*+]\s+/gm, '') .replace(/^\d+\.\s+/gm, '') .replace(/\|/g, ' ') .replace(/\s+/g, ' ') .trim(); } export function parseMarkdownToHtml(markdown: string): string { if (!markdown) return ''; let html = markdown .replace(/)<[^<]*)*<\/script>/gi, '') .replace(/\r\n/g, '\n'); html = html.replace(/```([a-zA-Z0-9_-]*)\n([\s\S]*?)```/g, (_match, _lang, code) => { const escaped = code .replace(/&/g, '&') .replace(//g, '>'); return `
${escaped}
`; }); html = html.replace(/`([^`]+)`/g, (_match, code) => { const escaped = code .replace(/&/g, '&') .replace(//g, '>'); return `${escaped}`; }); // 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; }); html = html.replace(/^#### (.*?)$/gm, '

$1

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

$1

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

$1

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

$1

'); // Blockquotes and GitHub alerts 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
'); // Horizontal Rules (---) html = html.replace(/^(?:---|\*\*\*|___)\s*$/gm, '
'); // Strikethrough 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'); html = html.replace(/^[-*+]\s+(.*?)$/gm, '
  • $1
  • '); html = html.replace(/^\d+\.\s+(.*?)$/gm, '
  • $1
  • '); 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; } /** * Converts formatted HTML from WYSIWYG composition into clean, semantic Markdown. * Preserves headings, bold, italic, blockquotes, lists, dividers, and links. */ export function htmlToMarkdown(html: string): string { if (!html) return ''; let md = html .replace(/)<[^<]*)*<\/script>/gi, '') .replace(/)<[^<]*)*<\/style>/gi, ''); // Line breaks & paragraph breaks md = md.replace(//gi, '\n'); md = md.replace(/<\/p>/gi, '\n\n'); md = md.replace(/]*>/gi, ''); // Headers md = md.replace(/]*>(.*?)<\/h1>/gis, '# $1\n\n'); md = md.replace(/]*>(.*?)<\/h2>/gis, '## $1\n\n'); md = md.replace(/]*>(.*?)<\/h3>/gis, '### $1\n\n'); md = md.replace(/]*>(.*?)<\/h4>/gis, '#### $1\n\n'); // Bold / Italic / Underline md = md.replace(/<(strong|b)[^>]*>(.*?)<\/\1>/gis, '**$2**'); md = md.replace(/<(em|i)[^>]*>(.*?)<\/\1>/gis, '*$2*'); md = md.replace(/]*>(.*?)<\/u>/gis, '$1'); // Blockquote md = md.replace(/]*>(.*?)<\/blockquote>/gis, (_match, p1) => { return ( p1 .trim() .split('\n') .map((l: string) => `> ${l.trim()}`) .join('\n') + '\n\n' ); }); // Unordered Lists md = md.replace(/]*>(.*?)<\/ul>/gis, (_match, p1) => { return p1.replace(/]*>(.*?)<\/li>/gis, '- $1\n') + '\n'; }); // Ordered Lists md = md.replace(/]*>(.*?)<\/ol>/gis, (_match, p1) => { let index = 1; return p1.replace(/]*>(.*?)<\/li>/gis, () => `${index++}. $1\n`) + '\n'; }); // Horizontal Rules md = md.replace(//gi, '\n---\n\n'); // Links md = md.replace(/]*href=["']([^"']*)["'][^>]*>(.*?)<\/a>/gis, '[$2]($1)'); // Divs md = md.replace(/<\/div>/gi, '\n'); md = md.replace(/]*>/gi, ''); // Strip any remaining HTML tags md = md.replace(/<[^>]+>/g, ''); // Unescape standard HTML entities md = md .replace(/ /g, ' ') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, "'"); // Collapse 3+ newlines into 2 return md.replace(/\n{3,}/g, '\n\n').trim(); }