import { useEffect, useMemo, useState } from "react"; import { getSingletonHighlighter, type ThemedToken } from "shiki"; interface CodeBlockProps { text: string; language?: string; } const TOKEN_CACHE = new Map(); function normalizeLanguage(language?: string): string { const raw = (language || "").toLowerCase().trim(); if (!raw) return "text"; const aliases: Record = { ts: "typescript", tsx: "tsx", js: "javascript", jsx: "jsx", md: "markdown", yml: "yaml", sh: "bash", zsh: "bash", shell: "bash", py: "python", rb: "ruby", rs: "rust", csharp: "csharp", "c#": "csharp", plaintext: "text", txt: "text", }; return aliases[raw] || raw; } function languageLabel(language: string): string { if (language === "text") return "text"; if (language === "typescript") return "ts"; if (language === "javascript") return "js"; return language; } export function CodeBlock({ text, language }: CodeBlockProps) { const [tokens, setTokens] = useState(null); const [copied, setCopied] = useState(false); const normalizedLanguage = useMemo(() => normalizeLanguage(language), [language]); const displayLanguage = useMemo(() => languageLabel(normalizedLanguage), [normalizedLanguage]); useEffect(() => { let cancelled = false; setTokens(null); if (normalizedLanguage === "text") return; const cacheKey = `${normalizedLanguage}::${text}`; const cached = TOKEN_CACHE.get(cacheKey); if (cached) { setTokens(cached); return; } getSingletonHighlighter({ themes: ["github-dark"], // eslint-disable-next-line @typescript-eslint/no-explicit-any langs: [normalizedLanguage as any], }) .then((highlighter) => { if (cancelled) return; const result = highlighter.codeToTokens(text, { // eslint-disable-next-line @typescript-eslint/no-explicit-any lang: normalizedLanguage as any, theme: "github-dark", }); if (TOKEN_CACHE.size >= 500) TOKEN_CACHE.clear(); TOKEN_CACHE.set(cacheKey, result.tokens); setTokens(result.tokens); }) .catch((err: unknown) => { console.warn('[code-block] Tokenization failed:', err); if (!cancelled) { setTokens(null); } }); return () => { cancelled = true; }; }, [text, normalizedLanguage]); const handleCopy = async () => { try { await navigator.clipboard.writeText(text); setCopied(true); window.setTimeout(() => setCopied(false), 1200); } catch { setCopied(false); } }; return (
{displayLanguage}
        {tokens ? (
          
            {tokens.map((lineTokens, lineIndex) => (
              
{lineTokens.map((token, tokenIndex) => ( {token.content} ))}
))}
) : ( {text} )}
); }