'use client' // Minimal, SAFE markdown -> React renderer. No dangerouslySetInnerHTML, no dependency. // Covers the subset LLMs emit: headings, bold, italic, inline code, fenced code, ul/ol, links. // Plus deliberate [n] citation markers (mapped via `citations`, NOT parsed for structure). export interface CiteHandler { count: number onSelect: (n: number) => void } const INLINE_RE = /(`[^`]+`|\*\*[^*]+\*\*|\*[^*\s][^*]*\*|_[^_]+_|\[[^\]]+\]\([^)]+\)|\[\d+\])/g function renderInline(text: string, kp: string, cite?: CiteHandler): React.ReactNode[] { const nodes: React.ReactNode[] = [] let last = 0 let idx = 0 INLINE_RE.lastIndex = 0 let m: RegExpExecArray | null while ((m = INLINE_RE.exec(text)) !== null) { if (m.index > last) nodes.push(text.slice(last, m.index)) const tok = m[0] const key = `${kp}-${idx++}` if (tok.startsWith('`')) { nodes.push( {tok.slice(1, -1)} , ) } else if (tok.startsWith('**')) { nodes.push( {tok.slice(2, -2)} , ) } else if (tok.startsWith('*') || tok.startsWith('_')) { nodes.push({tok.slice(1, -1)}) } else if (/^\[\d+\]$/.test(tok)) { const n = parseInt(tok.slice(1, -1), 10) if (cite && n >= 1 && n <= cite.count) { nodes.push( , ) } else { nodes.push(tok) } } else { const mm = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(tok) if (mm && /^https?:\/\//i.test(mm[2])) { nodes.push( {mm[1]} , ) } else { nodes.push(mm ? mm[1] : tok) } } last = m.index + tok.length } if (last < text.length) nodes.push(text.slice(last)) return nodes } function Heading({ level, kp, text, cite }: { level: number; kp: string; text: string; cite?: CiteHandler }) { const children = renderInline(text, kp, cite) if (level <= 1) return

{children}

if (level === 2) return

{children}

return
{children}
} const UL_RE = /^\s*[-*]\s+/ const OL_RE = /^\s*\d+\.\s+/ const H_RE = /^(#{1,6})\s+(.*)$/ export function Markdown({ content, citations }: { content: string; citations?: CiteHandler }) { const lines = (content || '').replace(/\r\n/g, '\n').split('\n') const blocks: React.ReactNode[] = [] let i = 0 while (i < lines.length) { const line = lines[i] const key = `b${blocks.length}` if (line.trim().startsWith('```')) { const buf: string[] = [] i++ while (i < lines.length && !lines[i].trim().startsWith('```')) { buf.push(lines[i]) i++ } i++ blocks.push(
          {buf.join('\n')}
        
, ) continue } if (line.trim() === '') { i++ continue } const h = H_RE.exec(line) if (h) { blocks.push() i++ continue } if (UL_RE.test(line)) { const items: string[] = [] while (i < lines.length && UL_RE.test(lines[i])) { items.push(lines[i].replace(UL_RE, '')) i++ } blocks.push( , ) continue } if (OL_RE.test(line)) { const items: string[] = [] while (i < lines.length && OL_RE.test(lines[i])) { items.push(lines[i].replace(OL_RE, '')) i++ } blocks.push(
    {items.map((it, k) => (
  1. {renderInline(it, `${key}-${k}`, citations)}
  2. ))}
, ) continue } const para: string[] = [] while ( i < lines.length && lines[i].trim() !== '' && !H_RE.test(lines[i]) && !UL_RE.test(lines[i]) && !OL_RE.test(lines[i]) && !lines[i].trim().startsWith('```') ) { para.push(lines[i]) i++ } blocks.push(

{renderInline(para.join(' '), key, citations)}

, ) } return
{blocks}
}