import { useMemo } from "preact/hooks"; import DOMPurify from "dompurify"; import { marked, Renderer } from "marked"; import { cn } from "@/lib/utils"; type MarkdownMode = "default" | "citationTokens"; type Props = { markdown: string; className?: string; mode?: MarkdownMode; resolveCitationIndex?: (href: string) => number | undefined; }; function replaceMarkdownLinksWithCitationTokens(markdown: string, resolveCitationIndex?: (href: string) => number | undefined) { if (!resolveCitationIndex) return markdown; return markdown.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, (_full, _label, href) => { const index = resolveCitationIndex(href); if (!index) return ""; return `[${index}]`; }); } const markdownRenderer = new Renderer(); const renderTable = markdownRenderer.table.bind(markdownRenderer); markdownRenderer.table = (token) => { return `
${renderTable(token)}
`; }; function renderMarkdown(markdown: string) { const rawHtml = marked.parse(markdown, { gfm: true, breaks: true, renderer: markdownRenderer }) as string; return DOMPurify.sanitize(rawHtml, { ADD_ATTR: ["class", "target", "rel"] }); } export function MarkdownContent({ markdown, className, mode = "default", resolveCitationIndex }: Props) { const html = useMemo(() => { const prepared = mode === "citationTokens" ? replaceMarkdownLinksWithCitationTokens(markdown, resolveCitationIndex) : markdown; return renderMarkdown(prepared); }, [markdown, mode, resolveCitationIndex]); return
; }