import { useCallback, useEffect, useRef, useState } from "react"; import { codeToHtml, resolveLang } from "../lib/shiki"; import { cn } from "../lib/cn"; import { Button } from "./ui/button"; import { IconCheck, IconCopy } from "./icons"; const highlightCache = new Map(); function cacheKey(code: string, lang: string) { return `${lang}::${code.length}::${code.slice(0, 64)}`; } function detectLanguage(code: string, hint?: string): string { if (hint) { return resolveLang(hint) ?? "json"; } const trimmed = code.trimStart(); if (trimmed.startsWith("{") || trimmed.startsWith("[")) { return "json"; } if (trimmed.startsWith("<")) { return "xml"; } if (trimmed.startsWith("---")) { return "yaml"; } return "json"; } export function CodeBlock(props: { code: string; lang?: string; className?: string; }) { const { code, lang: langHint, className } = props; const [html, setHtml] = useState(null); const [copied, setCopied] = useState(false); const mountedRef = useRef(true); const language = detectLanguage(code, langHint); const key = cacheKey(code, language); useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); useEffect(() => { const cached = highlightCache.get(key); if (cached) { setHtml(cached); return; } let cancelled = false; void codeToHtml(code, { lang: language }).then((result) => { if (cancelled) { return; } highlightCache.set(key, result); if (mountedRef.current) { setHtml(result); } }); return () => { cancelled = true; }; }, [code, key, language]); const handleCopy = useCallback(() => { void navigator.clipboard.writeText(code).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1500); }); }, [code]); return (
{html ? (
) : (
          {code}
        
)}
); }