"use client"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { cn } from "@/lib/utils"; import { CheckIcon, CopyIcon } from "lucide-react"; import type { ComponentProps, CSSProperties, HTMLAttributes } from "react"; import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react"; import type { BundledLanguage, BundledTheme, HighlighterGeneric, ThemedToken, } from "shiki"; import { createHighlighter } from "shiki"; // Shiki uses bitflags for font styles: 1=italic, 2=bold, 4=underline // oxlint-disable-next-line eslint(no-bitwise) const isItalic = (fontStyle: number | undefined) => fontStyle && fontStyle & 1; // oxlint-disable-next-line eslint(no-bitwise) const isBold = (fontStyle: number | undefined) => fontStyle && fontStyle & 2; const isUnderline = (fontStyle: number | undefined) => // oxlint-disable-next-line eslint(no-bitwise) fontStyle && fontStyle & 4; // Transform tokens to include pre-computed keys to avoid noArrayIndexKey lint interface KeyedToken { token: ThemedToken; key: string; } interface KeyedLine { tokens: KeyedToken[]; key: string; } const addKeysToTokens = (lines: ThemedToken[][]): KeyedLine[] => lines.map((line, lineIdx) => ({ key: `line-${lineIdx}`, tokens: line.map((token, tokenIdx) => ({ key: `line-${lineIdx}-${tokenIdx}`, token, })), })); // Token rendering component const TokenSpan = ({ token }: { token: ThemedToken }) => ( {token.content} ); // Line number styles using CSS counters const LINE_NUMBER_CLASSES = cn( "block", "before:content-[counter(line)]", "before:inline-block", "before:[counter-increment:line]", "before:w-8", "before:mr-4", "before:text-right", "before:text-muted-foreground/50", "before:font-mono", "before:select-none" ); // Line rendering component const LineSpan = ({ keyedLine, showLineNumbers, }: { keyedLine: KeyedLine; showLineNumbers: boolean; }) => ( {keyedLine.tokens.length === 0 ? "\n" : keyedLine.tokens.map(({ token, key }) => ( ))} ); // Types type CodeBlockProps = HTMLAttributes & { code: string; language: BundledLanguage; showLineNumbers?: boolean; }; interface TokenizedCode { tokens: ThemedToken[][]; fg: string; bg: string; } interface CodeBlockContextType { code: string; } // Context const CodeBlockContext = createContext({ code: "", }); // Highlighter cache (singleton per language) const highlighterCache = new Map< string, Promise> >(); // Token cache const tokensCache = new Map(); // Subscribers for async token updates const subscribers = new Map void>>(); const getTokensCacheKey = (code: string, language: BundledLanguage) => { const start = code.slice(0, 100); const end = code.length > 100 ? code.slice(-100) : ""; return `${language}:${code.length}:${start}:${end}`; }; const getHighlighter = ( language: BundledLanguage ): Promise> => { const cached = highlighterCache.get(language); if (cached) { return cached; } const highlighterPromise = createHighlighter({ langs: [language], themes: ["github-light", "github-dark"], }); highlighterCache.set(language, highlighterPromise); return highlighterPromise; }; // Create raw tokens for immediate display while highlighting loads const createRawTokens = (code: string): TokenizedCode => ({ bg: "transparent", fg: "inherit", tokens: code.split("\n").map((line) => line === "" ? [] : [ { color: "inherit", content: line, } as ThemedToken, ] ), }); // Synchronous highlight with callback for async results export const highlightCode = ( code: string, language: BundledLanguage, // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-callbacks) callback?: (result: TokenizedCode) => void ): TokenizedCode | null => { const tokensCacheKey = getTokensCacheKey(code, language); // Return cached result if available const cached = tokensCache.get(tokensCacheKey); if (cached) { return cached; } // Subscribe callback if provided if (callback) { if (!subscribers.has(tokensCacheKey)) { subscribers.set(tokensCacheKey, new Set()); } subscribers.get(tokensCacheKey)?.add(callback); } // Start highlighting in background - fire-and-forget async pattern getHighlighter(language) // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then) .then((highlighter) => { const availableLangs = highlighter.getLoadedLanguages(); const langToUse = availableLangs.includes(language) ? language : "text"; const result = highlighter.codeToTokens(code, { lang: langToUse, themes: { dark: "github-dark", light: "github-light", }, }); const tokenized: TokenizedCode = { bg: result.bg ?? "transparent", fg: result.fg ?? "inherit", tokens: result.tokens, }; // Cache the result tokensCache.set(tokensCacheKey, tokenized); // Notify all subscribers const subs = subscribers.get(tokensCacheKey); if (subs) { for (const sub of subs) { sub(tokenized); } subscribers.delete(tokensCacheKey); } }) // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then), eslint-plugin-promise(prefer-await-to-callbacks) .catch((error) => { console.error("Failed to highlight code:", error); subscribers.delete(tokensCacheKey); }); return null; }; const CodeBlockBody = memo( ({ tokenized, showLineNumbers, className, }: { tokenized: TokenizedCode; showLineNumbers: boolean; className?: string; }) => { const preStyle = useMemo( () => ({ backgroundColor: tokenized.bg, color: tokenized.fg, }), [tokenized.bg, tokenized.fg] ); const keyedLines = useMemo( () => addKeysToTokens(tokenized.tokens), [tokenized.tokens] ); return (
        
          {keyedLines.map((keyedLine) => (
            
          ))}
        
      
); }, (prevProps, nextProps) => prevProps.tokenized === nextProps.tokenized && prevProps.showLineNumbers === nextProps.showLineNumbers && prevProps.className === nextProps.className ); CodeBlockBody.displayName = "CodeBlockBody"; export const CodeBlockContainer = ({ className, language, style, ...props }: HTMLAttributes & { language: string }) => (
); export const CodeBlockHeader = ({ children, className, ...props }: HTMLAttributes) => (
{children}
); export const CodeBlockTitle = ({ children, className, ...props }: HTMLAttributes) => (
{children}
); export const CodeBlockFilename = ({ children, className, ...props }: HTMLAttributes) => ( {children} ); export const CodeBlockActions = ({ children, className, ...props }: HTMLAttributes) => (
{children}
); export const CodeBlockContent = ({ code, language, showLineNumbers = false, }: { code: string; language: BundledLanguage; showLineNumbers?: boolean; }) => { // Memoized raw tokens for immediate display const rawTokens = useMemo(() => createRawTokens(code), [code]); // Synchronous cache lookup — avoids setState in effect for cached results const syncTokens = useMemo( () => highlightCode(code, language) ?? rawTokens, [code, language, rawTokens] ); // Async highlighting result (populated after shiki loads) const [asyncTokens, setAsyncTokens] = useState(null); const asyncKeyRef = useRef({ code, language }); // Invalidate stale async tokens synchronously during render if ( asyncKeyRef.current.code !== code || asyncKeyRef.current.language !== language ) { asyncKeyRef.current = { code, language }; setAsyncTokens(null); } useEffect(() => { let cancelled = false; highlightCode(code, language, (result) => { if (!cancelled) { setAsyncTokens(result); } }); return () => { cancelled = true; }; }, [code, language]); const tokenized = asyncTokens ?? syncTokens; return (
); }; export const CodeBlock = ({ code, language, showLineNumbers = false, className, children, ...props }: CodeBlockProps) => { const contextValue = useMemo(() => ({ code }), [code]); return ( {children} ); }; export type CodeBlockCopyButtonProps = ComponentProps & { onCopy?: () => void; onError?: (error: Error) => void; timeout?: number; }; export const CodeBlockCopyButton = ({ onCopy, onError, timeout = 2000, children, className, ...props }: CodeBlockCopyButtonProps) => { const [isCopied, setIsCopied] = useState(false); const timeoutRef = useRef(0); const { code } = useContext(CodeBlockContext); const copyToClipboard = useCallback(async () => { if (typeof window === "undefined" || !navigator?.clipboard?.writeText) { onError?.(new Error("Clipboard API not available")); return; } try { if (!isCopied) { await navigator.clipboard.writeText(code); setIsCopied(true); onCopy?.(); timeoutRef.current = window.setTimeout( () => setIsCopied(false), timeout ); } } catch (error) { onError?.(error as Error); } }, [code, onCopy, onError, timeout, isCopied]); useEffect( () => () => { window.clearTimeout(timeoutRef.current); }, [] ); const Icon = isCopied ? CheckIcon : CopyIcon; return ( ); }; export type CodeBlockLanguageSelectorProps = ComponentProps; export const CodeBlockLanguageSelector = ( props: CodeBlockLanguageSelectorProps ) =>