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
@@ -10,6 +10,14 @@ export interface UseCodeViewerEditorOptions {
10
10
  editable: Ref<boolean>;
11
11
  onEmitModelValue: (value: object | string | null) => void;
12
12
  onEmitEditable: (editable: boolean) => void;
13
+ /** Callback when format changes (e.g., cycling languages) */
14
+ onEmitFormat?: (format: CodeFormat) => void;
15
+ /** Callback when user wants to exit the code block (Ctrl+Enter) */
16
+ onExit?: () => void;
17
+ /** Callback when user wants to delete the code block (Backspace/Delete on empty) */
18
+ onDelete?: () => void;
19
+ /** Callback when user wants to open the language search panel (Ctrl+Alt+Shift+L) */
20
+ onOpenLanguageSearch?: () => void;
13
21
  }
14
22
 
15
23
  export interface UseCodeViewerEditorReturn {
@@ -167,11 +175,27 @@ function getSmartIndent(lineInfo: { indent: string; lineContent: string }, forma
167
175
  return indent;
168
176
  }
169
177
 
178
+ /**
179
+ * Get available formats that can be cycled through based on current format.
180
+ * YAML/JSON formats cycle between each other only.
181
+ * Text/Markdown formats cycle between each other only.
182
+ * Other formats don't cycle.
183
+ */
184
+ function getAvailableFormats(format: CodeFormat): CodeFormat[] {
185
+ if (format === "json" || format === "yaml") {
186
+ return ["yaml", "json"];
187
+ }
188
+ if (format === "text" || format === "markdown") {
189
+ return ["text", "markdown"];
190
+ }
191
+ return [format];
192
+ }
193
+
170
194
  /**
171
195
  * Composable for CodeViewer editor functionality
172
196
  */
173
197
  export function useCodeViewerEditor(options: UseCodeViewerEditorOptions): UseCodeViewerEditorReturn {
174
- const { codeRef, codeFormat, currentFormat, canEdit, editable, onEmitModelValue, onEmitEditable } = options;
198
+ const { codeRef, codeFormat, currentFormat, canEdit, editable, onEmitModelValue, onEmitEditable, onEmitFormat, onExit, onDelete, onOpenLanguageSearch } = options;
175
199
 
176
200
  // Debounce timeout handles
177
201
  let validationTimeout: ReturnType<typeof setTimeout> | null = null;
@@ -231,10 +255,18 @@ export function useCodeViewerEditor(options: UseCodeViewerEditorOptions): UseCod
231
255
  }
232
256
  }
233
257
 
234
- // Update editing content when format changes
258
+ // Update editing content when format changes and re-apply syntax highlighting
235
259
  function updateEditingContentOnFormatChange(): void {
236
260
  if (isEditing.value) {
237
261
  editingContent.value = codeFormat.formattedContent.value;
262
+ // Update the cached highlighted content with new format
263
+ cachedHighlightedContent.value = highlightSyntax(editingContent.value, { format: currentFormat.value });
264
+ // Re-apply syntax highlighting with new format in the DOM
265
+ nextTick(() => {
266
+ if (codeRef.value) {
267
+ codeRef.value.innerHTML = cachedHighlightedContent.value;
268
+ }
269
+ });
238
270
  }
239
271
  }
240
272
 
@@ -256,6 +288,10 @@ export function useCodeViewerEditor(options: UseCodeViewerEditorOptions): UseCod
256
288
  highlightTimeout = setTimeout(() => {
257
289
  if (!codeRef.value || !isEditing.value) return;
258
290
 
291
+ // Check if document has active focus on the codeRef before replacing innerHTML
292
+ const activeElement = document.activeElement;
293
+ const hasFocus = activeElement === codeRef.value || codeRef.value.contains(activeElement);
294
+
259
295
  // Save cursor position
260
296
  const cursorOffset = getCursorOffset(codeRef.value);
261
297
 
@@ -264,6 +300,12 @@ export function useCodeViewerEditor(options: UseCodeViewerEditorOptions): UseCod
264
300
 
265
301
  // Restore cursor position
266
302
  setCursorOffset(codeRef.value, cursorOffset);
303
+
304
+ // Ensure the element maintains focus after innerHTML replacement
305
+ // This is important because innerHTML replacement can cause focus loss
306
+ if (hasFocus && document.activeElement !== codeRef.value) {
307
+ codeRef.value.focus();
308
+ }
267
309
  }, 300);
268
310
  }
269
311
 
@@ -339,31 +381,116 @@ export function useCodeViewerEditor(options: UseCodeViewerEditorOptions): UseCod
339
381
  }
340
382
  }
341
383
 
342
- // Handle keyboard shortcuts in edit mode
384
+ // Handle keyboard shortcuts (some work in any mode, some only in edit mode)
343
385
  function onKeyDown(event: KeyboardEvent): void {
386
+ // Check for Ctrl/Cmd + Alt + L combinations (with or without Shift)
387
+ // These should work even when not in edit mode
388
+ const isCtrlAltL = (event.ctrlKey || event.metaKey) && event.altKey && event.key.toLowerCase() === "l";
389
+
390
+ if (isCtrlAltL) {
391
+ event.preventDefault();
392
+ event.stopPropagation();
393
+
394
+ // Ctrl/Cmd + Alt + Shift + L - open language search panel
395
+ if (event.shiftKey && onOpenLanguageSearch) {
396
+ onOpenLanguageSearch();
397
+ return;
398
+ }
399
+
400
+ // Ctrl/Cmd + Alt + L (without Shift) - cycle through available formats/languages
401
+ if (!event.shiftKey && onEmitFormat) {
402
+ const availableFormats = getAvailableFormats(currentFormat.value);
403
+ if (availableFormats.length > 1) {
404
+ const currentIndex = availableFormats.indexOf(currentFormat.value);
405
+ const nextIndex = (currentIndex + 1) % availableFormats.length;
406
+ const nextFormat = availableFormats[nextIndex];
407
+ onEmitFormat(nextFormat);
408
+ }
409
+ }
410
+ return;
411
+ }
412
+
413
+ // All other shortcuts require edit mode
344
414
  if (!isEditing.value) return;
345
415
 
416
+ // Backspace/Delete on empty content - delete the code block
417
+ if ((event.key === "Backspace" || event.key === "Delete") && onDelete) {
418
+ const content = editingContent.value.trim();
419
+ if (content === "") {
420
+ event.preventDefault();
421
+ onDelete();
422
+ return;
423
+ }
424
+ }
425
+
426
+ // Ctrl/Cmd + Enter - exit the code block immediately
427
+ if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
428
+ event.preventDefault();
429
+ if (onExit) {
430
+ // Emit the current value before exiting
431
+ const parsed = codeFormat.parse(editingContent.value);
432
+ if (parsed) {
433
+ onEmitModelValue(parsed);
434
+ } else {
435
+ onEmitModelValue(editingContent.value);
436
+ }
437
+ onExit();
438
+ }
439
+ return;
440
+ }
441
+
346
442
  // Enter key - smart indentation
347
443
  if (event.key === "Enter") {
348
- const lineInfo = getCurrentLineInfo(editingContent.value, codeRef.value);
349
- if (lineInfo) {
350
- event.preventDefault();
351
- const smartIndent = getSmartIndent(lineInfo, currentFormat.value);
352
-
353
- const selection = window.getSelection();
354
- if (selection && selection.rangeCount > 0) {
355
- const range = selection.getRangeAt(0);
356
- range.deleteContents();
357
- const textNode = document.createTextNode("\n" + smartIndent);
358
- range.insertNode(textNode);
359
- range.setStartAfter(textNode);
360
- range.setEndAfter(textNode);
361
- selection.removeAllRanges();
362
- selection.addRange(range);
363
-
364
- codeRef.value?.dispatchEvent(new Event("input", { bubbles: true }));
444
+ const selection = window.getSelection();
445
+
446
+ // If no selection, try to ensure we have one by focusing the element
447
+ if (!selection || selection.rangeCount === 0) {
448
+ // Fallback: position cursor at end of content
449
+ if (codeRef.value) {
450
+ const range = document.createRange();
451
+ range.selectNodeContents(codeRef.value);
452
+ range.collapse(false);
453
+ selection?.removeAllRanges();
454
+ selection?.addRange(range);
365
455
  }
366
456
  }
457
+
458
+ // Re-check selection - if still none, let browser handle it
459
+ if (!selection || selection.rangeCount === 0) {
460
+ return;
461
+ }
462
+
463
+ event.preventDefault();
464
+
465
+ // Check if the range is actually valid and positioned within our codeRef
466
+ let range = selection.getRangeAt(0);
467
+ const isWithinCodeRef = codeRef.value?.contains(range.startContainer);
468
+
469
+ // If selection is not within codeRef, re-create it at the end of content
470
+ // This can happen after innerHTML replacement in debouncedHighlight
471
+ if (!isWithinCodeRef && codeRef.value) {
472
+ range = document.createRange();
473
+ range.selectNodeContents(codeRef.value);
474
+ range.collapse(false);
475
+ selection.removeAllRanges();
476
+ selection.addRange(range);
477
+ }
478
+
479
+ // IMPORTANT: Always use the DOM's actual content, not editingContent.value
480
+ // editingContent.value may be stale if isUserEditing is false (e.g., after debounced highlight)
481
+ const domTextContent = codeRef.value?.innerText || "";
482
+ const lineInfo = getCurrentLineInfo(domTextContent, codeRef.value);
483
+ const smartIndent = lineInfo ? getSmartIndent(lineInfo, currentFormat.value) : "";
484
+
485
+ range.deleteContents();
486
+ const textNode = document.createTextNode("\n" + smartIndent);
487
+ range.insertNode(textNode);
488
+ range.setStartAfter(textNode);
489
+ range.setEndAfter(textNode);
490
+ selection.removeAllRanges();
491
+ selection.addRange(range);
492
+
493
+ codeRef.value?.dispatchEvent(new Event("input", { bubbles: true }));
367
494
  }
368
495
 
369
496
  // Tab key - insert spaces instead of moving focus
@@ -384,6 +511,33 @@ export function useCodeViewerEditor(options: UseCodeViewerEditorOptions): UseCod
384
511
  event.preventDefault();
385
512
  onContentEditableBlur();
386
513
  }
514
+
515
+ // Ctrl/Cmd + A - select all content within this CodeViewer only
516
+ if ((event.ctrlKey || event.metaKey) && event.key === "a") {
517
+ event.preventDefault();
518
+ event.stopPropagation();
519
+
520
+ // Select all content in the current contenteditable element
521
+ const selection = window.getSelection();
522
+ if (selection && codeRef.value) {
523
+ const range = document.createRange();
524
+ range.selectNodeContents(codeRef.value);
525
+ selection.removeAllRanges();
526
+ selection.addRange(range);
527
+ }
528
+ }
529
+ }
530
+
531
+ // Initialize editing content when starting in edit mode
532
+ // This handles the case where editable=true is passed as initial prop
533
+ if (isEditing.value) {
534
+ editingContent.value = codeFormat.formattedContent.value;
535
+ // Set the pre element content after it mounts
536
+ nextTick(() => {
537
+ if (codeRef.value) {
538
+ codeRef.value.innerHTML = highlightSyntax(editingContent.value, { format: currentFormat.value });
539
+ }
540
+ });
387
541
  }
388
542
 
389
543
  // Cleanup timeouts on unmount
@@ -2,5 +2,5 @@ export * from "./datetime";
2
2
  export * from "./highlightSyntax";
3
3
  export * from "./numbers";
4
4
  export * from "./parsers";
5
- export * from "./renderMarkdown";
5
+ export * from "./markdown";
6
6
  export * from "./strings";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * HTML entity escaping for XSS prevention
3
+ */
4
+
5
+ /**
6
+ * Escape HTML entities to prevent XSS
7
+ */
8
+ export function escapeHtml(text: string): string {
9
+ return text
10
+ .replace(/&/g, "&amp;")
11
+ .replace(/</g, "&lt;")
12
+ .replace(/>/g, "&gt;")
13
+ .replace(/"/g, "&quot;")
14
+ .replace(/'/g, "&#039;");
15
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Markdown escape sequence handling
3
+ * Maps backslash-escaped characters to Unicode placeholders and back
4
+ * Using Private Use Area characters (U+E000-U+F8FF) to avoid conflicts
5
+ */
6
+
7
+ /**
8
+ * Escape sequences mapping - character to Unicode placeholder
9
+ */
10
+ export const ESCAPE_MAP: Record<string, string> = {
11
+ "\\*": "\uE000",
12
+ "\\_": "\uE001",
13
+ "\\~": "\uE002",
14
+ "\\`": "\uE003",
15
+ "\\[": "\uE004",
16
+ "\\]": "\uE005",
17
+ "\\#": "\uE006",
18
+ "\\&gt;": "\uE007", // Escaped > becomes &gt; after HTML escaping
19
+ "\\-": "\uE008",
20
+ "\\+": "\uE009",
21
+ "\\.": "\uE00A",
22
+ "\\!": "\uE00B",
23
+ "\\=": "\uE00C",
24
+ "\\^": "\uE00D"
25
+ };
26
+
27
+ /**
28
+ * Reverse mapping - placeholder back to literal character
29
+ * Generated from ESCAPE_MAP for DRY compliance
30
+ */
31
+ export const UNESCAPE_MAP: Record<string, string> = Object.fromEntries(
32
+ Object.entries(ESCAPE_MAP).map(([escaped, placeholder]) => {
33
+ // Extract the literal character from the escape sequence
34
+ // "\\*" -> "*", "\\&gt;" -> "&gt;" (special case for HTML-escaped >)
35
+ const literal = escaped.startsWith("\\&") ? escaped.slice(1) : escaped.slice(1);
36
+ return [placeholder, literal];
37
+ })
38
+ );
39
+
40
+ /**
41
+ * Apply escape sequences - convert backslash-escaped characters to placeholders
42
+ */
43
+ export function applyEscapes(text: string): string {
44
+ let result = text;
45
+ for (const [pattern, placeholder] of Object.entries(ESCAPE_MAP)) {
46
+ result = result.split(pattern).join(placeholder);
47
+ }
48
+ return result;
49
+ }
50
+
51
+ /**
52
+ * Revert escape sequences - convert placeholders back to literal characters
53
+ */
54
+ export function revertEscapes(text: string): string {
55
+ let result = text;
56
+ for (const [placeholder, literal] of Object.entries(UNESCAPE_MAP)) {
57
+ result = result.split(placeholder).join(literal);
58
+ }
59
+ return result;
60
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * HTML heading to markdown converter
3
+ * Converts h1-h6 elements to markdown # syntax
4
+ */
5
+
6
+ /**
7
+ * Check if an element is a heading element (h1-h6)
8
+ */
9
+ export function isHeadingElement(element: Element): boolean {
10
+ const tagName = element.tagName.toLowerCase();
11
+ return /^h[1-6]$/.test(tagName);
12
+ }
13
+
14
+ /**
15
+ * Get the heading level from an element (1-6, or 0 if not a heading)
16
+ */
17
+ export function getHeadingLevel(element: Element): number {
18
+ const tagName = element.tagName.toLowerCase();
19
+ const match = tagName.match(/^h([1-6])$/);
20
+ return match ? parseInt(match[1], 10) : 0;
21
+ }
22
+
23
+ /**
24
+ * Convert an HTML heading element to markdown
25
+ * @param element - The heading element (h1-h6)
26
+ * @returns Markdown string with appropriate # prefix
27
+ */
28
+ export function convertHeading(element: Element): string {
29
+ const level = getHeadingLevel(element);
30
+ if (level === 0) {
31
+ return "";
32
+ }
33
+
34
+ const content = element.textContent?.trim() || "";
35
+ if (!content) {
36
+ return "";
37
+ }
38
+
39
+ const prefix = "#".repeat(level);
40
+ return `${prefix} ${content}\n\n`;
41
+ }