@abraca/dabra 1.8.2 → 2.0.0
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/abracadabra-provider.cjs +12722 -9050
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +12683 -9061
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +1485 -118
- package/package.json +1 -1
- package/src/AbracadabraBaseProvider.ts +51 -2
- package/src/AbracadabraClient.ts +516 -66
- package/src/AbracadabraProvider.ts +22 -7
- package/src/AbracadabraWS.ts +1 -1
- package/src/ChatClient.ts +193 -113
- package/src/ContentManager.ts +228 -0
- package/src/CryptoIdentityKeystore.ts +3 -3
- package/src/DocConverters.ts +1862 -0
- package/src/DocKeyManager.ts +60 -12
- package/src/DocTypes.ts +628 -0
- package/src/DocUtils.ts +89 -0
- package/src/DocumentManager.ts +319 -0
- package/src/E2EAbracadabraProvider.ts +189 -0
- package/src/EncryptedChatClient.ts +173 -0
- package/src/EncryptedY.ts +2 -2
- package/src/FileBlobStore.ts +10 -0
- package/src/IdentityDoc.ts +25 -0
- package/src/MetaManager.ts +100 -0
- package/src/MnemonicKeyDerivation.ts +4 -4
- package/src/NotificationsClient.ts +120 -98
- package/src/OutgoingMessages/SubdocMessage.ts +2 -2
- package/src/RpcClient.ts +659 -0
- package/src/TreeManager.ts +473 -0
- package/src/TreeTimestamps.ts +28 -25
- package/src/index.ts +71 -1
- package/src/messageRecord.ts +121 -0
- package/src/types.ts +174 -16
- package/src/webrtc/AbracadabraWebRTC.ts +2 -2
- package/src/webrtc/DataChannelRouter.ts +2 -2
- package/src/webrtc/E2EEChannel.ts +3 -3
- package/src/webrtc/FileTransferChannel.ts +9 -2
|
@@ -0,0 +1,1862 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document content converters — markdown ↔ Y.js.
|
|
3
|
+
*
|
|
4
|
+
* Canonical implementations for converting between Markdown text and TipTap-
|
|
5
|
+
* compatible Y.XmlFragment structures. Previously lived in
|
|
6
|
+
* `mcp/converters/` — now the authoritative home is the provider package so
|
|
7
|
+
* any consumer of `@abraca/dabra` can use them without pulling in MCP deps.
|
|
8
|
+
*/
|
|
9
|
+
import * as Y from "yjs";
|
|
10
|
+
import type { PageMeta } from "./DocTypes.ts";
|
|
11
|
+
|
|
12
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
13
|
+
// Y.XmlFragment → Markdown (yjsToMarkdown)
|
|
14
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
15
|
+
|
|
16
|
+
// ── Inline text serialization ────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function deltaToMarkdown(
|
|
19
|
+
delta: { insert: string; attributes?: Record<string, any> }[],
|
|
20
|
+
): string {
|
|
21
|
+
return delta
|
|
22
|
+
.map((op) => {
|
|
23
|
+
let text = op.insert as string;
|
|
24
|
+
if (!op.attributes) return text;
|
|
25
|
+
|
|
26
|
+
const a = op.attributes;
|
|
27
|
+
|
|
28
|
+
if (a.code) text = `\`${text}\``;
|
|
29
|
+
if (a.bold) text = `**${text}**`;
|
|
30
|
+
if (a.italic) text = `*${text}*`;
|
|
31
|
+
if (a.strike) text = `~~${text}~~`;
|
|
32
|
+
if (a.link?.href) text = `[${text}](${a.link.href})`;
|
|
33
|
+
if (a.badge) text = `:badge[${a.badge.label || text}]`;
|
|
34
|
+
if (a.kbd) text = `:kbd{value="${a.kbd.value || text}"}`;
|
|
35
|
+
if (a.proseIcon) text = `:icon{name="${a.proseIcon.name}"}`;
|
|
36
|
+
|
|
37
|
+
return text;
|
|
38
|
+
})
|
|
39
|
+
.join("");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function xmlTextToMarkdown(xmlText: Y.XmlText): string {
|
|
43
|
+
const delta = xmlText.toDelta();
|
|
44
|
+
return deltaToMarkdown(delta);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function elementTextContent(el: Y.XmlElement | Y.XmlFragment): string {
|
|
48
|
+
const parts: string[] = [];
|
|
49
|
+
for (let i = 0; i < el.length; i++) {
|
|
50
|
+
const child = el.get(i);
|
|
51
|
+
if (child instanceof Y.XmlText) {
|
|
52
|
+
parts.push(xmlTextToMarkdown(child));
|
|
53
|
+
} else if (child instanceof Y.XmlElement) {
|
|
54
|
+
if (child.nodeName === "docLink") {
|
|
55
|
+
const docId = child.getAttribute("docId");
|
|
56
|
+
if (docId) parts.push(`[[${docId}]]`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
parts.push(elementTextContent(child));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return parts.join("");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Block serialization ──────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
function serializeElement(el: Y.XmlElement, indent = ""): string {
|
|
68
|
+
const name = el.nodeName;
|
|
69
|
+
|
|
70
|
+
switch (name) {
|
|
71
|
+
case "documentHeader":
|
|
72
|
+
case "documentMeta":
|
|
73
|
+
return "";
|
|
74
|
+
|
|
75
|
+
case "heading": {
|
|
76
|
+
const level = Number(el.getAttribute("level")) || 1;
|
|
77
|
+
const prefix = "#".repeat(level);
|
|
78
|
+
return `${prefix} ${elementTextContent(el)}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
case "paragraph":
|
|
82
|
+
return elementTextContent(el);
|
|
83
|
+
|
|
84
|
+
case "bulletList":
|
|
85
|
+
return serializeList(el, "bullet", indent);
|
|
86
|
+
|
|
87
|
+
case "orderedList":
|
|
88
|
+
return serializeList(el, "ordered", indent);
|
|
89
|
+
|
|
90
|
+
case "taskList":
|
|
91
|
+
return serializeTaskList(el, indent);
|
|
92
|
+
|
|
93
|
+
case "codeBlock": {
|
|
94
|
+
const lang = el.getAttribute("language") || "";
|
|
95
|
+
const code = elementTextContent(el);
|
|
96
|
+
return `\`\`\`${lang}\n${code}\n\`\`\``;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
case "blockquote": {
|
|
100
|
+
const lines: string[] = [];
|
|
101
|
+
for (let i = 0; i < el.length; i++) {
|
|
102
|
+
const child = el.get(i);
|
|
103
|
+
if (child instanceof Y.XmlElement) {
|
|
104
|
+
lines.push(serializeElement(child, indent));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return lines.map((l) => `> ${l}`).join("\n");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
case "horizontalRule":
|
|
111
|
+
return "---";
|
|
112
|
+
|
|
113
|
+
case "table":
|
|
114
|
+
return serializeTable(el);
|
|
115
|
+
|
|
116
|
+
case "docEmbed": {
|
|
117
|
+
const docId = el.getAttribute("docId");
|
|
118
|
+
if (!docId) return "";
|
|
119
|
+
const seamlessAttr = el.getAttribute("seamless");
|
|
120
|
+
const seamless =
|
|
121
|
+
(seamlessAttr as unknown) === true || seamlessAttr === "true";
|
|
122
|
+
return seamless ? `![[${docId}]]{seamless}` : `![[${docId}]]`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
case "svgEmbed": {
|
|
126
|
+
const svg = el.getAttribute("svg") || "";
|
|
127
|
+
const svgTitle = el.getAttribute("title") || "";
|
|
128
|
+
if (!svg) return "";
|
|
129
|
+
return `\`\`\`svg${svgTitle ? ` ${svgTitle}` : ""}\n${svg}\n\`\`\``;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
case "image": {
|
|
133
|
+
const src = el.getAttribute("src") || "";
|
|
134
|
+
const alt = el.getAttribute("alt") || "";
|
|
135
|
+
const w = el.getAttribute("width");
|
|
136
|
+
const h = el.getAttribute("height");
|
|
137
|
+
let attrs = "";
|
|
138
|
+
if (w || h) {
|
|
139
|
+
const imgParts: string[] = [];
|
|
140
|
+
if (w) imgParts.push(`width="${w}"`);
|
|
141
|
+
if (h) imgParts.push(`height="${h}"`);
|
|
142
|
+
attrs = `{${imgParts.join(" ")}}`;
|
|
143
|
+
}
|
|
144
|
+
return `${attrs}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
case "callout": {
|
|
148
|
+
const type = el.getAttribute("type") || "note";
|
|
149
|
+
const inner = serializeChildren(el, indent);
|
|
150
|
+
return `::${type}\n${inner}\n::`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
case "collapsible": {
|
|
154
|
+
const label = el.getAttribute("label") || "Details";
|
|
155
|
+
const open = el.getAttribute("open");
|
|
156
|
+
const props: string[] = [`label="${label}"`];
|
|
157
|
+
if ((open as unknown) === true || open === "true") props.push('open="true"');
|
|
158
|
+
const inner = serializeChildren(el, indent);
|
|
159
|
+
return `::collapsible{${props.join(" ")}}\n${inner}\n::`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
case "steps": {
|
|
163
|
+
const inner = serializeChildren(el, indent);
|
|
164
|
+
return `::steps\n${inner}\n::`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
case "card": {
|
|
168
|
+
const title = el.getAttribute("title") || "";
|
|
169
|
+
const icon = el.getAttribute("icon") || "";
|
|
170
|
+
const to = el.getAttribute("to") || "";
|
|
171
|
+
const props: string[] = [];
|
|
172
|
+
if (title) props.push(`title="${title}"`);
|
|
173
|
+
if (icon) props.push(`icon="${icon}"`);
|
|
174
|
+
if (to) props.push(`to="${to}"`);
|
|
175
|
+
const inner = serializeChildren(el, indent);
|
|
176
|
+
return `::card{${props.join(" ")}}\n${inner}\n::`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
case "cardGroup": {
|
|
180
|
+
const inner = serializeChildren(el, indent);
|
|
181
|
+
return `::card-group\n${inner}\n::`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
case "codeCollapse": {
|
|
185
|
+
const inner = serializeChildren(el, indent);
|
|
186
|
+
return `::code-collapse\n${inner}\n::`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
case "codeGroup": {
|
|
190
|
+
const inner = serializeChildren(el, indent);
|
|
191
|
+
return `::code-group\n${inner}\n::`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
case "codePreview": {
|
|
195
|
+
const inner = serializeChildren(el, indent);
|
|
196
|
+
return `::code-preview\n${inner}\n::`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
case "codeTree": {
|
|
200
|
+
const files = el.getAttribute("files") || "[]";
|
|
201
|
+
return `::code-tree{files="${files}"}\n::`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
case "accordion":
|
|
205
|
+
return serializeSlottedComponent(
|
|
206
|
+
el,
|
|
207
|
+
"accordion",
|
|
208
|
+
"accordionItem",
|
|
209
|
+
"item",
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
case "tabs":
|
|
213
|
+
return serializeSlottedComponent(el, "tabs", "tabsItem", "tab");
|
|
214
|
+
|
|
215
|
+
case "field": {
|
|
216
|
+
const fieldName = el.getAttribute("name") || "";
|
|
217
|
+
const fieldType = el.getAttribute("type") || "string";
|
|
218
|
+
const required = el.getAttribute("required");
|
|
219
|
+
const props: string[] = [];
|
|
220
|
+
if (fieldName) props.push(`name="${fieldName}"`);
|
|
221
|
+
props.push(`type="${fieldType}"`);
|
|
222
|
+
if ((required as unknown) === true || required === "true")
|
|
223
|
+
props.push('required="true"');
|
|
224
|
+
const inner = serializeChildren(el, indent);
|
|
225
|
+
return `::field{${props.join(" ")}}\n${inner}\n::`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
case "fieldGroup": {
|
|
229
|
+
const inner = serializeChildren(el, indent);
|
|
230
|
+
return `::field-group\n${inner}\n::`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
default: {
|
|
234
|
+
return serializeChildren(el, indent);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function serializeList(
|
|
240
|
+
el: Y.XmlElement,
|
|
241
|
+
type: "bullet" | "ordered",
|
|
242
|
+
indent: string,
|
|
243
|
+
): string {
|
|
244
|
+
const lines: string[] = [];
|
|
245
|
+
for (let i = 0; i < el.length; i++) {
|
|
246
|
+
const item = el.get(i);
|
|
247
|
+
if (
|
|
248
|
+
item instanceof Y.XmlElement &&
|
|
249
|
+
item.nodeName === "listItem"
|
|
250
|
+
) {
|
|
251
|
+
const prefix = type === "bullet" ? "- " : `${i + 1}. `;
|
|
252
|
+
const content = elementTextContent(item);
|
|
253
|
+
lines.push(`${indent}${prefix}${content}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return lines.join("\n");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function serializeTaskList(el: Y.XmlElement, indent: string): string {
|
|
260
|
+
const lines: string[] = [];
|
|
261
|
+
for (let i = 0; i < el.length; i++) {
|
|
262
|
+
const item = el.get(i);
|
|
263
|
+
if (
|
|
264
|
+
item instanceof Y.XmlElement &&
|
|
265
|
+
item.nodeName === "taskItem"
|
|
266
|
+
) {
|
|
267
|
+
const checked = item.getAttribute("checked");
|
|
268
|
+
const marker =
|
|
269
|
+
(checked as unknown) === true || checked === "true" ? "[x]" : "[ ]";
|
|
270
|
+
const content = elementTextContent(item);
|
|
271
|
+
lines.push(`${indent}- ${marker} ${content}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return lines.join("\n");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function serializeTable(el: Y.XmlElement): string {
|
|
278
|
+
const rows: string[][] = [];
|
|
279
|
+
|
|
280
|
+
for (let i = 0; i < el.length; i++) {
|
|
281
|
+
const row = el.get(i);
|
|
282
|
+
if (
|
|
283
|
+
!(row instanceof Y.XmlElement) ||
|
|
284
|
+
row.nodeName !== "tableRow"
|
|
285
|
+
)
|
|
286
|
+
continue;
|
|
287
|
+
|
|
288
|
+
const cells: string[] = [];
|
|
289
|
+
for (let j = 0; j < row.length; j++) {
|
|
290
|
+
const cell = row.get(j);
|
|
291
|
+
if (cell instanceof Y.XmlElement) {
|
|
292
|
+
cells.push(elementTextContent(cell));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
rows.push(cells);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!rows.length) return "";
|
|
299
|
+
|
|
300
|
+
const headerRow = rows[0]!;
|
|
301
|
+
const lines = [`| ${headerRow.join(" | ")} |`];
|
|
302
|
+
|
|
303
|
+
// Separator
|
|
304
|
+
lines.push(`| ${headerRow.map(() => "---").join(" | ")} |`);
|
|
305
|
+
|
|
306
|
+
// Data rows
|
|
307
|
+
for (let i = 1; i < rows.length; i++) {
|
|
308
|
+
lines.push(`| ${rows[i]!.join(" | ")} |`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return lines.join("\n");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function serializeSlottedComponent(
|
|
315
|
+
el: Y.XmlElement,
|
|
316
|
+
componentName: string,
|
|
317
|
+
childNodeName: string,
|
|
318
|
+
slotName: string,
|
|
319
|
+
): string {
|
|
320
|
+
const parts: string[] = [`::${componentName}`];
|
|
321
|
+
|
|
322
|
+
for (let i = 0; i < el.length; i++) {
|
|
323
|
+
const child = el.get(i);
|
|
324
|
+
if (
|
|
325
|
+
!(child instanceof Y.XmlElement) ||
|
|
326
|
+
child.nodeName !== childNodeName
|
|
327
|
+
)
|
|
328
|
+
continue;
|
|
329
|
+
|
|
330
|
+
const label = child.getAttribute("label") || `Item ${i + 1}`;
|
|
331
|
+
const icon = child.getAttribute("icon") || "";
|
|
332
|
+
const props: string[] = [`label="${label}"`];
|
|
333
|
+
if (icon) props.push(`icon="${icon}"`);
|
|
334
|
+
parts.push(`#${slotName}{${props.join(" ")}}`);
|
|
335
|
+
|
|
336
|
+
const inner = serializeChildren(child, "");
|
|
337
|
+
if (inner) parts.push(inner);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
parts.push("::");
|
|
341
|
+
return parts.join("\n");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function serializeChildren(
|
|
345
|
+
el: Y.XmlElement | Y.XmlFragment,
|
|
346
|
+
indent: string,
|
|
347
|
+
): string {
|
|
348
|
+
const parts: string[] = [];
|
|
349
|
+
for (let i = 0; i < el.length; i++) {
|
|
350
|
+
const child = el.get(i);
|
|
351
|
+
if (child instanceof Y.XmlElement) {
|
|
352
|
+
const serialized = serializeElement(child, indent);
|
|
353
|
+
if (serialized) parts.push(serialized);
|
|
354
|
+
} else if (child instanceof Y.XmlText) {
|
|
355
|
+
const text = xmlTextToMarkdown(child);
|
|
356
|
+
if (text) parts.push(text);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return parts.join("\n\n");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ── Public: Y.js → Markdown ─────────────────────────────────────────────────
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Converts a Y.XmlFragment (TipTap document) to markdown.
|
|
366
|
+
* Extracts the title from the documentHeader element.
|
|
367
|
+
*
|
|
368
|
+
* @returns `{ title, markdown }` where title is the H1/header text
|
|
369
|
+
*/
|
|
370
|
+
export function yjsToMarkdown(
|
|
371
|
+
fragment: Y.XmlFragment,
|
|
372
|
+
): { title: string; markdown: string } {
|
|
373
|
+
let title = "Untitled";
|
|
374
|
+
const bodyParts: string[] = [];
|
|
375
|
+
|
|
376
|
+
for (let i = 0; i < fragment.length; i++) {
|
|
377
|
+
const child = fragment.get(i);
|
|
378
|
+
if (!(child instanceof Y.XmlElement)) continue;
|
|
379
|
+
|
|
380
|
+
if (child.nodeName === "documentHeader") {
|
|
381
|
+
title = elementTextContent(child) || "Untitled";
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (child.nodeName === "documentMeta") {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const serialized = serializeElement(child);
|
|
390
|
+
if (serialized !== "") {
|
|
391
|
+
bodyParts.push(serialized);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return { title, markdown: bodyParts.join("\n\n") };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
399
|
+
// Markdown → Y.XmlFragment (populateYDocFromMarkdown)
|
|
400
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
401
|
+
|
|
402
|
+
// ── Filename → readable label ────────────────────────────────────────────────
|
|
403
|
+
|
|
404
|
+
export function filenameToLabel(raw: string): string {
|
|
405
|
+
const base = raw.replace(/\.[^.]+$/, "");
|
|
406
|
+
const spaced = base.replace(/([a-z])([A-Z])/g, "$1 $2");
|
|
407
|
+
const clean = spaced
|
|
408
|
+
.replace(/[-_.]+/g, " ")
|
|
409
|
+
.replace(/\s+/g, " ")
|
|
410
|
+
.trim();
|
|
411
|
+
return clean.charAt(0).toUpperCase() + clean.slice(1);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ── YAML frontmatter parser ──────────────────────────────────────────────────
|
|
415
|
+
|
|
416
|
+
export interface FrontmatterResult {
|
|
417
|
+
title?: string;
|
|
418
|
+
meta: Partial<PageMeta>;
|
|
419
|
+
body: string;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function parseInlineArray(raw: string): string[] {
|
|
423
|
+
return raw
|
|
424
|
+
.slice(1, -1)
|
|
425
|
+
.split(",")
|
|
426
|
+
.map((s) => s.trim())
|
|
427
|
+
.filter(Boolean);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function parseFrontmatter(markdown: string): FrontmatterResult {
|
|
431
|
+
const noResult: FrontmatterResult = { meta: {}, body: markdown };
|
|
432
|
+
|
|
433
|
+
const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
434
|
+
if (!match) return noResult;
|
|
435
|
+
|
|
436
|
+
const yamlBlock = match[1]!;
|
|
437
|
+
const body = markdown.slice(match[0].length);
|
|
438
|
+
|
|
439
|
+
const raw: Record<string, string | string[]> = {};
|
|
440
|
+
const lines = yamlBlock.split("\n");
|
|
441
|
+
let i = 0;
|
|
442
|
+
while (i < lines.length) {
|
|
443
|
+
const line = lines[i]!;
|
|
444
|
+
const blockSeqKey = line.match(/^(\w[\w-]*):\s*$/);
|
|
445
|
+
if (
|
|
446
|
+
blockSeqKey &&
|
|
447
|
+
i + 1 < lines.length &&
|
|
448
|
+
/^\s+-\s/.test(lines[i + 1]!)
|
|
449
|
+
) {
|
|
450
|
+
const key = blockSeqKey[1]!;
|
|
451
|
+
const items: string[] = [];
|
|
452
|
+
i++;
|
|
453
|
+
while (i < lines.length && /^\s+-\s/.test(lines[i]!)) {
|
|
454
|
+
items.push(lines[i]!.replace(/^\s+-\s/, "").trim());
|
|
455
|
+
i++;
|
|
456
|
+
}
|
|
457
|
+
raw[key] = items;
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
const kvMatch = line.match(/^(\w[\w-]*):\s*(.*)$/);
|
|
461
|
+
if (kvMatch) {
|
|
462
|
+
const key = kvMatch[1]!;
|
|
463
|
+
const val = kvMatch[2]!.trim();
|
|
464
|
+
if (val.startsWith("[") && val.endsWith("]")) {
|
|
465
|
+
raw[key] = parseInlineArray(val);
|
|
466
|
+
} else {
|
|
467
|
+
raw[key] = val;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
i++;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const meta: Partial<PageMeta> = {};
|
|
474
|
+
|
|
475
|
+
const getStr = (keys: string[]): string | undefined => {
|
|
476
|
+
for (const k of keys) {
|
|
477
|
+
const v = raw[k];
|
|
478
|
+
if (typeof v === "string" && v) return v;
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
if (raw["tags"])
|
|
483
|
+
meta.tags = Array.isArray(raw["tags"])
|
|
484
|
+
? raw["tags"]
|
|
485
|
+
: [raw["tags"] as string];
|
|
486
|
+
const color = getStr(["color"]);
|
|
487
|
+
if (color) meta.color = color;
|
|
488
|
+
const icon = getStr(["icon"]);
|
|
489
|
+
if (icon) meta.icon = icon;
|
|
490
|
+
const status = getStr(["status"]);
|
|
491
|
+
if (status) meta.status = status;
|
|
492
|
+
|
|
493
|
+
const priorityRaw = getStr(["priority"]);
|
|
494
|
+
if (priorityRaw !== undefined) {
|
|
495
|
+
const map: Record<string, number> = {
|
|
496
|
+
low: 1,
|
|
497
|
+
medium: 2,
|
|
498
|
+
high: 3,
|
|
499
|
+
urgent: 4,
|
|
500
|
+
};
|
|
501
|
+
meta.priority =
|
|
502
|
+
map[priorityRaw.toLowerCase()] ?? (Number(priorityRaw) || 0);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const checkedRaw = raw["checked"] ?? raw["done"];
|
|
506
|
+
if (checkedRaw !== undefined)
|
|
507
|
+
meta.checked = checkedRaw === "true" || (checkedRaw as unknown) === true;
|
|
508
|
+
|
|
509
|
+
const dateStart = getStr(["date", "created"]);
|
|
510
|
+
if (dateStart) meta.dateStart = dateStart;
|
|
511
|
+
const dateEnd = getStr(["due"]);
|
|
512
|
+
if (dateEnd) meta.dateEnd = dateEnd;
|
|
513
|
+
|
|
514
|
+
const subtitle = getStr(["description", "subtitle"]);
|
|
515
|
+
if (subtitle) meta.subtitle = subtitle;
|
|
516
|
+
const url = getStr(["url"]);
|
|
517
|
+
if (url) meta.url = url;
|
|
518
|
+
const email = getStr(["email"]);
|
|
519
|
+
if (email) meta.email = email;
|
|
520
|
+
const phone = getStr(["phone"]);
|
|
521
|
+
if (phone) meta.phone = phone;
|
|
522
|
+
|
|
523
|
+
const ratingRaw = getStr(["rating"]);
|
|
524
|
+
if (ratingRaw !== undefined) {
|
|
525
|
+
const n = Number(ratingRaw);
|
|
526
|
+
if (!Number.isNaN(n)) meta.rating = Math.min(5, Math.max(0, n));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Datetime fields
|
|
530
|
+
const datetimeStart = getStr(["datetimeStart"]);
|
|
531
|
+
if (datetimeStart) meta.datetimeStart = datetimeStart;
|
|
532
|
+
const datetimeEnd = getStr(["datetimeEnd"]);
|
|
533
|
+
if (datetimeEnd) meta.datetimeEnd = datetimeEnd;
|
|
534
|
+
const allDayRaw = raw["allDay"];
|
|
535
|
+
if (allDayRaw !== undefined)
|
|
536
|
+
meta.allDay = allDayRaw === "true" || (allDayRaw as unknown) === true;
|
|
537
|
+
|
|
538
|
+
// Geo fields
|
|
539
|
+
const geoLatRaw = getStr(["geoLat"]);
|
|
540
|
+
if (geoLatRaw !== undefined) {
|
|
541
|
+
const n = Number(geoLatRaw);
|
|
542
|
+
if (!Number.isNaN(n)) meta.geoLat = n;
|
|
543
|
+
}
|
|
544
|
+
const geoLngRaw = getStr(["geoLng"]);
|
|
545
|
+
if (geoLngRaw !== undefined) {
|
|
546
|
+
const n = Number(geoLngRaw);
|
|
547
|
+
if (!Number.isNaN(n)) meta.geoLng = n;
|
|
548
|
+
}
|
|
549
|
+
const geoType = getStr(["geoType"]);
|
|
550
|
+
if (
|
|
551
|
+
geoType &&
|
|
552
|
+
(geoType === "marker" || geoType === "line" || geoType === "measure")
|
|
553
|
+
) {
|
|
554
|
+
meta.geoType = geoType;
|
|
555
|
+
}
|
|
556
|
+
const geoDescription = getStr(["geoDescription"]);
|
|
557
|
+
if (geoDescription) meta.geoDescription = geoDescription;
|
|
558
|
+
|
|
559
|
+
// Numeric fields
|
|
560
|
+
const numberRaw = getStr(["number"]);
|
|
561
|
+
if (numberRaw !== undefined) {
|
|
562
|
+
const n = Number(numberRaw);
|
|
563
|
+
if (!Number.isNaN(n)) meta.number = n;
|
|
564
|
+
}
|
|
565
|
+
const unit = getStr(["unit"]);
|
|
566
|
+
if (unit) meta.unit = unit;
|
|
567
|
+
|
|
568
|
+
// Note
|
|
569
|
+
const note = getStr(["note"]);
|
|
570
|
+
if (note) meta.note = note;
|
|
571
|
+
|
|
572
|
+
const title = typeof raw["title"] === "string" ? raw["title"] : undefined;
|
|
573
|
+
|
|
574
|
+
return { title, meta, body };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ── Inline token parsing ─────────────────────────────────────────────────────
|
|
578
|
+
|
|
579
|
+
interface InlineToken {
|
|
580
|
+
text: string;
|
|
581
|
+
attrs?: Record<string, unknown>;
|
|
582
|
+
/** If set, emit an inline Y.XmlElement with this nodeName instead of text+marks. */
|
|
583
|
+
node?: string;
|
|
584
|
+
/** Attributes for the inline node (when node is set). */
|
|
585
|
+
nodeAttrs?: Record<string, string>;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function parseMdcProps(
|
|
589
|
+
propsStr: string | undefined,
|
|
590
|
+
): Record<string, string> {
|
|
591
|
+
if (!propsStr) return {};
|
|
592
|
+
const result: Record<string, string> = {};
|
|
593
|
+
const re = /(\w[\w-]*)="([^"]*)"/g;
|
|
594
|
+
let m: RegExpExecArray | null;
|
|
595
|
+
while ((m = re.exec(propsStr)) !== null) {
|
|
596
|
+
result[m[1]!] = m[2]!;
|
|
597
|
+
}
|
|
598
|
+
return result;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function parseInline(text: string): InlineToken[] {
|
|
602
|
+
const stripped = text
|
|
603
|
+
.replace(/\{lang="[^"]*"\}/g, "")
|
|
604
|
+
.replace(
|
|
605
|
+
/:(?!badge|icon|kbd)(\w[\w-]*)\[([^\]]*)\](\{[^}]*\})?/g,
|
|
606
|
+
"$2",
|
|
607
|
+
)
|
|
608
|
+
.replace(/:(?!badge|icon|kbd)(\w[\w-]*)(\{[^}]*\})/g, "");
|
|
609
|
+
|
|
610
|
+
const tokens: InlineToken[] = [];
|
|
611
|
+
const re =
|
|
612
|
+
/:badge\[([^\]]*)\](\{[^}]*\})?|:icon\{([^}]*)\}|:kbd\{([^}]*)\}|!?\[\[([^\]|]+?)(?:\|([^\]]+?))?\]\]|~~(.+?)~~|\*\*(.+?)\*\*|\*(.+?)\*|_(.+?)_|`(.+?)`|\[(.+?)\]\((.+?)\)/g;
|
|
613
|
+
let lastIndex = 0;
|
|
614
|
+
let match: RegExpExecArray | null;
|
|
615
|
+
|
|
616
|
+
while ((match = re.exec(stripped)) !== null) {
|
|
617
|
+
if (match.index > lastIndex) {
|
|
618
|
+
tokens.push({ text: stripped.slice(lastIndex, match.index) });
|
|
619
|
+
}
|
|
620
|
+
if (match[1] !== undefined) {
|
|
621
|
+
const badgeProps = parseMdcProps(match[2]);
|
|
622
|
+
tokens.push({
|
|
623
|
+
text: match[1] || "Badge",
|
|
624
|
+
attrs: {
|
|
625
|
+
badge: {
|
|
626
|
+
label: match[1] || "Badge",
|
|
627
|
+
color: badgeProps["color"] || "neutral",
|
|
628
|
+
variant: badgeProps["variant"] || "subtle",
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
});
|
|
632
|
+
} else if (match[3] !== undefined) {
|
|
633
|
+
const iconProps = parseMdcProps(`{${match[3]}}`);
|
|
634
|
+
tokens.push({
|
|
635
|
+
text: "\u200B",
|
|
636
|
+
attrs: {
|
|
637
|
+
proseIcon: {
|
|
638
|
+
name: iconProps["name"] || "i-lucide-star",
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
});
|
|
642
|
+
} else if (match[4] !== undefined) {
|
|
643
|
+
const kbdProps = parseMdcProps(`{${match[4]}}`);
|
|
644
|
+
tokens.push({
|
|
645
|
+
text: kbdProps["value"] || "",
|
|
646
|
+
attrs: { kbd: { value: kbdProps["value"] || "" } },
|
|
647
|
+
});
|
|
648
|
+
} else if (match[5] !== undefined) {
|
|
649
|
+
tokens.push({
|
|
650
|
+
text: "",
|
|
651
|
+
node: "docLink",
|
|
652
|
+
nodeAttrs: { docId: match[5]! },
|
|
653
|
+
});
|
|
654
|
+
} else if (match[7] !== undefined) {
|
|
655
|
+
tokens.push({ text: match[7], attrs: { strike: true } });
|
|
656
|
+
} else if (match[8] !== undefined) {
|
|
657
|
+
tokens.push({ text: match[8], attrs: { bold: true } });
|
|
658
|
+
} else if (match[9] !== undefined) {
|
|
659
|
+
tokens.push({ text: match[9], attrs: { italic: true } });
|
|
660
|
+
} else if (match[10] !== undefined) {
|
|
661
|
+
tokens.push({ text: match[10], attrs: { italic: true } });
|
|
662
|
+
} else if (match[11] !== undefined) {
|
|
663
|
+
tokens.push({ text: match[11], attrs: { code: true } });
|
|
664
|
+
} else if (match[12] !== undefined && match[13] !== undefined) {
|
|
665
|
+
tokens.push({
|
|
666
|
+
text: match[12],
|
|
667
|
+
attrs: { link: { href: match[13] } },
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
lastIndex = match.index + match[0].length;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (lastIndex < stripped.length) {
|
|
674
|
+
tokens.push({ text: stripped.slice(lastIndex) });
|
|
675
|
+
}
|
|
676
|
+
return tokens.filter((t) => t.node || t.text.length > 0);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// ── Block-level parser ───────────────────────────────────────────────────────
|
|
680
|
+
|
|
681
|
+
interface TaskItem {
|
|
682
|
+
text: string;
|
|
683
|
+
checked: boolean;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
type Block =
|
|
687
|
+
| { type: "heading"; level: number; text: string }
|
|
688
|
+
| { type: "paragraph"; text: string }
|
|
689
|
+
| { type: "bulletList"; items: string[] }
|
|
690
|
+
| { type: "orderedList"; items: string[] }
|
|
691
|
+
| { type: "taskList"; items: TaskItem[] }
|
|
692
|
+
| { type: "codeBlock"; lang: string; code: string }
|
|
693
|
+
| { type: "blockquote"; lines: string[] }
|
|
694
|
+
| { type: "table"; headerRow: string[]; dataRows: string[][] }
|
|
695
|
+
| { type: "hr" }
|
|
696
|
+
| { type: "callout"; calloutType: string; innerBlocks: Block[] }
|
|
697
|
+
| {
|
|
698
|
+
type: "collapsible";
|
|
699
|
+
label: string;
|
|
700
|
+
open: boolean;
|
|
701
|
+
innerBlocks: Block[];
|
|
702
|
+
}
|
|
703
|
+
| { type: "steps"; innerBlocks: Block[] }
|
|
704
|
+
| {
|
|
705
|
+
type: "card";
|
|
706
|
+
title: string;
|
|
707
|
+
icon: string;
|
|
708
|
+
to: string;
|
|
709
|
+
innerBlocks: Block[];
|
|
710
|
+
}
|
|
711
|
+
| { type: "cardGroup"; cards: Block[] }
|
|
712
|
+
| { type: "codeCollapse"; codeBlocks: Block[] }
|
|
713
|
+
| { type: "codeGroup"; codeBlocks: Block[] }
|
|
714
|
+
| { type: "codePreview"; innerBlocks: Block[]; codeBlocks: Block[] }
|
|
715
|
+
| { type: "codeTree"; files: string }
|
|
716
|
+
| {
|
|
717
|
+
type: "accordion";
|
|
718
|
+
items: { label: string; icon: string; innerBlocks: Block[] }[];
|
|
719
|
+
}
|
|
720
|
+
| {
|
|
721
|
+
type: "tabs";
|
|
722
|
+
items: { label: string; icon: string; innerBlocks: Block[] }[];
|
|
723
|
+
}
|
|
724
|
+
| {
|
|
725
|
+
type: "field";
|
|
726
|
+
name: string;
|
|
727
|
+
fieldType: string;
|
|
728
|
+
required: boolean;
|
|
729
|
+
innerBlocks: Block[];
|
|
730
|
+
}
|
|
731
|
+
| { type: "fieldGroup"; fields: Block[] }
|
|
732
|
+
| {
|
|
733
|
+
type: "image";
|
|
734
|
+
src: string;
|
|
735
|
+
alt: string;
|
|
736
|
+
width?: string;
|
|
737
|
+
height?: string;
|
|
738
|
+
}
|
|
739
|
+
| { type: "docEmbed"; docId: string; seamless?: boolean }
|
|
740
|
+
| { type: "svgEmbed"; svg: string; title: string };
|
|
741
|
+
|
|
742
|
+
function parseTableRow(line: string): string[] {
|
|
743
|
+
const parts = line.split("|");
|
|
744
|
+
return parts.slice(1, parts.length - 1).map((c) => c.trim());
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function isTableSeparator(line: string): boolean {
|
|
748
|
+
return /^\|[\s|:-]+\|$/.test(line.trim());
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function extractFencedCode(lines: string[]): Block[] {
|
|
752
|
+
const result: Block[] = [];
|
|
753
|
+
let i = 0;
|
|
754
|
+
while (i < lines.length) {
|
|
755
|
+
const line = lines[i]!;
|
|
756
|
+
const fenceMatch = line.match(/^(`{3,})(\w*)/);
|
|
757
|
+
if (fenceMatch) {
|
|
758
|
+
const fence = fenceMatch[1]!;
|
|
759
|
+
const lang = fenceMatch[2] ?? "";
|
|
760
|
+
const codeLines: string[] = [];
|
|
761
|
+
i++;
|
|
762
|
+
while (i < lines.length && !lines[i]!.startsWith(fence)) {
|
|
763
|
+
codeLines.push(lines[i]!);
|
|
764
|
+
i++;
|
|
765
|
+
}
|
|
766
|
+
i++;
|
|
767
|
+
result.push({
|
|
768
|
+
type: "codeBlock",
|
|
769
|
+
lang,
|
|
770
|
+
code: codeLines.join("\n"),
|
|
771
|
+
});
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
i++;
|
|
775
|
+
}
|
|
776
|
+
return result;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function parseMdcChildren(
|
|
780
|
+
innerLines: string[],
|
|
781
|
+
slotPrefix: string,
|
|
782
|
+
): { label: string; icon: string; innerBlocks: Block[] }[] {
|
|
783
|
+
const items: { label: string; icon: string; lines: string[] }[] = [];
|
|
784
|
+
let current: { label: string; icon: string; lines: string[] } | null =
|
|
785
|
+
null;
|
|
786
|
+
const slotRe = new RegExp(`^#${slotPrefix}(\\{[^}]*\\})?\\s*$`);
|
|
787
|
+
|
|
788
|
+
for (const line of innerLines) {
|
|
789
|
+
const slotMatch = line.match(slotRe);
|
|
790
|
+
if (slotMatch) {
|
|
791
|
+
if (current) items.push(current);
|
|
792
|
+
const props = parseMdcProps(slotMatch[1]);
|
|
793
|
+
current = {
|
|
794
|
+
label:
|
|
795
|
+
props["label"] ||
|
|
796
|
+
props["title"] ||
|
|
797
|
+
`Item ${items.length + 1}`,
|
|
798
|
+
icon: props["icon"] || "",
|
|
799
|
+
lines: [],
|
|
800
|
+
};
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
if (current) {
|
|
804
|
+
current.lines.push(line);
|
|
805
|
+
} else {
|
|
806
|
+
if (!items.length && !current) {
|
|
807
|
+
current = { label: "Item 1", icon: "", lines: [line] };
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
if (current) items.push(current);
|
|
812
|
+
|
|
813
|
+
return items.map((item) => ({
|
|
814
|
+
label: item.label,
|
|
815
|
+
icon: item.icon,
|
|
816
|
+
innerBlocks: parseBlocks(item.lines.join("\n")),
|
|
817
|
+
}));
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const TASK_RE = /^[-*+]\s+\[([ xX])\]\s+(.*)/;
|
|
821
|
+
|
|
822
|
+
function parseBlocks(markdown: string): Block[] {
|
|
823
|
+
const rawLines = markdown.split("\n");
|
|
824
|
+
let firstContentLine = 0;
|
|
825
|
+
while (firstContentLine < rawLines.length) {
|
|
826
|
+
const l = rawLines[firstContentLine]!;
|
|
827
|
+
if (l.trim() === "" || /^import\s/.test(l) || /^export\s/.test(l)) {
|
|
828
|
+
firstContentLine++;
|
|
829
|
+
} else {
|
|
830
|
+
break;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
const stripped = rawLines.slice(firstContentLine).join("\n");
|
|
834
|
+
|
|
835
|
+
const blocks: Block[] = [];
|
|
836
|
+
const lines = stripped.split("\n");
|
|
837
|
+
let i = 0;
|
|
838
|
+
|
|
839
|
+
while (i < lines.length) {
|
|
840
|
+
const line = lines[i]!;
|
|
841
|
+
|
|
842
|
+
const fenceBlockMatch = line.match(/^(`{3,})(.*)$/);
|
|
843
|
+
if (fenceBlockMatch) {
|
|
844
|
+
const fence = fenceBlockMatch[1]!;
|
|
845
|
+
const lang = fenceBlockMatch[2]!
|
|
846
|
+
.trim()
|
|
847
|
+
.replace(/\{[^}]*\}$/, "")
|
|
848
|
+
.replace(/\s*\[.*\]$/, "")
|
|
849
|
+
.trim();
|
|
850
|
+
const codeLines: string[] = [];
|
|
851
|
+
i++;
|
|
852
|
+
while (i < lines.length && !lines[i]!.startsWith(fence)) {
|
|
853
|
+
codeLines.push(lines[i]!);
|
|
854
|
+
i++;
|
|
855
|
+
}
|
|
856
|
+
i++;
|
|
857
|
+
if (lang === "svg" || lang.startsWith("svg ")) {
|
|
858
|
+
const svgTitle =
|
|
859
|
+
lang === "svg" ? "" : lang.slice(4).trim();
|
|
860
|
+
blocks.push({
|
|
861
|
+
type: "svgEmbed",
|
|
862
|
+
svg: codeLines.join("\n"),
|
|
863
|
+
title: svgTitle,
|
|
864
|
+
});
|
|
865
|
+
} else {
|
|
866
|
+
blocks.push({
|
|
867
|
+
type: "codeBlock",
|
|
868
|
+
lang,
|
|
869
|
+
code: codeLines.join("\n"),
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.*)/);
|
|
876
|
+
if (headingMatch) {
|
|
877
|
+
blocks.push({
|
|
878
|
+
type: "heading",
|
|
879
|
+
level: headingMatch[1]!.length,
|
|
880
|
+
text: headingMatch[2]!.trim(),
|
|
881
|
+
});
|
|
882
|
+
i++;
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (/^[-*_]{3,}\s*$/.test(line)) {
|
|
887
|
+
blocks.push({ type: "hr" });
|
|
888
|
+
i++;
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const docEmbedMatch = line.match(
|
|
893
|
+
/^!\[\[([^\]|]+?)(?:\|[^\]]*?)?\]\](\{[^}]*\})?\s*$/,
|
|
894
|
+
);
|
|
895
|
+
if (docEmbedMatch) {
|
|
896
|
+
const props = parseMdcProps(docEmbedMatch[2]);
|
|
897
|
+
const seamless =
|
|
898
|
+
"seamless" in props ||
|
|
899
|
+
props["seamless"] === "true" ||
|
|
900
|
+
/\{[^}]*\bseamless\b[^}]*\}/.test(docEmbedMatch[2] ?? "");
|
|
901
|
+
blocks.push({
|
|
902
|
+
type: "docEmbed",
|
|
903
|
+
docId: docEmbedMatch[1]!,
|
|
904
|
+
seamless: seamless || undefined,
|
|
905
|
+
});
|
|
906
|
+
i++;
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
const imgMatch = line.match(
|
|
911
|
+
/^!\[([^\]]*)\]\(([^)]+)\)(\{[^}]*\})?\s*$/,
|
|
912
|
+
);
|
|
913
|
+
if (imgMatch) {
|
|
914
|
+
const alt = imgMatch[1] ?? "";
|
|
915
|
+
const src = imgMatch[2] ?? "";
|
|
916
|
+
const attrs = parseMdcProps(imgMatch[3]);
|
|
917
|
+
blocks.push({
|
|
918
|
+
type: "image",
|
|
919
|
+
src,
|
|
920
|
+
alt,
|
|
921
|
+
width: attrs["width"],
|
|
922
|
+
height: attrs["height"],
|
|
923
|
+
});
|
|
924
|
+
i++;
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if (line.startsWith("> ") || line === ">") {
|
|
929
|
+
const bqLines: string[] = [];
|
|
930
|
+
while (
|
|
931
|
+
i < lines.length &&
|
|
932
|
+
(lines[i]!.startsWith("> ") || lines[i] === ">")
|
|
933
|
+
) {
|
|
934
|
+
bqLines.push(lines[i]!.replace(/^>\s?/, ""));
|
|
935
|
+
i++;
|
|
936
|
+
}
|
|
937
|
+
blocks.push({ type: "blockquote", lines: bqLines });
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
if (/^\s*\|/.test(line)) {
|
|
942
|
+
const tableLines: string[] = [];
|
|
943
|
+
while (i < lines.length && /^\s*\|/.test(lines[i]!)) {
|
|
944
|
+
tableLines.push(lines[i]!);
|
|
945
|
+
i++;
|
|
946
|
+
}
|
|
947
|
+
if (
|
|
948
|
+
tableLines.length >= 2 &&
|
|
949
|
+
isTableSeparator(tableLines[1]!)
|
|
950
|
+
) {
|
|
951
|
+
const headerRow = parseTableRow(tableLines[0]!);
|
|
952
|
+
const dataRows = tableLines
|
|
953
|
+
.slice(2)
|
|
954
|
+
.filter((l) => !isTableSeparator(l))
|
|
955
|
+
.map(parseTableRow);
|
|
956
|
+
blocks.push({ type: "table", headerRow, dataRows });
|
|
957
|
+
} else {
|
|
958
|
+
for (const l of tableLines)
|
|
959
|
+
blocks.push({ type: "paragraph", text: l });
|
|
960
|
+
}
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const MDC_OPEN = /^\s*(:{2,})(\w[\w-]*)(\{[^}]*\})?\s*$/;
|
|
965
|
+
if (MDC_OPEN.test(line)) {
|
|
966
|
+
const colons =
|
|
967
|
+
line.match(/^\s*(:+)/)?.[1]?.length ?? 2;
|
|
968
|
+
const componentName =
|
|
969
|
+
line.match(/^\s*:{2,}(\w[\w-]*)/)?.[1] ?? "";
|
|
970
|
+
const innerLines: string[] = [];
|
|
971
|
+
i++;
|
|
972
|
+
while (i < lines.length) {
|
|
973
|
+
const l = lines[i]!;
|
|
974
|
+
if (
|
|
975
|
+
new RegExp(`^\\s*:{${colons}}\\s*$`).test(l)
|
|
976
|
+
) {
|
|
977
|
+
i++;
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
const innerFence = l.match(/^(\s*`{3,})/);
|
|
981
|
+
if (innerFence) {
|
|
982
|
+
const fenceStr = innerFence[1]!.trimStart();
|
|
983
|
+
innerLines.push(l);
|
|
984
|
+
i++;
|
|
985
|
+
while (
|
|
986
|
+
i < lines.length &&
|
|
987
|
+
!lines[i]!.trimStart().startsWith(fenceStr)
|
|
988
|
+
) {
|
|
989
|
+
innerLines.push(lines[i]!);
|
|
990
|
+
i++;
|
|
991
|
+
}
|
|
992
|
+
if (i < lines.length) {
|
|
993
|
+
innerLines.push(lines[i]!);
|
|
994
|
+
i++;
|
|
995
|
+
}
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
innerLines.push(l);
|
|
999
|
+
i++;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const nonBlank = innerLines.filter(
|
|
1003
|
+
(l) => l.trim().length > 0,
|
|
1004
|
+
);
|
|
1005
|
+
if (nonBlank.length) {
|
|
1006
|
+
const minIndent = Math.min(
|
|
1007
|
+
...nonBlank.map(
|
|
1008
|
+
(l) => l.match(/^(\s*)/)?.[1]?.length ?? 0,
|
|
1009
|
+
),
|
|
1010
|
+
);
|
|
1011
|
+
if (minIndent > 0) {
|
|
1012
|
+
for (let j = 0; j < innerLines.length; j++) {
|
|
1013
|
+
innerLines[j] = innerLines[j]!.slice(
|
|
1014
|
+
Math.min(minIndent, innerLines[j]!.length),
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
let contentStart = 0;
|
|
1021
|
+
if (innerLines[0]?.trim() === "---") {
|
|
1022
|
+
const fmEnd = innerLines.findIndex(
|
|
1023
|
+
(l, idx) => idx > 0 && l.trim() === "---",
|
|
1024
|
+
);
|
|
1025
|
+
if (fmEnd !== -1) contentStart = fmEnd + 1;
|
|
1026
|
+
}
|
|
1027
|
+
const contentLines = innerLines.slice(contentStart);
|
|
1028
|
+
|
|
1029
|
+
const defaultSlotLines: string[] = [];
|
|
1030
|
+
const codeSlotLines: string[] = [];
|
|
1031
|
+
let currentSlot: "default" | "code" | "other" = "default";
|
|
1032
|
+
for (const l of contentLines) {
|
|
1033
|
+
if (/^#code\s*$/.test(l)) {
|
|
1034
|
+
currentSlot = "code";
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
if (/^#\w+/.test(l) && !/^#{2,}\s/.test(l)) {
|
|
1038
|
+
currentSlot = "other";
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
if (currentSlot === "default") defaultSlotLines.push(l);
|
|
1042
|
+
else if (currentSlot === "code") codeSlotLines.push(l);
|
|
1043
|
+
}
|
|
1044
|
+
const innerBlocks = parseBlocks(defaultSlotLines.join("\n"));
|
|
1045
|
+
|
|
1046
|
+
const codeBlocks = extractFencedCode(codeSlotLines);
|
|
1047
|
+
|
|
1048
|
+
const CALLOUT_NAMES = new Set([
|
|
1049
|
+
"tip",
|
|
1050
|
+
"note",
|
|
1051
|
+
"info",
|
|
1052
|
+
"warning",
|
|
1053
|
+
"caution",
|
|
1054
|
+
"danger",
|
|
1055
|
+
"callout",
|
|
1056
|
+
"alert",
|
|
1057
|
+
]);
|
|
1058
|
+
if (CALLOUT_NAMES.has(componentName.toLowerCase())) {
|
|
1059
|
+
blocks.push({
|
|
1060
|
+
type: "callout",
|
|
1061
|
+
calloutType: componentName.toLowerCase(),
|
|
1062
|
+
innerBlocks,
|
|
1063
|
+
});
|
|
1064
|
+
} else {
|
|
1065
|
+
const mdcProps = parseMdcProps(
|
|
1066
|
+
line.match(MDC_OPEN)?.[3],
|
|
1067
|
+
);
|
|
1068
|
+
const lc = componentName.toLowerCase();
|
|
1069
|
+
|
|
1070
|
+
if (lc === "collapsible") {
|
|
1071
|
+
blocks.push({
|
|
1072
|
+
type: "collapsible",
|
|
1073
|
+
label: mdcProps["label"] || "Details",
|
|
1074
|
+
open: mdcProps["open"] === "true",
|
|
1075
|
+
innerBlocks,
|
|
1076
|
+
});
|
|
1077
|
+
} else if (lc === "steps") {
|
|
1078
|
+
blocks.push({ type: "steps", innerBlocks });
|
|
1079
|
+
} else if (lc === "card") {
|
|
1080
|
+
blocks.push({
|
|
1081
|
+
type: "card",
|
|
1082
|
+
title: mdcProps["title"] || "",
|
|
1083
|
+
icon: mdcProps["icon"] || "",
|
|
1084
|
+
to: mdcProps["to"] || "",
|
|
1085
|
+
innerBlocks,
|
|
1086
|
+
});
|
|
1087
|
+
} else if (lc === "card-group") {
|
|
1088
|
+
const cards = innerBlocks.filter(
|
|
1089
|
+
(b) => b.type === "card",
|
|
1090
|
+
);
|
|
1091
|
+
if (cards.length) {
|
|
1092
|
+
blocks.push({ type: "cardGroup", cards });
|
|
1093
|
+
} else {
|
|
1094
|
+
blocks.push(...innerBlocks);
|
|
1095
|
+
}
|
|
1096
|
+
} else if (lc === "code-collapse") {
|
|
1097
|
+
blocks.push({
|
|
1098
|
+
type: "codeCollapse",
|
|
1099
|
+
codeBlocks: codeBlocks.length
|
|
1100
|
+
? codeBlocks
|
|
1101
|
+
: innerBlocks.filter(
|
|
1102
|
+
(b) => b.type === "codeBlock",
|
|
1103
|
+
),
|
|
1104
|
+
});
|
|
1105
|
+
} else if (lc === "code-group") {
|
|
1106
|
+
const allCode = [
|
|
1107
|
+
...innerBlocks.filter(
|
|
1108
|
+
(b) => b.type === "codeBlock",
|
|
1109
|
+
),
|
|
1110
|
+
...codeBlocks,
|
|
1111
|
+
];
|
|
1112
|
+
blocks.push({ type: "codeGroup", codeBlocks: allCode });
|
|
1113
|
+
} else if (lc === "code-preview") {
|
|
1114
|
+
blocks.push({
|
|
1115
|
+
type: "codePreview",
|
|
1116
|
+
innerBlocks,
|
|
1117
|
+
codeBlocks,
|
|
1118
|
+
});
|
|
1119
|
+
} else if (lc === "code-tree") {
|
|
1120
|
+
blocks.push({
|
|
1121
|
+
type: "codeTree",
|
|
1122
|
+
files: mdcProps["files"] || "[]",
|
|
1123
|
+
});
|
|
1124
|
+
} else if (lc === "accordion") {
|
|
1125
|
+
const items = parseMdcChildren(
|
|
1126
|
+
contentLines,
|
|
1127
|
+
"item",
|
|
1128
|
+
);
|
|
1129
|
+
if (items.length) {
|
|
1130
|
+
blocks.push({ type: "accordion", items });
|
|
1131
|
+
} else {
|
|
1132
|
+
blocks.push({
|
|
1133
|
+
type: "accordion",
|
|
1134
|
+
items: [
|
|
1135
|
+
{
|
|
1136
|
+
label: "Item 1",
|
|
1137
|
+
icon: "",
|
|
1138
|
+
innerBlocks,
|
|
1139
|
+
},
|
|
1140
|
+
],
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
} else if (lc === "tabs") {
|
|
1144
|
+
const items = parseMdcChildren(
|
|
1145
|
+
contentLines,
|
|
1146
|
+
"tab",
|
|
1147
|
+
);
|
|
1148
|
+
if (items.length) {
|
|
1149
|
+
blocks.push({ type: "tabs", items });
|
|
1150
|
+
} else {
|
|
1151
|
+
blocks.push({
|
|
1152
|
+
type: "tabs",
|
|
1153
|
+
items: [
|
|
1154
|
+
{
|
|
1155
|
+
label: "Tab 1",
|
|
1156
|
+
icon: "",
|
|
1157
|
+
innerBlocks,
|
|
1158
|
+
},
|
|
1159
|
+
],
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
} else if (lc === "field") {
|
|
1163
|
+
blocks.push({
|
|
1164
|
+
type: "field",
|
|
1165
|
+
name: mdcProps["name"] || "",
|
|
1166
|
+
fieldType: mdcProps["type"] || "string",
|
|
1167
|
+
required: mdcProps["required"] === "true",
|
|
1168
|
+
innerBlocks,
|
|
1169
|
+
});
|
|
1170
|
+
} else if (lc === "field-group") {
|
|
1171
|
+
const fields = innerBlocks.filter(
|
|
1172
|
+
(b) => b.type === "field",
|
|
1173
|
+
);
|
|
1174
|
+
if (fields.length) {
|
|
1175
|
+
blocks.push({ type: "fieldGroup", fields });
|
|
1176
|
+
} else {
|
|
1177
|
+
blocks.push(...innerBlocks);
|
|
1178
|
+
}
|
|
1179
|
+
} else {
|
|
1180
|
+
blocks.push(...innerBlocks);
|
|
1181
|
+
blocks.push(...codeBlocks);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
if (TASK_RE.test(line)) {
|
|
1188
|
+
const items: TaskItem[] = [];
|
|
1189
|
+
while (i < lines.length && TASK_RE.test(lines[i]!)) {
|
|
1190
|
+
const m = lines[i]!.match(TASK_RE)!;
|
|
1191
|
+
items.push({
|
|
1192
|
+
checked: m[1]!.toLowerCase() === "x",
|
|
1193
|
+
text: m[2]!,
|
|
1194
|
+
});
|
|
1195
|
+
i++;
|
|
1196
|
+
}
|
|
1197
|
+
blocks.push({ type: "taskList", items });
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
if (/^[-*+]\s+/.test(line)) {
|
|
1202
|
+
const items: string[] = [];
|
|
1203
|
+
while (
|
|
1204
|
+
i < lines.length &&
|
|
1205
|
+
/^[-*+]\s+/.test(lines[i]!) &&
|
|
1206
|
+
!TASK_RE.test(lines[i]!)
|
|
1207
|
+
) {
|
|
1208
|
+
items.push(lines[i]!.replace(/^[-*+]\s+/, ""));
|
|
1209
|
+
i++;
|
|
1210
|
+
}
|
|
1211
|
+
if (items.length) {
|
|
1212
|
+
blocks.push({ type: "bulletList", items });
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
if (/^\d+\.\s+/.test(line)) {
|
|
1218
|
+
const items: string[] = [];
|
|
1219
|
+
while (i < lines.length && /^\d+\.\s+/.test(lines[i]!)) {
|
|
1220
|
+
items.push(lines[i]!.replace(/^\d+\.\s+/, ""));
|
|
1221
|
+
i++;
|
|
1222
|
+
}
|
|
1223
|
+
blocks.push({ type: "orderedList", items });
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
if (line.trim() === "") {
|
|
1228
|
+
i++;
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
const paraLines: string[] = [];
|
|
1233
|
+
while (
|
|
1234
|
+
i < lines.length &&
|
|
1235
|
+
lines[i]!.trim() !== "" &&
|
|
1236
|
+
!/^(#{1,6}\s|[-*+]\s|\d+\.\s|>|`{3,}|\s*\||[-*_]{3,}\s*$|\s*:{2,}\w)/.test(
|
|
1237
|
+
lines[i]!,
|
|
1238
|
+
)
|
|
1239
|
+
) {
|
|
1240
|
+
paraLines.push(lines[i]!);
|
|
1241
|
+
i++;
|
|
1242
|
+
}
|
|
1243
|
+
if (paraLines.length) {
|
|
1244
|
+
blocks.push({ type: "paragraph", text: paraLines.join(" ") });
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
return blocks;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// ── Y.js content population helpers ──────────────────────────────────────────
|
|
1252
|
+
|
|
1253
|
+
function fillTextInto(
|
|
1254
|
+
el: Y.XmlElement,
|
|
1255
|
+
tokens: InlineToken[],
|
|
1256
|
+
): void {
|
|
1257
|
+
const filtered = tokens.filter((t) => t.node || t.text.length > 0);
|
|
1258
|
+
if (!filtered.length) return;
|
|
1259
|
+
|
|
1260
|
+
const children: (Y.XmlText | Y.XmlElement)[] = filtered.map((tok) => {
|
|
1261
|
+
if (tok.node) {
|
|
1262
|
+
const xe = new Y.XmlElement(tok.node);
|
|
1263
|
+
if (tok.nodeAttrs) {
|
|
1264
|
+
for (const [k, v] of Object.entries(tok.nodeAttrs))
|
|
1265
|
+
xe.setAttribute(k, v);
|
|
1266
|
+
}
|
|
1267
|
+
return xe;
|
|
1268
|
+
}
|
|
1269
|
+
return new Y.XmlText();
|
|
1270
|
+
});
|
|
1271
|
+
el.insert(0, children);
|
|
1272
|
+
|
|
1273
|
+
filtered.forEach((tok, i) => {
|
|
1274
|
+
if (tok.node) return;
|
|
1275
|
+
const xt = children[i] as Y.XmlText;
|
|
1276
|
+
if (tok.attrs) {
|
|
1277
|
+
xt.insert(
|
|
1278
|
+
0,
|
|
1279
|
+
tok.text,
|
|
1280
|
+
tok.attrs as Record<string, boolean | object>,
|
|
1281
|
+
);
|
|
1282
|
+
} else {
|
|
1283
|
+
xt.insert(0, tok.text);
|
|
1284
|
+
}
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function blockElName(b: Block): string {
|
|
1289
|
+
switch (b.type) {
|
|
1290
|
+
case "heading":
|
|
1291
|
+
return "heading";
|
|
1292
|
+
case "paragraph":
|
|
1293
|
+
return "paragraph";
|
|
1294
|
+
case "bulletList":
|
|
1295
|
+
return "bulletList";
|
|
1296
|
+
case "orderedList":
|
|
1297
|
+
return "orderedList";
|
|
1298
|
+
case "taskList":
|
|
1299
|
+
return "taskList";
|
|
1300
|
+
case "codeBlock":
|
|
1301
|
+
return "codeBlock";
|
|
1302
|
+
case "blockquote":
|
|
1303
|
+
return "blockquote";
|
|
1304
|
+
case "table":
|
|
1305
|
+
return "table";
|
|
1306
|
+
case "hr":
|
|
1307
|
+
return "horizontalRule";
|
|
1308
|
+
case "callout":
|
|
1309
|
+
return "callout";
|
|
1310
|
+
case "collapsible":
|
|
1311
|
+
return "collapsible";
|
|
1312
|
+
case "steps":
|
|
1313
|
+
return "steps";
|
|
1314
|
+
case "card":
|
|
1315
|
+
return "card";
|
|
1316
|
+
case "cardGroup":
|
|
1317
|
+
return "cardGroup";
|
|
1318
|
+
case "codeCollapse":
|
|
1319
|
+
return "codeCollapse";
|
|
1320
|
+
case "codeGroup":
|
|
1321
|
+
return "codeGroup";
|
|
1322
|
+
case "codePreview":
|
|
1323
|
+
return "codePreview";
|
|
1324
|
+
case "codeTree":
|
|
1325
|
+
return "codeTree";
|
|
1326
|
+
case "accordion":
|
|
1327
|
+
return "accordion";
|
|
1328
|
+
case "tabs":
|
|
1329
|
+
return "tabs";
|
|
1330
|
+
case "field":
|
|
1331
|
+
return "field";
|
|
1332
|
+
case "fieldGroup":
|
|
1333
|
+
return "fieldGroup";
|
|
1334
|
+
case "image":
|
|
1335
|
+
return "image";
|
|
1336
|
+
case "docEmbed":
|
|
1337
|
+
return "docEmbed";
|
|
1338
|
+
case "svgEmbed":
|
|
1339
|
+
return "svgEmbed";
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
function fillBlock(el: Y.XmlElement, block: Block): void {
|
|
1344
|
+
switch (block.type) {
|
|
1345
|
+
case "heading": {
|
|
1346
|
+
el.setAttribute("level", block.level as any);
|
|
1347
|
+
fillTextInto(el, parseInline(block.text));
|
|
1348
|
+
break;
|
|
1349
|
+
}
|
|
1350
|
+
case "paragraph": {
|
|
1351
|
+
fillTextInto(el, parseInline(block.text));
|
|
1352
|
+
break;
|
|
1353
|
+
}
|
|
1354
|
+
case "bulletList":
|
|
1355
|
+
case "orderedList": {
|
|
1356
|
+
const listItemEls = block.items.map(
|
|
1357
|
+
() => new Y.XmlElement("listItem"),
|
|
1358
|
+
);
|
|
1359
|
+
el.insert(0, listItemEls);
|
|
1360
|
+
block.items.forEach((text, i) => {
|
|
1361
|
+
const paraEl = new Y.XmlElement("paragraph");
|
|
1362
|
+
listItemEls[i]!.insert(0, [paraEl]);
|
|
1363
|
+
fillTextInto(paraEl, parseInline(text));
|
|
1364
|
+
});
|
|
1365
|
+
break;
|
|
1366
|
+
}
|
|
1367
|
+
case "taskList": {
|
|
1368
|
+
const taskItemEls = block.items.map(
|
|
1369
|
+
() => new Y.XmlElement("taskItem"),
|
|
1370
|
+
);
|
|
1371
|
+
el.insert(0, taskItemEls);
|
|
1372
|
+
block.items.forEach((item, i) => {
|
|
1373
|
+
taskItemEls[i]!.setAttribute(
|
|
1374
|
+
"checked",
|
|
1375
|
+
item.checked as any,
|
|
1376
|
+
);
|
|
1377
|
+
const paraEl = new Y.XmlElement("paragraph");
|
|
1378
|
+
taskItemEls[i]!.insert(0, [paraEl]);
|
|
1379
|
+
fillTextInto(paraEl, parseInline(item.text));
|
|
1380
|
+
});
|
|
1381
|
+
break;
|
|
1382
|
+
}
|
|
1383
|
+
case "codeBlock": {
|
|
1384
|
+
if (block.lang) el.setAttribute("language", block.lang);
|
|
1385
|
+
const xt = new Y.XmlText();
|
|
1386
|
+
el.insert(0, [xt]);
|
|
1387
|
+
xt.insert(0, block.code);
|
|
1388
|
+
break;
|
|
1389
|
+
}
|
|
1390
|
+
case "blockquote": {
|
|
1391
|
+
const paraEls = block.lines.map(
|
|
1392
|
+
() => new Y.XmlElement("paragraph"),
|
|
1393
|
+
);
|
|
1394
|
+
el.insert(0, paraEls);
|
|
1395
|
+
block.lines.forEach((line, i) =>
|
|
1396
|
+
fillTextInto(paraEls[i]!, parseInline(line)),
|
|
1397
|
+
);
|
|
1398
|
+
break;
|
|
1399
|
+
}
|
|
1400
|
+
case "table": {
|
|
1401
|
+
const headerRowEl = new Y.XmlElement("tableRow");
|
|
1402
|
+
const dataRowEls = block.dataRows.map(
|
|
1403
|
+
() => new Y.XmlElement("tableRow"),
|
|
1404
|
+
);
|
|
1405
|
+
el.insert(0, [headerRowEl, ...dataRowEls]);
|
|
1406
|
+
|
|
1407
|
+
const headerCellEls = block.headerRow.map(
|
|
1408
|
+
() => new Y.XmlElement("tableHeader"),
|
|
1409
|
+
);
|
|
1410
|
+
headerRowEl.insert(0, headerCellEls);
|
|
1411
|
+
block.headerRow.forEach((cellText, i) => {
|
|
1412
|
+
const paraEl = new Y.XmlElement("paragraph");
|
|
1413
|
+
headerCellEls[i]!.insert(0, [paraEl]);
|
|
1414
|
+
fillTextInto(paraEl, parseInline(cellText));
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
block.dataRows.forEach((row, ri) => {
|
|
1418
|
+
const cellEls = row.map(
|
|
1419
|
+
() => new Y.XmlElement("tableCell"),
|
|
1420
|
+
);
|
|
1421
|
+
dataRowEls[ri]!.insert(0, cellEls);
|
|
1422
|
+
row.forEach((cellText, ci) => {
|
|
1423
|
+
const paraEl = new Y.XmlElement("paragraph");
|
|
1424
|
+
cellEls[ci]!.insert(0, [paraEl]);
|
|
1425
|
+
fillTextInto(paraEl, parseInline(cellText));
|
|
1426
|
+
});
|
|
1427
|
+
});
|
|
1428
|
+
break;
|
|
1429
|
+
}
|
|
1430
|
+
case "hr":
|
|
1431
|
+
break;
|
|
1432
|
+
case "callout": {
|
|
1433
|
+
el.setAttribute("type", block.calloutType);
|
|
1434
|
+
if (!block.innerBlocks.length) {
|
|
1435
|
+
const paraEl = new Y.XmlElement("paragraph");
|
|
1436
|
+
el.insert(0, [paraEl]);
|
|
1437
|
+
break;
|
|
1438
|
+
}
|
|
1439
|
+
const innerEls = block.innerBlocks.map(
|
|
1440
|
+
(b) => new Y.XmlElement(blockElName(b)),
|
|
1441
|
+
);
|
|
1442
|
+
el.insert(0, innerEls);
|
|
1443
|
+
block.innerBlocks.forEach((b, i) =>
|
|
1444
|
+
fillBlock(innerEls[i]!, b),
|
|
1445
|
+
);
|
|
1446
|
+
break;
|
|
1447
|
+
}
|
|
1448
|
+
case "collapsible": {
|
|
1449
|
+
el.setAttribute("label", block.label);
|
|
1450
|
+
el.setAttribute("open", block.open as any);
|
|
1451
|
+
const inner = block.innerBlocks.length
|
|
1452
|
+
? block.innerBlocks
|
|
1453
|
+
: [{ type: "paragraph" as const, text: "" }];
|
|
1454
|
+
const innerEls = inner.map(
|
|
1455
|
+
(b) => new Y.XmlElement(blockElName(b)),
|
|
1456
|
+
);
|
|
1457
|
+
el.insert(0, innerEls);
|
|
1458
|
+
inner.forEach((b, i) => fillBlock(innerEls[i]!, b));
|
|
1459
|
+
break;
|
|
1460
|
+
}
|
|
1461
|
+
case "steps": {
|
|
1462
|
+
const inner = block.innerBlocks.length
|
|
1463
|
+
? block.innerBlocks
|
|
1464
|
+
: [{ type: "paragraph" as const, text: "" }];
|
|
1465
|
+
const innerEls = inner.map(
|
|
1466
|
+
(b) => new Y.XmlElement(blockElName(b)),
|
|
1467
|
+
);
|
|
1468
|
+
el.insert(0, innerEls);
|
|
1469
|
+
inner.forEach((b, i) => fillBlock(innerEls[i]!, b));
|
|
1470
|
+
break;
|
|
1471
|
+
}
|
|
1472
|
+
case "card": {
|
|
1473
|
+
if (block.title) el.setAttribute("title", block.title);
|
|
1474
|
+
if (block.icon) el.setAttribute("icon", block.icon);
|
|
1475
|
+
if (block.to) el.setAttribute("to", block.to);
|
|
1476
|
+
const inner = block.innerBlocks.length
|
|
1477
|
+
? block.innerBlocks
|
|
1478
|
+
: [{ type: "paragraph" as const, text: "" }];
|
|
1479
|
+
const innerEls = inner.map(
|
|
1480
|
+
(b) => new Y.XmlElement(blockElName(b)),
|
|
1481
|
+
);
|
|
1482
|
+
el.insert(0, innerEls);
|
|
1483
|
+
inner.forEach((b, i) => fillBlock(innerEls[i]!, b));
|
|
1484
|
+
break;
|
|
1485
|
+
}
|
|
1486
|
+
case "cardGroup": {
|
|
1487
|
+
const cardEls = block.cards.map(
|
|
1488
|
+
(b) => new Y.XmlElement(blockElName(b)),
|
|
1489
|
+
);
|
|
1490
|
+
el.insert(0, cardEls);
|
|
1491
|
+
block.cards.forEach((b, i) => fillBlock(cardEls[i]!, b));
|
|
1492
|
+
break;
|
|
1493
|
+
}
|
|
1494
|
+
case "codeCollapse": {
|
|
1495
|
+
const codes = block.codeBlocks.length
|
|
1496
|
+
? block.codeBlocks
|
|
1497
|
+
: [{ type: "codeBlock" as const, lang: "", code: "" }];
|
|
1498
|
+
const codeEl = new Y.XmlElement("codeBlock");
|
|
1499
|
+
el.insert(0, [codeEl]);
|
|
1500
|
+
fillBlock(codeEl, codes[0]!);
|
|
1501
|
+
break;
|
|
1502
|
+
}
|
|
1503
|
+
case "codeGroup": {
|
|
1504
|
+
const codes = block.codeBlocks.length
|
|
1505
|
+
? block.codeBlocks
|
|
1506
|
+
: [{ type: "codeBlock" as const, lang: "", code: "" }];
|
|
1507
|
+
const codeEls = codes.map(() => new Y.XmlElement("codeBlock"));
|
|
1508
|
+
el.insert(0, codeEls);
|
|
1509
|
+
codes.forEach((b, i) => fillBlock(codeEls[i]!, b));
|
|
1510
|
+
break;
|
|
1511
|
+
}
|
|
1512
|
+
case "codePreview": {
|
|
1513
|
+
const all = [...block.innerBlocks, ...block.codeBlocks];
|
|
1514
|
+
const inner = all.length
|
|
1515
|
+
? all
|
|
1516
|
+
: [{ type: "paragraph" as const, text: "" }];
|
|
1517
|
+
const innerEls = inner.map(
|
|
1518
|
+
(b) => new Y.XmlElement(blockElName(b)),
|
|
1519
|
+
);
|
|
1520
|
+
el.insert(0, innerEls);
|
|
1521
|
+
inner.forEach((b, i) => fillBlock(innerEls[i]!, b));
|
|
1522
|
+
break;
|
|
1523
|
+
}
|
|
1524
|
+
case "codeTree": {
|
|
1525
|
+
el.setAttribute("files", block.files);
|
|
1526
|
+
break;
|
|
1527
|
+
}
|
|
1528
|
+
case "accordion": {
|
|
1529
|
+
const itemEls = block.items.map(
|
|
1530
|
+
() => new Y.XmlElement("accordionItem"),
|
|
1531
|
+
);
|
|
1532
|
+
el.insert(0, itemEls);
|
|
1533
|
+
block.items.forEach((item, i) => {
|
|
1534
|
+
itemEls[i]!.setAttribute("label", item.label);
|
|
1535
|
+
if (item.icon)
|
|
1536
|
+
itemEls[i]!.setAttribute("icon", item.icon);
|
|
1537
|
+
const inner = item.innerBlocks.length
|
|
1538
|
+
? item.innerBlocks
|
|
1539
|
+
: [{ type: "paragraph" as const, text: "" }];
|
|
1540
|
+
const childEls = inner.map(
|
|
1541
|
+
(b) => new Y.XmlElement(blockElName(b)),
|
|
1542
|
+
);
|
|
1543
|
+
itemEls[i]!.insert(0, childEls);
|
|
1544
|
+
inner.forEach((b, ci) => fillBlock(childEls[ci]!, b));
|
|
1545
|
+
});
|
|
1546
|
+
break;
|
|
1547
|
+
}
|
|
1548
|
+
case "tabs": {
|
|
1549
|
+
const itemEls = block.items.map(
|
|
1550
|
+
() => new Y.XmlElement("tabsItem"),
|
|
1551
|
+
);
|
|
1552
|
+
el.insert(0, itemEls);
|
|
1553
|
+
block.items.forEach((item, i) => {
|
|
1554
|
+
itemEls[i]!.setAttribute("label", item.label);
|
|
1555
|
+
if (item.icon)
|
|
1556
|
+
itemEls[i]!.setAttribute("icon", item.icon);
|
|
1557
|
+
const inner = item.innerBlocks.length
|
|
1558
|
+
? item.innerBlocks
|
|
1559
|
+
: [{ type: "paragraph" as const, text: "" }];
|
|
1560
|
+
const childEls = inner.map(
|
|
1561
|
+
(b) => new Y.XmlElement(blockElName(b)),
|
|
1562
|
+
);
|
|
1563
|
+
itemEls[i]!.insert(0, childEls);
|
|
1564
|
+
inner.forEach((b, ci) => fillBlock(childEls[ci]!, b));
|
|
1565
|
+
});
|
|
1566
|
+
break;
|
|
1567
|
+
}
|
|
1568
|
+
case "field": {
|
|
1569
|
+
if (block.name) el.setAttribute("name", block.name);
|
|
1570
|
+
el.setAttribute("type", block.fieldType);
|
|
1571
|
+
el.setAttribute("required", block.required as any);
|
|
1572
|
+
const inner = block.innerBlocks.length
|
|
1573
|
+
? block.innerBlocks
|
|
1574
|
+
: [{ type: "paragraph" as const, text: "" }];
|
|
1575
|
+
const innerEls = inner.map(
|
|
1576
|
+
(b) => new Y.XmlElement(blockElName(b)),
|
|
1577
|
+
);
|
|
1578
|
+
el.insert(0, innerEls);
|
|
1579
|
+
inner.forEach((b, i) => fillBlock(innerEls[i]!, b));
|
|
1580
|
+
break;
|
|
1581
|
+
}
|
|
1582
|
+
case "fieldGroup": {
|
|
1583
|
+
const fieldEls = block.fields.map(
|
|
1584
|
+
(b) => new Y.XmlElement(blockElName(b)),
|
|
1585
|
+
);
|
|
1586
|
+
el.insert(0, fieldEls);
|
|
1587
|
+
block.fields.forEach((b, i) => fillBlock(fieldEls[i]!, b));
|
|
1588
|
+
break;
|
|
1589
|
+
}
|
|
1590
|
+
case "image": {
|
|
1591
|
+
el.setAttribute("src", block.src);
|
|
1592
|
+
if (block.alt) el.setAttribute("alt", block.alt);
|
|
1593
|
+
if (block.width) el.setAttribute("width", block.width);
|
|
1594
|
+
if (block.height) el.setAttribute("height", block.height);
|
|
1595
|
+
break;
|
|
1596
|
+
}
|
|
1597
|
+
case "docEmbed": {
|
|
1598
|
+
el.setAttribute("docId", block.docId);
|
|
1599
|
+
if (block.seamless) el.setAttribute("seamless", "true");
|
|
1600
|
+
break;
|
|
1601
|
+
}
|
|
1602
|
+
case "svgEmbed": {
|
|
1603
|
+
el.setAttribute("svg", block.svg);
|
|
1604
|
+
if (block.title) el.setAttribute("title", block.title);
|
|
1605
|
+
break;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// ── Public: Markdown → Y.js ─────────────────────────────────────────────────
|
|
1611
|
+
|
|
1612
|
+
export function populateYDocFromMarkdown(
|
|
1613
|
+
fragment: Y.XmlFragment,
|
|
1614
|
+
markdown: string,
|
|
1615
|
+
fallbackTitle = "Untitled",
|
|
1616
|
+
): void {
|
|
1617
|
+
const ydoc = fragment.doc;
|
|
1618
|
+
if (!ydoc) {
|
|
1619
|
+
console.warn(
|
|
1620
|
+
"[markdownToYjs] fragment has no doc — skipping population",
|
|
1621
|
+
);
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
const blocks = parseBlocks(markdown);
|
|
1626
|
+
|
|
1627
|
+
let title = fallbackTitle;
|
|
1628
|
+
let contentBlocks = blocks;
|
|
1629
|
+
const h1 = blocks.findIndex(
|
|
1630
|
+
(b) => b.type === "heading" && b.level === 1,
|
|
1631
|
+
);
|
|
1632
|
+
if (h1 !== -1) {
|
|
1633
|
+
title = (
|
|
1634
|
+
blocks[h1] as { type: "heading"; level: number; text: string }
|
|
1635
|
+
).text;
|
|
1636
|
+
contentBlocks = blocks.filter((_, i) => i !== h1);
|
|
1637
|
+
}
|
|
1638
|
+
if (!contentBlocks.length)
|
|
1639
|
+
contentBlocks = [{ type: "paragraph", text: "" }];
|
|
1640
|
+
|
|
1641
|
+
ydoc.transact(() => {
|
|
1642
|
+
const headerEl = new Y.XmlElement("documentHeader");
|
|
1643
|
+
const metaEl = new Y.XmlElement("documentMeta");
|
|
1644
|
+
const bodyEls: Y.XmlElement[] = contentBlocks.map((b) => {
|
|
1645
|
+
switch (b.type) {
|
|
1646
|
+
case "heading":
|
|
1647
|
+
return new Y.XmlElement("heading");
|
|
1648
|
+
case "paragraph":
|
|
1649
|
+
return new Y.XmlElement("paragraph");
|
|
1650
|
+
case "bulletList":
|
|
1651
|
+
return new Y.XmlElement("bulletList");
|
|
1652
|
+
case "orderedList":
|
|
1653
|
+
return new Y.XmlElement("orderedList");
|
|
1654
|
+
case "taskList":
|
|
1655
|
+
return new Y.XmlElement("taskList");
|
|
1656
|
+
case "codeBlock":
|
|
1657
|
+
return new Y.XmlElement("codeBlock");
|
|
1658
|
+
case "blockquote":
|
|
1659
|
+
return new Y.XmlElement("blockquote");
|
|
1660
|
+
case "table":
|
|
1661
|
+
return new Y.XmlElement("table");
|
|
1662
|
+
case "hr":
|
|
1663
|
+
return new Y.XmlElement("horizontalRule");
|
|
1664
|
+
case "callout":
|
|
1665
|
+
return new Y.XmlElement("callout");
|
|
1666
|
+
case "collapsible":
|
|
1667
|
+
return new Y.XmlElement("collapsible");
|
|
1668
|
+
case "steps":
|
|
1669
|
+
return new Y.XmlElement("steps");
|
|
1670
|
+
case "card":
|
|
1671
|
+
return new Y.XmlElement("card");
|
|
1672
|
+
case "cardGroup":
|
|
1673
|
+
return new Y.XmlElement("cardGroup");
|
|
1674
|
+
case "codeCollapse":
|
|
1675
|
+
return new Y.XmlElement("codeCollapse");
|
|
1676
|
+
case "codeGroup":
|
|
1677
|
+
return new Y.XmlElement("codeGroup");
|
|
1678
|
+
case "codePreview":
|
|
1679
|
+
return new Y.XmlElement("codePreview");
|
|
1680
|
+
case "codeTree":
|
|
1681
|
+
return new Y.XmlElement("codeTree");
|
|
1682
|
+
case "accordion":
|
|
1683
|
+
return new Y.XmlElement("accordion");
|
|
1684
|
+
case "tabs":
|
|
1685
|
+
return new Y.XmlElement("tabs");
|
|
1686
|
+
case "field":
|
|
1687
|
+
return new Y.XmlElement("field");
|
|
1688
|
+
case "fieldGroup":
|
|
1689
|
+
return new Y.XmlElement("fieldGroup");
|
|
1690
|
+
case "image":
|
|
1691
|
+
return new Y.XmlElement("image");
|
|
1692
|
+
case "docEmbed":
|
|
1693
|
+
return new Y.XmlElement("docEmbed");
|
|
1694
|
+
case "svgEmbed":
|
|
1695
|
+
return new Y.XmlElement("svgEmbed");
|
|
1696
|
+
}
|
|
1697
|
+
});
|
|
1698
|
+
|
|
1699
|
+
fragment.insert(0, [headerEl, metaEl, ...bodyEls]);
|
|
1700
|
+
|
|
1701
|
+
const headerXt = new Y.XmlText();
|
|
1702
|
+
headerEl.insert(0, [headerXt]);
|
|
1703
|
+
headerXt.insert(0, title);
|
|
1704
|
+
|
|
1705
|
+
contentBlocks.forEach((block, i) => fillBlock(bodyEls[i]!, block));
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
// ── Block element builders (exported for ContentManager and SDK consumers) ────
|
|
1710
|
+
|
|
1711
|
+
export function buildHeadingElement(text: string, level: 1|2|3|4|5|6 = 1): Y.XmlElement {
|
|
1712
|
+
const el = new Y.XmlElement("heading");
|
|
1713
|
+
el.setAttribute("level", level as any);
|
|
1714
|
+
fillTextInto(el, parseInline(text));
|
|
1715
|
+
return el;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
export function buildParagraphElement(text: string): Y.XmlElement {
|
|
1719
|
+
const el = new Y.XmlElement("paragraph");
|
|
1720
|
+
fillTextInto(el, parseInline(text));
|
|
1721
|
+
return el;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
export function buildBulletListElement(items: string[]): Y.XmlElement {
|
|
1725
|
+
const el = new Y.XmlElement("bulletList");
|
|
1726
|
+
const itemEls = items.map(() => new Y.XmlElement("listItem"));
|
|
1727
|
+
el.insert(0, itemEls);
|
|
1728
|
+
items.forEach((text, i) => {
|
|
1729
|
+
const paraEl = new Y.XmlElement("paragraph");
|
|
1730
|
+
itemEls[i]!.insert(0, [paraEl]);
|
|
1731
|
+
fillTextInto(paraEl, parseInline(text));
|
|
1732
|
+
});
|
|
1733
|
+
return el;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
export function buildOrderedListElement(items: string[]): Y.XmlElement {
|
|
1737
|
+
const el = new Y.XmlElement("orderedList");
|
|
1738
|
+
const itemEls = items.map(() => new Y.XmlElement("listItem"));
|
|
1739
|
+
el.insert(0, itemEls);
|
|
1740
|
+
items.forEach((text, i) => {
|
|
1741
|
+
const paraEl = new Y.XmlElement("paragraph");
|
|
1742
|
+
itemEls[i]!.insert(0, [paraEl]);
|
|
1743
|
+
fillTextInto(paraEl, parseInline(text));
|
|
1744
|
+
});
|
|
1745
|
+
return el;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
export function buildTaskListElement(
|
|
1749
|
+
items: Array<{ text: string; checked?: boolean }>,
|
|
1750
|
+
): Y.XmlElement {
|
|
1751
|
+
const el = new Y.XmlElement("taskList");
|
|
1752
|
+
const itemEls = items.map(() => new Y.XmlElement("taskItem"));
|
|
1753
|
+
el.insert(0, itemEls);
|
|
1754
|
+
items.forEach((item, i) => {
|
|
1755
|
+
itemEls[i]!.setAttribute("checked", (item.checked ?? false) as any);
|
|
1756
|
+
const paraEl = new Y.XmlElement("paragraph");
|
|
1757
|
+
itemEls[i]!.insert(0, [paraEl]);
|
|
1758
|
+
fillTextInto(paraEl, parseInline(item.text));
|
|
1759
|
+
});
|
|
1760
|
+
return el;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
export function buildCodeBlockElement(code: string, language?: string): Y.XmlElement {
|
|
1764
|
+
const el = new Y.XmlElement("codeBlock");
|
|
1765
|
+
if (language) el.setAttribute("language", language);
|
|
1766
|
+
const xt = new Y.XmlText();
|
|
1767
|
+
el.insert(0, [xt]);
|
|
1768
|
+
xt.insert(0, code);
|
|
1769
|
+
return el;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
export function buildBlockquoteElement(text: string): Y.XmlElement {
|
|
1773
|
+
const el = new Y.XmlElement("blockquote");
|
|
1774
|
+
const paraEl = new Y.XmlElement("paragraph");
|
|
1775
|
+
el.insert(0, [paraEl]);
|
|
1776
|
+
fillTextInto(paraEl, parseInline(text));
|
|
1777
|
+
return el;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
export function buildHorizontalRuleElement(): Y.XmlElement {
|
|
1781
|
+
return new Y.XmlElement("horizontalRule");
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
/** Parse markdown into block Y.XmlElements (no header/meta). */
|
|
1785
|
+
export function buildBlocksFromMarkdown(markdown: string): Y.XmlElement[] {
|
|
1786
|
+
const blocks = parseBlocks(markdown);
|
|
1787
|
+
return blocks
|
|
1788
|
+
.filter((b) => b.type !== "heading" || (b as any).level !== 1)
|
|
1789
|
+
.map((b) => {
|
|
1790
|
+
const el = new Y.XmlElement(blockElName(b));
|
|
1791
|
+
fillBlock(el, b);
|
|
1792
|
+
return el;
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
// ── Block reader ──────────────────────────────────────────────────────────────
|
|
1797
|
+
|
|
1798
|
+
export interface DocumentBlock {
|
|
1799
|
+
type: string;
|
|
1800
|
+
attrs: Record<string, unknown>;
|
|
1801
|
+
text: string;
|
|
1802
|
+
items?: string[];
|
|
1803
|
+
children?: DocumentBlock[];
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
export function readBlocksFromFragment(fragment: Y.XmlFragment): DocumentBlock[] {
|
|
1807
|
+
const result: DocumentBlock[] = [];
|
|
1808
|
+
for (let i = 0; i < fragment.length; i++) {
|
|
1809
|
+
const child = fragment.get(i);
|
|
1810
|
+
if (!(child instanceof Y.XmlElement)) continue;
|
|
1811
|
+
const name = child.nodeName;
|
|
1812
|
+
if (name === "documentHeader" || name === "documentMeta") continue;
|
|
1813
|
+
result.push(_xmlElToBlock(child));
|
|
1814
|
+
}
|
|
1815
|
+
return result;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
function _xmlElToBlock(el: Y.XmlElement): DocumentBlock {
|
|
1819
|
+
const name = el.nodeName;
|
|
1820
|
+
switch (name) {
|
|
1821
|
+
case "heading":
|
|
1822
|
+
return { type: "heading", attrs: { level: el.getAttribute("level") }, text: elementTextContent(el) };
|
|
1823
|
+
case "paragraph":
|
|
1824
|
+
case "blockquote":
|
|
1825
|
+
case "horizontalRule":
|
|
1826
|
+
return { type: name, attrs: {}, text: elementTextContent(el) };
|
|
1827
|
+
case "codeBlock": {
|
|
1828
|
+
const lang = el.getAttribute("language");
|
|
1829
|
+
return { type: "codeBlock", attrs: lang ? { language: lang } : {}, text: elementTextContent(el) };
|
|
1830
|
+
}
|
|
1831
|
+
case "bulletList":
|
|
1832
|
+
case "orderedList": {
|
|
1833
|
+
const items: string[] = [];
|
|
1834
|
+
for (let i = 0; i < el.length; i++) {
|
|
1835
|
+
const item = el.get(i);
|
|
1836
|
+
if (item instanceof Y.XmlElement && item.nodeName === "listItem")
|
|
1837
|
+
items.push(elementTextContent(item));
|
|
1838
|
+
}
|
|
1839
|
+
return { type: name, attrs: {}, text: "", items };
|
|
1840
|
+
}
|
|
1841
|
+
case "taskList": {
|
|
1842
|
+
const items: string[] = [];
|
|
1843
|
+
const children: DocumentBlock[] = [];
|
|
1844
|
+
for (let i = 0; i < el.length; i++) {
|
|
1845
|
+
const item = el.get(i);
|
|
1846
|
+
if (item instanceof Y.XmlElement && item.nodeName === "taskItem") {
|
|
1847
|
+
const checked = item.getAttribute("checked");
|
|
1848
|
+
const text = elementTextContent(item);
|
|
1849
|
+
items.push(text);
|
|
1850
|
+
children.push({
|
|
1851
|
+
type: "taskItem",
|
|
1852
|
+
attrs: { checked: checked === "true" || (checked as unknown) === true },
|
|
1853
|
+
text,
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
return { type: "taskList", attrs: {}, text: "", items, children };
|
|
1858
|
+
}
|
|
1859
|
+
default:
|
|
1860
|
+
return { type: name, attrs: {}, text: elementTextContent(el) };
|
|
1861
|
+
}
|
|
1862
|
+
}
|