import React, { useState, useMemo, useRef, useEffect } from 'react'; import Prism from 'prismjs'; import 'prismjs/components/prism-python'; import 'prismjs/components/prism-typescript'; import 'prismjs/components/prism-bash'; import 'prismjs/components/prism-json'; import 'prismjs/components/prism-go'; import 'prismjs/components/prism-sql'; import 'prismjs/components/prism-yaml'; import 'prismjs/components/prism-rust'; import 'prismjs/components/prism-c'; import 'prismjs/components/prism-cpp'; import 'prismjs/components/prism-java'; import 'prismjs/components/prism-php'; import 'prismjs/components/prism-docker'; import 'prismjs/components/prism-markdown'; import { Code, Copy, Check, Eye, Edit3 } from 'lucide-react'; import { DocumentBlock } from '../../lib/types'; export interface CodeBlockItemProps { block: DocumentBlock; isReadOnly: boolean; onChangeContent: (content: string) => void; onChangeLang: (lang: string) => void; onFocus?: () => void; onBlur?: () => void; inputRef?: (el: HTMLTextAreaElement | null) => void; } export const CODE_LANGUAGES = [ { id: 'javascript', label: 'JavaScript' }, { id: 'typescript', label: 'TypeScript' }, { id: 'python', label: 'Python' }, { id: 'go', label: 'Go' }, { id: 'bash', label: 'Bash / Shell' }, { id: 'json', label: 'JSON' }, { id: 'html', label: 'HTML / XML', alias: 'markup' }, { id: 'css', label: 'CSS' }, { id: 'sql', label: 'SQL' }, { id: 'rust', label: 'Rust' }, { id: 'cpp', label: 'C++' }, { id: 'c', label: 'C' }, { id: 'java', label: 'Java' }, { id: 'php', label: 'PHP' }, { id: 'yaml', label: 'YAML' }, { id: 'docker', label: 'Dockerfile' }, { id: 'markdown', label: 'Markdown' }, { id: 'text', label: 'Plain Text' }, ]; export const CodeBlockItem: React.FC = ({ block, isReadOnly, onChangeContent, onChangeLang, onFocus, onBlur, inputRef, }) => { const [isEditing, setIsEditing] = useState(false); const [copied, setCopied] = useState(false); const localTextareaRef = useRef(null); const currentLang = block.lang || 'javascript'; // Highlighted HTML generated by Prism const highlightedCode = useMemo(() => { const code = block.content || ''; if (!code) return ''; const prismLang = currentLang === 'html' ? 'markup' : currentLang; const grammar = Prism.languages[prismLang] || Prism.languages.javascript; if (!grammar) { return code .replace(/&/g, '&') .replace(//g, '>'); } try { return Prism.highlight(code, grammar, prismLang); } catch { return code .replace(/&/g, '&') .replace(//g, '>'); } }, [block.content, currentLang]); // Code lines for line numbers const lines = useMemo(() => { return (block.content || '').split('\n'); }, [block.content]); const handleCopy = async (e: React.MouseEvent) => { e.stopPropagation(); try { await navigator.clipboard.writeText(block.content || ''); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { // Fallback } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Escape') { setIsEditing(false); return; } // Tab key indent/outdent if (e.key === 'Tab') { e.preventDefault(); const textarea = e.currentTarget; const start = textarea.selectionStart; const end = textarea.selectionEnd; const val = textarea.value; if (e.shiftKey) { if (val.substring(start - 2, start) === ' ') { const newVal = val.substring(0, start - 2) + val.substring(start); onChangeContent(newVal); setTimeout(() => { textarea.selectionStart = textarea.selectionEnd = Math.max(0, start - 2); }, 0); } } else { const newVal = val.substring(0, start) + ' ' + val.substring(end); onChangeContent(newVal); setTimeout(() => { textarea.selectionStart = textarea.selectionEnd = start + 2; }, 0); } } }; const switchToEdit = () => { if (isReadOnly) return; setIsEditing(true); setTimeout(() => { localTextareaRef.current?.focus(); }, 20); }; return (
{/* Code Block Header Toolbar */}
{/* Left: Language Selection */}
{/* Right: Mode Toggle & Copy */}
{!isReadOnly && ( )}
{/* Code Block Content */} {isEditing && !isReadOnly ? (