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.
- package/dist/danx.es.js +17884 -12732
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +192 -118
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +11 -2
- package/scripts/publish.sh +76 -0
- package/src/components/Utility/Code/CodeViewer.vue +31 -14
- package/src/components/Utility/Code/CodeViewerCollapsed.vue +2 -0
- package/src/components/Utility/Code/CodeViewerFooter.vue +1 -1
- package/src/components/Utility/Code/LanguageBadge.vue +278 -5
- package/src/components/Utility/Code/MarkdownContent.vue +160 -6
- package/src/components/Utility/Code/index.ts +3 -0
- package/src/components/Utility/Markdown/ContextMenu.vue +314 -0
- package/src/components/Utility/Markdown/HotkeyHelpPopover.vue +259 -0
- package/src/components/Utility/Markdown/LineTypeMenu.vue +226 -0
- package/src/components/Utility/Markdown/LinkPopover.vue +331 -0
- package/src/components/Utility/Markdown/MarkdownEditor.vue +228 -0
- package/src/components/Utility/Markdown/MarkdownEditorContent.vue +235 -0
- package/src/components/Utility/Markdown/MarkdownEditorFooter.vue +50 -0
- package/src/components/Utility/Markdown/TablePopover.vue +420 -0
- package/src/components/Utility/Markdown/index.ts +11 -0
- package/src/components/Utility/Markdown/types.ts +27 -0
- package/src/components/Utility/index.ts +1 -0
- package/src/composables/index.ts +1 -0
- package/src/composables/markdown/features/useBlockquotes.spec.ts +428 -0
- package/src/composables/markdown/features/useBlockquotes.ts +248 -0
- package/src/composables/markdown/features/useCodeBlockManager.ts +369 -0
- package/src/composables/markdown/features/useCodeBlocks.spec.ts +779 -0
- package/src/composables/markdown/features/useCodeBlocks.ts +774 -0
- package/src/composables/markdown/features/useContextMenu.ts +444 -0
- package/src/composables/markdown/features/useFocusTracking.ts +116 -0
- package/src/composables/markdown/features/useHeadings.spec.ts +834 -0
- package/src/composables/markdown/features/useHeadings.ts +290 -0
- package/src/composables/markdown/features/useInlineFormatting.spec.ts +705 -0
- package/src/composables/markdown/features/useInlineFormatting.ts +402 -0
- package/src/composables/markdown/features/useLineTypeMenu.ts +285 -0
- package/src/composables/markdown/features/useLinks.spec.ts +369 -0
- package/src/composables/markdown/features/useLinks.ts +374 -0
- package/src/composables/markdown/features/useLists.spec.ts +834 -0
- package/src/composables/markdown/features/useLists.ts +747 -0
- package/src/composables/markdown/features/usePopoverManager.ts +181 -0
- package/src/composables/markdown/features/useTables.spec.ts +1601 -0
- package/src/composables/markdown/features/useTables.ts +1107 -0
- package/src/composables/markdown/index.ts +16 -0
- package/src/composables/markdown/useMarkdownEditor.spec.ts +332 -0
- package/src/composables/markdown/useMarkdownEditor.ts +1068 -0
- package/src/composables/markdown/useMarkdownHotkeys.spec.ts +791 -0
- package/src/composables/markdown/useMarkdownHotkeys.ts +266 -0
- package/src/composables/markdown/useMarkdownSelection.ts +219 -0
- package/src/composables/markdown/useMarkdownSync.ts +549 -0
- package/src/composables/useCodeViewerEditor.spec.ts +655 -0
- package/src/composables/useCodeViewerEditor.ts +174 -20
- package/src/helpers/formats/index.ts +1 -1
- package/src/helpers/formats/markdown/escapeHtml.ts +15 -0
- package/src/helpers/formats/markdown/escapeSequences.ts +60 -0
- package/src/helpers/formats/markdown/htmlToMarkdown/convertHeadings.ts +41 -0
- package/src/helpers/formats/markdown/htmlToMarkdown/index.spec.ts +489 -0
- package/src/helpers/formats/markdown/htmlToMarkdown/index.ts +412 -0
- package/src/helpers/formats/markdown/index.ts +92 -0
- package/src/helpers/formats/markdown/linePatterns.spec.ts +495 -0
- package/src/helpers/formats/markdown/linePatterns.ts +172 -0
- package/src/helpers/formats/markdown/parseInline.ts +124 -0
- package/src/helpers/formats/markdown/render/index.ts +92 -0
- package/src/helpers/formats/markdown/render/renderFootnotes.ts +30 -0
- package/src/helpers/formats/markdown/render/renderList.ts +69 -0
- package/src/helpers/formats/markdown/render/renderTable.ts +38 -0
- package/src/helpers/formats/markdown/state.ts +58 -0
- package/src/helpers/formats/markdown/tokenize/extractDefinitions.ts +39 -0
- package/src/helpers/formats/markdown/tokenize/index.ts +139 -0
- package/src/helpers/formats/markdown/tokenize/parseBlockquote.ts +34 -0
- package/src/helpers/formats/markdown/tokenize/parseCodeBlock.ts +85 -0
- package/src/helpers/formats/markdown/tokenize/parseDefinitionList.ts +88 -0
- package/src/helpers/formats/markdown/tokenize/parseHeading.ts +65 -0
- package/src/helpers/formats/markdown/tokenize/parseHorizontalRule.ts +22 -0
- package/src/helpers/formats/markdown/tokenize/parseList.ts +119 -0
- package/src/helpers/formats/markdown/tokenize/parseParagraph.ts +59 -0
- package/src/helpers/formats/markdown/tokenize/parseTable.ts +70 -0
- package/src/helpers/formats/markdown/tokenize/parseTaskList.ts +47 -0
- package/src/helpers/formats/markdown/tokenize/utils.ts +25 -0
- package/src/helpers/formats/markdown/types.ts +63 -0
- package/src/styles/danx.scss +1 -0
- package/src/styles/themes/danx/markdown.scss +96 -0
- package/src/test/helpers/editorTestUtils.spec.ts +296 -0
- package/src/test/helpers/editorTestUtils.ts +253 -0
- package/src/test/helpers/index.ts +1 -0
- package/src/test/setup.test.ts +12 -0
- package/src/test/setup.ts +12 -0
- package/vitest.config.ts +19 -0
- 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
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
range.
|
|
357
|
-
|
|
358
|
-
range
|
|
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
|
|
@@ -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, "&")
|
|
11
|
+
.replace(/</g, "<")
|
|
12
|
+
.replace(/>/g, ">")
|
|
13
|
+
.replace(/"/g, """)
|
|
14
|
+
.replace(/'/g, "'");
|
|
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
|
+
"\\>": "\uE007", // Escaped > becomes > 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
|
+
// "\\*" -> "*", "\\>" -> ">" (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
|
+
}
|