@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,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>;