import type { ReactNode } from 'react'; import type { CodeTheme } from '../../lib/code-highlighter'; import { StreamingCodeBlock } from './StreamingCodeBlock'; interface MarkdownContentProps { content: string; theme: CodeTheme; streaming?: boolean; } function safeLink(value: string): string | null { try { const url = new URL(value, globalThis.location?.href ?? 'https://localhost/'); return url.protocol === 'https:' || url.protocol === 'http:' ? url.href : null; } catch { return null; } } function inlineMarkdown(value: string): ReactNode[] { const parts = value.split(/(`[^`\n]+`|\[[^\]\n]+\]\([^\s)]+\))/g); return parts.map((part, index) => { if (part.startsWith('`') && part.endsWith('`')) { return {part.slice(1, -1)}; } const link = part.match(/^\[([^\]]+)\]\(([^)]+)\)$/); if (link) { const href = safeLink(link[2] ?? ''); if (href) { return {link[1]}; } } return part; }); } function renderTextBlock(lines: string[], key: string): ReactNode { const heading = lines.length === 1 ? lines[0]?.match(/^(#{1,4})\s+(.+)$/) : null; if (heading) { const level = heading[1]?.length ?? 2; const content = inlineMarkdown(heading[2] ?? ''); if (level === 1) return

{content}

; if (level === 2) return

{content}

; return
{content}
; } if (lines.every((line) => /^[-*]\s+/.test(line))) { return ; } if (lines.every((line) => /^\d+\.\s+/.test(line))) { return
    {lines.map((line, index) =>
  1. {inlineMarkdown(line.replace(/^\d+\.\s+/, ''))}
  2. )}
; } if (lines.every((line) => /^>\s?/.test(line))) { return
{inlineMarkdown(lines.map((line) => line.replace(/^>\s?/, '')).join('\n'))}
; } return

{lines.flatMap((line, index) => [ ...(index > 0 ? [
] : []), ...inlineMarkdown(line), ])}

; } export function MarkdownContent({ content, theme, streaming = false }: MarkdownContentProps) { const nodes: ReactNode[] = []; const lines = content.replace(/\r\n/g, '\n').split('\n'); let text: string[] = []; let code: string[] | null = null; let language = ''; const flushText = () => { if (text.length > 0) { nodes.push(renderTextBlock(text, `text-${nodes.length}`)); text = []; } }; for (const line of lines) { const fence = line.match(/^```\s*([^\s`]*)/); if (fence) { if (code === null) { flushText(); code = []; language = fence[1] ?? ''; } else { nodes.push( , ); code = null; language = ''; } continue; } if (code !== null) { code.push(line); } else if (!line.trim()) { flushText(); } else { const previous = text.at(-1); const listKindChanged = previous !== undefined && ( /^[-*]\s+/.test(previous) !== /^[-*]\s+/.test(line) || /^\d+\.\s+/.test(previous) !== /^\d+\.\s+/.test(line) ); if (listKindChanged) flushText(); text.push(line); } } if (code !== null) { nodes.push( , ); } flushText(); return
{nodes}
; }