import * as React from 'react' import { codeToHtml, bundledLanguages, type BundledLanguage } from 'shiki' import { Copy, Check } from "lucide-react" import { Button } from "@/components/ui/button" import { cn } from '@/lib/utils' export interface CodeBlockProps { code: string language?: string className?: string /** * Render mode affects code block styling: * - 'terminal': Minimal, keeps control chars visible * - 'minimal': Clean code, basic styling * - 'full': Rich styling with background, copy button, etc. */ mode?: 'terminal' | 'minimal' | 'full' } // Map common aliases to Shiki language names const LANGUAGE_ALIASES: Record = { js: 'javascript', ts: 'typescript', py: 'python', sh: 'bash', zsh: 'bash', yml: 'yaml', rb: 'ruby', rs: 'rust', kt: 'kotlin', 'objective-c': 'objc', objc: 'objc' } // Simple LRU cache for highlighted code const highlightCache = new Map() const CACHE_MAX_SIZE = 200 function getCacheKey(code: string, lang: string): string { return `${lang}:${code}` } function isValidLanguage(lang: string): lang is BundledLanguage { const normalized = LANGUAGE_ALIASES[lang] || lang return normalized in bundledLanguages } /** * CodeBlock - Syntax highlighted code block using Shiki * * Uses Shiki dual themes with CSS variables for light/dark switching. * No JS-based dark mode detection needed — theme switching is handled * entirely via CSS (see globals.css for .shiki/.dark .shiki rules). * * @see https://shiki.style/guide/dual-themes */ export function CodeBlock({ code, language = 'text', className, mode = 'full' }: CodeBlockProps): React.JSX.Element { const [highlighted, setHighlighted] = React.useState(null) const [isLoading, setIsLoading] = React.useState(true) const [copied, setCopied] = React.useState(false) // Resolve language alias - keep as string to allow 'text' fallback const langLower = language.toLowerCase() const resolvedLang: string = LANGUAGE_ALIASES[langLower] || langLower React.useEffect(() => { let cancelled = false async function highlight(): Promise { const cacheKey = getCacheKey(code, resolvedLang) const cached = highlightCache.get(cacheKey) if (cached) { if (!cancelled) { setHighlighted(cached) setIsLoading(false) } return } try { // Use valid language or fallback to plaintext const lang = isValidLanguage(resolvedLang) ? resolvedLang : 'text' // Dual themes: Shiki outputs CSS variables for both themes in one pass. // CSS handles switching via .dark selector (see globals.css). const html = await codeToHtml(code, { lang, themes: { light: 'github-light', dark: 'github-dark', }, defaultColor: false, }) // Cache the result if (highlightCache.size >= CACHE_MAX_SIZE) { const firstKey = highlightCache.keys().next().value if (firstKey) highlightCache.delete(firstKey) } highlightCache.set(cacheKey, html) if (!cancelled) { setHighlighted(html) setIsLoading(false) } } catch (error) { // Fallback to plain text on error console.warn(`Shiki highlighting failed for language "${resolvedLang}":`, error) if (!cancelled) { setHighlighted(null) setIsLoading(false) } } } highlight() return () => { cancelled = true } }, [code, resolvedLang]) const handleCopy = React.useCallback(async () => { try { await navigator.clipboard.writeText(code) setCopied(true) setTimeout(() => setCopied(false), 2000) } catch (err) { console.error('Failed to copy code:', err) } }, [code]) // Terminal mode: raw monospace with minimal styling if (mode === 'terminal') { return (
        {code}
      
) } // Minimal mode: just syntax highlighting, no chrome if (mode === 'minimal') { if (isLoading || !highlighted) { return (
          {code}
        
) } return (
) } // Full mode: rich styling with header and copy button return (
{/* Language label + copy button */}
{resolvedLang !== 'text' ? resolvedLang : 'plain text'}
{/* Code content */}
{isLoading || !highlighted ? (
            {code}
          
) : (
)}
) } /** * InlineCode - Styled inline code span * Features: subtle background (3%), subtle border (5%), 75% opacity text */ export function InlineCode({ children, className }: { children: React.ReactNode className?: string }): React.JSX.Element { return ( {children} ) }