import type { ReactNode } from 'react'; import { type ComponentProps, type CSSProperties, useRef, useState } from 'react'; import { CheckIcon, Copy } from '@/components/icons'; import { Button } from '@/components/ui/button'; import { ScrollArea } from '@/components/ui/scroll-area'; import { cn } from '@/lib/utils'; import { Mermaid, type MermaidPlacement } from './docs/Mermaid'; /** * Renders a fenced code block. The `data-*` props are produced at build time by * `lib/rehype-shiki.ts`; see that file for the markup contract. */ export interface CodeBlockProps extends ComponentProps<'pre'> { 'data-title'?: string; 'data-language'?: string; 'data-line-numbers'?: string; 'data-line-start'?: string; 'data-line-count'?: string; 'data-diff-markers'?: string; 'data-placement'?: MermaidPlacement; 'data-actions'?: string; } function reactChildrenToText(node: ReactNode): string { if (typeof node === 'string' || typeof node === 'number') { return String(node); } if (Array.isArray(node)) { return node.map(reactChildrenToText).join(''); } if (node && typeof node === 'object' && 'props' in (node as object)) { const props = (node as { props?: { children?: ReactNode; value?: unknown } }).props; if (typeof props?.value === 'string') { return props.value; } return reactChildrenToText(props?.children); } return ''; } /** * Joins the text of each rendered line. Lines marked as removed by * `// [!code --]` are skipped so the clipboard holds the "after" state; in a * `diff` block the +/- lines are content and are copied verbatim. */ function copyText(pre: HTMLPreElement | null, language?: string): string { if (!pre) { return ''; } const lines = [...pre.querySelectorAll('.line')]; if (!lines.length) { return pre.textContent || ''; } return lines .filter(line => language === 'diff' || line.dataset.diff !== 'remove') .map(line => line.textContent || '') .join('\n'); } export function CodeBlock({ children, className, style, ...rest }: CodeBlockProps) { const { 'data-title': title, 'data-language': language, 'data-line-start': lineStart, 'data-line-count': lineCount, 'data-placement': placement, 'data-actions': actions, ...preProps } = rest; const textInput = useRef(null); const [copied, setCopied] = useState(false); // ```mermaid fences render as diagrams; the raw definition stays in the // markup for search indexing and no-JS fallbacks. if (language === 'mermaid') { return ( ); } const start = Number(lineStart) || 1; const lastLine = start + Math.max(Number(lineCount) || 1, 1) - 1; const gutter = `${String(lastLine).length}ch`; const handleCopy = () => { setCopied(true); navigator?.clipboard?.writeText(copyText(textInput.current, language)); setTimeout(() => { setCopied(false); }, 1000); }; return (
{title ? (
{title}
) : null}
          {children}
        
); }