quasar-ui-danx 0.4.99 → 0.5.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 (90) hide show
  1. package/dist/danx.es.js +17884 -12732
  2. package/dist/danx.es.js.map +1 -1
  3. package/dist/danx.umd.js +192 -118
  4. package/dist/danx.umd.js.map +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +11 -2
  7. package/scripts/publish.sh +76 -0
  8. package/src/components/Utility/Code/CodeViewer.vue +31 -14
  9. package/src/components/Utility/Code/CodeViewerCollapsed.vue +2 -0
  10. package/src/components/Utility/Code/CodeViewerFooter.vue +1 -1
  11. package/src/components/Utility/Code/LanguageBadge.vue +278 -5
  12. package/src/components/Utility/Code/MarkdownContent.vue +160 -6
  13. package/src/components/Utility/Code/index.ts +3 -0
  14. package/src/components/Utility/Markdown/ContextMenu.vue +314 -0
  15. package/src/components/Utility/Markdown/HotkeyHelpPopover.vue +259 -0
  16. package/src/components/Utility/Markdown/LineTypeMenu.vue +226 -0
  17. package/src/components/Utility/Markdown/LinkPopover.vue +331 -0
  18. package/src/components/Utility/Markdown/MarkdownEditor.vue +228 -0
  19. package/src/components/Utility/Markdown/MarkdownEditorContent.vue +235 -0
  20. package/src/components/Utility/Markdown/MarkdownEditorFooter.vue +50 -0
  21. package/src/components/Utility/Markdown/TablePopover.vue +420 -0
  22. package/src/components/Utility/Markdown/index.ts +11 -0
  23. package/src/components/Utility/Markdown/types.ts +27 -0
  24. package/src/components/Utility/index.ts +1 -0
  25. package/src/composables/index.ts +1 -0
  26. package/src/composables/markdown/features/useBlockquotes.spec.ts +428 -0
  27. package/src/composables/markdown/features/useBlockquotes.ts +248 -0
  28. package/src/composables/markdown/features/useCodeBlockManager.ts +369 -0
  29. package/src/composables/markdown/features/useCodeBlocks.spec.ts +779 -0
  30. package/src/composables/markdown/features/useCodeBlocks.ts +774 -0
  31. package/src/composables/markdown/features/useContextMenu.ts +444 -0
  32. package/src/composables/markdown/features/useFocusTracking.ts +116 -0
  33. package/src/composables/markdown/features/useHeadings.spec.ts +834 -0
  34. package/src/composables/markdown/features/useHeadings.ts +290 -0
  35. package/src/composables/markdown/features/useInlineFormatting.spec.ts +705 -0
  36. package/src/composables/markdown/features/useInlineFormatting.ts +402 -0
  37. package/src/composables/markdown/features/useLineTypeMenu.ts +285 -0
  38. package/src/composables/markdown/features/useLinks.spec.ts +369 -0
  39. package/src/composables/markdown/features/useLinks.ts +374 -0
  40. package/src/composables/markdown/features/useLists.spec.ts +834 -0
  41. package/src/composables/markdown/features/useLists.ts +747 -0
  42. package/src/composables/markdown/features/usePopoverManager.ts +181 -0
  43. package/src/composables/markdown/features/useTables.spec.ts +1601 -0
  44. package/src/composables/markdown/features/useTables.ts +1107 -0
  45. package/src/composables/markdown/index.ts +16 -0
  46. package/src/composables/markdown/useMarkdownEditor.spec.ts +332 -0
  47. package/src/composables/markdown/useMarkdownEditor.ts +1068 -0
  48. package/src/composables/markdown/useMarkdownHotkeys.spec.ts +791 -0
  49. package/src/composables/markdown/useMarkdownHotkeys.ts +266 -0
  50. package/src/composables/markdown/useMarkdownSelection.ts +219 -0
  51. package/src/composables/markdown/useMarkdownSync.ts +549 -0
  52. package/src/composables/useCodeViewerEditor.spec.ts +655 -0
  53. package/src/composables/useCodeViewerEditor.ts +174 -20
  54. package/src/helpers/formats/index.ts +1 -1
  55. package/src/helpers/formats/markdown/escapeHtml.ts +15 -0
  56. package/src/helpers/formats/markdown/escapeSequences.ts +60 -0
  57. package/src/helpers/formats/markdown/htmlToMarkdown/convertHeadings.ts +41 -0
  58. package/src/helpers/formats/markdown/htmlToMarkdown/index.spec.ts +489 -0
  59. package/src/helpers/formats/markdown/htmlToMarkdown/index.ts +412 -0
  60. package/src/helpers/formats/markdown/index.ts +92 -0
  61. package/src/helpers/formats/markdown/linePatterns.spec.ts +495 -0
  62. package/src/helpers/formats/markdown/linePatterns.ts +172 -0
  63. package/src/helpers/formats/markdown/parseInline.ts +124 -0
  64. package/src/helpers/formats/markdown/render/index.ts +92 -0
  65. package/src/helpers/formats/markdown/render/renderFootnotes.ts +30 -0
  66. package/src/helpers/formats/markdown/render/renderList.ts +69 -0
  67. package/src/helpers/formats/markdown/render/renderTable.ts +38 -0
  68. package/src/helpers/formats/markdown/state.ts +58 -0
  69. package/src/helpers/formats/markdown/tokenize/extractDefinitions.ts +39 -0
  70. package/src/helpers/formats/markdown/tokenize/index.ts +139 -0
  71. package/src/helpers/formats/markdown/tokenize/parseBlockquote.ts +34 -0
  72. package/src/helpers/formats/markdown/tokenize/parseCodeBlock.ts +85 -0
  73. package/src/helpers/formats/markdown/tokenize/parseDefinitionList.ts +88 -0
  74. package/src/helpers/formats/markdown/tokenize/parseHeading.ts +65 -0
  75. package/src/helpers/formats/markdown/tokenize/parseHorizontalRule.ts +22 -0
  76. package/src/helpers/formats/markdown/tokenize/parseList.ts +119 -0
  77. package/src/helpers/formats/markdown/tokenize/parseParagraph.ts +59 -0
  78. package/src/helpers/formats/markdown/tokenize/parseTable.ts +70 -0
  79. package/src/helpers/formats/markdown/tokenize/parseTaskList.ts +47 -0
  80. package/src/helpers/formats/markdown/tokenize/utils.ts +25 -0
  81. package/src/helpers/formats/markdown/types.ts +63 -0
  82. package/src/styles/danx.scss +1 -0
  83. package/src/styles/themes/danx/markdown.scss +96 -0
  84. package/src/test/helpers/editorTestUtils.spec.ts +296 -0
  85. package/src/test/helpers/editorTestUtils.ts +253 -0
  86. package/src/test/helpers/index.ts +1 -0
  87. package/src/test/setup.test.ts +12 -0
  88. package/src/test/setup.ts +12 -0
  89. package/vitest.config.ts +19 -0
  90. package/src/helpers/formats/renderMarkdown.ts +0 -338
@@ -94,6 +94,24 @@
94
94
  list-style-type: decimal;
95
95
  }
96
96
 
97
+ // Task lists
98
+ .task-list {
99
+ list-style: none;
100
+ padding-left: 0;
101
+
102
+ .task-list-item {
103
+ display: flex;
104
+ align-items: flex-start;
105
+ gap: 0.5em;
106
+ margin: 0.25em 0;
107
+
108
+ input[type="checkbox"] {
109
+ margin-top: 0.3em;
110
+ cursor: default;
111
+ }
112
+ }
113
+ }
114
+
97
115
  // Links
98
116
  a {
99
117
  color: #60a5fa; // blue-400
@@ -121,6 +139,32 @@
121
139
  strong { font-weight: 600; }
122
140
  em { font-style: italic; }
123
141
 
142
+ // Strikethrough
143
+ del {
144
+ text-decoration: line-through;
145
+ opacity: 0.7;
146
+ }
147
+
148
+ // Highlight
149
+ mark {
150
+ background: rgba(250, 204, 21, 0.4); // yellow-400 with opacity
151
+ padding: 0.1em 0.2em;
152
+ border-radius: 2px;
153
+ }
154
+
155
+ // Subscript/Superscript
156
+ sub, sup {
157
+ font-size: 0.75em;
158
+ }
159
+
160
+ sub {
161
+ vertical-align: sub;
162
+ }
163
+
164
+ sup {
165
+ vertical-align: super;
166
+ }
167
+
124
168
  // Tables
125
169
  table {
126
170
  border-collapse: collapse;
@@ -142,4 +186,56 @@
142
186
  background: rgba(255, 255, 255, 0.05);
143
187
  }
144
188
  }
189
+
190
+ // Definition lists
191
+ dl {
192
+ margin: 1em 0;
193
+ }
194
+
195
+ dt {
196
+ font-weight: 600;
197
+ margin-top: 1em;
198
+
199
+ &:first-child {
200
+ margin-top: 0;
201
+ }
202
+ }
203
+
204
+ dd {
205
+ margin-left: 2em;
206
+ margin-top: 0.25em;
207
+ }
208
+
209
+ // Footnotes
210
+ .footnote-ref {
211
+ font-size: 0.75em;
212
+ vertical-align: super;
213
+ line-height: 0;
214
+
215
+ a {
216
+ text-decoration: none;
217
+ }
218
+ }
219
+
220
+ .footnotes {
221
+ margin-top: 2em;
222
+ font-size: 0.875em;
223
+
224
+ hr {
225
+ margin-bottom: 1em;
226
+ }
227
+
228
+ .footnote-list {
229
+ padding-left: 1.5em;
230
+ }
231
+
232
+ .footnote-item {
233
+ margin: 0.5em 0;
234
+ }
235
+
236
+ .footnote-backref {
237
+ margin-left: 0.5em;
238
+ text-decoration: none;
239
+ }
240
+ }
145
241
  }
@@ -0,0 +1,296 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { createTestEditor, TestEditorResult } from './editorTestUtils';
3
+
4
+ describe('editorTestUtils', () => {
5
+ let editor: TestEditorResult;
6
+
7
+ afterEach(() => {
8
+ if (editor) {
9
+ editor.destroy();
10
+ }
11
+ });
12
+
13
+ describe('createTestEditor', () => {
14
+ it('should create a contenteditable container', () => {
15
+ editor = createTestEditor('<p>Hello World</p>');
16
+
17
+ expect(editor.container).toBeInstanceOf(HTMLElement);
18
+ expect(editor.container.getAttribute('contenteditable')).toBe('true');
19
+ expect(editor.container.innerHTML).toBe('<p>Hello World</p>');
20
+ });
21
+
22
+ it('should trim whitespace from initial HTML', () => {
23
+ editor = createTestEditor(' <p>Test</p> ');
24
+
25
+ expect(editor.container.innerHTML).toBe('<p>Test</p>');
26
+ });
27
+ });
28
+
29
+ describe('getHtml', () => {
30
+ it('should return current HTML content', () => {
31
+ editor = createTestEditor('<p>Initial</p>');
32
+
33
+ expect(editor.getHtml()).toBe('<p>Initial</p>');
34
+
35
+ // Modify content
36
+ editor.container.innerHTML = '<p>Modified</p>';
37
+ expect(editor.getHtml()).toBe('<p>Modified</p>');
38
+ });
39
+ });
40
+
41
+ describe('getMarkdown', () => {
42
+ it('should convert HTML to markdown', () => {
43
+ editor = createTestEditor('<p>Hello <strong>bold</strong> text</p>');
44
+
45
+ const markdown = editor.getMarkdown();
46
+ expect(markdown).toBe('Hello **bold** text');
47
+ });
48
+
49
+ it('should handle headings', () => {
50
+ editor = createTestEditor('<h2>Heading Two</h2>');
51
+
52
+ expect(editor.getMarkdown()).toBe('## Heading Two');
53
+ });
54
+
55
+ it('should handle lists', () => {
56
+ editor = createTestEditor('<ul><li>Item 1</li><li>Item 2</li></ul>');
57
+
58
+ const markdown = editor.getMarkdown();
59
+ expect(markdown).toContain('- Item 1');
60
+ expect(markdown).toContain('- Item 2');
61
+ });
62
+ });
63
+
64
+ describe('getBlock and getBlocks', () => {
65
+ beforeEach(() => {
66
+ editor = createTestEditor('<p>First</p><p>Second</p><p>Third</p>');
67
+ });
68
+
69
+ it('should get a block by index', () => {
70
+ const block0 = editor.getBlock(0);
71
+ const block1 = editor.getBlock(1);
72
+ const block2 = editor.getBlock(2);
73
+
74
+ expect(block0?.textContent).toBe('First');
75
+ expect(block1?.textContent).toBe('Second');
76
+ expect(block2?.textContent).toBe('Third');
77
+ });
78
+
79
+ it('should return null for invalid index', () => {
80
+ expect(editor.getBlock(10)).toBeNull();
81
+ expect(editor.getBlock(-1)).toBeNull();
82
+ });
83
+
84
+ it('should get all blocks', () => {
85
+ const blocks = editor.getBlocks();
86
+
87
+ expect(blocks).toHaveLength(3);
88
+ expect(blocks[0].textContent).toBe('First');
89
+ expect(blocks[2].textContent).toBe('Third');
90
+ });
91
+ });
92
+
93
+ describe('setCursor and getCursorPosition', () => {
94
+ beforeEach(() => {
95
+ editor = createTestEditor('<p>Hello World</p>');
96
+ });
97
+
98
+ it('should set cursor position in a text node', () => {
99
+ const textNode = editor.getBlock(0)?.firstChild;
100
+ if (!textNode) throw new Error('Text node not found');
101
+
102
+ editor.setCursor(textNode, 5);
103
+
104
+ const pos = editor.getCursorPosition();
105
+ expect(pos.node).toBe(textNode);
106
+ expect(pos.offset).toBe(5);
107
+ });
108
+
109
+ it('should return null node when no selection', () => {
110
+ // Clear any existing selection
111
+ window.getSelection()?.removeAllRanges();
112
+
113
+ const pos = editor.getCursorPosition();
114
+ expect(pos.node).toBeNull();
115
+ expect(pos.offset).toBe(0);
116
+ });
117
+ });
118
+
119
+ describe('setCursorInBlock and getCursorOffsetInBlock', () => {
120
+ beforeEach(() => {
121
+ editor = createTestEditor('<p>First paragraph</p><p>Second paragraph</p>');
122
+ });
123
+
124
+ it('should set cursor at offset within a block', () => {
125
+ editor.setCursorInBlock(0, 6);
126
+
127
+ const offset = editor.getCursorOffsetInBlock(0);
128
+ expect(offset).toBe(6); // After "First "
129
+ });
130
+
131
+ it('should set cursor in second block', () => {
132
+ editor.setCursorInBlock(1, 7);
133
+
134
+ const offset = editor.getCursorOffsetInBlock(1);
135
+ expect(offset).toBe(7); // After "Second "
136
+ });
137
+
138
+ it('should return -1 for invalid block index', () => {
139
+ editor.setCursorInBlock(0, 0);
140
+
141
+ expect(editor.getCursorOffsetInBlock(10)).toBe(-1);
142
+ });
143
+
144
+ it('should return -1 when cursor is not in the specified block', () => {
145
+ editor.setCursorInBlock(0, 0);
146
+
147
+ expect(editor.getCursorOffsetInBlock(1)).toBe(-1);
148
+ });
149
+ });
150
+
151
+ describe('selectRange and selectInBlock', () => {
152
+ beforeEach(() => {
153
+ editor = createTestEditor('<p>Hello World</p>');
154
+ });
155
+
156
+ it('should select a text range', () => {
157
+ const textNode = editor.getBlock(0)?.firstChild;
158
+ if (!textNode) throw new Error('Text node not found');
159
+
160
+ editor.selectRange(textNode, 0, textNode, 5);
161
+
162
+ const sel = window.getSelection();
163
+ expect(sel?.toString()).toBe('Hello');
164
+ });
165
+
166
+ it('should select text within a block by offsets', () => {
167
+ editor.selectInBlock(0, 6, 11);
168
+
169
+ const sel = window.getSelection();
170
+ expect(sel?.toString()).toBe('World');
171
+ });
172
+ });
173
+
174
+ describe('pressKey', () => {
175
+ beforeEach(() => {
176
+ editor = createTestEditor('<p>Test</p>');
177
+ });
178
+
179
+ it('should dispatch keydown event', () => {
180
+ let receivedEvent: KeyboardEvent | null = null;
181
+ editor.container.addEventListener('keydown', (e) => {
182
+ receivedEvent = e;
183
+ });
184
+
185
+ editor.pressKey('a');
186
+
187
+ expect(receivedEvent).not.toBeNull();
188
+ expect(receivedEvent!.key).toBe('a');
189
+ expect(receivedEvent!.code).toBe('KeyA');
190
+ });
191
+
192
+ it('should include modifier keys', () => {
193
+ let receivedEvent: KeyboardEvent | null = null;
194
+ editor.container.addEventListener('keydown', (e) => {
195
+ receivedEvent = e;
196
+ });
197
+
198
+ editor.pressKey('b', { ctrl: true, shift: true });
199
+
200
+ expect(receivedEvent!.ctrlKey).toBe(true);
201
+ expect(receivedEvent!.shiftKey).toBe(true);
202
+ expect(receivedEvent!.altKey).toBe(false);
203
+ expect(receivedEvent!.metaKey).toBe(false);
204
+ });
205
+
206
+ it('should handle special keys', () => {
207
+ let receivedEvent: KeyboardEvent | null = null;
208
+ editor.container.addEventListener('keydown', (e) => {
209
+ receivedEvent = e;
210
+ });
211
+
212
+ editor.pressKey('Enter');
213
+
214
+ expect(receivedEvent!.key).toBe('Enter');
215
+ expect(receivedEvent!.code).toBe('Enter');
216
+ });
217
+ });
218
+
219
+ describe('type', () => {
220
+ beforeEach(() => {
221
+ editor = createTestEditor('<p>Hello</p>');
222
+ });
223
+
224
+ it('should insert text at cursor position', () => {
225
+ const textNode = editor.getBlock(0)?.firstChild;
226
+ if (!textNode) throw new Error('Text node not found');
227
+
228
+ editor.setCursor(textNode, 5);
229
+ editor.type(' World');
230
+
231
+ expect(editor.container.textContent).toContain('Hello');
232
+ expect(editor.container.textContent).toContain('World');
233
+ });
234
+
235
+ it('should dispatch input event', () => {
236
+ let inputFired = false;
237
+ editor.container.addEventListener('input', () => {
238
+ inputFired = true;
239
+ });
240
+
241
+ const textNode = editor.getBlock(0)?.firstChild;
242
+ if (!textNode) throw new Error('Text node not found');
243
+
244
+ editor.setCursor(textNode, 0);
245
+ editor.type('X');
246
+
247
+ expect(inputFired).toBe(true);
248
+ });
249
+ });
250
+
251
+ describe('contentRef', () => {
252
+ it('should provide a Vue ref to the container', () => {
253
+ editor = createTestEditor('<p>Test</p>');
254
+
255
+ expect(editor.contentRef.value).toBe(editor.container);
256
+ });
257
+ });
258
+
259
+ describe('destroy', () => {
260
+ it('should remove the container from DOM', () => {
261
+ editor = createTestEditor('<p>Test</p>');
262
+ const container = editor.container;
263
+
264
+ expect(document.body.contains(container)).toBe(true);
265
+
266
+ editor.destroy();
267
+
268
+ expect(document.body.contains(container)).toBe(false);
269
+ });
270
+ });
271
+
272
+ describe('complex content handling', () => {
273
+ it('should handle nested inline formatting', () => {
274
+ editor = createTestEditor('<p>This is <strong><em>bold italic</em></strong> text</p>');
275
+
276
+ editor.selectInBlock(0, 8, 19);
277
+
278
+ const sel = window.getSelection();
279
+ expect(sel?.toString()).toBe('bold italic');
280
+ });
281
+
282
+ it('should handle multiple blocks with cursor operations', () => {
283
+ editor = createTestEditor('<p>Line 1</p><p>Line 2</p><p>Line 3</p>');
284
+
285
+ // Set cursor in middle block
286
+ editor.setCursorInBlock(1, 3);
287
+
288
+ const offset = editor.getCursorOffsetInBlock(1);
289
+ expect(offset).toBe(3);
290
+
291
+ // Verify cursor is not in other blocks
292
+ expect(editor.getCursorOffsetInBlock(0)).toBe(-1);
293
+ expect(editor.getCursorOffsetInBlock(2)).toBe(-1);
294
+ });
295
+ });
296
+ });
@@ -0,0 +1,253 @@
1
+ import { ref } from 'vue';
2
+ import { htmlToMarkdown } from '../../helpers/formats/markdown/htmlToMarkdown';
3
+
4
+ export interface TestEditorResult {
5
+ /** The contenteditable container element */
6
+ container: HTMLElement;
7
+ /** Get the current HTML content */
8
+ getHtml: () => string;
9
+ /** Get the markdown output from current HTML */
10
+ getMarkdown: () => string;
11
+ /** Get cursor position as { node, offset } */
12
+ getCursorPosition: () => { node: Node | null; offset: number };
13
+ /** Get cursor offset within a specific block */
14
+ getCursorOffsetInBlock: (blockIndex: number) => number;
15
+ /** Set cursor at a position in a specific text node */
16
+ setCursor: (node: Node, offset: number) => void;
17
+ /** Set cursor at offset within block's text content */
18
+ setCursorInBlock: (blockIndex: number, offset: number) => void;
19
+ /** Select text range */
20
+ selectRange: (startNode: Node, startOffset: number, endNode: Node, endOffset: number) => void;
21
+ /** Select text within a block by offsets */
22
+ selectInBlock: (blockIndex: number, startOffset: number, endOffset: number) => void;
23
+ /** Simulate a keydown event */
24
+ pressKey: (key: string, modifiers?: { ctrl?: boolean; shift?: boolean; alt?: boolean; meta?: boolean }) => void;
25
+ /** Type text at current cursor position */
26
+ type: (text: string) => void;
27
+ /** Get block element by index */
28
+ getBlock: (index: number) => Element | null;
29
+ /** Get all block elements */
30
+ getBlocks: () => Element[];
31
+ /** Get the contentRef as a Vue ref (for composables) */
32
+ contentRef: ReturnType<typeof ref<HTMLElement | null>>;
33
+ /** Cleanup function */
34
+ destroy: () => void;
35
+ }
36
+
37
+ /**
38
+ * Find a text node at a given offset within an element's text content.
39
+ * Returns the text node and the offset within that node.
40
+ */
41
+ function findTextNodeAtOffset(element: Element, targetOffset: number): { node: Text; offset: number } | null {
42
+ let currentOffset = 0;
43
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
44
+ let node: Text | null;
45
+ let lastNode: Text | null = null;
46
+
47
+ while ((node = walker.nextNode() as Text)) {
48
+ lastNode = node;
49
+ const nodeLength = node.textContent?.length || 0;
50
+ if (currentOffset + nodeLength >= targetOffset) {
51
+ return { node, offset: targetOffset - currentOffset };
52
+ }
53
+ currentOffset += nodeLength;
54
+ }
55
+
56
+ // If we're past the content, return end of last text node
57
+ if (lastNode) {
58
+ return { node: lastNode, offset: lastNode.textContent?.length || 0 };
59
+ }
60
+
61
+ return null;
62
+ }
63
+
64
+ /**
65
+ * Get the key code string for common keys
66
+ */
67
+ function getKeyCode(key: string): string {
68
+ const keyCodes: Record<string, string> = {
69
+ 'Enter': 'Enter',
70
+ 'Backspace': 'Backspace',
71
+ 'Delete': 'Delete',
72
+ 'Tab': 'Tab',
73
+ 'Escape': 'Escape',
74
+ 'ArrowUp': 'ArrowUp',
75
+ 'ArrowDown': 'ArrowDown',
76
+ 'ArrowLeft': 'ArrowLeft',
77
+ 'ArrowRight': 'ArrowRight',
78
+ ' ': 'Space',
79
+ };
80
+
81
+ if (keyCodes[key]) {
82
+ return keyCodes[key];
83
+ }
84
+
85
+ // For single letters, use KeyX format
86
+ if (key.length === 1 && /[a-zA-Z]/.test(key)) {
87
+ return `Key${key.toUpperCase()}`;
88
+ }
89
+
90
+ // For digits
91
+ if (key.length === 1 && /[0-9]/.test(key)) {
92
+ return `Digit${key}`;
93
+ }
94
+
95
+ return key;
96
+ }
97
+
98
+ /**
99
+ * Create a test editor with initial HTML content
100
+ */
101
+ export function createTestEditor(initialHtml: string): TestEditorResult {
102
+ // Create container
103
+ const container = document.createElement('div');
104
+ container.setAttribute('contenteditable', 'true');
105
+ container.innerHTML = initialHtml.trim();
106
+ document.body.appendChild(container);
107
+
108
+ // Create Vue ref for composables
109
+ const contentRef = ref<HTMLElement | null>(container);
110
+
111
+ function getHtml(): string {
112
+ return container.innerHTML;
113
+ }
114
+
115
+ function getMarkdown(): string {
116
+ return htmlToMarkdown(container);
117
+ }
118
+
119
+ function getCursorPosition(): { node: Node | null; offset: number } {
120
+ const sel = window.getSelection();
121
+ if (!sel || sel.rangeCount === 0) {
122
+ return { node: null, offset: 0 };
123
+ }
124
+ const range = sel.getRangeAt(0);
125
+ return { node: range.startContainer, offset: range.startOffset };
126
+ }
127
+
128
+ function getBlock(index: number): Element | null {
129
+ const blocks = Array.from(container.children);
130
+ return blocks[index] || null;
131
+ }
132
+
133
+ function getBlocks(): Element[] {
134
+ return Array.from(container.children);
135
+ }
136
+
137
+ function getCursorOffsetInBlock(blockIndex: number): number {
138
+ const block = getBlock(blockIndex);
139
+ if (!block) return -1;
140
+
141
+ const sel = window.getSelection();
142
+ if (!sel || sel.rangeCount === 0) return -1;
143
+
144
+ const range = sel.getRangeAt(0);
145
+ if (!block.contains(range.startContainer)) return -1;
146
+
147
+ // Calculate offset within block's text content
148
+ const preRange = document.createRange();
149
+ preRange.selectNodeContents(block);
150
+ preRange.setEnd(range.startContainer, range.startOffset);
151
+ return preRange.toString().length;
152
+ }
153
+
154
+ function setCursor(node: Node, offset: number): void {
155
+ const range = document.createRange();
156
+ range.setStart(node, offset);
157
+ range.collapse(true);
158
+
159
+ const sel = window.getSelection();
160
+ sel?.removeAllRanges();
161
+ sel?.addRange(range);
162
+ }
163
+
164
+ function setCursorInBlock(blockIndex: number, offset: number): void {
165
+ const block = getBlock(blockIndex);
166
+ if (!block) return;
167
+
168
+ const result = findTextNodeAtOffset(block, offset);
169
+ if (result) {
170
+ setCursor(result.node, result.offset);
171
+ }
172
+ }
173
+
174
+ function selectRange(startNode: Node, startOffset: number, endNode: Node, endOffset: number): void {
175
+ const range = document.createRange();
176
+ range.setStart(startNode, startOffset);
177
+ range.setEnd(endNode, endOffset);
178
+
179
+ const sel = window.getSelection();
180
+ sel?.removeAllRanges();
181
+ sel?.addRange(range);
182
+ }
183
+
184
+ function selectInBlock(blockIndex: number, startOffset: number, endOffset: number): void {
185
+ const block = getBlock(blockIndex);
186
+ if (!block) return;
187
+
188
+ const start = findTextNodeAtOffset(block, startOffset);
189
+ const end = findTextNodeAtOffset(block, endOffset);
190
+
191
+ if (start && end) {
192
+ selectRange(start.node, start.offset, end.node, end.offset);
193
+ }
194
+ }
195
+
196
+ function pressKey(key: string, modifiers: { ctrl?: boolean; shift?: boolean; alt?: boolean; meta?: boolean } = {}): void {
197
+ const event = new KeyboardEvent('keydown', {
198
+ key,
199
+ code: getKeyCode(key),
200
+ ctrlKey: modifiers.ctrl || false,
201
+ shiftKey: modifiers.shift || false,
202
+ altKey: modifiers.alt || false,
203
+ metaKey: modifiers.meta || false,
204
+ bubbles: true,
205
+ cancelable: true,
206
+ });
207
+
208
+ container.dispatchEvent(event);
209
+ }
210
+
211
+ function type(text: string): void {
212
+ // Insert text at cursor position
213
+ const sel = window.getSelection();
214
+ if (!sel || sel.rangeCount === 0) return;
215
+
216
+ const range = sel.getRangeAt(0);
217
+ range.deleteContents();
218
+
219
+ const textNode = document.createTextNode(text);
220
+ range.insertNode(textNode);
221
+
222
+ // Move cursor after inserted text
223
+ range.setStartAfter(textNode);
224
+ range.collapse(true);
225
+ sel.removeAllRanges();
226
+ sel.addRange(range);
227
+
228
+ // Dispatch input event
229
+ container.dispatchEvent(new InputEvent('input', { bubbles: true }));
230
+ }
231
+
232
+ function destroy(): void {
233
+ container.remove();
234
+ }
235
+
236
+ return {
237
+ container,
238
+ getHtml,
239
+ getMarkdown,
240
+ getCursorPosition,
241
+ getCursorOffsetInBlock,
242
+ setCursor,
243
+ setCursorInBlock,
244
+ selectRange,
245
+ selectInBlock,
246
+ pressKey,
247
+ type,
248
+ getBlock,
249
+ getBlocks,
250
+ contentRef,
251
+ destroy,
252
+ };
253
+ }
@@ -0,0 +1 @@
1
+ export * from './editorTestUtils';
@@ -0,0 +1,12 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ describe('Vitest Setup', () => {
4
+ it('should run tests successfully', () => {
5
+ expect(true).toBe(true);
6
+ });
7
+
8
+ it('should have access to jsdom environment', () => {
9
+ expect(typeof window).toBe('object');
10
+ expect(typeof document).toBe('object');
11
+ });
12
+ });
@@ -0,0 +1,12 @@
1
+ import { vi } from 'vitest';
2
+
3
+ // Mock window.getSelection for jsdom
4
+ // jsdom's Selection API is limited, so we may need to enhance it
5
+ if (typeof window !== 'undefined') {
6
+ // Ensure getSelection exists
7
+ if (!window.getSelection) {
8
+ window.getSelection = vi.fn(() => null);
9
+ }
10
+ }
11
+
12
+ // Global test utilities can be added here
@@ -0,0 +1,19 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { defineConfig } from 'vitest/config';
3
+ import vue from '@vitejs/plugin-vue';
4
+
5
+ export default defineConfig({
6
+ plugins: [vue()],
7
+ test: {
8
+ environment: 'jsdom',
9
+ globals: true,
10
+ include: ['src/**/*.{test,spec}.{js,ts}'],
11
+ setupFiles: ['./src/test/setup.ts'],
12
+ root: fileURLToPath(new URL('./', import.meta.url)),
13
+ },
14
+ resolve: {
15
+ alias: {
16
+ '@': fileURLToPath(new URL('./src', import.meta.url)),
17
+ },
18
+ },
19
+ });