@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,324 @@
1
+ import { Extension } from '@tiptap/core';
2
+ import { PluginKey } from '@tiptap/pm/state';
3
+ import { Plugin } from '@tiptap/pm/state';
4
+ import tippy from 'tippy.js';
5
+ export const SlashCommands = Extension.create({
6
+ name: 'slashCommands',
7
+ addOptions() {
8
+ return {
9
+ commands: [],
10
+ char: '/',
11
+ allowSpaces: false,
12
+ startOfLine: false,
13
+ };
14
+ },
15
+ addProseMirrorPlugins() {
16
+ let slashView = null;
17
+ return [
18
+ new Plugin({
19
+ key: new PluginKey('slashCommands'),
20
+ view: () => {
21
+ slashView = new SlashCommandsView({
22
+ editor: this.editor,
23
+ commands: this.options.commands,
24
+ char: this.options.char,
25
+ allowSpaces: this.options.allowSpaces,
26
+ startOfLine: this.options.startOfLine,
27
+ });
28
+ return slashView;
29
+ },
30
+ props: {
31
+ handleKeyDown: (_, event) => {
32
+ if (slashView) {
33
+ // Track when "/" is typed to distinguish from cursor movement
34
+ if (event.key === '/') {
35
+ slashView.lastInputWasSlash = true;
36
+ }
37
+ else {
38
+ slashView.lastInputWasSlash = false;
39
+ }
40
+ if (slashView.dropdown) {
41
+ return slashView.handleKeyDown(event);
42
+ }
43
+ }
44
+ return false;
45
+ }
46
+ }
47
+ }),
48
+ ];
49
+ },
50
+ });
51
+ class SlashCommandsView {
52
+ editor;
53
+ commands;
54
+ char;
55
+ allowSpaces;
56
+ startOfLine;
57
+ dropdown = null;
58
+ popup = null;
59
+ range = null;
60
+ query = '';
61
+ selectedIndex = 0;
62
+ lastInputWasSlash = false;
63
+ outsideClickHandler = null;
64
+ constructor({ editor, commands, char, allowSpaces, startOfLine, }) {
65
+ this.editor = editor;
66
+ this.commands = commands;
67
+ this.char = char;
68
+ this.allowSpaces = allowSpaces;
69
+ this.startOfLine = startOfLine;
70
+ this.editor.on('selectionUpdate', this.selectionUpdate.bind(this));
71
+ this.editor.on('update', this.selectionUpdate.bind(this));
72
+ }
73
+ selectionUpdate() {
74
+ const { selection } = this.editor.state;
75
+ const { $from } = selection;
76
+ // Get text before cursor in current node
77
+ const currentNode = $from.parent;
78
+ const currentNodeText = currentNode.textContent;
79
+ const posInNode = $from.parentOffset;
80
+ const textBeforeCursor = currentNodeText.slice(0, posInNode);
81
+ // Look for slash command pattern
82
+ const match = textBeforeCursor.match(new RegExp(`${this.char}([^\\s${this.char}]*)$`));
83
+ if (match && this.lastInputWasSlash) {
84
+ const query = match[1];
85
+ const from = $from.pos - match[0].length;
86
+ const to = $from.pos;
87
+ this.range = { from, to };
88
+ this.query = query;
89
+ this.selectedIndex = 0;
90
+ this.showSuggestions();
91
+ }
92
+ else {
93
+ this.hideSuggestions();
94
+ // Reset the flag when we're not in a slash command context
95
+ if (!match) {
96
+ this.lastInputWasSlash = false;
97
+ }
98
+ }
99
+ }
100
+ createDropdown() {
101
+ const dropdown = document.createElement('div');
102
+ dropdown.className = 'slash-commands-dropdown';
103
+ const filteredCommands = this.getFilteredCommands();
104
+ if (filteredCommands.length === 0) {
105
+ const noResults = document.createElement('div');
106
+ noResults.className = 'slash-command-item';
107
+ noResults.innerHTML = `
108
+ <div class="slash-command-content">
109
+ <div class="slash-command-text">
110
+ <div class="slash-command-title">No results</div>
111
+ <div class="slash-command-description">No commands found for "${this.query}"</div>
112
+ </div>
113
+ </div>
114
+ `;
115
+ dropdown.appendChild(noResults);
116
+ }
117
+ else {
118
+ filteredCommands.forEach((command, index) => {
119
+ const item = document.createElement('button');
120
+ item.className = `slash-command-item ${index === this.selectedIndex ? 'selected' : ''}`;
121
+ item.innerHTML = `
122
+ <div class="slash-command-content">
123
+ ${command.icon ? `<span class="slash-command-icon">${command.icon}</span>` : ''}
124
+ <div class="slash-command-text">
125
+ <div class="slash-command-title">${command.title}</div>
126
+ <div class="slash-command-description">${command.description}</div>
127
+ </div>
128
+ </div>
129
+ `;
130
+ item.addEventListener('click', () => this.selectCommand(command));
131
+ dropdown.appendChild(item);
132
+ });
133
+ }
134
+ // Add keyboard event handling
135
+ dropdown.addEventListener('keydown', this.handleKeyDown.bind(this));
136
+ return dropdown;
137
+ }
138
+ handleKeyDown(event) {
139
+ const filteredCommands = this.getFilteredCommands();
140
+ switch (event.key) {
141
+ case 'ArrowUp':
142
+ event.preventDefault();
143
+ if (this.selectedIndex === 0) {
144
+ // Exit dropdown when at the start and pressing up
145
+ this.hideSuggestions();
146
+ return false; // Let editor handle the event
147
+ }
148
+ else {
149
+ this.selectedIndex = this.selectedIndex - 1;
150
+ this.updateSelection();
151
+ }
152
+ return true;
153
+ case 'ArrowDown':
154
+ event.preventDefault();
155
+ if (this.selectedIndex === filteredCommands.length - 1) {
156
+ // Cycle to start when at the end and pressing down
157
+ this.selectedIndex = 0;
158
+ }
159
+ else {
160
+ this.selectedIndex = this.selectedIndex + 1;
161
+ }
162
+ this.updateSelection();
163
+ return true;
164
+ case 'Enter':
165
+ event.preventDefault();
166
+ const selectedCommand = filteredCommands[this.selectedIndex];
167
+ if (selectedCommand) {
168
+ this.selectCommand(selectedCommand);
169
+ }
170
+ return true;
171
+ case 'Escape':
172
+ event.preventDefault();
173
+ this.hideSuggestions();
174
+ return true;
175
+ default:
176
+ return false;
177
+ }
178
+ }
179
+ updateSelection() {
180
+ if (!this.dropdown)
181
+ return;
182
+ const items = this.dropdown.querySelectorAll('.slash-command-item');
183
+ items.forEach((item, index) => {
184
+ if (index === this.selectedIndex) {
185
+ item.classList.add('selected');
186
+ }
187
+ else {
188
+ item.classList.remove('selected');
189
+ }
190
+ });
191
+ }
192
+ showSuggestions() {
193
+ // Always recreate the dropdown to ensure fresh content and event listeners
194
+ this.dropdown = this.createDropdown();
195
+ if (this.popup) {
196
+ // Update existing popup content
197
+ this.popup.setContent(this.dropdown);
198
+ }
199
+ else {
200
+ // Create new popup
201
+ const instances = tippy(document.body, {
202
+ getReferenceClientRect: () => {
203
+ if (!this.range) {
204
+ // Return a default rect if range is null
205
+ return new DOMRect(0, 0, 0, 0);
206
+ }
207
+ const { view } = this.editor;
208
+ const { from } = this.range;
209
+ const start = view.coordsAtPos(from);
210
+ const end = view.coordsAtPos(this.range.to);
211
+ return {
212
+ top: start.top,
213
+ bottom: end.bottom,
214
+ left: start.left,
215
+ right: end.right,
216
+ width: end.right - start.left,
217
+ height: end.bottom - start.top,
218
+ x: start.left,
219
+ y: start.top,
220
+ toJSON: () => ({
221
+ top: start.top,
222
+ bottom: end.bottom,
223
+ left: start.left,
224
+ right: end.right,
225
+ width: end.right - start.left,
226
+ height: end.bottom - start.top,
227
+ x: start.left,
228
+ y: start.top,
229
+ })
230
+ };
231
+ },
232
+ appendTo: () => document.body,
233
+ content: this.dropdown,
234
+ showOnCreate: true,
235
+ interactive: true,
236
+ trigger: 'manual',
237
+ placement: 'bottom-start',
238
+ theme: 'slash-commands',
239
+ maxWidth: 'none',
240
+ onShow: () => {
241
+ // Add outside click handler when dropdown is shown
242
+ this.addOutsideClickHandler();
243
+ },
244
+ onHide: () => {
245
+ // Remove outside click handler when dropdown is hidden
246
+ this.removeOutsideClickHandler();
247
+ }
248
+ });
249
+ this.popup = instances[0];
250
+ }
251
+ }
252
+ hideSuggestions() {
253
+ this.removeOutsideClickHandler();
254
+ if (this.popup) {
255
+ this.popup.destroy();
256
+ this.popup = null;
257
+ }
258
+ // Force cleanup of any remaining tippy instances
259
+ const existingTippyInstances = document.querySelectorAll('[data-tippy-root]');
260
+ existingTippyInstances.forEach(instance => {
261
+ instance.remove();
262
+ });
263
+ // Also remove any dropdown elements that might be lingering
264
+ const existingDropdowns = document.querySelectorAll('.slash-commands-dropdown');
265
+ existingDropdowns.forEach(dropdown => {
266
+ console.log('Force removing dropdown element');
267
+ dropdown.remove();
268
+ });
269
+ this.dropdown = null;
270
+ this.range = null;
271
+ this.query = '';
272
+ this.selectedIndex = 0;
273
+ this.lastInputWasSlash = false;
274
+ }
275
+ addOutsideClickHandler() {
276
+ if (this.outsideClickHandler) {
277
+ this.removeOutsideClickHandler();
278
+ }
279
+ this.outsideClickHandler = (event) => {
280
+ const target = event.target;
281
+ // Check if click is outside the dropdown and editor
282
+ if (this.dropdown && !this.dropdown.contains(target) &&
283
+ !this.editor.view.dom.contains(target)) {
284
+ this.hideSuggestions();
285
+ }
286
+ };
287
+ // Add listener with a slight delay to avoid immediate closure
288
+ setTimeout(() => {
289
+ document.addEventListener('click', this.outsideClickHandler, true);
290
+ }, 100);
291
+ }
292
+ removeOutsideClickHandler() {
293
+ if (this.outsideClickHandler) {
294
+ document.removeEventListener('click', this.outsideClickHandler, true);
295
+ this.outsideClickHandler = null;
296
+ }
297
+ }
298
+ getFilteredCommands() {
299
+ if (!this.query) {
300
+ return this.commands;
301
+ }
302
+ return this.commands.filter(command => command.title.toLowerCase().includes(this.query.toLowerCase()) ||
303
+ command.description.toLowerCase().includes(this.query.toLowerCase()));
304
+ }
305
+ selectCommand(command) {
306
+ if (this.range) {
307
+ // Store the range before hiding suggestions, as hideSuggestions() might clear it
308
+ const range = this.range;
309
+ console.log('Executing command, hiding suggestions first');
310
+ // Hide suggestions first to ensure dropdown closes
311
+ this.hideSuggestions();
312
+ console.log('Suggestions hidden, executing command');
313
+ // Then execute the command with the stored range
314
+ command.command({ editor: this.editor, range });
315
+ console.log('Command execution completed');
316
+ }
317
+ }
318
+ destroy() {
319
+ this.removeOutsideClickHandler();
320
+ this.hideSuggestions();
321
+ this.editor.off('selectionUpdate', this.selectionUpdate);
322
+ this.editor.off('update', this.selectionUpdate);
323
+ }
324
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Extended TaskItem extension with configurable input regex rules.
3
+ */
4
+ export interface ExtendedTaskItemOptions {
5
+ nested: boolean;
6
+ inputRegex?: RegExp;
7
+ }
8
+ export declare const ExtendedTaskItem: import("@tiptap/core").Node<ExtendedTaskItemOptions, any>;
@@ -0,0 +1,67 @@
1
+ import { TaskItem } from '@tiptap/extension-task-item';
2
+ import { wrappingInputRule } from '@tiptap/core';
3
+ export const ExtendedTaskItem = TaskItem.extend({
4
+ addOptions() {
5
+ return {
6
+ ...this.parent?.(),
7
+ nested: true,
8
+ inputRegex: /^\s*[-+*]\s\[( |x|X)\](?:\s)?$/, // Match `- [ ]` or `- [x]`, with optional space
9
+ };
10
+ },
11
+ addInputRules() {
12
+ const { inputRegex } = this.options;
13
+ return [
14
+ wrappingInputRule({
15
+ find: inputRegex,
16
+ type: this.type,
17
+ getAttributes: (match) => {
18
+ return {
19
+ checked: match[1] === 'x' || match[1] === 'X',
20
+ };
21
+ },
22
+ })
23
+ ];
24
+ },
25
+ addNodeView() {
26
+ return ({ node, getPos, editor }) => {
27
+ const listItem = document.createElement('li');
28
+ listItem.setAttribute('data-type', 'taskItem');
29
+ listItem.setAttribute('data-checked', node.attrs.checked);
30
+ const checkboxWrapper = document.createElement('label');
31
+ checkboxWrapper.contentEditable = 'false';
32
+ const checkbox = document.createElement('input');
33
+ checkbox.type = 'checkbox';
34
+ checkbox.checked = node.attrs.checked;
35
+ // Add click handler to the checkbox
36
+ checkbox.addEventListener('change', () => {
37
+ const pos = getPos();
38
+ if (typeof pos === 'number') {
39
+ const { tr } = editor.state;
40
+ tr.setNodeMarkup(pos, undefined, {
41
+ ...node.attrs,
42
+ checked: checkbox.checked
43
+ });
44
+ editor.view.dispatch(tr);
45
+ }
46
+ });
47
+ const content = document.createElement('div');
48
+ content.style.display = 'inline';
49
+ checkboxWrapper.appendChild(checkbox);
50
+ listItem.appendChild(checkboxWrapper);
51
+ listItem.appendChild(content);
52
+ return {
53
+ dom: listItem,
54
+ contentDOM: content,
55
+ update: (updatedNode) => {
56
+ if (updatedNode.type !== this.type) {
57
+ return false;
58
+ }
59
+ // Update checkbox state when node is updated
60
+ checkbox.checked = updatedNode.attrs.checked;
61
+ listItem.setAttribute('data-checked', updatedNode.attrs.checked);
62
+ return true;
63
+ }
64
+ };
65
+ };
66
+ },
67
+ });
@@ -0,0 +1,19 @@
1
+ import { Editor, EditorOptions, Extension } from '@tiptap/core';
2
+ import { MarkdownStorage } from 'tiptap-markdown';
3
+ import { FileSystemOptions } from './extensions/filesystem';
4
+ export type MarkdownEditorOptions = Partial<EditorOptions> & {
5
+ extensions?: Extension[];
6
+ fs?: FileSystemOptions;
7
+ };
8
+ export type MarkdownEditor = Editor & {
9
+ storage: {
10
+ markdown: MarkdownStorage;
11
+ } & Record<string, any>;
12
+ };
13
+ /**
14
+ * Create a Markdown-ready Tiptap Editor with default extensions and options.
15
+ *
16
+ * @param options Optional overrides for the Tiptap Editor options.
17
+ * @returns An instance of Tiptap Editor.
18
+ */
19
+ export declare function createEditor(options?: MarkdownEditorOptions): MarkdownEditor;
@@ -0,0 +1,68 @@
1
+ import { Editor } from '@tiptap/core';
2
+ import StarterKit from '@tiptap/starter-kit';
3
+ import TaskList from '@tiptap/extension-task-list';
4
+ import { TableKit } from '@tiptap/extension-table';
5
+ import { Markdown } from 'tiptap-markdown';
6
+ import { ExtendedCodeblock } from './extensions/codeblock';
7
+ import { ExtendedTaskItem } from './extensions/taskitem';
8
+ import { FileSystem } from './extensions/filesystem';
9
+ import { styleModule } from './styles';
10
+ import { ExtendedLink } from './extensions/link';
11
+ import { SlashCommands } from './extensions/slash-commands';
12
+ import { defaultSlashCommands } from './commands';
13
+ import { StyleModule } from 'style-mod';
14
+ /**
15
+ * Create a Markdown-ready Tiptap Editor with default extensions and options.
16
+ *
17
+ * @param options Optional overrides for the Tiptap Editor options.
18
+ * @returns An instance of Tiptap Editor.
19
+ */
20
+ export function createEditor(options = {}) {
21
+ const editor = new Editor({
22
+ extensions: [
23
+ FileSystem.configure(options.fs || {}),
24
+ ExtendedLink.configure({}),
25
+ StarterKit.configure({
26
+ codeBlock: false,
27
+ // bulletList: false, // As Markdown handles bullet lists and allows us to configure the marker to prevent task item conflicts
28
+ }),
29
+ Markdown.configure({
30
+ html: false,
31
+ tightLists: true,
32
+ tightListClass: 'tight',
33
+ bulletListMarker: '*',
34
+ linkify: true,
35
+ breaks: true,
36
+ transformPastedText: true,
37
+ transformCopiedText: true,
38
+ }),
39
+ ExtendedCodeblock,
40
+ TaskList,
41
+ ExtendedTaskItem.configure({
42
+ nested: true,
43
+ }),
44
+ TableKit.configure({
45
+ table: { resizable: true, allowTableNodeSelection: true },
46
+ }),
47
+ SlashCommands.configure({
48
+ commands: defaultSlashCommands,
49
+ }),
50
+ ...(options.extensions || []),
51
+ ],
52
+ editorProps: {
53
+ attributes: options.editorProps?.attributes || {},
54
+ ...(options.editorProps || {}),
55
+ },
56
+ content: options.content || '',
57
+ onUpdate: options.onUpdate || (() => { }),
58
+ autofocus: options.autofocus,
59
+ editable: options.editable,
60
+ injectCSS: options.injectCSS,
61
+ ...options,
62
+ });
63
+ editor.view.dom.classList.add('ezco-mde');
64
+ if (typeof document !== 'undefined') {
65
+ StyleModule.mount(document, styleModule);
66
+ }
67
+ return editor;
68
+ }
@@ -0,0 +1,2 @@
1
+ import { StyleModule } from 'style-mod';
2
+ export declare const styleModule: StyleModule;
@@ -0,0 +1,153 @@
1
+ import { StyleModule } from 'style-mod';
2
+ const darkModeStyles = {
3
+ '--ezco-mde-code-bg': 'var(--ezco-mde-code-bg-dark)',
4
+ '--ezco-mde-bg': 'var(--ezco-mde-bg-dark)',
5
+ '--ezco-mde-table-bg': 'var(--cm-toolbar-bg-dark)',
6
+ };
7
+ export const styleModule = new StyleModule({
8
+ ':root[data-theme="dark"], [data-theme="dark"] .ezco-mde, .ezco-mde[data-theme="dark"]': darkModeStyles,
9
+ '@media (prefers-color-scheme: dark)': {
10
+ 'div.ezco-mde': darkModeStyles
11
+ },
12
+ ':root, :root[data-theme="light"], [data-theme="light"] .ezco-mde, .ezco-mde[data-theme="light"]': {
13
+ // Light/dark mode vars
14
+ '--ezco-mde-code-bg-light': '#f1f1f1',
15
+ '--ezco-mde-code-bg-dark': '#2c2c2c',
16
+ '--ezco-mde-bg-light': '#ffffff',
17
+ '--ezco-mde-bg-dark': '#1e1e1e',
18
+ '--ezco-mde-link-color': '#5861ff',
19
+ '--ezco-mde-link-color-hover': '#383ea3',
20
+ // Default to light mode, overridden by media query
21
+ '--ezco-mde-code-bg': 'var(--ezco-mde-code-bg-light)',
22
+ '--ezco-mde-bg': 'var(--ezco-mde-bg-light)',
23
+ '--ezco-mde-table-bg': 'var(--cm-toolbar-bg-light)',
24
+ },
25
+ '.ezco-mde': {
26
+ // Base editor styles
27
+ 'background': 'transparent',
28
+ '& a': {
29
+ color: 'var(--ezco-mde-link-color)',
30
+ 'text-decoration': 'inherit',
31
+ },
32
+ '& a:hover': {
33
+ color: 'var(--ezco-mde-link-color-hover)',
34
+ cursor: 'pointer',
35
+ },
36
+ // Codeblock styles
37
+ '& .cm-editor': {
38
+ margin: '2rem 0',
39
+ border: '2px solid var(--ezco-mde-table-bg)'
40
+ },
41
+ // Inline code styles
42
+ '& > :not(.cm-editor) code': {
43
+ 'font-family': 'monospace',
44
+ background: 'var(--ezco-mde-code-bg)',
45
+ padding: '0.1em 0.3em',
46
+ 'border-radius': '3px',
47
+ },
48
+ // Table styles
49
+ '&.tableWrapper': {
50
+ margin: '1.5rem 0',
51
+ 'overflow-x': 'auto'
52
+ },
53
+ '& table': {
54
+ "border-collapse": "collapse",
55
+ "width": "100%",
56
+ "margin": "2em 0",
57
+ border: '2px solid var(--ezco-mde-table-bg)',
58
+ overflow: 'hidden',
59
+ 'table-layout': 'fixed',
60
+ '& p': {
61
+ margin: 0
62
+ },
63
+ '& > .column-resize-handle': {
64
+ 'background-color': 'red',
65
+ bottom: '-2px',
66
+ 'pointer-events': 'none',
67
+ position: 'absolute',
68
+ right: '-2px',
69
+ top: 0,
70
+ width: '4px',
71
+ },
72
+ '& th': {
73
+ 'font-weight': 'bold',
74
+ 'background-color': 'var(--ezco-mde-table-bg)',
75
+ 'text-align': 'left',
76
+ },
77
+ '& th, & td': {
78
+ border: 'none',
79
+ padding: '0.5em',
80
+ 'vertical-align': 'top',
81
+ position: 'relative',
82
+ },
83
+ },
84
+ '& .selectedCell::after': {
85
+ 'z-index': 2,
86
+ position: 'absolute',
87
+ content: '""',
88
+ left: 0,
89
+ right: 0,
90
+ top: 0,
91
+ bottom: 0,
92
+ background: 'rgba(0, 123, 255, 0.1)',
93
+ 'pointer-events': 'none',
94
+ },
95
+ '&.resize-cursor': {
96
+ '&': {
97
+ cursor: 'ew-resize',
98
+ },
99
+ cursor: 'col-resize',
100
+ },
101
+ // Tight list styles
102
+ '& .tight': {
103
+ margin: '0 18px',
104
+ '& li': {
105
+ 'padding-left': '2px',
106
+ }
107
+ },
108
+ // List styles
109
+ '& ul, & ol, & menu': {
110
+ padding: 0,
111
+ },
112
+ // List item styles
113
+ '& li > p': {
114
+ 'margin-top': 0,
115
+ 'margin-bottom': 0,
116
+ },
117
+ // Task list styles
118
+ '& li[data-checked="true"]>div>p': {
119
+ "text-decoration": "line-through",
120
+ "color": "#888",
121
+ },
122
+ '& ul[data-type="taskList"]': {
123
+ 'list-style': 'none',
124
+ 'padding': 0,
125
+ 'margin': 0,
126
+ '& p': {
127
+ 'margin': 0,
128
+ },
129
+ '& p + ul[data-type="taskList"]': {
130
+ 'margin-top': '0.25em'
131
+ },
132
+ '& p + p': {
133
+ 'margin-top': '0.25em',
134
+ },
135
+ '& li': {
136
+ display: 'flex',
137
+ 'align-items': 'flex-start',
138
+ },
139
+ '& li + li': {
140
+ 'margin-top': '0.25em'
141
+ },
142
+ '& li > label': {
143
+ 'margin-right': '6px'
144
+ },
145
+ '& li > label > input': {
146
+ margin: 0
147
+ },
148
+ '& li > div': {
149
+ flex: 1
150
+ }
151
+ },
152
+ }
153
+ });
@@ -0,0 +1,2 @@
1
+ export * from './editor/index';
2
+ export type { MarkdownEditor, MarkdownEditorOptions } from './editor/index';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ // Main library entry point
2
+ export * from './editor/index';
package/index.html ADDED
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
+ <title>Vite + React + TS</title>
9
+ </head>
10
+
11
+ <body>
12
+ <div id="root"></div>
13
+ <script type="module" src="/src/main.tsx"></script>
14
+ </body>
15
+
16
+ </html>