import { Suspense, lazy, useCallback, useState, type ReactNode } from "react"; import { Check, Copy } from "lucide-react"; import { useTranslation } from "react-i18next"; import { useThemeValue } from "@/hooks/useTheme"; import { hasAnsi, parseAnsiSegments, stripAnsi } from "@/lib/ansi"; import { copyTextToClipboard } from "@/lib/clipboard"; import { normalizeCodeLanguage } from "@/lib/code-language"; import { cn } from "@/lib/utils"; interface CodeBlockProps { language?: string; code: string; className?: string; chrome?: "default" | "none"; highlight?: boolean; showLineNumbers?: boolean; wrapLongLines?: boolean; } interface HighlightedCodeProps { language?: string; code: string; isDark: boolean; chrome: "default" | "none"; showLineNumbers: boolean; wrapLongLines: boolean; } const CODE_FONT_STACK = [ '"JetBrains Mono"', '"SFMono-Regular"', '"SF Mono"', '"Fira Code"', '"Cascadia Code"', '"Source Code Pro"', "Menlo", "Consolas", "monospace", ].join(", "); const ANSI_LANGUAGES = new Set(["ansi", "ansi-output"]); const CODE_SURFACE_LIGHT = "#f4f4f5"; const CODE_SURFACE_DARK = "#27272a"; const LazyHighlightedCode = lazy(async () => { const [ { default: SyntaxHighlighter }, { default: oneDark }, { default: oneLight }, ] = await Promise.all([ import("react-syntax-highlighter/dist/esm/prism-async-light"), import("react-syntax-highlighter/dist/esm/styles/prism/one-dark"), import("react-syntax-highlighter/dist/esm/styles/prism/one-light"), ]); return { default({ language, code, isDark, chrome, showLineNumbers, wrapLongLines, }: HighlightedCodeProps) { const theme = isDark ? oneDark : oneLight; const transparentTheme = chrome === "none" ? { ...theme, 'pre[class*="language-"]': { ...theme['pre[class*="language-"]'], background: "transparent", }, 'code[class*="language-"]': { ...theme['code[class*="language-"]'], background: "transparent", }, } : theme; return ( {code} ); }, }; }); function renderPlainText(value: string): ReactNode { return value; } function renderAnsiText(value: string): ReactNode { return parseAnsiSegments(value).map((segment, index) => ( {segment.text} )); } function CodeTextBlock({ code, chrome, showLineNumbers, testId, className, renderText = renderPlainText, }: { code: string; chrome: "default" | "none"; showLineNumbers: boolean; testId: string; className?: string; renderText?: (value: string) => ReactNode; }) { const lines = code.split("\n"); return (
      
        {showLineNumbers ? (
          lines.map((line, index) => (
            
              
                {index + 1}
              
              {renderText(line || " ")}
              {index < lines.length - 1 ? "\n" : null}
            
          ))
        ) : renderText(code)}
      
    
); } function shouldRenderAnsi(language: string | undefined, code: string): boolean { const normalized = language?.trim().toLowerCase(); return Boolean((normalized && ANSI_LANGUAGES.has(normalized)) || hasAnsi(code)); } export function CodeBlock({ language, code, className, chrome = "default", highlight = true, showLineNumbers = false, wrapLongLines = true, }: CodeBlockProps) { const { t } = useTranslation(); const [copied, setCopied] = useState(false); const isDark = useThemeValue() === "dark"; const hasChrome = chrome === "default"; const renderAnsi = shouldRenderAnsi(language, code); const syntaxLanguage = normalizeCodeLanguage(language); const onCopy = useCallback(() => { void copyTextToClipboard(renderAnsi ? stripAnsi(code) : code).then((ok) => { if (!ok) return; setCopied(true); setTimeout(() => setCopied(false), 1_500); }); }, [code, renderAnsi]); return (
{hasChrome ? (
{language || t("code.fallbackLanguage")}
) : null} {renderAnsi ? ( ) : highlight ? ( } > ) : ( )}
); }