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 (
{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 (