UNPKG

claritykit-svelte

Version:

A comprehensive Svelte component library focused on accessibility, ADHD-optimized design, developer experience, and full SSR compatibility

279 lines (278 loc) 10.9 kB
import { Node, mergeAttributes, textblockTypeInputRule } from '@tiptap/core'; import { Plugin, PluginKey } from '@tiptap/pm/state'; export const CodeBlockExtension = Node.create({ name: 'codeBlockEnhanced', addOptions() { return { languageClassPrefix: 'language-', exitOnTripleEnter: true, exitOnArrowDown: true, defaultLanguage: null, HTMLAttributes: { class: 'code-block-enhanced', }, supportedLanguages: [ 'javascript', 'typescript', 'python', 'java', 'csharp', 'cpp', 'c', 'php', 'ruby', 'go', 'rust', 'swift', 'kotlin', 'html', 'css', 'scss', 'json', 'xml', 'yaml', 'markdown', 'sql', 'bash', 'shell', 'powershell', 'dockerfile', 'nginx', 'apache', 'plaintext', ], showLineNumbers: true, showLanguageSelector: true, syntaxHighlighting: true, }; }, content: 'text*', marks: '', group: 'block', code: true, defining: true, addAttributes() { return { language: { default: this.options.defaultLanguage, parseHTML: element => { const { languageClassPrefix } = this.options; const classNames = [...(element.firstElementChild?.classList || [])]; const languages = classNames .filter(className => className.startsWith(languageClassPrefix)) .map(className => className.replace(languageClassPrefix, '')); const language = languages[0]; if (!language) { return null; } return language; }, rendered: false, }, }; }, parseHTML() { return [ { tag: 'pre', preserveWhitespace: 'full', }, ]; }, renderHTML({ node, HTMLAttributes }) { const language = node.attrs.language || this.options.defaultLanguage; const languageClass = language ? `${this.options.languageClassPrefix}${language}` : ''; return [ 'div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { class: `code-block-container ${language ? `language-${language}` : ''}`, 'data-language': language || 'plaintext', }), [ 'div', { class: 'code-block-header' }, [ 'div', { class: 'code-block-language' }, language || 'plaintext', ], [ 'button', { class: 'code-block-copy', type: 'button', 'aria-label': 'Copy code', title: 'Copy to clipboard', }, '📋', ], ], [ 'pre', { class: `code-block-content ${this.options.showLineNumbers ? 'with-line-numbers' : ''}`, }, [ 'code', { class: languageClass, spellcheck: 'false', }, 0, ], ], ]; }, addCommands() { return { setCodeBlock: (attributes) => ({ commands }) => { return commands.setNode(this.name, attributes); }, toggleCodeBlock: (attributes) => ({ commands }) => { return commands.toggleNode(this.name, 'paragraph', attributes); }, }; }, addKeyboardShortcuts() { return { 'Mod-Alt-c': () => this.editor.commands.toggleCodeBlock(), // Exit code block on triple enter Enter: ({ editor }) => { if (!this.options.exitOnTripleEnter) { return false; } const { state } = editor; const { selection } = state; const { $from, empty } = selection; if (!empty || $from.parent.type !== this.type) { return false; } const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2; const endsWithDoubleNewline = $from.parent.textContent.endsWith('\n\n'); if (!isAtEnd || !endsWithDoubleNewline) { return false; } return editor .chain() .command(({ tr }) => { tr.delete($from.pos - 2, $from.pos); return true; }) .exitCode() .run(); }, // Exit code block on arrow down ArrowDown: ({ editor }) => { if (!this.options.exitOnArrowDown) { return false; } const { state } = editor; const { selection, doc } = state; const { $from, empty } = selection; if (!empty || $from.parent.type !== this.type) { return false; } const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2; if (!isAtEnd) { return false; } const after = $from.after(); if (after === undefined) { return false; } const nodeAfter = doc.nodeAt(after); if (nodeAfter) { return false; } return editor.commands.exitCode(); }, }; }, addInputRules() { return [ textblockTypeInputRule({ find: /^```([a-z]+)?[\s\n]$/, type: this.type, getAttributes: match => ({ language: match[1], }), }), ]; }, addProseMirrorPlugins() { return [ new Plugin({ key: new PluginKey('codeBlockEnhanced'), props: { handleDOMEvents: { // Handle copy button clicks click: (view, event) => { const target = event.target; if (target.classList.contains('code-block-copy')) { event.preventDefault(); event.stopPropagation(); const codeBlock = target.closest('.code-block-container'); const codeElement = codeBlock?.querySelector('code'); if (codeElement) { const code = codeElement.textContent || ''; // Copy to clipboard if (navigator.clipboard) { navigator.clipboard.writeText(code).then(() => { // Show feedback target.textContent = '✅'; setTimeout(() => { target.textContent = '📋'; }, 2000); }).catch(() => { console.error('Failed to copy code to clipboard'); }); } else { // Fallback for older browsers const textArea = document.createElement('textarea'); textArea.value = code; document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); target.textContent = '✅'; setTimeout(() => { target.textContent = '📋'; }, 2000); } } return true; } return false; }, }, }, }), ]; }, }); // Language detection helper export function detectLanguage(code) { // Simple language detection based on common patterns const patterns = [ { language: 'javascript', patterns: [/function\s+\w+\s*\(/, /const\s+\w+\s*=/, /=>\s*{/, /console\.log/] }, { language: 'typescript', patterns: [/interface\s+\w+/, /type\s+\w+\s*=/, /:\s*string/, /:\s*number/] }, { language: 'python', patterns: [/def\s+\w+\s*\(/, /import\s+\w+/, /from\s+\w+\s+import/, /print\s*\(/] }, { language: 'java', patterns: [/public\s+class/, /public\s+static\s+void\s+main/, /System\.out\.println/] }, { language: 'csharp', patterns: [/using\s+System/, /public\s+class/, /Console\.WriteLine/] }, { language: 'html', patterns: [/<html/, /<div/, /<span/, /<p>/] }, { language: 'css', patterns: [/\.\w+\s*{/, /#\w+\s*{/, /@media/, /display\s*:/] }, { language: 'json', patterns: [/^\s*{/, /"\w+"\s*:/, /^\s*\[/] }, { language: 'sql', patterns: [/SELECT\s+/, /FROM\s+/, /WHERE\s+/, /INSERT\s+INTO/i] }, { language: 'bash', patterns: [/^#!\/bin\/bash/, /echo\s+/, /if\s*\[/, /for\s+\w+\s+in/] }, ]; for (const { language, patterns: langPatterns } of patterns) { if (langPatterns.some(pattern => pattern.test(code))) { return language; } } return 'plaintext'; } export default CodeBlockExtension;