"use client"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { cn } from "@/lib/utils"; import { CheckmarkCircle01Icon, CopyIcon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import type { ComponentProps, CSSProperties, HTMLAttributes } from "react"; import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react"; import type { BundledLanguage, ThemedToken } from "shiki"; import type { HighlighterCore } from "shiki/core"; // Explicit grammar map. Adding a language here is a deliberate decision — // every entry ships in the bundle, so resist the urge to add long-tail langs. // Aliases (e.g. "ts" → typescript, "py" → python) are auto-registered by // each grammar; a loader is keyed by canonical name only. type GrammarLoader = () => Promise; const GRAMMARS: Record = { typescript: () => import("shiki/langs/typescript.mjs"), tsx: () => import("shiki/langs/tsx.mjs"), javascript: () => import("shiki/langs/javascript.mjs"), jsx: () => import("shiki/langs/jsx.mjs"), json: () => import("shiki/langs/json.mjs"), markdown: () => import("shiki/langs/markdown.mjs"), bash: () => import("shiki/langs/bash.mjs"), rust: () => import("shiki/langs/rust.mjs"), python: () => import("shiki/langs/python.mjs"), css: () => import("shiki/langs/css.mjs"), html: () => import("shiki/langs/html.mjs"), yaml: () => import("shiki/langs/yaml.mjs"), toml: () => import("shiki/langs/toml.mjs"), go: () => import("shiki/langs/go.mjs"), java: () => import("shiki/langs/java.mjs"), c: () => import("shiki/langs/c.mjs"), cpp: () => import("shiki/langs/cpp.mjs"), ruby: () => import("shiki/langs/ruby.mjs"), swift: () => import("shiki/langs/swift.mjs"), kotlin: () => import("shiki/langs/kotlin.mjs"), sql: () => import("shiki/langs/sql.mjs"), diff: () => import("shiki/langs/diff.mjs"), }; // Aliases → canonical grammar name. Required because the `language` we // receive may be a short alias the highlighter doesn't yet have loaded. const ALIASES: Record = { ts: "typescript", js: "javascript", md: "markdown", sh: "bash", zsh: "bash", shell: "bash", rs: "rust", py: "python", yml: "yaml", }; function canonical(lang: string): string { return ALIASES[lang] ?? lang; } // 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: "", }); // Singleton highlighter, lazily initialized on first use. Uses shiki/core // with the JS regex engine (no Oniguruma WASM) and only the grammars we // explicitly opt into above. let highlighterPromise: Promise | null = null; const loadedLangs = new Set(); const loadingLangs = new Map>(); const tokensCache = new Map(); 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}`; }; async function getHighlighter(): Promise { if (!highlighterPromise) { highlighterPromise = (async () => { const [{ createHighlighterCore }, { createJavaScriptRegexEngine }] = await Promise.all([ import("shiki/core"), import("shiki/engine/javascript"), ]); return createHighlighterCore({ themes: [ import("shiki/themes/github-light.mjs"), import("shiki/themes/github-dark.mjs"), ], langs: [], engine: createJavaScriptRegexEngine(), }); })(); } return highlighterPromise; } async function ensureLanguage(language: string): Promise { const name = canonical(language); if (!GRAMMARS[name]) return "text"; if (loadedLangs.has(name)) return name; let pending = loadingLangs.get(name); if (!pending) { pending = (async () => { const hl = await getHighlighter(); const mod = await GRAMMARS[name]!(); await hl.loadLanguage(mod as Parameters[0]); loadedLangs.add(name); })(); loadingLangs.set(name, pending); } await pending; return name; } // 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 (async () => { const langToUse = await ensureLanguage(language); const highlighter = await getHighlighter(); return highlighter.codeToTokens(code, { lang: langToUse, themes: { dark: "github-dark", light: "github-light" }, }); })() // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then) .then((result) => { 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 ? CheckmarkCircle01Icon : CopyIcon; return ( ); }; export type CodeBlockLanguageSelectorProps = ComponentProps; export const CodeBlockLanguageSelector = ( props: CodeBlockLanguageSelectorProps ) =>