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(headingMatch[2])}`); i++; continue; } // Horizontal rule if (/^---+\s*$/.test(line)) { blocks.push('
'); i++; continue; } // Unordered list if (/^\s*[-*]\s+/.test(line)) { const items: string[] = []; while (i < lines.length && /^\s*[-*]\s+/.test(lines[i])) { items.push(`
  • ${renderInline(lines[i].replace(/^\s*[-*]\s+/, ''))}
  • `); i++; } blocks.push(``); continue; } // Ordered list if (/^\s*\d+\.\s+/.test(line)) { const items: string[] = []; while (i < lines.length && /^\s*\d+\.\s+/.test(lines[i])) { items.push(`
  • ${renderInline(lines[i].replace(/^\s*\d+\.\s+/, ''))}
  • `); i++; } blocks.push(`
      ${items.join('')}
    `); continue; } // Blockquote if (/^>\s?/.test(line)) { const quoteLines: string[] = []; while (i < lines.length && /^>\s?/.test(lines[i])) { quoteLines.push(lines[i].replace(/^>\s?/, '')); i++; } 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(c)}`).join(''); const trRows = rows.map(r => `${r.map(c => `${renderInline(c)}`).join('')}`).join(''); blocks.push(`${thCells}${trRows}
    `); continue; } // Blank line if (!line.trim()) { i++; continue; } // Paragraph const paraLines: string[] = []; while (i < lines.length && lines[i].trim() && !/^#{1,4}\s/.test(lines[i]) && !/^\s*[-*]\s+/.test(lines[i]) && !/^\s*\d+\.\s+/.test(lines[i]) && !/^>\s?/.test(lines[i]) && !line.trimStart().startsWith('```')) { paraLines.push(lines[i]); i++; if (i >= lines.length) break; } if (paraLines.length) { blocks.push(`

    ${renderInline(paraLines.join(' '))}

    `); } } return blocks.join('\n'); }