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,549 @@
1
+ import { onUnmounted, Ref, ref } from "vue";
2
+ import { renderMarkdown } from "../../helpers/formats/markdown";
3
+ import { CodeBlockState } from "./features/useCodeBlocks";
4
+
5
+ /**
6
+ * Options for useMarkdownSync composable
7
+ */
8
+ export interface UseMarkdownSyncOptions {
9
+ contentRef: Ref<HTMLElement | null>;
10
+ onEmitValue: (markdown: string) => void;
11
+ debounceMs?: number;
12
+ /** Optional function to look up code block state by ID */
13
+ getCodeBlockById?: (id: string) => CodeBlockState | undefined;
14
+ /** Optional function to register a new code block in state */
15
+ registerCodeBlock?: (id: string, content: string, language: string) => void;
16
+ }
17
+
18
+ /**
19
+ * Return type for useMarkdownSync composable
20
+ */
21
+ export interface UseMarkdownSyncReturn {
22
+ renderedHtml: Ref<string>;
23
+ isInternalUpdate: Ref<boolean>;
24
+ syncFromMarkdown: (markdown: string) => void;
25
+ syncFromHtml: () => void;
26
+ debouncedSyncFromHtml: () => void;
27
+ }
28
+
29
+ /** Code block state lookup function type */
30
+ type CodeBlockLookup = (id: string) => CodeBlockState | undefined;
31
+
32
+ /** Code block registration function type */
33
+ type CodeBlockRegister = (id: string, content: string, language: string) => void;
34
+
35
+ /**
36
+ * Generate a unique ID for code blocks
37
+ */
38
+ function generateCodeBlockId(): string {
39
+ return `cb-${crypto.randomUUID()}`;
40
+ }
41
+
42
+ /**
43
+ * Convert <pre><code> elements in HTML string to code block wrapper structure.
44
+ * This allows CodeBlockManager to mount CodeViewer instances.
45
+ */
46
+ function convertCodeBlocksToWrappers(html: string, registerCodeBlock?: CodeBlockRegister): string {
47
+ // Parse the HTML
48
+ const temp = document.createElement("div");
49
+ temp.innerHTML = html;
50
+
51
+ // Find all <pre> elements (code blocks)
52
+ const preElements = temp.querySelectorAll("pre");
53
+
54
+ for (const pre of Array.from(preElements)) {
55
+ // Get the code element inside
56
+ const codeElement = pre.querySelector("code");
57
+ if (!codeElement) continue;
58
+
59
+ // Extract content and language
60
+ const content = codeElement.textContent || "";
61
+ const langMatch = codeElement.className.match(/language-(\w+)/);
62
+ const language = langMatch ? langMatch[1] : "";
63
+
64
+ // Generate unique ID
65
+ const id = generateCodeBlockId();
66
+
67
+ // Register in state if callback provided
68
+ if (registerCodeBlock) {
69
+ registerCodeBlock(id, content, language);
70
+ }
71
+
72
+ // Create wrapper structure
73
+ const wrapper = document.createElement("div");
74
+ wrapper.className = "code-block-wrapper";
75
+ wrapper.setAttribute("contenteditable", "false");
76
+ wrapper.setAttribute("data-code-block-id", id);
77
+
78
+ // Create mount point for CodeViewer
79
+ const mountPoint = document.createElement("div");
80
+ mountPoint.className = "code-viewer-mount-point";
81
+ mountPoint.setAttribute("data-content", content);
82
+ mountPoint.setAttribute("data-language", language);
83
+ wrapper.appendChild(mountPoint);
84
+
85
+ // Replace the <pre> with the wrapper
86
+ pre.parentNode?.replaceChild(wrapper, pre);
87
+ }
88
+
89
+ return temp.innerHTML;
90
+ }
91
+
92
+ /**
93
+ * Convert HTML back to markdown
94
+ * This handles the reverse conversion from rendered HTML to markdown source
95
+ */
96
+ function htmlToMarkdown(html: string, getCodeBlockById?: CodeBlockLookup): string {
97
+ // Create a temporary element to parse the HTML
98
+ const temp = document.createElement("div");
99
+ temp.innerHTML = html;
100
+
101
+ return processNode(temp, getCodeBlockById);
102
+ }
103
+
104
+ /**
105
+ * Process a DOM node and convert it to markdown
106
+ */
107
+ function processNode(node: Node, getCodeBlockById?: CodeBlockLookup): string {
108
+ const parts: string[] = [];
109
+
110
+ for (const child of Array.from(node.childNodes)) {
111
+ if (child.nodeType === Node.TEXT_NODE) {
112
+ parts.push(child.textContent || "");
113
+ } else if (child.nodeType === Node.ELEMENT_NODE) {
114
+ const element = child as Element;
115
+ const tagName = element.tagName.toLowerCase();
116
+
117
+ // Check for code block wrapper (non-editable island)
118
+ const codeBlockId = element.getAttribute("data-code-block-id");
119
+ if (codeBlockId && getCodeBlockById) {
120
+ const state = getCodeBlockById(codeBlockId);
121
+ if (state) {
122
+ const lang = state.language || "";
123
+ const content = state.content || "";
124
+ // Strip any trailing zero-width spaces from content
125
+ const cleanContent = content.replace(/\u200B/g, "");
126
+ parts.push(`\`\`\`${lang}\n${cleanContent}\n\`\`\`\n\n`);
127
+ continue;
128
+ }
129
+ }
130
+
131
+ switch (tagName) {
132
+ // Headings
133
+ case "h1":
134
+ parts.push(`# ${getTextContent(element)}\n\n`);
135
+ break;
136
+ case "h2":
137
+ parts.push(`## ${getTextContent(element)}\n\n`);
138
+ break;
139
+ case "h3":
140
+ parts.push(`### ${getTextContent(element)}\n\n`);
141
+ break;
142
+ case "h4":
143
+ parts.push(`#### ${getTextContent(element)}\n\n`);
144
+ break;
145
+ case "h5":
146
+ parts.push(`##### ${getTextContent(element)}\n\n`);
147
+ break;
148
+ case "h6":
149
+ parts.push(`###### ${getTextContent(element)}\n\n`);
150
+ break;
151
+
152
+ // Paragraphs
153
+ case "p":
154
+ parts.push(`${processInlineContent(element)}\n\n`);
155
+ break;
156
+
157
+ // Line breaks
158
+ case "br":
159
+ parts.push(" \n");
160
+ break;
161
+
162
+ // Bold
163
+ case "strong":
164
+ case "b":
165
+ parts.push(`**${processInlineContent(element)}**`);
166
+ break;
167
+
168
+ // Italic
169
+ case "em":
170
+ case "i":
171
+ parts.push(`*${processInlineContent(element)}*`);
172
+ break;
173
+
174
+ // Inline code
175
+ case "code":
176
+ if (element.parentElement?.tagName.toLowerCase() !== "pre") {
177
+ parts.push(`\`${element.textContent || ""}\``);
178
+ } else {
179
+ parts.push(element.textContent || "");
180
+ }
181
+ break;
182
+
183
+ // Code blocks (legacy PRE/CODE structure)
184
+ case "pre": {
185
+ const codeElement = element.querySelector("code");
186
+ const code = codeElement?.textContent || element.textContent || "";
187
+ // Strip zero-width spaces (cursor anchors)
188
+ const cleanCode = code.replace(/\u200B/g, "");
189
+ const langClass = codeElement?.className.match(/language-(\w+)/);
190
+ const lang = langClass ? langClass[1] : "";
191
+ parts.push(`\`\`\`${lang}\n${cleanCode}\n\`\`\`\n\n`);
192
+ break;
193
+ }
194
+
195
+ // Blockquotes
196
+ case "blockquote": {
197
+ const content = processNode(element, getCodeBlockById).trim();
198
+ const quotedLines = content.split("\n").map(line => `> ${line}`).join("\n");
199
+ parts.push(`${quotedLines}\n\n`);
200
+ break;
201
+ }
202
+
203
+ // Unordered lists
204
+ case "ul":
205
+ parts.push(processListItems(element, "-"));
206
+ break;
207
+
208
+ // Ordered lists
209
+ case "ol":
210
+ parts.push(processListItems(element, "1."));
211
+ break;
212
+
213
+ // List items (handled by processListItems)
214
+ case "li":
215
+ parts.push(processInlineContent(element));
216
+ break;
217
+
218
+ // Links
219
+ case "a": {
220
+ const href = element.getAttribute("href") || "";
221
+ const text = processInlineContent(element);
222
+ parts.push(`[${text}](${href})`);
223
+ break;
224
+ }
225
+
226
+ // Images
227
+ case "img": {
228
+ const src = element.getAttribute("src") || "";
229
+ const alt = element.getAttribute("alt") || "";
230
+ parts.push(`![${alt}](${src})`);
231
+ break;
232
+ }
233
+
234
+ // Horizontal rules
235
+ case "hr":
236
+ parts.push("---\n\n");
237
+ break;
238
+
239
+ // Strikethrough
240
+ case "del":
241
+ case "s":
242
+ parts.push(`~~${processInlineContent(element)}~~`);
243
+ break;
244
+
245
+ // Highlight
246
+ case "mark":
247
+ parts.push(`==${processInlineContent(element)}==`);
248
+ break;
249
+
250
+ // Underline (no markdown equivalent, preserve as HTML)
251
+ case "u":
252
+ parts.push(`<u>${processInlineContent(element)}</u>`);
253
+ break;
254
+
255
+ // Superscript
256
+ case "sup":
257
+ parts.push(`^${processInlineContent(element)}^`);
258
+ break;
259
+
260
+ // Subscript
261
+ case "sub":
262
+ parts.push(`~${processInlineContent(element)}~`);
263
+ break;
264
+
265
+ // Divs - could be code block wrappers or regular divs
266
+ case "div": {
267
+ // Already handled code block wrappers above
268
+ const content = processInlineContent(element);
269
+ // Only add paragraph breaks if div has content
270
+ if (content.trim()) {
271
+ parts.push(`${content}\n\n`);
272
+ }
273
+ break;
274
+ }
275
+
276
+ // Spans - inline containers, just process children
277
+ case "span":
278
+ parts.push(processNode(element, getCodeBlockById));
279
+ break;
280
+
281
+ // Tables
282
+ case "table":
283
+ parts.push(processTable(element));
284
+ break;
285
+
286
+ default:
287
+ // Unknown elements - just get text content
288
+ parts.push(processNode(element, getCodeBlockById));
289
+ }
290
+ }
291
+ }
292
+
293
+ return parts.join("");
294
+ }
295
+
296
+ /**
297
+ * Process inline content (text with inline formatting)
298
+ */
299
+ function processInlineContent(element: Element): string {
300
+ const parts: string[] = [];
301
+
302
+ for (const child of Array.from(element.childNodes)) {
303
+ if (child.nodeType === Node.TEXT_NODE) {
304
+ parts.push(child.textContent || "");
305
+ } else if (child.nodeType === Node.ELEMENT_NODE) {
306
+ const el = child as Element;
307
+ const tagName = el.tagName.toLowerCase();
308
+
309
+ switch (tagName) {
310
+ case "strong":
311
+ case "b":
312
+ parts.push(`**${processInlineContent(el)}**`);
313
+ break;
314
+ case "em":
315
+ case "i":
316
+ parts.push(`*${processInlineContent(el)}*`);
317
+ break;
318
+ case "code":
319
+ parts.push(`\`${el.textContent || ""}\``);
320
+ break;
321
+ case "a": {
322
+ const href = el.getAttribute("href") || "";
323
+ parts.push(`[${processInlineContent(el)}](${href})`);
324
+ break;
325
+ }
326
+ case "img": {
327
+ const src = el.getAttribute("src") || "";
328
+ const alt = el.getAttribute("alt") || "";
329
+ parts.push(`![${alt}](${src})`);
330
+ break;
331
+ }
332
+ case "del":
333
+ case "s":
334
+ parts.push(`~~${processInlineContent(el)}~~`);
335
+ break;
336
+ case "mark":
337
+ parts.push(`==${processInlineContent(el)}==`);
338
+ break;
339
+ case "u":
340
+ parts.push(`<u>${processInlineContent(el)}</u>`);
341
+ break;
342
+ case "sup":
343
+ parts.push(`^${processInlineContent(el)}^`);
344
+ break;
345
+ case "sub":
346
+ parts.push(`~${processInlineContent(el)}~`);
347
+ break;
348
+ case "br":
349
+ parts.push(" \n");
350
+ break;
351
+ default:
352
+ parts.push(processInlineContent(el));
353
+ }
354
+ }
355
+ }
356
+
357
+ return parts.join("");
358
+ }
359
+
360
+ /**
361
+ * Process list items
362
+ */
363
+ function processListItems(listElement: Element, marker: string): string {
364
+ const items: string[] = [];
365
+ let index = 1;
366
+
367
+ for (const child of Array.from(listElement.children)) {
368
+ if (child.tagName.toLowerCase() === "li") {
369
+ const prefix = marker === "1." ? `${index}. ` : `${marker} `;
370
+ const content = processInlineContent(child);
371
+
372
+ // Check for nested lists
373
+ const nestedUl = child.querySelector("ul");
374
+ const nestedOl = child.querySelector("ol");
375
+
376
+ if (nestedUl || nestedOl) {
377
+ // Get text content before nested list
378
+ const textParts: string[] = [];
379
+ for (const node of Array.from(child.childNodes)) {
380
+ if (node.nodeType === Node.TEXT_NODE) {
381
+ textParts.push(node.textContent || "");
382
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
383
+ const el = node as Element;
384
+ if (el.tagName.toLowerCase() !== "ul" && el.tagName.toLowerCase() !== "ol") {
385
+ textParts.push(processInlineContent(el));
386
+ }
387
+ }
388
+ }
389
+ items.push(`${prefix}${textParts.join("").trim()}`);
390
+
391
+ // Process nested list with indentation
392
+ if (nestedUl) {
393
+ const nestedItems = processListItems(nestedUl, "-").split("\n").filter(Boolean);
394
+ items.push(...nestedItems.map(item => ` ${item}`));
395
+ }
396
+ if (nestedOl) {
397
+ const nestedItems = processListItems(nestedOl, "1.").split("\n").filter(Boolean);
398
+ items.push(...nestedItems.map(item => ` ${item}`));
399
+ }
400
+ } else {
401
+ items.push(`${prefix}${content}`);
402
+ }
403
+
404
+ index++;
405
+ }
406
+ }
407
+
408
+ return items.join("\n") + "\n\n";
409
+ }
410
+
411
+ /**
412
+ * Process table element
413
+ */
414
+ function processTable(table: Element): string {
415
+ const rows: string[][] = [];
416
+ const alignments: string[] = [];
417
+
418
+ // Process thead
419
+ const thead = table.querySelector("thead");
420
+ if (thead) {
421
+ const headerRow = thead.querySelector("tr");
422
+ if (headerRow) {
423
+ const cells: string[] = [];
424
+ for (const th of Array.from(headerRow.querySelectorAll("th"))) {
425
+ cells.push(processInlineContent(th).trim());
426
+ // Detect alignment from style or class
427
+ const style = th.getAttribute("style") || "";
428
+ if (style.includes("text-align: center")) {
429
+ alignments.push(":---:");
430
+ } else if (style.includes("text-align: right")) {
431
+ alignments.push("---:");
432
+ } else {
433
+ alignments.push("---");
434
+ }
435
+ }
436
+ rows.push(cells);
437
+ }
438
+ }
439
+
440
+ // Process tbody
441
+ const tbody = table.querySelector("tbody") || table;
442
+ for (const tr of Array.from(tbody.querySelectorAll("tr"))) {
443
+ if (thead && tr.parentElement === thead) continue;
444
+ const cells: string[] = [];
445
+ for (const td of Array.from(tr.querySelectorAll("td, th"))) {
446
+ cells.push(processInlineContent(td).trim());
447
+ }
448
+ if (cells.length > 0) {
449
+ rows.push(cells);
450
+ }
451
+ }
452
+
453
+ if (rows.length === 0) return "";
454
+
455
+ // Build markdown table
456
+ const lines: string[] = [];
457
+
458
+ // Header row
459
+ if (rows.length > 0) {
460
+ lines.push(`| ${rows[0].join(" | ")} |`);
461
+ // Separator with alignments
462
+ if (alignments.length > 0) {
463
+ lines.push(`| ${alignments.join(" | ")} |`);
464
+ } else {
465
+ lines.push(`| ${rows[0].map(() => "---").join(" | ")} |`);
466
+ }
467
+ }
468
+
469
+ // Data rows
470
+ for (let i = 1; i < rows.length; i++) {
471
+ lines.push(`| ${rows[i].join(" | ")} |`);
472
+ }
473
+
474
+ return lines.join("\n") + "\n\n";
475
+ }
476
+
477
+ /**
478
+ * Get plain text content of an element
479
+ */
480
+ function getTextContent(element: Element): string {
481
+ return element.textContent?.trim() || "";
482
+ }
483
+
484
+ /**
485
+ * Composable for bidirectional markdown <-> HTML synchronization
486
+ */
487
+ export function useMarkdownSync(options: UseMarkdownSyncOptions): UseMarkdownSyncReturn {
488
+ const { contentRef, onEmitValue, debounceMs = 300, getCodeBlockById, registerCodeBlock } = options;
489
+
490
+ const renderedHtml = ref("");
491
+ // Flag to track when changes originate from the editor itself (vs external prop changes)
492
+ // This prevents cursor jumping when the watch triggers setMarkdown after internal edits
493
+ const isInternalUpdate = ref(false);
494
+ let syncTimeout: ReturnType<typeof setTimeout> | null = null;
495
+
496
+ /**
497
+ * Convert markdown to HTML and update rendered content.
498
+ * Code blocks are converted to wrapper structures for CodeViewer mounting.
499
+ */
500
+ function syncFromMarkdown(markdown: string): void {
501
+ const baseHtml = renderMarkdown(markdown);
502
+ // Convert <pre><code> to code block wrappers so CodeBlockManager can mount CodeViewer
503
+ renderedHtml.value = convertCodeBlocksToWrappers(baseHtml, registerCodeBlock);
504
+ }
505
+
506
+ /**
507
+ * Convert HTML from content element back to markdown and emit
508
+ */
509
+ function syncFromHtml(): void {
510
+ if (!contentRef.value) return;
511
+
512
+ const html = contentRef.value.innerHTML;
513
+ const markdown = htmlToMarkdown(html, getCodeBlockById);
514
+
515
+ // Clean up extra whitespace
516
+ const cleaned = markdown.replace(/\n{3,}/g, "\n\n").trim();
517
+
518
+ // Mark as internal update before emitting to prevent watch from triggering setMarkdown
519
+ isInternalUpdate.value = true;
520
+ onEmitValue(cleaned);
521
+ }
522
+
523
+ /**
524
+ * Debounced version of syncFromHtml for input handling
525
+ */
526
+ function debouncedSyncFromHtml(): void {
527
+ if (syncTimeout) {
528
+ clearTimeout(syncTimeout);
529
+ }
530
+ syncTimeout = setTimeout(() => {
531
+ syncFromHtml();
532
+ }, debounceMs);
533
+ }
534
+
535
+ // Cleanup on unmount
536
+ onUnmounted(() => {
537
+ if (syncTimeout) {
538
+ clearTimeout(syncTimeout);
539
+ }
540
+ });
541
+
542
+ return {
543
+ renderedHtml,
544
+ isInternalUpdate,
545
+ syncFromMarkdown,
546
+ syncFromHtml,
547
+ debouncedSyncFromHtml
548
+ };
549
+ }
@@ -2,7 +2,10 @@ import { computed, ref, Ref } from "vue";
2
2
  import { parse as parseYAML, stringify as yamlStringify } from "yaml";
3
3
  import { fJSON, parseMarkdownJSON, parseMarkdownYAML } from "../helpers/formats/parsers";
4
4
 
5
- export type CodeFormat = "json" | "yaml" | "text" | "markdown";
5
+ // Version for HMR cache busting
6
+ export const USE_CODE_FORMAT_VERSION = "1.0.4";
7
+
8
+ export type CodeFormat = "json" | "yaml" | "text" | "markdown" | "html" | "css" | "javascript";
6
9
 
7
10
  export interface UseCodeFormatOptions {
8
11
  initialFormat?: CodeFormat;
@@ -58,8 +61,8 @@ export function useCodeFormat(options: UseCodeFormatOptions = {}): UseCodeFormat
58
61
  function formatValueToString(value: object | string | null, targetFormat: CodeFormat = format.value): string {
59
62
  if (!value) return "";
60
63
 
61
- // Text and markdown formats - just return as-is
62
- if (targetFormat === "text" || targetFormat === "markdown") {
64
+ // Text, markdown, and code formats (CSS, JavaScript, HTML) - just return as-is
65
+ if (targetFormat === "text" || targetFormat === "markdown" || targetFormat === "css" || targetFormat === "javascript" || targetFormat === "html") {
63
66
  return typeof value === "string" ? value : JSON.stringify(value, null, 2);
64
67
  }
65
68
 
@@ -79,11 +82,12 @@ export function useCodeFormat(options: UseCodeFormatOptions = {}): UseCodeFormat
79
82
  }
80
83
 
81
84
  // Validate string content for a format
85
+ // v2: Added CSS/JavaScript/HTML as always-valid formats
82
86
  function validateContent(content: string, targetFormat: CodeFormat = format.value): boolean {
83
87
  if (!content) return true;
84
88
 
85
- // Text and markdown formats are always valid
86
- if (targetFormat === "text" || targetFormat === "markdown") return true;
89
+ // Text, markdown, and code formats are always valid
90
+ if (targetFormat === "text" || targetFormat === "markdown" || targetFormat === "css" || targetFormat === "javascript" || targetFormat === "html") return true;
87
91
 
88
92
  try {
89
93
  if (targetFormat === "json") {
@@ -98,11 +102,12 @@ export function useCodeFormat(options: UseCodeFormatOptions = {}): UseCodeFormat
98
102
  }
99
103
 
100
104
  // Validate and return error details if invalid
105
+ // v2: Added CSS/JavaScript/HTML as always-valid formats
101
106
  function validateContentWithError(content: string, targetFormat: CodeFormat = format.value): ValidationError | null {
102
107
  if (!content) return null;
103
108
 
104
- // Text and markdown formats are always valid
105
- if (targetFormat === "text" || targetFormat === "markdown") return null;
109
+ // Text, markdown, and code formats are always valid
110
+ if (targetFormat === "text" || targetFormat === "markdown" || targetFormat === "css" || targetFormat === "javascript" || targetFormat === "html") return null;
106
111
 
107
112
  try {
108
113
  if (targetFormat === "json") {
@@ -144,16 +149,18 @@ export function useCodeFormat(options: UseCodeFormatOptions = {}): UseCodeFormat
144
149
 
145
150
  // Initialize with value if provided
146
151
  if (options.initialValue) {
147
- rawContent.value = formatValueToString(options.initialValue, format.value);
152
+ const formatted = formatValueToString(options.initialValue, format.value);
153
+ rawContent.value = formatted;
148
154
  }
149
155
 
150
156
  // Computed: parsed object from raw content
151
157
  const parsedValue = computed(() => parseContent(rawContent.value));
152
158
 
153
159
  // Computed: formatted string
154
- // For text and markdown formats, return rawContent directly without parsing
160
+ // v3: For text, markdown, and code formats (CSS, JavaScript, HTML), return rawContent directly without parsing
155
161
  const formattedContent = computed(() => {
156
- if (format.value === "text" || format.value === "markdown") {
162
+ const isStringFormat = format.value === "text" || format.value === "markdown" || format.value === "css" || format.value === "javascript" || format.value === "html";
163
+ if (isStringFormat) {
157
164
  return rawContent.value;
158
165
  }
159
166
  return formatValueToString(parsedValue.value, format.value);