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
JavaScript
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;