'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
{buf.join('\n')}
,
)
continue
}
if (line.trim() === '') {
i++
continue
}
const h = H_RE.exec(line)
if (h) {
blocks.push({renderInline(para.join(' '), key, citations)}
, ) } return