// A deliberately tiny Markdown subset for the texts of reference: paragraphs, // line breaks, lists, **bold**, *italic*, `code`. HTML is always escaped first, // so a proposal can never inject markup. const ESCAPES: Record = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; export function escapeHtml(s: string): string { return s.replace(/[&<>"']/g, (c) => ESCAPES[c] ?? c); } export function inline(s: string): string { return escapeHtml(s) .replace(/`([^`]+)`/g, '$1') .replace(/\*\*([^*]+)\*\*/g, '$1') .replace(/(^|[^*])\*([^*\s][^*]*?)\*(?!\*)/g, '$1$2'); } const BULLET = /^\s*[-*]\s+/; const NUMBERED = /^\s*\d+[.)]\s+/; export function render(md: string): string { const blocks = md.replace(/\r\n?/g, '\n').trim().split(/\n\s*\n/); return blocks .map((block) => { const lines = block.split('\n').filter((l) => l.trim().length > 0); if (lines.length > 0 && lines.every((l) => BULLET.test(l))) { return ``; } if (lines.length > 0 && lines.every((l) => NUMBERED.test(l))) { return `
    ${lines.map((l) => `
  1. ${inline(l.replace(NUMBERED, ''))}
  2. `).join('')}
`; } return `

${lines.map(inline).join('
')}

`; }) .join(''); } /** Plain text, for one-line excerpts. */ export function plain(md: string): string { return md.replace(/[*`]/g, '').replace(/^\s*[-*]\s+/gm, '').replace(/\s+/g, ' ').trim(); }