@joinezco/markdown-editor 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +9 -0
- package/TEST_README.md +359 -0
- package/dist/editor/commands/index.d.ts +2 -0
- package/dist/editor/commands/index.js +96 -0
- package/dist/editor/extensions/codeblock.d.ts +30 -0
- package/dist/editor/extensions/codeblock.js +390 -0
- package/dist/editor/extensions/filesystem.d.ts +8 -0
- package/dist/editor/extensions/filesystem.js +54 -0
- package/dist/editor/extensions/link.d.ts +1 -0
- package/dist/editor/extensions/link.js +24 -0
- package/dist/editor/extensions/slash-commands.d.ts +17 -0
- package/dist/editor/extensions/slash-commands.js +324 -0
- package/dist/editor/extensions/taskitem.d.ts +8 -0
- package/dist/editor/extensions/taskitem.js +67 -0
- package/dist/editor/index.d.ts +19 -0
- package/dist/editor/index.js +68 -0
- package/dist/editor/styles.d.ts +2 -0
- package/dist/editor/styles.js +153 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/index.html +16 -0
- package/package.json +73 -0
- package/public/fonts/Source_Serif_4/OFL.txt +91 -0
- package/public/fonts/Source_Serif_4/README.txt +128 -0
- package/public/fonts/Source_Serif_4/SourceSerif4-Italic-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Source_Serif_4/SourceSerif4-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-Black.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-BlackItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-Bold.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-BoldItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-ExtraBold.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-ExtraBoldItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-ExtraLight.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-ExtraLightItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-Italic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-Light.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-LightItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-Medium.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-MediumItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-Regular.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-SemiBold.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-SemiBoldItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-Black.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-BlackItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-Bold.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-BoldItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-ExtraBold.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-ExtraBoldItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-ExtraLight.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-ExtraLightItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-Italic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-Light.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-LightItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-Medium.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-MediumItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-Regular.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-SemiBold.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-SemiBoldItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-Black.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-BlackItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-Bold.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-BoldItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-ExtraBold.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-ExtraBoldItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-ExtraLight.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-ExtraLightItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-Italic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-Light.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-LightItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-Medium.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-MediumItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-Regular.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-SemiBold.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-SemiBoldItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-Black.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-BlackItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-Bold.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-BoldItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-ExtraBold.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-ExtraBoldItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-ExtraLight.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-ExtraLightItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-Italic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-Light.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-LightItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-Medium.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-MediumItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-Regular.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-SemiBold.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-SemiBoldItalic.ttf +0 -0
- package/public/snapshot.bin +0 -0
- package/src/App.css +236 -0
- package/src/App.tsx +73 -0
- package/src/index.css +63 -0
- package/src/lib/editor/commands/index.ts +110 -0
- package/src/lib/editor/extensions/__screenshots__/bullet-to-task.test.ts/BulletToTaskConverter-should-convert-bullet-list-item-to-checked-task-item-when--x--is-typed-1.png +0 -0
- package/src/lib/editor/extensions/__screenshots__/bullet-to-task.test.ts/BulletToTaskConverter-should-convert-bullet-list-item-to-task-item-when-----is-typed-1.png +0 -0
- package/src/lib/editor/extensions/bullet-to-task.test.ts +38 -0
- package/src/lib/editor/extensions/codeblock.ts +447 -0
- package/src/lib/editor/extensions/filesystem.ts +68 -0
- package/src/lib/editor/extensions/link.ts +31 -0
- package/src/lib/editor/extensions/slash-commands.ts +383 -0
- package/src/lib/editor/extensions/taskitem.ts +86 -0
- package/src/lib/editor/index.ts +82 -0
- package/src/lib/editor/styles.ts +166 -0
- package/src/lib/index.ts +3 -0
- package/src/lib/types/css.d.ts +4 -0
- package/src/main.tsx +10 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Basic-Editor-Functionality-should-be-focusable-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Basic-Editor-Functionality-should-create-an-editor-instance-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Basic-Editor-Functionality-should-have-initial-content-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Basic-Editor-Functionality-should-render-in-the-DOM-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Browser-specific-Features-should-handle-copy-and-paste-operations-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Browser-specific-Features-should-handle-undo-and-redo-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Editor-State-and-Updates-should-maintain-state-across-content-changes-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Editor-State-and-Updates-should-trigger-update-callbacks-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Error-Handling-should-handle-invalid-markdown-gracefully-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Error-Handling-should-handle-very-long-content-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Keyboard-Shortcuts-should-handle-Ctrl-B-for-bold-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Keyboard-Shortcuts-should-handle-Ctrl-I-for-italic-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Content-Management-should-get-markdown-content-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Content-Management-should-handle-empty-content-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Content-Management-should-set-markdown-content-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-bold-text-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-code-blocks-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-headings-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-inline-code-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-italic-text-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-lists-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-task-lists-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Selection-and-Cursor-Management-should-handle-cursor-positioning-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Selection-and-Cursor-Management-should-set-and-get-selection-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Text-Input-and-Editing-should-handle-line-breaks-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Text-Input-and-Editing-should-handle-typing-at-different-positions-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Text-Input-and-Editing-should-insert-text-at-cursor-position-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Code-Block-Extension-should-handle-code-blocks-without-language-specification-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Code-Block-Extension-should-handle-different-programming-languages-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Code-Block-Extension-should-handle-inline-code-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Code-Block-Extension-should-render-code-blocks-with-syntax-highlighting-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Extension-Interactions-should-handle-multiple-extensions-working-together-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Extension-Interactions-should-maintain-editor-state-across-complex-operations-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-File-System-Integration-should-handle-file-references-in-code-blocks-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-File-System-Integration-should-maintain-file-system-state-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Link-Extension-should-auto-detect-URLs-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Link-Extension-should-handle-email-links-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Link-Extension-should-render-links-correctly-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Markdown-Storage-should-provide-markdown-storage-interface-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Markdown-Storage-should-sync-markdown-content-with-storage-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Slash-Commands-Extension-should-handle-heading-commands-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Slash-Commands-Extension-should-trigger-slash-commands-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Table-Extension-should-handle-table-navigation-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Table-Extension-should-render-tables-correctly-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Task-List-Extension-should-handle-nested-task-lists-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Task-List-Extension-should-render-task-lists-correctly-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Task-List-Extension-should-toggle-task-completion-1.png +0 -0
- package/src/test/editor.test.ts +346 -0
- package/src/test/example.ts +92 -0
- package/src/test/extensions.test.ts +333 -0
- package/src/test/setup.ts +55 -0
- package/src/test/utils.ts +212 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.app.json +29 -0
- package/tsconfig.json +7 -0
- package/tsconfig.lib.json +17 -0
- package/tsconfig.node.json +24 -0
- package/vite.config.ts +89 -0
- package/vitest.config.ts +65 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { Selection, TextSelection } from '@tiptap/pm/state';
|
|
2
|
+
import { Node, mergeAttributes, textblockTypeInputRule } from '@tiptap/core';
|
|
3
|
+
import { basicSetup, codeblock, CodeblockFS, extOrLanguageToLanguageId, SearchIndex, setThemeEffect } from '@joinezco/codeblock';
|
|
4
|
+
import { EditorView, keymap } from '@codemirror/view';
|
|
5
|
+
import { EditorState } from "@codemirror/state";
|
|
6
|
+
import { exitCode } from "prosemirror-commands";
|
|
7
|
+
import { redo, undo } from "prosemirror-history";
|
|
8
|
+
// Function to reassign z-index values to all codeblocks in DOM order
|
|
9
|
+
function reassignZIndexes() {
|
|
10
|
+
const editors = document.querySelectorAll('.cm-editor');
|
|
11
|
+
editors.forEach((editor, index) => {
|
|
12
|
+
// Assign z-index from highest (n) to lowest (0) based on DOM order
|
|
13
|
+
// This ensures later codeblocks (lower in document) have higher z-index
|
|
14
|
+
editor.style.zIndex = (editors.length - index).toString();
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
// Global registry for codeblock instances
|
|
18
|
+
class CodeblockRegistry {
|
|
19
|
+
static instance;
|
|
20
|
+
codeblocks = new Set();
|
|
21
|
+
static getInstance() {
|
|
22
|
+
if (!CodeblockRegistry.instance) {
|
|
23
|
+
CodeblockRegistry.instance = new CodeblockRegistry();
|
|
24
|
+
}
|
|
25
|
+
return CodeblockRegistry.instance;
|
|
26
|
+
}
|
|
27
|
+
register(view) {
|
|
28
|
+
this.codeblocks.add(view);
|
|
29
|
+
}
|
|
30
|
+
unregister(view) {
|
|
31
|
+
this.codeblocks.delete(view);
|
|
32
|
+
}
|
|
33
|
+
setTheme(options) {
|
|
34
|
+
this.codeblocks.forEach(view => {
|
|
35
|
+
view.dispatch({
|
|
36
|
+
effects: setThemeEffect.of(options)
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
getCount() {
|
|
41
|
+
return this.codeblocks.size;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export const codeblockRegistry = CodeblockRegistry.getInstance();
|
|
45
|
+
let fsWorkerPromise = null;
|
|
46
|
+
function getFileSystemWorker() {
|
|
47
|
+
if (!fsWorkerPromise) {
|
|
48
|
+
fsWorkerPromise = CodeblockFS.worker();
|
|
49
|
+
}
|
|
50
|
+
return fsWorkerPromise;
|
|
51
|
+
}
|
|
52
|
+
export const ExtendedCodeblock = Node.create({
|
|
53
|
+
name: 'ezcodeBlock', // Unique name for your node
|
|
54
|
+
group: 'block', // Belongs to the 'block' group (like paragraph, heading)
|
|
55
|
+
content: 'text*', // Can contain text content
|
|
56
|
+
marks: '', // No marks (like bold, italic) allowed inside
|
|
57
|
+
defining: true, // A defining node encapsulates its content
|
|
58
|
+
code: true, // Indicates this node represents code
|
|
59
|
+
isolating: true, // Content inside is isolated from outside editing actions
|
|
60
|
+
addAttributes() {
|
|
61
|
+
return {
|
|
62
|
+
language: {
|
|
63
|
+
default: 'markdown', // Default language
|
|
64
|
+
// Parse language from HTML structure if available
|
|
65
|
+
parseHTML: element => {
|
|
66
|
+
const className = element.querySelector('code')?.getAttribute('class');
|
|
67
|
+
const extracted = className?.replace('language-', '');
|
|
68
|
+
// If the extracted value looks like a filename (contains a dot),
|
|
69
|
+
// we'll handle this in the file attribute parseHTML instead
|
|
70
|
+
if (extracted && extracted.includes('.')) {
|
|
71
|
+
const ext = extracted.split('.').pop()?.toLowerCase() || '';
|
|
72
|
+
return extOrLanguageToLanguageId[ext] || 'markdown';
|
|
73
|
+
}
|
|
74
|
+
return extracted;
|
|
75
|
+
},
|
|
76
|
+
// Render language back to HTML structure
|
|
77
|
+
renderHTML: attributes => {
|
|
78
|
+
console.log('renderHTML attributes', { attributes });
|
|
79
|
+
if (!attributes.language || attributes.language === 'plaintext') {
|
|
80
|
+
return {}; // No class needed for plaintext
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
// Add class="language-js" (or ts, py etc) to the inner <code> tag
|
|
84
|
+
class: `language-${attributes.language}`,
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
file: {
|
|
89
|
+
default: null,
|
|
90
|
+
// Parse filename from HTML class if it looks like a filename
|
|
91
|
+
parseHTML: element => {
|
|
92
|
+
const className = element.querySelector('code')?.getAttribute('class');
|
|
93
|
+
const extracted = className?.replace('language-', '');
|
|
94
|
+
// If the extracted value contains a dot, treat it as a filename
|
|
95
|
+
if (extracted && extracted.includes('.')) {
|
|
96
|
+
return extracted;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
addStorage() {
|
|
104
|
+
return {
|
|
105
|
+
markdown: {
|
|
106
|
+
serialize(state, node) {
|
|
107
|
+
if (node.attrs.file) {
|
|
108
|
+
state.write(`\`\`\`${node.attrs.file}\n`);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
state.write("```" + (node.attrs.language || "") + "\n");
|
|
112
|
+
}
|
|
113
|
+
state.text(node.textContent, false);
|
|
114
|
+
state.ensureNewLine();
|
|
115
|
+
state.write("```");
|
|
116
|
+
state.closeBlock(node);
|
|
117
|
+
},
|
|
118
|
+
parse: {
|
|
119
|
+
setup(markdownit) {
|
|
120
|
+
markdownit.set({
|
|
121
|
+
langPrefix: this.options.languageClassPrefix ?? 'language-',
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
updateDOM(element) {
|
|
125
|
+
element.innerHTML = element.innerHTML.replace(/\n<\/code><\/pre>/g, '</code></pre>');
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
// How to parse this node from HTML
|
|
132
|
+
parseHTML() {
|
|
133
|
+
return [
|
|
134
|
+
{
|
|
135
|
+
tag: 'pre', // Matches <pre> elements
|
|
136
|
+
// Optional: preserveWhitespace: 'full', // Keep all whitespace
|
|
137
|
+
// Ensure it has a <code> tag directly inside for specificity
|
|
138
|
+
contentElement: 'code', // Tell tiptap content is inside the code tag
|
|
139
|
+
},
|
|
140
|
+
];
|
|
141
|
+
},
|
|
142
|
+
// How to render this node back to HTML
|
|
143
|
+
renderHTML({ HTMLAttributes }) {
|
|
144
|
+
// mergeAttributes correctly handles the language attribute rendering defined above
|
|
145
|
+
// It renders a <pre> tag, and inside it a <code> tag with the language class
|
|
146
|
+
return ['pre', ['code', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]];
|
|
147
|
+
// The '0' is a "hole" where the content (text) will be rendered
|
|
148
|
+
},
|
|
149
|
+
// Register input rules (e.g., ``` or ~~~ at the start of a line)
|
|
150
|
+
addInputRules() {
|
|
151
|
+
return [
|
|
152
|
+
textblockTypeInputRule({
|
|
153
|
+
find: /^```([^\s`]+)?\s$/,
|
|
154
|
+
type: this.type,
|
|
155
|
+
getAttributes: match => {
|
|
156
|
+
const input = match[1]?.trim();
|
|
157
|
+
console.log('input', { input });
|
|
158
|
+
if (!input)
|
|
159
|
+
return { language: 'markdown' };
|
|
160
|
+
// If input contains a dot, treat it as a filename
|
|
161
|
+
if (input.includes('.')) {
|
|
162
|
+
const ext = input.split('.').pop()?.toLowerCase() || '';
|
|
163
|
+
const lang = extOrLanguageToLanguageId[ext] || 'markdown';
|
|
164
|
+
return {
|
|
165
|
+
file: input,
|
|
166
|
+
language: lang,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
// Otherwise, check if it's a language name
|
|
170
|
+
const matchingLanguage = Object.entries(extOrLanguageToLanguageId).find(([ext, lang]) => {
|
|
171
|
+
return lang.includes(input) || ext === input;
|
|
172
|
+
});
|
|
173
|
+
if (matchingLanguage) {
|
|
174
|
+
return {
|
|
175
|
+
language: matchingLanguage[1],
|
|
176
|
+
file: null,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
// If no language match found, default to markdown
|
|
180
|
+
return { language: 'markdown' };
|
|
181
|
+
},
|
|
182
|
+
}),
|
|
183
|
+
];
|
|
184
|
+
},
|
|
185
|
+
addNodeView() {
|
|
186
|
+
return ({ editor, node, getPos }) => {
|
|
187
|
+
const { view, schema } = editor;
|
|
188
|
+
let updating = false;
|
|
189
|
+
let cm;
|
|
190
|
+
let fsWorker = null;
|
|
191
|
+
const forwardUpdate = (cmView, update) => {
|
|
192
|
+
if (updating)
|
|
193
|
+
return;
|
|
194
|
+
// Allow forwarding updates even when not focused during initial file loading
|
|
195
|
+
// This ensures that asynchronously loaded file content gets propagated to ProseMirror
|
|
196
|
+
const pos = getPos();
|
|
197
|
+
if (pos === undefined)
|
|
198
|
+
return;
|
|
199
|
+
let offset = pos + 1, { main } = update.state.selection;
|
|
200
|
+
let selFrom = offset + main.from, selTo = offset + main.to;
|
|
201
|
+
let pmSel = view.state.selection;
|
|
202
|
+
if (update.docChanged || pmSel.from != selFrom || pmSel.to != selTo) {
|
|
203
|
+
let tr = view.state.tr;
|
|
204
|
+
// Ensure we're working within valid document bounds
|
|
205
|
+
const docLength = tr.doc.length;
|
|
206
|
+
update.changes.iterChanges((fromA, toA, fromB, toB, text) => {
|
|
207
|
+
const replaceFrom = offset + fromA;
|
|
208
|
+
const replaceTo = offset + toA;
|
|
209
|
+
// Validate positions are within bounds
|
|
210
|
+
if (replaceFrom < 0 || replaceTo > docLength || replaceFrom > replaceTo) {
|
|
211
|
+
console.warn('Invalid position range, skipping change:', { replaceFrom, replaceTo, docLength });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (text.length)
|
|
215
|
+
tr.replaceWith(replaceFrom, replaceTo, schema.text(text.toString()));
|
|
216
|
+
else
|
|
217
|
+
tr.delete(replaceFrom, replaceTo);
|
|
218
|
+
offset += (toB - fromB) - (toA - fromA);
|
|
219
|
+
});
|
|
220
|
+
// Only set selection if the editor has focus or if this is a document change without focus
|
|
221
|
+
// (which happens during initial file loading)
|
|
222
|
+
if (cmView.hasFocus || update.docChanged) {
|
|
223
|
+
const finalDocLength = tr.doc.length;
|
|
224
|
+
const clampedSelFrom = Math.max(0, Math.min(selFrom, finalDocLength));
|
|
225
|
+
const clampedSelTo = Math.max(0, Math.min(selTo, finalDocLength));
|
|
226
|
+
if (clampedSelFrom <= finalDocLength && clampedSelTo <= finalDocLength) {
|
|
227
|
+
tr.setSelection(TextSelection.create(tr.doc, clampedSelFrom, clampedSelTo));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
view.dispatch(tr);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
const maybeEscape = (unit, dir) => {
|
|
234
|
+
let { state } = cm, { main } = state.selection;
|
|
235
|
+
if (!main.empty)
|
|
236
|
+
return false;
|
|
237
|
+
if (unit == "line")
|
|
238
|
+
main = state.doc.lineAt(main.head);
|
|
239
|
+
if (dir < 0 ? main.from > 0 : main.to < state.doc.length)
|
|
240
|
+
return false;
|
|
241
|
+
// @ts-ignore
|
|
242
|
+
let targetPos = getPos() + (dir < 0 ? 0 : node.nodeSize);
|
|
243
|
+
let selection = Selection.near(view.state.doc.resolve(targetPos), dir);
|
|
244
|
+
let tr = view.state.tr.setSelection(selection).scrollIntoView();
|
|
245
|
+
view.dispatch(tr);
|
|
246
|
+
view.focus();
|
|
247
|
+
return true;
|
|
248
|
+
};
|
|
249
|
+
const maybeExit = () => {
|
|
250
|
+
if (!exitCode(view.state, view.dispatch))
|
|
251
|
+
return false;
|
|
252
|
+
view.focus();
|
|
253
|
+
return true;
|
|
254
|
+
};
|
|
255
|
+
const maybeDelete = () => {
|
|
256
|
+
// if the codeblock is empty, delete it and move our cursor to the previous position
|
|
257
|
+
if (node.textContent.length == 0) {
|
|
258
|
+
const pos = getPos();
|
|
259
|
+
if (pos !== undefined) {
|
|
260
|
+
let selection = Selection.near(view.state.doc.resolve(pos), -1);
|
|
261
|
+
let tr = view.state.tr.setSelection(selection).scrollIntoView();
|
|
262
|
+
tr.delete(pos, pos + node.nodeSize);
|
|
263
|
+
view.dispatch(tr);
|
|
264
|
+
view.focus();
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return false;
|
|
269
|
+
};
|
|
270
|
+
const codemirrorKeymap = () => {
|
|
271
|
+
return [
|
|
272
|
+
{ key: "Backspace", run: maybeDelete },
|
|
273
|
+
{ key: "ArrowUp", run: () => maybeEscape("line", -1) },
|
|
274
|
+
{ key: "ArrowLeft", run: () => maybeEscape("char", -1) },
|
|
275
|
+
{ key: "ArrowDown", run: () => maybeEscape("line", 1) },
|
|
276
|
+
{ key: "ArrowRight", run: () => maybeEscape("char", 1) },
|
|
277
|
+
{ key: "Shift-Enter", run: maybeExit },
|
|
278
|
+
{ key: "Ctrl-Enter", run: maybeExit },
|
|
279
|
+
{
|
|
280
|
+
key: "Ctrl-z", mac: "Cmd-z",
|
|
281
|
+
run: () => undo(view.state, view.dispatch)
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
key: "Shift-Ctrl-z", mac: "Shift-Cmd-z",
|
|
285
|
+
run: () => redo(view.state, view.dispatch)
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
key: "Ctrl-y", mac: "Cmd-y",
|
|
289
|
+
run: () => redo(view.state, view.dispatch)
|
|
290
|
+
}
|
|
291
|
+
];
|
|
292
|
+
};
|
|
293
|
+
// Create initial state without codeblock extension
|
|
294
|
+
const initialState = EditorState.create({
|
|
295
|
+
doc: node.textContent || '',
|
|
296
|
+
extensions: [
|
|
297
|
+
keymap.of(codemirrorKeymap()),
|
|
298
|
+
basicSetup,
|
|
299
|
+
EditorView.updateListener.of((update) => { forwardUpdate(cm, update); }),
|
|
300
|
+
]
|
|
301
|
+
});
|
|
302
|
+
cm = new EditorView({ state: initialState });
|
|
303
|
+
const dom = cm.dom;
|
|
304
|
+
// Reassign z-indexes for all codeblocks whenever a new one is created
|
|
305
|
+
// This ensures proper stacking order based on DOM position
|
|
306
|
+
reassignZIndexes();
|
|
307
|
+
// Initialize filesystem worker and update extensions asynchronously
|
|
308
|
+
getFileSystemWorker().then(fs => {
|
|
309
|
+
fsWorker = fs;
|
|
310
|
+
SearchIndex.get(fsWorker, '.codeblock/index.json').then(index => {
|
|
311
|
+
// Reconfigure with codeblock extension once fs is ready
|
|
312
|
+
cm.setState(EditorState.create({
|
|
313
|
+
doc: node.textContent || '',
|
|
314
|
+
extensions: [
|
|
315
|
+
keymap.of(codemirrorKeymap()),
|
|
316
|
+
basicSetup,
|
|
317
|
+
EditorView.updateListener.of((update) => forwardUpdate(cm, update)),
|
|
318
|
+
codeblock({
|
|
319
|
+
content: node.textContent,
|
|
320
|
+
fs: fsWorker,
|
|
321
|
+
language: node.attrs.language,
|
|
322
|
+
filepath: node.attrs.file,
|
|
323
|
+
index,
|
|
324
|
+
dark: true,
|
|
325
|
+
}),
|
|
326
|
+
]
|
|
327
|
+
}));
|
|
328
|
+
});
|
|
329
|
+
}).catch(error => {
|
|
330
|
+
console.error('Failed to initialize filesystem worker:', error);
|
|
331
|
+
});
|
|
332
|
+
// Register the codeblock instance
|
|
333
|
+
codeblockRegistry.register(cm);
|
|
334
|
+
return {
|
|
335
|
+
dom,
|
|
336
|
+
setSelection(anchor, head) {
|
|
337
|
+
cm.focus();
|
|
338
|
+
updating = true;
|
|
339
|
+
cm.dispatch({ selection: { anchor, head } });
|
|
340
|
+
updating = false;
|
|
341
|
+
},
|
|
342
|
+
destroy() {
|
|
343
|
+
// Unregister before destroying
|
|
344
|
+
codeblockRegistry.unregister(cm);
|
|
345
|
+
cm.destroy();
|
|
346
|
+
requestAnimationFrame(reassignZIndexes);
|
|
347
|
+
},
|
|
348
|
+
selectNode() { cm.focus(); },
|
|
349
|
+
stopEvent() { return true; },
|
|
350
|
+
update(updated) {
|
|
351
|
+
if (updated.type != node.type)
|
|
352
|
+
return false;
|
|
353
|
+
node = updated;
|
|
354
|
+
if (updating)
|
|
355
|
+
return true;
|
|
356
|
+
let newText = updated.textContent, curText = cm.state.doc.toString();
|
|
357
|
+
if (newText != curText) {
|
|
358
|
+
let start = 0, curEnd = curText.length, newEnd = newText.length;
|
|
359
|
+
while (start < curEnd &&
|
|
360
|
+
curText.charCodeAt(start) == newText.charCodeAt(start)) {
|
|
361
|
+
++start;
|
|
362
|
+
}
|
|
363
|
+
while (curEnd > start && newEnd > start &&
|
|
364
|
+
curText.charCodeAt(curEnd - 1) == newText.charCodeAt(newEnd - 1)) {
|
|
365
|
+
curEnd--;
|
|
366
|
+
newEnd--;
|
|
367
|
+
}
|
|
368
|
+
updating = true;
|
|
369
|
+
cm.dispatch({
|
|
370
|
+
changes: {
|
|
371
|
+
from: start, to: curEnd,
|
|
372
|
+
insert: newText.slice(start, newEnd)
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
updating = false;
|
|
376
|
+
}
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
};
|
|
381
|
+
},
|
|
382
|
+
addCommands() {
|
|
383
|
+
return {
|
|
384
|
+
setCodeblockTheme: (options) => () => {
|
|
385
|
+
codeblockRegistry.setTheme(options);
|
|
386
|
+
return true;
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
},
|
|
390
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Extension } from '@tiptap/core';
|
|
2
|
+
import { VfsInterface } from '@joinezco/codeblock';
|
|
3
|
+
export interface FileSystemOptions {
|
|
4
|
+
fs?: VfsInterface;
|
|
5
|
+
filepath?: string;
|
|
6
|
+
autoSave?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare const FileSystem: Extension<FileSystemOptions, any>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Extension } from '@tiptap/core';
|
|
2
|
+
export const FileSystem = Extension.create({
|
|
3
|
+
name: 'persistence',
|
|
4
|
+
// @ts-expect-error
|
|
5
|
+
_saveTimeout: null,
|
|
6
|
+
addOptions() {
|
|
7
|
+
return {
|
|
8
|
+
fs: undefined,
|
|
9
|
+
filepath: undefined,
|
|
10
|
+
autoSave: false,
|
|
11
|
+
};
|
|
12
|
+
},
|
|
13
|
+
addStorage() {
|
|
14
|
+
return {
|
|
15
|
+
options: this.options,
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
onCreate() {
|
|
19
|
+
const { fs, filepath } = this.options;
|
|
20
|
+
// Store options in editor storage for access by other components
|
|
21
|
+
this.storage.options = this.options;
|
|
22
|
+
if (fs && filepath) {
|
|
23
|
+
fs.readFile(filepath)
|
|
24
|
+
.then(content => {
|
|
25
|
+
this.editor.commands.setContent(content);
|
|
26
|
+
})
|
|
27
|
+
.catch(error => {
|
|
28
|
+
console.warn(`[Filesystem] Failed to load content from ${filepath}:`, error);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
onUpdate() {
|
|
33
|
+
const { fs, filepath, autoSave } = this.options;
|
|
34
|
+
if (!fs || !filepath || !autoSave)
|
|
35
|
+
return;
|
|
36
|
+
// Debounced auto-save mechanism
|
|
37
|
+
// @ts-expect-error
|
|
38
|
+
if (this._saveTimeout)
|
|
39
|
+
clearTimeout(this._saveTimeout);
|
|
40
|
+
// @ts-expect-error
|
|
41
|
+
this._saveTimeout = setTimeout(() => {
|
|
42
|
+
// @ts-expect-error
|
|
43
|
+
const markdown = this.editor.storage.markdown.getMarkdown();
|
|
44
|
+
fs.writeFile(filepath, markdown).catch(error => {
|
|
45
|
+
console.error(`[Filesystem] Failed to save content to ${filepath}:`, error);
|
|
46
|
+
});
|
|
47
|
+
}, 500); // debounce by 500ms
|
|
48
|
+
},
|
|
49
|
+
onDestroy() {
|
|
50
|
+
// @ts-expect-error
|
|
51
|
+
if (this._saveTimeout)
|
|
52
|
+
clearTimeout(this._saveTimeout);
|
|
53
|
+
},
|
|
54
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const ExtendedLink: import("@tiptap/core").Mark<import("@tiptap/extension-link").LinkOptions, any>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import Link from "@tiptap/extension-link";
|
|
2
|
+
// dumb regex which is absolutely not guaranteed to work in all cases it may have to handle
|
|
3
|
+
const LINK_INPUT_REGEX = /\[([^[]+)]\((\S+)\)$/;
|
|
4
|
+
export const ExtendedLink = Link.extend({
|
|
5
|
+
addInputRules() {
|
|
6
|
+
return [
|
|
7
|
+
{
|
|
8
|
+
find: LINK_INPUT_REGEX,
|
|
9
|
+
handler: ({ range, match, chain }) => {
|
|
10
|
+
const [, text, href] = match;
|
|
11
|
+
const { from, to } = range;
|
|
12
|
+
// Replace the markdown link with the plain text and apply the link mark
|
|
13
|
+
chain()
|
|
14
|
+
.insertContentAt({ from, to }, text)
|
|
15
|
+
.command(({ tr, state }) => {
|
|
16
|
+
tr.addMark(from, from + text.length, state.schema.marks.link.create({ href }));
|
|
17
|
+
return true;
|
|
18
|
+
})
|
|
19
|
+
.run();
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
];
|
|
23
|
+
},
|
|
24
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Extension } from '@tiptap/core';
|
|
2
|
+
export interface SlashCommand {
|
|
3
|
+
title: string;
|
|
4
|
+
description: string;
|
|
5
|
+
icon?: string;
|
|
6
|
+
command: ({ editor, range }: {
|
|
7
|
+
editor: any;
|
|
8
|
+
range: any;
|
|
9
|
+
}) => void;
|
|
10
|
+
}
|
|
11
|
+
export interface SlashCommandsOptions {
|
|
12
|
+
commands: SlashCommand[];
|
|
13
|
+
char: string;
|
|
14
|
+
allowSpaces: boolean;
|
|
15
|
+
startOfLine: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare const SlashCommands: Extension<SlashCommandsOptions, any>;
|