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
|
@@ -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(``);
|
|
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(``);
|
|
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
|
+
}
|