import { useMemo } from 'react' import { marked, type Tokens } from 'marked' import { escapeHtml, highlightCodeLines, isHighlightable } from '../chat/highlightCode' type Props = { content: string } const renderer = new marked.Renderer() renderer.code = function ({ text, lang }: Tokens.Code) { const languageLabel = escapeHtml(lang || 'code') const lines = text.split('\n') const hasLanguage = isHighlightable(lang) const highlightedLines = highlightCodeLines(text, lang) // Show line numbers only when language is known (actual code). // Plain text, file trees, command output etc. look better without them. const body = hasLanguage ? highlightedLines .map((line, index) => `
${index + 1} ${line || ' '}
`) .join('') : highlightedLines .map((line) => `
${line || ' '}
`) .join('') return `
${languageLabel} ${lines.length} ${lines.length === 1 ? 'line' : 'lines'}
${body}
` } marked.setOptions({ breaks: true, gfm: true, }) marked.use({ renderer }) export function MarkdownRenderer({ content }: Props) { const html = useMemo(() => { try { return marked.parse(content) as string } catch { return content } }, [content]) const handleClick = async (event: React.MouseEvent) => { const target = event.target as HTMLElement | null const button = target?.closest('[data-copy-code]') if (!button) return const text = button.getAttribute('data-copy-code') if (!text) return try { await navigator.clipboard.writeText(text) const original = button.textContent button.textContent = 'Copied' window.setTimeout(() => { button.textContent = original }, 1500) } catch { // Ignore clipboard errors and keep the original label. } } return (
) }