@abraca/convert 2.3.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.
@@ -0,0 +1,3237 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
12
+ key = keys[i];
13
+ if (!__hasOwnProp.call(to, key) && key !== except) {
14
+ __defProp(to, key, {
15
+ get: ((k) => from[k]).bind(null, key),
16
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
17
+ });
18
+ }
19
+ }
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
24
+ value: mod,
25
+ enumerable: true
26
+ }) : target, mod));
27
+
28
+ //#endregion
29
+ let yjs = require("yjs");
30
+ yjs = __toESM(yjs);
31
+
32
+ //#region packages/convert/src/markdown-to-yjs.ts
33
+ /**
34
+ * Converts a filename (without extension) to a human-readable label.
35
+ *
36
+ * - `this-is-a-doc` → `"This is a doc"` (kebab/snake: sentence case)
37
+ * - `ThisIsADoc` → `"This Is A Doc"` (PascalCase: preserves word caps)
38
+ * - `thisIsADoc` → `"This Is A Doc"` (camelCase: preserves word caps)
39
+ */
40
+ function filenameToLabel(raw) {
41
+ const clean = raw.replace(/\.[^.]+$/, "").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[-_.]+/g, " ").replace(/\s+/g, " ").trim();
42
+ return clean.charAt(0).toUpperCase() + clean.slice(1);
43
+ }
44
+ function parseInlineArray(raw) {
45
+ return raw.slice(1, -1).split(",").map((s) => s.trim()).filter(Boolean);
46
+ }
47
+ function stripQuotes(s) {
48
+ if (s.length >= 2 && (s.startsWith("\"") && s.endsWith("\"") || s.startsWith("'") && s.endsWith("'"))) return s.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
49
+ return s;
50
+ }
51
+ function parseFrontmatter(markdown) {
52
+ const noResult = {
53
+ meta: {},
54
+ body: markdown
55
+ };
56
+ const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
57
+ if (!match) return noResult;
58
+ const yamlBlock = match[1];
59
+ const body = markdown.slice(match[0].length);
60
+ const raw = {};
61
+ const lines = yamlBlock.split("\n");
62
+ let i = 0;
63
+ while (i < lines.length) {
64
+ const line = lines[i];
65
+ const blockSeqKey = line.match(/^(\w[\w-]*):\s*$/);
66
+ if (blockSeqKey && i + 1 < lines.length && /^\s+-\s/.test(lines[i + 1])) {
67
+ const key = blockSeqKey[1];
68
+ const items = [];
69
+ i++;
70
+ while (i < lines.length && /^\s+-\s/.test(lines[i])) {
71
+ items.push(lines[i].replace(/^\s+-\s/, "").trim());
72
+ i++;
73
+ }
74
+ raw[key] = items;
75
+ continue;
76
+ }
77
+ const kvMatch = line.match(/^(\w[\w-]*):\s*(.*)$/);
78
+ if (kvMatch) {
79
+ const key = kvMatch[1];
80
+ const val = kvMatch[2].trim();
81
+ if (val.startsWith("[") && val.endsWith("]")) raw[key] = parseInlineArray(val).map(stripQuotes);
82
+ else raw[key] = stripQuotes(val);
83
+ }
84
+ i++;
85
+ }
86
+ const meta = {};
87
+ const getStr = (keys) => {
88
+ for (const k of keys) {
89
+ const v = raw[k];
90
+ if (typeof v === "string" && v) return v;
91
+ }
92
+ };
93
+ if (raw["tags"]) meta.tags = Array.isArray(raw["tags"]) ? raw["tags"] : [raw["tags"]];
94
+ const color = getStr(["color"]);
95
+ if (color) meta.color = color;
96
+ const icon = getStr(["icon"]);
97
+ if (icon) meta.icon = icon;
98
+ const status = getStr(["status"]);
99
+ if (status) meta.status = status;
100
+ const priorityRaw = getStr(["priority"]);
101
+ if (priorityRaw !== void 0) meta.priority = {
102
+ low: 1,
103
+ medium: 2,
104
+ high: 3,
105
+ urgent: 4
106
+ }[priorityRaw.toLowerCase()] ?? (Number(priorityRaw) || 0);
107
+ const checkedRaw = raw["checked"] ?? raw["done"];
108
+ if (checkedRaw !== void 0) meta.checked = checkedRaw === "true" || checkedRaw === true;
109
+ const dateStart = getStr([
110
+ "dateStart",
111
+ "date",
112
+ "created"
113
+ ]);
114
+ if (dateStart) meta.dateStart = dateStart;
115
+ const dateEnd = getStr(["dateEnd", "due"]);
116
+ if (dateEnd) meta.dateEnd = dateEnd;
117
+ const subtitle = getStr(["subtitle", "description"]);
118
+ if (subtitle) meta.subtitle = subtitle;
119
+ const url = getStr(["url"]);
120
+ if (url) meta.url = url;
121
+ const ratingRaw = getStr(["rating"]);
122
+ if (ratingRaw !== void 0) {
123
+ const n = Number(ratingRaw);
124
+ if (!Number.isNaN(n)) meta.rating = Math.min(5, Math.max(0, n));
125
+ }
126
+ const rawTitle = typeof raw["title"] === "string" ? raw["title"] : void 0;
127
+ return {
128
+ title: rawTitle !== void 0 ? stripQuotes(rawTitle) : void 0,
129
+ type: getStr(["type"]),
130
+ meta,
131
+ body
132
+ };
133
+ }
134
+ function parseInline(text) {
135
+ const stripped = text.replace(/\{lang="[^"]*"\}/g, "").replace(/:(?!badge|icon|kbd)(\w[\w-]*)\[([^\]]*)\](\{[^}]*\})?/g, "$2").replace(/:(?!badge|icon|kbd)(\w[\w-]*)(\{[^}]*\})/g, "");
136
+ const tokens = [];
137
+ const re = /\$([^$\n]+?)\$|@\[([^\]]+?)\]\(user:([^)]+?)\)|:badge\[([^\]]*)\](\{[^}]*\})?|:icon\{([^}]*)\}|:kbd\{([^}]*)\}|\[\[([0-9a-fA-F-]{36})(?:\|([^\]]+?))?\]\]|~~(.+?)~~|\*\*(.+?)\*\*|\*(.+?)\*|_(.+?)_|`(.+?)`|\[(.+?)\]\((.+?)\)/g;
138
+ let lastIndex = 0;
139
+ let match;
140
+ while ((match = re.exec(stripped)) !== null) {
141
+ if (match.index > lastIndex) tokens.push({ text: stripped.slice(lastIndex, match.index) });
142
+ if (match[1] !== void 0) tokens.push({
143
+ text: match[1],
144
+ attrs: { mathInline: { expression: match[1] } }
145
+ });
146
+ else if (match[2] !== void 0 && match[3] !== void 0) tokens.push({
147
+ text: match[2],
148
+ attrs: { mention: {
149
+ userId: match[3],
150
+ label: match[2]
151
+ } }
152
+ });
153
+ else if (match[4] !== void 0) {
154
+ const badgeProps = parseMdcProps(match[5]);
155
+ tokens.push({
156
+ text: match[4] || "Badge",
157
+ attrs: { badge: {
158
+ label: match[4] || "Badge",
159
+ color: badgeProps["color"] || "neutral",
160
+ variant: badgeProps["variant"] || "subtle"
161
+ } }
162
+ });
163
+ } else if (match[6] !== void 0) {
164
+ const iconProps = parseMdcProps(`{${match[6]}}`);
165
+ tokens.push({
166
+ text: "​",
167
+ attrs: { proseIcon: { name: iconProps["name"] || "i-lucide-star" } }
168
+ });
169
+ } else if (match[7] !== void 0) {
170
+ const kbdProps = parseMdcProps(`{${match[7]}}`);
171
+ tokens.push({
172
+ text: kbdProps["value"] || "",
173
+ attrs: { kbd: { value: kbdProps["value"] || "" } }
174
+ });
175
+ } else if (match[8] !== void 0) {
176
+ const docId = match[8];
177
+ const label = match[9] ?? docId;
178
+ tokens.push({
179
+ text: label,
180
+ attrs: { docLink: { docId } }
181
+ });
182
+ } else if (match[10] !== void 0) tokens.push({
183
+ text: match[10],
184
+ attrs: { strike: true }
185
+ });
186
+ else if (match[11] !== void 0) tokens.push({
187
+ text: match[11],
188
+ attrs: { bold: true }
189
+ });
190
+ else if (match[12] !== void 0) tokens.push({
191
+ text: match[12],
192
+ attrs: { italic: true }
193
+ });
194
+ else if (match[13] !== void 0) tokens.push({
195
+ text: match[13],
196
+ attrs: { italic: true }
197
+ });
198
+ else if (match[14] !== void 0) tokens.push({
199
+ text: match[14],
200
+ attrs: { code: true }
201
+ });
202
+ else if (match[15] !== void 0 && match[16] !== void 0) tokens.push({
203
+ text: match[15],
204
+ attrs: { link: { href: match[16] } }
205
+ });
206
+ lastIndex = match.index + match[0].length;
207
+ }
208
+ if (lastIndex < stripped.length) tokens.push({ text: stripped.slice(lastIndex) });
209
+ return tokens.filter((t) => t.text.length > 0);
210
+ }
211
+ function parseTableRow(line) {
212
+ const parts = line.split("|");
213
+ return parts.slice(1, parts.length - 1).map((c) => c.trim());
214
+ }
215
+ function isTableSeparator(line) {
216
+ return /^\|[\s|:-]+\|$/.test(line.trim());
217
+ }
218
+ /** Extract fenced code blocks from MDC #code slot lines. */
219
+ function extractFencedCode(lines) {
220
+ const result = [];
221
+ let i = 0;
222
+ while (i < lines.length) {
223
+ const fenceMatch = lines[i].match(/^(`{3,})(\w*)/);
224
+ if (fenceMatch) {
225
+ const fence = fenceMatch[1];
226
+ const lang = fenceMatch[2] ?? "";
227
+ const codeLines = [];
228
+ i++;
229
+ while (i < lines.length && !lines[i].startsWith(fence)) {
230
+ codeLines.push(lines[i]);
231
+ i++;
232
+ }
233
+ i++;
234
+ result.push({
235
+ type: "codeBlock",
236
+ lang,
237
+ code: codeLines.join("\n")
238
+ });
239
+ continue;
240
+ }
241
+ i++;
242
+ }
243
+ return result;
244
+ }
245
+ /** Extract key="value" pairs from MDC prop syntax `{key="value" other="x"}` */
246
+ function parseMdcProps(propsStr) {
247
+ if (!propsStr) return {};
248
+ const result = {};
249
+ let s = propsStr.trim();
250
+ if (s.startsWith("{") && s.endsWith("}")) s = s.slice(1, -1);
251
+ const re = /(\w[\w-]*)(?:=(?:"([^"]*)"|([^\s"}]+)))?/g;
252
+ let m;
253
+ while ((m = re.exec(s)) !== null) {
254
+ const key = m[1];
255
+ if (m[2] !== void 0) result[key] = m[2];
256
+ else if (m[3] !== void 0) result[key] = m[3];
257
+ else result[key] = "true";
258
+ }
259
+ return result;
260
+ }
261
+ /** Parse named child MDC blocks from inner lines (e.g. #item for accordion, #tab for tabs) */
262
+ function parseMdcChildren(innerLines, slotPrefix) {
263
+ const items = [];
264
+ let current = null;
265
+ const slotRe = new RegExp(`^#${slotPrefix}(\\{[^}]*\\})?\\s*$`);
266
+ for (const line of innerLines) {
267
+ const slotMatch = line.match(slotRe);
268
+ if (slotMatch) {
269
+ if (current) items.push(current);
270
+ const props = parseMdcProps(slotMatch[1]);
271
+ current = {
272
+ label: props["label"] || props["title"] || `Item ${items.length + 1}`,
273
+ icon: props["icon"] || "",
274
+ lines: []
275
+ };
276
+ continue;
277
+ }
278
+ if (current) current.lines.push(line);
279
+ else if (!items.length && !current) current = {
280
+ label: `Item 1`,
281
+ icon: "",
282
+ lines: [line]
283
+ };
284
+ }
285
+ if (current) items.push(current);
286
+ return items.map((item) => ({
287
+ label: item.label,
288
+ icon: item.icon,
289
+ innerBlocks: parseBlocks(item.lines.join("\n"))
290
+ }));
291
+ }
292
+ const TASK_RE = /^[-*+]\s+\[([ xX])\]\s+(.*)/;
293
+ /**
294
+ * Consume a list (bullet / ordered / task) starting at `start`. Indented
295
+ * continuation lines and nested lists are captured into each item's
296
+ * `innerBlocks` so the parse → serialise → parse cycle preserves tree
297
+ * structure instead of flattening nested lists onto a single line.
298
+ *
299
+ * `indent` is the column of the item marker for the current list. A
300
+ * nested list starts ≥2 columns deeper. Lines with less indent than
301
+ * `indent` belong to the outer block and stop consumption.
302
+ */
303
+ function consumeList(lines, start, indent, kind) {
304
+ const items = [];
305
+ let i = start;
306
+ while (i < lines.length) {
307
+ const line = lines[i];
308
+ if (line.trim() === "") {
309
+ let j = i + 1;
310
+ while (j < lines.length && lines[j].trim() === "") j++;
311
+ if (j >= lines.length) break;
312
+ const lookahead = lines[j];
313
+ if (leadingSpaces(lookahead) < indent) break;
314
+ if (!matchMarker(lookahead.slice(indent), kind)) break;
315
+ i = j;
316
+ continue;
317
+ }
318
+ const leading = leadingSpaces(line);
319
+ if (leading < indent) break;
320
+ if (leading > indent) break;
321
+ const m = matchMarker(line.slice(indent), kind);
322
+ if (!m) break;
323
+ const item = { text: m.text };
324
+ if (kind === "task") item.checked = m.checked;
325
+ i++;
326
+ const contLines = [];
327
+ while (i < lines.length) {
328
+ const next = lines[i];
329
+ if (next.trim() === "") {
330
+ let k = i + 1;
331
+ while (k < lines.length && lines[k].trim() === "") k++;
332
+ if (k >= lines.length) break;
333
+ if (leadingSpaces(lines[k]) <= indent) break;
334
+ contLines.push("");
335
+ i++;
336
+ continue;
337
+ }
338
+ const nextIndent = leadingSpaces(next);
339
+ if (nextIndent <= indent) break;
340
+ const deindentBy = Math.min(nextIndent, indent + 2);
341
+ contLines.push(next.slice(deindentBy));
342
+ i++;
343
+ }
344
+ if (contLines.length > 0) item.innerBlocks = parseBlocks(contLines.join("\n"));
345
+ items.push(item);
346
+ }
347
+ return {
348
+ items,
349
+ next: i
350
+ };
351
+ }
352
+ function leadingSpaces(s) {
353
+ let n = 0;
354
+ while (n < s.length && s[n] === " ") n++;
355
+ return n;
356
+ }
357
+ function matchMarker(s, kind) {
358
+ if (kind === "task") {
359
+ const m = s.match(TASK_RE);
360
+ if (!m) return null;
361
+ return {
362
+ text: m[2],
363
+ checked: m[1].toLowerCase() === "x"
364
+ };
365
+ }
366
+ if (kind === "bullet") {
367
+ if (TASK_RE.test(s)) return null;
368
+ const m = s.match(/^[-*+]\s+(.*)$/);
369
+ if (!m) return null;
370
+ return {
371
+ text: m[1],
372
+ checked: false
373
+ };
374
+ }
375
+ const m = s.match(/^\d+\.\s+(.*)$/);
376
+ if (!m) return null;
377
+ return {
378
+ text: m[1],
379
+ checked: false
380
+ };
381
+ }
382
+ function parseBlocks(markdown) {
383
+ const rawLines = markdown.split("\n");
384
+ let firstContentLine = 0;
385
+ while (firstContentLine < rawLines.length) {
386
+ const l = rawLines[firstContentLine];
387
+ if (l.trim() === "" || /^import\s/.test(l) || /^export\s/.test(l)) firstContentLine++;
388
+ else break;
389
+ }
390
+ const stripped = rawLines.slice(firstContentLine).join("\n");
391
+ const blocks = [];
392
+ const lines = stripped.split("\n");
393
+ let i = 0;
394
+ while (i < lines.length) {
395
+ const line = lines[i];
396
+ const fenceBlockMatch = line.match(/^(`{3,})(.*)$/);
397
+ if (fenceBlockMatch) {
398
+ const fence = fenceBlockMatch[1];
399
+ const lang = fenceBlockMatch[2].trim().replace(/\{[^}]*\}$/, "").replace(/\s*\[.*\]$/, "").trim();
400
+ const codeLines = [];
401
+ i++;
402
+ while (i < lines.length && !lines[i].startsWith(fence)) {
403
+ codeLines.push(lines[i]);
404
+ i++;
405
+ }
406
+ i++;
407
+ const code = codeLines.join("\n");
408
+ if (lang === "math") blocks.push({
409
+ type: "mathBlock",
410
+ expression: code
411
+ });
412
+ else blocks.push({
413
+ type: "codeBlock",
414
+ lang,
415
+ code
416
+ });
417
+ continue;
418
+ }
419
+ const headingMatch = line.match(/^(#{1,6})\s+(.*)/);
420
+ if (headingMatch) {
421
+ blocks.push({
422
+ type: "heading",
423
+ level: headingMatch[1].length,
424
+ text: headingMatch[2].trim()
425
+ });
426
+ i++;
427
+ continue;
428
+ }
429
+ if (/^[-*_]{3,}\s*$/.test(line)) {
430
+ blocks.push({ type: "hr" });
431
+ i++;
432
+ continue;
433
+ }
434
+ const embedMatch = line.match(/^!\[\[([0-9a-fA-F-]{36})(?:\|([^\]]+))?\]\](\{[^}]*\})?\s*$/);
435
+ if (embedMatch) {
436
+ const docId = embedMatch[1];
437
+ const label = embedMatch[2] ?? "";
438
+ const props = parseMdcProps(embedMatch[3]);
439
+ blocks.push({
440
+ type: "docEmbed",
441
+ docId,
442
+ label,
443
+ props
444
+ });
445
+ i++;
446
+ continue;
447
+ }
448
+ const imgMatch = line.match(/^!\[([^\]]*)\]\(([^)]+)\)(\{[^}]*\})?\s*$/);
449
+ if (imgMatch) {
450
+ const alt = imgMatch[1] ?? "";
451
+ const src = imgMatch[2] ?? "";
452
+ const attrs = parseMdcProps(imgMatch[3]);
453
+ blocks.push({
454
+ type: "image",
455
+ src,
456
+ alt,
457
+ width: attrs["width"],
458
+ height: attrs["height"]
459
+ });
460
+ i++;
461
+ continue;
462
+ }
463
+ if (line.startsWith("> ") || line === ">") {
464
+ const bqLines = [];
465
+ while (i < lines.length && (lines[i].startsWith("> ") || lines[i] === ">")) {
466
+ bqLines.push(lines[i].replace(/^>\s?/, ""));
467
+ i++;
468
+ }
469
+ blocks.push({
470
+ type: "blockquote",
471
+ lines: bqLines
472
+ });
473
+ continue;
474
+ }
475
+ if (/^\s*\|/.test(line)) {
476
+ const tableLines = [];
477
+ while (i < lines.length && /^\s*\|/.test(lines[i])) {
478
+ tableLines.push(lines[i]);
479
+ i++;
480
+ }
481
+ if (tableLines.length >= 2 && isTableSeparator(tableLines[1])) {
482
+ const headerRow = parseTableRow(tableLines[0]);
483
+ const dataRows = tableLines.slice(2).filter((l) => !isTableSeparator(l)).map(parseTableRow);
484
+ blocks.push({
485
+ type: "table",
486
+ headerRow,
487
+ dataRows
488
+ });
489
+ } else for (const l of tableLines) blocks.push({
490
+ type: "paragraph",
491
+ text: l
492
+ });
493
+ continue;
494
+ }
495
+ const atomMatch = line.match(/^:(\w[\w-]*)(\{[^}]*\})?\s*$/);
496
+ if (atomMatch && atomMatch[1] === "file") {
497
+ const props = parseMdcProps(atomMatch[2]);
498
+ const uploadId = props["upload-id"] ?? props["uploadId"] ?? "";
499
+ const filename = props["filename"] ?? "";
500
+ const mime = props["mime"] ?? "";
501
+ const src = props["src"] ?? (uploadId && filename ? `.abracadabra/files/${uploadId}-${filename}` : "");
502
+ blocks.push({
503
+ type: "fileBlock",
504
+ src,
505
+ mime,
506
+ uploadId,
507
+ filename
508
+ });
509
+ i++;
510
+ continue;
511
+ }
512
+ const MDC_OPEN = /^\s*(:{2,})(\w[\w-]*)(\{[^}]*\})?\s*$/;
513
+ if (MDC_OPEN.test(line)) {
514
+ const colons = line.match(/^\s*(:+)/)?.[1]?.length ?? 2;
515
+ const componentName = line.match(/^\s*:{2,}(\w[\w-]*)/)?.[1] ?? "";
516
+ const innerLines = [];
517
+ i++;
518
+ while (i < lines.length) {
519
+ const l = lines[i];
520
+ if (new RegExp(`^\\s*:{${colons}}\\s*$`).test(l)) {
521
+ i++;
522
+ break;
523
+ }
524
+ const innerFence = l.match(/^(\s*`{3,})/);
525
+ if (innerFence) {
526
+ const fenceStr = innerFence[1].trimStart();
527
+ innerLines.push(l);
528
+ i++;
529
+ while (i < lines.length && !lines[i].trimStart().startsWith(fenceStr)) {
530
+ innerLines.push(lines[i]);
531
+ i++;
532
+ }
533
+ if (i < lines.length) {
534
+ innerLines.push(lines[i]);
535
+ i++;
536
+ }
537
+ continue;
538
+ }
539
+ innerLines.push(l);
540
+ i++;
541
+ }
542
+ const nonBlank = innerLines.filter((l) => l.trim().length > 0);
543
+ if (nonBlank.length) {
544
+ const minIndent = Math.min(...nonBlank.map((l) => l.match(/^(\s*)/)?.[1]?.length ?? 0));
545
+ if (minIndent > 0) for (let j = 0; j < innerLines.length; j++) innerLines[j] = innerLines[j].slice(Math.min(minIndent, innerLines[j].length));
546
+ }
547
+ let contentStart = 0;
548
+ if (innerLines[0]?.trim() === "---") {
549
+ const fmEnd = innerLines.findIndex((l, idx) => idx > 0 && l.trim() === "---");
550
+ if (fmEnd !== -1) contentStart = fmEnd + 1;
551
+ }
552
+ const contentLines = innerLines.slice(contentStart);
553
+ const defaultSlotLines = [];
554
+ const codeSlotLines = [];
555
+ let currentSlot = "default";
556
+ for (const l of contentLines) {
557
+ if (/^#code\s*$/.test(l)) {
558
+ currentSlot = "code";
559
+ continue;
560
+ }
561
+ if (/^#\w+/.test(l) && !/^#{2,}\s/.test(l)) {
562
+ currentSlot = "other";
563
+ continue;
564
+ }
565
+ if (currentSlot === "default") defaultSlotLines.push(l);
566
+ else if (currentSlot === "code") codeSlotLines.push(l);
567
+ }
568
+ const innerBlocks = parseBlocks(defaultSlotLines.join("\n"));
569
+ const codeBlocks = extractFencedCode(codeSlotLines);
570
+ if (new Set([
571
+ "tip",
572
+ "note",
573
+ "info",
574
+ "warning",
575
+ "caution",
576
+ "danger",
577
+ "callout",
578
+ "alert"
579
+ ]).has(componentName.toLowerCase())) blocks.push({
580
+ type: "callout",
581
+ calloutType: componentName.toLowerCase(),
582
+ innerBlocks
583
+ });
584
+ else {
585
+ const mdcProps = parseMdcProps(line.match(MDC_OPEN)?.[3]);
586
+ const lc = componentName.toLowerCase();
587
+ if (lc === "collapsible") blocks.push({
588
+ type: "collapsible",
589
+ label: mdcProps["label"] || "Details",
590
+ open: mdcProps["open"] === "true",
591
+ innerBlocks
592
+ });
593
+ else if (lc === "steps") blocks.push({
594
+ type: "steps",
595
+ innerBlocks
596
+ });
597
+ else if (lc === "card") blocks.push({
598
+ type: "card",
599
+ title: mdcProps["title"] || "",
600
+ icon: mdcProps["icon"] || "",
601
+ to: mdcProps["to"] || "",
602
+ innerBlocks
603
+ });
604
+ else if (lc === "card-group") {
605
+ const cards = innerBlocks.filter((b) => b.type === "card");
606
+ if (cards.length) blocks.push({
607
+ type: "cardGroup",
608
+ cards
609
+ });
610
+ else blocks.push(...innerBlocks);
611
+ } else if (lc === "code-collapse") blocks.push({
612
+ type: "codeCollapse",
613
+ codeBlocks: codeBlocks.length ? codeBlocks : innerBlocks.filter((b) => b.type === "codeBlock")
614
+ });
615
+ else if (lc === "code-group") {
616
+ const allCode = [...innerBlocks.filter((b) => b.type === "codeBlock"), ...codeBlocks];
617
+ blocks.push({
618
+ type: "codeGroup",
619
+ codeBlocks: allCode
620
+ });
621
+ } else if (lc === "code-preview") blocks.push({
622
+ type: "codePreview",
623
+ innerBlocks,
624
+ codeBlocks
625
+ });
626
+ else if (lc === "code-tree") blocks.push({
627
+ type: "codeTree",
628
+ files: mdcProps["files"] || "[]"
629
+ });
630
+ else if (lc === "accordion") {
631
+ const items = parseMdcChildren(contentLines, "item");
632
+ if (items.length) blocks.push({
633
+ type: "accordion",
634
+ items
635
+ });
636
+ else blocks.push({
637
+ type: "accordion",
638
+ items: [{
639
+ label: "Item 1",
640
+ icon: "",
641
+ innerBlocks
642
+ }]
643
+ });
644
+ } else if (lc === "tabs") {
645
+ const items = parseMdcChildren(contentLines, "tab");
646
+ if (items.length) blocks.push({
647
+ type: "tabs",
648
+ items
649
+ });
650
+ else blocks.push({
651
+ type: "tabs",
652
+ items: [{
653
+ label: "Tab 1",
654
+ icon: "",
655
+ innerBlocks
656
+ }]
657
+ });
658
+ } else if (lc === "field") blocks.push({
659
+ type: "field",
660
+ name: mdcProps["name"] || "",
661
+ fieldType: mdcProps["type"] || "string",
662
+ required: mdcProps["required"] === "true",
663
+ innerBlocks
664
+ });
665
+ else if (lc === "field-group") {
666
+ const fields = innerBlocks.filter((b) => b.type === "field");
667
+ if (fields.length) blocks.push({
668
+ type: "fieldGroup",
669
+ fields
670
+ });
671
+ else blocks.push(...innerBlocks);
672
+ } else {
673
+ blocks.push(...innerBlocks);
674
+ blocks.push(...codeBlocks);
675
+ }
676
+ }
677
+ continue;
678
+ }
679
+ if (TASK_RE.test(line)) {
680
+ const { items, next } = consumeList(lines, i, 0, "task");
681
+ i = next;
682
+ blocks.push({
683
+ type: "taskList",
684
+ items
685
+ });
686
+ continue;
687
+ }
688
+ if (/^[-*+]\s+/.test(line)) {
689
+ const { items, next } = consumeList(lines, i, 0, "bullet");
690
+ if (items.length > 0) {
691
+ i = next;
692
+ blocks.push({
693
+ type: "bulletList",
694
+ items
695
+ });
696
+ continue;
697
+ }
698
+ }
699
+ if (/^\d+\.\s+/.test(line)) {
700
+ const { items, next } = consumeList(lines, i, 0, "ordered");
701
+ if (items.length > 0) {
702
+ i = next;
703
+ blocks.push({
704
+ type: "orderedList",
705
+ items
706
+ });
707
+ continue;
708
+ }
709
+ }
710
+ if (line.trim() === "") {
711
+ i++;
712
+ continue;
713
+ }
714
+ const paraLines = [];
715
+ while (i < lines.length && lines[i].trim() !== "" && !/^(#{1,6}\s|[-*+]\s|\d+\.\s|>|`{3,}|\s*\||[-*_]{3,}\s*$|\s*:{2,}\w)/.test(lines[i])) {
716
+ paraLines.push(lines[i]);
717
+ i++;
718
+ }
719
+ if (paraLines.length) blocks.push({
720
+ type: "paragraph",
721
+ text: paraLines.join(" ")
722
+ });
723
+ }
724
+ return blocks;
725
+ }
726
+ /**
727
+ * Insert formatted inline tokens into an already-attached Y.XmlElement.
728
+ * Creates one Y.XmlText per token (attach first, fill second).
729
+ */
730
+ function fillTextInto(el, tokens) {
731
+ const filtered = tokens.filter((t) => t.text.length > 0);
732
+ if (!filtered.length) return;
733
+ const xtNodes = filtered.map(() => new yjs.XmlText());
734
+ el.insert(0, xtNodes);
735
+ filtered.forEach((tok, i) => {
736
+ if (tok.attrs) xtNodes[i].insert(0, tok.text, tok.attrs);
737
+ else xtNodes[i].insert(0, tok.text);
738
+ });
739
+ }
740
+ function blockElName(b) {
741
+ switch (b.type) {
742
+ case "heading": return "heading";
743
+ case "paragraph": return "paragraph";
744
+ case "bulletList": return "bulletList";
745
+ case "orderedList": return "orderedList";
746
+ case "taskList": return "taskList";
747
+ case "codeBlock": return "codeBlock";
748
+ case "blockquote": return "blockquote";
749
+ case "table": return "table";
750
+ case "hr": return "horizontalRule";
751
+ case "callout": return "callout";
752
+ case "collapsible": return "collapsible";
753
+ case "steps": return "steps";
754
+ case "card": return "card";
755
+ case "cardGroup": return "cardGroup";
756
+ case "codeCollapse": return "codeCollapse";
757
+ case "codeGroup": return "codeGroup";
758
+ case "codePreview": return "codePreview";
759
+ case "codeTree": return "codeTree";
760
+ case "accordion": return "accordion";
761
+ case "tabs": return "tabs";
762
+ case "field": return "field";
763
+ case "fieldGroup": return "fieldGroup";
764
+ case "image": return "image";
765
+ case "docEmbed": return "docEmbed";
766
+ case "mathBlock": return "mathBlock";
767
+ case "fileBlock": return "fileBlock";
768
+ }
769
+ }
770
+ function populateListItemChildren(itemEl, item, _itemKind) {
771
+ const paraEl = new yjs.XmlElement("paragraph");
772
+ itemEl.insert(itemEl.length, [paraEl]);
773
+ fillTextInto(paraEl, parseInline(item.text));
774
+ if (!item.innerBlocks?.length) return;
775
+ const innerEls = item.innerBlocks.map((b) => new yjs.XmlElement(blockElName(b)));
776
+ itemEl.insert(itemEl.length, innerEls);
777
+ item.innerBlocks.forEach((b, i) => fillBlock(innerEls[i], b));
778
+ }
779
+ function fillBlock(el, block) {
780
+ switch (block.type) {
781
+ case "heading":
782
+ el.setAttribute("level", block.level);
783
+ fillTextInto(el, parseInline(block.text));
784
+ break;
785
+ case "paragraph":
786
+ fillTextInto(el, parseInline(block.text));
787
+ break;
788
+ case "bulletList":
789
+ case "orderedList": {
790
+ const listItemEls = block.items.map(() => new yjs.XmlElement("listItem"));
791
+ el.insert(0, listItemEls);
792
+ block.items.forEach((item, i) => {
793
+ populateListItemChildren(listItemEls[i], item, "listItem");
794
+ });
795
+ break;
796
+ }
797
+ case "taskList": {
798
+ const taskItemEls = block.items.map(() => new yjs.XmlElement("taskItem"));
799
+ el.insert(0, taskItemEls);
800
+ block.items.forEach((item, i) => {
801
+ taskItemEls[i].setAttribute("checked", !!item.checked);
802
+ populateListItemChildren(taskItemEls[i], item, "taskItem");
803
+ });
804
+ break;
805
+ }
806
+ case "codeBlock": {
807
+ if (block.lang) el.setAttribute("language", block.lang);
808
+ const xt = new yjs.XmlText();
809
+ el.insert(0, [xt]);
810
+ xt.insert(0, block.code);
811
+ break;
812
+ }
813
+ case "blockquote": {
814
+ const paraEls = block.lines.map(() => new yjs.XmlElement("paragraph"));
815
+ el.insert(0, paraEls);
816
+ block.lines.forEach((line, i) => fillTextInto(paraEls[i], parseInline(line)));
817
+ break;
818
+ }
819
+ case "table": {
820
+ const headerRowEl = new yjs.XmlElement("tableRow");
821
+ const dataRowEls = block.dataRows.map(() => new yjs.XmlElement("tableRow"));
822
+ el.insert(0, [headerRowEl, ...dataRowEls]);
823
+ const headerCellEls = block.headerRow.map(() => new yjs.XmlElement("tableHeader"));
824
+ headerRowEl.insert(0, headerCellEls);
825
+ block.headerRow.forEach((cellText, i) => {
826
+ const paraEl = new yjs.XmlElement("paragraph");
827
+ headerCellEls[i].insert(0, [paraEl]);
828
+ fillTextInto(paraEl, parseInline(cellText));
829
+ });
830
+ block.dataRows.forEach((row, ri) => {
831
+ const cellEls = row.map(() => new yjs.XmlElement("tableCell"));
832
+ dataRowEls[ri].insert(0, cellEls);
833
+ row.forEach((cellText, ci) => {
834
+ const paraEl = new yjs.XmlElement("paragraph");
835
+ cellEls[ci].insert(0, [paraEl]);
836
+ fillTextInto(paraEl, parseInline(cellText));
837
+ });
838
+ });
839
+ break;
840
+ }
841
+ case "hr": break;
842
+ case "callout": {
843
+ el.setAttribute("type", block.calloutType);
844
+ if (!block.innerBlocks.length) {
845
+ const paraEl = new yjs.XmlElement("paragraph");
846
+ el.insert(0, [paraEl]);
847
+ break;
848
+ }
849
+ const innerEls = block.innerBlocks.map((b) => new yjs.XmlElement(blockElName(b)));
850
+ el.insert(0, innerEls);
851
+ block.innerBlocks.forEach((b, i) => fillBlock(innerEls[i], b));
852
+ break;
853
+ }
854
+ case "collapsible": {
855
+ el.setAttribute("label", block.label);
856
+ el.setAttribute("open", block.open);
857
+ const inner = block.innerBlocks.length ? block.innerBlocks : [{
858
+ type: "paragraph",
859
+ text: ""
860
+ }];
861
+ const innerEls = inner.map((b) => new yjs.XmlElement(blockElName(b)));
862
+ el.insert(0, innerEls);
863
+ inner.forEach((b, i) => fillBlock(innerEls[i], b));
864
+ break;
865
+ }
866
+ case "steps": {
867
+ const inner = block.innerBlocks.length ? block.innerBlocks : [{
868
+ type: "paragraph",
869
+ text: ""
870
+ }];
871
+ const innerEls = inner.map((b) => new yjs.XmlElement(blockElName(b)));
872
+ el.insert(0, innerEls);
873
+ inner.forEach((b, i) => fillBlock(innerEls[i], b));
874
+ break;
875
+ }
876
+ case "card": {
877
+ if (block.title) el.setAttribute("title", block.title);
878
+ if (block.icon) el.setAttribute("icon", block.icon);
879
+ if (block.to) el.setAttribute("to", block.to);
880
+ const inner = block.innerBlocks.length ? block.innerBlocks : [{
881
+ type: "paragraph",
882
+ text: ""
883
+ }];
884
+ const innerEls = inner.map((b) => new yjs.XmlElement(blockElName(b)));
885
+ el.insert(0, innerEls);
886
+ inner.forEach((b, i) => fillBlock(innerEls[i], b));
887
+ break;
888
+ }
889
+ case "cardGroup": {
890
+ const cardEls = block.cards.map((b) => new yjs.XmlElement(blockElName(b)));
891
+ el.insert(0, cardEls);
892
+ block.cards.forEach((b, i) => fillBlock(cardEls[i], b));
893
+ break;
894
+ }
895
+ case "codeCollapse": {
896
+ const codes = block.codeBlocks.length ? block.codeBlocks : [{
897
+ type: "codeBlock",
898
+ lang: "",
899
+ code: ""
900
+ }];
901
+ const codeEl = new yjs.XmlElement("codeBlock");
902
+ el.insert(0, [codeEl]);
903
+ fillBlock(codeEl, codes[0]);
904
+ break;
905
+ }
906
+ case "codeGroup": {
907
+ const codes = block.codeBlocks.length ? block.codeBlocks : [{
908
+ type: "codeBlock",
909
+ lang: "",
910
+ code: ""
911
+ }];
912
+ const codeEls = codes.map(() => new yjs.XmlElement("codeBlock"));
913
+ el.insert(0, codeEls);
914
+ codes.forEach((b, i) => fillBlock(codeEls[i], b));
915
+ break;
916
+ }
917
+ case "codePreview": {
918
+ const all = [...block.innerBlocks, ...block.codeBlocks];
919
+ const inner = all.length ? all : [{
920
+ type: "paragraph",
921
+ text: ""
922
+ }];
923
+ const innerEls = inner.map((b) => new yjs.XmlElement(blockElName(b)));
924
+ el.insert(0, innerEls);
925
+ inner.forEach((b, i) => fillBlock(innerEls[i], b));
926
+ break;
927
+ }
928
+ case "codeTree":
929
+ el.setAttribute("files", block.files);
930
+ break;
931
+ case "accordion": {
932
+ const itemEls = block.items.map(() => new yjs.XmlElement("accordionItem"));
933
+ el.insert(0, itemEls);
934
+ block.items.forEach((item, i) => {
935
+ itemEls[i].setAttribute("label", item.label);
936
+ if (item.icon) itemEls[i].setAttribute("icon", item.icon);
937
+ const inner = item.innerBlocks.length ? item.innerBlocks : [{
938
+ type: "paragraph",
939
+ text: ""
940
+ }];
941
+ const childEls = inner.map((b) => new yjs.XmlElement(blockElName(b)));
942
+ itemEls[i].insert(0, childEls);
943
+ inner.forEach((b, ci) => fillBlock(childEls[ci], b));
944
+ });
945
+ break;
946
+ }
947
+ case "tabs": {
948
+ const itemEls = block.items.map(() => new yjs.XmlElement("tabsItem"));
949
+ el.insert(0, itemEls);
950
+ block.items.forEach((item, i) => {
951
+ itemEls[i].setAttribute("label", item.label);
952
+ if (item.icon) itemEls[i].setAttribute("icon", item.icon);
953
+ const inner = item.innerBlocks.length ? item.innerBlocks : [{
954
+ type: "paragraph",
955
+ text: ""
956
+ }];
957
+ const childEls = inner.map((b) => new yjs.XmlElement(blockElName(b)));
958
+ itemEls[i].insert(0, childEls);
959
+ inner.forEach((b, ci) => fillBlock(childEls[ci], b));
960
+ });
961
+ break;
962
+ }
963
+ case "field": {
964
+ if (block.name) el.setAttribute("name", block.name);
965
+ el.setAttribute("type", block.fieldType);
966
+ el.setAttribute("required", block.required);
967
+ const inner = block.innerBlocks.length ? block.innerBlocks : [{
968
+ type: "paragraph",
969
+ text: ""
970
+ }];
971
+ const innerEls = inner.map((b) => new yjs.XmlElement(blockElName(b)));
972
+ el.insert(0, innerEls);
973
+ inner.forEach((b, i) => fillBlock(innerEls[i], b));
974
+ break;
975
+ }
976
+ case "fieldGroup": {
977
+ const fieldEls = block.fields.map((b) => new yjs.XmlElement(blockElName(b)));
978
+ el.insert(0, fieldEls);
979
+ block.fields.forEach((b, i) => fillBlock(fieldEls[i], b));
980
+ break;
981
+ }
982
+ case "image":
983
+ el.setAttribute("src", block.src);
984
+ if (block.alt) el.setAttribute("alt", block.alt);
985
+ if (block.width) el.setAttribute("width", block.width);
986
+ if (block.height) el.setAttribute("height", block.height);
987
+ break;
988
+ case "docEmbed":
989
+ el.setAttribute("docId", block.docId);
990
+ for (const flag of [
991
+ "collapsed",
992
+ "tall",
993
+ "seamless"
994
+ ]) if (block.props[flag] === "true" || block.props[flag] === "1") el.setAttribute(flag, true);
995
+ break;
996
+ case "mathBlock":
997
+ el.setAttribute("expression", block.expression);
998
+ break;
999
+ case "fileBlock":
1000
+ if (block.src) el.setAttribute("src", block.src);
1001
+ if (block.mime) el.setAttribute("mime", block.mime);
1002
+ if (block.uploadId) el.setAttribute("uploadId", block.uploadId);
1003
+ if (block.filename) el.setAttribute("filename", block.filename);
1004
+ break;
1005
+ }
1006
+ }
1007
+ /**
1008
+ * Parses markdown text and writes the result into a Y.XmlFragment that
1009
+ * TipTap's Collaboration extension can read.
1010
+ *
1011
+ * Requires `fragment.doc` to be set (i.e. the fragment must already be
1012
+ * obtained from a live Y.Doc via `ydoc.getXmlFragment('default')`).
1013
+ *
1014
+ * @param fragment The target `Y.Doc.getXmlFragment('default')`
1015
+ * @param markdown Raw markdown string
1016
+ * @param fallbackTitle Used as the title when the markdown has no H1
1017
+ */
1018
+ function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled") {
1019
+ const ydoc = fragment.doc;
1020
+ if (!ydoc) {
1021
+ console.warn("[markdownToYjs] fragment has no doc — skipping population");
1022
+ return;
1023
+ }
1024
+ const fm = parseFrontmatter(markdown);
1025
+ const blocks = parseBlocks(fm.body);
1026
+ let title = fallbackTitle;
1027
+ let titleSource;
1028
+ if (fm.title !== void 0) {
1029
+ title = fm.title;
1030
+ titleSource = "frontmatter";
1031
+ }
1032
+ let contentBlocks = blocks;
1033
+ const h1 = blocks.findIndex((b) => b.type === "heading" && b.level === 1);
1034
+ if (h1 !== -1) {
1035
+ title = blocks[h1].text;
1036
+ contentBlocks = blocks.filter((_, i) => i !== h1);
1037
+ titleSource = "h1";
1038
+ }
1039
+ ydoc.transact(() => {
1040
+ const headerEl = new yjs.XmlElement("documentHeader");
1041
+ const metaEl = new yjs.XmlElement("documentMeta");
1042
+ const bodyEls = contentBlocks.map((b) => {
1043
+ switch (b.type) {
1044
+ case "heading": return new yjs.XmlElement("heading");
1045
+ case "paragraph": return new yjs.XmlElement("paragraph");
1046
+ case "bulletList": return new yjs.XmlElement("bulletList");
1047
+ case "orderedList": return new yjs.XmlElement("orderedList");
1048
+ case "taskList": return new yjs.XmlElement("taskList");
1049
+ case "codeBlock": return new yjs.XmlElement("codeBlock");
1050
+ case "blockquote": return new yjs.XmlElement("blockquote");
1051
+ case "table": return new yjs.XmlElement("table");
1052
+ case "hr": return new yjs.XmlElement("horizontalRule");
1053
+ case "callout": return new yjs.XmlElement("callout");
1054
+ case "collapsible": return new yjs.XmlElement("collapsible");
1055
+ case "steps": return new yjs.XmlElement("steps");
1056
+ case "card": return new yjs.XmlElement("card");
1057
+ case "cardGroup": return new yjs.XmlElement("cardGroup");
1058
+ case "codeCollapse": return new yjs.XmlElement("codeCollapse");
1059
+ case "codeGroup": return new yjs.XmlElement("codeGroup");
1060
+ case "codePreview": return new yjs.XmlElement("codePreview");
1061
+ case "codeTree": return new yjs.XmlElement("codeTree");
1062
+ case "accordion": return new yjs.XmlElement("accordion");
1063
+ case "tabs": return new yjs.XmlElement("tabs");
1064
+ case "field": return new yjs.XmlElement("field");
1065
+ case "fieldGroup": return new yjs.XmlElement("fieldGroup");
1066
+ case "image": return new yjs.XmlElement("image");
1067
+ case "docEmbed": return new yjs.XmlElement("docEmbed");
1068
+ case "mathBlock": return new yjs.XmlElement("mathBlock");
1069
+ case "fileBlock": return new yjs.XmlElement("fileBlock");
1070
+ }
1071
+ });
1072
+ fragment.insert(0, [
1073
+ headerEl,
1074
+ metaEl,
1075
+ ...bodyEls
1076
+ ]);
1077
+ if (titleSource) headerEl.setAttribute("titleSource", titleSource);
1078
+ const headerXt = new yjs.XmlText();
1079
+ headerEl.insert(0, [headerXt]);
1080
+ headerXt.insert(0, title);
1081
+ for (const k of Object.keys(fm.meta)) {
1082
+ const v = fm.meta[k];
1083
+ if (v === void 0 || v === null) continue;
1084
+ metaEl.setAttribute(k, v);
1085
+ }
1086
+ if (fm.type) metaEl.setAttribute("type", fm.type);
1087
+ contentBlocks.forEach((block, i) => fillBlock(bodyEls[i], block));
1088
+ });
1089
+ }
1090
+
1091
+ //#endregion
1092
+ //#region packages/convert/src/yjs-to-markdown.ts
1093
+ function serializeDelta(delta) {
1094
+ let result = "";
1095
+ for (const op of delta) {
1096
+ if (typeof op.insert !== "string") continue;
1097
+ let text = op.insert;
1098
+ const attrs = op.attributes ?? {};
1099
+ if (attrs.code) {
1100
+ result += `\`${text}\``;
1101
+ continue;
1102
+ }
1103
+ if (attrs.badge) {
1104
+ const b = attrs.badge;
1105
+ const props = [];
1106
+ if (b.color && b.color !== "neutral") props.push(`color="${b.color}"`);
1107
+ if (b.variant && b.variant !== "subtle") props.push(`variant="${b.variant}"`);
1108
+ result += `:badge[${b.label || text}]${props.length ? `{${props.join(" ")}}` : ""}`;
1109
+ continue;
1110
+ }
1111
+ if (attrs.proseIcon) {
1112
+ const icon = attrs.proseIcon.name || "i-lucide-star";
1113
+ result += `:icon{name="${icon}"}`;
1114
+ continue;
1115
+ }
1116
+ if (attrs.kbd) {
1117
+ const value = attrs.kbd.value || text;
1118
+ result += `:kbd{value="${value}"}`;
1119
+ continue;
1120
+ }
1121
+ if (attrs.docLink) {
1122
+ const docId = attrs.docLink.docId;
1123
+ if (docId) {
1124
+ result += text === docId ? `[[${docId}]]` : `[[${docId}|${text}]]`;
1125
+ continue;
1126
+ }
1127
+ }
1128
+ if (attrs.mention) {
1129
+ const { userId, label } = attrs.mention;
1130
+ if (userId) {
1131
+ result += `@[${label || text}](user:${userId})`;
1132
+ continue;
1133
+ }
1134
+ }
1135
+ if (attrs.mathInline) {
1136
+ const expr = attrs.mathInline.expression ?? text;
1137
+ result += `$${expr}$`;
1138
+ continue;
1139
+ }
1140
+ if (attrs.bold) text = `**${text}**`;
1141
+ if (attrs.italic) text = `*${text}*`;
1142
+ if (attrs.strike) text = `~~${text}~~`;
1143
+ if (attrs.link) {
1144
+ const href = attrs.link.href ?? "";
1145
+ text = `[${text}](${href})`;
1146
+ }
1147
+ result += text;
1148
+ }
1149
+ return result;
1150
+ }
1151
+ function serializeInline(el) {
1152
+ const parts = [];
1153
+ for (const child of el.toArray()) if (child instanceof yjs.XmlText) parts.push(serializeDelta(child.toDelta()));
1154
+ else if (child instanceof yjs.XmlElement) parts.push(serializeInline(child));
1155
+ return parts.join("");
1156
+ }
1157
+ function serializeBlock(el, indent = "") {
1158
+ if (el instanceof yjs.XmlText) return serializeDelta(el.toDelta());
1159
+ switch (el.nodeName) {
1160
+ case "documentHeader":
1161
+ case "documentMeta": return "";
1162
+ case "heading": {
1163
+ const level = Number(el.getAttribute("level") ?? 2);
1164
+ return `${"#".repeat(level)} ${serializeInline(el)}`;
1165
+ }
1166
+ case "paragraph": return serializeInline(el);
1167
+ case "bulletList": return serializeListItems(el, "bullet", indent);
1168
+ case "orderedList": return serializeListItems(el, "ordered", indent);
1169
+ case "taskList": return serializeTaskList(el, indent);
1170
+ case "codeBlock": {
1171
+ const lang = el.getAttribute("language") ?? "";
1172
+ const code = getCodeBlockText(el);
1173
+ if (code === "") return `\`\`\`${lang}\n\`\`\``;
1174
+ return `\`\`\`${lang}\n${code}\n\`\`\``;
1175
+ }
1176
+ case "blockquote": {
1177
+ const lines = [];
1178
+ for (const child of el.toArray()) if (child instanceof yjs.XmlElement) {
1179
+ const text = serializeBlock(child);
1180
+ for (const line of text.split("\n")) lines.push(`> ${line}`);
1181
+ }
1182
+ return lines.join("\n");
1183
+ }
1184
+ case "table": return serializeTable(el);
1185
+ case "horizontalRule": return "---";
1186
+ case "image": {
1187
+ const src = el.getAttribute("src") ?? "";
1188
+ const alt = el.getAttribute("alt") ?? "";
1189
+ const width = el.getAttribute("width");
1190
+ const height = el.getAttribute("height");
1191
+ const attrs = [];
1192
+ if (width) attrs.push(`width=${width}`);
1193
+ if (height) attrs.push(`height=${height}`);
1194
+ return `![${alt}](${src})${attrs.length ? `{${attrs.join(" ")}}` : ""}`;
1195
+ }
1196
+ case "docEmbed": {
1197
+ const docId = el.getAttribute("docId") ?? "";
1198
+ const collapsed = el.getAttribute("collapsed");
1199
+ const tall = el.getAttribute("tall");
1200
+ const seamless = el.getAttribute("seamless");
1201
+ const flags = [];
1202
+ if (collapsed === true || collapsed === "true") flags.push("collapsed");
1203
+ if (tall === true || tall === "true") flags.push("tall");
1204
+ if (seamless === true || seamless === "true") flags.push("seamless");
1205
+ return `![[${docId}]]${flags.length ? `{${flags.join(" ")}}` : ""}`;
1206
+ }
1207
+ case "mathBlock": return `\`\`\`math\n${el.getAttribute("expression") ?? ""}\n\`\`\``;
1208
+ case "fileBlock": {
1209
+ const uploadId = el.getAttribute("uploadId") ?? "";
1210
+ const filename = el.getAttribute("filename") ?? "";
1211
+ const mime = el.getAttribute("mime") ?? "";
1212
+ const src = el.getAttribute("src") ?? (uploadId && filename ? `.abracadabra/files/${uploadId}-${filename}` : "");
1213
+ const props = [];
1214
+ if (src) props.push(`src="${src}"`);
1215
+ if (mime) props.push(`mime="${mime}"`);
1216
+ if (uploadId) props.push(`upload-id="${uploadId}"`);
1217
+ if (filename) props.push(`filename="${filename}"`);
1218
+ return `:file{${props.join(" ")}}`;
1219
+ }
1220
+ case "callout": return `::${el.getAttribute("type") ?? "note"}\n${serializeChildren(el)}\n::`;
1221
+ case "collapsible": {
1222
+ const label = el.getAttribute("label") ?? "Details";
1223
+ const open = el.getAttribute("open");
1224
+ const props = [`label="${label}"`];
1225
+ if (open === true || open === "true") props.push("open=\"true\"");
1226
+ return `::collapsible{${props.join(" ")}}\n${serializeChildren(el)}\n::`;
1227
+ }
1228
+ case "steps": return `::steps\n${serializeChildren(el)}\n::`;
1229
+ case "card": {
1230
+ const props = [];
1231
+ const title = el.getAttribute("title");
1232
+ const icon = el.getAttribute("icon");
1233
+ const to = el.getAttribute("to");
1234
+ if (title) props.push(`title="${title}"`);
1235
+ if (icon) props.push(`icon="${icon}"`);
1236
+ if (to) props.push(`to="${to}"`);
1237
+ return `::card${props.length ? `{${props.join(" ")}}` : ""}\n${serializeChildren(el)}\n::`;
1238
+ }
1239
+ case "cardGroup": return `::card-group\n${el.toArray().filter((c) => c instanceof yjs.XmlElement).map((c) => serializeBlock(c)).join("\n\n")}\n::`;
1240
+ case "codeCollapse": return `::code-collapse\n${el.toArray().filter((c) => c instanceof yjs.XmlElement && c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n")}\n::`;
1241
+ case "codeGroup": return `::code-group\n${el.toArray().filter((c) => c instanceof yjs.XmlElement && c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n")}\n::`;
1242
+ case "codePreview": {
1243
+ const children = el.toArray().filter((c) => c instanceof yjs.XmlElement);
1244
+ const nonCode = children.filter((c) => c.nodeName !== "codeBlock").map((c) => serializeBlock(c)).join("\n\n");
1245
+ const code = children.filter((c) => c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n");
1246
+ const parts = [nonCode];
1247
+ if (code) parts.push(`#code\n${code}`);
1248
+ return `::code-preview\n${parts.filter(Boolean).join("\n\n")}\n::`;
1249
+ }
1250
+ case "codeTree": return `::code-tree{files="${el.getAttribute("files") ?? "[]"}"}\n::`;
1251
+ case "accordion": return serializeSlottedContainer(el, "accordion", "accordionItem", "item");
1252
+ case "tabs": return serializeSlottedContainer(el, "tabs", "tabsItem", "tab");
1253
+ case "field": {
1254
+ const fieldName = el.getAttribute("name") ?? "";
1255
+ const fieldType = el.getAttribute("type") ?? "string";
1256
+ const required = el.getAttribute("required");
1257
+ const props = [`name="${fieldName}"`, `type="${fieldType}"`];
1258
+ if (required === true || required === "true") props.push("required=\"true\"");
1259
+ return `::field{${props.join(" ")}}\n${serializeChildren(el)}\n::`;
1260
+ }
1261
+ case "fieldGroup": return `::field-group\n${el.toArray().filter((c) => c instanceof yjs.XmlElement).map((c) => serializeBlock(c)).join("\n\n")}\n::`;
1262
+ default: return serializeChildren(el);
1263
+ }
1264
+ }
1265
+ function serializeChildren(el) {
1266
+ const blocks = [];
1267
+ for (const child of el.toArray()) if (child instanceof yjs.XmlElement) {
1268
+ const text = serializeBlock(child);
1269
+ if (text) blocks.push(text);
1270
+ } else if (child instanceof yjs.XmlText) {
1271
+ const text = serializeDelta(child.toDelta());
1272
+ if (text) blocks.push(text);
1273
+ }
1274
+ return blocks.join("\n\n");
1275
+ }
1276
+ function serializeListItems(el, type, indent) {
1277
+ const lines = [];
1278
+ let counter = 1;
1279
+ for (const child of el.toArray()) {
1280
+ if (!(child instanceof yjs.XmlElement) || child.nodeName !== "listItem") continue;
1281
+ const prefix = type === "bullet" ? "- " : `${counter++}. `;
1282
+ const subParts = [];
1283
+ for (const sub of child.toArray()) {
1284
+ if (!(sub instanceof yjs.XmlElement)) continue;
1285
+ if (sub.nodeName === "bulletList") subParts.push(serializeListItems(sub, "bullet", indent + " "));
1286
+ else if (sub.nodeName === "orderedList") subParts.push(serializeListItems(sub, "ordered", indent + " "));
1287
+ else subParts.push(serializeInline(sub));
1288
+ }
1289
+ if (subParts.length <= 1) lines.push(`${indent}${prefix}${subParts[0] ?? ""}`);
1290
+ else {
1291
+ lines.push(`${indent}${prefix}${subParts[0] ?? ""}`);
1292
+ for (let i = 1; i < subParts.length; i++) lines.push(subParts[i]);
1293
+ }
1294
+ }
1295
+ return lines.join("\n");
1296
+ }
1297
+ function serializeTaskList(el, indent) {
1298
+ const lines = [];
1299
+ for (const child of el.toArray()) {
1300
+ if (!(child instanceof yjs.XmlElement) || child.nodeName !== "taskItem") continue;
1301
+ const checked = child.getAttribute("checked");
1302
+ const marker = checked === true || checked === "true" ? "[x]" : "[ ]";
1303
+ let header = "";
1304
+ const nestedParts = [];
1305
+ for (const sub of child.toArray()) {
1306
+ if (!(sub instanceof yjs.XmlElement)) continue;
1307
+ if (sub.nodeName === "paragraph" && header === "") header = serializeInline(sub);
1308
+ else if (sub.nodeName === "bulletList") nestedParts.push(serializeListItems(sub, "bullet", indent + " "));
1309
+ else if (sub.nodeName === "orderedList") nestedParts.push(serializeListItems(sub, "ordered", indent + " "));
1310
+ else if (sub.nodeName === "taskList") nestedParts.push(serializeTaskList(sub, indent + " "));
1311
+ else nestedParts.push(indent + " " + serializeBlock(sub, indent + " "));
1312
+ }
1313
+ lines.push(`${indent}- ${marker} ${header}`);
1314
+ for (const part of nestedParts) lines.push(part);
1315
+ }
1316
+ return lines.join("\n");
1317
+ }
1318
+ function getCodeBlockText(el) {
1319
+ for (const child of el.toArray()) if (child instanceof yjs.XmlText) return child.toString();
1320
+ return "";
1321
+ }
1322
+ function serializeTable(el) {
1323
+ const rows = el.toArray().filter((c) => c instanceof yjs.XmlElement);
1324
+ if (!rows.length) return "";
1325
+ const serializedRows = [];
1326
+ for (const row of rows) {
1327
+ const cells = row.toArray().filter((c) => c instanceof yjs.XmlElement).map((cell) => {
1328
+ return cell.toArray().filter((c) => c instanceof yjs.XmlElement).map((c) => serializeInline(c)).join(" ");
1329
+ });
1330
+ serializedRows.push(cells);
1331
+ }
1332
+ if (!serializedRows.length) return "";
1333
+ const colCount = Math.max(...serializedRows.map((r) => r.length));
1334
+ const headerRow = serializedRows[0];
1335
+ const separator = Array(colCount).fill("---");
1336
+ const dataRows = serializedRows.slice(1);
1337
+ const formatRow = (cells) => {
1338
+ return `| ${Array(colCount).fill("").map((_, i) => cells[i] ?? "").join(" | ")} |`;
1339
+ };
1340
+ return [
1341
+ formatRow(headerRow),
1342
+ formatRow(separator),
1343
+ ...dataRows.map(formatRow)
1344
+ ].join("\n");
1345
+ }
1346
+ function serializeSlottedContainer(el, containerName, childName, slotPrefix) {
1347
+ return `::${containerName}\n${el.toArray().filter((c) => c instanceof yjs.XmlElement && c.nodeName === childName).map((item) => {
1348
+ const label = item.getAttribute("label") ?? "";
1349
+ const icon = item.getAttribute("icon") ?? "";
1350
+ const props = [];
1351
+ if (label) props.push(`label="${label}"`);
1352
+ if (icon) props.push(`icon="${icon}"`);
1353
+ const content = serializeChildren(item);
1354
+ return `#${slotPrefix}{${props.join(" ")}}\n${content}`;
1355
+ }).join("\n\n")}\n::`;
1356
+ }
1357
+ function generateFrontmatter(label, meta, type) {
1358
+ const lines = [];
1359
+ if (label !== void 0) lines.push(`title: "${escapeYaml(label)}"`);
1360
+ if (type && type !== "doc") lines.push(`type: ${type}`);
1361
+ if (!meta) return `---\n${lines.join("\n")}\n---`;
1362
+ if (meta.tags?.length) lines.push(`tags: [${meta.tags.join(", ")}]`);
1363
+ if (meta.color) lines.push(`color: ${yamlScalar(meta.color)}`);
1364
+ if (meta.icon) lines.push(`icon: ${yamlScalar(meta.icon)}`);
1365
+ if (meta.status) lines.push(`status: ${yamlScalar(meta.status)}`);
1366
+ if (meta.priority !== void 0 && meta.priority !== 0) lines.push(`priority: ${{
1367
+ 1: "low",
1368
+ 2: "medium",
1369
+ 3: "high",
1370
+ 4: "urgent"
1371
+ }[meta.priority] ?? meta.priority}`);
1372
+ if (meta.checked !== void 0) lines.push(`checked: ${meta.checked}`);
1373
+ if (meta.dateStart) lines.push(`dateStart: "${escapeYaml(meta.dateStart)}"`);
1374
+ if (meta.dateEnd) lines.push(`dateEnd: "${escapeYaml(meta.dateEnd)}"`);
1375
+ if (meta.subtitle) lines.push(`subtitle: "${escapeYaml(meta.subtitle)}"`);
1376
+ if (meta.url) lines.push(`url: ${meta.url}`);
1377
+ if (meta.rating !== void 0 && meta.rating !== 0) lines.push(`rating: ${meta.rating}`);
1378
+ return `---\n${lines.join("\n")}\n---`;
1379
+ }
1380
+ /**
1381
+ * Render a YAML scalar — bare when safe, double-quoted when the value
1382
+ * needs escaping. YAML treats `#`, `:`, leading whitespace, and a few
1383
+ * other characters as syntactically significant, so anything starting
1384
+ * with one of those gets quoted to stay round-trip safe.
1385
+ */
1386
+ function yamlScalar(s) {
1387
+ if (s === "") return "\"\"";
1388
+ if (/^[#&*!|>%@`]/.test(s)) return `"${escapeYaml(s)}"`;
1389
+ if (/[:"]/.test(s)) return `"${escapeYaml(s)}"`;
1390
+ if (/^\s|\s$/.test(s)) return `"${escapeYaml(s)}"`;
1391
+ return s;
1392
+ }
1393
+ function escapeYaml(s) {
1394
+ return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
1395
+ }
1396
+ function serializeBlockToHtml(el) {
1397
+ if (el instanceof yjs.XmlText) return serializeDeltaToHtml(el.toDelta());
1398
+ const name = el.nodeName;
1399
+ switch (name) {
1400
+ case "documentHeader":
1401
+ case "documentMeta": return "";
1402
+ case "heading": {
1403
+ const level = Number(el.getAttribute("level") ?? 2);
1404
+ return `<h${level}>${serializeInlineHtml(el)}</h${level}>`;
1405
+ }
1406
+ case "paragraph": return `<p>${serializeInlineHtml(el)}</p>`;
1407
+ case "bulletList": return `<ul>${serializeListHtml(el)}</ul>`;
1408
+ case "orderedList": return `<ol>${serializeListHtml(el)}</ol>`;
1409
+ case "taskList": return `<ul>${serializeTaskListHtml(el)}</ul>`;
1410
+ case "codeBlock": {
1411
+ const lang = el.getAttribute("language") ?? "";
1412
+ const code = escapeHtml(getCodeBlockText(el));
1413
+ return lang ? `<pre><code class="language-${lang}">${code}</code></pre>` : `<pre><code>${code}</code></pre>`;
1414
+ }
1415
+ case "blockquote": return `<blockquote>\n${el.toArray().filter((c) => c instanceof yjs.XmlElement).map((c) => serializeBlockToHtml(c)).join("\n")}\n</blockquote>`;
1416
+ case "table": return serializeTableHtml(el);
1417
+ case "horizontalRule": return "<hr>";
1418
+ case "image": {
1419
+ const src = el.getAttribute("src") ?? "";
1420
+ const alt = el.getAttribute("alt") ?? "";
1421
+ return `<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}">`;
1422
+ }
1423
+ case "fileBlock": {
1424
+ const uploadId = el.getAttribute("uploadId") ?? "";
1425
+ const filename = el.getAttribute("filename") ?? "file";
1426
+ if (uploadId) return `<!--fileblock:${uploadId}:${filename}-->`;
1427
+ return `<!-- file: ${filename} -->`;
1428
+ }
1429
+ default: return `<div data-type="${name}">\n${el.toArray().filter((c) => c instanceof yjs.XmlElement || c instanceof yjs.XmlText).map((c) => c instanceof yjs.XmlElement ? serializeBlockToHtml(c) : serializeDeltaToHtml(c.toDelta())).join("\n")}\n</div>`;
1430
+ }
1431
+ }
1432
+ function serializeInlineHtml(el) {
1433
+ const parts = [];
1434
+ for (const child of el.toArray()) if (child instanceof yjs.XmlText) parts.push(serializeDeltaToHtml(child.toDelta()));
1435
+ else if (child instanceof yjs.XmlElement) parts.push(serializeInlineHtml(child));
1436
+ return parts.join("");
1437
+ }
1438
+ function serializeDeltaToHtml(delta) {
1439
+ let result = "";
1440
+ for (const op of delta) {
1441
+ if (typeof op.insert !== "string") continue;
1442
+ let text = escapeHtml(op.insert);
1443
+ const attrs = op.attributes ?? {};
1444
+ if (attrs.code) text = `<code>${text}</code>`;
1445
+ if (attrs.bold) text = `<strong>${text}</strong>`;
1446
+ if (attrs.italic) text = `<em>${text}</em>`;
1447
+ if (attrs.strike) text = `<s>${text}</s>`;
1448
+ if (attrs.link) text = `<a href="${escapeHtml(attrs.link.href ?? "")}">${text}</a>`;
1449
+ result += text;
1450
+ }
1451
+ return result;
1452
+ }
1453
+ function serializeListHtml(el) {
1454
+ return el.toArray().filter((c) => c instanceof yjs.XmlElement && c.nodeName === "listItem").map((li) => `<li>${li.toArray().filter((c) => c instanceof yjs.XmlElement).map((c) => serializeBlockToHtml(c)).join("")}</li>`).join("\n");
1455
+ }
1456
+ function serializeTaskListHtml(el) {
1457
+ return el.toArray().filter((c) => c instanceof yjs.XmlElement && c.nodeName === "taskItem").map((ti) => {
1458
+ const rawChecked = ti.getAttribute("checked");
1459
+ const checked = rawChecked === true || rawChecked === "true";
1460
+ const text = ti.toArray().filter((c) => c instanceof yjs.XmlElement).map((c) => serializeInlineHtml(c)).join("");
1461
+ return `<li><input type="checkbox"${checked ? " checked" : ""} disabled> ${text}</li>`;
1462
+ }).join("\n");
1463
+ }
1464
+ function serializeTableHtml(el) {
1465
+ const rows = el.toArray().filter((c) => c instanceof yjs.XmlElement);
1466
+ if (!rows.length) return "";
1467
+ return `<table>\n${rows.map((row, ri) => {
1468
+ const tag = ri === 0 ? "th" : "td";
1469
+ return `<tr>${row.toArray().filter((c) => c instanceof yjs.XmlElement).map((cell) => {
1470
+ return `<${tag}>${cell.toArray().filter((c) => c instanceof yjs.XmlElement).map((c) => serializeInlineHtml(c)).join("")}</${tag}>`;
1471
+ }).join("")}</tr>`;
1472
+ }).join("\n")}\n</table>`;
1473
+ }
1474
+ function escapeHtml(s) {
1475
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1476
+ }
1477
+ function yjsToMarkdown(fragment, label, meta, type) {
1478
+ const { text: headerText, source: titleSource } = readDocumentHeader(fragment);
1479
+ const effectiveTitle = headerText || label;
1480
+ const docMeta = readDocumentMeta(fragment);
1481
+ const effectiveMeta = meta ?? docMeta.meta;
1482
+ const effectiveType = type ?? docMeta.type;
1483
+ const metaIsEmpty = isMetaEmpty(effectiveMeta);
1484
+ const typeIsDefault = !effectiveType || effectiveType === "doc";
1485
+ const bodyBlocks = collectBodyBlocks(fragment);
1486
+ let body;
1487
+ if (titleSource === "h1" && effectiveTitle) {
1488
+ const tail = serializeBlocksClean(bodyBlocks);
1489
+ body = tail === "" ? `# ${effectiveTitle}` : `# ${effectiveTitle}\n\n${tail}`;
1490
+ } else body = serializeBlocksClean(bodyBlocks);
1491
+ const wantFrontmatterTitle = titleSource === "frontmatter";
1492
+ if (!wantFrontmatterTitle && !(!metaIsEmpty || !typeIsDefault)) return body === "" ? "" : `${body}\n`;
1493
+ const frontmatter = generateFrontmatter(wantFrontmatterTitle ? effectiveTitle : void 0, effectiveMeta, effectiveType);
1494
+ if (body === "") return `${frontmatter}\n`;
1495
+ return `${frontmatter}\n\n${body}\n`;
1496
+ }
1497
+ function readDocumentMeta(fragment) {
1498
+ const meta = {};
1499
+ let type;
1500
+ for (const child of fragment.toArray()) {
1501
+ if (!(child instanceof yjs.XmlElement) || child.nodeName !== "documentMeta") continue;
1502
+ const attrs = child.getAttributes();
1503
+ for (const k of Object.keys(attrs)) {
1504
+ const v = attrs[k];
1505
+ if (v === void 0 || v === null) continue;
1506
+ if (k === "type" && typeof v === "string") {
1507
+ type = v;
1508
+ continue;
1509
+ }
1510
+ meta[k] = v;
1511
+ }
1512
+ break;
1513
+ }
1514
+ return {
1515
+ meta,
1516
+ type
1517
+ };
1518
+ }
1519
+ function readDocumentHeader(fragment) {
1520
+ for (const child of fragment.toArray()) {
1521
+ if (!(child instanceof yjs.XmlElement) || child.nodeName !== "documentHeader") continue;
1522
+ const text = child.toArray().find((c) => c instanceof yjs.XmlText);
1523
+ const src = child.getAttribute("titleSource");
1524
+ const source = src === "h1" || src === "frontmatter" ? src : void 0;
1525
+ return {
1526
+ text: text ? text.toString() : "",
1527
+ source
1528
+ };
1529
+ }
1530
+ return { text: "" };
1531
+ }
1532
+ function collectBodyBlocks(fragment) {
1533
+ const out = [];
1534
+ for (const child of fragment.toArray()) {
1535
+ if (!(child instanceof yjs.XmlElement)) continue;
1536
+ if (child.nodeName === "documentHeader" || child.nodeName === "documentMeta") continue;
1537
+ out.push(child);
1538
+ }
1539
+ return out;
1540
+ }
1541
+ function serializeBlocksClean(blocks) {
1542
+ const parts = [];
1543
+ for (const block of blocks) {
1544
+ if (block.nodeName === "paragraph" && block.length === 0) {
1545
+ parts.push("");
1546
+ continue;
1547
+ }
1548
+ parts.push(serializeBlock(block));
1549
+ }
1550
+ while (parts.length && parts[parts.length - 1] === "") parts.pop();
1551
+ return parts.join("\n\n");
1552
+ }
1553
+ function isMetaEmpty(meta) {
1554
+ if (!meta) return true;
1555
+ for (const key of Object.keys(meta)) {
1556
+ const v = meta[key];
1557
+ if (v === void 0 || v === null) continue;
1558
+ if (typeof v === "string" && v === "") continue;
1559
+ if (Array.isArray(v) && v.length === 0) continue;
1560
+ return false;
1561
+ }
1562
+ return true;
1563
+ }
1564
+ /**
1565
+ * Walk the Y.XmlFragment and concatenate all text content with no
1566
+ * markup or frontmatter. Block boundaries become newlines. Useful for
1567
+ * accessibility tooling, search indexing, and snippet previews.
1568
+ */
1569
+ function yjsToPlainText(fragment) {
1570
+ const out = [];
1571
+ const visit = (node) => {
1572
+ if (node instanceof yjs.XmlText) {
1573
+ out.push(node.toString());
1574
+ return;
1575
+ }
1576
+ if (node.nodeName === "documentMeta") return;
1577
+ if (node.nodeName === "image") {
1578
+ const alt = node.getAttribute("alt") ?? "";
1579
+ if (alt) out.push(alt);
1580
+ return;
1581
+ }
1582
+ for (const child of node.toArray()) if (child instanceof yjs.XmlText || child instanceof yjs.XmlElement) visit(child);
1583
+ if (node.nodeName !== "paragraph" && node.length === 0) return;
1584
+ out.push("\n");
1585
+ };
1586
+ for (const child of fragment.toArray()) if (child instanceof yjs.XmlText || child instanceof yjs.XmlElement) visit(child);
1587
+ return out.join("").replace(/\n+$/, "").replace(/\n{3,}/g, "\n\n");
1588
+ }
1589
+ function yjsToHtml(fragment, label) {
1590
+ const title = escapeHtml(label);
1591
+ const bodyParts = [];
1592
+ for (const child of fragment.toArray()) if (child instanceof yjs.XmlElement) {
1593
+ const html = serializeBlockToHtml(child);
1594
+ if (html) bodyParts.push(html);
1595
+ }
1596
+ return `<!DOCTYPE html>
1597
+ <html>
1598
+ <head><meta charset="utf-8"><title>${title}</title></head>
1599
+ <body>
1600
+ <h1>${title}</h1>
1601
+ ${bodyParts.join("\n")}
1602
+ </body>
1603
+ </html>
1604
+ `;
1605
+ }
1606
+
1607
+ //#endregion
1608
+ //#region packages/convert/src/html-to-yjs.ts
1609
+ function getTextContent(node) {
1610
+ return node.textContent ?? "";
1611
+ }
1612
+ function langFromClass(el) {
1613
+ for (const cls of Array.from(el.classList)) if (cls.startsWith("language-")) return cls.slice(9);
1614
+ return "";
1615
+ }
1616
+ /**
1617
+ * Collect inline marks from a DOM element's ancestry.
1618
+ * Returns merged marks object accumulated from the provided stack.
1619
+ */
1620
+ function mergeMarks(stack) {
1621
+ const merged = {};
1622
+ for (const m of stack) {
1623
+ if (m.bold) merged.bold = true;
1624
+ if (m.italic) merged.italic = true;
1625
+ if (m.code) merged.code = true;
1626
+ if (m.strike) merged.strike = true;
1627
+ if (m.link) merged.link = m.link;
1628
+ }
1629
+ return merged;
1630
+ }
1631
+ /** Walk an inline subtree and collect { text, marks } runs. */
1632
+ function collectInlineRuns(node, markStack) {
1633
+ const runs = [];
1634
+ if (node.nodeType === Node.TEXT_NODE) {
1635
+ const text = node.textContent ?? "";
1636
+ if (text) runs.push({
1637
+ text,
1638
+ marks: mergeMarks(markStack)
1639
+ });
1640
+ return runs;
1641
+ }
1642
+ if (node.nodeType !== Node.ELEMENT_NODE) return runs;
1643
+ const el = node;
1644
+ const tag = el.tagName.toLowerCase();
1645
+ const newMarks = {};
1646
+ if (tag === "strong" || tag === "b") newMarks.bold = true;
1647
+ if (tag === "em" || tag === "i") newMarks.italic = true;
1648
+ if (tag === "code") newMarks.code = true;
1649
+ if (tag === "s" || tag === "del" || tag === "strike") newMarks.strike = true;
1650
+ if (tag === "a") {
1651
+ const href = el.getAttribute("href");
1652
+ if (href) newMarks.link = { href };
1653
+ }
1654
+ const nextStack = Object.keys(newMarks).length ? [...markStack, newMarks] : markStack;
1655
+ for (const child of Array.from(el.childNodes)) runs.push(...collectInlineRuns(child, nextStack));
1656
+ return runs;
1657
+ }
1658
+ /** Fill an already-attached Y.XmlElement with inline text runs. */
1659
+ function fillInlineRuns(paraEl, runs) {
1660
+ if (!runs.length) return;
1661
+ const xtNodes = runs.map(() => new yjs.XmlText());
1662
+ paraEl.insert(0, xtNodes);
1663
+ runs.forEach((run, i) => {
1664
+ const attrs = {};
1665
+ if (run.marks.bold) attrs["bold"] = true;
1666
+ if (run.marks.italic) attrs["italic"] = true;
1667
+ if (run.marks.code) attrs["code"] = true;
1668
+ if (run.marks.strike) attrs["strike"] = true;
1669
+ if (run.marks.link) attrs["link"] = run.marks.link;
1670
+ if (Object.keys(attrs).length) xtNodes[i].insert(0, run.text, attrs);
1671
+ else xtNodes[i].insert(0, run.text);
1672
+ });
1673
+ }
1674
+ /** Convert a single block-level DOM element to Y.XmlElement(s) and append to `container`. */
1675
+ function convertBlockElement(el, container) {
1676
+ const tag = el.tagName.toLowerCase();
1677
+ if (tag === "div" && el.getAttribute("data-type") === "file-block") {
1678
+ const fileBlockEl = new yjs.XmlElement("fileBlock");
1679
+ container.insert(container.length, [fileBlockEl]);
1680
+ const uploadId = el.getAttribute("uploadid") ?? "";
1681
+ const docId = el.getAttribute("docid") ?? "";
1682
+ const filename = el.getAttribute("filename") ?? "";
1683
+ const mimeType = el.getAttribute("mimetype") ?? "";
1684
+ if (uploadId) fileBlockEl.setAttribute("uploadId", uploadId);
1685
+ if (docId) fileBlockEl.setAttribute("docId", docId);
1686
+ if (filename) fileBlockEl.setAttribute("filename", filename);
1687
+ if (mimeType) fileBlockEl.setAttribute("mimeType", mimeType);
1688
+ return;
1689
+ }
1690
+ const headingMatch = tag.match(/^h([1-6])$/);
1691
+ if (headingMatch) {
1692
+ const level = parseInt(headingMatch[1]);
1693
+ const headingEl = new yjs.XmlElement("heading");
1694
+ container.insert(container.length, [headingEl]);
1695
+ headingEl.setAttribute("level", level);
1696
+ fillInlineRuns(headingEl, collectInlineRuns(el, []));
1697
+ return;
1698
+ }
1699
+ if (tag === "p") {
1700
+ const paraEl = new yjs.XmlElement("paragraph");
1701
+ container.insert(container.length, [paraEl]);
1702
+ fillInlineRuns(paraEl, collectInlineRuns(el, []));
1703
+ return;
1704
+ }
1705
+ if (tag === "hr") {
1706
+ const hrEl = new yjs.XmlElement("horizontalRule");
1707
+ container.insert(container.length, [hrEl]);
1708
+ return;
1709
+ }
1710
+ if (tag === "pre") {
1711
+ const codeEl = el.querySelector("code");
1712
+ const codeBlock = new yjs.XmlElement("codeBlock");
1713
+ container.insert(container.length, [codeBlock]);
1714
+ const lang = codeEl ? langFromClass(codeEl) : "";
1715
+ if (lang) codeBlock.setAttribute("language", lang);
1716
+ const xt = new yjs.XmlText();
1717
+ codeBlock.insert(0, [xt]);
1718
+ xt.insert(0, (codeEl ?? el).textContent ?? "");
1719
+ return;
1720
+ }
1721
+ if (tag === "blockquote") {
1722
+ const bqEl = new yjs.XmlElement("blockquote");
1723
+ container.insert(container.length, [bqEl]);
1724
+ const paraEl = new yjs.XmlElement("paragraph");
1725
+ bqEl.insert(0, [paraEl]);
1726
+ fillInlineRuns(paraEl, collectInlineRuns(el, []));
1727
+ return;
1728
+ }
1729
+ if (tag === "ul" || tag === "ol") {
1730
+ const items = Array.from(el.querySelectorAll(":scope > li"));
1731
+ if (items.some((li) => li.querySelector("input[type=\"checkbox\"]"))) {
1732
+ const taskListEl = new yjs.XmlElement("taskList");
1733
+ container.insert(container.length, [taskListEl]);
1734
+ const taskItemEls = items.map(() => new yjs.XmlElement("taskItem"));
1735
+ taskListEl.insert(0, taskItemEls);
1736
+ items.forEach((li, i) => {
1737
+ const checked = li.querySelector("input[type=\"checkbox\"]")?.checked ?? false;
1738
+ taskItemEls[i].setAttribute("checked", checked);
1739
+ const paraEl = new yjs.XmlElement("paragraph");
1740
+ taskItemEls[i].insert(0, [paraEl]);
1741
+ const clone = li.cloneNode(true);
1742
+ clone.querySelector("input")?.remove();
1743
+ fillInlineRuns(paraEl, collectInlineRuns(clone, []));
1744
+ });
1745
+ } else {
1746
+ const listType = tag === "ul" ? "bulletList" : "orderedList";
1747
+ const listEl = new yjs.XmlElement(listType);
1748
+ container.insert(container.length, [listEl]);
1749
+ const listItemEls = items.map(() => new yjs.XmlElement("listItem"));
1750
+ listEl.insert(0, listItemEls);
1751
+ items.forEach((li, i) => {
1752
+ const paraEl = new yjs.XmlElement("paragraph");
1753
+ listItemEls[i].insert(0, [paraEl]);
1754
+ fillInlineRuns(paraEl, collectInlineRuns(li, []));
1755
+ });
1756
+ }
1757
+ return;
1758
+ }
1759
+ if (tag === "table") {
1760
+ const tableEl = new yjs.XmlElement("table");
1761
+ container.insert(container.length, [tableEl]);
1762
+ const rows = Array.from(el.querySelectorAll("tr"));
1763
+ const rowEls = rows.map(() => new yjs.XmlElement("tableRow"));
1764
+ tableEl.insert(0, rowEls);
1765
+ rows.forEach((row, ri) => {
1766
+ const cells = Array.from(row.querySelectorAll("th, td"));
1767
+ const cellType = cells.some((c) => c.tagName.toLowerCase() === "th") ? "tableHeader" : "tableCell";
1768
+ const cellEls = cells.map(() => new yjs.XmlElement(cellType));
1769
+ rowEls[ri].insert(0, cellEls);
1770
+ cells.forEach((cell, ci) => {
1771
+ const paraEl = new yjs.XmlElement("paragraph");
1772
+ cellEls[ci].insert(0, [paraEl]);
1773
+ fillInlineRuns(paraEl, collectInlineRuns(cell, []));
1774
+ });
1775
+ });
1776
+ return;
1777
+ }
1778
+ if (getTextContent(el).trim()) {
1779
+ const paraEl = new yjs.XmlElement("paragraph");
1780
+ container.insert(container.length, [paraEl]);
1781
+ fillInlineRuns(paraEl, collectInlineRuns(el, []));
1782
+ }
1783
+ }
1784
+ /**
1785
+ * Parses an HTML string and writes the result into a Y.XmlFragment that
1786
+ * TipTap's Collaboration extension can read.
1787
+ *
1788
+ * @param fragment The target `Y.Doc.getXmlFragment('default')`
1789
+ * @param html Raw HTML string
1790
+ * @param fallbackTitle Used when no <title> or <h1> is found
1791
+ */
1792
+ function populateYDocFromHtml(fragment, html, fallbackTitle = "Untitled") {
1793
+ const ydoc = fragment.doc;
1794
+ if (!ydoc) {
1795
+ console.warn("[htmlToYjs] fragment has no doc — skipping population");
1796
+ return;
1797
+ }
1798
+ const doc = new DOMParser().parseFromString(html, "text/html");
1799
+ let title = doc.title?.trim() || fallbackTitle;
1800
+ const firstH1 = doc.body.querySelector("h1");
1801
+ if (firstH1) title = firstH1.textContent?.trim() || title;
1802
+ ydoc.transact(() => {
1803
+ const headerEl = new yjs.XmlElement("documentHeader");
1804
+ const metaEl = new yjs.XmlElement("documentMeta");
1805
+ fragment.insert(0, [headerEl, metaEl]);
1806
+ const headerXt = new yjs.XmlText();
1807
+ headerEl.insert(0, [headerXt]);
1808
+ headerXt.insert(0, title);
1809
+ const blockTags = new Set([
1810
+ "p",
1811
+ "h1",
1812
+ "h2",
1813
+ "h3",
1814
+ "h4",
1815
+ "h5",
1816
+ "h6",
1817
+ "ul",
1818
+ "ol",
1819
+ "pre",
1820
+ "blockquote",
1821
+ "table",
1822
+ "hr",
1823
+ "div",
1824
+ "section",
1825
+ "article",
1826
+ "header",
1827
+ "footer",
1828
+ "main",
1829
+ "aside"
1830
+ ]);
1831
+ let hasContent = false;
1832
+ for (const child of Array.from(doc.body.children)) {
1833
+ const tag = child.tagName.toLowerCase();
1834
+ if (!blockTags.has(tag)) continue;
1835
+ if (tag === "h1" && child === firstH1) continue;
1836
+ convertBlockElement(child, fragment);
1837
+ hasContent = true;
1838
+ }
1839
+ if (!hasContent) {
1840
+ const paraEl = new yjs.XmlElement("paragraph");
1841
+ fragment.insert(fragment.length, [paraEl]);
1842
+ }
1843
+ });
1844
+ }
1845
+ /**
1846
+ * Appends blocks from an HTML string to an existing Y.XmlFragment.
1847
+ * Does NOT insert documentHeader/documentMeta — for appending to an existing doc.
1848
+ */
1849
+ function appendHtmlToFragment(fragment, html) {
1850
+ const ydoc = fragment.doc;
1851
+ if (!ydoc) {
1852
+ console.warn("[htmlToYjs] appendHtmlToFragment: fragment has no doc — skipping");
1853
+ return;
1854
+ }
1855
+ const doc = new DOMParser().parseFromString(html, "text/html");
1856
+ const blockTags = new Set([
1857
+ "p",
1858
+ "h1",
1859
+ "h2",
1860
+ "h3",
1861
+ "h4",
1862
+ "h5",
1863
+ "h6",
1864
+ "ul",
1865
+ "ol",
1866
+ "pre",
1867
+ "blockquote",
1868
+ "table",
1869
+ "hr",
1870
+ "div",
1871
+ "section",
1872
+ "article",
1873
+ "header",
1874
+ "footer",
1875
+ "main",
1876
+ "aside"
1877
+ ]);
1878
+ ydoc.transact(() => {
1879
+ for (const child of Array.from(doc.body.children)) if (blockTags.has(child.tagName.toLowerCase())) convertBlockElement(child, fragment);
1880
+ });
1881
+ }
1882
+
1883
+ //#endregion
1884
+ //#region packages/convert/src/diff.ts
1885
+ function attrsToJson(el) {
1886
+ const out = {};
1887
+ let count = 0;
1888
+ for (const key of Object.keys(el.getAttributes())) {
1889
+ out[key] = normaliseAttr(el.getAttribute(key));
1890
+ count++;
1891
+ }
1892
+ if (count === 0) return void 0;
1893
+ return sortKeys(out);
1894
+ }
1895
+ function normaliseAttr(v) {
1896
+ if (v == null) return null;
1897
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") return v;
1898
+ if (Array.isArray(v)) return v.map(normaliseAttr);
1899
+ if (typeof v === "object") {
1900
+ const out = {};
1901
+ for (const k of Object.keys(v).sort()) out[k] = normaliseAttr(v[k]);
1902
+ return out;
1903
+ }
1904
+ return null;
1905
+ }
1906
+ function sortKeys(obj) {
1907
+ const out = {};
1908
+ for (const k of Object.keys(obj).sort()) out[k] = obj[k];
1909
+ return out;
1910
+ }
1911
+ function textToJson(text) {
1912
+ const delta = text.toDelta();
1913
+ const runs = [];
1914
+ for (const op of delta) {
1915
+ if (typeof op.insert !== "string") continue;
1916
+ const run = { insert: op.insert };
1917
+ if (op.attributes && Object.keys(op.attributes).length > 0) {
1918
+ const normalised = {};
1919
+ for (const k of Object.keys(op.attributes).sort()) normalised[k] = normaliseAttr(op.attributes[k]);
1920
+ run.attributes = normalised;
1921
+ }
1922
+ runs.push(run);
1923
+ }
1924
+ return {
1925
+ kind: "text",
1926
+ runs
1927
+ };
1928
+ }
1929
+ function elementToJson(el) {
1930
+ const attrs = attrsToJson(el);
1931
+ const children = [];
1932
+ for (const child of el.toArray()) if (child instanceof yjs.XmlElement) children.push(elementToJson(child));
1933
+ else if (child instanceof yjs.XmlText) children.push(textToJson(child));
1934
+ const out = {
1935
+ kind: "element",
1936
+ tag: el.nodeName,
1937
+ children
1938
+ };
1939
+ if (attrs) out.attrs = attrs;
1940
+ return out;
1941
+ }
1942
+ function yfragmentToJson(frag) {
1943
+ const children = [];
1944
+ for (const child of frag.toArray()) if (child instanceof yjs.XmlElement) children.push(elementToJson(child));
1945
+ else if (child instanceof yjs.XmlText) children.push(textToJson(child));
1946
+ return {
1947
+ kind: "element",
1948
+ tag: "#fragment",
1949
+ children
1950
+ };
1951
+ }
1952
+ function populateElement(el, node) {
1953
+ if (node.attrs) for (const k of Object.keys(node.attrs)) {
1954
+ const v = node.attrs[k];
1955
+ el.setAttribute(k, v);
1956
+ }
1957
+ for (const child of node.children) if (child.kind === "element") {
1958
+ const sub = new yjs.XmlElement(child.tag);
1959
+ el.insert(el.length, [sub]);
1960
+ populateElement(sub, child);
1961
+ } else {
1962
+ const text = new yjs.XmlText();
1963
+ el.insert(el.length, [text]);
1964
+ populateText(text, child);
1965
+ }
1966
+ }
1967
+ function populateText(text, node) {
1968
+ let offset = 0;
1969
+ for (const run of node.runs) {
1970
+ if (run.attributes) text.insert(offset, run.insert, run.attributes);
1971
+ else text.insert(offset, run.insert);
1972
+ offset += run.insert.length;
1973
+ }
1974
+ }
1975
+ /**
1976
+ * Build a Y.XmlFragment from a previously captured golden JSON. The
1977
+ * returned fragment is bound to a fresh Y.Doc so consumers can hand
1978
+ * it to serialisers immediately. All Yjs mutations happen inside a
1979
+ * single transaction.
1980
+ */
1981
+ function jsonToYFragment(root, doc, key = "body") {
1982
+ if (root.tag !== "#fragment") throw new Error(`jsonToYFragment expected tag="#fragment", got "${root.tag}"`);
1983
+ const ydoc = doc ?? new yjs.Doc();
1984
+ const frag = ydoc.getXmlFragment(key);
1985
+ ydoc.transact(() => {
1986
+ for (const child of root.children) if (child.kind === "element") {
1987
+ const sub = new yjs.XmlElement(child.tag);
1988
+ frag.insert(frag.length, [sub]);
1989
+ populateElement(sub, child);
1990
+ } else {
1991
+ const text = new yjs.XmlText();
1992
+ frag.insert(frag.length, [text]);
1993
+ populateText(text, child);
1994
+ }
1995
+ });
1996
+ return frag;
1997
+ }
1998
+ function diffYjs(a, b) {
1999
+ return diffNode(yfragmentToJson(a), yfragmentToJson(b), "");
2000
+ }
2001
+ function diffJson(a, b) {
2002
+ return diffNode(a, b, "");
2003
+ }
2004
+ function diffNode(a, b, path) {
2005
+ if (a.kind !== b.kind) return {
2006
+ equal: false,
2007
+ path,
2008
+ reason: `kind mismatch: ${a.kind} vs ${b.kind}`
2009
+ };
2010
+ if (a.kind === "text" && b.kind === "text") {
2011
+ const ar = a.runs;
2012
+ const br = b.runs;
2013
+ if (ar.length !== br.length) return {
2014
+ equal: false,
2015
+ path,
2016
+ reason: `text run count: ${ar.length} vs ${br.length}`
2017
+ };
2018
+ for (let i = 0; i < ar.length; i++) {
2019
+ const sub = `${path}/runs[${i}]`;
2020
+ const ai = ar[i];
2021
+ const bi = br[i];
2022
+ if (ai.insert !== bi.insert) return {
2023
+ equal: false,
2024
+ path: sub,
2025
+ reason: `insert: ${JSON.stringify(ai.insert)} vs ${JSON.stringify(bi.insert)}`
2026
+ };
2027
+ const attrDiff = diffAttrs(ai.attributes, bi.attributes, sub);
2028
+ if (!attrDiff.equal) return attrDiff;
2029
+ }
2030
+ return {
2031
+ equal: true,
2032
+ path: "",
2033
+ reason: ""
2034
+ };
2035
+ }
2036
+ if (a.kind === "element" && b.kind === "element") {
2037
+ if (a.tag !== b.tag) return {
2038
+ equal: false,
2039
+ path,
2040
+ reason: `tag: ${a.tag} vs ${b.tag}`
2041
+ };
2042
+ const attrDiff = diffAttrs(a.attrs, b.attrs, path);
2043
+ if (!attrDiff.equal) return attrDiff;
2044
+ if (a.children.length !== b.children.length) return {
2045
+ equal: false,
2046
+ path,
2047
+ reason: `child count: ${a.children.length} vs ${b.children.length}`
2048
+ };
2049
+ for (let i = 0; i < a.children.length; i++) {
2050
+ const sub = path === "" ? `[${i}]` : `${path}[${i}]`;
2051
+ const childDiff = diffNode(a.children[i], b.children[i], sub);
2052
+ if (!childDiff.equal) return childDiff;
2053
+ }
2054
+ return {
2055
+ equal: true,
2056
+ path: "",
2057
+ reason: ""
2058
+ };
2059
+ }
2060
+ return {
2061
+ equal: false,
2062
+ path,
2063
+ reason: "unreachable kind combination"
2064
+ };
2065
+ }
2066
+ function diffAttrs(a, b, path) {
2067
+ const ak = a ? Object.keys(a).sort() : [];
2068
+ const bk = b ? Object.keys(b).sort() : [];
2069
+ if (ak.length !== bk.length || ak.some((k, i) => k !== bk[i])) return {
2070
+ equal: false,
2071
+ path: `${path}.attrs`,
2072
+ reason: `attr keys: [${ak.join(",")}] vs [${bk.join(",")}]`
2073
+ };
2074
+ for (const k of ak) {
2075
+ const av = a[k];
2076
+ const bv = b[k];
2077
+ if (!attrEqual(av, bv)) return {
2078
+ equal: false,
2079
+ path: `${path}.attrs.${k}`,
2080
+ reason: `attr value: ${JSON.stringify(av)} vs ${JSON.stringify(bv)}`
2081
+ };
2082
+ }
2083
+ return {
2084
+ equal: true,
2085
+ path: "",
2086
+ reason: ""
2087
+ };
2088
+ }
2089
+ function attrEqual(a, b) {
2090
+ if (a === b) return true;
2091
+ if (a === null || b === null) return false;
2092
+ if (typeof a !== typeof b) return false;
2093
+ if (Array.isArray(a) && Array.isArray(b)) {
2094
+ if (a.length !== b.length) return false;
2095
+ return a.every((v, i) => attrEqual(v, b[i]));
2096
+ }
2097
+ if (typeof a === "object" && typeof b === "object") {
2098
+ const ak = Object.keys(a).sort();
2099
+ const bk = Object.keys(b).sort();
2100
+ if (ak.length !== bk.length || ak.some((k, i) => k !== bk[i])) return false;
2101
+ return ak.every((k) => attrEqual(a[k], b[k]));
2102
+ }
2103
+ return false;
2104
+ }
2105
+ /** Stringify a YjsJsonNode with sorted keys for byte-stable golden files. */
2106
+ function stringifyJsonNode(node) {
2107
+ return JSON.stringify(node, null, 2) + "\n";
2108
+ }
2109
+
2110
+ //#endregion
2111
+ //#region packages/convert/src/spec/nodes.ts
2112
+ const BOOL_FALSE_DEFAULT = {
2113
+ key: "",
2114
+ type: "boolean",
2115
+ default: false,
2116
+ optional: true
2117
+ };
2118
+ const bool = (key) => ({
2119
+ ...BOOL_FALSE_DEFAULT,
2120
+ key
2121
+ });
2122
+ const str = (key, def) => ({
2123
+ key,
2124
+ type: "string",
2125
+ default: def,
2126
+ optional: true
2127
+ });
2128
+ const num = (key) => ({
2129
+ key,
2130
+ type: "number",
2131
+ optional: true
2132
+ });
2133
+ const int = (key) => ({
2134
+ key,
2135
+ type: "integer",
2136
+ optional: true
2137
+ });
2138
+ const VANILLA_BLOCKS = [
2139
+ {
2140
+ name: "documentHeader",
2141
+ group: "block",
2142
+ wire: "special",
2143
+ doc: "Holds the title; hoisted to frontmatter on serialise."
2144
+ },
2145
+ {
2146
+ name: "documentMeta",
2147
+ group: "block",
2148
+ wire: "special",
2149
+ doc: "Holds page-level meta; serialised into frontmatter."
2150
+ },
2151
+ {
2152
+ name: "paragraph",
2153
+ group: "block",
2154
+ wire: "vanilla",
2155
+ contentBearing: true
2156
+ },
2157
+ {
2158
+ name: "heading",
2159
+ group: "block",
2160
+ wire: "vanilla",
2161
+ attrs: [int("level")],
2162
+ contentBearing: true
2163
+ },
2164
+ {
2165
+ name: "blockquote",
2166
+ group: "block",
2167
+ wire: "vanilla",
2168
+ contentBearing: true
2169
+ },
2170
+ {
2171
+ name: "codeBlock",
2172
+ group: "block",
2173
+ wire: "fence",
2174
+ attrs: [str("language", "")]
2175
+ },
2176
+ {
2177
+ name: "bulletList",
2178
+ group: "block",
2179
+ wire: "vanilla"
2180
+ },
2181
+ {
2182
+ name: "orderedList",
2183
+ group: "block",
2184
+ wire: "vanilla"
2185
+ },
2186
+ {
2187
+ name: "listItem",
2188
+ group: "block",
2189
+ wire: "vanilla",
2190
+ contentBearing: true
2191
+ },
2192
+ {
2193
+ name: "taskList",
2194
+ group: "block",
2195
+ wire: "vanilla"
2196
+ },
2197
+ {
2198
+ name: "taskItem",
2199
+ group: "block",
2200
+ wire: "vanilla",
2201
+ attrs: [bool("checked")],
2202
+ contentBearing: true
2203
+ },
2204
+ {
2205
+ name: "table",
2206
+ group: "block",
2207
+ wire: "vanilla"
2208
+ },
2209
+ {
2210
+ name: "tableRow",
2211
+ group: "block",
2212
+ wire: "vanilla"
2213
+ },
2214
+ {
2215
+ name: "tableHeader",
2216
+ group: "block",
2217
+ wire: "vanilla",
2218
+ contentBearing: true
2219
+ },
2220
+ {
2221
+ name: "tableCell",
2222
+ group: "block",
2223
+ wire: "vanilla",
2224
+ contentBearing: true
2225
+ },
2226
+ {
2227
+ name: "horizontalRule",
2228
+ group: "block",
2229
+ wire: "vanilla"
2230
+ },
2231
+ {
2232
+ name: "image",
2233
+ group: "block",
2234
+ wire: "special",
2235
+ attrs: [
2236
+ str("src"),
2237
+ str("alt", ""),
2238
+ int("width"),
2239
+ int("height")
2240
+ ]
2241
+ },
2242
+ {
2243
+ name: "hardBreak",
2244
+ group: "inline",
2245
+ wire: "vanilla"
2246
+ }
2247
+ ];
2248
+ const MDC_CONTAINERS = [
2249
+ {
2250
+ name: "callout",
2251
+ group: "block",
2252
+ wire: "mdc-container",
2253
+ attrs: [
2254
+ {
2255
+ key: "type",
2256
+ type: "string",
2257
+ default: "note",
2258
+ optional: true,
2259
+ values: [
2260
+ "note",
2261
+ "tip",
2262
+ "warning",
2263
+ "danger",
2264
+ "info",
2265
+ "caution",
2266
+ "alert",
2267
+ "success",
2268
+ "error"
2269
+ ]
2270
+ },
2271
+ str("title"),
2272
+ str("icon")
2273
+ ]
2274
+ },
2275
+ {
2276
+ name: "collapsible",
2277
+ group: "block",
2278
+ wire: "mdc-container",
2279
+ attrs: [str("label", "Details"), bool("open")]
2280
+ },
2281
+ {
2282
+ name: "accordion",
2283
+ group: "block",
2284
+ wire: "mdc-slotted",
2285
+ slotChild: "accordionItem"
2286
+ },
2287
+ {
2288
+ name: "accordionItem",
2289
+ group: "block",
2290
+ wire: "mdc-container",
2291
+ mdcTag: "accordion-item",
2292
+ attrs: [str("label", "Item"), str("icon")]
2293
+ },
2294
+ {
2295
+ name: "tabs",
2296
+ group: "block",
2297
+ wire: "mdc-slotted",
2298
+ slotChild: "tabsItem"
2299
+ },
2300
+ {
2301
+ name: "tabsItem",
2302
+ group: "block",
2303
+ wire: "mdc-container",
2304
+ mdcTag: "tabs-item",
2305
+ attrs: [str("label"), str("icon")]
2306
+ },
2307
+ {
2308
+ name: "steps",
2309
+ group: "block",
2310
+ wire: "mdc-container"
2311
+ },
2312
+ {
2313
+ name: "card",
2314
+ group: "block",
2315
+ wire: "mdc-container",
2316
+ attrs: [
2317
+ str("title"),
2318
+ str("icon"),
2319
+ str("to")
2320
+ ]
2321
+ },
2322
+ {
2323
+ name: "cardGroup",
2324
+ group: "block",
2325
+ wire: "mdc-slotted",
2326
+ mdcTag: "card-group",
2327
+ slotChild: "card"
2328
+ },
2329
+ {
2330
+ name: "field",
2331
+ group: "block",
2332
+ wire: "mdc-container",
2333
+ attrs: [
2334
+ str("name"),
2335
+ str("type", "string"),
2336
+ bool("required")
2337
+ ]
2338
+ },
2339
+ {
2340
+ name: "fieldGroup",
2341
+ group: "block",
2342
+ wire: "mdc-slotted",
2343
+ mdcTag: "field-group",
2344
+ slotChild: "field"
2345
+ },
2346
+ {
2347
+ name: "codeGroup",
2348
+ group: "block",
2349
+ wire: "mdc-slotted",
2350
+ mdcTag: "code-group",
2351
+ slotChild: "codeBlock"
2352
+ },
2353
+ {
2354
+ name: "codeCollapse",
2355
+ group: "block",
2356
+ wire: "mdc-container",
2357
+ mdcTag: "code-collapse"
2358
+ },
2359
+ {
2360
+ name: "codePreview",
2361
+ group: "block",
2362
+ wire: "mdc-container",
2363
+ mdcTag: "code-preview"
2364
+ },
2365
+ {
2366
+ name: "codeTree",
2367
+ group: "block",
2368
+ wire: "mdc-atom-block",
2369
+ mdcTag: "code-tree",
2370
+ attrs: [{
2371
+ key: "files",
2372
+ type: "json"
2373
+ }]
2374
+ },
2375
+ {
2376
+ name: "figure",
2377
+ group: "block",
2378
+ wire: "mdc-container",
2379
+ attrs: [
2380
+ str("src"),
2381
+ str("alt", ""),
2382
+ str("caption")
2383
+ ]
2384
+ },
2385
+ {
2386
+ name: "video",
2387
+ group: "block",
2388
+ wire: "mdc-atom-block",
2389
+ attrs: [
2390
+ str("src"),
2391
+ str("poster"),
2392
+ bool("autoplay"),
2393
+ bool("loop"),
2394
+ bool("controls")
2395
+ ]
2396
+ },
2397
+ {
2398
+ name: "embed",
2399
+ group: "block",
2400
+ wire: "mdc-atom-block",
2401
+ attrs: [str("src"), str("title")]
2402
+ },
2403
+ {
2404
+ name: "svgEmbed",
2405
+ group: "block",
2406
+ wire: "fence",
2407
+ attrs: [str("title")],
2408
+ mdcTag: "svg",
2409
+ doc: "Serialised as a ```svg fenced block; the SVG markup is the body."
2410
+ },
2411
+ {
2412
+ name: "divider",
2413
+ group: "block",
2414
+ wire: "mdc-atom-block",
2415
+ attrs: [str("label"), str("icon")]
2416
+ },
2417
+ {
2418
+ name: "quote",
2419
+ group: "block",
2420
+ wire: "mdc-container",
2421
+ attrs: [str("cite")]
2422
+ },
2423
+ {
2424
+ name: "progress",
2425
+ group: "block",
2426
+ wire: "mdc-atom-block",
2427
+ attrs: [
2428
+ num("value"),
2429
+ num("max"),
2430
+ str("label")
2431
+ ]
2432
+ },
2433
+ {
2434
+ name: "spoiler",
2435
+ group: "block",
2436
+ wire: "mdc-container",
2437
+ attrs: [str("label")]
2438
+ },
2439
+ {
2440
+ name: "colorSwatch",
2441
+ group: "block",
2442
+ wire: "mdc-atom-block",
2443
+ mdcTag: "color-swatch",
2444
+ attrs: [str("color"), str("label")]
2445
+ },
2446
+ {
2447
+ name: "stat",
2448
+ group: "block",
2449
+ wire: "mdc-container",
2450
+ attrs: [
2451
+ str("label"),
2452
+ str("value"),
2453
+ str("icon")
2454
+ ]
2455
+ },
2456
+ {
2457
+ name: "statGroup",
2458
+ group: "block",
2459
+ wire: "mdc-slotted",
2460
+ mdcTag: "stat-group",
2461
+ slotChild: "stat"
2462
+ },
2463
+ {
2464
+ name: "button",
2465
+ group: "block",
2466
+ wire: "mdc-atom-block",
2467
+ attrs: [
2468
+ str("label"),
2469
+ str("to"),
2470
+ str("icon"),
2471
+ str("variant")
2472
+ ]
2473
+ },
2474
+ {
2475
+ name: "buttonGroup",
2476
+ group: "block",
2477
+ wire: "mdc-slotted",
2478
+ mdcTag: "button-group",
2479
+ slotChild: "button"
2480
+ },
2481
+ {
2482
+ name: "timeline",
2483
+ group: "block",
2484
+ wire: "mdc-slotted",
2485
+ slotChild: "timelineItem"
2486
+ },
2487
+ {
2488
+ name: "timelineItem",
2489
+ group: "block",
2490
+ wire: "mdc-container",
2491
+ mdcTag: "timeline-item",
2492
+ attrs: [
2493
+ str("label"),
2494
+ str("icon"),
2495
+ str("date")
2496
+ ]
2497
+ },
2498
+ {
2499
+ name: "diff",
2500
+ group: "block",
2501
+ wire: "mdc-atom-block",
2502
+ attrs: [str("language", ""), {
2503
+ key: "value",
2504
+ type: "string"
2505
+ }]
2506
+ }
2507
+ ];
2508
+ const INLINE_AND_SPECIAL = [
2509
+ {
2510
+ name: "docLink",
2511
+ group: "inline",
2512
+ wire: "special",
2513
+ attrs: [str("docId")],
2514
+ doc: "Wire form `[[uuid|label]]`; label regenerated on export."
2515
+ },
2516
+ {
2517
+ name: "docEmbed",
2518
+ group: "block",
2519
+ wire: "special",
2520
+ attrs: [
2521
+ str("docId"),
2522
+ bool("collapsed"),
2523
+ bool("tall"),
2524
+ bool("seamless")
2525
+ ],
2526
+ doc: "Wire form `![[uuid|label]]{collapsed tall seamless}`."
2527
+ },
2528
+ {
2529
+ name: "mention",
2530
+ group: "inline",
2531
+ wire: "special",
2532
+ attrs: [str("userId")],
2533
+ doc: "Wire form `@[label](user:uuid)`; label regenerated on export."
2534
+ },
2535
+ {
2536
+ name: "mathInline",
2537
+ group: "inline",
2538
+ wire: "special",
2539
+ attrs: [{
2540
+ key: "expression",
2541
+ type: "string"
2542
+ }],
2543
+ doc: "Wire form `$expression$`."
2544
+ },
2545
+ {
2546
+ name: "mathBlock",
2547
+ group: "block",
2548
+ wire: "fence",
2549
+ attrs: [{
2550
+ key: "expression",
2551
+ type: "string"
2552
+ }],
2553
+ mdcTag: "math",
2554
+ doc: "Wire form ``` ```math\\nexpression\\n``` ```."
2555
+ },
2556
+ {
2557
+ name: "fileBlock",
2558
+ group: "block",
2559
+ wire: "mdc-atom-block",
2560
+ mdcTag: "file",
2561
+ attrs: [
2562
+ str("src"),
2563
+ str("mime"),
2564
+ str("uploadId"),
2565
+ str("filename")
2566
+ ],
2567
+ doc: "Wire form `:file{src=… mime=… upload-id=… filename=…}`; binary in sidecar."
2568
+ },
2569
+ {
2570
+ name: "badge",
2571
+ group: "inline",
2572
+ wire: "mdc-atom-inl",
2573
+ attrs: [
2574
+ str("label"),
2575
+ str("color"),
2576
+ str("variant", "subtle")
2577
+ ],
2578
+ doc: "Wire form `:badge[Label]{color=… variant=…}`."
2579
+ },
2580
+ {
2581
+ name: "proseIcon",
2582
+ group: "inline",
2583
+ wire: "mdc-atom-inl",
2584
+ mdcTag: "icon",
2585
+ attrs: [str("name")],
2586
+ doc: "Wire form `:icon{name=…}`."
2587
+ },
2588
+ {
2589
+ name: "kbd",
2590
+ group: "inline",
2591
+ wire: "mdc-atom-inl",
2592
+ attrs: [str("value")],
2593
+ doc: "Wire form `:kbd{value=…}`."
2594
+ }
2595
+ ];
2596
+ const NODE_SPECS = [
2597
+ ...VANILLA_BLOCKS,
2598
+ ...MDC_CONTAINERS,
2599
+ ...INLINE_AND_SPECIAL
2600
+ ];
2601
+ const NODE_SPEC_BY_NAME = new Map(NODE_SPECS.map((spec) => [spec.name, spec]));
2602
+ /** Derive the MDC tag for a node — `mdcTag` override or kebab-case of `name`. */
2603
+ function mdcTagOf(spec) {
2604
+ if (spec.mdcTag) return spec.mdcTag;
2605
+ return spec.name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
2606
+ }
2607
+
2608
+ //#endregion
2609
+ //#region packages/convert/src/spec/marks.ts
2610
+ const MARK_SPECS = [
2611
+ {
2612
+ name: "bold",
2613
+ wire: "delimited",
2614
+ delim: "**"
2615
+ },
2616
+ {
2617
+ name: "italic",
2618
+ wire: "delimited",
2619
+ delim: "*"
2620
+ },
2621
+ {
2622
+ name: "strike",
2623
+ wire: "delimited",
2624
+ delim: "~~"
2625
+ },
2626
+ {
2627
+ name: "code",
2628
+ wire: "delimited",
2629
+ delim: "`"
2630
+ },
2631
+ {
2632
+ name: "link",
2633
+ wire: "link",
2634
+ attrs: [{
2635
+ key: "href",
2636
+ type: "string"
2637
+ }, {
2638
+ key: "title",
2639
+ type: "string",
2640
+ optional: true
2641
+ }]
2642
+ },
2643
+ {
2644
+ name: "underline",
2645
+ wire: "delimited",
2646
+ delim: "__",
2647
+ doc: "Disambiguated from bold by delimiter character. Two underscores = underline; two asterisks = bold."
2648
+ },
2649
+ {
2650
+ name: "highlight",
2651
+ wire: "delimited",
2652
+ delim: "==",
2653
+ doc: "Pandoc-style."
2654
+ },
2655
+ {
2656
+ name: "subscript",
2657
+ wire: "delimited",
2658
+ delim: "~",
2659
+ doc: "Single tilde; double tilde is strike."
2660
+ },
2661
+ {
2662
+ name: "superscript",
2663
+ wire: "delimited",
2664
+ delim: "^"
2665
+ },
2666
+ {
2667
+ name: "textStyle",
2668
+ wire: "mdc-span",
2669
+ attrs: [
2670
+ {
2671
+ key: "color",
2672
+ type: "string",
2673
+ optional: true
2674
+ },
2675
+ {
2676
+ key: "backgroundColor",
2677
+ type: "string",
2678
+ optional: true
2679
+ },
2680
+ {
2681
+ key: "fontSize",
2682
+ type: "string",
2683
+ optional: true
2684
+ },
2685
+ {
2686
+ key: "fontFamily",
2687
+ type: "string",
2688
+ optional: true
2689
+ }
2690
+ ],
2691
+ doc: "Wire form `:span[text]{color=\"…\" font-size=\"…\"}`. Any of the attrs may be set."
2692
+ }
2693
+ ];
2694
+ const MARK_SPEC_BY_NAME = new Map(MARK_SPECS.map((spec) => [spec.name, spec]));
2695
+ const MARK_SPEC_BY_DELIM = new Map(MARK_SPECS.filter((spec) => spec.wire === "delimited" && !!spec.delim).map((spec) => [spec.delim, spec]));
2696
+
2697
+ //#endregion
2698
+ //#region packages/convert/src/spec/universal-meta.ts
2699
+ const UNIVERSAL_META_KEYS = [
2700
+ {
2701
+ key: "title",
2702
+ type: "string",
2703
+ doc: "Display title; the first H1 is hoisted into this field on import."
2704
+ },
2705
+ {
2706
+ key: "type",
2707
+ type: "string",
2708
+ doc: "Page type (doc, kanban, table, …). Omitted on serialise when \"doc\"."
2709
+ },
2710
+ {
2711
+ key: "color",
2712
+ type: "string",
2713
+ doc: "Hex or CSS color name."
2714
+ },
2715
+ {
2716
+ key: "icon",
2717
+ type: "string",
2718
+ doc: "Lucide icon name in kebab-case."
2719
+ },
2720
+ {
2721
+ key: "datetimeStart",
2722
+ type: "iso-datetime"
2723
+ },
2724
+ {
2725
+ key: "datetimeEnd",
2726
+ type: "iso-datetime"
2727
+ },
2728
+ {
2729
+ key: "allDay",
2730
+ type: "boolean"
2731
+ },
2732
+ {
2733
+ key: "dateTaken",
2734
+ type: "iso-datetime"
2735
+ },
2736
+ {
2737
+ key: "dateStart",
2738
+ type: "iso-date",
2739
+ parseAliases: ["date", "created"]
2740
+ },
2741
+ {
2742
+ key: "dateEnd",
2743
+ type: "iso-date",
2744
+ parseAliases: ["due"]
2745
+ },
2746
+ {
2747
+ key: "timeStart",
2748
+ type: "hh-mm"
2749
+ },
2750
+ {
2751
+ key: "timeEnd",
2752
+ type: "hh-mm"
2753
+ },
2754
+ {
2755
+ key: "tags",
2756
+ type: "string[]"
2757
+ },
2758
+ {
2759
+ key: "checked",
2760
+ type: "boolean",
2761
+ parseAliases: ["done"]
2762
+ },
2763
+ {
2764
+ key: "priority",
2765
+ type: "integer",
2766
+ min: 0,
2767
+ max: 4,
2768
+ doc: "Numeric or named (low/medium/high/urgent → 1/2/3/4)."
2769
+ },
2770
+ {
2771
+ key: "status",
2772
+ type: "string"
2773
+ },
2774
+ {
2775
+ key: "rating",
2776
+ type: "number",
2777
+ min: 0,
2778
+ max: 5
2779
+ },
2780
+ {
2781
+ key: "url",
2782
+ type: "string"
2783
+ },
2784
+ {
2785
+ key: "email",
2786
+ type: "string"
2787
+ },
2788
+ {
2789
+ key: "phone",
2790
+ type: "string"
2791
+ },
2792
+ {
2793
+ key: "number",
2794
+ type: "number"
2795
+ },
2796
+ {
2797
+ key: "unit",
2798
+ type: "string"
2799
+ },
2800
+ {
2801
+ key: "subtitle",
2802
+ type: "string",
2803
+ parseAliases: ["description"]
2804
+ },
2805
+ {
2806
+ key: "note",
2807
+ type: "string"
2808
+ },
2809
+ {
2810
+ key: "taskProgress",
2811
+ type: "integer",
2812
+ min: 0,
2813
+ max: 100
2814
+ },
2815
+ {
2816
+ key: "members",
2817
+ type: "members"
2818
+ },
2819
+ {
2820
+ key: "coverUploadId",
2821
+ type: "string"
2822
+ },
2823
+ {
2824
+ key: "coverDocId",
2825
+ type: "string"
2826
+ },
2827
+ {
2828
+ key: "coverMimeType",
2829
+ type: "string"
2830
+ },
2831
+ {
2832
+ key: "geoType",
2833
+ type: "string-enum",
2834
+ values: [
2835
+ "marker",
2836
+ "line",
2837
+ "measure"
2838
+ ]
2839
+ },
2840
+ {
2841
+ key: "geoLat",
2842
+ type: "number"
2843
+ },
2844
+ {
2845
+ key: "geoLng",
2846
+ type: "number"
2847
+ },
2848
+ {
2849
+ key: "geoDescription",
2850
+ type: "string"
2851
+ },
2852
+ {
2853
+ key: "deskX",
2854
+ type: "number"
2855
+ },
2856
+ {
2857
+ key: "deskY",
2858
+ type: "number"
2859
+ },
2860
+ {
2861
+ key: "deskZ",
2862
+ type: "number"
2863
+ },
2864
+ {
2865
+ key: "deskMode",
2866
+ type: "string-enum",
2867
+ values: [
2868
+ "icon",
2869
+ "widget-sm",
2870
+ "widget-lg"
2871
+ ]
2872
+ },
2873
+ {
2874
+ key: "mmX",
2875
+ type: "number"
2876
+ },
2877
+ {
2878
+ key: "mmY",
2879
+ type: "number"
2880
+ },
2881
+ {
2882
+ key: "graphX",
2883
+ type: "number"
2884
+ },
2885
+ {
2886
+ key: "graphY",
2887
+ type: "number"
2888
+ },
2889
+ {
2890
+ key: "graphPinned",
2891
+ type: "boolean"
2892
+ },
2893
+ {
2894
+ key: "spX",
2895
+ type: "number"
2896
+ },
2897
+ {
2898
+ key: "spY",
2899
+ type: "number"
2900
+ },
2901
+ {
2902
+ key: "spZ",
2903
+ type: "number"
2904
+ },
2905
+ {
2906
+ key: "spRX",
2907
+ type: "number"
2908
+ },
2909
+ {
2910
+ key: "spRY",
2911
+ type: "number"
2912
+ },
2913
+ {
2914
+ key: "spRZ",
2915
+ type: "number"
2916
+ },
2917
+ {
2918
+ key: "spSX",
2919
+ type: "number"
2920
+ },
2921
+ {
2922
+ key: "spSY",
2923
+ type: "number"
2924
+ },
2925
+ {
2926
+ key: "spSZ",
2927
+ type: "number"
2928
+ },
2929
+ {
2930
+ key: "spShape",
2931
+ type: "string-enum",
2932
+ values: [
2933
+ "box",
2934
+ "sphere",
2935
+ "cylinder",
2936
+ "cone",
2937
+ "plane",
2938
+ "torus",
2939
+ "glb"
2940
+ ]
2941
+ },
2942
+ {
2943
+ key: "spOpacity",
2944
+ type: "integer",
2945
+ min: 0,
2946
+ max: 100
2947
+ },
2948
+ {
2949
+ key: "spModelUploadId",
2950
+ type: "string"
2951
+ },
2952
+ {
2953
+ key: "spModelDocId",
2954
+ type: "string"
2955
+ },
2956
+ {
2957
+ key: "slidesTransition",
2958
+ type: "string-enum",
2959
+ values: [
2960
+ "none",
2961
+ "fade",
2962
+ "slide"
2963
+ ]
2964
+ },
2965
+ {
2966
+ key: "slidesTheme",
2967
+ type: "string-enum",
2968
+ values: ["dark", "light"]
2969
+ },
2970
+ {
2971
+ key: "__schemaVersion",
2972
+ type: "integer",
2973
+ min: 0
2974
+ }
2975
+ ];
2976
+ const UNIVERSAL_META_KEY_NAMES = new Set(UNIVERSAL_META_KEYS.map((k) => k.key));
2977
+ /**
2978
+ * Build a map of every recognised input key (canonical + aliases) to
2979
+ * its canonical key. Used by the frontmatter parser.
2980
+ */
2981
+ function buildAliasMap() {
2982
+ const map = /* @__PURE__ */ new Map();
2983
+ for (const entry of UNIVERSAL_META_KEYS) {
2984
+ map.set(entry.key, entry.key);
2985
+ for (const alias of entry.parseAliases ?? []) map.set(alias, entry.key);
2986
+ }
2987
+ return map;
2988
+ }
2989
+
2990
+ //#endregion
2991
+ //#region packages/convert/src/file-blocks/manifest.ts
2992
+ let _adapter = null;
2993
+ /** Install a filesystem adapter. Call this once at boot. */
2994
+ function setFsAdapter(adapter) {
2995
+ _adapter = adapter;
2996
+ }
2997
+ /** Read back the active adapter — useful for tests and for layered code. */
2998
+ function getFsAdapter() {
2999
+ return _adapter;
3000
+ }
3001
+ async function loadFsApi() {
3002
+ if (_adapter) return _adapter;
3003
+ throw new Error("@abraca/convert: no FsAdapter installed. Call setFsAdapter({readTextFile, writeTextFile, mkdir, …}) at boot. Tauri hosts can pass `@tauri-apps/plugin-fs` directly; Node hosts can pass a wrapper over `fs/promises`; tests can use the in-memory adapter from tests/file-blocks.test.ts as a template.");
3004
+ }
3005
+ const MANIFEST_DIR = ".abracadabra";
3006
+ const MANIFEST_FILE = "manifest.json";
3007
+ function manifestPath(syncDir) {
3008
+ return `${syncDir}/${MANIFEST_DIR}/${MANIFEST_FILE}`;
3009
+ }
3010
+ function manifestDir(syncDir) {
3011
+ return `${syncDir}/${MANIFEST_DIR}`;
3012
+ }
3013
+ function trashDir(syncDir) {
3014
+ return `${syncDir}/${MANIFEST_DIR}/trash`;
3015
+ }
3016
+ function orphansDir(syncDir) {
3017
+ return `${syncDir}/${MANIFEST_DIR}/orphans`;
3018
+ }
3019
+ function conflictsDir(syncDir) {
3020
+ return `${syncDir}/${MANIFEST_DIR}/conflicts`;
3021
+ }
3022
+ function createEmptyManifest(spaceId) {
3023
+ return {
3024
+ version: 2,
3025
+ spaceId,
3026
+ lastSyncAt: 0,
3027
+ entries: {}
3028
+ };
3029
+ }
3030
+ async function loadManifest(syncDir, spaceId) {
3031
+ const fs = await loadFsApi();
3032
+ try {
3033
+ const raw = await fs.readTextFile(manifestPath(syncDir));
3034
+ const parsed = JSON.parse(raw);
3035
+ if (parsed.version === 2) return parsed;
3036
+ } catch {}
3037
+ return createEmptyManifest(spaceId);
3038
+ }
3039
+ async function saveManifest(syncDir, manifest) {
3040
+ const fs = await loadFsApi();
3041
+ const dir = `${syncDir}/${MANIFEST_DIR}`;
3042
+ try {
3043
+ await fs.mkdir(dir, { recursive: true });
3044
+ } catch {}
3045
+ manifest.lastSyncAt = Date.now();
3046
+ await fs.writeTextFile(manifestPath(syncDir), JSON.stringify(manifest, null, 2));
3047
+ }
3048
+ function lookupByDocId(manifest, docId) {
3049
+ return manifest.entries[docId];
3050
+ }
3051
+ function lookupByPath(manifest, relativePath) {
3052
+ for (const entry of Object.values(manifest.entries)) if (entry.relativePath === relativePath) return entry;
3053
+ }
3054
+ function lookupByHash(manifest, contentHash) {
3055
+ for (const entry of Object.values(manifest.entries)) if (entry.contentHash === contentHash) return entry;
3056
+ }
3057
+ function setEntry(manifest, entry) {
3058
+ manifest.entries[entry.docId] = entry;
3059
+ }
3060
+ function removeEntry(manifest, docId) {
3061
+ const entry = manifest.entries[docId];
3062
+ if (entry) delete manifest.entries[docId];
3063
+ return entry;
3064
+ }
3065
+ function buildReverseLookup(manifest) {
3066
+ const map = /* @__PURE__ */ new Map();
3067
+ for (const [docId, entry] of Object.entries(manifest.entries)) map.set(entry.relativePath, docId);
3068
+ return map;
3069
+ }
3070
+
3071
+ //#endregion
3072
+ //#region packages/convert/src/file-blocks/paths.ts
3073
+ /**
3074
+ * Convert a document label to a filesystem-safe filename (without extension).
3075
+ * e.g. "My Project!" -> "my-project"
3076
+ */
3077
+ function labelToFilename(label) {
3078
+ return label.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "untitled";
3079
+ }
3080
+ /**
3081
+ * Convert a filename back to a label (best-effort).
3082
+ * e.g. "my-project" -> "my project", "my-project~a3f2" -> "my project"
3083
+ */
3084
+ function fsFilenameToLabel(filename) {
3085
+ return filename.replace(/~[a-z0-9]{4}$/, "").replace(/-/g, " ");
3086
+ }
3087
+ /**
3088
+ * Check if a doc has children in the tree (needs _index.md convention).
3089
+ */
3090
+ function hasChildren(docId, treeData) {
3091
+ for (const entry of Object.values(treeData)) if (entry.parentId === docId) return true;
3092
+ return false;
3093
+ }
3094
+ /**
3095
+ * Resolve filename collisions by appending ~XXXX (first 4 chars of docId).
3096
+ * Returns the filename (without extension) that should be used.
3097
+ */
3098
+ function resolveCollision(desiredFilename, docId, parentPath, manifest, isIndex) {
3099
+ const ext = ".md";
3100
+ const desiredRelative = isIndex ? `${parentPath}${parentPath ? "/" : ""}${desiredFilename}/_index${ext}` : `${parentPath}${parentPath ? "/" : ""}${desiredFilename}${ext}`;
3101
+ for (const [entryDocId, entry] of Object.entries(manifest.entries)) {
3102
+ if (entryDocId === docId) continue;
3103
+ if (entry.relativePath === desiredRelative) return `${desiredFilename}~${docId.substring(0, 4)}`;
3104
+ }
3105
+ return desiredFilename;
3106
+ }
3107
+ /**
3108
+ * Build the relative path for a document (from syncDir root).
3109
+ * Handles:
3110
+ * - Ancestor chain walking
3111
+ * - _index.md for docs with children
3112
+ * - Collision resolution via ~XXXX suffix
3113
+ */
3114
+ function buildRelativePath(docId, treeData, manifest) {
3115
+ const entry = treeData[docId];
3116
+ if (!entry) return `${docId}.md`;
3117
+ const segments = [];
3118
+ let current = entry;
3119
+ let currentId = docId;
3120
+ const visited = /* @__PURE__ */ new Set();
3121
+ while (current) {
3122
+ if (visited.has(currentId)) break;
3123
+ visited.add(currentId);
3124
+ segments.unshift(labelToFilename(current.label));
3125
+ if (!current.parentId) break;
3126
+ currentId = current.parentId;
3127
+ current = treeData[currentId];
3128
+ }
3129
+ const filename = segments.pop();
3130
+ const parentPath = segments.join("/");
3131
+ const isIndex = hasChildren(docId, treeData);
3132
+ const resolvedFilename = resolveCollision(filename, docId, parentPath, manifest, isIndex);
3133
+ if (isIndex) return `${parentPath}${parentPath ? "/" : ""}${resolvedFilename}/_index.md`;
3134
+ return `${parentPath}${parentPath ? "/" : ""}${resolvedFilename}.md`;
3135
+ }
3136
+ /**
3137
+ * Get the directory portion of a relative path for a doc.
3138
+ * For _index.md docs: returns the directory containing _index.md
3139
+ * For leaf docs: returns the parent directory
3140
+ */
3141
+ function getDocDir(relativePath) {
3142
+ if (relativePath.endsWith("/_index.md")) return relativePath.replace("/_index.md", "");
3143
+ const lastSlash = relativePath.lastIndexOf("/");
3144
+ return lastSlash >= 0 ? relativePath.substring(0, lastSlash) : "";
3145
+ }
3146
+ /**
3147
+ * Determine the parent docId from a filesystem relative path by walking the
3148
+ * path segments and matching against the tree.
3149
+ */
3150
+ function resolveParentFromPath(relativePath, treeData) {
3151
+ const parts = relativePath.split("/");
3152
+ parts.pop();
3153
+ if (relativePath.endsWith("/_index.md") && parts.length > 0) parts.pop();
3154
+ if (parts.length === 0) return null;
3155
+ let parentId = null;
3156
+ for (const segment of parts) {
3157
+ const found = Object.entries(treeData).find(([, e]) => labelToFilename(e.label) === segment && e.parentId === parentId);
3158
+ if (found) parentId = found[0];
3159
+ else break;
3160
+ }
3161
+ return parentId;
3162
+ }
3163
+ /**
3164
+ * Simple string hash (same as current useFsSync).
3165
+ */
3166
+ function simpleHash(str) {
3167
+ let hash = 0;
3168
+ for (let i = 0; i < str.length; i++) hash = (hash << 5) - hash + str.charCodeAt(i) | 0;
3169
+ return hash.toString(36);
3170
+ }
3171
+ /**
3172
+ * Get all tree data as a flat record.
3173
+ */
3174
+ function getTreeData(treeMap) {
3175
+ const data = {};
3176
+ treeMap.forEach((val, key) => {
3177
+ if (val && typeof val === "object") data[key] = val;
3178
+ });
3179
+ return data;
3180
+ }
3181
+ /**
3182
+ * Find the next order value for a given parent (max sibling order + 1).
3183
+ */
3184
+ function nextOrder(treeData, parentId) {
3185
+ let max = -1;
3186
+ for (const entry of Object.values(treeData)) if (entry.parentId === parentId && entry.order > max) max = entry.order;
3187
+ return max + 1;
3188
+ }
3189
+
3190
+ //#endregion
3191
+ exports.MARK_SPECS = MARK_SPECS;
3192
+ exports.MARK_SPEC_BY_DELIM = MARK_SPEC_BY_DELIM;
3193
+ exports.MARK_SPEC_BY_NAME = MARK_SPEC_BY_NAME;
3194
+ exports.NODE_SPECS = NODE_SPECS;
3195
+ exports.NODE_SPEC_BY_NAME = NODE_SPEC_BY_NAME;
3196
+ exports.UNIVERSAL_META_KEYS = UNIVERSAL_META_KEYS;
3197
+ exports.UNIVERSAL_META_KEY_NAMES = UNIVERSAL_META_KEY_NAMES;
3198
+ exports.appendHtmlToFragment = appendHtmlToFragment;
3199
+ exports.buildAliasMap = buildAliasMap;
3200
+ exports.buildRelativePath = buildRelativePath;
3201
+ exports.buildReverseLookup = buildReverseLookup;
3202
+ exports.conflictsDir = conflictsDir;
3203
+ exports.createEmptyManifest = createEmptyManifest;
3204
+ exports.diffJson = diffJson;
3205
+ exports.diffYjs = diffYjs;
3206
+ exports.filenameToLabel = filenameToLabel;
3207
+ exports.fsFilenameToLabel = fsFilenameToLabel;
3208
+ exports.getDocDir = getDocDir;
3209
+ exports.getFsAdapter = getFsAdapter;
3210
+ exports.getTreeData = getTreeData;
3211
+ exports.hasChildren = hasChildren;
3212
+ exports.jsonToYFragment = jsonToYFragment;
3213
+ exports.labelToFilename = labelToFilename;
3214
+ exports.loadManifest = loadManifest;
3215
+ exports.lookupByDocId = lookupByDocId;
3216
+ exports.lookupByHash = lookupByHash;
3217
+ exports.lookupByPath = lookupByPath;
3218
+ exports.manifestDir = manifestDir;
3219
+ exports.mdcTagOf = mdcTagOf;
3220
+ exports.nextOrder = nextOrder;
3221
+ exports.orphansDir = orphansDir;
3222
+ exports.parseFrontmatter = parseFrontmatter;
3223
+ exports.populateYDocFromHtml = populateYDocFromHtml;
3224
+ exports.populateYDocFromMarkdown = populateYDocFromMarkdown;
3225
+ exports.removeEntry = removeEntry;
3226
+ exports.resolveParentFromPath = resolveParentFromPath;
3227
+ exports.saveManifest = saveManifest;
3228
+ exports.setEntry = setEntry;
3229
+ exports.setFsAdapter = setFsAdapter;
3230
+ exports.simpleHash = simpleHash;
3231
+ exports.stringifyJsonNode = stringifyJsonNode;
3232
+ exports.trashDir = trashDir;
3233
+ exports.yfragmentToJson = yfragmentToJson;
3234
+ exports.yjsToHtml = yjsToHtml;
3235
+ exports.yjsToMarkdown = yjsToMarkdown;
3236
+ exports.yjsToPlainText = yjsToPlainText;
3237
+ //# sourceMappingURL=abracadabra-convert.cjs.map