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,27 @@
1
+ export type LineType = "paragraph" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "ul" | "ol" | "code" | "blockquote";
2
+
3
+ export interface LineTypeOption {
4
+ value: LineType;
5
+ label: string;
6
+ icon: string;
7
+ shortcut: string;
8
+ }
9
+
10
+ export type ContextMenuContext = "table" | "list" | "code" | "text";
11
+
12
+ export interface ContextMenuItem {
13
+ id: string;
14
+ label: string;
15
+ icon?: string;
16
+ shortcut?: string;
17
+ action?: () => void; // Optional - not needed if has children
18
+ disabled?: boolean;
19
+ children?: ContextMenuItem[]; // For nested submenus
20
+ divider?: boolean; // For visual dividers between items
21
+ }
22
+
23
+ export interface ContextMenuGroup {
24
+ id: string;
25
+ label: string;
26
+ items: ContextMenuItem[];
27
+ }
@@ -5,6 +5,7 @@ export * from "./Dialogs";
5
5
  export * from "./Files";
6
6
  export * from "./Formats";
7
7
  export * from "./Layouts";
8
+ export * from "./Markdown";
8
9
  export * from "./Popovers";
9
10
  export * from "./Tabs";
10
11
  export * from "./Tools";
@@ -7,3 +7,4 @@ export * from "./useKeyboardNavigation";
7
7
  export * from "./useThumbnailScroll";
8
8
  export * from "./useTranscodeLoader";
9
9
  export * from "./useVirtualCarousel";
10
+ export * from "./markdown";
@@ -0,0 +1,428 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { useBlockquotes } from "./useBlockquotes";
3
+ import { createTestEditor, TestEditorResult } from "../../../test/helpers/editorTestUtils";
4
+
5
+ describe("useBlockquotes", () => {
6
+ let editor: TestEditorResult;
7
+ let onContentChange: ReturnType<typeof vi.fn>;
8
+
9
+ afterEach(() => {
10
+ if (editor) {
11
+ editor.destroy();
12
+ }
13
+ });
14
+
15
+ function createBlockquotes() {
16
+ return useBlockquotes({
17
+ contentRef: editor.contentRef,
18
+ onContentChange
19
+ });
20
+ }
21
+
22
+ describe("toggleBlockquote", () => {
23
+ describe("wrapping in blockquote", () => {
24
+ beforeEach(() => {
25
+ onContentChange = vi.fn();
26
+ });
27
+
28
+ it("wraps paragraph in blockquote", () => {
29
+ editor = createTestEditor("<p>Hello world</p>");
30
+ const blockquotes = createBlockquotes();
31
+ editor.setCursorInBlock(0, 5);
32
+
33
+ blockquotes.toggleBlockquote();
34
+
35
+ expect(editor.getHtml()).toBe("<blockquote><p>Hello world</p></blockquote>");
36
+ expect(onContentChange).toHaveBeenCalled();
37
+ });
38
+
39
+ it("wraps h1 in blockquote", () => {
40
+ editor = createTestEditor("<h1>Hello world</h1>");
41
+ const blockquotes = createBlockquotes();
42
+ editor.setCursorInBlock(0, 0);
43
+
44
+ blockquotes.toggleBlockquote();
45
+
46
+ expect(editor.getHtml()).toBe("<blockquote><h1>Hello world</h1></blockquote>");
47
+ expect(onContentChange).toHaveBeenCalled();
48
+ });
49
+
50
+ it("wraps h2 in blockquote", () => {
51
+ editor = createTestEditor("<h2>Test heading</h2>");
52
+ const blockquotes = createBlockquotes();
53
+ editor.setCursorInBlock(0, 0);
54
+
55
+ blockquotes.toggleBlockquote();
56
+
57
+ expect(editor.getHtml()).toBe("<blockquote><h2>Test heading</h2></blockquote>");
58
+ });
59
+
60
+ it("wraps div in blockquote", () => {
61
+ editor = createTestEditor("<div>Div content</div>");
62
+ const blockquotes = createBlockquotes();
63
+ editor.setCursorInBlock(0, 0);
64
+
65
+ blockquotes.toggleBlockquote();
66
+
67
+ expect(editor.getHtml()).toBe("<blockquote><div>Div content</div></blockquote>");
68
+ });
69
+
70
+ it("preserves cursor position after wrapping", () => {
71
+ editor = createTestEditor("<p>Hello world</p>");
72
+ const blockquotes = createBlockquotes();
73
+ editor.setCursorInBlock(0, 5);
74
+
75
+ blockquotes.toggleBlockquote();
76
+
77
+ // Verify cursor is still at offset 5
78
+ const sel = window.getSelection();
79
+ expect(sel).not.toBeNull();
80
+ expect(sel!.rangeCount).toBeGreaterThan(0);
81
+ });
82
+
83
+ it("only wraps current block, not siblings", () => {
84
+ editor = createTestEditor("<p>First</p><p>Second</p><p>Third</p>");
85
+ const blockquotes = createBlockquotes();
86
+ editor.setCursorInBlock(1, 0); // Cursor in second paragraph
87
+
88
+ blockquotes.toggleBlockquote();
89
+
90
+ expect(editor.getHtml()).toBe("<p>First</p><blockquote><p>Second</p></blockquote><p>Third</p>");
91
+ });
92
+ });
93
+
94
+ describe("unwrapping from blockquote", () => {
95
+ beforeEach(() => {
96
+ onContentChange = vi.fn();
97
+ });
98
+
99
+ it("unwraps paragraph from blockquote", () => {
100
+ editor = createTestEditor("<blockquote><p>Hello world</p></blockquote>");
101
+ const blockquotes = createBlockquotes();
102
+ // Set cursor inside the paragraph within blockquote
103
+ const p = editor.container.querySelector("blockquote p");
104
+ if (p?.firstChild) {
105
+ editor.setCursor(p.firstChild, 5);
106
+ }
107
+
108
+ blockquotes.toggleBlockquote();
109
+
110
+ expect(editor.getHtml()).toBe("<p>Hello world</p>");
111
+ expect(onContentChange).toHaveBeenCalled();
112
+ });
113
+
114
+ it("unwraps heading from blockquote", () => {
115
+ editor = createTestEditor("<blockquote><h1>Heading</h1></blockquote>");
116
+ const blockquotes = createBlockquotes();
117
+ const h1 = editor.container.querySelector("blockquote h1");
118
+ if (h1?.firstChild) {
119
+ editor.setCursor(h1.firstChild, 0);
120
+ }
121
+
122
+ blockquotes.toggleBlockquote();
123
+
124
+ expect(editor.getHtml()).toBe("<h1>Heading</h1>");
125
+ });
126
+
127
+ it("preserves cursor position after unwrapping", () => {
128
+ editor = createTestEditor("<blockquote><p>Hello world</p></blockquote>");
129
+ const blockquotes = createBlockquotes();
130
+ const p = editor.container.querySelector("blockquote p");
131
+ if (p?.firstChild) {
132
+ editor.setCursor(p.firstChild, 5);
133
+ }
134
+
135
+ blockquotes.toggleBlockquote();
136
+
137
+ const sel = window.getSelection();
138
+ expect(sel).not.toBeNull();
139
+ expect(sel!.rangeCount).toBeGreaterThan(0);
140
+ });
141
+
142
+ it("unwraps only the blockquote, keeps other blocks", () => {
143
+ editor = createTestEditor("<p>Before</p><blockquote><p>Quoted</p></blockquote><p>After</p>");
144
+ const blockquotes = createBlockquotes();
145
+ const quotedP = editor.container.querySelector("blockquote p");
146
+ if (quotedP?.firstChild) {
147
+ editor.setCursor(quotedP.firstChild, 0);
148
+ }
149
+
150
+ blockquotes.toggleBlockquote();
151
+
152
+ expect(editor.getHtml()).toBe("<p>Before</p><p>Quoted</p><p>After</p>");
153
+ });
154
+ });
155
+
156
+ describe("with inline formatting preserved", () => {
157
+ beforeEach(() => {
158
+ onContentChange = vi.fn();
159
+ });
160
+
161
+ it("preserves bold formatting when wrapping", () => {
162
+ editor = createTestEditor("<p>Hello <strong>bold</strong> world</p>");
163
+ const blockquotes = createBlockquotes();
164
+ editor.setCursorInBlock(0, 0);
165
+
166
+ blockquotes.toggleBlockquote();
167
+
168
+ expect(editor.getHtml()).toBe("<blockquote><p>Hello <strong>bold</strong> world</p></blockquote>");
169
+ });
170
+
171
+ it("preserves italic formatting when wrapping", () => {
172
+ editor = createTestEditor("<p>Hello <em>italic</em> world</p>");
173
+ const blockquotes = createBlockquotes();
174
+ editor.setCursorInBlock(0, 0);
175
+
176
+ blockquotes.toggleBlockquote();
177
+
178
+ expect(editor.getHtml()).toBe("<blockquote><p>Hello <em>italic</em> world</p></blockquote>");
179
+ });
180
+
181
+ it("preserves nested formatting when wrapping", () => {
182
+ editor = createTestEditor("<p>Hello <strong><em>bold italic</em></strong> world</p>");
183
+ const blockquotes = createBlockquotes();
184
+ editor.setCursorInBlock(0, 0);
185
+
186
+ blockquotes.toggleBlockquote();
187
+
188
+ expect(editor.getHtml()).toBe("<blockquote><p>Hello <strong><em>bold italic</em></strong> world</p></blockquote>");
189
+ });
190
+
191
+ it("preserves bold formatting when unwrapping", () => {
192
+ editor = createTestEditor("<blockquote><p>Hello <strong>bold</strong> world</p></blockquote>");
193
+ const blockquotes = createBlockquotes();
194
+ const p = editor.container.querySelector("blockquote p");
195
+ if (p?.firstChild) {
196
+ editor.setCursor(p.firstChild, 0);
197
+ }
198
+
199
+ blockquotes.toggleBlockquote();
200
+
201
+ expect(editor.getHtml()).toBe("<p>Hello <strong>bold</strong> world</p>");
202
+ });
203
+
204
+ it("preserves link formatting when wrapping", () => {
205
+ editor = createTestEditor("<p>Hello <a href=\"https://example.com\">link</a> world</p>");
206
+ const blockquotes = createBlockquotes();
207
+ editor.setCursorInBlock(0, 0);
208
+
209
+ blockquotes.toggleBlockquote();
210
+
211
+ expect(editor.getHtml()).toBe("<blockquote><p>Hello <a href=\"https://example.com\">link</a> world</p></blockquote>");
212
+ });
213
+
214
+ it("preserves code formatting when wrapping", () => {
215
+ editor = createTestEditor("<p>Hello <code>code</code> world</p>");
216
+ const blockquotes = createBlockquotes();
217
+ editor.setCursorInBlock(0, 0);
218
+
219
+ blockquotes.toggleBlockquote();
220
+
221
+ expect(editor.getHtml()).toBe("<blockquote><p>Hello <code>code</code> world</p></blockquote>");
222
+ });
223
+ });
224
+ });
225
+
226
+ describe("isInBlockquote", () => {
227
+ beforeEach(() => {
228
+ onContentChange = vi.fn();
229
+ });
230
+
231
+ it("returns false when cursor is in plain paragraph", () => {
232
+ editor = createTestEditor("<p>Hello world</p>");
233
+ const blockquotes = createBlockquotes();
234
+ editor.setCursorInBlock(0, 5);
235
+
236
+ expect(blockquotes.isInBlockquote()).toBe(false);
237
+ });
238
+
239
+ it("returns true when cursor is inside blockquote", () => {
240
+ editor = createTestEditor("<blockquote><p>Hello world</p></blockquote>");
241
+ const blockquotes = createBlockquotes();
242
+ const p = editor.container.querySelector("blockquote p");
243
+ if (p?.firstChild) {
244
+ editor.setCursor(p.firstChild, 5);
245
+ }
246
+
247
+ expect(blockquotes.isInBlockquote()).toBe(true);
248
+ });
249
+
250
+ it("returns false when cursor is outside blockquote", () => {
251
+ editor = createTestEditor("<p>Outside</p><blockquote><p>Inside</p></blockquote>");
252
+ const blockquotes = createBlockquotes();
253
+ editor.setCursorInBlock(0, 0); // Cursor in first paragraph
254
+
255
+ expect(blockquotes.isInBlockquote()).toBe(false);
256
+ });
257
+
258
+ it("returns true for nested content inside blockquote", () => {
259
+ editor = createTestEditor("<blockquote><p><strong>Bold text</strong></p></blockquote>");
260
+ const blockquotes = createBlockquotes();
261
+ const strong = editor.container.querySelector("blockquote strong");
262
+ if (strong?.firstChild) {
263
+ editor.setCursor(strong.firstChild, 2);
264
+ }
265
+
266
+ expect(blockquotes.isInBlockquote()).toBe(true);
267
+ });
268
+
269
+ it("returns false when contentRef is null", () => {
270
+ editor = createTestEditor("<p>Hello world</p>");
271
+ const { isInBlockquote } = useBlockquotes({
272
+ contentRef: { value: null },
273
+ onContentChange
274
+ });
275
+
276
+ expect(isInBlockquote()).toBe(false);
277
+ });
278
+
279
+ it("returns false when no selection exists", () => {
280
+ editor = createTestEditor("<blockquote><p>Hello world</p></blockquote>");
281
+ const blockquotes = createBlockquotes();
282
+ window.getSelection()?.removeAllRanges();
283
+
284
+ expect(blockquotes.isInBlockquote()).toBe(false);
285
+ });
286
+ });
287
+
288
+ describe("edge cases", () => {
289
+ beforeEach(() => {
290
+ onContentChange = vi.fn();
291
+ });
292
+
293
+ it("does nothing when contentRef is null", () => {
294
+ editor = createTestEditor("<p>Hello world</p>");
295
+ const { toggleBlockquote } = useBlockquotes({
296
+ contentRef: { value: null },
297
+ onContentChange
298
+ });
299
+
300
+ toggleBlockquote();
301
+
302
+ expect(editor.getHtml()).toBe("<p>Hello world</p>");
303
+ expect(onContentChange).not.toHaveBeenCalled();
304
+ });
305
+
306
+ it("does nothing when selection is outside content area", () => {
307
+ editor = createTestEditor("<p>Hello world</p>");
308
+ const blockquotes = createBlockquotes();
309
+
310
+ // Create a selection outside the editor
311
+ const externalDiv = document.createElement("div");
312
+ externalDiv.textContent = "External text";
313
+ document.body.appendChild(externalDiv);
314
+
315
+ const range = document.createRange();
316
+ range.selectNodeContents(externalDiv);
317
+ const sel = window.getSelection();
318
+ sel?.removeAllRanges();
319
+ sel?.addRange(range);
320
+
321
+ blockquotes.toggleBlockquote();
322
+
323
+ expect(editor.getHtml()).toBe("<p>Hello world</p>");
324
+ expect(onContentChange).not.toHaveBeenCalled();
325
+
326
+ externalDiv.remove();
327
+ });
328
+
329
+ it("does nothing when no selection exists", () => {
330
+ editor = createTestEditor("<p>Hello world</p>");
331
+ const blockquotes = createBlockquotes();
332
+ window.getSelection()?.removeAllRanges();
333
+
334
+ blockquotes.toggleBlockquote();
335
+
336
+ expect(editor.getHtml()).toBe("<p>Hello world</p>");
337
+ expect(onContentChange).not.toHaveBeenCalled();
338
+ });
339
+
340
+ it("handles empty paragraph", () => {
341
+ editor = createTestEditor("<p></p>");
342
+ const blockquotes = createBlockquotes();
343
+ const p = editor.getBlock(0);
344
+ if (p) {
345
+ const range = document.createRange();
346
+ range.selectNodeContents(p);
347
+ range.collapse(true);
348
+ const sel = window.getSelection();
349
+ sel?.removeAllRanges();
350
+ sel?.addRange(range);
351
+ }
352
+
353
+ blockquotes.toggleBlockquote();
354
+
355
+ expect(editor.getHtml()).toBe("<blockquote><p></p></blockquote>");
356
+ });
357
+
358
+ it("handles whitespace-only content", () => {
359
+ editor = createTestEditor("<p> </p>");
360
+ const blockquotes = createBlockquotes();
361
+ editor.setCursorInBlock(0, 1);
362
+
363
+ blockquotes.toggleBlockquote();
364
+
365
+ expect(editor.getHtml()).toBe("<blockquote><p> </p></blockquote>");
366
+ });
367
+ });
368
+
369
+ describe("round-trip toggle", () => {
370
+ beforeEach(() => {
371
+ onContentChange = vi.fn();
372
+ });
373
+
374
+ it("toggle wrap then unwrap returns to original", () => {
375
+ editor = createTestEditor("<p>Hello world</p>");
376
+ const blockquotes = createBlockquotes();
377
+ editor.setCursorInBlock(0, 5);
378
+
379
+ // First toggle - wrap
380
+ blockquotes.toggleBlockquote();
381
+ expect(editor.getHtml()).toBe("<blockquote><p>Hello world</p></blockquote>");
382
+
383
+ // Set cursor again inside the blockquote
384
+ const p = editor.container.querySelector("blockquote p");
385
+ if (p?.firstChild) {
386
+ editor.setCursor(p.firstChild, 5);
387
+ }
388
+
389
+ // Second toggle - unwrap
390
+ blockquotes.toggleBlockquote();
391
+ expect(editor.getHtml()).toBe("<p>Hello world</p>");
392
+ });
393
+
394
+ it("toggle unwrap then wrap returns to original", () => {
395
+ editor = createTestEditor("<blockquote><p>Hello world</p></blockquote>");
396
+ const blockquotes = createBlockquotes();
397
+ const p = editor.container.querySelector("blockquote p");
398
+ if (p?.firstChild) {
399
+ editor.setCursor(p.firstChild, 5);
400
+ }
401
+
402
+ // First toggle - unwrap
403
+ blockquotes.toggleBlockquote();
404
+ expect(editor.getHtml()).toBe("<p>Hello world</p>");
405
+
406
+ // Set cursor in the unwrapped paragraph
407
+ editor.setCursorInBlock(0, 5);
408
+
409
+ // Second toggle - wrap
410
+ blockquotes.toggleBlockquote();
411
+ expect(editor.getHtml()).toBe("<blockquote><p>Hello world</p></blockquote>");
412
+ });
413
+ });
414
+
415
+ describe("return type", () => {
416
+ beforeEach(() => {
417
+ onContentChange = vi.fn();
418
+ editor = createTestEditor("<p>test</p>");
419
+ });
420
+
421
+ it("returns both functions", () => {
422
+ const blockquotes = createBlockquotes();
423
+
424
+ expect(typeof blockquotes.toggleBlockquote).toBe("function");
425
+ expect(typeof blockquotes.isInBlockquote).toBe("function");
426
+ });
427
+ });
428
+ });