quasar-ui-danx 0.5.0 → 0.5.2

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 (81) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/dist/danx.es.js +16119 -10641
  3. package/dist/danx.es.js.map +1 -1
  4. package/dist/danx.umd.js +202 -123
  5. package/dist/danx.umd.js.map +1 -1
  6. package/dist/style.css +1 -1
  7. package/package.json +8 -1
  8. package/src/components/Utility/Buttons/ActionButton.vue +15 -5
  9. package/src/components/Utility/Code/CodeViewer.vue +41 -16
  10. package/src/components/Utility/Code/CodeViewerCollapsed.vue +2 -0
  11. package/src/components/Utility/Code/CodeViewerFooter.vue +3 -1
  12. package/src/components/Utility/Code/LanguageBadge.vue +278 -5
  13. package/src/components/Utility/Code/MarkdownContent.vue +31 -163
  14. package/src/components/Utility/Code/index.ts +3 -0
  15. package/src/components/Utility/Markdown/ContextMenu.vue +314 -0
  16. package/src/components/Utility/Markdown/HotkeyHelpPopover.vue +259 -0
  17. package/src/components/Utility/Markdown/LineTypeMenu.vue +226 -0
  18. package/src/components/Utility/Markdown/LinkPopover.vue +331 -0
  19. package/src/components/Utility/Markdown/MarkdownEditor.vue +233 -0
  20. package/src/components/Utility/Markdown/MarkdownEditorContent.vue +296 -0
  21. package/src/components/Utility/Markdown/MarkdownEditorFooter.vue +50 -0
  22. package/src/components/Utility/Markdown/TablePopover.vue +420 -0
  23. package/src/components/Utility/Markdown/index.ts +11 -0
  24. package/src/components/Utility/Markdown/types.ts +27 -0
  25. package/src/components/Utility/Widgets/LabelPillWidget.vue +20 -0
  26. package/src/components/Utility/index.ts +1 -0
  27. package/src/composables/index.ts +1 -0
  28. package/src/composables/markdown/features/useBlockquotes.spec.ts +428 -0
  29. package/src/composables/markdown/features/useBlockquotes.ts +248 -0
  30. package/src/composables/markdown/features/useCodeBlockManager.ts +369 -0
  31. package/src/composables/markdown/features/useCodeBlocks.spec.ts +805 -0
  32. package/src/composables/markdown/features/useCodeBlocks.ts +774 -0
  33. package/src/composables/markdown/features/useContextMenu.ts +444 -0
  34. package/src/composables/markdown/features/useFocusTracking.ts +116 -0
  35. package/src/composables/markdown/features/useHeadings.spec.ts +834 -0
  36. package/src/composables/markdown/features/useHeadings.ts +290 -0
  37. package/src/composables/markdown/features/useInlineFormatting.spec.ts +705 -0
  38. package/src/composables/markdown/features/useInlineFormatting.ts +402 -0
  39. package/src/composables/markdown/features/useLineTypeMenu.ts +285 -0
  40. package/src/composables/markdown/features/useLinks.spec.ts +388 -0
  41. package/src/composables/markdown/features/useLinks.ts +374 -0
  42. package/src/composables/markdown/features/useLists.spec.ts +834 -0
  43. package/src/composables/markdown/features/useLists.ts +747 -0
  44. package/src/composables/markdown/features/usePopoverManager.ts +181 -0
  45. package/src/composables/markdown/features/useTables.spec.ts +1601 -0
  46. package/src/composables/markdown/features/useTables.ts +1107 -0
  47. package/src/composables/markdown/index.ts +16 -0
  48. package/src/composables/markdown/useMarkdownEditor.spec.ts +332 -0
  49. package/src/composables/markdown/useMarkdownEditor.ts +1077 -0
  50. package/src/composables/markdown/useMarkdownHotkeys.spec.ts +791 -0
  51. package/src/composables/markdown/useMarkdownHotkeys.ts +266 -0
  52. package/src/composables/markdown/useMarkdownSelection.ts +219 -0
  53. package/src/composables/markdown/useMarkdownSync.ts +549 -0
  54. package/src/composables/useCodeFormat.ts +17 -10
  55. package/src/composables/useCodeViewerEditor.spec.ts +655 -0
  56. package/src/composables/useCodeViewerEditor.ts +174 -20
  57. package/src/helpers/formats/highlightCSS.ts +236 -0
  58. package/src/helpers/formats/highlightHTML.ts +483 -0
  59. package/src/helpers/formats/highlightJavaScript.ts +346 -0
  60. package/src/helpers/formats/highlightSyntax.ts +15 -4
  61. package/src/helpers/formats/index.ts +3 -0
  62. package/src/helpers/formats/markdown/htmlToMarkdown/convertHeadings.ts +41 -0
  63. package/src/helpers/formats/markdown/htmlToMarkdown/index.spec.ts +489 -0
  64. package/src/helpers/formats/markdown/htmlToMarkdown/index.ts +425 -0
  65. package/src/helpers/formats/markdown/index.ts +7 -0
  66. package/src/helpers/formats/markdown/linePatterns.spec.ts +498 -0
  67. package/src/helpers/formats/markdown/linePatterns.ts +172 -0
  68. package/src/styles/danx.scss +3 -3
  69. package/src/styles/index.scss +5 -5
  70. package/src/styles/themes/danx/code.scss +257 -1
  71. package/src/styles/themes/danx/index.scss +10 -10
  72. package/src/styles/themes/danx/markdown.scss +59 -0
  73. package/src/test/helpers/editorTestUtils.spec.ts +296 -0
  74. package/src/test/helpers/editorTestUtils.ts +253 -0
  75. package/src/test/helpers/index.ts +1 -0
  76. package/src/test/highlighters.test.ts +153 -0
  77. package/src/test/setup.test.ts +12 -0
  78. package/src/test/setup.ts +12 -0
  79. package/src/types/widgets.d.ts +2 -2
  80. package/vite.config.js +5 -1
  81. package/vitest.config.ts +19 -0
@@ -0,0 +1,805 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { useCodeBlocks, CURSOR_ANCHOR } from "./useCodeBlocks";
3
+ import { useMarkdownSelection } from "../useMarkdownSelection";
4
+ import { createTestEditor, TestEditorResult } from "../../../test/helpers/editorTestUtils";
5
+ import { htmlToMarkdown } from "../../../helpers/formats/markdown/htmlToMarkdown";
6
+
7
+ describe("useCodeBlocks", () => {
8
+ let editor: TestEditorResult;
9
+ let onContentChange: ReturnType<typeof vi.fn>;
10
+
11
+ afterEach(() => {
12
+ if (editor) {
13
+ editor.destroy();
14
+ }
15
+ });
16
+
17
+ /**
18
+ * Create code blocks composable with test editor
19
+ */
20
+ function createCodeBlocks() {
21
+ const selection = useMarkdownSelection(editor.contentRef);
22
+ return useCodeBlocks({
23
+ contentRef: editor.contentRef,
24
+ selection,
25
+ onContentChange
26
+ });
27
+ }
28
+
29
+ /**
30
+ * Helper to verify code block wrapper structure
31
+ * The new implementation creates wrapper divs with mount points instead of <pre><code>
32
+ */
33
+ function expectCodeBlockWrapper(content: string, language = "") {
34
+ const wrapper = editor.container.querySelector(".code-block-wrapper");
35
+ expect(wrapper).not.toBeNull();
36
+ expect(wrapper?.getAttribute("contenteditable")).toBe("false");
37
+ expect(wrapper?.hasAttribute("data-code-block-id")).toBe(true);
38
+
39
+ const mountPoint = wrapper?.querySelector(".code-viewer-mount-point");
40
+ expect(mountPoint).not.toBeNull();
41
+ expect(mountPoint?.getAttribute("data-content")).toBe(content);
42
+ expect(mountPoint?.getAttribute("data-language")).toBe(language);
43
+ }
44
+
45
+ /**
46
+ * Helper to check if a code block wrapper exists
47
+ */
48
+ function hasCodeBlockWrapper(): boolean {
49
+ return editor.container.querySelector(".code-block-wrapper") !== null;
50
+ }
51
+
52
+ describe("toggleCodeBlock", () => {
53
+ beforeEach(() => {
54
+ onContentChange = vi.fn();
55
+ });
56
+
57
+ it("converts paragraph to code block", () => {
58
+ editor = createTestEditor("<p>Hello world</p>");
59
+ const codeBlocks = createCodeBlocks();
60
+ editor.setCursorInBlock(0, 5);
61
+
62
+ codeBlocks.toggleCodeBlock();
63
+
64
+ expectCodeBlockWrapper("Hello world");
65
+ expect(onContentChange).toHaveBeenCalled();
66
+ });
67
+
68
+ it("converts DIV to code block", () => {
69
+ editor = createTestEditor("<div>Hello world</div>");
70
+ const codeBlocks = createCodeBlocks();
71
+ editor.setCursorInBlock(0, 0);
72
+
73
+ codeBlocks.toggleCodeBlock();
74
+
75
+ expectCodeBlockWrapper("Hello world");
76
+ expect(onContentChange).toHaveBeenCalled();
77
+ });
78
+
79
+ it("converts H1 to code block", () => {
80
+ editor = createTestEditor("<h1>Hello world</h1>");
81
+ const codeBlocks = createCodeBlocks();
82
+ editor.setCursorInBlock(0, 0);
83
+
84
+ codeBlocks.toggleCodeBlock();
85
+
86
+ expectCodeBlockWrapper("Hello world");
87
+ expect(onContentChange).toHaveBeenCalled();
88
+ });
89
+
90
+ it("converts H2 to code block", () => {
91
+ editor = createTestEditor("<h2>Hello world</h2>");
92
+ const codeBlocks = createCodeBlocks();
93
+ editor.setCursorInBlock(0, 0);
94
+
95
+ codeBlocks.toggleCodeBlock();
96
+
97
+ expectCodeBlockWrapper("Hello world");
98
+ expect(onContentChange).toHaveBeenCalled();
99
+ });
100
+
101
+ it("converts code block back to paragraph", () => {
102
+ editor = createTestEditor("<pre><code>Hello world</code></pre>");
103
+ const codeBlocks = createCodeBlocks();
104
+ // Set cursor inside the code element
105
+ const code = editor.container.querySelector("code");
106
+ if (code?.firstChild) {
107
+ editor.setCursor(code.firstChild, 5);
108
+ }
109
+
110
+ codeBlocks.toggleCodeBlock();
111
+
112
+ expect(editor.getHtml()).toBe("<p>Hello world</p>");
113
+ expect(onContentChange).toHaveBeenCalled();
114
+ });
115
+
116
+ it("preserves content when toggling to code block", () => {
117
+ editor = createTestEditor("<p>const x = 1;</p>");
118
+ const codeBlocks = createCodeBlocks();
119
+ editor.setCursorInBlock(0, 0);
120
+
121
+ codeBlocks.toggleCodeBlock();
122
+
123
+ expectCodeBlockWrapper("const x = 1;");
124
+ });
125
+
126
+ it("preserves content when toggling from code block", () => {
127
+ editor = createTestEditor("<pre><code>const x = 1;</code></pre>");
128
+ const codeBlocks = createCodeBlocks();
129
+ const code = editor.container.querySelector("code");
130
+ if (code?.firstChild) {
131
+ editor.setCursor(code.firstChild, 0);
132
+ }
133
+
134
+ codeBlocks.toggleCodeBlock();
135
+
136
+ expect(editor.getHtml()).toBe("<p>const x = 1;</p>");
137
+ });
138
+
139
+ it("handles empty paragraph", () => {
140
+ editor = createTestEditor("<p></p>");
141
+ const codeBlocks = createCodeBlocks();
142
+ const p = editor.getBlock(0);
143
+ if (p) {
144
+ const range = document.createRange();
145
+ range.selectNodeContents(p);
146
+ range.collapse(true);
147
+ const sel = window.getSelection();
148
+ sel?.removeAllRanges();
149
+ sel?.addRange(range);
150
+ }
151
+
152
+ codeBlocks.toggleCodeBlock();
153
+
154
+ expectCodeBlockWrapper("");
155
+ });
156
+
157
+ it("does not convert list items directly", () => {
158
+ editor = createTestEditor("<ul><li>List item</li></ul>");
159
+ const codeBlocks = createCodeBlocks();
160
+ const li = editor.container.querySelector("li");
161
+ if (li?.firstChild) {
162
+ editor.setCursor(li.firstChild, 0);
163
+ }
164
+
165
+ codeBlocks.toggleCodeBlock();
166
+
167
+ // Should remain a list (list items require conversion to paragraph first)
168
+ expect(editor.getHtml()).toBe("<ul><li>List item</li></ul>");
169
+ expect(onContentChange).not.toHaveBeenCalled();
170
+ });
171
+ });
172
+
173
+ describe("checkAndConvertCodeBlockPattern", () => {
174
+ beforeEach(() => {
175
+ onContentChange = vi.fn();
176
+ });
177
+
178
+ it("does not convert just ``` without language (requires language identifier)", () => {
179
+ // Implementation intentionally requires at least one character in language identifier
180
+ // to avoid triggering on just "```" before user finishes typing the language
181
+ editor = createTestEditor("<p>```</p>");
182
+ const codeBlocks = createCodeBlocks();
183
+ editor.setCursorInBlock(0, 3);
184
+
185
+ const result = codeBlocks.checkAndConvertCodeBlockPattern();
186
+
187
+ expect(result).toBe(false);
188
+ expect(editor.getHtml()).toBe("<p>```</p>");
189
+ expect(onContentChange).not.toHaveBeenCalled();
190
+ });
191
+
192
+ it("converts ```javascript to code block with language", () => {
193
+ editor = createTestEditor("<p>```javascript</p>");
194
+ const codeBlocks = createCodeBlocks();
195
+ editor.setCursorInBlock(0, 13);
196
+
197
+ const result = codeBlocks.checkAndConvertCodeBlockPattern();
198
+
199
+ expect(result).toBe(true);
200
+ expectCodeBlockWrapper("", "javascript");
201
+ expect(onContentChange).toHaveBeenCalled();
202
+ });
203
+
204
+ it("converts ```python to code block with language", () => {
205
+ editor = createTestEditor("<p>```python</p>");
206
+ const codeBlocks = createCodeBlocks();
207
+ editor.setCursorInBlock(0, 9);
208
+
209
+ const result = codeBlocks.checkAndConvertCodeBlockPattern();
210
+
211
+ expect(result).toBe(true);
212
+ expectCodeBlockWrapper("", "python");
213
+ });
214
+
215
+ it("converts ```typescript to code block with language", () => {
216
+ editor = createTestEditor("<p>```typescript</p>");
217
+ const codeBlocks = createCodeBlocks();
218
+ editor.setCursorInBlock(0, 13);
219
+
220
+ const result = codeBlocks.checkAndConvertCodeBlockPattern();
221
+
222
+ expect(result).toBe(true);
223
+ expectCodeBlockWrapper("", "typescript");
224
+ });
225
+
226
+ it("returns false when no pattern is present", () => {
227
+ editor = createTestEditor("<p>Hello world</p>");
228
+ const codeBlocks = createCodeBlocks();
229
+ editor.setCursorInBlock(0, 5);
230
+
231
+ const result = codeBlocks.checkAndConvertCodeBlockPattern();
232
+
233
+ expect(result).toBe(false);
234
+ expect(editor.getHtml()).toBe("<p>Hello world</p>");
235
+ expect(onContentChange).not.toHaveBeenCalled();
236
+ });
237
+
238
+ it("returns false for incomplete `` pattern", () => {
239
+ editor = createTestEditor("<p>``</p>");
240
+ const codeBlocks = createCodeBlocks();
241
+ editor.setCursorInBlock(0, 2);
242
+
243
+ const result = codeBlocks.checkAndConvertCodeBlockPattern();
244
+
245
+ expect(result).toBe(false);
246
+ expect(editor.getHtml()).toBe("<p>``</p>");
247
+ });
248
+
249
+ it("does not convert if already in code block", () => {
250
+ editor = createTestEditor("<pre><code>```javascript</code></pre>");
251
+ const codeBlocks = createCodeBlocks();
252
+ const code = editor.container.querySelector("code");
253
+ if (code?.firstChild) {
254
+ editor.setCursor(code.firstChild, 13);
255
+ }
256
+
257
+ const result = codeBlocks.checkAndConvertCodeBlockPattern();
258
+
259
+ expect(result).toBe(false);
260
+ expect(editor.getHtml()).toBe("<pre><code>```javascript</code></pre>");
261
+ expect(onContentChange).not.toHaveBeenCalled();
262
+ });
263
+
264
+ it("converts heading with code fence pattern", () => {
265
+ editor = createTestEditor("<h1>```javascript</h1>");
266
+ const codeBlocks = createCodeBlocks();
267
+ editor.setCursorInBlock(0, 13);
268
+
269
+ const result = codeBlocks.checkAndConvertCodeBlockPattern();
270
+
271
+ // Headings ARE convertible blocks, so this should convert
272
+ expect(result).toBe(true);
273
+ expectCodeBlockWrapper("", "javascript");
274
+ });
275
+
276
+ it("converts DIV elements (browser default)", () => {
277
+ editor = createTestEditor("<div>```rust</div>");
278
+ const codeBlocks = createCodeBlocks();
279
+ editor.setCursorInBlock(0, 7);
280
+
281
+ const result = codeBlocks.checkAndConvertCodeBlockPattern();
282
+
283
+ expect(result).toBe(true);
284
+ expectCodeBlockWrapper("", "rust");
285
+ });
286
+
287
+ it("does not convert list items", () => {
288
+ editor = createTestEditor("<ul><li>```javascript</li></ul>");
289
+ const codeBlocks = createCodeBlocks();
290
+ const li = editor.container.querySelector("li");
291
+ if (li?.firstChild) {
292
+ editor.setCursor(li.firstChild, 13);
293
+ }
294
+
295
+ const result = codeBlocks.checkAndConvertCodeBlockPattern();
296
+
297
+ expect(result).toBe(false);
298
+ expect(editor.getHtml()).toBe("<ul><li>```javascript</li></ul>");
299
+ expect(onContentChange).not.toHaveBeenCalled();
300
+ });
301
+
302
+ it("creates wrapper structure after conversion", () => {
303
+ editor = createTestEditor("<p>```javascript</p>");
304
+ const codeBlocks = createCodeBlocks();
305
+ editor.setCursorInBlock(0, 13);
306
+
307
+ codeBlocks.checkAndConvertCodeBlockPattern();
308
+
309
+ // Verify wrapper structure is created
310
+ expectCodeBlockWrapper("", "javascript");
311
+
312
+ // Verify the wrapper has an id that can be used for mounting
313
+ const wrapper = editor.container.querySelector(".code-block-wrapper");
314
+ expect(wrapper?.getAttribute("data-code-block-id")).toBeTruthy();
315
+ });
316
+
317
+ it("cursor anchor is stripped during markdown conversion", () => {
318
+ editor = createTestEditor("<p>```javascript</p>");
319
+ const codeBlocks = createCodeBlocks();
320
+ editor.setCursorInBlock(0, 13);
321
+
322
+ codeBlocks.checkAndConvertCodeBlockPattern();
323
+
324
+ // Verify the cursor anchor is stripped during markdown conversion
325
+ const markdown = htmlToMarkdown(editor.container);
326
+
327
+ // The markdown should have empty code block (no zero-width space)
328
+ expect(markdown).toBe("```javascript\n\n```");
329
+ });
330
+ });
331
+
332
+ describe("isInCodeBlock", () => {
333
+ beforeEach(() => {
334
+ onContentChange = vi.fn();
335
+ });
336
+
337
+ it("returns true when cursor is in code block", () => {
338
+ editor = createTestEditor("<pre><code>Hello world</code></pre>");
339
+ const codeBlocks = createCodeBlocks();
340
+ const code = editor.container.querySelector("code");
341
+ if (code?.firstChild) {
342
+ editor.setCursor(code.firstChild, 5);
343
+ }
344
+
345
+ expect(codeBlocks.isInCodeBlock()).toBe(true);
346
+ });
347
+
348
+ it("returns false when cursor is in paragraph", () => {
349
+ editor = createTestEditor("<p>Hello world</p>");
350
+ const codeBlocks = createCodeBlocks();
351
+ editor.setCursorInBlock(0, 5);
352
+
353
+ expect(codeBlocks.isInCodeBlock()).toBe(false);
354
+ });
355
+
356
+ it("returns false when cursor is in heading", () => {
357
+ editor = createTestEditor("<h1>Hello world</h1>");
358
+ const codeBlocks = createCodeBlocks();
359
+ editor.setCursorInBlock(0, 5);
360
+
361
+ expect(codeBlocks.isInCodeBlock()).toBe(false);
362
+ });
363
+
364
+ it("returns false when cursor is in list", () => {
365
+ editor = createTestEditor("<ul><li>Hello world</li></ul>");
366
+ const codeBlocks = createCodeBlocks();
367
+ const li = editor.container.querySelector("li");
368
+ if (li?.firstChild) {
369
+ editor.setCursor(li.firstChild, 5);
370
+ }
371
+
372
+ expect(codeBlocks.isInCodeBlock()).toBe(false);
373
+ });
374
+
375
+ it("returns false when no selection", () => {
376
+ editor = createTestEditor("<pre><code>Hello world</code></pre>");
377
+ const codeBlocks = createCodeBlocks();
378
+ window.getSelection()?.removeAllRanges();
379
+
380
+ expect(codeBlocks.isInCodeBlock()).toBe(false);
381
+ });
382
+ });
383
+
384
+ describe("getCurrentCodeBlockLanguage", () => {
385
+ beforeEach(() => {
386
+ onContentChange = vi.fn();
387
+ });
388
+
389
+ it("returns language when in code block with language", () => {
390
+ editor = createTestEditor('<pre><code class="language-javascript">const x = 1;</code></pre>');
391
+ const codeBlocks = createCodeBlocks();
392
+ const code = editor.container.querySelector("code");
393
+ if (code?.firstChild) {
394
+ editor.setCursor(code.firstChild, 0);
395
+ }
396
+
397
+ expect(codeBlocks.getCurrentCodeBlockLanguage()).toBe("javascript");
398
+ });
399
+
400
+ it("returns empty string when in code block without language", () => {
401
+ editor = createTestEditor("<pre><code>const x = 1;</code></pre>");
402
+ const codeBlocks = createCodeBlocks();
403
+ const code = editor.container.querySelector("code");
404
+ if (code?.firstChild) {
405
+ editor.setCursor(code.firstChild, 0);
406
+ }
407
+
408
+ expect(codeBlocks.getCurrentCodeBlockLanguage()).toBe("");
409
+ });
410
+
411
+ it("returns null when not in code block", () => {
412
+ editor = createTestEditor("<p>Hello world</p>");
413
+ const codeBlocks = createCodeBlocks();
414
+ editor.setCursorInBlock(0, 5);
415
+
416
+ expect(codeBlocks.getCurrentCodeBlockLanguage()).toBeNull();
417
+ });
418
+
419
+ it("returns correct language for various languages", () => {
420
+ const languages = ["python", "typescript", "rust", "go", "java", "css", "html"];
421
+
422
+ for (const lang of languages) {
423
+ editor = createTestEditor(`<pre><code class="language-${lang}">code</code></pre>`);
424
+ const codeBlocks = createCodeBlocks();
425
+ const code = editor.container.querySelector("code");
426
+ if (code?.firstChild) {
427
+ editor.setCursor(code.firstChild, 0);
428
+ }
429
+
430
+ expect(codeBlocks.getCurrentCodeBlockLanguage()).toBe(lang);
431
+ editor.destroy();
432
+ }
433
+ });
434
+ });
435
+
436
+ describe("setCodeBlockLanguage", () => {
437
+ beforeEach(() => {
438
+ onContentChange = vi.fn();
439
+ });
440
+
441
+ it("sets language on code block without existing language", () => {
442
+ editor = createTestEditor("<pre><code>const x = 1;</code></pre>");
443
+ const codeBlocks = createCodeBlocks();
444
+ const code = editor.container.querySelector("code");
445
+ if (code?.firstChild) {
446
+ editor.setCursor(code.firstChild, 0);
447
+ }
448
+
449
+ codeBlocks.setCodeBlockLanguage("javascript");
450
+
451
+ expect(editor.getHtml()).toBe('<pre><code class="language-javascript">const x = 1;</code></pre>');
452
+ expect(onContentChange).toHaveBeenCalled();
453
+ });
454
+
455
+ it("replaces existing language", () => {
456
+ editor = createTestEditor('<pre><code class="language-javascript">const x = 1;</code></pre>');
457
+ const codeBlocks = createCodeBlocks();
458
+ const code = editor.container.querySelector("code");
459
+ if (code?.firstChild) {
460
+ editor.setCursor(code.firstChild, 0);
461
+ }
462
+
463
+ codeBlocks.setCodeBlockLanguage("typescript");
464
+
465
+ expect(editor.getHtml()).toBe('<pre><code class="language-typescript">const x = 1;</code></pre>');
466
+ expect(onContentChange).toHaveBeenCalled();
467
+ });
468
+
469
+ it("removes language when set to empty string", () => {
470
+ editor = createTestEditor('<pre><code class="language-javascript">const x = 1;</code></pre>');
471
+ const codeBlocks = createCodeBlocks();
472
+ const code = editor.container.querySelector("code");
473
+ if (code?.firstChild) {
474
+ editor.setCursor(code.firstChild, 0);
475
+ }
476
+
477
+ codeBlocks.setCodeBlockLanguage("");
478
+
479
+ expect(editor.getHtml()).toBe("<pre><code>const x = 1;</code></pre>");
480
+ expect(onContentChange).toHaveBeenCalled();
481
+ });
482
+
483
+ it("does nothing when not in code block", () => {
484
+ editor = createTestEditor("<p>Hello world</p>");
485
+ const codeBlocks = createCodeBlocks();
486
+ editor.setCursorInBlock(0, 5);
487
+
488
+ codeBlocks.setCodeBlockLanguage("javascript");
489
+
490
+ expect(editor.getHtml()).toBe("<p>Hello world</p>");
491
+ expect(onContentChange).not.toHaveBeenCalled();
492
+ });
493
+
494
+ it("preserves other classes when setting language", () => {
495
+ editor = createTestEditor('<pre><code class="highlight other-class">code</code></pre>');
496
+ const codeBlocks = createCodeBlocks();
497
+ const code = editor.container.querySelector("code");
498
+ if (code?.firstChild) {
499
+ editor.setCursor(code.firstChild, 0);
500
+ }
501
+
502
+ codeBlocks.setCodeBlockLanguage("python");
503
+
504
+ const resultCode = editor.container.querySelector("code");
505
+ expect(resultCode?.className).toContain("highlight");
506
+ expect(resultCode?.className).toContain("other-class");
507
+ expect(resultCode?.className).toContain("language-python");
508
+ });
509
+ });
510
+
511
+ describe("handleCodeBlockEnter", () => {
512
+ beforeEach(() => {
513
+ onContentChange = vi.fn();
514
+ });
515
+
516
+ it("returns false when not in a code block", () => {
517
+ editor = createTestEditor("<p>Hello world</p>");
518
+ const codeBlocks = createCodeBlocks();
519
+ editor.setCursorInBlock(0, 5);
520
+
521
+ const handled = codeBlocks.handleCodeBlockEnter();
522
+
523
+ expect(handled).toBe(false);
524
+ expect(onContentChange).not.toHaveBeenCalled();
525
+ });
526
+
527
+ it("inserts newline when Enter is pressed inside code block", () => {
528
+ editor = createTestEditor("<pre><code>line1</code></pre>");
529
+ const codeBlocks = createCodeBlocks();
530
+ const code = editor.container.querySelector("code");
531
+ if (code?.firstChild) {
532
+ editor.setCursor(code.firstChild, 5); // At end of "line1"
533
+ }
534
+
535
+ const handled = codeBlocks.handleCodeBlockEnter();
536
+
537
+ expect(handled).toBe(true);
538
+ // The code block should still exist with a newline inserted
539
+ const codeElement = editor.container.querySelector("code");
540
+ expect(codeElement).not.toBeNull();
541
+ expect(codeElement?.textContent).toContain("\n");
542
+ expect(onContentChange).toHaveBeenCalled();
543
+ });
544
+
545
+ it("inserts newline in middle of code content", () => {
546
+ editor = createTestEditor("<pre><code>HelloWorld</code></pre>");
547
+ const codeBlocks = createCodeBlocks();
548
+ const code = editor.container.querySelector("code");
549
+ if (code?.firstChild) {
550
+ editor.setCursor(code.firstChild, 5); // After "Hello"
551
+ }
552
+
553
+ codeBlocks.handleCodeBlockEnter();
554
+
555
+ const codeElement = editor.container.querySelector("code");
556
+ expect(codeElement?.textContent).toContain("\n");
557
+ expect(onContentChange).toHaveBeenCalled();
558
+ });
559
+
560
+ it("exits code block and creates paragraph after three consecutive Enters at end", () => {
561
+ // Simulate content that already has two newlines at the end (user pressed Enter twice)
562
+ editor = createTestEditor("<pre><code>some code\n\n</code></pre>");
563
+ const codeBlocks = createCodeBlocks();
564
+ const code = editor.container.querySelector("code");
565
+ if (code?.firstChild) {
566
+ // Position cursor at the very end
567
+ editor.setCursor(code.firstChild, code.firstChild.textContent?.length || 0);
568
+ }
569
+
570
+ const handled = codeBlocks.handleCodeBlockEnter();
571
+
572
+ expect(handled).toBe(true);
573
+ // Should have created a paragraph after the code block
574
+ const paragraph = editor.container.querySelector("p");
575
+ expect(paragraph).not.toBeNull();
576
+ // The code block should have the trailing newlines removed
577
+ const codeElement = editor.container.querySelector("code");
578
+ expect(codeElement?.textContent?.endsWith("\n\n")).toBe(false);
579
+ expect(onContentChange).toHaveBeenCalled();
580
+ });
581
+
582
+ it("does not exit code block when double newline is not at cursor position", () => {
583
+ // Content has double newline at end, but cursor is in the middle
584
+ editor = createTestEditor("<pre><code>line1\n\nline2</code></pre>");
585
+ const codeBlocks = createCodeBlocks();
586
+ const code = editor.container.querySelector("code");
587
+ if (code?.firstChild) {
588
+ editor.setCursor(code.firstChild, 5); // After "line1", before first newline
589
+ }
590
+
591
+ const handled = codeBlocks.handleCodeBlockEnter();
592
+
593
+ expect(handled).toBe(true);
594
+ // Should NOT create a paragraph - just insert a newline
595
+ const paragraph = editor.container.querySelector("p");
596
+ expect(paragraph).toBeNull();
597
+ expect(onContentChange).toHaveBeenCalled();
598
+ });
599
+
600
+ it("handles empty code block correctly", () => {
601
+ editor = createTestEditor(`<pre><code>${CURSOR_ANCHOR}</code></pre>`);
602
+ const codeBlocks = createCodeBlocks();
603
+ const code = editor.container.querySelector("code");
604
+ if (code?.firstChild) {
605
+ editor.setCursor(code.firstChild, 1); // After cursor anchor
606
+ }
607
+
608
+ const handled = codeBlocks.handleCodeBlockEnter();
609
+
610
+ expect(handled).toBe(true);
611
+ expect(onContentChange).toHaveBeenCalled();
612
+ });
613
+
614
+ it("preserves code block content when exiting", () => {
615
+ editor = createTestEditor("<pre><code>const x = 1;\nconst y = 2;\n\n</code></pre>");
616
+ const codeBlocks = createCodeBlocks();
617
+ const code = editor.container.querySelector("code");
618
+ if (code?.firstChild) {
619
+ editor.setCursor(code.firstChild, code.firstChild.textContent?.length || 0);
620
+ }
621
+
622
+ codeBlocks.handleCodeBlockEnter();
623
+
624
+ const codeElement = editor.container.querySelector("code");
625
+ // Content should have trailing newlines removed but rest preserved
626
+ expect(codeElement?.textContent).toBe("const x = 1;\nconst y = 2;");
627
+ });
628
+
629
+ it("positions cursor in new paragraph after exiting code block", () => {
630
+ editor = createTestEditor("<pre><code>code\n\n</code></pre>");
631
+ const codeBlocks = createCodeBlocks();
632
+ const code = editor.container.querySelector("code");
633
+ if (code?.firstChild) {
634
+ editor.setCursor(code.firstChild, code.firstChild.textContent?.length || 0);
635
+ }
636
+
637
+ codeBlocks.handleCodeBlockEnter();
638
+
639
+ // Verify cursor is in the new paragraph
640
+ const sel = window.getSelection();
641
+ expect(sel).not.toBeNull();
642
+ expect(sel?.rangeCount).toBe(1);
643
+
644
+ const paragraph = editor.container.querySelector("p");
645
+ expect(paragraph).not.toBeNull();
646
+ });
647
+
648
+ it("handles code block with language class when exiting", () => {
649
+ editor = createTestEditor('<pre><code class="language-javascript">const x = 1;\n\n</code></pre>');
650
+ const codeBlocks = createCodeBlocks();
651
+ const code = editor.container.querySelector("code");
652
+ if (code?.firstChild) {
653
+ editor.setCursor(code.firstChild, code.firstChild.textContent?.length || 0);
654
+ }
655
+
656
+ codeBlocks.handleCodeBlockEnter();
657
+
658
+ // Code block should still have its language class
659
+ const codeElement = editor.container.querySelector("code");
660
+ expect(codeElement?.className).toContain("language-javascript");
661
+ // And paragraph should be created after
662
+ const paragraph = editor.container.querySelector("p");
663
+ expect(paragraph).not.toBeNull();
664
+ });
665
+
666
+ it("adds cursor anchor when inserting newline at end to make trailing newline visible", () => {
667
+ editor = createTestEditor("<pre><code>line1</code></pre>");
668
+ const codeBlocks = createCodeBlocks();
669
+ const code = editor.container.querySelector("code");
670
+ if (code?.firstChild) {
671
+ editor.setCursor(code.firstChild, 5); // At end of "line1"
672
+ }
673
+
674
+ codeBlocks.handleCodeBlockEnter();
675
+
676
+ // The code block should contain a cursor anchor (zero-width space) after the newline
677
+ const codeElement = editor.container.querySelector("code");
678
+ expect(codeElement?.textContent).toBe("line1\n" + CURSOR_ANCHOR);
679
+ });
680
+
681
+ it("removes old cursor anchor and adds new one when pressing Enter at end multiple times", () => {
682
+ // Start with content that already has a cursor anchor from previous Enter
683
+ editor = createTestEditor(`<pre><code>line1\n${CURSOR_ANCHOR}</code></pre>`);
684
+ const codeBlocks = createCodeBlocks();
685
+ const code = editor.container.querySelector("code");
686
+ if (code?.firstChild) {
687
+ // Position cursor after the newline, before the cursor anchor
688
+ editor.setCursor(code.firstChild, 6);
689
+ }
690
+
691
+ codeBlocks.handleCodeBlockEnter();
692
+
693
+ // Should have only one cursor anchor, not accumulate them
694
+ const codeElement = editor.container.querySelector("code");
695
+ const content = codeElement?.textContent || "";
696
+ const anchorCount = (content.match(/\u200B/g) || []).length;
697
+ expect(anchorCount).toBe(1);
698
+ // Content should be line1 + two newlines + cursor anchor
699
+ expect(content).toBe("line1\n\n" + CURSOR_ANCHOR);
700
+ });
701
+
702
+ it("does not add cursor anchor when inserting newline in the middle of code", () => {
703
+ editor = createTestEditor("<pre><code>HelloWorld</code></pre>");
704
+ const codeBlocks = createCodeBlocks();
705
+ const code = editor.container.querySelector("code");
706
+ if (code?.firstChild) {
707
+ editor.setCursor(code.firstChild, 5); // After "Hello"
708
+ }
709
+
710
+ codeBlocks.handleCodeBlockEnter();
711
+
712
+ // Should not have a cursor anchor since we inserted in the middle
713
+ const codeElement = editor.container.querySelector("code");
714
+ const content = codeElement?.textContent || "";
715
+ expect(content).not.toContain(CURSOR_ANCHOR);
716
+ expect(content).toBe("Hello\nWorld");
717
+ });
718
+
719
+ it("positions cursor correctly after inserting newline at end", () => {
720
+ editor = createTestEditor("<pre><code>line1</code></pre>");
721
+ const codeBlocks = createCodeBlocks();
722
+ const code = editor.container.querySelector("code");
723
+ if (code?.firstChild) {
724
+ editor.setCursor(code.firstChild, 5); // At end of "line1"
725
+ }
726
+
727
+ codeBlocks.handleCodeBlockEnter();
728
+
729
+ // Cursor should be positioned after the newline, before the cursor anchor
730
+ const sel = window.getSelection();
731
+ expect(sel).not.toBeNull();
732
+ expect(sel?.rangeCount).toBe(1);
733
+
734
+ const range = sel?.getRangeAt(0);
735
+ // Cursor should be at offset 6 (after "line1\n", before cursor anchor)
736
+ expect(range?.startOffset).toBe(6);
737
+ });
738
+ });
739
+
740
+ describe("edge cases", () => {
741
+ beforeEach(() => {
742
+ onContentChange = vi.fn();
743
+ });
744
+
745
+ it("handles code block with empty code element", () => {
746
+ editor = createTestEditor("<pre><code></code></pre>");
747
+ const codeBlocks = createCodeBlocks();
748
+ const code = editor.container.querySelector("code");
749
+ if (code) {
750
+ const range = document.createRange();
751
+ range.selectNodeContents(code);
752
+ range.collapse(true);
753
+ const sel = window.getSelection();
754
+ sel?.removeAllRanges();
755
+ sel?.addRange(range);
756
+ }
757
+
758
+ codeBlocks.toggleCodeBlock();
759
+
760
+ expect(editor.getHtml()).toBe("<p></p>");
761
+ });
762
+
763
+ it("handles pre without code element", () => {
764
+ editor = createTestEditor("<pre>Hello world</pre>");
765
+ const codeBlocks = createCodeBlocks();
766
+ const pre = editor.container.querySelector("pre");
767
+ if (pre?.firstChild) {
768
+ editor.setCursor(pre.firstChild, 0);
769
+ }
770
+
771
+ // Should still detect as code block
772
+ expect(codeBlocks.isInCodeBlock()).toBe(true);
773
+ });
774
+
775
+ it("handles multiple blocks - only affects current block", () => {
776
+ editor = createTestEditor("<p>First</p><p>Second</p><p>Third</p>");
777
+ const codeBlocks = createCodeBlocks();
778
+ editor.setCursorInBlock(1, 0); // Cursor in second block
779
+
780
+ codeBlocks.toggleCodeBlock();
781
+
782
+ // Verify only the second block was converted
783
+ const html = editor.getHtml();
784
+ expect(html).toContain("<p>First</p>");
785
+ expect(html).toContain("<p>Third</p>");
786
+ expect(html).toContain("code-block-wrapper");
787
+
788
+ // Verify the wrapper has the correct content
789
+ const mountPoint = editor.container.querySelector(".code-viewer-mount-point");
790
+ expect(mountPoint?.getAttribute("data-content")).toBe("Second");
791
+ });
792
+
793
+ it("handles multiline content in code block", () => {
794
+ editor = createTestEditor("<p>Line 1\nLine 2\nLine 3</p>");
795
+ const codeBlocks = createCodeBlocks();
796
+ editor.setCursorInBlock(0, 0);
797
+
798
+ codeBlocks.toggleCodeBlock();
799
+
800
+ // Content should be preserved in the data-content attribute
801
+ const mountPoint = editor.container.querySelector(".code-viewer-mount-point");
802
+ expect(mountPoint?.getAttribute("data-content")).toBe("Line 1\nLine 2\nLine 3");
803
+ });
804
+ });
805
+ });