import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react"; import { jsx, jsxs, Fragment } from "react/jsx-runtime"; import { toJsxRuntime } from "hast-util-to-jsx-runtime"; import { getHighlighter, resolveLang, THEME } from "../lib/shiki"; import { cn } from "../lib/utils"; // --------------------------------------------------------------------------- // Language detection // --------------------------------------------------------------------------- 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"; } // --------------------------------------------------------------------------- // Icons // --------------------------------------------------------------------------- const CopyIcon = () => ( ); const CheckIcon = () => ( ); // --------------------------------------------------------------------------- // Highlight hook // --------------------------------------------------------------------------- function useHighlighted(code: string, lang: string): ReactNode | null { const [highlighted, setHighlighted] = useState(null); useEffect(() => { let cancelled = false; getHighlighter().then((highlighter) => { if (cancelled) return; const hast = highlighter.codeToHast(code, { lang, theme: THEME }); const nodes = toJsxRuntime(hast, { jsx, jsxs, Fragment }); if (!cancelled) setHighlighted(nodes); }); return () => { cancelled = true; }; }, [code, lang]); return highlighted; } // --------------------------------------------------------------------------- // CodeBlock // --------------------------------------------------------------------------- export function CodeBlock(props: { code: string; lang?: string; title?: string; maxHeight?: string; className?: string; }) { const { code, lang: langHint, title, className } = props; const [expanded, setExpanded] = useState(false); const [copied, setCopied] = useState(false); const language = useMemo(() => detectLanguage(code, langHint), [code, langHint]); const highlighted = useHighlighted(code, language); const lines = code.split("\n"); const isLong = lines.length > 24; const maxH = !expanded && isLong ? props.maxHeight ?? "24rem" : undefined; const handleCopy = useCallback(() => { void navigator.clipboard.writeText(code).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1500); }); }, [code]); return ( {title && ( {title} {copied ? : } )} {!title && ( {copied ? : } )} {highlighted ?? ( {code} )} {isLong && !expanded && ( setExpanded(true)} className="rounded-md border border-border bg-background px-2.5 py-1 text-[11px] font-medium text-muted-foreground hover:text-foreground transition-colors" > Show all ({lines.length} lines) )} ); }
{code}