/* eslint-disable react-refresh/only-export-components */ 'use client' import { type ComponentProps, createContext, type HTMLAttributes, useContext, useEffect, useState, } from 'react' import type { Element } from 'hast' import { CheckIcon, CopyIcon } from 'lucide-react' import { type BundledLanguage, codeToHtml, type ShikiTransformer, } from 'shiki/bundle/web' import { cn } from '@/lib/utils' import { Button } from '@/components/ui/button' type CodeBlockProps = HTMLAttributes & { code: string language: BundledLanguage showLineNumbers?: boolean } type CodeBlockContextType = { code: string } const CodeBlockContext = createContext({ code: '', }) const lineNumberTransformer: ShikiTransformer = { name: 'line-numbers', line(node: Element, line: number) { node.children.unshift({ type: 'element', tagName: 'span', properties: { className: [ 'inline-block', 'min-w-10', 'mr-4', 'text-right', 'select-none', 'text-muted-foreground', ], }, children: [{ type: 'text', value: String(line) }], }) }, } export async function highlightCode( code: string, language: BundledLanguage, showLineNumbers = false ) { const transformers: ShikiTransformer[] = showLineNumbers ? [lineNumberTransformer] : [] return codeToHtml(code, { lang: language, themes: { light: 'one-light', dark: 'one-dark-pro', }, transformers, }) } export const CodeBlock = ({ code, language, showLineNumbers = false, className, children, ...props }: CodeBlockProps) => { const [html, setHtml] = useState('') useEffect(() => { let cancelled = false highlightCode(code, language, showLineNumbers).then((next) => { if (!cancelled) { setHtml(next) } }) return () => { cancelled = true } }, [code, language, showLineNumbers]) return (
{children && (
{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 { code } = useContext(CodeBlockContext) const copyToClipboard = async () => { if (typeof window === 'undefined' || !navigator?.clipboard?.writeText) { onError?.(new Error('Clipboard API not available')) return } try { await navigator.clipboard.writeText(code) setIsCopied(true) onCopy?.() setTimeout(() => setIsCopied(false), timeout) } catch (error) { onError?.(error as Error) } } const Icon = isCopied ? CheckIcon : CopyIcon return ( ) }