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.
- package/.claude/settings.local.json +8 -0
- package/dist/danx.es.js +16119 -10641
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +202 -123
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +8 -1
- package/src/components/Utility/Buttons/ActionButton.vue +15 -5
- package/src/components/Utility/Code/CodeViewer.vue +41 -16
- package/src/components/Utility/Code/CodeViewerCollapsed.vue +2 -0
- package/src/components/Utility/Code/CodeViewerFooter.vue +3 -1
- package/src/components/Utility/Code/LanguageBadge.vue +278 -5
- package/src/components/Utility/Code/MarkdownContent.vue +31 -163
- 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 +233 -0
- package/src/components/Utility/Markdown/MarkdownEditorContent.vue +296 -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/Widgets/LabelPillWidget.vue +20 -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 +805 -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 +388 -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 +1077 -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/useCodeFormat.ts +17 -10
- package/src/composables/useCodeViewerEditor.spec.ts +655 -0
- package/src/composables/useCodeViewerEditor.ts +174 -20
- package/src/helpers/formats/highlightCSS.ts +236 -0
- package/src/helpers/formats/highlightHTML.ts +483 -0
- package/src/helpers/formats/highlightJavaScript.ts +346 -0
- package/src/helpers/formats/highlightSyntax.ts +15 -4
- package/src/helpers/formats/index.ts +3 -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 +425 -0
- package/src/helpers/formats/markdown/index.ts +7 -0
- package/src/helpers/formats/markdown/linePatterns.spec.ts +498 -0
- package/src/helpers/formats/markdown/linePatterns.ts +172 -0
- package/src/styles/danx.scss +3 -3
- package/src/styles/index.scss +5 -5
- package/src/styles/themes/danx/code.scss +257 -1
- package/src/styles/themes/danx/index.scss +10 -10
- package/src/styles/themes/danx/markdown.scss +59 -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/highlighters.test.ts +153 -0
- package/src/test/setup.test.ts +12 -0
- package/src/test/setup.ts +12 -0
- package/src/types/widgets.d.ts +2 -2
- package/vite.config.js +5 -1
- 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(``);
|
|
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
|
+
}
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
160
|
+
// v3: For text, markdown, and code formats (CSS, JavaScript, HTML), return rawContent directly without parsing
|
|
155
161
|
const formattedContent = computed(() => {
|
|
156
|
-
|
|
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);
|