import copy from "copy-to-clipboard"; import hljs from "highlight.js"; import { CheckIcon, CopyIcon } from "lucide-react"; import { isValidElement, type ReactElement, type ReactNode, useEffect, useMemo, useState } from "react"; import { useAuth } from "@/contexts/AuthContext"; import { cn } from "@/lib/utils"; import { getThemeWithFallback, resolveTheme } from "@/utils/theme"; import { MermaidBlock } from "./MermaidBlock"; import type { ReactMarkdownProps } from "./markdown/types"; import { extractCodeContent, extractLanguage } from "./utils"; interface CodeBlockProps extends ReactMarkdownProps { children?: ReactNode; className?: string; } export const CodeBlock = ({ children, className, node: _node, ...props }: CodeBlockProps) => { const { userGeneralSetting } = useAuth(); const [copied, setCopied] = useState(false); const codeElement = isValidElement(children) ? (children as ReactElement<{ className?: string }>) : null; const codeClassName = codeElement?.props.className || ""; const codeContent = extractCodeContent(children); const language = extractLanguage(codeClassName); // If it's a mermaid block, render with MermaidBlock component if (language === "mermaid") { return (
        
          {children}
        
      
); } const theme = getThemeWithFallback(userGeneralSetting?.theme); const resolvedTheme = resolveTheme(theme); const isDarkTheme = resolvedTheme.includes("dark"); // Dynamically load highlight.js theme based on app theme useEffect(() => { const dynamicImportStyle = async () => { // Remove any existing highlight.js style const existingStyle = document.querySelector("style[data-hljs-theme]"); if (existingStyle) { existingStyle.remove(); } try { const cssModule = isDarkTheme ? await import("highlight.js/styles/github-dark-dimmed.css?inline") : await import("highlight.js/styles/github.css?inline"); // Create and inject the style const style = document.createElement("style"); style.textContent = cssModule.default; style.setAttribute("data-hljs-theme", isDarkTheme ? "dark" : "light"); document.head.appendChild(style); } catch (error) { console.warn("Failed to load highlight.js theme:", error); } }; dynamicImportStyle(); }, [resolvedTheme, isDarkTheme]); // Highlight code using highlight.js const highlightedCode = useMemo(() => { try { const lang = hljs.getLanguage(language); if (lang) { return hljs.highlight(codeContent, { language: language, }).value; } } catch { // Skip error and use default highlighted code. } // Escape any HTML entities when rendering original content. return Object.assign(document.createElement("span"), { textContent: codeContent, }).innerHTML; }, [language, codeContent]); const handleCopy = async () => { try { // Try native clipboard API first (requires HTTPS or localhost) if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(codeContent); setCopied(true); setTimeout(() => setCopied(false), 2000); } else { // Fallback to copy-to-clipboard library for non-secure contexts const success = await copy(codeContent); if (success) { setCopied(true); setTimeout(() => setCopied(false), 2000); } else { console.error("Failed to copy code"); } } } catch (err) { // If native API fails, try fallback console.warn("Native clipboard failed, using fallback:", err); const success = await copy(codeContent); if (success) { setCopied(true); setTimeout(() => setCopied(false), 2000); } else { console.error("Failed to copy code:", err); } } }; return (
      {/* Header with language label and copy button */}
      
{language || "text"}
{/* Code content */}
); };