@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,92 @@
1
+ export const file = `
2
+ # \`@joinezco/markdown-editor\`
3
+
4
+ This editor supports **Markdown** syntax.
5
+
6
+ ## Features
7
+
8
+ ### Bring-your-own-LLM
9
+
10
+ Use \`/settings\` to configure, \`ctrl + enter\` to trigger a completion
11
+
12
+ * [ ] \`TODO: actually use settings\`
13
+ * [ ] \`TODO: actually use llms\`
14
+ ### Lists
15
+
16
+ #### Bullets
17
+
18
+ * Paragraphs
19
+ * Headings
20
+ * *Italic* and **Bold** text
21
+ * \`Inline code\`
22
+ * Links (auto-detected [example.com](http://example.com) and [manual](https://google.com)
23
+
24
+ #### Ordered
25
+
26
+ 1. \`todo:\` Emojis
27
+ 2.
28
+
29
+ #### Tasks
30
+
31
+ * [x] Task 1 (Done)
32
+
33
+ * [ ] Task 2 (Pending)
34
+
35
+ * [ ] Subtask 2.1
36
+
37
+ * [ ] Task 3
38
+
39
+ ### Tables
40
+
41
+ | Header 1 | Header 2 | Header 3 |
42
+ |----------|----------|----------|
43
+ | Cell 1 | Cell 2 | Cell 3 |
44
+ | Cell 4 | Cell 5 | Cell 6 |
45
+
46
+ - [ ] \`TODO: fix pasting typical md syntax not producing tables\`
47
+
48
+ ### Codeblocks
49
+
50
+ \`\`\`javascript
51
+ function greet(name) {
52
+ console.log(\`Hello, \${name}!\`);
53
+ }
54
+
55
+ greet('World');
56
+ \`\`\`
57
+
58
+ - [ ] \`TODO: support registering/calling execution handlers for each file extension/mime\` (e.g. allowing to run files)
59
+
60
+ #### Language server support
61
+
62
+ Lazily-loaded language server support for Typescript/Javascript, Python, Rust, and Go.
63
+
64
+ \`\`\`python
65
+ def add(a, b):
66
+ """Adds two numbers."""
67
+ return a + b
68
+
69
+ print(add(5, 3))
70
+ \`\`\`
71
+
72
+
73
+ * [ ] \`TODO: support LSPs\`
74
+
75
+ * [x] \`js/ts\`
76
+
77
+ * [ ] \`python\`
78
+
79
+ * [ ] \`rust\`
80
+
81
+ * [ ] \`go\`
82
+
83
+
84
+ #### Virtual filesystem
85
+
86
+ Reference and change files in a document-local filesystem.
87
+
88
+ \`\`\`src/App.tsx
89
+ \`\`\`
90
+
91
+ Try editing the content!
92
+ `;
@@ -0,0 +1,333 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
+ import { MarkdownEditor } from '../lib/editor'
3
+ import {
4
+ createTestContainer,
5
+ createTestEditor,
6
+ cleanupEditor,
7
+ } from './utils'
8
+
9
+ describe('MarkdownEditor Extensions', () => {
10
+ let container: HTMLElement
11
+ let editor: MarkdownEditor
12
+
13
+ beforeEach(async () => {
14
+ container = createTestContainer()
15
+ editor = await createTestEditor(container)
16
+ // await waitForEditor(editor)
17
+ })
18
+
19
+ afterEach(() => {
20
+ cleanupEditor(editor, container)
21
+ })
22
+
23
+ describe('Task List Extension', () => {
24
+ it('should render task lists correctly', () => {
25
+ editor.commands.setContent('- [ ] Unchecked task\n- [x] Checked task\n- [ ] Another unchecked')
26
+
27
+ const uncheckedTasks = editor.view.dom.querySelectorAll('[data-checked="false"]')
28
+ const checkedTasks = editor.view.dom.querySelectorAll('[data-checked="true"]')
29
+
30
+ expect(uncheckedTasks.length).toBe(2)
31
+ expect(checkedTasks.length).toBe(1)
32
+
33
+ expect(uncheckedTasks[0].textContent).toContain('Unchecked task')
34
+ expect(checkedTasks[0].textContent).toContain('Checked task')
35
+ expect(uncheckedTasks[1].textContent).toContain('Another unchecked')
36
+ })
37
+
38
+ it('should toggle task completion', () => {
39
+ editor.commands.setContent('- [ ] Task to toggle')
40
+ editor.commands.focus()
41
+
42
+ // Find the task item and verify initial state
43
+ const initialTaskElement = editor.view.dom.querySelector('[data-checked="false"]')
44
+ expect(initialTaskElement).toBeTruthy()
45
+ expect(initialTaskElement?.textContent).toContain('Task to toggle')
46
+
47
+ // Simulate task toggle (this would normally be done by clicking)
48
+ editor.commands.setContent('- [x] Task to toggle')
49
+
50
+ const toggledTaskElement = editor.view.dom.querySelector('[data-checked="true"]')
51
+ expect(toggledTaskElement).toBeTruthy()
52
+ expect(toggledTaskElement?.textContent).toContain('Task to toggle')
53
+
54
+ // Verify the unchecked task no longer exists
55
+ const uncheckedTask = editor.view.dom.querySelector('[data-checked="false"]')
56
+ expect(uncheckedTask).toBeNull()
57
+ })
58
+
59
+ it('should handle nested task lists', () => {
60
+ const nestedTasks = `- [ ] Parent task
61
+ - [ ] Child task 1
62
+ - [x] Child task 2
63
+ - [x] Another parent task`
64
+
65
+ editor.commands.setContent(nestedTasks)
66
+
67
+ const allTasks = editor.view.dom.querySelectorAll('[data-checked]')
68
+ expect(allTasks.length).toBe(4)
69
+
70
+ const uncheckedTasks = editor.view.dom.querySelectorAll('[data-checked="false"]')
71
+ const checkedTasks = editor.view.dom.querySelectorAll('[data-checked="true"]')
72
+
73
+ expect(uncheckedTasks.length).toBe(2)
74
+ expect(checkedTasks.length).toBe(2)
75
+
76
+ // Verify task content
77
+ const taskTexts = Array.from(allTasks).map(task => task.textContent?.trim())
78
+ expect(taskTexts).toEqual(expect.arrayContaining([
79
+ expect.stringContaining('Parent task'),
80
+ expect.stringContaining('Child task 1'),
81
+ expect.stringContaining('Child task 2'),
82
+ expect.stringContaining('Another parent task')
83
+ ]))
84
+ })
85
+ })
86
+
87
+ describe('Table Extension', () => {
88
+ it('should render tables correctly', () => {
89
+ const tableMarkdown = `| Header 1 | Header 2 | Header 3 |
90
+ |----------|----------|----------|
91
+ | Cell 1 | Cell 2 | Cell 3 |
92
+ | Cell 4 | Cell 5 | Cell 6 |`
93
+
94
+ editor.commands.setContent(tableMarkdown)
95
+
96
+ const table = editor.view.dom.querySelector('table')
97
+ expect(table).toBeTruthy()
98
+
99
+ const thead = table?.querySelector('thead')
100
+ const tbody = table?.querySelector('tbody')
101
+ expect(thead).toBeTruthy()
102
+ expect(tbody).toBeTruthy()
103
+
104
+ const headers = thead?.querySelectorAll('th')
105
+ expect(headers?.length).toBe(3)
106
+ expect(headers?.[0].textContent?.trim()).toBe('Header 1')
107
+ expect(headers?.[1].textContent?.trim()).toBe('Header 2')
108
+ expect(headers?.[2].textContent?.trim()).toBe('Header 3')
109
+
110
+ const rows = tbody?.querySelectorAll('tr')
111
+ expect(rows?.length).toBe(2)
112
+
113
+ const firstRowCells = rows?.[0].querySelectorAll('td')
114
+ expect(firstRowCells?.length).toBe(3)
115
+ expect(firstRowCells?.[0].textContent?.trim()).toBe('Cell 1')
116
+ expect(firstRowCells?.[1].textContent?.trim()).toBe('Cell 2')
117
+ expect(firstRowCells?.[2].textContent?.trim()).toBe('Cell 3')
118
+ })
119
+
120
+ it('should handle table navigation', () => {
121
+ const tableMarkdown = `| A | B |
122
+ |---|---|
123
+ | 1 | 2 |`
124
+
125
+ editor.commands.setContent(tableMarkdown)
126
+ editor.commands.focus()
127
+
128
+ // Position cursor in first cell
129
+ const firstCell = editor.view.dom.querySelector('td')
130
+ if (firstCell) {
131
+ firstCell.focus()
132
+ }
133
+
134
+ // Test that table structure is maintained
135
+ const table = editor.view.dom.querySelector('table')
136
+ expect(table).toBeTruthy()
137
+
138
+ const cells = table?.querySelectorAll('td')
139
+ expect(cells?.length).toBe(2)
140
+ expect(cells?.[0].textContent?.trim()).toBe('1')
141
+ expect(cells?.[1].textContent?.trim()).toBe('2')
142
+ })
143
+ })
144
+
145
+ describe('Link Extension', () => {
146
+ it('should render links correctly', () => {
147
+ editor.commands.setContent('Visit [Google](https://google.com) for search.')
148
+
149
+ const link = editor.view.dom.querySelector('a')
150
+ expect(link).toBeTruthy()
151
+ expect(link?.getAttribute('href')).toBe('https://google.com')
152
+ expect(link?.textContent).toBe('Google')
153
+ })
154
+
155
+ it('should auto-detect URLs', () => {
156
+ editor.commands.setContent('Visit https://example.com for more info.')
157
+
158
+ // Check if URL is present in the content (may or may not be auto-linked depending on extension)
159
+ const content = editor.view.dom.textContent
160
+ expect(content).toContain('https://example.com')
161
+
162
+ // If auto-linking is enabled, check for link element
163
+ const autoLink = editor.view.dom.querySelector('a[href="https://example.com"]')
164
+ if (autoLink) {
165
+ expect(autoLink.textContent).toBe('https://example.com')
166
+ }
167
+ })
168
+
169
+ it('should handle email links', () => {
170
+ editor.commands.setContent('Contact us at [email](mailto:test@example.com)')
171
+
172
+ const emailLink = editor.view.dom.querySelector('a[href="mailto:test@example.com"]')
173
+ expect(emailLink).toBeTruthy()
174
+ expect(emailLink?.textContent).toBe('email')
175
+ expect(emailLink?.getAttribute('href')).toBe('mailto:test@example.com')
176
+ })
177
+ })
178
+
179
+ describe('Code Block Extension', () => {
180
+ it('should render code blocks with syntax highlighting', () => {
181
+ const codeBlock = '```javascript\nfunction hello() {\n console.log("Hello, World!");\n}\n```'
182
+ editor.commands.setContent(codeBlock)
183
+
184
+ const preElement = editor.view.dom.querySelector('pre')
185
+ expect(preElement).toBeTruthy()
186
+
187
+ const codeContent = preElement?.textContent || ''
188
+ expect(codeContent).toContain('function hello')
189
+ expect(codeContent).toContain('console.log')
190
+ expect(codeContent).toContain('Hello, World!')
191
+ })
192
+
193
+ it('should handle different programming languages', () => {
194
+ const pythonCode = '```python\ndef hello():\n print("Hello, World!")\n```'
195
+ editor.commands.setContent(pythonCode)
196
+
197
+ const preElement = editor.view.dom.querySelector('pre')
198
+ expect(preElement).toBeTruthy()
199
+
200
+ const codeContent = preElement?.textContent || ''
201
+ expect(codeContent).toContain('def hello')
202
+ expect(codeContent).toContain('print')
203
+ expect(codeContent).toContain('Hello, World!')
204
+ })
205
+
206
+ it('should handle code blocks without language specification', () => {
207
+ const plainCode = '```\nplain text code\nno syntax highlighting\n```'
208
+ editor.commands.setContent(plainCode)
209
+
210
+ const preElement = editor.view.dom.querySelector('pre')
211
+ expect(preElement).toBeTruthy()
212
+
213
+ const codeContent = preElement?.textContent || ''
214
+ expect(codeContent).toContain('plain text code')
215
+ expect(codeContent).toContain('no syntax highlighting')
216
+ })
217
+
218
+ it('should handle inline code', () => {
219
+ editor.commands.setContent('Use the `console.log()` function to debug.')
220
+
221
+ const codeElement = editor.view.dom.querySelector('code')
222
+ expect(codeElement).toBeTruthy()
223
+ expect(codeElement?.textContent).toBe('console.log()')
224
+ })
225
+ })
226
+
227
+ describe('Slash Commands Extension', () => {
228
+ it('should trigger slash commands', () => {
229
+ editor.commands.focus()
230
+ editor.commands.setTextSelection(0)
231
+
232
+ // Type slash to trigger command menu
233
+ const { from } = editor.state.selection
234
+ editor.commands.insertContentAt(from, '/')
235
+
236
+ // TODO:
237
+ })
238
+ })
239
+
240
+ describe('File System Integration', () => {
241
+ it('should handle file references in code blocks', () => {
242
+ const fileReference = '```src/example.js\nconsole.log("File content");\n```'
243
+ editor.commands.setContent(fileReference)
244
+
245
+ const preElement = editor.view.dom.querySelector('pre')
246
+ expect(preElement).toBeTruthy()
247
+
248
+ const codeContent = preElement?.textContent || ''
249
+ expect(codeContent).toContain('console.log')
250
+ expect(codeContent).toContain('File content')
251
+ })
252
+
253
+ it('should maintain file system state', () => {
254
+ // Test that the editor maintains its filesystem integration
255
+ expect(editor.storage).toBeDefined()
256
+ expect(editor.storage.markdown).toBeDefined()
257
+ })
258
+ })
259
+
260
+ describe('Markdown Storage', () => {
261
+ it('should provide markdown storage interface', () => {
262
+ expect(editor.storage.markdown).toBeDefined()
263
+ expect(typeof editor.storage.markdown.getMarkdown).toBe('function')
264
+ })
265
+
266
+ it('should sync markdown content with storage', () => {
267
+ const testContent = '# Storage Test\n\nThis tests the storage interface.'
268
+ editor.commands.setContent(testContent)
269
+
270
+ const storedContent = editor.storage.markdown.getMarkdown()
271
+ expect(storedContent).toContain('# Storage Test')
272
+ expect(storedContent).toContain('This tests the storage interface.')
273
+ })
274
+ })
275
+
276
+ describe('Extension Interactions', () => {
277
+ it('should handle multiple extensions working together', () => {
278
+ const complexContent = `# Document with Multiple Features
279
+
280
+ ## Task List
281
+ - [ ] Incomplete task
282
+ - [x] Complete task
283
+
284
+ ## Code Example
285
+ \`\`\`javascript
286
+ function example() {
287
+ return "Hello, World!";
288
+ }
289
+ \`\`\`
290
+
291
+ ## Table
292
+ | Feature | Status |
293
+ |---------|--------|
294
+ | Tasks | ✓ |
295
+ | Code | ✓ |
296
+ | Tables | ✓ |
297
+
298
+ ## Links
299
+ Visit [our website](https://example.com) for more info.`
300
+
301
+ editor.commands.setContent(complexContent)
302
+
303
+ // Verify all extensions are working
304
+ const h1 = editor.view.dom.querySelector('h1')
305
+ const h2Elements = editor.view.dom.querySelectorAll('h2')
306
+ expect(h1).toBeTruthy()
307
+ expect(h1?.textContent).toContain('Document with Multiple Features')
308
+ expect(h2Elements.length).toBeGreaterThan(0)
309
+
310
+ // Task lists
311
+ const taskElements = editor.view.dom.querySelectorAll('[data-checked]')
312
+ expect(taskElements.length).toBe(2)
313
+ expect(editor.view.dom.querySelector('[data-checked="false"]')).toBeTruthy()
314
+ expect(editor.view.dom.querySelector('[data-checked="true"]')).toBeTruthy()
315
+
316
+ // Code blocks
317
+ const preElement = editor.view.dom.querySelector('pre')
318
+ expect(preElement).toBeTruthy()
319
+ expect(preElement?.textContent).toContain('function example')
320
+
321
+ // Tables
322
+ const table = editor.view.dom.querySelector('table')
323
+ expect(table).toBeTruthy()
324
+ const tableHeaders = table?.querySelectorAll('th')
325
+ expect(tableHeaders?.length).toBe(2)
326
+
327
+ // Links
328
+ const link = editor.view.dom.querySelector('a[href="https://example.com"]')
329
+ expect(link).toBeTruthy()
330
+ expect(link?.textContent).toBe('our website')
331
+ })
332
+ })
333
+ })
@@ -0,0 +1,55 @@
1
+ import { beforeAll, afterEach } from 'vitest'
2
+
3
+ // Global test setup
4
+ beforeAll(() => {
5
+ // Setup any global test configuration here
6
+ console.log('Setting up test environment for markdown editor...')
7
+ })
8
+
9
+ // Cleanup after each test
10
+ afterEach(() => {
11
+ // Clean up any DOM elements created during tests
12
+ document.body.innerHTML = ''
13
+ })
14
+
15
+ // Mock window.matchMedia for tests
16
+ Object.defineProperty(window, 'matchMedia', {
17
+ writable: true,
18
+ value: (query: string) => ({
19
+ matches: false,
20
+ media: query,
21
+ onchange: null,
22
+ addListener: () => { },
23
+ removeListener: () => { },
24
+ addEventListener: () => { },
25
+ removeEventListener: () => { },
26
+ dispatchEvent: () => { },
27
+ }),
28
+ })
29
+
30
+ // Mock ResizeObserver
31
+ globalThis.ResizeObserver = class ResizeObserver {
32
+ observe() { }
33
+ unobserve() { }
34
+ disconnect() { }
35
+ }
36
+
37
+ // Mock IntersectionObserver
38
+ globalThis.IntersectionObserver = class IntersectionObserver {
39
+ root = null
40
+ rootMargin = ''
41
+ thresholds = []
42
+
43
+ constructor() { }
44
+ observe() { }
45
+ unobserve() { }
46
+ disconnect() { }
47
+ takeRecords() { return [] }
48
+ }
49
+
50
+ // Mock getComputedStyle
51
+ Object.defineProperty(window, 'getComputedStyle', {
52
+ value: () => ({
53
+ getPropertyValue: () => '',
54
+ }),
55
+ })
@@ -0,0 +1,212 @@
1
+ import { MarkdownEditor, createEditor, MarkdownEditorOptions } from '../lib/editor'
2
+ import { CodeblockFS, type Fs } from '@joinezco/codeblock'
3
+
4
+ /**
5
+ * Test utilities for markdown editor testing
6
+ */
7
+
8
+ /**
9
+ * Creates a test container element in the DOM
10
+ */
11
+ export function createTestContainer(id = 'test-editor'): HTMLElement {
12
+ const container = document.createElement('div')
13
+ container.id = id
14
+ container.style.width = '800px'
15
+ container.style.height = '600px'
16
+ document.body.appendChild(container)
17
+ return container
18
+ }
19
+
20
+ /**
21
+ * Removes a test container from the DOM
22
+ */
23
+ export function removeTestContainer(container: HTMLElement): void {
24
+ if (container.parentNode) {
25
+ container.parentNode.removeChild(container)
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Creates a mock filesystem for testing
31
+ */
32
+ export async function createMockFS(): Promise<Fs | null> {
33
+ try {
34
+ // Create a minimal filesystem for testing
35
+ const fs = await CodeblockFS.worker()
36
+ return fs
37
+ } catch (error) {
38
+ console.warn('Failed to create filesystem for testing:', error)
39
+ return null
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Creates a test markdown editor instance
45
+ */
46
+ export async function createTestEditor(
47
+ container: HTMLElement,
48
+ options: Partial<MarkdownEditorOptions> = {}
49
+ ): Promise<MarkdownEditor> {
50
+ try {
51
+ const fs = await createMockFS()
52
+
53
+ const defaultOptions: MarkdownEditorOptions = {
54
+ element: container,
55
+ content: '# Test Document\n\nThis is a test document.',
56
+ // Only include fs if it was successfully created
57
+ ...(fs ? {
58
+ fs: {
59
+ fs: fs,
60
+ filepath: 'test.md',
61
+ autoSave: false,
62
+ }
63
+ } : {}),
64
+ ...options,
65
+ }
66
+
67
+ const editor = createEditor(defaultOptions)
68
+
69
+ // Give the editor a moment to initialize
70
+ await new Promise(resolve => setTimeout(resolve, 100))
71
+
72
+ return editor
73
+ } catch (error) {
74
+ console.error('Failed to create test editor:', error)
75
+ throw error
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Waits for the editor to be ready and rendered
81
+ */
82
+ export function waitForEditor(editor: MarkdownEditor, timeout = 5000): Promise<void> {
83
+ return new Promise((resolve, reject) => {
84
+ const startTime = Date.now()
85
+
86
+ const checkReady = () => {
87
+ if (editor.view && editor.view.dom && editor.view.dom.isConnected) {
88
+ resolve()
89
+ } else if (Date.now() - startTime > timeout) {
90
+ reject(new Error('Editor did not become ready within timeout'))
91
+ } else {
92
+ setTimeout(checkReady, 100)
93
+ }
94
+ }
95
+
96
+ checkReady()
97
+ })
98
+ }
99
+
100
+ /**
101
+ * Gets the current markdown content from the editor
102
+ */
103
+ export function getMarkdownContent(editor: MarkdownEditor): string {
104
+ return editor.storage.markdown.getMarkdown()
105
+ }
106
+
107
+ /**
108
+ * Sets markdown content in the editor
109
+ */
110
+ export function setMarkdownContent(editor: MarkdownEditor, content: string): void {
111
+ editor.commands.setContent(content)
112
+ }
113
+
114
+ /**
115
+ * Simulates typing text in the editor
116
+ */
117
+ export function typeText(editor: MarkdownEditor, text: string): void {
118
+ // Use insertContentAt with the current selection position to ensure
119
+ // text is inserted exactly where the cursor is
120
+ const { from } = editor.state.selection
121
+ console.log('editor state selection', { from })
122
+ editor.commands.insertContentAt(from, text)
123
+ }
124
+
125
+ /**
126
+ * Simulates key press in the editor
127
+ */
128
+ export function pressKey(editor: MarkdownEditor, key: string, modifiers: { ctrl?: boolean, shift?: boolean, alt?: boolean } = {}): void {
129
+ const event = new KeyboardEvent('keydown', {
130
+ key,
131
+ ctrlKey: modifiers.ctrl || false,
132
+ shiftKey: modifiers.shift || false,
133
+ altKey: modifiers.alt || false,
134
+ bubbles: true,
135
+ })
136
+
137
+ editor.view.dom.dispatchEvent(event)
138
+ }
139
+
140
+ /**
141
+ * Gets the HTML content of the editor
142
+ */
143
+ export function getHTMLContent(editor: MarkdownEditor): string {
144
+ return editor.getHTML()
145
+ }
146
+
147
+ /**
148
+ * Checks if the editor has focus
149
+ */
150
+ export function isEditorFocused(editor: MarkdownEditor): boolean {
151
+ return editor.isFocused
152
+ }
153
+
154
+ /**
155
+ * Focuses the editor
156
+ */
157
+ export function focusEditor(editor: MarkdownEditor): void {
158
+ editor.commands.focus()
159
+ }
160
+
161
+ /**
162
+ * Gets the current selection in the editor
163
+ */
164
+ export function getSelection(editor: MarkdownEditor) {
165
+ return editor.state.selection
166
+ }
167
+
168
+ /**
169
+ * Sets the selection in the editor
170
+ */
171
+ export function setSelection(editor: MarkdownEditor, from: number, to?: number): void {
172
+ to ? editor.commands.setTextSelection({ from, to }) : editor.commands.setTextSelection(from)
173
+ }
174
+
175
+ /**
176
+ * Waits for a specific condition to be true
177
+ */
178
+ export function waitFor(
179
+ condition: () => boolean,
180
+ timeout = 5000,
181
+ interval = 100
182
+ ): Promise<void> {
183
+ return new Promise((resolve, reject) => {
184
+ const startTime = Date.now()
185
+
186
+ const check = () => {
187
+ if (condition()) {
188
+ resolve()
189
+ } else if (Date.now() - startTime > timeout) {
190
+ reject(new Error('Condition not met within timeout'))
191
+ } else {
192
+ setTimeout(check, interval)
193
+ }
194
+ }
195
+
196
+ check()
197
+ })
198
+ }
199
+
200
+ /**
201
+ * Cleanup function to destroy editor and remove container
202
+ */
203
+ export function cleanupEditor(editor: MarkdownEditor | undefined, container: HTMLElement): void {
204
+ if (editor && typeof editor.destroy === 'function') {
205
+ try {
206
+ editor.destroy()
207
+ } catch (error) {
208
+ console.warn('Error destroying editor:', error)
209
+ }
210
+ }
211
+ removeTestContainer(container)
212
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />