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,791 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { ref } from "vue";
3
+ import {
4
+ parseKeyCombo,
5
+ matchesKeyCombo,
6
+ useMarkdownHotkeys,
7
+ ParsedKey,
8
+ HotkeyDefinition
9
+ } from "./useMarkdownHotkeys";
10
+
11
+ /**
12
+ * Helper to create a mock KeyboardEvent with specified properties
13
+ */
14
+ function createKeyboardEvent(options: {
15
+ key: string;
16
+ code?: string;
17
+ ctrlKey?: boolean;
18
+ shiftKey?: boolean;
19
+ altKey?: boolean;
20
+ metaKey?: boolean;
21
+ }): KeyboardEvent {
22
+ return {
23
+ key: options.key,
24
+ code: options.code || "",
25
+ ctrlKey: options.ctrlKey || false,
26
+ shiftKey: options.shiftKey || false,
27
+ altKey: options.altKey || false,
28
+ metaKey: options.metaKey || false,
29
+ preventDefault: vi.fn()
30
+ } as unknown as KeyboardEvent;
31
+ }
32
+
33
+ describe("useMarkdownHotkeys", () => {
34
+ describe("parseKeyCombo", () => {
35
+ it("parses simple key without modifiers", () => {
36
+ const result = parseKeyCombo("a");
37
+ expect(result).toEqual({
38
+ key: "a",
39
+ ctrl: false,
40
+ shift: false,
41
+ alt: false,
42
+ meta: false
43
+ });
44
+ });
45
+
46
+ it("parses ctrl+key combination", () => {
47
+ const result = parseKeyCombo("ctrl+b");
48
+ expect(result).toEqual({
49
+ key: "b",
50
+ ctrl: true,
51
+ shift: false,
52
+ alt: false,
53
+ meta: false
54
+ });
55
+ });
56
+
57
+ it("parses control as alias for ctrl", () => {
58
+ const result = parseKeyCombo("control+b");
59
+ expect(result).toEqual({
60
+ key: "b",
61
+ ctrl: true,
62
+ shift: false,
63
+ alt: false,
64
+ meta: false
65
+ });
66
+ });
67
+
68
+ it("parses shift+key combination", () => {
69
+ const result = parseKeyCombo("shift+a");
70
+ expect(result).toEqual({
71
+ key: "a",
72
+ ctrl: false,
73
+ shift: true,
74
+ alt: false,
75
+ meta: false
76
+ });
77
+ });
78
+
79
+ it("parses alt+key combination", () => {
80
+ const result = parseKeyCombo("alt+x");
81
+ expect(result).toEqual({
82
+ key: "x",
83
+ ctrl: false,
84
+ shift: false,
85
+ alt: true,
86
+ meta: false
87
+ });
88
+ });
89
+
90
+ it("parses option as alias for alt (Mac)", () => {
91
+ const result = parseKeyCombo("option+x");
92
+ expect(result).toEqual({
93
+ key: "x",
94
+ ctrl: false,
95
+ shift: false,
96
+ alt: true,
97
+ meta: false
98
+ });
99
+ });
100
+
101
+ it("parses meta+key combination", () => {
102
+ const result = parseKeyCombo("meta+s");
103
+ expect(result).toEqual({
104
+ key: "s",
105
+ ctrl: false,
106
+ shift: false,
107
+ alt: false,
108
+ meta: true
109
+ });
110
+ });
111
+
112
+ it("parses cmd as alias for meta (Mac)", () => {
113
+ const result = parseKeyCombo("cmd+s");
114
+ expect(result).toEqual({
115
+ key: "s",
116
+ ctrl: false,
117
+ shift: false,
118
+ alt: false,
119
+ meta: true
120
+ });
121
+ });
122
+
123
+ it("parses command as alias for meta (Mac)", () => {
124
+ const result = parseKeyCombo("command+s");
125
+ expect(result).toEqual({
126
+ key: "s",
127
+ ctrl: false,
128
+ shift: false,
129
+ alt: false,
130
+ meta: true
131
+ });
132
+ });
133
+
134
+ it("parses win as alias for meta (Windows)", () => {
135
+ const result = parseKeyCombo("win+e");
136
+ expect(result).toEqual({
137
+ key: "e",
138
+ ctrl: false,
139
+ shift: false,
140
+ alt: false,
141
+ meta: true
142
+ });
143
+ });
144
+
145
+ it("parses windows as alias for meta (Windows)", () => {
146
+ const result = parseKeyCombo("windows+e");
147
+ expect(result).toEqual({
148
+ key: "e",
149
+ ctrl: false,
150
+ shift: false,
151
+ alt: false,
152
+ meta: true
153
+ });
154
+ });
155
+
156
+ it("parses ctrl+shift+key combination", () => {
157
+ const result = parseKeyCombo("ctrl+shift+b");
158
+ expect(result).toEqual({
159
+ key: "b",
160
+ ctrl: true,
161
+ shift: true,
162
+ alt: false,
163
+ meta: false
164
+ });
165
+ });
166
+
167
+ it("parses ctrl+alt+key combination", () => {
168
+ const result = parseKeyCombo("ctrl+alt+d");
169
+ expect(result).toEqual({
170
+ key: "d",
171
+ ctrl: true,
172
+ shift: false,
173
+ alt: true,
174
+ meta: false
175
+ });
176
+ });
177
+
178
+ it("parses all modifiers together", () => {
179
+ const result = parseKeyCombo("ctrl+shift+alt+meta+x");
180
+ expect(result).toEqual({
181
+ key: "x",
182
+ ctrl: true,
183
+ shift: true,
184
+ alt: true,
185
+ meta: true
186
+ });
187
+ });
188
+
189
+ it("handles uppercase input (normalizes to lowercase)", () => {
190
+ const result = parseKeyCombo("CTRL+SHIFT+B");
191
+ expect(result).toEqual({
192
+ key: "b",
193
+ ctrl: true,
194
+ shift: true,
195
+ alt: false,
196
+ meta: false
197
+ });
198
+ });
199
+
200
+ it("parses number keys", () => {
201
+ const result = parseKeyCombo("ctrl+1");
202
+ expect(result).toEqual({
203
+ key: "1",
204
+ ctrl: true,
205
+ shift: false,
206
+ alt: false,
207
+ meta: false
208
+ });
209
+ });
210
+
211
+ it("parses bracket key [", () => {
212
+ const result = parseKeyCombo("ctrl+shift+[");
213
+ expect(result).toEqual({
214
+ key: "[",
215
+ ctrl: true,
216
+ shift: true,
217
+ alt: false,
218
+ meta: false
219
+ });
220
+ });
221
+
222
+ it("parses bracket key ]", () => {
223
+ const result = parseKeyCombo("ctrl+shift+]");
224
+ expect(result).toEqual({
225
+ key: "]",
226
+ ctrl: true,
227
+ shift: true,
228
+ alt: false,
229
+ meta: false
230
+ });
231
+ });
232
+
233
+ it("parses special characters", () => {
234
+ const result = parseKeyCombo("ctrl+/");
235
+ expect(result).toEqual({
236
+ key: "/",
237
+ ctrl: true,
238
+ shift: false,
239
+ alt: false,
240
+ meta: false
241
+ });
242
+ });
243
+ });
244
+
245
+ describe("matchesKeyCombo", () => {
246
+ describe("basic key matching", () => {
247
+ it("matches simple key press", () => {
248
+ const event = createKeyboardEvent({ key: "a" });
249
+ const parsed = parseKeyCombo("a");
250
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
251
+ });
252
+
253
+ it("does not match wrong key", () => {
254
+ const event = createKeyboardEvent({ key: "b" });
255
+ const parsed = parseKeyCombo("a");
256
+ expect(matchesKeyCombo(event, parsed)).toBe(false);
257
+ });
258
+
259
+ it("matches case-insensitively", () => {
260
+ const event = createKeyboardEvent({ key: "A" });
261
+ const parsed = parseKeyCombo("a");
262
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
263
+ });
264
+ });
265
+
266
+ describe("modifier key matching", () => {
267
+ it("matches ctrl+key on Windows/Linux", () => {
268
+ const event = createKeyboardEvent({ key: "b", ctrlKey: true });
269
+ const parsed = parseKeyCombo("ctrl+b");
270
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
271
+ });
272
+
273
+ it("does not match when ctrl is expected but not pressed", () => {
274
+ const event = createKeyboardEvent({ key: "b", ctrlKey: false });
275
+ const parsed = parseKeyCombo("ctrl+b");
276
+ expect(matchesKeyCombo(event, parsed)).toBe(false);
277
+ });
278
+
279
+ it("does not match when extra modifiers are pressed", () => {
280
+ const event = createKeyboardEvent({ key: "b", ctrlKey: true, shiftKey: true });
281
+ const parsed = parseKeyCombo("ctrl+b");
282
+ expect(matchesKeyCombo(event, parsed)).toBe(false);
283
+ });
284
+
285
+ it("matches shift+key", () => {
286
+ const event = createKeyboardEvent({ key: "a", shiftKey: true });
287
+ const parsed = parseKeyCombo("shift+a");
288
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
289
+ });
290
+
291
+ it("matches alt+key", () => {
292
+ const event = createKeyboardEvent({ key: "x", altKey: true });
293
+ const parsed = parseKeyCombo("alt+x");
294
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
295
+ });
296
+
297
+ it("matches ctrl+shift+key", () => {
298
+ const event = createKeyboardEvent({ key: "b", ctrlKey: true, shiftKey: true });
299
+ const parsed = parseKeyCombo("ctrl+shift+b");
300
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
301
+ });
302
+
303
+ it("matches meta+key (for explicit meta hotkeys)", () => {
304
+ const event = createKeyboardEvent({ key: "s", metaKey: true });
305
+ const parsed = parseKeyCombo("meta+s");
306
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
307
+ });
308
+ });
309
+
310
+ describe("number key matching", () => {
311
+ it("matches ctrl+1", () => {
312
+ const event = createKeyboardEvent({ key: "1", code: "Digit1", ctrlKey: true });
313
+ const parsed = parseKeyCombo("ctrl+1");
314
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
315
+ });
316
+
317
+ it("matches ctrl+2", () => {
318
+ const event = createKeyboardEvent({ key: "2", code: "Digit2", ctrlKey: true });
319
+ const parsed = parseKeyCombo("ctrl+2");
320
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
321
+ });
322
+
323
+ it("matches ctrl+3", () => {
324
+ const event = createKeyboardEvent({ key: "3", code: "Digit3", ctrlKey: true });
325
+ const parsed = parseKeyCombo("ctrl+3");
326
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
327
+ });
328
+
329
+ it("matches ctrl+4", () => {
330
+ const event = createKeyboardEvent({ key: "4", code: "Digit4", ctrlKey: true });
331
+ const parsed = parseKeyCombo("ctrl+4");
332
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
333
+ });
334
+
335
+ it("matches ctrl+5", () => {
336
+ const event = createKeyboardEvent({ key: "5", code: "Digit5", ctrlKey: true });
337
+ const parsed = parseKeyCombo("ctrl+5");
338
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
339
+ });
340
+
341
+ it("matches ctrl+6", () => {
342
+ const event = createKeyboardEvent({ key: "6", code: "Digit6", ctrlKey: true });
343
+ const parsed = parseKeyCombo("ctrl+6");
344
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
345
+ });
346
+
347
+ it("matches numpad keys", () => {
348
+ const event = createKeyboardEvent({ key: "1", code: "Numpad1", ctrlKey: true });
349
+ const parsed = parseKeyCombo("ctrl+1");
350
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
351
+ });
352
+
353
+ it("does not match wrong number", () => {
354
+ const event = createKeyboardEvent({ key: "2", code: "Digit2", ctrlKey: true });
355
+ const parsed = parseKeyCombo("ctrl+1");
356
+ expect(matchesKeyCombo(event, parsed)).toBe(false);
357
+ });
358
+ });
359
+
360
+ describe("shifted key handling", () => {
361
+ it("matches > when browser reports > directly", () => {
362
+ // Browser reports the shifted character
363
+ const event = createKeyboardEvent({ key: ">", shiftKey: true, ctrlKey: true });
364
+ const parsed = parseKeyCombo("ctrl+>");
365
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
366
+ });
367
+
368
+ it("matches < when browser reports < directly", () => {
369
+ const event = createKeyboardEvent({ key: "<", shiftKey: true, ctrlKey: true });
370
+ const parsed = parseKeyCombo("ctrl+<");
371
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
372
+ });
373
+
374
+ it("matches ? when browser reports ? directly", () => {
375
+ const event = createKeyboardEvent({ key: "?", shiftKey: true, ctrlKey: true });
376
+ const parsed = parseKeyCombo("ctrl+?");
377
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
378
+ });
379
+
380
+ it("matches > via shift+. combination", () => {
381
+ // Browser reports the base key with shift
382
+ const event = createKeyboardEvent({ key: ".", shiftKey: true, ctrlKey: true });
383
+ const parsed = parseKeyCombo("ctrl+>");
384
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
385
+ });
386
+
387
+ it("matches < via shift+, combination", () => {
388
+ const event = createKeyboardEvent({ key: ",", shiftKey: true, ctrlKey: true });
389
+ const parsed = parseKeyCombo("ctrl+<");
390
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
391
+ });
392
+
393
+ it("matches ? via shift+/ combination", () => {
394
+ const event = createKeyboardEvent({ key: "/", shiftKey: true, ctrlKey: true });
395
+ const parsed = parseKeyCombo("ctrl+?");
396
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
397
+ });
398
+
399
+ it("matches ! when browser reports !", () => {
400
+ const event = createKeyboardEvent({ key: "!", shiftKey: true, ctrlKey: true });
401
+ const parsed = parseKeyCombo("ctrl+!");
402
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
403
+ });
404
+
405
+ it("matches @ when browser reports @", () => {
406
+ const event = createKeyboardEvent({ key: "@", shiftKey: true, ctrlKey: true });
407
+ const parsed = parseKeyCombo("ctrl+@");
408
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
409
+ });
410
+
411
+ it("matches # when browser reports #", () => {
412
+ const event = createKeyboardEvent({ key: "#", shiftKey: true, ctrlKey: true });
413
+ const parsed = parseKeyCombo("ctrl+#");
414
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
415
+ });
416
+ });
417
+
418
+ describe("bracket hotkeys (ctrl+shift+[ and ctrl+shift+])", () => {
419
+ it("matches ctrl+shift+[ when browser reports { (shifted character)", () => {
420
+ // When pressing Ctrl+Shift+[, browser reports key as "{"
421
+ const event = createKeyboardEvent({ key: "{", shiftKey: true, ctrlKey: true });
422
+ const parsed = parseKeyCombo("ctrl+shift+[");
423
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
424
+ });
425
+
426
+ it("matches ctrl+shift+] when browser reports } (shifted character)", () => {
427
+ // When pressing Ctrl+Shift+], browser reports key as "}"
428
+ const event = createKeyboardEvent({ key: "}", shiftKey: true, ctrlKey: true });
429
+ const parsed = parseKeyCombo("ctrl+shift+]");
430
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
431
+ });
432
+
433
+ it("matches ctrl+shift+[ when browser reports [ with shift", () => {
434
+ // Alternative: browser might report base key with shift modifier
435
+ const event = createKeyboardEvent({ key: "[", shiftKey: true, ctrlKey: true });
436
+ const parsed = parseKeyCombo("ctrl+shift+[");
437
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
438
+ });
439
+
440
+ it("matches ctrl+shift+] when browser reports ] with shift", () => {
441
+ const event = createKeyboardEvent({ key: "]", shiftKey: true, ctrlKey: true });
442
+ const parsed = parseKeyCombo("ctrl+shift+]");
443
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
444
+ });
445
+
446
+ it("does not match ctrl+[ without shift", () => {
447
+ const event = createKeyboardEvent({ key: "[", shiftKey: false, ctrlKey: true });
448
+ const parsed = parseKeyCombo("ctrl+shift+[");
449
+ expect(matchesKeyCombo(event, parsed)).toBe(false);
450
+ });
451
+
452
+ it("does not match ctrl+] without shift", () => {
453
+ const event = createKeyboardEvent({ key: "]", shiftKey: false, ctrlKey: true });
454
+ const parsed = parseKeyCombo("ctrl+shift+]");
455
+ expect(matchesKeyCombo(event, parsed)).toBe(false);
456
+ });
457
+
458
+ it("does not match shift+[ without ctrl", () => {
459
+ const event = createKeyboardEvent({ key: "{", shiftKey: true, ctrlKey: false });
460
+ const parsed = parseKeyCombo("ctrl+shift+[");
461
+ expect(matchesKeyCombo(event, parsed)).toBe(false);
462
+ });
463
+ });
464
+
465
+ describe("arrow key normalization", () => {
466
+ it("matches ArrowUp to ctrl+alt+shift+up", () => {
467
+ const event = createKeyboardEvent({
468
+ key: "ArrowUp",
469
+ ctrlKey: true,
470
+ altKey: true,
471
+ shiftKey: true
472
+ });
473
+ const parsed = parseKeyCombo("ctrl+alt+shift+up");
474
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
475
+ });
476
+
477
+ it("matches ArrowDown to ctrl+alt+shift+down", () => {
478
+ const event = createKeyboardEvent({
479
+ key: "ArrowDown",
480
+ ctrlKey: true,
481
+ altKey: true,
482
+ shiftKey: true
483
+ });
484
+ const parsed = parseKeyCombo("ctrl+alt+shift+down");
485
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
486
+ });
487
+
488
+ it("matches ArrowLeft to ctrl+alt+shift+left", () => {
489
+ const event = createKeyboardEvent({
490
+ key: "ArrowLeft",
491
+ ctrlKey: true,
492
+ altKey: true,
493
+ shiftKey: true
494
+ });
495
+ const parsed = parseKeyCombo("ctrl+alt+shift+left");
496
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
497
+ });
498
+
499
+ it("matches ArrowRight to ctrl+alt+shift+right", () => {
500
+ const event = createKeyboardEvent({
501
+ key: "ArrowRight",
502
+ ctrlKey: true,
503
+ altKey: true,
504
+ shiftKey: true
505
+ });
506
+ const parsed = parseKeyCombo("ctrl+alt+shift+right");
507
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
508
+ });
509
+
510
+ it("matches ArrowUp with only ctrl modifier", () => {
511
+ const event = createKeyboardEvent({
512
+ key: "ArrowUp",
513
+ ctrlKey: true
514
+ });
515
+ const parsed = parseKeyCombo("ctrl+up");
516
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
517
+ });
518
+
519
+ it("matches ArrowDown with only alt modifier", () => {
520
+ const event = createKeyboardEvent({
521
+ key: "ArrowDown",
522
+ altKey: true
523
+ });
524
+ const parsed = parseKeyCombo("alt+down");
525
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
526
+ });
527
+
528
+ it("does not match ArrowUp when expecting ArrowDown", () => {
529
+ const event = createKeyboardEvent({
530
+ key: "ArrowUp",
531
+ ctrlKey: true
532
+ });
533
+ const parsed = parseKeyCombo("ctrl+down");
534
+ expect(matchesKeyCombo(event, parsed)).toBe(false);
535
+ });
536
+
537
+ it("does not match when modifiers are different", () => {
538
+ const event = createKeyboardEvent({
539
+ key: "ArrowUp",
540
+ ctrlKey: true,
541
+ shiftKey: false
542
+ });
543
+ const parsed = parseKeyCombo("ctrl+shift+up");
544
+ expect(matchesKeyCombo(event, parsed)).toBe(false);
545
+ });
546
+ });
547
+
548
+ describe("cross-platform modifier handling", () => {
549
+ // Note: These tests check the function behavior; actual Mac detection
550
+ // depends on navigator.platform which is mocked by jsdom
551
+
552
+ it("matches ctrl+key when only ctrl is pressed (non-Mac)", () => {
553
+ const event = createKeyboardEvent({ key: "c", ctrlKey: true, metaKey: false });
554
+ const parsed = parseKeyCombo("ctrl+c");
555
+ expect(matchesKeyCombo(event, parsed)).toBe(true);
556
+ });
557
+
558
+ it("does not match key press when ctrl is required but neither ctrl nor meta pressed", () => {
559
+ const event = createKeyboardEvent({ key: "c", ctrlKey: false, metaKey: false });
560
+ const parsed = parseKeyCombo("ctrl+c");
561
+ expect(matchesKeyCombo(event, parsed)).toBe(false);
562
+ });
563
+ });
564
+ });
565
+
566
+ describe("useMarkdownHotkeys composable", () => {
567
+ let contentRef: ReturnType<typeof ref<HTMLElement | null>>;
568
+ let onShowHotkeyHelp: ReturnType<typeof vi.fn>;
569
+
570
+ beforeEach(() => {
571
+ contentRef = ref(document.createElement("div"));
572
+ onShowHotkeyHelp = vi.fn();
573
+ });
574
+
575
+ it("registers and retrieves hotkey definitions", () => {
576
+ const { registerHotkey, getHotkeyDefinitions } = useMarkdownHotkeys({
577
+ contentRef,
578
+ onShowHotkeyHelp
579
+ });
580
+
581
+ const def: HotkeyDefinition = {
582
+ key: "ctrl+b",
583
+ action: vi.fn(),
584
+ description: "Bold",
585
+ group: "formatting"
586
+ };
587
+
588
+ registerHotkey(def);
589
+
590
+ const definitions = getHotkeyDefinitions();
591
+ expect(definitions).toHaveLength(1);
592
+ expect(definitions[0].key).toBe("ctrl+b");
593
+ expect(definitions[0].description).toBe("Bold");
594
+ });
595
+
596
+ it("unregisters hotkeys", () => {
597
+ const { registerHotkey, unregisterHotkey, getHotkeyDefinitions } = useMarkdownHotkeys({
598
+ contentRef,
599
+ onShowHotkeyHelp
600
+ });
601
+
602
+ registerHotkey({
603
+ key: "ctrl+b",
604
+ action: vi.fn(),
605
+ description: "Bold",
606
+ group: "formatting"
607
+ });
608
+
609
+ expect(getHotkeyDefinitions()).toHaveLength(1);
610
+
611
+ unregisterHotkey("ctrl+b");
612
+
613
+ expect(getHotkeyDefinitions()).toHaveLength(0);
614
+ });
615
+
616
+ it("handles keydown and executes registered action", () => {
617
+ const { registerHotkey, handleKeyDown } = useMarkdownHotkeys({
618
+ contentRef,
619
+ onShowHotkeyHelp
620
+ });
621
+
622
+ const action = vi.fn();
623
+ registerHotkey({
624
+ key: "ctrl+b",
625
+ action,
626
+ description: "Bold",
627
+ group: "formatting"
628
+ });
629
+
630
+ const event = createKeyboardEvent({ key: "b", ctrlKey: true });
631
+ const handled = handleKeyDown(event);
632
+
633
+ expect(handled).toBe(true);
634
+ expect(action).toHaveBeenCalledTimes(1);
635
+ expect(event.preventDefault).toHaveBeenCalled();
636
+ });
637
+
638
+ it("returns false when no hotkey matches", () => {
639
+ const { handleKeyDown } = useMarkdownHotkeys({
640
+ contentRef,
641
+ onShowHotkeyHelp
642
+ });
643
+
644
+ const event = createKeyboardEvent({ key: "x" });
645
+ const handled = handleKeyDown(event);
646
+
647
+ expect(handled).toBe(false);
648
+ expect(event.preventDefault).not.toHaveBeenCalled();
649
+ });
650
+
651
+ it("triggers help on ctrl+/", () => {
652
+ const { handleKeyDown } = useMarkdownHotkeys({
653
+ contentRef,
654
+ onShowHotkeyHelp
655
+ });
656
+
657
+ const event = createKeyboardEvent({ key: "/", ctrlKey: true });
658
+ const handled = handleKeyDown(event);
659
+
660
+ expect(handled).toBe(true);
661
+ expect(onShowHotkeyHelp).toHaveBeenCalledTimes(1);
662
+ });
663
+
664
+ it("triggers help on ctrl+?", () => {
665
+ const { handleKeyDown } = useMarkdownHotkeys({
666
+ contentRef,
667
+ onShowHotkeyHelp
668
+ });
669
+
670
+ const event = createKeyboardEvent({ key: "?", ctrlKey: true });
671
+ const handled = handleKeyDown(event);
672
+
673
+ expect(handled).toBe(true);
674
+ expect(onShowHotkeyHelp).toHaveBeenCalledTimes(1);
675
+ });
676
+
677
+ it("triggers help on meta+/ (Mac)", () => {
678
+ const { handleKeyDown } = useMarkdownHotkeys({
679
+ contentRef,
680
+ onShowHotkeyHelp
681
+ });
682
+
683
+ const event = createKeyboardEvent({ key: "/", metaKey: true });
684
+ const handled = handleKeyDown(event);
685
+
686
+ expect(handled).toBe(true);
687
+ expect(onShowHotkeyHelp).toHaveBeenCalledTimes(1);
688
+ });
689
+
690
+ it("handles multiple registered hotkeys", () => {
691
+ const { registerHotkey, handleKeyDown } = useMarkdownHotkeys({
692
+ contentRef,
693
+ onShowHotkeyHelp
694
+ });
695
+
696
+ const boldAction = vi.fn();
697
+ const italicAction = vi.fn();
698
+
699
+ registerHotkey({
700
+ key: "ctrl+b",
701
+ action: boldAction,
702
+ description: "Bold",
703
+ group: "formatting"
704
+ });
705
+
706
+ registerHotkey({
707
+ key: "ctrl+i",
708
+ action: italicAction,
709
+ description: "Italic",
710
+ group: "formatting"
711
+ });
712
+
713
+ const boldEvent = createKeyboardEvent({ key: "b", ctrlKey: true });
714
+ handleKeyDown(boldEvent);
715
+ expect(boldAction).toHaveBeenCalledTimes(1);
716
+ expect(italicAction).not.toHaveBeenCalled();
717
+
718
+ const italicEvent = createKeyboardEvent({ key: "i", ctrlKey: true });
719
+ handleKeyDown(italicEvent);
720
+ expect(italicAction).toHaveBeenCalledTimes(1);
721
+ });
722
+
723
+ it("normalizes hotkey keys to lowercase", () => {
724
+ const { registerHotkey, handleKeyDown } = useMarkdownHotkeys({
725
+ contentRef,
726
+ onShowHotkeyHelp
727
+ });
728
+
729
+ const action = vi.fn();
730
+ registerHotkey({
731
+ key: "CTRL+B", // Uppercase input
732
+ action,
733
+ description: "Bold",
734
+ group: "formatting"
735
+ });
736
+
737
+ // Event with lowercase key
738
+ const event = createKeyboardEvent({ key: "b", ctrlKey: true });
739
+ const handled = handleKeyDown(event);
740
+
741
+ expect(handled).toBe(true);
742
+ expect(action).toHaveBeenCalled();
743
+ });
744
+
745
+ describe("bracket hotkey integration", () => {
746
+ it("handles ctrl+shift+[ hotkey registration and execution", () => {
747
+ const { registerHotkey, handleKeyDown } = useMarkdownHotkeys({
748
+ contentRef,
749
+ onShowHotkeyHelp
750
+ });
751
+
752
+ const increaseHeadingAction = vi.fn();
753
+ registerHotkey({
754
+ key: "ctrl+shift+[",
755
+ action: increaseHeadingAction,
756
+ description: "Increase heading level",
757
+ group: "headings"
758
+ });
759
+
760
+ // Browser reports "{" when Ctrl+Shift+[ is pressed
761
+ const event = createKeyboardEvent({ key: "{", ctrlKey: true, shiftKey: true });
762
+ const handled = handleKeyDown(event);
763
+
764
+ expect(handled).toBe(true);
765
+ expect(increaseHeadingAction).toHaveBeenCalledTimes(1);
766
+ });
767
+
768
+ it("handles ctrl+shift+] hotkey registration and execution", () => {
769
+ const { registerHotkey, handleKeyDown } = useMarkdownHotkeys({
770
+ contentRef,
771
+ onShowHotkeyHelp
772
+ });
773
+
774
+ const decreaseHeadingAction = vi.fn();
775
+ registerHotkey({
776
+ key: "ctrl+shift+]",
777
+ action: decreaseHeadingAction,
778
+ description: "Decrease heading level",
779
+ group: "headings"
780
+ });
781
+
782
+ // Browser reports "}" when Ctrl+Shift+] is pressed
783
+ const event = createKeyboardEvent({ key: "}", ctrlKey: true, shiftKey: true });
784
+ const handled = handleKeyDown(event);
785
+
786
+ expect(handled).toBe(true);
787
+ expect(decreaseHeadingAction).toHaveBeenCalledTimes(1);
788
+ });
789
+ });
790
+ });
791
+ });