import { escapeHtml } from './escape';
import { sanitizeUrl } from './url-safety';
const INLINE_PATTERNS: [RegExp, (m: RegExpMatchArray) => string][] = [
[/\*\*(.+?)\*\*/g, m => `${m[1]}`],
[/(? `${m[1]}`],
[/`([^`]+?)`/g, m => `${m[1]}`],
[/\[([^\]]+)\]\(([^)]+)\)/g, m => {
const href = sanitizeUrl(m[2]);
if (!href) return escapeHtml(m[1]);
return `${m[1]}`;
}],
];
function renderInline(text: string): string {
for (const [re, fn] of INLINE_PATTERNS) {
text = text.replace(re, (...args) => fn(args as any));
}
return text;
}
export function renderMarkdown(input: string): string {
const blocks: string[] = [];
const lines = input.split('\n');
let i = 0;
while (i < lines.length) {
const line = lines[i];
// Code fence
if (line.trimStart().startsWith('```')) {
const lang = line.trim().slice(3);
const codeLines: string[] = [];
i++;
while (i < lines.length && !lines[i].trimStart().startsWith('```')) {
codeLines.push(lines[i]);
i++;
}
i++; // skip closing fence
blocks.push(`
${escapeHtml(codeLines.join('\n'))}`);
continue;
}
// Heading
const headingMatch = line.match(/^(#{1,4})\s+(.+)/);
if (headingMatch) {
const level = headingMatch[1].length + 1; // h2-h5 (h1 reserved for page title)
blocks.push(`${renderInline(quoteLines.join(' '))}`); continue; } // Table if (/^\|(.+)\|$/.test(line) && i + 1 < lines.length && /^\|[-:| ]+\|$/.test(lines[i + 1])) { const headerCells = line.split('|').map(c => c.trim()).filter(Boolean); i += 2; // skip header and separator const rows: string[][] = []; while (i < lines.length && /^\|(.+)\|$/.test(lines[i])) { rows.push(lines[i].split('|').map(c => c.trim()).filter(Boolean)); i++; } const thCells = headerCells.map(c => `
${renderInline(paraLines.join(' '))}
`); } } return blocks.join('\n'); }