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,248 @@
1
+ import { Ref } from "vue";
2
+
3
+ /**
4
+ * Options for useBlockquotes composable
5
+ */
6
+ export interface UseBlockquotesOptions {
7
+ contentRef: Ref<HTMLElement | null>;
8
+ onContentChange: () => void;
9
+ }
10
+
11
+ /**
12
+ * Return type for useBlockquotes composable
13
+ */
14
+ export interface UseBlockquotesReturn {
15
+ /** Toggle blockquote on the current block */
16
+ toggleBlockquote: () => void;
17
+ /** Check if cursor is inside a blockquote */
18
+ isInBlockquote: () => boolean;
19
+ }
20
+
21
+ /**
22
+ * Block-level tags that can be wrapped in or unwrapped from blockquotes
23
+ */
24
+ const BLOCK_TAGS = ["P", "H1", "H2", "H3", "H4", "H5", "H6", "DIV"];
25
+
26
+ /**
27
+ * Find the nearest block-level element containing the cursor
28
+ */
29
+ function findCurrentBlock(node: Node | null, contentRef: HTMLElement): Element | null {
30
+ if (!node) return null;
31
+
32
+ let current: Node | null = node;
33
+ while (current && current !== contentRef) {
34
+ if (current.nodeType === Node.ELEMENT_NODE) {
35
+ const element = current as Element;
36
+ if (BLOCK_TAGS.includes(element.tagName) || element.tagName === "BLOCKQUOTE") {
37
+ return element;
38
+ }
39
+ }
40
+ current = current.parentNode;
41
+ }
42
+
43
+ return null;
44
+ }
45
+
46
+ /**
47
+ * Find the blockquote ancestor if one exists
48
+ */
49
+ function findBlockquoteAncestor(node: Node | null, contentRef: HTMLElement): HTMLQuoteElement | null {
50
+ if (!node) return null;
51
+
52
+ let current: Node | null = node;
53
+ while (current && current !== contentRef) {
54
+ if (current.nodeType === Node.ELEMENT_NODE && (current as Element).tagName === "BLOCKQUOTE") {
55
+ return current as HTMLQuoteElement;
56
+ }
57
+ current = current.parentNode;
58
+ }
59
+
60
+ return null;
61
+ }
62
+
63
+ /**
64
+ * Get the cursor offset within a block element's text content
65
+ */
66
+ function getCursorOffset(element: HTMLElement): number {
67
+ const selection = window.getSelection();
68
+ if (!selection || !selection.rangeCount) return 0;
69
+
70
+ const range = selection.getRangeAt(0);
71
+ const preCaretRange = document.createRange();
72
+ preCaretRange.selectNodeContents(element);
73
+ preCaretRange.setEnd(range.startContainer, range.startOffset);
74
+
75
+ return preCaretRange.toString().length;
76
+ }
77
+
78
+ /**
79
+ * Set cursor to a specific offset within an element's text content
80
+ */
81
+ function setCursorOffset(element: HTMLElement, targetOffset: number): void {
82
+ const selection = window.getSelection();
83
+ if (!selection) return;
84
+
85
+ let currentOffset = 0;
86
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
87
+ let node = walker.nextNode();
88
+
89
+ while (node) {
90
+ const nodeLength = node.textContent?.length || 0;
91
+ if (currentOffset + nodeLength >= targetOffset) {
92
+ const range = document.createRange();
93
+ range.setStart(node, targetOffset - currentOffset);
94
+ range.collapse(true);
95
+ selection.removeAllRanges();
96
+ selection.addRange(range);
97
+ return;
98
+ }
99
+ currentOffset += nodeLength;
100
+ node = walker.nextNode();
101
+ }
102
+
103
+ // If offset not found, place cursor at end
104
+ const range = document.createRange();
105
+ range.selectNodeContents(element);
106
+ range.collapse(false);
107
+ selection.removeAllRanges();
108
+ selection.addRange(range);
109
+ }
110
+
111
+ /**
112
+ * Composable for blockquote operations in markdown editor
113
+ */
114
+ export function useBlockquotes(options: UseBlockquotesOptions): UseBlockquotesReturn {
115
+ const { contentRef, onContentChange } = options;
116
+
117
+ /**
118
+ * Check if the cursor is currently inside a blockquote
119
+ */
120
+ function isInBlockquote(): boolean {
121
+ if (!contentRef.value) return false;
122
+
123
+ const selection = window.getSelection();
124
+ if (!selection || !selection.rangeCount) return false;
125
+
126
+ const range = selection.getRangeAt(0);
127
+ return findBlockquoteAncestor(range.startContainer, contentRef.value) !== null;
128
+ }
129
+
130
+ /**
131
+ * Toggle blockquote on the current block
132
+ *
133
+ * Behavior:
134
+ * - If cursor is inside a blockquote: unwrap the block from the blockquote
135
+ * - If cursor is not in a blockquote: wrap the current block in a blockquote
136
+ * - Preserves cursor position after transformation
137
+ */
138
+ function toggleBlockquote(): void {
139
+ if (!contentRef.value) return;
140
+
141
+ const selection = window.getSelection();
142
+ if (!selection || !selection.rangeCount) return;
143
+
144
+ const range = selection.getRangeAt(0);
145
+
146
+ // Check if selection is within our content area
147
+ if (!contentRef.value.contains(range.startContainer)) return;
148
+
149
+ const blockquote = findBlockquoteAncestor(range.startContainer, contentRef.value);
150
+
151
+ if (blockquote) {
152
+ unwrapBlockquote(blockquote);
153
+ } else {
154
+ wrapInBlockquote();
155
+ }
156
+
157
+ onContentChange();
158
+ }
159
+
160
+ /**
161
+ * Unwrap content from a blockquote
162
+ */
163
+ function unwrapBlockquote(blockquote: HTMLQuoteElement): void {
164
+ const parent = blockquote.parentNode;
165
+ if (!parent) return;
166
+
167
+ // Save cursor position
168
+ const selection = window.getSelection();
169
+ let cursorOffset = 0;
170
+ let targetBlock: Element | null = null;
171
+
172
+ if (selection && selection.rangeCount > 0) {
173
+ const range = selection.getRangeAt(0);
174
+ const currentBlock = findCurrentBlock(range.startContainer, contentRef.value!);
175
+
176
+ // If the current block is inside the blockquote, get its offset
177
+ if (currentBlock && blockquote.contains(currentBlock)) {
178
+ targetBlock = currentBlock;
179
+ cursorOffset = getCursorOffset(currentBlock as HTMLElement);
180
+ } else if (currentBlock === blockquote) {
181
+ // Cursor is directly in blockquote text node
182
+ cursorOffset = getCursorOffset(blockquote);
183
+ }
184
+ }
185
+
186
+ // Move all children out of the blockquote
187
+ const children = Array.from(blockquote.childNodes);
188
+ let firstMovedElement: Element | null = null;
189
+
190
+ for (const child of children) {
191
+ const insertedNode = parent.insertBefore(child, blockquote);
192
+ if (!firstMovedElement && insertedNode.nodeType === Node.ELEMENT_NODE) {
193
+ firstMovedElement = insertedNode as Element;
194
+ }
195
+ }
196
+
197
+ // Remove the empty blockquote
198
+ parent.removeChild(blockquote);
199
+
200
+ // Restore cursor position
201
+ if (targetBlock && parent.contains(targetBlock)) {
202
+ setCursorOffset(targetBlock as HTMLElement, cursorOffset);
203
+ } else if (firstMovedElement) {
204
+ setCursorOffset(firstMovedElement as HTMLElement, cursorOffset);
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Wrap the current block in a blockquote
210
+ */
211
+ function wrapInBlockquote(): void {
212
+ if (!contentRef.value) return;
213
+
214
+ const selection = window.getSelection();
215
+ if (!selection || !selection.rangeCount) return;
216
+
217
+ const range = selection.getRangeAt(0);
218
+ const currentBlock = findCurrentBlock(range.startContainer, contentRef.value);
219
+
220
+ if (!currentBlock) return;
221
+
222
+ // Don't wrap if already in a blockquote
223
+ if (currentBlock.tagName === "BLOCKQUOTE") return;
224
+
225
+ // Save cursor position
226
+ const cursorOffset = getCursorOffset(currentBlock as HTMLElement);
227
+
228
+ // Create blockquote and wrap the block
229
+ const blockquote = document.createElement("blockquote");
230
+
231
+ // Insert blockquote before the current block
232
+ const parent = currentBlock.parentNode;
233
+ if (!parent) return;
234
+
235
+ parent.insertBefore(blockquote, currentBlock);
236
+
237
+ // Move the block into the blockquote
238
+ blockquote.appendChild(currentBlock);
239
+
240
+ // Restore cursor position
241
+ setCursorOffset(currentBlock as HTMLElement, cursorOffset);
242
+ }
243
+
244
+ return {
245
+ toggleBlockquote,
246
+ isInBlockquote
247
+ };
248
+ }
@@ -0,0 +1,369 @@
1
+ import { Quasar } from "quasar";
2
+ import { App, computed, createApp, h, nextTick, onUnmounted, ref, Ref, watch } from "vue";
3
+ import CodeViewer from "../../../components/Utility/Code/CodeViewer.vue";
4
+ import { CodeBlockState } from "./useCodeBlocks";
5
+
6
+ /**
7
+ * Options for useCodeBlockManager composable
8
+ */
9
+ export interface UseCodeBlockManagerOptions {
10
+ /** Reference to the contenteditable container */
11
+ contentRef: Ref<HTMLElement | null>;
12
+ /** Reactive map of code block states */
13
+ codeBlocks: Map<string, CodeBlockState>;
14
+ /** Callback to update code block content */
15
+ updateCodeBlockContent: (id: string, content: string) => void;
16
+ /** Callback to update code block language */
17
+ updateCodeBlockLanguage: (id: string, language: string) => void;
18
+ /** Callback when a code block requests to exit (e.g., double-Enter at end) */
19
+ onCodeBlockExit?: (id: string) => void;
20
+ /** Callback when a code block requests to be deleted (e.g., Backspace/Delete on empty) */
21
+ onCodeBlockDelete?: (id: string) => void;
22
+ /** Callback when a code block is mounted */
23
+ onCodeBlockMounted?: (id: string, wrapper: HTMLElement) => void;
24
+ }
25
+
26
+ /**
27
+ * Return type for useCodeBlockManager composable
28
+ */
29
+ export interface UseCodeBlockManagerReturn {
30
+ /** Mount CodeViewer instances for all unmounted code blocks */
31
+ mountCodeViewers: () => void;
32
+ /** Unmount a specific CodeViewer instance */
33
+ unmountCodeViewer: (id: string) => void;
34
+ /** Unmount all CodeViewer instances */
35
+ unmountAllCodeViewers: () => void;
36
+ /** Get mounted instance count */
37
+ getMountedCount: () => number;
38
+ }
39
+
40
+ /**
41
+ * Mounted CodeViewer instance tracking
42
+ */
43
+ interface MountedInstance {
44
+ app: App;
45
+ mountPoint: HTMLElement;
46
+ }
47
+
48
+ /**
49
+ * Map language aliases to CodeViewer format
50
+ */
51
+ function mapLanguageToFormat(language: string): string {
52
+ const formatMap: Record<string, string> = {
53
+ js: "javascript",
54
+ ts: "typescript",
55
+ py: "python",
56
+ rb: "ruby",
57
+ yml: "yaml",
58
+ md: "markdown",
59
+ sh: "bash",
60
+ shell: "bash"
61
+ };
62
+
63
+ return formatMap[language] || language || "text";
64
+ }
65
+
66
+ /**
67
+ * Composable for managing CodeViewer instances within the markdown editor.
68
+ * Handles mounting/unmounting Vue apps for code block "islands".
69
+ */
70
+ export function useCodeBlockManager(options: UseCodeBlockManagerOptions): UseCodeBlockManagerReturn {
71
+ const { contentRef, codeBlocks, updateCodeBlockContent, updateCodeBlockLanguage, onCodeBlockExit, onCodeBlockDelete, onCodeBlockMounted } = options;
72
+
73
+ // Track mounted instances by code block ID
74
+ const mountedInstances = new Map<string, MountedInstance>();
75
+
76
+ // Track watchers for cleanup
77
+ const mountedWatchers = new Map<string, () => void>();
78
+
79
+ // MutationObserver to watch for new code block wrappers
80
+ let observer: MutationObserver | null = null;
81
+
82
+ /**
83
+ * Create and mount a CodeViewer instance for a code block
84
+ */
85
+ function mountCodeViewer(wrapper: HTMLElement): void {
86
+ const id = wrapper.getAttribute("data-code-block-id");
87
+ if (!id) return;
88
+
89
+ // Skip if already mounted
90
+ if (mountedInstances.has(id)) return;
91
+
92
+ const mountPoint = wrapper.querySelector(".code-viewer-mount-point") as HTMLElement;
93
+ if (!mountPoint) return;
94
+
95
+ // Get state from the codeBlocks map
96
+ const state = codeBlocks.get(id);
97
+
98
+ // If no state in map, try to get from data attributes (initial load)
99
+ const initialContent = state?.content ?? mountPoint.getAttribute("data-content") ?? "";
100
+ const initialLanguage = state?.language ?? mountPoint.getAttribute("data-language") ?? "";
101
+
102
+ // Ensure state exists in the map
103
+ if (!state) {
104
+ codeBlocks.set(id, {
105
+ id,
106
+ content: initialContent,
107
+ language: initialLanguage
108
+ });
109
+ }
110
+
111
+ // Create reactive refs for the CodeViewer props
112
+ // These will be updated by watchers when the codeBlocks map changes
113
+ const reactiveContent = ref(initialContent);
114
+ const reactiveLanguage = ref(initialLanguage);
115
+
116
+ // Set up watchers to track changes to this code block's state
117
+ // We need separate watchers for content and language to ensure Vue properly
118
+ // tracks the reactive properties accessed within each getter function
119
+ const stopContentWatcher = watch(
120
+ () => codeBlocks.get(id)?.content,
121
+ (newContent) => {
122
+ if (newContent !== undefined) {
123
+ reactiveContent.value = newContent;
124
+ }
125
+ }
126
+ );
127
+
128
+ const stopLanguageWatcher = watch(
129
+ () => codeBlocks.get(id)?.language,
130
+ (newLanguage) => {
131
+ if (newLanguage !== undefined) {
132
+ reactiveLanguage.value = newLanguage;
133
+ }
134
+ }
135
+ );
136
+
137
+ // Combined cleanup function for both watchers
138
+ const stopWatcher = () => {
139
+ stopContentWatcher();
140
+ stopLanguageWatcher();
141
+ };
142
+
143
+ // Store the watcher cleanup function
144
+ mountedWatchers.set(id, stopWatcher);
145
+
146
+ // Create Vue app for CodeViewer
147
+ const app = createApp({
148
+ setup() {
149
+ // Handle model value updates
150
+ const onUpdateModelValue = (value: object | string | null) => {
151
+ const content = typeof value === "string" ? value : JSON.stringify(value);
152
+ updateCodeBlockContent(id, content);
153
+ };
154
+
155
+ // Handle format/language updates
156
+ const onUpdateFormat = (format: string) => {
157
+ updateCodeBlockLanguage(id, format);
158
+ };
159
+
160
+ // Handle exit (double-Enter at end of code block)
161
+ const onExit = () => {
162
+ if (onCodeBlockExit) {
163
+ onCodeBlockExit(id);
164
+ }
165
+ };
166
+
167
+ // Handle delete (Backspace/Delete on empty code block)
168
+ const onDelete = () => {
169
+ if (onCodeBlockDelete) {
170
+ onCodeBlockDelete(id);
171
+ }
172
+ };
173
+
174
+ // Create a computed for the format to ensure proper reactivity tracking
175
+ // The render function needs a computed that Vue can track for re-renders
176
+ const computedFormat = computed(() => mapLanguageToFormat(reactiveLanguage.value));
177
+
178
+ return () => h(CodeViewer, {
179
+ modelValue: reactiveContent.value,
180
+ format: computedFormat.value,
181
+ canEdit: true,
182
+ editable: true,
183
+ allowAnyLanguage: true,
184
+ class: "code-block-island",
185
+ "onUpdate:modelValue": onUpdateModelValue,
186
+ "onUpdate:format": onUpdateFormat,
187
+ onExit,
188
+ onDelete
189
+ });
190
+ }
191
+ });
192
+
193
+ // Install Quasar on the dynamically created app
194
+ // This is required because createApp() doesn't inherit plugins from the parent app
195
+ app.use(Quasar, { plugins: {} });
196
+
197
+ // Clear mount point content (remove data attributes div structure)
198
+ mountPoint.innerHTML = "";
199
+
200
+ // Mount the app
201
+ app.mount(mountPoint);
202
+
203
+ // Track the instance
204
+ mountedInstances.set(id, { app, mountPoint });
205
+
206
+ // Notify that the code block was mounted (after nextTick to ensure DOM is ready)
207
+ if (onCodeBlockMounted) {
208
+ nextTick(() => {
209
+ onCodeBlockMounted(id, wrapper);
210
+ });
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Unmount a specific CodeViewer instance
216
+ */
217
+ function unmountCodeViewer(id: string): void {
218
+ // Clean up watcher
219
+ const stopWatcher = mountedWatchers.get(id);
220
+ if (stopWatcher) {
221
+ stopWatcher();
222
+ mountedWatchers.delete(id);
223
+ }
224
+
225
+ // Unmount Vue app
226
+ const instance = mountedInstances.get(id);
227
+ if (instance) {
228
+ instance.app.unmount();
229
+ mountedInstances.delete(id);
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Unmount all CodeViewer instances
235
+ */
236
+ function unmountAllCodeViewers(): void {
237
+ // Clean up all watchers
238
+ for (const [id, stopWatcher] of mountedWatchers) {
239
+ stopWatcher();
240
+ mountedWatchers.delete(id);
241
+ }
242
+
243
+ // Unmount all Vue apps
244
+ for (const [id, instance] of mountedInstances) {
245
+ instance.app.unmount();
246
+ mountedInstances.delete(id);
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Mount CodeViewer instances for all unmounted code blocks
252
+ */
253
+ function mountCodeViewers(): void {
254
+ if (!contentRef.value) return;
255
+
256
+ const wrappers = contentRef.value.querySelectorAll("[data-code-block-id]");
257
+ wrappers.forEach((wrapper) => {
258
+ mountCodeViewer(wrapper as HTMLElement);
259
+ });
260
+ }
261
+
262
+ /**
263
+ * Get mounted instance count
264
+ */
265
+ function getMountedCount(): number {
266
+ return mountedInstances.size;
267
+ }
268
+
269
+ /**
270
+ * Handle mutations to the content element
271
+ */
272
+ function handleMutations(mutations: MutationRecord[]): void {
273
+ for (const mutation of mutations) {
274
+ // Check for added nodes that are code block wrappers
275
+ for (const node of Array.from(mutation.addedNodes)) {
276
+ if (node.nodeType === Node.ELEMENT_NODE) {
277
+ const element = node as HTMLElement;
278
+
279
+ // Check if the added node is a code block wrapper
280
+ if (element.hasAttribute && element.hasAttribute("data-code-block-id")) {
281
+ nextTick(() => mountCodeViewer(element));
282
+ }
283
+
284
+ // Check children for code block wrappers
285
+ const childWrappers = element.querySelectorAll?.("[data-code-block-id]");
286
+ if (childWrappers && childWrappers.length > 0) {
287
+ childWrappers.forEach((wrapper) => {
288
+ nextTick(() => mountCodeViewer(wrapper as HTMLElement));
289
+ });
290
+ }
291
+ }
292
+ }
293
+
294
+ // Check for removed nodes that were code block wrappers
295
+ for (const node of Array.from(mutation.removedNodes)) {
296
+ if (node.nodeType === Node.ELEMENT_NODE) {
297
+ const element = node as HTMLElement;
298
+
299
+ // Check if the removed node is a code block wrapper
300
+ const id = element.getAttribute?.("data-code-block-id");
301
+ if (id) {
302
+ unmountCodeViewer(id);
303
+ }
304
+
305
+ // Check children for code block wrappers
306
+ const childWrappers = element.querySelectorAll?.("[data-code-block-id]");
307
+ if (childWrappers) {
308
+ childWrappers.forEach((wrapper) => {
309
+ const childId = wrapper.getAttribute("data-code-block-id");
310
+ if (childId) {
311
+ unmountCodeViewer(childId);
312
+ }
313
+ });
314
+ }
315
+ }
316
+ }
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Start observing for DOM changes
322
+ */
323
+ function startObserver(): void {
324
+ if (!contentRef.value || observer) return;
325
+
326
+ observer = new MutationObserver(handleMutations);
327
+ observer.observe(contentRef.value, {
328
+ childList: true,
329
+ subtree: true
330
+ });
331
+ }
332
+
333
+ /**
334
+ * Stop observing DOM changes
335
+ */
336
+ function stopObserver(): void {
337
+ if (observer) {
338
+ observer.disconnect();
339
+ observer = null;
340
+ }
341
+ }
342
+
343
+ // Watch for contentRef changes to set up/tear down observer
344
+ watch(contentRef, (newRef, oldRef) => {
345
+ if (oldRef && !newRef) {
346
+ // Content ref was removed - clean up
347
+ stopObserver();
348
+ unmountAllCodeViewers();
349
+ } else if (newRef && !oldRef) {
350
+ // Content ref was added - set up
351
+ startObserver();
352
+ // Mount any existing code blocks
353
+ nextTick(() => mountCodeViewers());
354
+ }
355
+ }, { immediate: true });
356
+
357
+ // Clean up on unmount
358
+ onUnmounted(() => {
359
+ stopObserver();
360
+ unmountAllCodeViewers();
361
+ });
362
+
363
+ return {
364
+ mountCodeViewers,
365
+ unmountCodeViewer,
366
+ unmountAllCodeViewers,
367
+ getMountedCount
368
+ };
369
+ }