Files
pfm/PostFundManagement.PartnerUI/src/lib/markdownUtils.ts
T

228 lines
9.0 KiB
TypeScript

/**
* 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('<!DOCTYPE') ||
trimmed.startsWith('<html') ||
trimmed.startsWith('<div') ||
trimmed.startsWith('<table') ||
trimmed.startsWith('<p') ||
trimmed.startsWith('<h') ||
trimmed.startsWith('<ul') ||
trimmed.startsWith('<ol') ||
trimmed.startsWith('<blockquote') ||
(trimmed.includes('</') && trimmed.includes('>')) ||
/<(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\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
return `<pre style="background:#0b1120;border:1px solid #1e293b;border-radius:12px;padding:14px;overflow-x:auto;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:12px;color:#e2e8f0;margin:12px 0;"><code>${escaped}</code></pre>`;
});
html = html.replace(/`([^`]+)`/g, (_match, code) => {
const escaped = code
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
return `<code style="background:#1e293b;color:#34d399;padding:2px 6px;border-radius:6px;font-family:monospace;font-size:12px;">${escaped}</code>`;
});
// 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 = `<div style="overflow-x:auto;margin:14px 0;"><table style="width:100%;border-collapse:collapse;border:1px solid #334155;border-radius:8px;font-size:12px;text-align:left;">`;
tableHtml += `<thead><tr style="background:#1e293b;color:#f8fafc;">`;
headers.forEach((h) => {
tableHtml += `<th style="padding:8px 12px;border:1px solid #334155;font-weight:600;">${h}</th>`;
});
tableHtml += `</tr></thead><tbody>`;
bodyRows.forEach((row, i) => {
const cells = parseCells(row);
const bg = i % 2 === 0 ? '#0f172a' : '#1e293b/40';
tableHtml += `<tr style="background:${bg};color:#cbd5e1;">`;
cells.forEach((c) => {
tableHtml += `<td style="padding:8px 12px;border:1px solid #334155;">${c}</td>`;
});
tableHtml += `</tr>`;
});
tableHtml += `</tbody></table></div>`;
return tableHtml;
});
html = html.replace(/^#### (.*?)$/gm, '<h4 style="font-size:14px;font-weight:700;color:#f8fafc;margin:12px 0 6px 0;">$1</h4>');
html = html.replace(/^### (.*?)$/gm, '<h3 style="font-size:16px;font-weight:700;color:#f8fafc;margin:14px 0 8px 0;">$1</h3>');
html = html.replace(/^## (.*?)$/gm, '<h2 style="font-size:18px;font-weight:800;color:#ffffff;margin:16px 0 8px 0;border-bottom:1px solid #1e293b;padding-bottom:6px;">$1</h2>');
html = html.replace(/^# (.*?)$/gm, '<h1 style="font-size:22px;font-weight:800;color:#ffffff;margin:18px 0 10px 0;border-bottom:1px solid #334155;padding-bottom:8px;">$1</h1>');
// Blockquotes and GitHub alerts
html = html.replace(/^>\s*\[!NOTE\]\s*(.*?)$/gim, '<div style="background:#0c4a6e22;border-left:4px solid #0284c7;padding:10px 14px;border-radius:8px;margin:10px 0;color:#bae6fd;font-size:12px;"><strong>NOTE:</strong> $1</div>');
html = html.replace(/^>\s*\[!IMPORTANT\]\s*(.*?)$/gim, '<div style="background:#83184322;border-left:4px solid #db2777;padding:10px 14px;border-radius:8px;margin:10px 0;color:#fbcfe8;font-size:12px;"><strong>IMPORTANT:</strong> $1</div>');
html = html.replace(/^>\s*\[!WARNING\]\s*(.*?)$/gim, '<div style="background:#713f1222;border-left:4px solid #d97706;padding:10px 14px;border-radius:8px;margin:10px 0;color:#fef08a;font-size:12px;"><strong>WARNING:</strong> $1</div>');
html = html.replace(/^>\s*\[!TIP\]\s*(.*?)$/gim, '<div style="background:#064e3b22;border-left:4px solid #059669;padding:10px 14px;border-radius:8px;margin:10px 0;color:#a7f3d0;font-size:12px;"><strong>TIP:</strong> $1</div>');
html = html.replace(/^>\s*(.*?)$/gm, '<blockquote style="border-left:3px solid #10b981;padding-left:12px;margin:8px 0;color:#94a3b8;font-style:italic;">$1</blockquote>');
// Horizontal Rules (---)
html = html.replace(/^(?:---|\*\*\*|___)\s*$/gm, '<hr style="border:none;border-top:1px solid #334155;margin:16px 0;" />');
// Strikethrough
html = html.replace(/~~(.*?)~~/g, '<del style="color:#64748b;">$1</del>');
html = html.replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em>$1</em></strong>');
html = html.replace(/\*\*(.*?)\*\*/g, '<strong style="color:#ffffff;">$1</strong>');
html = html.replace(/__(.*?)__/g, '<strong style="color:#ffffff;">$1</strong>');
html = html.replace(/\*(.*?)\*/g, '<em style="color:#cbd5e1;">$1</em>');
html = html.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer" style="color:#34d399;text-decoration:underline;">$1</a>');
html = html.replace(/^[-*+]\s+(.*?)$/gm, '<li style="margin-left:20px;list-style-type:disc;color:#cbd5e1;padding:2px 0;">$1</li>');
html = html.replace(/^\d+\.\s+(.*?)$/gm, '<li style="margin-left:20px;list-style-type:decimal;color:#cbd5e1;padding:2px 0;">$1</li>');
const paragraphs = html.split(/\n{2,}/);
html = paragraphs
.map((p) => {
const trimmed = p.trim();
if (!trimmed) return '';
if (
trimmed.startsWith('<h') ||
trimmed.startsWith('<div') ||
trimmed.startsWith('<table') ||
trimmed.startsWith('<pre') ||
trimmed.startsWith('<blockquote') ||
trimmed.startsWith('<hr') ||
trimmed.startsWith('<li')
) {
return trimmed;
}
return `<p style="margin:8px 0;line-height:1.6;color:#cbd5e1;">${trimmed.replace(/\n/g, '<br />')}</p>`;
})
.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\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '');
// Line breaks & paragraph breaks
md = md.replace(/<br\s*\/?>/gi, '\n');
md = md.replace(/<\/p>/gi, '\n\n');
md = md.replace(/<p[^>]*>/gi, '');
// Headers
md = md.replace(/<h1[^>]*>(.*?)<\/h1>/gis, '# $1\n\n');
md = md.replace(/<h2[^>]*>(.*?)<\/h2>/gis, '## $1\n\n');
md = md.replace(/<h3[^>]*>(.*?)<\/h3>/gis, '### $1\n\n');
md = md.replace(/<h4[^>]*>(.*?)<\/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[^>]*>(.*?)<\/u>/gis, '$1');
// Blockquote
md = md.replace(/<blockquote[^>]*>(.*?)<\/blockquote>/gis, (_match, p1) => {
return (
p1
.trim()
.split('\n')
.map((l: string) => `> ${l.trim()}`)
.join('\n') + '\n\n'
);
});
// Unordered Lists
md = md.replace(/<ul[^>]*>(.*?)<\/ul>/gis, (_match, p1) => {
return p1.replace(/<li[^>]*>(.*?)<\/li>/gis, '- $1\n') + '\n';
});
// Ordered Lists
md = md.replace(/<ol[^>]*>(.*?)<\/ol>/gis, (_match, p1) => {
let index = 1;
return p1.replace(/<li[^>]*>(.*?)<\/li>/gis, () => `${index++}. $1\n`) + '\n';
});
// Horizontal Rules
md = md.replace(/<hr\s*\/?>/gi, '\n---\n\n');
// Links
md = md.replace(/<a\b[^>]*href=["']([^"']*)["'][^>]*>(.*?)<\/a>/gis, '[$2]($1)');
// Divs
md = md.replace(/<\/div>/gi, '\n');
md = md.replace(/<div[^>]*>/gi, '');
// Strip any remaining HTML tags
md = md.replace(/<[^>]+>/g, '');
// Unescape standard HTML entities
md = md
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'");
// Collapse 3+ newlines into 2
return md.replace(/\n{3,}/g, '\n\n').trim();
}