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
@@ -0,0 +1,655 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { ref, nextTick } from 'vue';
3
+ import { useCodeViewerEditor, UseCodeViewerEditorOptions } from './useCodeViewerEditor';
4
+ import { useCodeFormat } from './useCodeFormat';
5
+ import { CodeFormat } from './useCodeFormat';
6
+
7
+ describe('useCodeViewerEditor', () => {
8
+ let container: HTMLPreElement;
9
+ let codeRef: ReturnType<typeof ref<HTMLPreElement | null>>;
10
+ let onEmitModelValue: ReturnType<typeof vi.fn>;
11
+ let onEmitEditable: ReturnType<typeof vi.fn>;
12
+ let onExit: ReturnType<typeof vi.fn>;
13
+ let onDelete: ReturnType<typeof vi.fn>;
14
+ let execCommandMock: ReturnType<typeof vi.fn>;
15
+
16
+ beforeEach(() => {
17
+ // Create a contenteditable pre element
18
+ container = document.createElement('pre');
19
+ container.setAttribute('contenteditable', 'true');
20
+ document.body.appendChild(container);
21
+ codeRef = ref<HTMLPreElement | null>(container);
22
+
23
+ // Create mock callbacks
24
+ onEmitModelValue = vi.fn();
25
+ onEmitEditable = vi.fn();
26
+ onExit = vi.fn();
27
+ onDelete = vi.fn();
28
+
29
+ // Mock document.execCommand for Tab key tests (not available in jsdom)
30
+ execCommandMock = vi.fn(() => true);
31
+ (document as any).execCommand = execCommandMock;
32
+ });
33
+
34
+ afterEach(() => {
35
+ container.remove();
36
+ vi.restoreAllMocks();
37
+ delete (document as any).execCommand;
38
+ });
39
+
40
+ /**
41
+ * Helper to create the editor with options
42
+ */
43
+ function createEditor(
44
+ initialValue: object | string | null,
45
+ format: CodeFormat = 'yaml',
46
+ editable: boolean = true
47
+ ) {
48
+ const currentFormat = ref<CodeFormat>(format);
49
+ const canEdit = ref(true);
50
+ const editableRef = ref(editable);
51
+ const codeFormat = useCodeFormat(ref(initialValue), currentFormat);
52
+
53
+ const options: UseCodeViewerEditorOptions = {
54
+ codeRef,
55
+ codeFormat,
56
+ currentFormat,
57
+ canEdit,
58
+ editable: editableRef,
59
+ onEmitModelValue,
60
+ onEmitEditable,
61
+ onExit,
62
+ onDelete
63
+ };
64
+
65
+ return {
66
+ ...useCodeViewerEditor(options),
67
+ codeFormat,
68
+ currentFormat,
69
+ canEdit,
70
+ editableRef
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Helper to set cursor at a specific offset in the pre element's text content
76
+ */
77
+ function setCursorAtOffset(element: HTMLPreElement, targetOffset: number): void {
78
+ let currentOffset = 0;
79
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
80
+ let node: Text | null;
81
+
82
+ while ((node = walker.nextNode() as Text)) {
83
+ const nodeLength = node.textContent?.length || 0;
84
+ if (currentOffset + nodeLength >= targetOffset) {
85
+ const range = document.createRange();
86
+ range.setStart(node, targetOffset - currentOffset);
87
+ range.collapse(true);
88
+ const sel = window.getSelection();
89
+ sel?.removeAllRanges();
90
+ sel?.addRange(range);
91
+ return;
92
+ }
93
+ currentOffset += nodeLength;
94
+ }
95
+
96
+ // If past content, place at end
97
+ const range = document.createRange();
98
+ range.selectNodeContents(element);
99
+ range.collapse(false);
100
+ const sel = window.getSelection();
101
+ sel?.removeAllRanges();
102
+ sel?.addRange(range);
103
+ }
104
+
105
+ /**
106
+ * Helper to set cursor at the end of the element
107
+ */
108
+ function setCursorAtEnd(element: HTMLPreElement): void {
109
+ const range = document.createRange();
110
+ range.selectNodeContents(element);
111
+ range.collapse(false);
112
+ const sel = window.getSelection();
113
+ sel?.removeAllRanges();
114
+ sel?.addRange(range);
115
+ }
116
+
117
+ /**
118
+ * Helper to dispatch a keydown event
119
+ */
120
+ function pressKey(element: HTMLElement, key: string, modifiers: { ctrl?: boolean; meta?: boolean } = {}): KeyboardEvent {
121
+ const event = new KeyboardEvent('keydown', {
122
+ key,
123
+ code: key === 'Enter' ? 'Enter' : key === 'Tab' ? 'Tab' : key === 'Escape' ? 'Escape' : key === 'Backspace' ? 'Backspace' : `Key${key.toUpperCase()}`,
124
+ ctrlKey: modifiers.ctrl || false,
125
+ metaKey: modifiers.meta || false,
126
+ bubbles: true,
127
+ cancelable: true
128
+ });
129
+ element.dispatchEvent(event);
130
+ return event;
131
+ }
132
+
133
+ describe('Enter key behavior - DOM content usage', () => {
134
+ // This test verifies the critical fix: Enter key should read from DOM, not stale editingContent
135
+ it('should use actual DOM content when editingContent is stale', async () => {
136
+ const initialValue = { name: 'test' };
137
+ const editor = createEditor(initialValue, 'yaml', true);
138
+
139
+ // Enter edit mode
140
+ if (!editor.isEditing.value) {
141
+ editor.toggleEdit();
142
+ await nextTick();
143
+ }
144
+
145
+ // Set up the scenario where editingContent becomes stale
146
+ const originalContent = 'name: test';
147
+ const newContent = 'name: test\nhello: world';
148
+
149
+ // 1. Set the DOM to have new content
150
+ container.innerText = newContent;
151
+
152
+ // 2. But editingContent is stale (simulates debounced highlight resetting it)
153
+ editor.isUserEditing.value = false;
154
+ editor.editingContent.value = originalContent; // Stale!
155
+
156
+ // Verify the stale state
157
+ expect(editor.editingContent.value).toBe(originalContent);
158
+ expect(container.innerText).toBe(newContent);
159
+
160
+ // This is the key assertion: the fix should make Enter key use DOM content
161
+ // We can verify this by checking that the internal logic would read from DOM
162
+ // Since we can't easily test the Enter key in jsdom (selection API limitations),
163
+ // we verify that the state is correctly set up for the fix to work
164
+ expect(container.innerText).not.toBe(editor.editingContent.value);
165
+ expect(container.innerText.length).toBeGreaterThan(editor.editingContent.value.length);
166
+ });
167
+
168
+ it('should have editingContent sync correctly when user is editing', async () => {
169
+ const initialValue = { key: 'value' };
170
+ const editor = createEditor(initialValue, 'yaml', true);
171
+
172
+ if (!editor.isEditing.value) {
173
+ editor.toggleEdit();
174
+ await nextTick();
175
+ }
176
+
177
+ // Simulate user editing - this should keep isUserEditing true
178
+ container.innerText = 'key: newvalue';
179
+ editor.isUserEditing.value = true;
180
+ editor.editingContent.value = 'key: newvalue';
181
+
182
+ // While user is editing, syncEditingContentFromValue should NOT reset content
183
+ editor.syncEditingContentFromValue();
184
+
185
+ // Since isUserEditing is true, it should NOT have reset
186
+ expect(editor.editingContent.value).toBe('key: newvalue');
187
+ });
188
+
189
+ it('should reset editingContent when user is NOT editing', async () => {
190
+ const initialValue = { key: 'value' };
191
+ const editor = createEditor(initialValue, 'yaml', true);
192
+
193
+ if (!editor.isEditing.value) {
194
+ editor.toggleEdit();
195
+ await nextTick();
196
+ }
197
+
198
+ // User has stopped editing
199
+ editor.isUserEditing.value = false;
200
+
201
+ // syncEditingContentFromValue should reset to formatted content
202
+ editor.syncEditingContentFromValue();
203
+
204
+ // Should be reset to the formatted content from codeFormat
205
+ expect(editor.editingContent.value).toBe(editor.codeFormat.formattedContent.value);
206
+ });
207
+ });
208
+
209
+ describe('Tab key behavior', () => {
210
+ it('should call document.execCommand on Tab press', async () => {
211
+ const editor = createEditor({}, 'yaml', true);
212
+
213
+ if (!editor.isEditing.value) {
214
+ editor.toggleEdit();
215
+ await nextTick();
216
+ }
217
+
218
+ container.innerText = 'key:';
219
+ setCursorAtEnd(container);
220
+
221
+ const event = pressKey(container, 'Tab');
222
+ editor.onKeyDown(event);
223
+
224
+ // Tab should be prevented and execCommand called
225
+ expect(event.defaultPrevented).toBe(true);
226
+ expect(execCommandMock).toHaveBeenCalledWith('insertText', false, ' ');
227
+ });
228
+ });
229
+
230
+ describe('Escape key behavior', () => {
231
+ it('should exit edit mode on Escape', async () => {
232
+ const editor = createEditor({ key: 'value' }, 'yaml', true);
233
+
234
+ if (!editor.isEditing.value) {
235
+ editor.toggleEdit();
236
+ await nextTick();
237
+ }
238
+
239
+ expect(editor.isEditing.value).toBe(true);
240
+
241
+ container.innerText = 'key: value';
242
+ setCursorAtEnd(container);
243
+
244
+ const event = pressKey(container, 'Escape');
245
+ editor.onKeyDown(event);
246
+
247
+ expect(editor.isEditing.value).toBe(false);
248
+ });
249
+ });
250
+
251
+ describe('Ctrl+Enter behavior', () => {
252
+ it('should call onExit when Ctrl+Enter is pressed', async () => {
253
+ const editor = createEditor({ key: 'value' }, 'yaml', true);
254
+
255
+ if (!editor.isEditing.value) {
256
+ editor.toggleEdit();
257
+ await nextTick();
258
+ }
259
+
260
+ container.innerText = 'key: value';
261
+ editor.editingContent.value = 'key: value';
262
+ setCursorAtEnd(container);
263
+
264
+ const event = pressKey(container, 'Enter', { ctrl: true });
265
+ editor.onKeyDown(event);
266
+
267
+ expect(onExit).toHaveBeenCalled();
268
+ expect(onEmitModelValue).toHaveBeenCalled();
269
+ });
270
+ });
271
+
272
+ describe('Delete/Backspace on empty content', () => {
273
+ it('should call onDelete when Backspace is pressed on empty content', async () => {
274
+ const editor = createEditor({}, 'yaml', true);
275
+
276
+ if (!editor.isEditing.value) {
277
+ editor.toggleEdit();
278
+ await nextTick();
279
+ }
280
+
281
+ container.innerText = '';
282
+ editor.editingContent.value = '';
283
+
284
+ const event = new KeyboardEvent('keydown', {
285
+ key: 'Backspace',
286
+ code: 'Backspace',
287
+ bubbles: true,
288
+ cancelable: true
289
+ });
290
+ container.dispatchEvent(event);
291
+ editor.onKeyDown(event);
292
+
293
+ expect(onDelete).toHaveBeenCalled();
294
+ });
295
+
296
+ it('should NOT call onDelete when content is not empty', async () => {
297
+ const editor = createEditor({ key: 'value' }, 'yaml', true);
298
+
299
+ if (!editor.isEditing.value) {
300
+ editor.toggleEdit();
301
+ await nextTick();
302
+ }
303
+
304
+ container.innerText = 'key: value';
305
+ editor.editingContent.value = 'key: value';
306
+
307
+ const event = new KeyboardEvent('keydown', {
308
+ key: 'Backspace',
309
+ code: 'Backspace',
310
+ bubbles: true,
311
+ cancelable: true
312
+ });
313
+ container.dispatchEvent(event);
314
+ editor.onKeyDown(event);
315
+
316
+ expect(onDelete).not.toHaveBeenCalled();
317
+ });
318
+ });
319
+
320
+ describe('toggleEdit', () => {
321
+ it('should toggle between edit and view mode', async () => {
322
+ const editor = createEditor({ key: 'value' }, 'yaml', false);
323
+
324
+ expect(editor.isEditing.value).toBe(false);
325
+
326
+ editor.toggleEdit();
327
+ await nextTick();
328
+
329
+ expect(editor.isEditing.value).toBe(true);
330
+ expect(onEmitEditable).toHaveBeenCalledWith(true);
331
+
332
+ editor.toggleEdit();
333
+
334
+ expect(editor.isEditing.value).toBe(false);
335
+ expect(onEmitEditable).toHaveBeenCalledWith(false);
336
+ });
337
+
338
+ it('should clear validation error when exiting edit mode', async () => {
339
+ const editor = createEditor({ key: 'value' }, 'yaml', true);
340
+
341
+ // Enter edit mode
342
+ editor.toggleEdit();
343
+ await nextTick();
344
+
345
+ // Set a validation error
346
+ editor.validationError.value = { message: 'Test error', line: 1, column: 1 };
347
+ expect(editor.hasValidationError.value).toBe(true);
348
+
349
+ // Exit edit mode
350
+ editor.toggleEdit();
351
+
352
+ expect(editor.validationError.value).toBeNull();
353
+ expect(editor.hasValidationError.value).toBe(false);
354
+ });
355
+ });
356
+
357
+ describe('content input handling', () => {
358
+ it('should update editingContent on input event with target', async () => {
359
+ const editor = createEditor({ key: 'value' }, 'yaml', true);
360
+
361
+ if (!editor.isEditing.value) {
362
+ editor.toggleEdit();
363
+ await nextTick();
364
+ }
365
+
366
+ // Create an input event with a proper target
367
+ container.innerText = 'new: content';
368
+ const inputEvent = new InputEvent('input', { bubbles: true });
369
+ Object.defineProperty(inputEvent, 'target', { value: container });
370
+
371
+ editor.onContentEditableInput(inputEvent);
372
+
373
+ expect(editor.editingContent.value).toBe('new: content');
374
+ expect(editor.isUserEditing.value).toBe(true);
375
+ });
376
+ });
377
+
378
+ describe('blur handling', () => {
379
+ it('should emit model value on blur when user was editing', async () => {
380
+ const editor = createEditor({ key: 'value' }, 'yaml', true);
381
+
382
+ if (!editor.isEditing.value) {
383
+ editor.toggleEdit();
384
+ await nextTick();
385
+ }
386
+
387
+ container.innerText = 'updated: value';
388
+ editor.editingContent.value = 'updated: value';
389
+ editor.isUserEditing.value = true;
390
+
391
+ editor.onContentEditableBlur();
392
+
393
+ expect(onEmitModelValue).toHaveBeenCalled();
394
+ expect(editor.isUserEditing.value).toBe(false);
395
+ });
396
+
397
+ it('should NOT emit when user was NOT editing', async () => {
398
+ const editor = createEditor({ key: 'value' }, 'yaml', true);
399
+
400
+ if (!editor.isEditing.value) {
401
+ editor.toggleEdit();
402
+ await nextTick();
403
+ }
404
+
405
+ editor.isUserEditing.value = false;
406
+
407
+ editor.onContentEditableBlur();
408
+
409
+ expect(onEmitModelValue).not.toHaveBeenCalled();
410
+ });
411
+ });
412
+
413
+ describe('computed properties', () => {
414
+ it('should compute isValid correctly', async () => {
415
+ const editor = createEditor({ key: 'value' }, 'yaml', true);
416
+
417
+ // No validation error, codeFormat is valid
418
+ expect(editor.isValid.value).toBe(true);
419
+
420
+ // Set validation error
421
+ editor.validationError.value = { message: 'Error', line: 1, column: 1 };
422
+ expect(editor.isValid.value).toBe(false);
423
+ });
424
+
425
+ it('should compute charCount correctly', async () => {
426
+ const editor = createEditor({ key: 'value' }, 'yaml', true);
427
+
428
+ if (!editor.isEditing.value) {
429
+ editor.toggleEdit();
430
+ await nextTick();
431
+ }
432
+
433
+ editor.editingContent.value = '12345';
434
+ editor.isUserEditing.value = true;
435
+
436
+ expect(editor.charCount.value).toBe(5);
437
+ });
438
+
439
+ it('should use formattedContent when not user editing', async () => {
440
+ const editor = createEditor({ key: 'value' }, 'yaml', false);
441
+
442
+ editor.isUserEditing.value = false;
443
+
444
+ // displayContent should come from codeFormat.formattedContent
445
+ expect(editor.displayContent.value).toBe(editor.codeFormat.formattedContent.value);
446
+ });
447
+ });
448
+
449
+ describe('format change handling', () => {
450
+ it('should update editing content when format changes in edit mode', async () => {
451
+ const editor = createEditor({ key: 'value' }, 'yaml', true);
452
+
453
+ if (!editor.isEditing.value) {
454
+ editor.toggleEdit();
455
+ await nextTick();
456
+ }
457
+
458
+ // Simulate format change callback
459
+ editor.updateEditingContentOnFormatChange();
460
+
461
+ // Content should be updated to new formatted content
462
+ expect(editor.editingContent.value).toBe(editor.codeFormat.formattedContent.value);
463
+ });
464
+ });
465
+
466
+ describe('Ctrl+Alt+L language cycling', () => {
467
+ let onEmitFormat: ReturnType<typeof vi.fn>;
468
+
469
+ /**
470
+ * Helper to create an editor with onEmitFormat callback
471
+ */
472
+ function createEditorWithFormatCallback(
473
+ initialValue: object | string | null,
474
+ format: CodeFormat = 'yaml',
475
+ editable: boolean = false
476
+ ) {
477
+ const currentFormat = ref<CodeFormat>(format);
478
+ const canEdit = ref(true);
479
+ const editableRef = ref(editable);
480
+ const codeFormat = useCodeFormat(ref(initialValue), currentFormat);
481
+ onEmitFormat = vi.fn();
482
+
483
+ const options: UseCodeViewerEditorOptions = {
484
+ codeRef,
485
+ codeFormat,
486
+ currentFormat,
487
+ canEdit,
488
+ editable: editableRef,
489
+ onEmitModelValue,
490
+ onEmitEditable,
491
+ onEmitFormat,
492
+ onExit,
493
+ onDelete
494
+ };
495
+
496
+ return {
497
+ ...useCodeViewerEditor(options),
498
+ codeFormat,
499
+ currentFormat,
500
+ canEdit,
501
+ editableRef,
502
+ onEmitFormat
503
+ };
504
+ }
505
+
506
+ /**
507
+ * Helper to dispatch a Ctrl+Alt+L keydown event
508
+ */
509
+ function pressCtrlAltL(element: HTMLElement): KeyboardEvent {
510
+ const event = new KeyboardEvent('keydown', {
511
+ key: 'l',
512
+ code: 'KeyL',
513
+ ctrlKey: true,
514
+ altKey: true,
515
+ bubbles: true,
516
+ cancelable: true
517
+ });
518
+ element.dispatchEvent(event);
519
+ return event;
520
+ }
521
+
522
+ it('should cycle from yaml to json on Ctrl+Alt+L', async () => {
523
+ const editor = createEditorWithFormatCallback({ key: 'value' }, 'yaml', false);
524
+
525
+ const event = pressCtrlAltL(container);
526
+ editor.onKeyDown(event);
527
+
528
+ // yaml -> json in the yaml/json cycle
529
+ expect(editor.onEmitFormat).toHaveBeenCalledWith('json');
530
+ });
531
+
532
+ it('should cycle from json to yaml on Ctrl+Alt+L', async () => {
533
+ const editor = createEditorWithFormatCallback({ key: 'value' }, 'json', false);
534
+
535
+ const event = pressCtrlAltL(container);
536
+ editor.onKeyDown(event);
537
+
538
+ // json -> yaml in the yaml/json cycle
539
+ expect(editor.onEmitFormat).toHaveBeenCalledWith('yaml');
540
+ });
541
+
542
+ it('should cycle from text to markdown on Ctrl+Alt+L (text/markdown cycle)', async () => {
543
+ // When format is 'text', the cycle is [text, markdown]
544
+ const editor = createEditorWithFormatCallback('plain text content', 'text', false);
545
+
546
+ const event = pressCtrlAltL(container);
547
+ editor.onKeyDown(event);
548
+
549
+ // text -> markdown in the text/markdown cycle
550
+ expect(editor.onEmitFormat).toHaveBeenCalledWith('markdown');
551
+ });
552
+
553
+ it('should work in read-only mode (not editing)', async () => {
554
+ const editor = createEditorWithFormatCallback({ key: 'value' }, 'yaml', false);
555
+
556
+ // Ensure we're NOT in edit mode
557
+ expect(editor.isEditing.value).toBe(false);
558
+
559
+ const event = pressCtrlAltL(container);
560
+ editor.onKeyDown(event);
561
+
562
+ // Should still call onEmitFormat even when not editing
563
+ expect(editor.onEmitFormat).toHaveBeenCalledWith('json');
564
+ });
565
+
566
+ it('should work in edit mode', async () => {
567
+ const editor = createEditorWithFormatCallback({ key: 'value' }, 'yaml', true);
568
+
569
+ // Already in edit mode (editable=true means internalEditable starts true)
570
+ // isEditing = canEdit && internalEditable
571
+ expect(editor.isEditing.value).toBe(true);
572
+
573
+ const event = pressCtrlAltL(container);
574
+ editor.onKeyDown(event);
575
+
576
+ expect(editor.onEmitFormat).toHaveBeenCalledWith('json');
577
+ });
578
+
579
+ it('should prevent default and stop propagation', async () => {
580
+ const editor = createEditorWithFormatCallback({ key: 'value' }, 'yaml', false);
581
+
582
+ const event = pressCtrlAltL(container);
583
+ editor.onKeyDown(event);
584
+
585
+ expect(event.defaultPrevented).toBe(true);
586
+ });
587
+
588
+ it('should complete full cycle yaml -> json -> yaml', async () => {
589
+ // When format is yaml or json, cycle is [yaml, json]
590
+ const editor = createEditorWithFormatCallback({ key: 'value' }, 'yaml', false);
591
+
592
+ // First press: yaml -> json
593
+ let event = pressCtrlAltL(container);
594
+ editor.onKeyDown(event);
595
+ expect(editor.onEmitFormat).toHaveBeenCalledWith('json');
596
+
597
+ // Simulate format change
598
+ editor.currentFormat.value = 'json';
599
+ editor.onEmitFormat.mockClear();
600
+
601
+ // Second press: json -> yaml (cycles back)
602
+ event = pressCtrlAltL(container);
603
+ editor.onKeyDown(event);
604
+ expect(editor.onEmitFormat).toHaveBeenCalledWith('yaml');
605
+ });
606
+
607
+ it('should cycle text -> markdown -> text', async () => {
608
+ // When format is text or markdown, cycle is [text, markdown]
609
+ const editor = createEditorWithFormatCallback('plain text', 'text', false);
610
+
611
+ // First press: text -> markdown
612
+ let event = pressCtrlAltL(container);
613
+ editor.onKeyDown(event);
614
+ expect(editor.onEmitFormat).toHaveBeenCalledWith('markdown');
615
+
616
+ // Simulate format change
617
+ editor.currentFormat.value = 'markdown';
618
+ editor.onEmitFormat.mockClear();
619
+
620
+ // Second press: markdown -> text (cycles back)
621
+ event = pressCtrlAltL(container);
622
+ editor.onKeyDown(event);
623
+ expect(editor.onEmitFormat).toHaveBeenCalledWith('text');
624
+ });
625
+
626
+ it('should work with Cmd+Alt+L (Mac) as well as Ctrl+Alt+L', async () => {
627
+ const editor = createEditorWithFormatCallback({ key: 'value' }, 'yaml', false);
628
+
629
+ // Test with metaKey (Cmd on Mac)
630
+ const event = new KeyboardEvent('keydown', {
631
+ key: 'l',
632
+ code: 'KeyL',
633
+ metaKey: true,
634
+ altKey: true,
635
+ bubbles: true,
636
+ cancelable: true
637
+ });
638
+ container.dispatchEvent(event);
639
+ editor.onKeyDown(event);
640
+
641
+ expect(editor.onEmitFormat).toHaveBeenCalledWith('json');
642
+ });
643
+
644
+ it('should not call onEmitFormat when onEmitFormat callback is not provided', async () => {
645
+ // Use the regular createEditor which doesn't set onEmitFormat
646
+ const editor = createEditor({ key: 'value' }, 'yaml', false);
647
+
648
+ const event = pressCtrlAltL(container);
649
+
650
+ // Should not throw and should still prevent default
651
+ expect(() => editor.onKeyDown(event)).not.toThrow();
652
+ expect(event.defaultPrevented).toBe(true);
653
+ });
654
+ });
655
+ });