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