228 lines
9.0 KiB
TypeScript
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, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>');
|
|
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, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>');
|
|
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(/ /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();
|
|
}
|
|
|