@joinezco/markdown-editor 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +9 -0
- package/TEST_README.md +359 -0
- package/dist/editor/commands/index.d.ts +2 -0
- package/dist/editor/commands/index.js +96 -0
- package/dist/editor/extensions/codeblock.d.ts +30 -0
- package/dist/editor/extensions/codeblock.js +390 -0
- package/dist/editor/extensions/filesystem.d.ts +8 -0
- package/dist/editor/extensions/filesystem.js +54 -0
- package/dist/editor/extensions/link.d.ts +1 -0
- package/dist/editor/extensions/link.js +24 -0
- package/dist/editor/extensions/slash-commands.d.ts +17 -0
- package/dist/editor/extensions/slash-commands.js +324 -0
- package/dist/editor/extensions/taskitem.d.ts +8 -0
- package/dist/editor/extensions/taskitem.js +67 -0
- package/dist/editor/index.d.ts +19 -0
- package/dist/editor/index.js +68 -0
- package/dist/editor/styles.d.ts +2 -0
- package/dist/editor/styles.js +153 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/index.html +16 -0
- package/package.json +73 -0
- package/public/fonts/Source_Serif_4/OFL.txt +91 -0
- package/public/fonts/Source_Serif_4/README.txt +128 -0
- package/public/fonts/Source_Serif_4/SourceSerif4-Italic-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Source_Serif_4/SourceSerif4-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-Black.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-BlackItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-Bold.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-BoldItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-ExtraBold.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-ExtraBoldItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-ExtraLight.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-ExtraLightItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-Italic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-Light.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-LightItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-Medium.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-MediumItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-Regular.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-SemiBold.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4-SemiBoldItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-Black.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-BlackItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-Bold.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-BoldItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-ExtraBold.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-ExtraBoldItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-ExtraLight.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-ExtraLightItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-Italic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-Light.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-LightItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-Medium.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-MediumItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-Regular.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-SemiBold.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_18pt-SemiBoldItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-Black.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-BlackItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-Bold.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-BoldItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-ExtraBold.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-ExtraBoldItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-ExtraLight.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-ExtraLightItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-Italic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-Light.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-LightItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-Medium.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-MediumItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-Regular.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-SemiBold.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_36pt-SemiBoldItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-Black.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-BlackItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-Bold.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-BoldItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-ExtraBold.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-ExtraBoldItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-ExtraLight.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-ExtraLightItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-Italic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-Light.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-LightItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-Medium.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-MediumItalic.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-Regular.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-SemiBold.ttf +0 -0
- package/public/fonts/Source_Serif_4/static/SourceSerif4_48pt-SemiBoldItalic.ttf +0 -0
- package/public/snapshot.bin +0 -0
- package/src/App.css +236 -0
- package/src/App.tsx +73 -0
- package/src/index.css +63 -0
- package/src/lib/editor/commands/index.ts +110 -0
- package/src/lib/editor/extensions/__screenshots__/bullet-to-task.test.ts/BulletToTaskConverter-should-convert-bullet-list-item-to-checked-task-item-when--x--is-typed-1.png +0 -0
- package/src/lib/editor/extensions/__screenshots__/bullet-to-task.test.ts/BulletToTaskConverter-should-convert-bullet-list-item-to-task-item-when-----is-typed-1.png +0 -0
- package/src/lib/editor/extensions/bullet-to-task.test.ts +38 -0
- package/src/lib/editor/extensions/codeblock.ts +447 -0
- package/src/lib/editor/extensions/filesystem.ts +68 -0
- package/src/lib/editor/extensions/link.ts +31 -0
- package/src/lib/editor/extensions/slash-commands.ts +383 -0
- package/src/lib/editor/extensions/taskitem.ts +86 -0
- package/src/lib/editor/index.ts +82 -0
- package/src/lib/editor/styles.ts +166 -0
- package/src/lib/index.ts +3 -0
- package/src/lib/types/css.d.ts +4 -0
- package/src/main.tsx +10 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Basic-Editor-Functionality-should-be-focusable-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Basic-Editor-Functionality-should-create-an-editor-instance-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Basic-Editor-Functionality-should-have-initial-content-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Basic-Editor-Functionality-should-render-in-the-DOM-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Browser-specific-Features-should-handle-copy-and-paste-operations-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Browser-specific-Features-should-handle-undo-and-redo-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Editor-State-and-Updates-should-maintain-state-across-content-changes-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Editor-State-and-Updates-should-trigger-update-callbacks-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Error-Handling-should-handle-invalid-markdown-gracefully-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Error-Handling-should-handle-very-long-content-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Keyboard-Shortcuts-should-handle-Ctrl-B-for-bold-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Keyboard-Shortcuts-should-handle-Ctrl-I-for-italic-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Content-Management-should-get-markdown-content-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Content-Management-should-handle-empty-content-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Content-Management-should-set-markdown-content-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-bold-text-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-code-blocks-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-headings-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-inline-code-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-italic-text-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-lists-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Markdown-Formatting-should-handle-task-lists-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Selection-and-Cursor-Management-should-handle-cursor-positioning-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Selection-and-Cursor-Management-should-set-and-get-selection-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Text-Input-and-Editing-should-handle-line-breaks-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Text-Input-and-Editing-should-handle-typing-at-different-positions-1.png +0 -0
- package/src/test/__screenshots__/editor.test.ts/MarkdownEditor-Text-Input-and-Editing-should-insert-text-at-cursor-position-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Code-Block-Extension-should-handle-code-blocks-without-language-specification-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Code-Block-Extension-should-handle-different-programming-languages-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Code-Block-Extension-should-handle-inline-code-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Code-Block-Extension-should-render-code-blocks-with-syntax-highlighting-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Extension-Interactions-should-handle-multiple-extensions-working-together-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Extension-Interactions-should-maintain-editor-state-across-complex-operations-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-File-System-Integration-should-handle-file-references-in-code-blocks-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-File-System-Integration-should-maintain-file-system-state-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Link-Extension-should-auto-detect-URLs-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Link-Extension-should-handle-email-links-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Link-Extension-should-render-links-correctly-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Markdown-Storage-should-provide-markdown-storage-interface-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Markdown-Storage-should-sync-markdown-content-with-storage-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Slash-Commands-Extension-should-handle-heading-commands-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Slash-Commands-Extension-should-trigger-slash-commands-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Table-Extension-should-handle-table-navigation-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Table-Extension-should-render-tables-correctly-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Task-List-Extension-should-handle-nested-task-lists-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Task-List-Extension-should-render-task-lists-correctly-1.png +0 -0
- package/src/test/__screenshots__/extensions.test.ts/MarkdownEditor-Extensions-Task-List-Extension-should-toggle-task-completion-1.png +0 -0
- package/src/test/editor.test.ts +346 -0
- package/src/test/example.ts +92 -0
- package/src/test/extensions.test.ts +333 -0
- package/src/test/setup.ts +55 -0
- package/src/test/utils.ts +212 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.app.json +29 -0
- package/tsconfig.json +7 -0
- package/tsconfig.lib.json +17 -0
- package/tsconfig.node.json +24 -0
- package/vite.config.ts +89 -0
- package/vitest.config.ts +65 -0
|
@@ -0,0 +1,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" />
|