@abraca/cli 2.22.0 → 2.24.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.
@@ -1318,2351 +1318,2506 @@ registerCommand({
1318
1318
  });
1319
1319
 
1320
1320
  //#endregion
1321
- //#region packages/convert/src/markdown-to-yjs.ts
1322
- function parseInlineArray(raw) {
1323
- return raw.slice(1, -1).split(",").map((s) => s.trim()).filter(Boolean);
1324
- }
1325
- function stripQuotes(s) {
1326
- if (s.length >= 2 && (s.startsWith("\"") && s.endsWith("\"") || s.startsWith("'") && s.endsWith("'"))) return s.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
1327
- return s;
1328
- }
1329
- function parseFrontmatter(markdown) {
1330
- const noResult = {
1331
- meta: {},
1332
- body: markdown
1333
- };
1334
- const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
1335
- if (!match) return noResult;
1336
- const yamlBlock = match[1];
1337
- const body = markdown.slice(match[0].length);
1338
- const raw = {};
1339
- const lines = yamlBlock.split("\n");
1340
- let i = 0;
1341
- while (i < lines.length) {
1342
- const line = lines[i];
1343
- const blockSeqKey = line.match(/^(\w[\w-]*):\s*$/);
1344
- if (blockSeqKey && i + 1 < lines.length && /^\s+-\s/.test(lines[i + 1])) {
1345
- const key = blockSeqKey[1];
1346
- const items = [];
1347
- i++;
1348
- while (i < lines.length && /^\s+-\s/.test(lines[i])) {
1349
- items.push(lines[i].replace(/^\s+-\s/, "").trim());
1350
- i++;
1351
- }
1352
- raw[key] = items;
1353
- continue;
1354
- }
1355
- const kvMatch = line.match(/^(\w[\w-]*):\s*(.*)$/);
1356
- if (kvMatch) {
1357
- const key = kvMatch[1];
1358
- const val = kvMatch[2].trim();
1359
- if (val.startsWith("[") && val.endsWith("]")) raw[key] = parseInlineArray(val).map(stripQuotes);
1360
- else raw[key] = stripQuotes(val);
1361
- }
1362
- i++;
1363
- }
1364
- const meta = {};
1365
- const getStr = (keys) => {
1366
- for (const k of keys) {
1367
- const v = raw[k];
1368
- if (typeof v === "string" && v) return v;
1369
- }
1370
- };
1371
- if (raw["tags"]) meta.tags = Array.isArray(raw["tags"]) ? raw["tags"] : [raw["tags"]];
1372
- const color = getStr(["color"]);
1373
- if (color) meta.color = color;
1374
- const icon = getStr(["icon"]);
1375
- if (icon) meta.icon = icon;
1376
- const status = getStr(["status"]);
1377
- if (status) meta.status = status;
1378
- const priorityRaw = getStr(["priority"]);
1379
- if (priorityRaw !== void 0) meta.priority = {
1380
- low: 1,
1381
- medium: 2,
1382
- high: 3,
1383
- urgent: 4
1384
- }[priorityRaw.toLowerCase()] ?? (Number(priorityRaw) || 0);
1385
- const checkedRaw = raw["checked"] ?? raw["done"];
1386
- if (checkedRaw !== void 0) meta.checked = checkedRaw === "true" || checkedRaw === true;
1387
- const dateStart = getStr([
1388
- "dateStart",
1389
- "date",
1390
- "created"
1391
- ]);
1392
- if (dateStart) meta.dateStart = dateStart;
1393
- const dateEnd = getStr(["dateEnd", "due"]);
1394
- if (dateEnd) meta.dateEnd = dateEnd;
1395
- const subtitle = getStr(["subtitle", "description"]);
1396
- if (subtitle) meta.subtitle = subtitle;
1397
- const url = getStr(["url"]);
1398
- if (url) meta.url = url;
1399
- const language = getStr(["language"]);
1400
- if (language) meta.language = language;
1401
- const fileExtension = getStr(["fileExtension"]);
1402
- if (fileExtension) meta.fileExtension = fileExtension;
1403
- const codeTheme = getStr(["codeTheme"]);
1404
- if (codeTheme) meta.codeTheme = codeTheme;
1405
- const ratingRaw = getStr(["rating"]);
1406
- if (ratingRaw !== void 0) {
1407
- const n = Number(ratingRaw);
1408
- if (!Number.isNaN(n)) meta.rating = Math.min(5, Math.max(0, n));
1409
- }
1410
- const rawTitle = typeof raw["title"] === "string" ? raw["title"] : void 0;
1411
- return {
1412
- title: rawTitle !== void 0 ? stripQuotes(rawTitle) : void 0,
1413
- type: getStr(["type"]),
1414
- meta,
1415
- body
1416
- };
1417
- }
1418
- function pushNested(out, inner, wrap) {
1419
- const children = parseInline(inner);
1420
- if (children.length === 0) {
1421
- out.push({
1422
- text: inner,
1423
- attrs: { ...wrap }
1424
- });
1425
- return;
1426
- }
1427
- for (const child of children) out.push({
1428
- text: child.text,
1429
- attrs: {
1430
- ...child.attrs ?? {},
1431
- ...wrap
1432
- }
1433
- });
1434
- }
1435
- function parseInline(text) {
1436
- const stripped = text.replace(/\{lang="[^"]*"\}/g, "").replace(/:(?!badge|icon|kbd)(\w[\w-]*)\[([^\]]*)\](\{[^}]*\})?/g, "$2").replace(/:(?!badge|icon|kbd)(\w[\w-]*)(\{[^}]*\})/g, "");
1437
- const tokens = [];
1438
- const re = /\$([^$\n]+?)\$|@\[([^\]]+?)\]\(user:([^)]+?)\)|:badge\[([^\]]*)\](\{[^}]*\})?|:icon\{([^}]*)\}|:kbd\{([^}]*)\}|\[\[([0-9a-fA-F-]{36})(?:\|([^\]]+?))?\]\]|~~(.+?)~~|\*\*(.+?)\*\*|\*(.+?)\*|_(.+?)_|`(.+?)`|\[(.+?)\]\((.+?)\)/g;
1439
- let lastIndex = 0;
1440
- let match;
1441
- while ((match = re.exec(stripped)) !== null) {
1442
- if (match.index > lastIndex) tokens.push({ text: stripped.slice(lastIndex, match.index) });
1443
- if (match[1] !== void 0) tokens.push({
1444
- text: match[1],
1445
- attrs: { mathInline: { expression: match[1] } }
1446
- });
1447
- else if (match[2] !== void 0 && match[3] !== void 0) tokens.push({
1448
- text: match[2],
1449
- attrs: { mention: {
1450
- userId: match[3],
1451
- label: match[2]
1452
- } }
1453
- });
1454
- else if (match[4] !== void 0) {
1455
- const badgeProps = parseMdcProps(match[5]);
1456
- tokens.push({
1457
- text: match[4] || "Badge",
1458
- attrs: { badge: {
1459
- label: match[4] || "Badge",
1460
- color: badgeProps["color"] || "neutral",
1461
- variant: badgeProps["variant"] || "subtle"
1462
- } }
1463
- });
1464
- } else if (match[6] !== void 0) {
1465
- const iconProps = parseMdcProps(`{${match[6]}}`);
1466
- tokens.push({
1467
- text: "​",
1468
- attrs: { proseIcon: { name: iconProps["name"] || "i-lucide-star" } }
1469
- });
1470
- } else if (match[7] !== void 0) {
1471
- const kbdProps = parseMdcProps(`{${match[7]}}`);
1472
- tokens.push({
1473
- text: kbdProps["value"] || "",
1474
- attrs: { kbd: { value: kbdProps["value"] || "" } }
1475
- });
1476
- } else if (match[8] !== void 0) {
1477
- const docId = match[8];
1478
- const label = match[9] ?? docId;
1479
- tokens.push({
1480
- text: label,
1481
- attrs: { docLink: { docId } }
1482
- });
1483
- } else if (match[10] !== void 0) pushNested(tokens, match[10], { strike: true });
1484
- else if (match[11] !== void 0) pushNested(tokens, match[11], { bold: true });
1485
- else if (match[12] !== void 0) pushNested(tokens, match[12], { italic: true });
1486
- else if (match[13] !== void 0) pushNested(tokens, match[13], { italic: true });
1487
- else if (match[14] !== void 0) tokens.push({
1488
- text: match[14],
1489
- attrs: { code: true }
1490
- });
1491
- else if (match[15] !== void 0 && match[16] !== void 0) tokens.push({
1492
- text: match[15],
1493
- attrs: { link: { href: match[16] } }
1494
- });
1495
- lastIndex = match.index + match[0].length;
1496
- }
1497
- if (lastIndex < stripped.length) tokens.push({ text: stripped.slice(lastIndex) });
1498
- return tokens.filter((t) => t.text.length > 0);
1499
- }
1500
- function parseTableRow(line) {
1501
- const parts = line.split("|");
1502
- return parts.slice(1, parts.length - 1).map((c) => c.trim());
1503
- }
1504
- function isTableSeparator(line) {
1505
- return /^\|[\s|:-]+\|$/.test(line.trim());
1506
- }
1507
- /** Extract fenced code blocks from MDC #code slot lines. */
1508
- function extractFencedCode(lines) {
1509
- const result = [];
1510
- let i = 0;
1511
- while (i < lines.length) {
1512
- const fenceMatch = lines[i].match(/^(`{3,})(\w*)/);
1513
- if (fenceMatch) {
1514
- const fence = fenceMatch[1];
1515
- const lang = fenceMatch[2] ?? "";
1516
- const codeLines = [];
1517
- i++;
1518
- while (i < lines.length && !lines[i].startsWith(fence)) {
1519
- codeLines.push(lines[i]);
1520
- i++;
1521
- }
1522
- i++;
1523
- result.push({
1524
- type: "codeBlock",
1525
- lang,
1526
- code: codeLines.join("\n")
1527
- });
1528
- continue;
1529
- }
1530
- i++;
1531
- }
1532
- return result;
1533
- }
1534
- /** Extract key="value" pairs from MDC prop syntax `{key="value" other="x"}` */
1535
- function parseMdcProps(propsStr) {
1536
- if (!propsStr) return {};
1537
- const result = {};
1538
- let s = propsStr.trim();
1539
- if (s.startsWith("{") && s.endsWith("}")) s = s.slice(1, -1);
1540
- const re = /(\w[\w-]*)(?:=(?:"([^"]*)"|([^\s"}]+)))?/g;
1541
- let m;
1542
- while ((m = re.exec(s)) !== null) {
1543
- const key = m[1];
1544
- if (m[2] !== void 0) result[key] = m[2];
1545
- else if (m[3] !== void 0) result[key] = m[3];
1546
- else result[key] = "true";
1547
- }
1548
- return result;
1549
- }
1550
- /** Parse named child MDC blocks from inner lines (e.g. #item for accordion, #tab for tabs) */
1551
- function parseMdcChildren(innerLines, slotPrefix) {
1552
- const items = [];
1553
- let current = null;
1554
- const slotRe = new RegExp(`^#${slotPrefix}(\\{[^}]*\\})?\\s*$`);
1555
- for (const line of innerLines) {
1556
- const slotMatch = line.match(slotRe);
1557
- if (slotMatch) {
1558
- if (current) items.push(current);
1559
- const props = parseMdcProps(slotMatch[1]);
1560
- current = {
1561
- label: props["label"] || props["title"] || `Item ${items.length + 1}`,
1562
- icon: props["icon"] || "",
1563
- lines: []
1564
- };
1565
- continue;
1566
- }
1567
- if (current) current.lines.push(line);
1568
- else if (!items.length && !current) current = {
1569
- label: `Item 1`,
1570
- icon: "",
1571
- lines: [line]
1572
- };
1573
- }
1574
- if (current) items.push(current);
1575
- return items.map((item) => ({
1576
- label: item.label,
1577
- icon: item.icon,
1578
- innerBlocks: parseBlocks(item.lines.join("\n"))
1579
- }));
1580
- }
1581
- const TASK_RE = /^[-*+]\s+\[([ xX])\]\s+(.*)/;
1582
- /**
1583
- * Consume a list (bullet / ordered / task) starting at `start`. Indented
1584
- * continuation lines and nested lists are captured into each item's
1585
- * `innerBlocks` so the parse → serialise → parse cycle preserves tree
1586
- * structure instead of flattening nested lists onto a single line.
1587
- *
1588
- * `indent` is the column of the item marker for the current list. A
1589
- * nested list starts ≥2 columns deeper. Lines with less indent than
1590
- * `indent` belong to the outer block and stop consumption.
1591
- */
1592
- function consumeList(lines, start, indent, kind) {
1593
- const items = [];
1594
- let i = start;
1595
- while (i < lines.length) {
1596
- const line = lines[i];
1597
- if (line.trim() === "") {
1598
- let j = i + 1;
1599
- while (j < lines.length && lines[j].trim() === "") j++;
1600
- if (j >= lines.length) break;
1601
- const lookahead = lines[j];
1602
- if (leadingSpaces(lookahead) < indent) break;
1603
- if (!matchMarker(lookahead.slice(indent), kind)) break;
1604
- i = j;
1605
- continue;
1606
- }
1607
- const leading = leadingSpaces(line);
1608
- if (leading < indent) break;
1609
- if (leading > indent) break;
1610
- const m = matchMarker(line.slice(indent), kind);
1611
- if (!m) break;
1612
- const item = { text: m.text };
1613
- if (kind === "task") item.checked = m.checked;
1614
- i++;
1615
- const contLines = [];
1616
- while (i < lines.length) {
1617
- const next = lines[i];
1618
- if (next.trim() === "") {
1619
- let k = i + 1;
1620
- while (k < lines.length && lines[k].trim() === "") k++;
1621
- if (k >= lines.length) break;
1622
- if (leadingSpaces(lines[k]) <= indent) break;
1623
- contLines.push("");
1624
- i++;
1625
- continue;
1626
- }
1627
- const nextIndent = leadingSpaces(next);
1628
- if (nextIndent <= indent) break;
1629
- const deindentBy = Math.min(nextIndent, indent + 2);
1630
- contLines.push(next.slice(deindentBy));
1631
- i++;
1632
- }
1633
- if (contLines.length > 0) item.innerBlocks = parseBlocks(contLines.join("\n"));
1634
- items.push(item);
1635
- }
1636
- return {
1637
- items,
1638
- next: i
1639
- };
1640
- }
1641
- function leadingSpaces(s) {
1642
- let n = 0;
1643
- while (n < s.length && s[n] === " ") n++;
1644
- return n;
1645
- }
1646
- function matchMarker(s, kind) {
1647
- if (kind === "task") {
1648
- const m = s.match(TASK_RE);
1649
- if (!m) return null;
1650
- return {
1651
- text: m[2],
1652
- checked: m[1].toLowerCase() === "x"
1653
- };
1654
- }
1655
- if (kind === "bullet") {
1656
- if (TASK_RE.test(s)) return null;
1657
- const m = s.match(/^[-*+]\s+(.*)$/);
1658
- if (!m) return null;
1659
- return {
1660
- text: m[1],
1661
- checked: false
1662
- };
1663
- }
1664
- const m = s.match(/^\d+\.\s+(.*)$/);
1665
- if (!m) return null;
1666
- return {
1667
- text: m[1],
1668
- checked: false
1669
- };
1670
- }
1671
- function parseBlocks(markdown) {
1672
- const rawLines = markdown.split("\n");
1673
- let firstContentLine = 0;
1674
- while (firstContentLine < rawLines.length) {
1675
- const l = rawLines[firstContentLine];
1676
- if (l.trim() === "" || /^import\s/.test(l) || /^export\s/.test(l)) firstContentLine++;
1677
- else break;
1678
- }
1679
- const stripped = rawLines.slice(firstContentLine).join("\n");
1680
- const blocks = [];
1681
- const lines = stripped.split("\n");
1682
- let i = 0;
1683
- while (i < lines.length) {
1684
- const line = lines[i];
1685
- const fenceBlockMatch = line.match(/^(`{3,})(.*)$/);
1686
- if (fenceBlockMatch) {
1687
- const fence = fenceBlockMatch[1];
1688
- const lang = fenceBlockMatch[2].trim().replace(/\{[^}]*\}$/, "").replace(/\s*\[.*\]$/, "").trim();
1689
- const codeLines = [];
1690
- i++;
1691
- while (i < lines.length && !lines[i].startsWith(fence)) {
1692
- codeLines.push(lines[i]);
1693
- i++;
1694
- }
1695
- i++;
1696
- const code = codeLines.join("\n");
1697
- if (lang === "math") blocks.push({
1698
- type: "mathBlock",
1699
- expression: code
1700
- });
1701
- else blocks.push({
1702
- type: "codeBlock",
1703
- lang,
1704
- code
1705
- });
1706
- continue;
1707
- }
1708
- const headingMatch = line.match(/^(#{1,6})\s+(.*)/);
1709
- if (headingMatch) {
1710
- blocks.push({
1711
- type: "heading",
1712
- level: headingMatch[1].length,
1713
- text: headingMatch[2].trim()
1714
- });
1715
- i++;
1716
- continue;
1717
- }
1718
- if (/^[-*_]{3,}\s*$/.test(line)) {
1719
- blocks.push({ type: "hr" });
1720
- i++;
1721
- continue;
1722
- }
1723
- const embedMatch = line.match(/^!\[\[([0-9a-fA-F-]{36})(?:\|([^\]]+))?\]\](\{[^}]*\})?\s*$/);
1724
- if (embedMatch) {
1725
- const docId = embedMatch[1];
1726
- const label = embedMatch[2] ?? "";
1727
- const props = parseMdcProps(embedMatch[3]);
1728
- blocks.push({
1729
- type: "docEmbed",
1730
- docId,
1731
- label,
1732
- props
1733
- });
1734
- i++;
1735
- continue;
1736
- }
1737
- const imgMatch = line.match(/^!\[([^\]]*)\]\(([^)]+)\)(\{[^}]*\})?\s*$/);
1738
- if (imgMatch) {
1739
- const alt = imgMatch[1] ?? "";
1740
- const src = imgMatch[2] ?? "";
1741
- const attrs = parseMdcProps(imgMatch[3]);
1742
- blocks.push({
1743
- type: "image",
1744
- src,
1745
- alt,
1746
- width: attrs["width"],
1747
- height: attrs["height"]
1748
- });
1749
- i++;
1750
- continue;
1751
- }
1752
- if (line.startsWith(">")) {
1753
- const bqLines = [];
1754
- while (i < lines.length && lines[i].startsWith(">")) {
1755
- bqLines.push(lines[i].replace(/^>\s?/, ""));
1756
- i++;
1757
- }
1758
- blocks.push({
1759
- type: "blockquote",
1760
- lines: bqLines
1761
- });
1762
- continue;
1763
- }
1764
- if (/^\s*\|/.test(line)) {
1765
- const tableLines = [];
1766
- while (i < lines.length && /^\s*\|/.test(lines[i])) {
1767
- tableLines.push(lines[i]);
1768
- i++;
1769
- }
1770
- if (tableLines.length >= 2 && isTableSeparator(tableLines[1])) {
1771
- const headerRow = parseTableRow(tableLines[0]);
1772
- const dataRows = tableLines.slice(2).filter((l) => !isTableSeparator(l)).map(parseTableRow);
1773
- blocks.push({
1774
- type: "table",
1775
- headerRow,
1776
- dataRows
1777
- });
1778
- } else for (const l of tableLines) blocks.push({
1779
- type: "paragraph",
1780
- text: l
1781
- });
1782
- continue;
1783
- }
1784
- const atomMatch = line.match(/^:(\w[\w-]*)(\{[^}]*\})?\s*$/);
1785
- if (atomMatch && atomMatch[1] === "file") {
1786
- const props = parseMdcProps(atomMatch[2]);
1787
- const uploadId = props["upload-id"] ?? props["uploadId"] ?? "";
1788
- const filename = props["filename"] ?? "";
1789
- const mime = props["mime"] ?? "";
1790
- const src = props["src"] ?? (uploadId && filename ? `.abracadabra/files/${uploadId}-${filename}` : "");
1791
- blocks.push({
1792
- type: "fileBlock",
1793
- src,
1794
- mime,
1795
- uploadId,
1796
- filename
1797
- });
1798
- i++;
1799
- continue;
1800
- }
1801
- const MDC_OPEN = /^\s*(:{2,})(\w[\w-]*)(\{[^}]*\})?\s*$/;
1802
- if (MDC_OPEN.test(line)) {
1803
- const colons = line.match(/^\s*(:+)/)?.[1]?.length ?? 2;
1804
- const componentName = line.match(/^\s*:{2,}(\w[\w-]*)/)?.[1] ?? "";
1805
- const innerLines = [];
1806
- i++;
1807
- while (i < lines.length) {
1808
- const l = lines[i];
1809
- if (new RegExp(`^\\s*:{${colons}}\\s*$`).test(l)) {
1810
- i++;
1811
- break;
1812
- }
1813
- const innerFence = l.match(/^(\s*`{3,})/);
1814
- if (innerFence) {
1815
- const fenceStr = innerFence[1].trimStart();
1816
- innerLines.push(l);
1817
- i++;
1818
- while (i < lines.length && !lines[i].trimStart().startsWith(fenceStr)) {
1819
- innerLines.push(lines[i]);
1820
- i++;
1821
- }
1822
- if (i < lines.length) {
1823
- innerLines.push(lines[i]);
1824
- i++;
1825
- }
1826
- continue;
1827
- }
1828
- innerLines.push(l);
1829
- i++;
1830
- }
1831
- const nonBlank = innerLines.filter((l) => l.trim().length > 0);
1832
- if (nonBlank.length) {
1833
- const minIndent = Math.min(...nonBlank.map((l) => l.match(/^(\s*)/)?.[1]?.length ?? 0));
1834
- if (minIndent > 0) for (let j = 0; j < innerLines.length; j++) innerLines[j] = innerLines[j].slice(Math.min(minIndent, innerLines[j].length));
1835
- }
1836
- let contentStart = 0;
1837
- if (innerLines[0]?.trim() === "---") {
1838
- const fmEnd = innerLines.findIndex((l, idx) => idx > 0 && l.trim() === "---");
1839
- if (fmEnd !== -1) contentStart = fmEnd + 1;
1840
- }
1841
- const contentLines = innerLines.slice(contentStart);
1842
- const defaultSlotLines = [];
1843
- const codeSlotLines = [];
1844
- let currentSlot = "default";
1845
- for (const l of contentLines) {
1846
- if (/^#code\s*$/.test(l)) {
1847
- currentSlot = "code";
1848
- continue;
1849
- }
1850
- if (/^#\w+/.test(l) && !/^#{2,}\s/.test(l)) {
1851
- currentSlot = "other";
1852
- continue;
1853
- }
1854
- if (currentSlot === "default") defaultSlotLines.push(l);
1855
- else if (currentSlot === "code") codeSlotLines.push(l);
1856
- }
1857
- const innerBlocks = parseBlocks(defaultSlotLines.join("\n"));
1858
- const codeBlocks = extractFencedCode(codeSlotLines);
1859
- if (new Set([
1860
- "tip",
1861
- "note",
1862
- "info",
1863
- "warning",
1864
- "caution",
1865
- "danger",
1866
- "callout",
1867
- "alert"
1868
- ]).has(componentName.toLowerCase())) blocks.push({
1869
- type: "callout",
1870
- calloutType: componentName.toLowerCase(),
1871
- innerBlocks
1872
- });
1873
- else {
1874
- const mdcProps = parseMdcProps(line.match(MDC_OPEN)?.[3]);
1875
- const lc = componentName.toLowerCase();
1876
- if (lc === "collapsible") blocks.push({
1877
- type: "collapsible",
1878
- label: mdcProps["label"] || "Details",
1879
- open: mdcProps["open"] === "true",
1880
- innerBlocks
1881
- });
1882
- else if (lc === "steps") blocks.push({
1883
- type: "steps",
1884
- innerBlocks
1885
- });
1886
- else if (lc === "card") blocks.push({
1887
- type: "card",
1888
- title: mdcProps["title"] || "",
1889
- icon: mdcProps["icon"] || "",
1890
- to: mdcProps["to"] || "",
1891
- innerBlocks
1892
- });
1893
- else if (lc === "card-group") {
1894
- const cards = innerBlocks.filter((b) => b.type === "card");
1895
- if (cards.length) blocks.push({
1896
- type: "cardGroup",
1897
- cards
1898
- });
1899
- else blocks.push(...innerBlocks);
1900
- } else if (lc === "code-collapse") blocks.push({
1901
- type: "codeCollapse",
1902
- codeBlocks: codeBlocks.length ? codeBlocks : innerBlocks.filter((b) => b.type === "codeBlock")
1903
- });
1904
- else if (lc === "code-group") {
1905
- const allCode = [...innerBlocks.filter((b) => b.type === "codeBlock"), ...codeBlocks];
1906
- blocks.push({
1907
- type: "codeGroup",
1908
- codeBlocks: allCode
1909
- });
1910
- } else if (lc === "code-preview") blocks.push({
1911
- type: "codePreview",
1912
- innerBlocks,
1913
- codeBlocks
1914
- });
1915
- else if (lc === "code-tree") blocks.push({
1916
- type: "codeTree",
1917
- files: mdcProps["files"] || "[]"
1918
- });
1919
- else if (lc === "accordion") {
1920
- const items = parseMdcChildren(contentLines, "item");
1921
- if (items.length) blocks.push({
1922
- type: "accordion",
1923
- items
1924
- });
1925
- else blocks.push({
1926
- type: "accordion",
1927
- items: [{
1928
- label: "Item 1",
1929
- icon: "",
1930
- innerBlocks
1931
- }]
1932
- });
1933
- } else if (lc === "tabs") {
1934
- const items = parseMdcChildren(contentLines, "tab");
1935
- if (items.length) blocks.push({
1936
- type: "tabs",
1937
- items
1938
- });
1939
- else blocks.push({
1940
- type: "tabs",
1941
- items: [{
1942
- label: "Tab 1",
1943
- icon: "",
1944
- innerBlocks
1945
- }]
1946
- });
1947
- } else if (lc === "field") blocks.push({
1948
- type: "field",
1949
- name: mdcProps["name"] || "",
1950
- fieldType: mdcProps["type"] || "string",
1951
- required: mdcProps["required"] === "true",
1952
- innerBlocks
1953
- });
1954
- else if (lc === "field-group") {
1955
- const fields = innerBlocks.filter((b) => b.type === "field");
1956
- if (fields.length) blocks.push({
1957
- type: "fieldGroup",
1958
- fields
1959
- });
1960
- else blocks.push(...innerBlocks);
1961
- } else {
1962
- blocks.push(...innerBlocks);
1963
- blocks.push(...codeBlocks);
1964
- }
1965
- }
1966
- continue;
1967
- }
1968
- if (TASK_RE.test(line)) {
1969
- const { items, next } = consumeList(lines, i, 0, "task");
1970
- i = next;
1971
- blocks.push({
1972
- type: "taskList",
1973
- items
1974
- });
1975
- continue;
1976
- }
1977
- if (/^[-*+]\s+/.test(line)) {
1978
- const { items, next } = consumeList(lines, i, 0, "bullet");
1979
- if (items.length > 0) {
1980
- i = next;
1981
- blocks.push({
1982
- type: "bulletList",
1983
- items
1984
- });
1985
- continue;
1986
- }
1987
- }
1988
- if (/^\d+\.\s+/.test(line)) {
1989
- const { items, next } = consumeList(lines, i, 0, "ordered");
1990
- if (items.length > 0) {
1991
- i = next;
1992
- blocks.push({
1993
- type: "orderedList",
1994
- items
1995
- });
1996
- continue;
1997
- }
1998
- }
1999
- if (line.trim() === "") {
2000
- i++;
2001
- continue;
2002
- }
2003
- const paraLines = [];
2004
- while (i < lines.length && lines[i].trim() !== "" && !/^(#{1,6}\s|[-*+]\s|\d+\.\s|>|`{3,}|\s*\||[-*_]{3,}\s*$|\s*:{2,}\w)/.test(lines[i])) {
2005
- paraLines.push(lines[i]);
2006
- i++;
2007
- }
2008
- if (paraLines.length) blocks.push({
2009
- type: "paragraph",
2010
- text: paraLines.join(" ")
2011
- });
2012
- else {
2013
- blocks.push({
2014
- type: "paragraph",
2015
- text: line
2016
- });
2017
- i++;
2018
- }
1321
+ //#region packages/convert/src/spec/universal-meta.ts
1322
+ const UNIVERSAL_META_KEYS = [
1323
+ {
1324
+ key: "title",
1325
+ type: "string",
1326
+ doc: "Display title; the first H1 is hoisted into this field on import."
1327
+ },
1328
+ {
1329
+ key: "type",
1330
+ type: "string",
1331
+ doc: "Page type (doc, kanban, table, …). Omitted on serialise when \"doc\"."
1332
+ },
1333
+ {
1334
+ key: "color",
1335
+ type: "string",
1336
+ doc: "Hex or CSS color name."
1337
+ },
1338
+ {
1339
+ key: "icon",
1340
+ type: "string",
1341
+ doc: "Lucide icon name in kebab-case."
1342
+ },
1343
+ {
1344
+ key: "datetimeStart",
1345
+ type: "iso-datetime"
1346
+ },
1347
+ {
1348
+ key: "datetimeEnd",
1349
+ type: "iso-datetime"
1350
+ },
1351
+ {
1352
+ key: "allDay",
1353
+ type: "boolean"
1354
+ },
1355
+ {
1356
+ key: "dateTaken",
1357
+ type: "iso-datetime"
1358
+ },
1359
+ {
1360
+ key: "dateStart",
1361
+ type: "iso-date",
1362
+ parseAliases: ["date", "created"]
1363
+ },
1364
+ {
1365
+ key: "dateEnd",
1366
+ type: "iso-date",
1367
+ parseAliases: ["due"]
1368
+ },
1369
+ {
1370
+ key: "timeStart",
1371
+ type: "hh-mm"
1372
+ },
1373
+ {
1374
+ key: "timeEnd",
1375
+ type: "hh-mm"
1376
+ },
1377
+ {
1378
+ key: "tags",
1379
+ type: "string[]"
1380
+ },
1381
+ {
1382
+ key: "checked",
1383
+ type: "boolean",
1384
+ parseAliases: ["done"]
1385
+ },
1386
+ {
1387
+ key: "priority",
1388
+ type: "integer",
1389
+ min: 0,
1390
+ max: 4,
1391
+ doc: "Numeric or named (low/medium/high/urgent → 1/2/3/4)."
1392
+ },
1393
+ {
1394
+ key: "status",
1395
+ type: "string"
1396
+ },
1397
+ {
1398
+ key: "rating",
1399
+ type: "number",
1400
+ min: 0,
1401
+ max: 5
1402
+ },
1403
+ {
1404
+ key: "url",
1405
+ type: "string"
1406
+ },
1407
+ {
1408
+ key: "email",
1409
+ type: "string"
1410
+ },
1411
+ {
1412
+ key: "phone",
1413
+ type: "string"
1414
+ },
1415
+ {
1416
+ key: "number",
1417
+ type: "number"
1418
+ },
1419
+ {
1420
+ key: "unit",
1421
+ type: "string"
1422
+ },
1423
+ {
1424
+ key: "subtitle",
1425
+ type: "string",
1426
+ parseAliases: ["description"]
1427
+ },
1428
+ {
1429
+ key: "note",
1430
+ type: "string"
1431
+ },
1432
+ {
1433
+ key: "taskProgress",
1434
+ type: "integer",
1435
+ min: 0,
1436
+ max: 100
1437
+ },
1438
+ {
1439
+ key: "members",
1440
+ type: "members"
1441
+ },
1442
+ {
1443
+ key: "coverUploadId",
1444
+ type: "string"
1445
+ },
1446
+ {
1447
+ key: "coverDocId",
1448
+ type: "string"
1449
+ },
1450
+ {
1451
+ key: "coverMimeType",
1452
+ type: "string"
1453
+ },
1454
+ {
1455
+ key: "geoType",
1456
+ type: "string-enum",
1457
+ values: [
1458
+ "marker",
1459
+ "line",
1460
+ "measure"
1461
+ ]
1462
+ },
1463
+ {
1464
+ key: "geoLat",
1465
+ type: "number"
1466
+ },
1467
+ {
1468
+ key: "geoLng",
1469
+ type: "number"
1470
+ },
1471
+ {
1472
+ key: "geoDescription",
1473
+ type: "string"
1474
+ },
1475
+ {
1476
+ key: "deskX",
1477
+ type: "number"
1478
+ },
1479
+ {
1480
+ key: "deskY",
1481
+ type: "number"
1482
+ },
1483
+ {
1484
+ key: "deskZ",
1485
+ type: "number"
1486
+ },
1487
+ {
1488
+ key: "deskMode",
1489
+ type: "string-enum",
1490
+ values: [
1491
+ "icon",
1492
+ "widget-sm",
1493
+ "widget-lg"
1494
+ ]
1495
+ },
1496
+ {
1497
+ key: "mmX",
1498
+ type: "number"
1499
+ },
1500
+ {
1501
+ key: "mmY",
1502
+ type: "number"
1503
+ },
1504
+ {
1505
+ key: "graphX",
1506
+ type: "number"
1507
+ },
1508
+ {
1509
+ key: "graphY",
1510
+ type: "number"
1511
+ },
1512
+ {
1513
+ key: "graphPinned",
1514
+ type: "boolean"
1515
+ },
1516
+ {
1517
+ key: "spX",
1518
+ type: "number"
1519
+ },
1520
+ {
1521
+ key: "spY",
1522
+ type: "number"
1523
+ },
1524
+ {
1525
+ key: "spZ",
1526
+ type: "number"
1527
+ },
1528
+ {
1529
+ key: "spRX",
1530
+ type: "number"
1531
+ },
1532
+ {
1533
+ key: "spRY",
1534
+ type: "number"
1535
+ },
1536
+ {
1537
+ key: "spRZ",
1538
+ type: "number"
1539
+ },
1540
+ {
1541
+ key: "spSX",
1542
+ type: "number"
1543
+ },
1544
+ {
1545
+ key: "spSY",
1546
+ type: "number"
1547
+ },
1548
+ {
1549
+ key: "spSZ",
1550
+ type: "number"
1551
+ },
1552
+ {
1553
+ key: "spShape",
1554
+ type: "string-enum",
1555
+ values: [
1556
+ "box",
1557
+ "sphere",
1558
+ "cylinder",
1559
+ "cone",
1560
+ "plane",
1561
+ "torus",
1562
+ "glb"
1563
+ ]
1564
+ },
1565
+ {
1566
+ key: "spOpacity",
1567
+ type: "integer",
1568
+ min: 0,
1569
+ max: 100
1570
+ },
1571
+ {
1572
+ key: "spModelUploadId",
1573
+ type: "string"
1574
+ },
1575
+ {
1576
+ key: "spModelDocId",
1577
+ type: "string"
1578
+ },
1579
+ {
1580
+ key: "slidesTransition",
1581
+ type: "string-enum",
1582
+ values: [
1583
+ "none",
1584
+ "fade",
1585
+ "slide"
1586
+ ]
1587
+ },
1588
+ {
1589
+ key: "slidesTheme",
1590
+ type: "string-enum",
1591
+ values: ["dark", "light"]
1592
+ },
1593
+ {
1594
+ key: "__schemaVersion",
1595
+ type: "integer",
1596
+ min: 0
2019
1597
  }
2020
- return blocks;
2021
- }
1598
+ ];
1599
+ const UNIVERSAL_META_KEY_NAMES = new Set(UNIVERSAL_META_KEYS.map((k) => k.key));
2022
1600
  /**
2023
- * Insert formatted inline tokens into an already-attached Y.XmlElement.
2024
- * Creates one Y.XmlText per token (attach first, fill second).
1601
+ * Build a map of every recognised input key (canonical + aliases) to
1602
+ * its canonical key. Used by the frontmatter parser.
2025
1603
  */
2026
- function fillTextInto(el, tokens) {
2027
- const filtered = tokens.filter((t) => t.text.length > 0);
2028
- if (!filtered.length) return;
2029
- const children = filtered.map((tok) => {
2030
- return (tok.attrs?.docLink)?.docId ? new Y.XmlElement("docLink") : new Y.XmlText();
2031
- });
2032
- el.insert(0, children);
2033
- filtered.forEach((tok, i) => {
2034
- const node = children[i];
2035
- if (node instanceof Y.XmlElement) {
2036
- const dl = tok.attrs.docLink;
2037
- node.setAttribute("docId", dl.docId);
2038
- return;
2039
- }
2040
- if (tok.attrs) node.insert(0, tok.text, tok.attrs);
2041
- else node.insert(0, tok.text);
2042
- });
2043
- }
2044
- function blockElName(b) {
2045
- switch (b.type) {
2046
- case "heading": return "heading";
2047
- case "paragraph": return "paragraph";
2048
- case "bulletList": return "bulletList";
2049
- case "orderedList": return "orderedList";
2050
- case "taskList": return "taskList";
2051
- case "codeBlock": return "codeBlock";
2052
- case "blockquote": return "blockquote";
2053
- case "table": return "table";
2054
- case "hr": return "horizontalRule";
2055
- case "callout": return "callout";
2056
- case "collapsible": return "collapsible";
2057
- case "steps": return "steps";
2058
- case "card": return "card";
2059
- case "cardGroup": return "cardGroup";
2060
- case "codeCollapse": return "codeCollapse";
2061
- case "codeGroup": return "codeGroup";
2062
- case "codePreview": return "codePreview";
2063
- case "codeTree": return "codeTree";
2064
- case "accordion": return "accordion";
2065
- case "tabs": return "tabs";
2066
- case "field": return "field";
2067
- case "fieldGroup": return "fieldGroup";
2068
- case "image": return "image";
2069
- case "docEmbed": return "docEmbed";
2070
- case "mathBlock": return "mathBlock";
2071
- case "fileBlock": return "fileBlock";
1604
+ function buildAliasMap() {
1605
+ const map = /* @__PURE__ */ new Map();
1606
+ for (const entry of UNIVERSAL_META_KEYS) {
1607
+ map.set(entry.key, entry.key);
1608
+ for (const alias of entry.parseAliases ?? []) map.set(alias, entry.key);
2072
1609
  }
1610
+ return map;
2073
1611
  }
2074
- function populateListItemChildren(itemEl, item, _itemKind) {
2075
- const paraEl = new Y.XmlElement("paragraph");
2076
- itemEl.insert(itemEl.length, [paraEl]);
2077
- fillTextInto(paraEl, parseInline(item.text));
2078
- if (!item.innerBlocks?.length) return;
2079
- const innerEls = item.innerBlocks.map((b) => new Y.XmlElement(blockElName(b)));
2080
- itemEl.insert(itemEl.length, innerEls);
2081
- item.innerBlocks.forEach((b, i) => fillBlock(innerEls[i], b));
1612
+
1613
+ //#endregion
1614
+ //#region packages/convert/src/markdown-to-yjs.ts
1615
+ function parseInlineArray(raw) {
1616
+ return raw.slice(1, -1).split(",").map((s) => s.trim()).filter(Boolean);
2082
1617
  }
2083
- function fillBlock(el, block) {
2084
- switch (block.type) {
2085
- case "heading":
2086
- el.setAttribute("level", block.level);
2087
- fillTextInto(el, parseInline(block.text));
2088
- break;
2089
- case "paragraph":
2090
- fillTextInto(el, parseInline(block.text));
2091
- break;
2092
- case "bulletList":
2093
- case "orderedList": {
2094
- const listItemEls = block.items.map(() => new Y.XmlElement("listItem"));
2095
- el.insert(0, listItemEls);
2096
- block.items.forEach((item, i) => {
2097
- populateListItemChildren(listItemEls[i], item, "listItem");
2098
- });
2099
- break;
2100
- }
2101
- case "taskList": {
2102
- const taskItemEls = block.items.map(() => new Y.XmlElement("taskItem"));
2103
- el.insert(0, taskItemEls);
2104
- block.items.forEach((item, i) => {
2105
- taskItemEls[i].setAttribute("checked", !!item.checked);
2106
- populateListItemChildren(taskItemEls[i], item, "taskItem");
2107
- });
2108
- break;
2109
- }
2110
- case "codeBlock": {
2111
- if (block.lang) el.setAttribute("language", block.lang);
2112
- const xt = new Y.XmlText();
2113
- el.insert(0, [xt]);
2114
- xt.insert(0, block.code);
2115
- break;
2116
- }
2117
- case "blockquote": {
2118
- const paraEls = block.lines.map(() => new Y.XmlElement("paragraph"));
2119
- el.insert(0, paraEls);
2120
- block.lines.forEach((line, i) => fillTextInto(paraEls[i], parseInline(line)));
2121
- break;
2122
- }
2123
- case "table": {
2124
- const headerRowEl = new Y.XmlElement("tableRow");
2125
- const dataRowEls = block.dataRows.map(() => new Y.XmlElement("tableRow"));
2126
- el.insert(0, [headerRowEl, ...dataRowEls]);
2127
- const headerCellEls = block.headerRow.map(() => new Y.XmlElement("tableHeader"));
2128
- headerRowEl.insert(0, headerCellEls);
2129
- block.headerRow.forEach((cellText, i) => {
2130
- const paraEl = new Y.XmlElement("paragraph");
2131
- headerCellEls[i].insert(0, [paraEl]);
2132
- fillTextInto(paraEl, parseInline(cellText));
2133
- });
2134
- block.dataRows.forEach((row, ri) => {
2135
- const cellEls = row.map(() => new Y.XmlElement("tableCell"));
2136
- dataRowEls[ri].insert(0, cellEls);
2137
- row.forEach((cellText, ci) => {
2138
- const paraEl = new Y.XmlElement("paragraph");
2139
- cellEls[ci].insert(0, [paraEl]);
2140
- fillTextInto(paraEl, parseInline(cellText));
2141
- });
2142
- });
2143
- break;
2144
- }
2145
- case "hr": break;
2146
- case "callout": {
2147
- el.setAttribute("type", block.calloutType);
2148
- if (!block.innerBlocks.length) {
2149
- const paraEl = new Y.XmlElement("paragraph");
2150
- el.insert(0, [paraEl]);
2151
- break;
1618
+ function stripQuotes(s) {
1619
+ if (s.length >= 2 && (s.startsWith("\"") && s.endsWith("\"") || s.startsWith("'") && s.endsWith("'"))) return s.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
1620
+ return s;
1621
+ }
1622
+ function parseFrontmatter(markdown) {
1623
+ const noResult = {
1624
+ meta: {},
1625
+ body: markdown
1626
+ };
1627
+ const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
1628
+ if (!match) return noResult;
1629
+ const yamlBlock = match[1];
1630
+ const body = markdown.slice(match[0].length);
1631
+ const raw = {};
1632
+ const lines = yamlBlock.split("\n");
1633
+ let i = 0;
1634
+ while (i < lines.length) {
1635
+ const line = lines[i];
1636
+ const blockSeqKey = line.match(/^(\w[\w-]*):\s*$/);
1637
+ if (blockSeqKey && i + 1 < lines.length && /^\s+-\s/.test(lines[i + 1])) {
1638
+ const key = blockSeqKey[1];
1639
+ const items = [];
1640
+ i++;
1641
+ while (i < lines.length && /^\s+-\s/.test(lines[i])) {
1642
+ items.push(lines[i].replace(/^\s+-\s/, "").trim());
1643
+ i++;
2152
1644
  }
2153
- const innerEls = block.innerBlocks.map((b) => new Y.XmlElement(blockElName(b)));
2154
- el.insert(0, innerEls);
2155
- block.innerBlocks.forEach((b, i) => fillBlock(innerEls[i], b));
2156
- break;
2157
- }
2158
- case "collapsible": {
2159
- el.setAttribute("label", block.label);
2160
- el.setAttribute("open", block.open);
2161
- const inner = block.innerBlocks.length ? block.innerBlocks : [{
2162
- type: "paragraph",
2163
- text: ""
2164
- }];
2165
- const innerEls = inner.map((b) => new Y.XmlElement(blockElName(b)));
2166
- el.insert(0, innerEls);
2167
- inner.forEach((b, i) => fillBlock(innerEls[i], b));
2168
- break;
1645
+ raw[key] = items;
1646
+ continue;
2169
1647
  }
2170
- case "steps": {
2171
- const inner = block.innerBlocks.length ? block.innerBlocks : [{
2172
- type: "paragraph",
2173
- text: ""
2174
- }];
2175
- const innerEls = inner.map((b) => new Y.XmlElement(blockElName(b)));
2176
- el.insert(0, innerEls);
2177
- inner.forEach((b, i) => fillBlock(innerEls[i], b));
2178
- break;
1648
+ const kvMatch = line.match(/^(\w[\w-]*):\s*(.*)$/);
1649
+ if (kvMatch) {
1650
+ const key = kvMatch[1];
1651
+ const val = kvMatch[2].trim();
1652
+ if (val.startsWith("[") && val.endsWith("]")) raw[key] = parseInlineArray(val).map(stripQuotes);
1653
+ else raw[key] = stripQuotes(val);
2179
1654
  }
2180
- case "card": {
2181
- if (block.title) el.setAttribute("title", block.title);
2182
- if (block.icon) el.setAttribute("icon", block.icon);
2183
- if (block.to) el.setAttribute("to", block.to);
2184
- const inner = block.innerBlocks.length ? block.innerBlocks : [{
2185
- type: "paragraph",
2186
- text: ""
2187
- }];
2188
- const innerEls = inner.map((b) => new Y.XmlElement(blockElName(b)));
2189
- el.insert(0, innerEls);
2190
- inner.forEach((b, i) => fillBlock(innerEls[i], b));
2191
- break;
1655
+ i++;
1656
+ }
1657
+ const meta = {};
1658
+ const getStr = (keys) => {
1659
+ for (const k of keys) {
1660
+ const v = raw[k];
1661
+ if (typeof v === "string" && v) return v;
2192
1662
  }
2193
- case "cardGroup": {
2194
- const cardEls = block.cards.map((b) => new Y.XmlElement(blockElName(b)));
2195
- el.insert(0, cardEls);
2196
- block.cards.forEach((b, i) => fillBlock(cardEls[i], b));
2197
- break;
1663
+ };
1664
+ if (raw["tags"]) meta.tags = Array.isArray(raw["tags"]) ? raw["tags"] : [raw["tags"]];
1665
+ const color = getStr(["color"]);
1666
+ if (color) meta.color = color;
1667
+ const icon = getStr(["icon"]);
1668
+ if (icon) meta.icon = icon;
1669
+ const status = getStr(["status"]);
1670
+ if (status) meta.status = status;
1671
+ const priorityRaw = getStr(["priority"]);
1672
+ if (priorityRaw !== void 0) meta.priority = {
1673
+ low: 1,
1674
+ medium: 2,
1675
+ high: 3,
1676
+ urgent: 4
1677
+ }[priorityRaw.toLowerCase()] ?? (Number(priorityRaw) || 0);
1678
+ const checkedRaw = raw["checked"] ?? raw["done"];
1679
+ if (checkedRaw !== void 0) meta.checked = checkedRaw === "true" || checkedRaw === true;
1680
+ const dateStart = getStr([
1681
+ "dateStart",
1682
+ "date",
1683
+ "created"
1684
+ ]);
1685
+ if (dateStart) meta.dateStart = dateStart;
1686
+ const dateEnd = getStr(["dateEnd", "due"]);
1687
+ if (dateEnd) meta.dateEnd = dateEnd;
1688
+ const subtitle = getStr(["subtitle", "description"]);
1689
+ if (subtitle) meta.subtitle = subtitle;
1690
+ const url = getStr(["url"]);
1691
+ if (url) meta.url = url;
1692
+ const language = getStr(["language"]);
1693
+ if (language) meta.language = language;
1694
+ const fileExtension = getStr(["fileExtension"]);
1695
+ if (fileExtension) meta.fileExtension = fileExtension;
1696
+ const codeTheme = getStr(["codeTheme"]);
1697
+ if (codeTheme) meta.codeTheme = codeTheme;
1698
+ const ratingRaw = getStr(["rating"]);
1699
+ if (ratingRaw !== void 0) {
1700
+ const n = Number(ratingRaw);
1701
+ if (!Number.isNaN(n)) meta.rating = Math.min(5, Math.max(0, n));
1702
+ }
1703
+ for (const [rawKey, rawVal] of Object.entries(raw)) {
1704
+ if (CONSUMED_FM_KEYS.has(rawKey)) continue;
1705
+ if (rawKey.startsWith("_")) continue;
1706
+ const canonical = FM_ALIAS_MAP.get(rawKey);
1707
+ if (canonical) {
1708
+ if (canonical === "title" || canonical === "type") continue;
1709
+ if (meta[canonical] !== void 0) continue;
1710
+ const coerced = coerceMetaValue(rawVal, FM_SPEC_BY_KEY.get(canonical)?.type);
1711
+ if (coerced !== void 0) meta[canonical] = coerced;
1712
+ } else {
1713
+ const coerced = coerceMetaValue(rawVal, void 0);
1714
+ if (coerced !== void 0) meta[rawKey] = coerced;
2198
1715
  }
2199
- case "codeCollapse": {
2200
- const codes = block.codeBlocks.length ? block.codeBlocks : [{
2201
- type: "codeBlock",
2202
- lang: "",
2203
- code: ""
2204
- }];
2205
- const codeEl = new Y.XmlElement("codeBlock");
2206
- el.insert(0, [codeEl]);
2207
- fillBlock(codeEl, codes[0]);
2208
- break;
1716
+ }
1717
+ const rawTitle = typeof raw["title"] === "string" ? raw["title"] : void 0;
1718
+ return {
1719
+ title: rawTitle !== void 0 ? stripQuotes(rawTitle) : void 0,
1720
+ type: getStr(["type"]),
1721
+ meta,
1722
+ body
1723
+ };
1724
+ }
1725
+ /** Raw YAML keys the hand-rolled section of parseFrontmatter consumes. */
1726
+ const CONSUMED_FM_KEYS = new Set([
1727
+ "title",
1728
+ "type",
1729
+ "tags",
1730
+ "color",
1731
+ "icon",
1732
+ "status",
1733
+ "priority",
1734
+ "checked",
1735
+ "done",
1736
+ "dateStart",
1737
+ "date",
1738
+ "created",
1739
+ "dateEnd",
1740
+ "due",
1741
+ "subtitle",
1742
+ "description",
1743
+ "url",
1744
+ "language",
1745
+ "fileExtension",
1746
+ "codeTheme",
1747
+ "rating"
1748
+ ]);
1749
+ const FM_ALIAS_MAP = buildAliasMap();
1750
+ const FM_SPEC_BY_KEY = new Map(UNIVERSAL_META_KEYS.map((k) => [k.key, k]));
1751
+ /**
1752
+ * Coerce a raw YAML scalar/array to the spec's value type. With no spec
1753
+ * (custom key) the coercion is best-effort: booleans and numbers are
1754
+ * recognised by shape, everything else stays a string. Returns undefined
1755
+ * when the value can't be represented (caller skips the key).
1756
+ */
1757
+ function coerceMetaValue(rawVal, specType) {
1758
+ if (Array.isArray(rawVal)) return rawVal;
1759
+ const v = rawVal;
1760
+ if (v === "") return void 0;
1761
+ switch (specType) {
1762
+ case "number":
1763
+ case "integer": {
1764
+ const n = Number(v);
1765
+ return Number.isFinite(n) ? n : void 0;
2209
1766
  }
2210
- case "codeGroup": {
2211
- const codes = block.codeBlocks.length ? block.codeBlocks : [{
2212
- type: "codeBlock",
2213
- lang: "",
2214
- code: ""
2215
- }];
2216
- const codeEls = codes.map(() => new Y.XmlElement("codeBlock"));
2217
- el.insert(0, codeEls);
2218
- codes.forEach((b, i) => fillBlock(codeEls[i], b));
2219
- break;
1767
+ case "boolean": return v === "true";
1768
+ case "string[]": return [v];
1769
+ case "members":
1770
+ case "json": try {
1771
+ return JSON.parse(v);
1772
+ } catch {
1773
+ return;
2220
1774
  }
2221
- case "codePreview": {
2222
- const all = [...block.innerBlocks, ...block.codeBlocks];
2223
- const inner = all.length ? all : [{
2224
- type: "paragraph",
2225
- text: ""
2226
- }];
2227
- const innerEls = inner.map((b) => new Y.XmlElement(blockElName(b)));
2228
- el.insert(0, innerEls);
2229
- inner.forEach((b, i) => fillBlock(innerEls[i], b));
2230
- break;
1775
+ case "string":
1776
+ case "string-enum":
1777
+ case "iso-date":
1778
+ case "iso-datetime":
1779
+ case "hh-mm": return v;
1780
+ case void 0:
1781
+ if (v === "true") return true;
1782
+ if (v === "false") return false;
1783
+ if (/^-?\d+(\.\d+)?$/.test(v)) return Number(v);
1784
+ if (v.startsWith("{")) try {
1785
+ return JSON.parse(v);
1786
+ } catch {}
1787
+ return v;
1788
+ }
1789
+ }
1790
+ function pushNested(out, inner, wrap) {
1791
+ const children = parseInline(inner);
1792
+ if (children.length === 0) {
1793
+ out.push({
1794
+ text: inner,
1795
+ attrs: { ...wrap }
1796
+ });
1797
+ return;
1798
+ }
1799
+ for (const child of children) out.push({
1800
+ text: child.text,
1801
+ attrs: {
1802
+ ...child.attrs ?? {},
1803
+ ...wrap
2231
1804
  }
2232
- case "codeTree":
2233
- el.setAttribute("files", block.files);
2234
- break;
2235
- case "accordion": {
2236
- const itemEls = block.items.map(() => new Y.XmlElement("accordionItem"));
2237
- el.insert(0, itemEls);
2238
- block.items.forEach((item, i) => {
2239
- itemEls[i].setAttribute("label", item.label);
2240
- if (item.icon) itemEls[i].setAttribute("icon", item.icon);
2241
- const inner = item.innerBlocks.length ? item.innerBlocks : [{
2242
- type: "paragraph",
2243
- text: ""
2244
- }];
2245
- const childEls = inner.map((b) => new Y.XmlElement(blockElName(b)));
2246
- itemEls[i].insert(0, childEls);
2247
- inner.forEach((b, ci) => fillBlock(childEls[ci], b));
1805
+ });
1806
+ }
1807
+ function parseInline(text) {
1808
+ const stripped = text.replace(/\{lang="[^"]*"\}/g, "").replace(/:(?!badge|icon|kbd)(\w[\w-]*)\[([^\]]*)\](\{[^}]*\})?/g, "$2").replace(/:(?!badge|icon|kbd)(\w[\w-]*)(\{[^}]*\})/g, "");
1809
+ const tokens = [];
1810
+ const re = /\$([^$\n]+?)\$|@\[([^\]]+?)\]\(user:([^)]+?)\)|:badge\[([^\]]*)\](\{[^}]*\})?|:icon\{([^}]*)\}|:kbd\{([^}]*)\}|\[\[([0-9a-fA-F-]{36})(?:\|([^\]]+?))?\]\]|~~(.+?)~~|\*\*(.+?)\*\*|\*(.+?)\*|_(.+?)_|`(.+?)`|\[(.+?)\]\((.+?)\)/g;
1811
+ let lastIndex = 0;
1812
+ let match;
1813
+ while ((match = re.exec(stripped)) !== null) {
1814
+ if (match.index > lastIndex) tokens.push({ text: stripped.slice(lastIndex, match.index) });
1815
+ if (match[1] !== void 0) tokens.push({
1816
+ text: match[1],
1817
+ attrs: { mathInline: { expression: match[1] } }
1818
+ });
1819
+ else if (match[2] !== void 0 && match[3] !== void 0) tokens.push({
1820
+ text: match[2],
1821
+ attrs: { mention: {
1822
+ userId: match[3],
1823
+ label: match[2]
1824
+ } }
1825
+ });
1826
+ else if (match[4] !== void 0) {
1827
+ const badgeProps = parseMdcProps(match[5]);
1828
+ tokens.push({
1829
+ text: match[4] || "Badge",
1830
+ attrs: { badge: {
1831
+ label: match[4] || "Badge",
1832
+ color: badgeProps["color"] || "neutral",
1833
+ variant: badgeProps["variant"] || "subtle"
1834
+ } }
2248
1835
  });
2249
- break;
2250
- }
2251
- case "tabs": {
2252
- const itemEls = block.items.map(() => new Y.XmlElement("tabsItem"));
2253
- el.insert(0, itemEls);
2254
- block.items.forEach((item, i) => {
2255
- itemEls[i].setAttribute("label", item.label);
2256
- if (item.icon) itemEls[i].setAttribute("icon", item.icon);
2257
- const inner = item.innerBlocks.length ? item.innerBlocks : [{
2258
- type: "paragraph",
2259
- text: ""
2260
- }];
2261
- const childEls = inner.map((b) => new Y.XmlElement(blockElName(b)));
2262
- itemEls[i].insert(0, childEls);
2263
- inner.forEach((b, ci) => fillBlock(childEls[ci], b));
1836
+ } else if (match[6] !== void 0) {
1837
+ const iconProps = parseMdcProps(`{${match[6]}}`);
1838
+ tokens.push({
1839
+ text: "",
1840
+ attrs: { proseIcon: { name: iconProps["name"] || "i-lucide-star" } }
2264
1841
  });
2265
- break;
2266
- }
2267
- case "field": {
2268
- if (block.name) el.setAttribute("name", block.name);
2269
- el.setAttribute("type", block.fieldType);
2270
- el.setAttribute("required", block.required);
2271
- const inner = block.innerBlocks.length ? block.innerBlocks : [{
2272
- type: "paragraph",
2273
- text: ""
2274
- }];
2275
- const innerEls = inner.map((b) => new Y.XmlElement(blockElName(b)));
2276
- el.insert(0, innerEls);
2277
- inner.forEach((b, i) => fillBlock(innerEls[i], b));
2278
- break;
1842
+ } else if (match[7] !== void 0) {
1843
+ const kbdProps = parseMdcProps(`{${match[7]}}`);
1844
+ tokens.push({
1845
+ text: kbdProps["value"] || "",
1846
+ attrs: { kbd: { value: kbdProps["value"] || "" } }
1847
+ });
1848
+ } else if (match[8] !== void 0) {
1849
+ const docId = match[8];
1850
+ const label = match[9] ?? docId;
1851
+ tokens.push({
1852
+ text: label,
1853
+ attrs: { docLink: { docId } }
1854
+ });
1855
+ } else if (match[10] !== void 0) pushNested(tokens, match[10], { strike: true });
1856
+ else if (match[11] !== void 0) pushNested(tokens, match[11], { bold: true });
1857
+ else if (match[12] !== void 0) pushNested(tokens, match[12], { italic: true });
1858
+ else if (match[13] !== void 0) pushNested(tokens, match[13], { italic: true });
1859
+ else if (match[14] !== void 0) tokens.push({
1860
+ text: match[14],
1861
+ attrs: { code: true }
1862
+ });
1863
+ else if (match[15] !== void 0 && match[16] !== void 0) tokens.push({
1864
+ text: match[15],
1865
+ attrs: { link: { href: match[16] } }
1866
+ });
1867
+ lastIndex = match.index + match[0].length;
1868
+ }
1869
+ if (lastIndex < stripped.length) tokens.push({ text: stripped.slice(lastIndex) });
1870
+ return tokens.filter((t) => t.text.length > 0);
1871
+ }
1872
+ function parseTableRow(line) {
1873
+ const parts = line.split("|");
1874
+ return parts.slice(1, parts.length - 1).map((c) => c.trim());
1875
+ }
1876
+ function isTableSeparator(line) {
1877
+ return /^\|[\s|:-]+\|$/.test(line.trim());
1878
+ }
1879
+ /** Extract fenced code blocks from MDC #code slot lines. */
1880
+ function extractFencedCode(lines) {
1881
+ const result = [];
1882
+ let i = 0;
1883
+ while (i < lines.length) {
1884
+ const fenceMatch = lines[i].match(/^(`{3,})(\w*)/);
1885
+ if (fenceMatch) {
1886
+ const fence = fenceMatch[1];
1887
+ const lang = fenceMatch[2] ?? "";
1888
+ const codeLines = [];
1889
+ i++;
1890
+ while (i < lines.length && !lines[i].startsWith(fence)) {
1891
+ codeLines.push(lines[i]);
1892
+ i++;
1893
+ }
1894
+ i++;
1895
+ result.push({
1896
+ type: "codeBlock",
1897
+ lang,
1898
+ code: codeLines.join("\n")
1899
+ });
1900
+ continue;
2279
1901
  }
2280
- case "fieldGroup": {
2281
- const fieldEls = block.fields.map((b) => new Y.XmlElement(blockElName(b)));
2282
- el.insert(0, fieldEls);
2283
- block.fields.forEach((b, i) => fillBlock(fieldEls[i], b));
2284
- break;
1902
+ i++;
1903
+ }
1904
+ return result;
1905
+ }
1906
+ /** Extract key="value" pairs from MDC prop syntax `{key="value" other="x"}` */
1907
+ function parseMdcProps(propsStr) {
1908
+ if (!propsStr) return {};
1909
+ const result = {};
1910
+ let s = propsStr.trim();
1911
+ if (s.startsWith("{") && s.endsWith("}")) s = s.slice(1, -1);
1912
+ const re = /(\w[\w-]*)(?:=(?:"([^"]*)"|([^\s"}]+)))?/g;
1913
+ let m;
1914
+ while ((m = re.exec(s)) !== null) {
1915
+ const key = m[1];
1916
+ if (m[2] !== void 0) result[key] = m[2];
1917
+ else if (m[3] !== void 0) result[key] = m[3];
1918
+ else result[key] = "true";
1919
+ }
1920
+ return result;
1921
+ }
1922
+ /** Parse named child MDC blocks from inner lines (e.g. #item for accordion, #tab for tabs) */
1923
+ function parseMdcChildren(innerLines, slotPrefix) {
1924
+ const items = [];
1925
+ let current = null;
1926
+ const slotRe = new RegExp(`^#${slotPrefix}(\\{[^}]*\\})?\\s*$`);
1927
+ for (const line of innerLines) {
1928
+ const slotMatch = line.match(slotRe);
1929
+ if (slotMatch) {
1930
+ if (current) items.push(current);
1931
+ const props = parseMdcProps(slotMatch[1]);
1932
+ current = {
1933
+ label: props["label"] || props["title"] || `Item ${items.length + 1}`,
1934
+ icon: props["icon"] || "",
1935
+ lines: []
1936
+ };
1937
+ continue;
2285
1938
  }
2286
- case "image":
2287
- el.setAttribute("src", block.src);
2288
- if (block.alt) el.setAttribute("alt", block.alt);
2289
- if (block.width) el.setAttribute("width", block.width);
2290
- if (block.height) el.setAttribute("height", block.height);
2291
- break;
2292
- case "docEmbed":
2293
- el.setAttribute("docId", block.docId);
2294
- for (const flag of [
2295
- "collapsed",
2296
- "tall",
2297
- "seamless"
2298
- ]) if (block.props[flag] === "true" || block.props[flag] === "1") el.setAttribute(flag, true);
2299
- break;
2300
- case "mathBlock":
2301
- el.setAttribute("expression", block.expression);
2302
- break;
2303
- case "fileBlock":
2304
- if (block.src) el.setAttribute("src", block.src);
2305
- if (block.mime) el.setAttribute("mime", block.mime);
2306
- if (block.uploadId) el.setAttribute("uploadId", block.uploadId);
2307
- if (block.filename) el.setAttribute("filename", block.filename);
2308
- break;
1939
+ if (current) current.lines.push(line);
1940
+ else if (!items.length && !current) current = {
1941
+ label: `Item 1`,
1942
+ icon: "",
1943
+ lines: [line]
1944
+ };
2309
1945
  }
1946
+ if (current) items.push(current);
1947
+ return items.map((item) => ({
1948
+ label: item.label,
1949
+ icon: item.icon,
1950
+ innerBlocks: parseBlocks(item.lines.join("\n"))
1951
+ }));
2310
1952
  }
1953
+ const TASK_RE = /^[-*+]\s+\[([ xX])\]\s+(.*)/;
2311
1954
  /**
2312
- * Parses markdown text and writes the result into a Y.XmlFragment that
2313
- * TipTap's Collaboration extension can read.
2314
- *
2315
- * Requires `fragment.doc` to be set (i.e. the fragment must already be
2316
- * obtained from a live Y.Doc via `ydoc.getXmlFragment('default')`).
1955
+ * Consume a list (bullet / ordered / task) starting at `start`. Indented
1956
+ * continuation lines and nested lists are captured into each item's
1957
+ * `innerBlocks` so the parse → serialise → parse cycle preserves tree
1958
+ * structure instead of flattening nested lists onto a single line.
2317
1959
  *
2318
- * @param fragment The target `Y.Doc.getXmlFragment('default')`
2319
- * @param markdown Raw markdown string
2320
- * @param fallbackTitle Used as the title when the markdown has no H1
1960
+ * `indent` is the column of the item marker for the current list. A
1961
+ * nested list starts ≥2 columns deeper. Lines with less indent than
1962
+ * `indent` belong to the outer block and stop consumption.
2321
1963
  */
2322
- function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled") {
2323
- const ydoc = fragment.doc;
2324
- if (!ydoc) {
2325
- console.warn("[markdownToYjs] fragment has no doc — skipping population");
2326
- return;
2327
- }
2328
- const fm = parseFrontmatter(markdown);
2329
- const blocks = parseBlocks(fm.body);
2330
- let title = fallbackTitle;
2331
- let titleSource;
2332
- if (fm.title !== void 0) {
2333
- title = fm.title;
2334
- titleSource = "frontmatter";
2335
- }
2336
- let contentBlocks = blocks;
2337
- const h1 = blocks.findIndex((b) => b.type === "heading" && b.level === 1);
2338
- if (h1 !== -1) {
2339
- title = blocks[h1].text;
2340
- contentBlocks = blocks.filter((_, i) => i !== h1);
2341
- titleSource = "h1";
2342
- }
2343
- ydoc.transact(() => {
2344
- const headerEl = new Y.XmlElement("documentHeader");
2345
- const metaEl = new Y.XmlElement("documentMeta");
2346
- const bodyEls = contentBlocks.map((b) => {
2347
- switch (b.type) {
2348
- case "heading": return new Y.XmlElement("heading");
2349
- case "paragraph": return new Y.XmlElement("paragraph");
2350
- case "bulletList": return new Y.XmlElement("bulletList");
2351
- case "orderedList": return new Y.XmlElement("orderedList");
2352
- case "taskList": return new Y.XmlElement("taskList");
2353
- case "codeBlock": return new Y.XmlElement("codeBlock");
2354
- case "blockquote": return new Y.XmlElement("blockquote");
2355
- case "table": return new Y.XmlElement("table");
2356
- case "hr": return new Y.XmlElement("horizontalRule");
2357
- case "callout": return new Y.XmlElement("callout");
2358
- case "collapsible": return new Y.XmlElement("collapsible");
2359
- case "steps": return new Y.XmlElement("steps");
2360
- case "card": return new Y.XmlElement("card");
2361
- case "cardGroup": return new Y.XmlElement("cardGroup");
2362
- case "codeCollapse": return new Y.XmlElement("codeCollapse");
2363
- case "codeGroup": return new Y.XmlElement("codeGroup");
2364
- case "codePreview": return new Y.XmlElement("codePreview");
2365
- case "codeTree": return new Y.XmlElement("codeTree");
2366
- case "accordion": return new Y.XmlElement("accordion");
2367
- case "tabs": return new Y.XmlElement("tabs");
2368
- case "field": return new Y.XmlElement("field");
2369
- case "fieldGroup": return new Y.XmlElement("fieldGroup");
2370
- case "image": return new Y.XmlElement("image");
2371
- case "docEmbed": return new Y.XmlElement("docEmbed");
2372
- case "mathBlock": return new Y.XmlElement("mathBlock");
2373
- case "fileBlock": return new Y.XmlElement("fileBlock");
1964
+ function consumeList(lines, start, indent, kind) {
1965
+ const items = [];
1966
+ let i = start;
1967
+ while (i < lines.length) {
1968
+ const line = lines[i];
1969
+ if (line.trim() === "") {
1970
+ let j = i + 1;
1971
+ while (j < lines.length && lines[j].trim() === "") j++;
1972
+ if (j >= lines.length) break;
1973
+ const lookahead = lines[j];
1974
+ if (leadingSpaces(lookahead) < indent) break;
1975
+ if (!matchMarker(lookahead.slice(indent), kind)) break;
1976
+ i = j;
1977
+ continue;
1978
+ }
1979
+ const leading = leadingSpaces(line);
1980
+ if (leading < indent) break;
1981
+ if (leading > indent) break;
1982
+ const m = matchMarker(line.slice(indent), kind);
1983
+ if (!m) break;
1984
+ const item = { text: m.text };
1985
+ if (kind === "task") item.checked = m.checked;
1986
+ i++;
1987
+ const contLines = [];
1988
+ let contMinIndent = Infinity;
1989
+ while (i < lines.length) {
1990
+ const next = lines[i];
1991
+ if (next.trim() === "") {
1992
+ let k = i + 1;
1993
+ while (k < lines.length && lines[k].trim() === "") k++;
1994
+ if (k >= lines.length) break;
1995
+ if (leadingSpaces(lines[k]) <= indent) break;
1996
+ contLines.push("");
1997
+ i++;
1998
+ continue;
2374
1999
  }
2375
- });
2376
- fragment.insert(0, [
2377
- headerEl,
2378
- metaEl,
2379
- ...bodyEls
2380
- ]);
2381
- if (titleSource) headerEl.setAttribute("titleSource", titleSource);
2382
- const headerXt = new Y.XmlText();
2383
- headerEl.insert(0, [headerXt]);
2384
- headerXt.insert(0, title);
2385
- for (const k of Object.keys(fm.meta)) {
2386
- const v = fm.meta[k];
2387
- if (v === void 0 || v === null) continue;
2388
- metaEl.setAttribute(k, v);
2000
+ const nextIndent = leadingSpaces(next);
2001
+ if (nextIndent <= indent) break;
2002
+ if (nextIndent < contMinIndent) contMinIndent = nextIndent;
2003
+ contLines.push(next);
2004
+ i++;
2389
2005
  }
2390
- if (fm.type) metaEl.setAttribute("type", fm.type);
2391
- contentBlocks.forEach((block, i) => fillBlock(bodyEls[i], block));
2392
- });
2393
- }
2394
-
2395
- //#endregion
2396
- //#region packages/convert/src/yjs-to-markdown.ts
2397
- function isXElem(n) {
2398
- return !!n && typeof n.nodeName === "string";
2006
+ if (contLines.length > 0) {
2007
+ const deindentBy = contMinIndent === Infinity ? 0 : contMinIndent;
2008
+ item.innerBlocks = parseBlocks(contLines.map((l) => l.slice(deindentBy)).join("\n"));
2009
+ }
2010
+ items.push(item);
2011
+ }
2012
+ return {
2013
+ items,
2014
+ next: i
2015
+ };
2399
2016
  }
2400
- function isXText(n) {
2401
- return !!n && typeof n.nodeName !== "string" && typeof n.toDelta === "function";
2017
+ function leadingSpaces(s) {
2018
+ let n = 0;
2019
+ while (n < s.length && s[n] === " ") n++;
2020
+ return n;
2402
2021
  }
2403
- function localizeFragment(fragment) {
2404
- return fragment;
2022
+ function matchMarker(s, kind) {
2023
+ if (kind === "task") {
2024
+ const m = s.match(TASK_RE);
2025
+ if (!m) return null;
2026
+ return {
2027
+ text: m[2],
2028
+ checked: m[1].toLowerCase() === "x"
2029
+ };
2030
+ }
2031
+ if (kind === "bullet") {
2032
+ if (TASK_RE.test(s)) return null;
2033
+ const m = s.match(/^[-*+]\s+(.*)$/);
2034
+ if (!m) return null;
2035
+ return {
2036
+ text: m[1],
2037
+ checked: false
2038
+ };
2039
+ }
2040
+ const m = s.match(/^\d+\.\s+(.*)$/);
2041
+ if (!m) return null;
2042
+ return {
2043
+ text: m[1],
2044
+ checked: false
2045
+ };
2405
2046
  }
2406
- function serializeDelta(delta) {
2407
- let result = "";
2408
- for (const op of delta) {
2409
- if (typeof op.insert !== "string") continue;
2410
- let text = op.insert;
2411
- const attrs = op.attributes ?? {};
2412
- if (attrs.code) {
2413
- result += `\`${text}\``;
2047
+ function parseBlocks(markdown) {
2048
+ const rawLines = markdown.split("\n");
2049
+ let firstContentLine = 0;
2050
+ while (firstContentLine < rawLines.length) {
2051
+ const l = rawLines[firstContentLine];
2052
+ if (l.trim() === "" || /^import\s/.test(l) || /^export\s/.test(l)) firstContentLine++;
2053
+ else break;
2054
+ }
2055
+ const stripped = rawLines.slice(firstContentLine).join("\n");
2056
+ const blocks = [];
2057
+ const lines = stripped.split("\n");
2058
+ let i = 0;
2059
+ while (i < lines.length) {
2060
+ const line = lines[i];
2061
+ const fenceBlockMatch = line.match(/^(`{3,})(.*)$/);
2062
+ if (fenceBlockMatch) {
2063
+ const fence = fenceBlockMatch[1];
2064
+ const lang = fenceBlockMatch[2].trim().replace(/\{[^}]*\}$/, "").replace(/\s*\[.*\]$/, "").trim();
2065
+ const codeLines = [];
2066
+ i++;
2067
+ while (i < lines.length && !lines[i].startsWith(fence)) {
2068
+ codeLines.push(lines[i]);
2069
+ i++;
2070
+ }
2071
+ i++;
2072
+ const code = codeLines.join("\n");
2073
+ if (lang === "math") blocks.push({
2074
+ type: "mathBlock",
2075
+ expression: code
2076
+ });
2077
+ else blocks.push({
2078
+ type: "codeBlock",
2079
+ lang,
2080
+ code
2081
+ });
2414
2082
  continue;
2415
2083
  }
2416
- if (attrs.badge) {
2417
- const b = attrs.badge;
2418
- const props = [];
2419
- if (b.color && b.color !== "neutral") props.push(`color="${b.color}"`);
2420
- if (b.variant && b.variant !== "subtle") props.push(`variant="${b.variant}"`);
2421
- result += `:badge[${b.label || text}]${props.length ? `{${props.join(" ")}}` : ""}`;
2084
+ const headingMatch = line.match(/^(#{1,6})\s+(.*)/);
2085
+ if (headingMatch) {
2086
+ blocks.push({
2087
+ type: "heading",
2088
+ level: headingMatch[1].length,
2089
+ text: headingMatch[2].trim()
2090
+ });
2091
+ i++;
2422
2092
  continue;
2423
2093
  }
2424
- if (attrs.proseIcon) {
2425
- const icon = attrs.proseIcon.name || "i-lucide-star";
2426
- result += `:icon{name="${icon}"}`;
2094
+ if (/^[-*_]{3,}\s*$/.test(line)) {
2095
+ blocks.push({ type: "hr" });
2096
+ i++;
2427
2097
  continue;
2428
2098
  }
2429
- if (attrs.kbd) {
2430
- const value = attrs.kbd.value || text;
2431
- result += `:kbd{value="${value}"}`;
2099
+ const embedMatch = line.match(/^!\[\[([0-9a-fA-F-]{36})(?:\|([^\]]+))?\]\](\{[^}]*\})?\s*$/);
2100
+ if (embedMatch) {
2101
+ const docId = embedMatch[1];
2102
+ const label = embedMatch[2] ?? "";
2103
+ const props = parseMdcProps(embedMatch[3]);
2104
+ blocks.push({
2105
+ type: "docEmbed",
2106
+ docId,
2107
+ label,
2108
+ props
2109
+ });
2110
+ i++;
2432
2111
  continue;
2433
2112
  }
2434
- if (attrs.docLink) {
2435
- const docId = attrs.docLink.docId;
2436
- if (docId) {
2437
- result += text === docId ? `[[${docId}]]` : `[[${docId}|${text}]]`;
2438
- continue;
2439
- }
2113
+ const imgMatch = line.match(/^!\[([^\]]*)\]\(([^)]+)\)(\{[^}]*\})?\s*$/);
2114
+ if (imgMatch) {
2115
+ const alt = imgMatch[1] ?? "";
2116
+ const src = imgMatch[2] ?? "";
2117
+ const attrs = parseMdcProps(imgMatch[3]);
2118
+ blocks.push({
2119
+ type: "image",
2120
+ src,
2121
+ alt,
2122
+ width: attrs["width"],
2123
+ height: attrs["height"]
2124
+ });
2125
+ i++;
2126
+ continue;
2440
2127
  }
2441
- if (attrs.mention) {
2442
- const { userId, label } = attrs.mention;
2443
- if (userId) {
2444
- result += `@[${label || text}](user:${userId})`;
2445
- continue;
2128
+ if (line.startsWith(">")) {
2129
+ const bqLines = [];
2130
+ while (i < lines.length && lines[i].startsWith(">")) {
2131
+ bqLines.push(lines[i].replace(/^>\s?/, ""));
2132
+ i++;
2446
2133
  }
2447
- }
2448
- if (attrs.mathInline) {
2449
- const expr = attrs.mathInline.expression ?? text;
2450
- result += `$${expr}$`;
2134
+ blocks.push({
2135
+ type: "blockquote",
2136
+ lines: bqLines
2137
+ });
2451
2138
  continue;
2452
2139
  }
2453
- if (attrs.bold) text = `**${text}**`;
2454
- if (attrs.italic) text = `*${text}*`;
2455
- if (attrs.strike) text = `~~${text}~~`;
2456
- if (attrs.link) {
2457
- const href = attrs.link.href ?? "";
2458
- text = `[${text}](${href})`;
2459
- }
2460
- result += text;
2461
- }
2462
- return result;
2463
- }
2464
- function serializeInline(el) {
2465
- const parts = [];
2466
- for (const child of el.toArray()) if (isXText(child)) parts.push(serializeDelta(child.toDelta()));
2467
- else if (isXElem(child)) if (child.nodeName === "docLink") {
2468
- const docId = child.getAttribute("docId") ?? "";
2469
- parts.push(`[[${docId}]]`);
2470
- } else parts.push(serializeInline(child));
2471
- return parts.join("");
2472
- }
2473
- function serializeBlock(el, indent = "") {
2474
- if (isXText(el)) return serializeDelta(el.toDelta());
2475
- switch (el.nodeName) {
2476
- case "documentHeader":
2477
- case "documentMeta": return "";
2478
- case "heading": {
2479
- const level = Number(el.getAttribute("level") ?? 2);
2480
- return `${"#".repeat(level)} ${serializeInline(el)}`;
2481
- }
2482
- case "paragraph": return serializeInline(el);
2483
- case "bulletList": return serializeListItems(el, "bullet", indent);
2484
- case "orderedList": return serializeListItems(el, "ordered", indent);
2485
- case "taskList": return serializeTaskList(el, indent);
2486
- case "codeBlock": {
2487
- const lang = el.getAttribute("language") ?? "";
2488
- const code = getCodeBlockText(el);
2489
- if (code === "") return `\`\`\`${lang}\n\`\`\``;
2490
- return `\`\`\`${lang}\n${code}\n\`\`\``;
2491
- }
2492
- case "blockquote": {
2493
- const lines = [];
2494
- for (const child of el.toArray()) if (isXElem(child)) {
2495
- const text = serializeBlock(child);
2496
- for (const line of text.split("\n")) lines.push(`> ${line}`);
2140
+ if (/^\s*\|/.test(line)) {
2141
+ const tableLines = [];
2142
+ while (i < lines.length && /^\s*\|/.test(lines[i])) {
2143
+ tableLines.push(lines[i]);
2144
+ i++;
2497
2145
  }
2498
- return lines.join("\n");
2499
- }
2500
- case "table": return serializeTable(el);
2501
- case "horizontalRule": return "---";
2502
- case "image": {
2503
- const src = el.getAttribute("src") ?? "";
2504
- const alt = el.getAttribute("alt") ?? "";
2505
- const width = el.getAttribute("width");
2506
- const height = el.getAttribute("height");
2507
- const attrs = [];
2508
- if (width) attrs.push(`width=${width}`);
2509
- if (height) attrs.push(`height=${height}`);
2510
- return `![${alt}](${src})${attrs.length ? `{${attrs.join(" ")}}` : ""}`;
2511
- }
2512
- case "docEmbed": {
2513
- const docId = el.getAttribute("docId") ?? "";
2514
- const collapsed = el.getAttribute("collapsed");
2515
- const tall = el.getAttribute("tall");
2516
- const seamless = el.getAttribute("seamless");
2517
- const flags = [];
2518
- if (collapsed === true || collapsed === "true") flags.push("collapsed");
2519
- if (tall === true || tall === "true") flags.push("tall");
2520
- if (seamless === true || seamless === "true") flags.push("seamless");
2521
- return `![[${docId}]]${flags.length ? `{${flags.join(" ")}}` : ""}`;
2522
- }
2523
- case "mathBlock": return `\`\`\`math\n${el.getAttribute("expression") ?? ""}\n\`\`\``;
2524
- case "fileBlock": {
2525
- const uploadId = el.getAttribute("uploadId") ?? "";
2526
- const filename = el.getAttribute("filename") ?? "";
2527
- const mime = el.getAttribute("mime") ?? "";
2528
- const src = el.getAttribute("src") ?? (uploadId && filename ? `.abracadabra/files/${uploadId}-${filename}` : "");
2529
- const props = [];
2530
- if (src) props.push(`src="${src}"`);
2531
- if (mime) props.push(`mime="${mime}"`);
2532
- if (uploadId) props.push(`upload-id="${uploadId}"`);
2533
- if (filename) props.push(`filename="${filename}"`);
2534
- return `:file{${props.join(" ")}}`;
2535
- }
2536
- case "callout": return `::${el.getAttribute("type") ?? "note"}\n${serializeChildren(el)}\n::`;
2537
- case "collapsible": {
2538
- const label = el.getAttribute("label") ?? "Details";
2539
- const open = el.getAttribute("open");
2540
- const props = [`label="${label}"`];
2541
- if (open === true || open === "true") props.push("open=\"true\"");
2542
- return `::collapsible{${props.join(" ")}}\n${serializeChildren(el)}\n::`;
2543
- }
2544
- case "steps": return `::steps\n${serializeChildren(el)}\n::`;
2545
- case "card": {
2546
- const props = [];
2547
- const title = el.getAttribute("title");
2548
- const icon = el.getAttribute("icon");
2549
- const to = el.getAttribute("to");
2550
- if (title) props.push(`title="${title}"`);
2551
- if (icon) props.push(`icon="${icon}"`);
2552
- if (to) props.push(`to="${to}"`);
2553
- return `::card${props.length ? `{${props.join(" ")}}` : ""}\n${serializeChildren(el)}\n::`;
2554
- }
2555
- case "cardGroup": return `::card-group\n${el.toArray().filter((c) => isXElem(c)).map((c) => serializeBlock(c)).join("\n\n")}\n::`;
2556
- case "codeCollapse": return `::code-collapse\n${el.toArray().filter((c) => isXElem(c) && c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n")}\n::`;
2557
- case "codeGroup": return `::code-group\n${el.toArray().filter((c) => isXElem(c) && c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n")}\n::`;
2558
- case "codePreview": {
2559
- const children = el.toArray().filter((c) => isXElem(c));
2560
- const nonCode = children.filter((c) => c.nodeName !== "codeBlock").map((c) => serializeBlock(c)).join("\n\n");
2561
- const code = children.filter((c) => c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n");
2562
- const parts = [nonCode];
2563
- if (code) parts.push(`#code\n${code}`);
2564
- return `::code-preview\n${parts.filter(Boolean).join("\n\n")}\n::`;
2565
- }
2566
- case "codeTree": return `::code-tree{files="${el.getAttribute("files") ?? "[]"}"}\n::`;
2567
- case "accordion": return serializeSlottedContainer(el, "accordion", "accordionItem", "item");
2568
- case "tabs": return serializeSlottedContainer(el, "tabs", "tabsItem", "tab");
2569
- case "field": {
2570
- const fieldName = el.getAttribute("name") ?? "";
2571
- const fieldType = el.getAttribute("type") ?? "string";
2572
- const required = el.getAttribute("required");
2573
- const props = [`name="${fieldName}"`, `type="${fieldType}"`];
2574
- if (required === true || required === "true") props.push("required=\"true\"");
2575
- return `::field{${props.join(" ")}}\n${serializeChildren(el)}\n::`;
2576
- }
2577
- case "fieldGroup": return `::field-group\n${el.toArray().filter((c) => isXElem(c)).map((c) => serializeBlock(c)).join("\n\n")}\n::`;
2578
- default: return serializeChildren(el);
2579
- }
2580
- }
2581
- function serializeChildren(el) {
2582
- const blocks = [];
2583
- for (const child of el.toArray()) if (isXElem(child)) {
2584
- const text = serializeBlock(child);
2585
- if (text) blocks.push(text);
2586
- } else if (isXText(child)) {
2587
- const text = serializeDelta(child.toDelta());
2588
- if (text) blocks.push(text);
2589
- }
2590
- return blocks.join("\n\n");
2591
- }
2592
- function serializeListItems(el, type, indent) {
2593
- const lines = [];
2594
- let counter = 1;
2595
- for (const child of el.toArray()) {
2596
- if (!isXElem(child) || child.nodeName !== "listItem") continue;
2597
- const prefix = type === "bullet" ? "- " : `${counter++}. `;
2598
- const subParts = [];
2599
- for (const sub of child.toArray()) {
2600
- if (!isXElem(sub)) continue;
2601
- if (sub.nodeName === "bulletList") subParts.push(serializeListItems(sub, "bullet", indent + " "));
2602
- else if (sub.nodeName === "orderedList") subParts.push(serializeListItems(sub, "ordered", indent + " "));
2603
- else subParts.push(serializeInline(sub));
2604
- }
2605
- if (subParts.length <= 1) lines.push(`${indent}${prefix}${subParts[0] ?? ""}`);
2606
- else {
2607
- lines.push(`${indent}${prefix}${subParts[0] ?? ""}`);
2608
- for (let i = 1; i < subParts.length; i++) lines.push(subParts[i]);
2146
+ if (tableLines.length >= 2 && isTableSeparator(tableLines[1])) {
2147
+ const headerRow = parseTableRow(tableLines[0]);
2148
+ const dataRows = tableLines.slice(2).filter((l) => !isTableSeparator(l)).map(parseTableRow);
2149
+ blocks.push({
2150
+ type: "table",
2151
+ headerRow,
2152
+ dataRows
2153
+ });
2154
+ } else for (const l of tableLines) blocks.push({
2155
+ type: "paragraph",
2156
+ text: l
2157
+ });
2158
+ continue;
2609
2159
  }
2610
- }
2611
- return lines.join("\n");
2612
- }
2613
- function serializeTaskList(el, indent) {
2614
- const lines = [];
2615
- for (const child of el.toArray()) {
2616
- if (!isXElem(child) || child.nodeName !== "taskItem") continue;
2617
- const checked = child.getAttribute("checked");
2618
- const marker = checked === true || checked === "true" ? "[x]" : "[ ]";
2619
- let header = "";
2620
- const nestedParts = [];
2621
- for (const sub of child.toArray()) {
2622
- if (!isXElem(sub)) continue;
2623
- if (sub.nodeName === "paragraph" && header === "") header = serializeInline(sub);
2624
- else if (sub.nodeName === "bulletList") nestedParts.push(serializeListItems(sub, "bullet", indent + " "));
2625
- else if (sub.nodeName === "orderedList") nestedParts.push(serializeListItems(sub, "ordered", indent + " "));
2626
- else if (sub.nodeName === "taskList") nestedParts.push(serializeTaskList(sub, indent + " "));
2627
- else nestedParts.push(indent + " " + serializeBlock(sub, indent + " "));
2160
+ const atomMatch = line.match(/^:(\w[\w-]*)(\{[^}]*\})?\s*$/);
2161
+ if (atomMatch && atomMatch[1] === "file") {
2162
+ const props = parseMdcProps(atomMatch[2]);
2163
+ const uploadId = props["upload-id"] ?? props["uploadId"] ?? "";
2164
+ const filename = props["filename"] ?? "";
2165
+ const mime = props["mime"] ?? "";
2166
+ const src = props["src"] ?? (uploadId && filename ? `.abracadabra/files/${uploadId}-${filename}` : "");
2167
+ blocks.push({
2168
+ type: "fileBlock",
2169
+ src,
2170
+ mime,
2171
+ uploadId,
2172
+ filename
2173
+ });
2174
+ i++;
2175
+ continue;
2628
2176
  }
2629
- lines.push(`${indent}- ${marker} ${header}`);
2630
- for (const part of nestedParts) lines.push(part);
2631
- }
2632
- return lines.join("\n");
2633
- }
2634
- function getCodeBlockText(el) {
2635
- for (const child of el.toArray()) if (isXText(child)) return child.toString();
2636
- return "";
2637
- }
2638
- function serializeTable(el) {
2639
- const rows = el.toArray().filter((c) => isXElem(c));
2640
- if (!rows.length) return "";
2641
- const serializedRows = [];
2642
- for (const row of rows) {
2643
- const cells = row.toArray().filter((c) => isXElem(c)).map((cell) => {
2644
- return cell.toArray().filter((c) => isXElem(c)).map((c) => serializeInline(c)).join(" ");
2645
- });
2646
- serializedRows.push(cells);
2647
- }
2648
- if (!serializedRows.length) return "";
2649
- const colCount = Math.max(...serializedRows.map((r) => r.length));
2650
- const headerRow = serializedRows[0];
2651
- const separator = Array(colCount).fill("---");
2652
- const dataRows = serializedRows.slice(1);
2653
- const formatRow = (cells) => {
2654
- return `| ${Array(colCount).fill("").map((_, i) => cells[i] ?? "").join(" | ")} |`;
2655
- };
2656
- return [
2657
- formatRow(headerRow),
2658
- formatRow(separator),
2659
- ...dataRows.map(formatRow)
2660
- ].join("\n");
2661
- }
2662
- function serializeSlottedContainer(el, containerName, childName, slotPrefix) {
2663
- return `::${containerName}\n${el.toArray().filter((c) => isXElem(c) && c.nodeName === childName).map((item) => {
2664
- const label = item.getAttribute("label") ?? "";
2665
- const icon = item.getAttribute("icon") ?? "";
2666
- const props = [];
2667
- if (label) props.push(`label="${label}"`);
2668
- if (icon) props.push(`icon="${icon}"`);
2669
- const content = serializeChildren(item);
2670
- return `#${slotPrefix}{${props.join(" ")}}\n${content}`;
2671
- }).join("\n\n")}\n::`;
2672
- }
2673
- function generateFrontmatter(label, meta, type) {
2674
- const lines = [];
2675
- if (label !== void 0) lines.push(`title: "${escapeYaml(label)}"`);
2676
- if (type && type !== "doc") lines.push(`type: ${type}`);
2677
- if (!meta) return `---\n${lines.join("\n")}\n---`;
2678
- if (meta.tags?.length) lines.push(`tags: [${meta.tags.join(", ")}]`);
2679
- if (meta.color) lines.push(`color: ${yamlScalar(meta.color)}`);
2680
- if (meta.icon) lines.push(`icon: ${yamlScalar(meta.icon)}`);
2681
- if (meta.status) lines.push(`status: ${yamlScalar(meta.status)}`);
2682
- if (meta.priority !== void 0 && meta.priority !== 0) lines.push(`priority: ${{
2683
- 1: "low",
2684
- 2: "medium",
2685
- 3: "high",
2686
- 4: "urgent"
2687
- }[meta.priority] ?? meta.priority}`);
2688
- if (meta.checked !== void 0) lines.push(`checked: ${meta.checked}`);
2689
- if (meta.language) lines.push(`language: ${yamlScalar(meta.language)}`);
2690
- if (meta.fileExtension) lines.push(`fileExtension: ${yamlScalar(meta.fileExtension)}`);
2691
- if (meta.codeTheme) lines.push(`codeTheme: ${yamlScalar(meta.codeTheme)}`);
2692
- if (meta.dateStart) lines.push(`dateStart: "${escapeYaml(meta.dateStart)}"`);
2693
- if (meta.dateEnd) lines.push(`dateEnd: "${escapeYaml(meta.dateEnd)}"`);
2694
- if (meta.subtitle) lines.push(`subtitle: "${escapeYaml(meta.subtitle)}"`);
2695
- if (meta.url) lines.push(`url: ${meta.url}`);
2696
- if (meta.rating !== void 0 && meta.rating !== 0) lines.push(`rating: ${meta.rating}`);
2697
- return `---\n${lines.join("\n")}\n---`;
2698
- }
2699
- /**
2700
- * Render a YAML scalar — bare when safe, double-quoted when the value
2701
- * needs escaping. YAML treats `#`, `:`, leading whitespace, and a few
2702
- * other characters as syntactically significant, so anything starting
2703
- * with one of those gets quoted to stay round-trip safe.
2704
- */
2705
- function yamlScalar(s) {
2706
- if (s === "") return "\"\"";
2707
- if (/^[#&*!|>%@`]/.test(s)) return `"${escapeYaml(s)}"`;
2708
- if (/[:"]/.test(s)) return `"${escapeYaml(s)}"`;
2709
- if (/^\s|\s$/.test(s)) return `"${escapeYaml(s)}"`;
2710
- return s;
2711
- }
2712
- function escapeYaml(s) {
2713
- return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
2714
- }
2715
- function yjsToMarkdown(fragment, label, meta, type) {
2716
- fragment = localizeFragment(fragment);
2717
- const { text: headerText, source: titleSource } = readDocumentHeader(fragment);
2718
- const effectiveTitle = headerText || label;
2719
- const docMeta = readDocumentMeta(fragment);
2720
- const effectiveMeta = meta ?? docMeta.meta;
2721
- const effectiveType = type ?? docMeta.type;
2722
- const metaIsEmpty = isMetaEmpty(effectiveMeta);
2723
- const typeIsDefault = !effectiveType || effectiveType === "doc";
2724
- const bodyBlocks = collectBodyBlocks(fragment);
2725
- let body;
2726
- if (titleSource === "h1" && effectiveTitle) {
2727
- const tail = serializeBlocksClean(bodyBlocks);
2728
- body = tail === "" ? `# ${effectiveTitle}` : `# ${effectiveTitle}\n\n${tail}`;
2729
- } else body = serializeBlocksClean(bodyBlocks);
2730
- const wantFrontmatterTitle = titleSource === "frontmatter";
2731
- if (!wantFrontmatterTitle && !(!metaIsEmpty || !typeIsDefault)) return body === "" ? "" : `${body}\n`;
2732
- const frontmatter = generateFrontmatter(wantFrontmatterTitle ? effectiveTitle : void 0, effectiveMeta, effectiveType);
2733
- if (body === "") return `${frontmatter}\n`;
2734
- return `${frontmatter}\n\n${body}\n`;
2735
- }
2736
- function readDocumentMeta(fragment) {
2737
- const meta = {};
2738
- let type;
2739
- for (const child of fragment.toArray()) {
2740
- if (!isXElem(child) || child.nodeName !== "documentMeta") continue;
2741
- const attrs = child.getAttributes();
2742
- for (const k of Object.keys(attrs)) {
2743
- const v = attrs[k];
2744
- if (v === void 0 || v === null) continue;
2745
- if (k === "type" && typeof v === "string") {
2746
- type = v;
2177
+ const MDC_OPEN = /^\s*(:{2,})(\w[\w-]*)(\{[^}]*\})?\s*$/;
2178
+ if (MDC_OPEN.test(line)) {
2179
+ const colons = line.match(/^\s*(:+)/)?.[1]?.length ?? 2;
2180
+ const componentName = line.match(/^\s*:{2,}(\w[\w-]*)/)?.[1] ?? "";
2181
+ const innerLines = [];
2182
+ i++;
2183
+ while (i < lines.length) {
2184
+ const l = lines[i];
2185
+ if (new RegExp(`^\\s*:{${colons}}\\s*$`).test(l)) {
2186
+ i++;
2187
+ break;
2188
+ }
2189
+ const innerFence = l.match(/^(\s*`{3,})/);
2190
+ if (innerFence) {
2191
+ const fenceStr = innerFence[1].trimStart();
2192
+ innerLines.push(l);
2193
+ i++;
2194
+ while (i < lines.length && !lines[i].trimStart().startsWith(fenceStr)) {
2195
+ innerLines.push(lines[i]);
2196
+ i++;
2197
+ }
2198
+ if (i < lines.length) {
2199
+ innerLines.push(lines[i]);
2200
+ i++;
2201
+ }
2202
+ continue;
2203
+ }
2204
+ innerLines.push(l);
2205
+ i++;
2206
+ }
2207
+ const nonBlank = innerLines.filter((l) => l.trim().length > 0);
2208
+ if (nonBlank.length) {
2209
+ const minIndent = Math.min(...nonBlank.map((l) => l.match(/^(\s*)/)?.[1]?.length ?? 0));
2210
+ if (minIndent > 0) for (let j = 0; j < innerLines.length; j++) innerLines[j] = innerLines[j].slice(Math.min(minIndent, innerLines[j].length));
2211
+ }
2212
+ let contentStart = 0;
2213
+ if (innerLines[0]?.trim() === "---") {
2214
+ const fmEnd = innerLines.findIndex((l, idx) => idx > 0 && l.trim() === "---");
2215
+ if (fmEnd !== -1) contentStart = fmEnd + 1;
2216
+ }
2217
+ const contentLines = innerLines.slice(contentStart);
2218
+ const defaultSlotLines = [];
2219
+ const codeSlotLines = [];
2220
+ let currentSlot = "default";
2221
+ for (const l of contentLines) {
2222
+ if (/^#code\s*$/.test(l)) {
2223
+ currentSlot = "code";
2224
+ continue;
2225
+ }
2226
+ if (/^#\w+/.test(l) && !/^#{2,}\s/.test(l)) {
2227
+ currentSlot = "other";
2228
+ continue;
2229
+ }
2230
+ if (currentSlot === "default") defaultSlotLines.push(l);
2231
+ else if (currentSlot === "code") codeSlotLines.push(l);
2232
+ }
2233
+ const innerBlocks = parseBlocks(defaultSlotLines.join("\n"));
2234
+ const codeBlocks = extractFencedCode(codeSlotLines);
2235
+ if (new Set([
2236
+ "tip",
2237
+ "note",
2238
+ "info",
2239
+ "warning",
2240
+ "caution",
2241
+ "danger",
2242
+ "callout",
2243
+ "alert"
2244
+ ]).has(componentName.toLowerCase())) blocks.push({
2245
+ type: "callout",
2246
+ calloutType: componentName.toLowerCase(),
2247
+ innerBlocks
2248
+ });
2249
+ else {
2250
+ const mdcProps = parseMdcProps(line.match(MDC_OPEN)?.[3]);
2251
+ const lc = componentName.toLowerCase();
2252
+ if (lc === "collapsible") blocks.push({
2253
+ type: "collapsible",
2254
+ label: mdcProps["label"] || "Details",
2255
+ open: mdcProps["open"] === "true",
2256
+ innerBlocks
2257
+ });
2258
+ else if (lc === "steps") blocks.push({
2259
+ type: "steps",
2260
+ innerBlocks
2261
+ });
2262
+ else if (lc === "card") blocks.push({
2263
+ type: "card",
2264
+ title: mdcProps["title"] || "",
2265
+ icon: mdcProps["icon"] || "",
2266
+ to: mdcProps["to"] || "",
2267
+ innerBlocks
2268
+ });
2269
+ else if (lc === "card-group") {
2270
+ const cards = innerBlocks.filter((b) => b.type === "card");
2271
+ if (cards.length) blocks.push({
2272
+ type: "cardGroup",
2273
+ cards
2274
+ });
2275
+ else blocks.push(...innerBlocks);
2276
+ } else if (lc === "code-collapse") blocks.push({
2277
+ type: "codeCollapse",
2278
+ codeBlocks: codeBlocks.length ? codeBlocks : innerBlocks.filter((b) => b.type === "codeBlock")
2279
+ });
2280
+ else if (lc === "code-group") {
2281
+ const allCode = [...innerBlocks.filter((b) => b.type === "codeBlock"), ...codeBlocks];
2282
+ blocks.push({
2283
+ type: "codeGroup",
2284
+ codeBlocks: allCode
2285
+ });
2286
+ } else if (lc === "code-preview") blocks.push({
2287
+ type: "codePreview",
2288
+ innerBlocks,
2289
+ codeBlocks
2290
+ });
2291
+ else if (lc === "code-tree") blocks.push({
2292
+ type: "codeTree",
2293
+ files: mdcProps["files"] || "[]"
2294
+ });
2295
+ else if (lc === "accordion") {
2296
+ const items = parseMdcChildren(contentLines, "item");
2297
+ if (items.length) blocks.push({
2298
+ type: "accordion",
2299
+ items
2300
+ });
2301
+ else blocks.push({
2302
+ type: "accordion",
2303
+ items: [{
2304
+ label: "Item 1",
2305
+ icon: "",
2306
+ innerBlocks
2307
+ }]
2308
+ });
2309
+ } else if (lc === "tabs") {
2310
+ const items = parseMdcChildren(contentLines, "tab");
2311
+ if (items.length) blocks.push({
2312
+ type: "tabs",
2313
+ items
2314
+ });
2315
+ else blocks.push({
2316
+ type: "tabs",
2317
+ items: [{
2318
+ label: "Tab 1",
2319
+ icon: "",
2320
+ innerBlocks
2321
+ }]
2322
+ });
2323
+ } else if (lc === "field") blocks.push({
2324
+ type: "field",
2325
+ name: mdcProps["name"] || "",
2326
+ fieldType: mdcProps["type"] || "string",
2327
+ required: mdcProps["required"] === "true",
2328
+ innerBlocks
2329
+ });
2330
+ else if (lc === "field-group") {
2331
+ const fields = innerBlocks.filter((b) => b.type === "field");
2332
+ if (fields.length) blocks.push({
2333
+ type: "fieldGroup",
2334
+ fields
2335
+ });
2336
+ else blocks.push(...innerBlocks);
2337
+ } else {
2338
+ blocks.push(...innerBlocks);
2339
+ blocks.push(...codeBlocks);
2340
+ }
2341
+ }
2342
+ continue;
2343
+ }
2344
+ if (TASK_RE.test(line)) {
2345
+ const { items, next } = consumeList(lines, i, 0, "task");
2346
+ i = next;
2347
+ blocks.push({
2348
+ type: "taskList",
2349
+ items
2350
+ });
2351
+ continue;
2352
+ }
2353
+ if (/^[-*+]\s+/.test(line)) {
2354
+ const { items, next } = consumeList(lines, i, 0, "bullet");
2355
+ if (items.length > 0) {
2356
+ i = next;
2357
+ blocks.push({
2358
+ type: "bulletList",
2359
+ items
2360
+ });
2747
2361
  continue;
2748
2362
  }
2749
- meta[k] = v;
2750
2363
  }
2751
- break;
2752
- }
2753
- return {
2754
- meta,
2755
- type
2756
- };
2757
- }
2758
- function readDocumentHeader(fragment) {
2759
- for (const child of fragment.toArray()) {
2760
- if (!isXElem(child) || child.nodeName !== "documentHeader") continue;
2761
- const text = child.toArray().find((c) => isXText(c));
2762
- const src = child.getAttribute("titleSource");
2763
- const source = src === "h1" || src === "frontmatter" ? src : void 0;
2764
- return {
2765
- text: text ? text.toString() : "",
2766
- source
2767
- };
2768
- }
2769
- return { text: "" };
2770
- }
2771
- function collectBodyBlocks(fragment) {
2772
- const out = [];
2773
- for (const child of fragment.toArray()) {
2774
- if (!isXElem(child)) continue;
2775
- if (child.nodeName === "documentHeader" || child.nodeName === "documentMeta") continue;
2776
- out.push(child);
2777
- }
2778
- return out;
2779
- }
2780
- function serializeBlocksClean(blocks) {
2781
- const parts = [];
2782
- for (const block of blocks) {
2783
- if (block.nodeName === "paragraph" && block.length === 0) {
2784
- parts.push("");
2364
+ if (/^\d+\.\s+/.test(line)) {
2365
+ const { items, next } = consumeList(lines, i, 0, "ordered");
2366
+ if (items.length > 0) {
2367
+ i = next;
2368
+ blocks.push({
2369
+ type: "orderedList",
2370
+ items
2371
+ });
2372
+ continue;
2373
+ }
2374
+ }
2375
+ if (line.trim() === "") {
2376
+ i++;
2785
2377
  continue;
2786
2378
  }
2787
- parts.push(serializeBlock(block));
2788
- }
2789
- while (parts.length && parts[parts.length - 1] === "") parts.pop();
2790
- return parts.join("\n\n");
2791
- }
2792
- function isMetaEmpty(meta) {
2793
- if (!meta) return true;
2794
- for (const key of Object.keys(meta)) {
2795
- const v = meta[key];
2796
- if (v === void 0 || v === null) continue;
2797
- if (typeof v === "string" && v === "") continue;
2798
- if (Array.isArray(v) && v.length === 0) continue;
2799
- return false;
2800
- }
2801
- return true;
2802
- }
2803
-
2804
- //#endregion
2805
- //#region packages/convert/src/spec/nodes.ts
2806
- const BOOL_FALSE_DEFAULT = {
2807
- key: "",
2808
- type: "boolean",
2809
- default: false,
2810
- optional: true
2811
- };
2812
- const bool = (key) => ({
2813
- ...BOOL_FALSE_DEFAULT,
2814
- key
2815
- });
2816
- const str = (key, def) => ({
2817
- key,
2818
- type: "string",
2819
- default: def,
2820
- optional: true
2821
- });
2822
- const num = (key) => ({
2823
- key,
2824
- type: "number",
2825
- optional: true
2826
- });
2827
- const int = (key) => ({
2828
- key,
2829
- type: "integer",
2830
- optional: true
2831
- });
2832
- const VANILLA_BLOCKS = [
2833
- {
2834
- name: "documentHeader",
2835
- group: "block",
2836
- wire: "special",
2837
- doc: "Holds the title; hoisted to frontmatter on serialise."
2838
- },
2839
- {
2840
- name: "documentMeta",
2841
- group: "block",
2842
- wire: "special",
2843
- doc: "Holds page-level meta; serialised into frontmatter."
2844
- },
2845
- {
2846
- name: "paragraph",
2847
- group: "block",
2848
- wire: "vanilla",
2849
- contentBearing: true
2850
- },
2851
- {
2852
- name: "heading",
2853
- group: "block",
2854
- wire: "vanilla",
2855
- attrs: [int("level")],
2856
- contentBearing: true
2857
- },
2858
- {
2859
- name: "blockquote",
2860
- group: "block",
2861
- wire: "vanilla",
2862
- contentBearing: true
2863
- },
2864
- {
2865
- name: "codeBlock",
2866
- group: "block",
2867
- wire: "fence",
2868
- attrs: [str("language", "")]
2869
- },
2870
- {
2871
- name: "bulletList",
2872
- group: "block",
2873
- wire: "vanilla"
2874
- },
2875
- {
2876
- name: "orderedList",
2877
- group: "block",
2878
- wire: "vanilla"
2879
- },
2880
- {
2881
- name: "listItem",
2882
- group: "block",
2883
- wire: "vanilla",
2884
- contentBearing: true
2885
- },
2886
- {
2887
- name: "taskList",
2888
- group: "block",
2889
- wire: "vanilla"
2890
- },
2891
- {
2892
- name: "taskItem",
2893
- group: "block",
2894
- wire: "vanilla",
2895
- attrs: [bool("checked")],
2896
- contentBearing: true
2897
- },
2898
- {
2899
- name: "table",
2900
- group: "block",
2901
- wire: "vanilla"
2902
- },
2903
- {
2904
- name: "tableRow",
2905
- group: "block",
2906
- wire: "vanilla"
2907
- },
2908
- {
2909
- name: "tableHeader",
2910
- group: "block",
2911
- wire: "vanilla",
2912
- contentBearing: true
2913
- },
2914
- {
2915
- name: "tableCell",
2916
- group: "block",
2917
- wire: "vanilla",
2918
- contentBearing: true
2919
- },
2920
- {
2921
- name: "horizontalRule",
2922
- group: "block",
2923
- wire: "vanilla"
2924
- },
2925
- {
2926
- name: "image",
2927
- group: "block",
2928
- wire: "special",
2929
- attrs: [
2930
- str("src"),
2931
- str("alt", ""),
2932
- int("width"),
2933
- int("height")
2934
- ]
2935
- },
2936
- {
2937
- name: "hardBreak",
2938
- group: "inline",
2939
- wire: "vanilla"
2379
+ const paraLines = [];
2380
+ while (i < lines.length && lines[i].trim() !== "" && !/^(#{1,6}\s|[-*+]\s|\d+\.\s|>|`{3,}|\s*\||[-*_]{3,}\s*$|\s*:{2,}\w)/.test(lines[i])) {
2381
+ paraLines.push(lines[i]);
2382
+ i++;
2383
+ }
2384
+ if (paraLines.length) blocks.push({
2385
+ type: "paragraph",
2386
+ text: paraLines.join(" ")
2387
+ });
2388
+ else {
2389
+ blocks.push({
2390
+ type: "paragraph",
2391
+ text: line
2392
+ });
2393
+ i++;
2394
+ }
2940
2395
  }
2941
- ];
2942
- const MDC_CONTAINERS = [
2943
- {
2944
- name: "callout",
2945
- group: "block",
2946
- wire: "mdc-container",
2947
- attrs: [
2948
- {
2949
- key: "type",
2950
- type: "string",
2951
- default: "note",
2952
- optional: true,
2953
- values: [
2954
- "note",
2955
- "tip",
2956
- "warning",
2957
- "danger",
2958
- "info",
2959
- "caution",
2960
- "alert",
2961
- "success",
2962
- "error"
2963
- ]
2964
- },
2965
- str("title"),
2966
- str("icon")
2967
- ]
2968
- },
2969
- {
2970
- name: "collapsible",
2971
- group: "block",
2972
- wire: "mdc-container",
2973
- attrs: [str("label", "Details"), bool("open")]
2974
- },
2975
- {
2976
- name: "accordion",
2977
- group: "block",
2978
- wire: "mdc-slotted",
2979
- slotChild: "accordionItem"
2980
- },
2981
- {
2982
- name: "accordionItem",
2983
- group: "block",
2984
- wire: "mdc-container",
2985
- mdcTag: "accordion-item",
2986
- attrs: [str("label", "Item"), str("icon")]
2987
- },
2988
- {
2989
- name: "tabs",
2990
- group: "block",
2991
- wire: "mdc-slotted",
2992
- slotChild: "tabsItem"
2993
- },
2994
- {
2995
- name: "tabsItem",
2996
- group: "block",
2997
- wire: "mdc-container",
2998
- mdcTag: "tabs-item",
2999
- attrs: [str("label"), str("icon")]
3000
- },
3001
- {
3002
- name: "steps",
3003
- group: "block",
3004
- wire: "mdc-container"
3005
- },
3006
- {
3007
- name: "card",
3008
- group: "block",
3009
- wire: "mdc-container",
3010
- attrs: [
3011
- str("title"),
3012
- str("icon"),
3013
- str("to")
3014
- ]
3015
- },
3016
- {
3017
- name: "cardGroup",
3018
- group: "block",
3019
- wire: "mdc-slotted",
3020
- mdcTag: "card-group",
3021
- slotChild: "card"
3022
- },
3023
- {
3024
- name: "field",
3025
- group: "block",
3026
- wire: "mdc-container",
3027
- attrs: [
3028
- str("name"),
3029
- str("type", "string"),
3030
- bool("required")
3031
- ]
3032
- },
3033
- {
3034
- name: "fieldGroup",
3035
- group: "block",
3036
- wire: "mdc-slotted",
3037
- mdcTag: "field-group",
3038
- slotChild: "field"
3039
- },
3040
- {
3041
- name: "codeGroup",
3042
- group: "block",
3043
- wire: "mdc-slotted",
3044
- mdcTag: "code-group",
3045
- slotChild: "codeBlock"
3046
- },
3047
- {
3048
- name: "codeCollapse",
3049
- group: "block",
3050
- wire: "mdc-container",
3051
- mdcTag: "code-collapse"
3052
- },
3053
- {
3054
- name: "codePreview",
3055
- group: "block",
3056
- wire: "mdc-container",
3057
- mdcTag: "code-preview"
3058
- },
3059
- {
3060
- name: "codeTree",
3061
- group: "block",
3062
- wire: "mdc-atom-block",
3063
- mdcTag: "code-tree",
3064
- attrs: [{
3065
- key: "files",
3066
- type: "json"
3067
- }]
3068
- },
3069
- {
3070
- name: "figure",
3071
- group: "block",
3072
- wire: "mdc-container",
3073
- attrs: [
3074
- str("src"),
3075
- str("alt", ""),
3076
- str("caption")
3077
- ]
3078
- },
3079
- {
3080
- name: "video",
3081
- group: "block",
3082
- wire: "mdc-atom-block",
3083
- attrs: [
3084
- str("src"),
3085
- str("poster"),
3086
- bool("autoplay"),
3087
- bool("loop"),
3088
- bool("controls")
3089
- ]
3090
- },
3091
- {
3092
- name: "embed",
3093
- group: "block",
3094
- wire: "mdc-atom-block",
3095
- attrs: [str("src"), str("title")]
3096
- },
3097
- {
3098
- name: "svgEmbed",
3099
- group: "block",
3100
- wire: "fence",
3101
- attrs: [str("title")],
3102
- mdcTag: "svg",
3103
- doc: "Serialised as a ```svg fenced block; the SVG markup is the body."
3104
- },
3105
- {
3106
- name: "divider",
3107
- group: "block",
3108
- wire: "mdc-atom-block",
3109
- attrs: [str("label"), str("icon")]
3110
- },
3111
- {
3112
- name: "quote",
3113
- group: "block",
3114
- wire: "mdc-container",
3115
- attrs: [str("cite")]
3116
- },
3117
- {
3118
- name: "progress",
3119
- group: "block",
3120
- wire: "mdc-atom-block",
3121
- attrs: [
3122
- num("value"),
3123
- num("max"),
3124
- str("label")
3125
- ]
3126
- },
3127
- {
3128
- name: "spoiler",
3129
- group: "block",
3130
- wire: "mdc-container",
3131
- attrs: [str("label")]
3132
- },
3133
- {
3134
- name: "colorSwatch",
3135
- group: "block",
3136
- wire: "mdc-atom-block",
3137
- mdcTag: "color-swatch",
3138
- attrs: [str("color"), str("label")]
3139
- },
3140
- {
3141
- name: "stat",
3142
- group: "block",
3143
- wire: "mdc-container",
3144
- attrs: [
3145
- str("label"),
3146
- str("value"),
3147
- str("icon")
3148
- ]
3149
- },
3150
- {
3151
- name: "statGroup",
3152
- group: "block",
3153
- wire: "mdc-slotted",
3154
- mdcTag: "stat-group",
3155
- slotChild: "stat"
3156
- },
3157
- {
3158
- name: "button",
3159
- group: "block",
3160
- wire: "mdc-atom-block",
3161
- attrs: [
3162
- str("label"),
3163
- str("to"),
3164
- str("icon"),
3165
- str("variant")
3166
- ]
3167
- },
2396
+ return blocks;
2397
+ }
2398
+ /**
2399
+ * Insert formatted inline tokens into an already-attached Y.XmlElement.
2400
+ * Creates one Y.XmlText per token (attach first, fill second).
2401
+ */
2402
+ function fillTextInto(el, tokens) {
2403
+ const filtered = tokens.filter((t) => t.text.length > 0);
2404
+ if (!filtered.length) return;
2405
+ const children = filtered.map((tok) => {
2406
+ return (tok.attrs?.docLink)?.docId ? new Y.XmlElement("docLink") : new Y.XmlText();
2407
+ });
2408
+ el.insert(0, children);
2409
+ filtered.forEach((tok, i) => {
2410
+ const node = children[i];
2411
+ if (node instanceof Y.XmlElement) {
2412
+ const dl = tok.attrs.docLink;
2413
+ node.setAttribute("docId", dl.docId);
2414
+ return;
2415
+ }
2416
+ if (tok.attrs) node.insert(0, tok.text, tok.attrs);
2417
+ else node.insert(0, tok.text);
2418
+ });
2419
+ }
2420
+ function blockElName(b) {
2421
+ switch (b.type) {
2422
+ case "heading": return "heading";
2423
+ case "paragraph": return "paragraph";
2424
+ case "bulletList": return "bulletList";
2425
+ case "orderedList": return "orderedList";
2426
+ case "taskList": return "taskList";
2427
+ case "codeBlock": return "codeBlock";
2428
+ case "blockquote": return "blockquote";
2429
+ case "table": return "table";
2430
+ case "hr": return "horizontalRule";
2431
+ case "callout": return "callout";
2432
+ case "collapsible": return "collapsible";
2433
+ case "steps": return "steps";
2434
+ case "card": return "card";
2435
+ case "cardGroup": return "cardGroup";
2436
+ case "codeCollapse": return "codeCollapse";
2437
+ case "codeGroup": return "codeGroup";
2438
+ case "codePreview": return "codePreview";
2439
+ case "codeTree": return "codeTree";
2440
+ case "accordion": return "accordion";
2441
+ case "tabs": return "tabs";
2442
+ case "field": return "field";
2443
+ case "fieldGroup": return "fieldGroup";
2444
+ case "image": return "image";
2445
+ case "docEmbed": return "docEmbed";
2446
+ case "mathBlock": return "mathBlock";
2447
+ case "fileBlock": return "fileBlock";
2448
+ }
2449
+ }
2450
+ function populateListItemChildren(itemEl, item, _itemKind) {
2451
+ const paraEl = new Y.XmlElement("paragraph");
2452
+ itemEl.insert(itemEl.length, [paraEl]);
2453
+ fillTextInto(paraEl, parseInline(item.text));
2454
+ if (!item.innerBlocks?.length) return;
2455
+ const innerEls = item.innerBlocks.map((b) => new Y.XmlElement(blockElName(b)));
2456
+ itemEl.insert(itemEl.length, innerEls);
2457
+ item.innerBlocks.forEach((b, i) => fillBlock(innerEls[i], b));
2458
+ }
2459
+ function fillBlock(el, block) {
2460
+ switch (block.type) {
2461
+ case "heading":
2462
+ el.setAttribute("level", block.level);
2463
+ fillTextInto(el, parseInline(block.text));
2464
+ break;
2465
+ case "paragraph":
2466
+ fillTextInto(el, parseInline(block.text));
2467
+ break;
2468
+ case "bulletList":
2469
+ case "orderedList": {
2470
+ const listItemEls = block.items.map(() => new Y.XmlElement("listItem"));
2471
+ el.insert(0, listItemEls);
2472
+ block.items.forEach((item, i) => {
2473
+ populateListItemChildren(listItemEls[i], item, "listItem");
2474
+ });
2475
+ break;
2476
+ }
2477
+ case "taskList": {
2478
+ const taskItemEls = block.items.map(() => new Y.XmlElement("taskItem"));
2479
+ el.insert(0, taskItemEls);
2480
+ block.items.forEach((item, i) => {
2481
+ taskItemEls[i].setAttribute("checked", !!item.checked);
2482
+ populateListItemChildren(taskItemEls[i], item, "taskItem");
2483
+ });
2484
+ break;
2485
+ }
2486
+ case "codeBlock": {
2487
+ if (block.lang) el.setAttribute("language", block.lang);
2488
+ const xt = new Y.XmlText();
2489
+ el.insert(0, [xt]);
2490
+ xt.insert(0, block.code);
2491
+ break;
2492
+ }
2493
+ case "blockquote": {
2494
+ const paraEls = block.lines.map(() => new Y.XmlElement("paragraph"));
2495
+ el.insert(0, paraEls);
2496
+ block.lines.forEach((line, i) => fillTextInto(paraEls[i], parseInline(line)));
2497
+ break;
2498
+ }
2499
+ case "table": {
2500
+ const headerRowEl = new Y.XmlElement("tableRow");
2501
+ const dataRowEls = block.dataRows.map(() => new Y.XmlElement("tableRow"));
2502
+ el.insert(0, [headerRowEl, ...dataRowEls]);
2503
+ const headerCellEls = block.headerRow.map(() => new Y.XmlElement("tableHeader"));
2504
+ headerRowEl.insert(0, headerCellEls);
2505
+ block.headerRow.forEach((cellText, i) => {
2506
+ const paraEl = new Y.XmlElement("paragraph");
2507
+ headerCellEls[i].insert(0, [paraEl]);
2508
+ fillTextInto(paraEl, parseInline(cellText));
2509
+ });
2510
+ block.dataRows.forEach((row, ri) => {
2511
+ const cellEls = row.map(() => new Y.XmlElement("tableCell"));
2512
+ dataRowEls[ri].insert(0, cellEls);
2513
+ row.forEach((cellText, ci) => {
2514
+ const paraEl = new Y.XmlElement("paragraph");
2515
+ cellEls[ci].insert(0, [paraEl]);
2516
+ fillTextInto(paraEl, parseInline(cellText));
2517
+ });
2518
+ });
2519
+ break;
2520
+ }
2521
+ case "hr": break;
2522
+ case "callout": {
2523
+ el.setAttribute("type", block.calloutType);
2524
+ if (!block.innerBlocks.length) {
2525
+ const paraEl = new Y.XmlElement("paragraph");
2526
+ el.insert(0, [paraEl]);
2527
+ break;
2528
+ }
2529
+ const innerEls = block.innerBlocks.map((b) => new Y.XmlElement(blockElName(b)));
2530
+ el.insert(0, innerEls);
2531
+ block.innerBlocks.forEach((b, i) => fillBlock(innerEls[i], b));
2532
+ break;
2533
+ }
2534
+ case "collapsible": {
2535
+ el.setAttribute("label", block.label);
2536
+ el.setAttribute("open", block.open);
2537
+ const inner = block.innerBlocks.length ? block.innerBlocks : [{
2538
+ type: "paragraph",
2539
+ text: ""
2540
+ }];
2541
+ const innerEls = inner.map((b) => new Y.XmlElement(blockElName(b)));
2542
+ el.insert(0, innerEls);
2543
+ inner.forEach((b, i) => fillBlock(innerEls[i], b));
2544
+ break;
2545
+ }
2546
+ case "steps": {
2547
+ const inner = block.innerBlocks.length ? block.innerBlocks : [{
2548
+ type: "paragraph",
2549
+ text: ""
2550
+ }];
2551
+ const innerEls = inner.map((b) => new Y.XmlElement(blockElName(b)));
2552
+ el.insert(0, innerEls);
2553
+ inner.forEach((b, i) => fillBlock(innerEls[i], b));
2554
+ break;
2555
+ }
2556
+ case "card": {
2557
+ if (block.title) el.setAttribute("title", block.title);
2558
+ if (block.icon) el.setAttribute("icon", block.icon);
2559
+ if (block.to) el.setAttribute("to", block.to);
2560
+ const inner = block.innerBlocks.length ? block.innerBlocks : [{
2561
+ type: "paragraph",
2562
+ text: ""
2563
+ }];
2564
+ const innerEls = inner.map((b) => new Y.XmlElement(blockElName(b)));
2565
+ el.insert(0, innerEls);
2566
+ inner.forEach((b, i) => fillBlock(innerEls[i], b));
2567
+ break;
2568
+ }
2569
+ case "cardGroup": {
2570
+ const cardEls = block.cards.map((b) => new Y.XmlElement(blockElName(b)));
2571
+ el.insert(0, cardEls);
2572
+ block.cards.forEach((b, i) => fillBlock(cardEls[i], b));
2573
+ break;
2574
+ }
2575
+ case "codeCollapse": {
2576
+ const codes = block.codeBlocks.length ? block.codeBlocks : [{
2577
+ type: "codeBlock",
2578
+ lang: "",
2579
+ code: ""
2580
+ }];
2581
+ const codeEl = new Y.XmlElement("codeBlock");
2582
+ el.insert(0, [codeEl]);
2583
+ fillBlock(codeEl, codes[0]);
2584
+ break;
2585
+ }
2586
+ case "codeGroup": {
2587
+ const codes = block.codeBlocks.length ? block.codeBlocks : [{
2588
+ type: "codeBlock",
2589
+ lang: "",
2590
+ code: ""
2591
+ }];
2592
+ const codeEls = codes.map(() => new Y.XmlElement("codeBlock"));
2593
+ el.insert(0, codeEls);
2594
+ codes.forEach((b, i) => fillBlock(codeEls[i], b));
2595
+ break;
2596
+ }
2597
+ case "codePreview": {
2598
+ const all = [...block.innerBlocks, ...block.codeBlocks];
2599
+ const inner = all.length ? all : [{
2600
+ type: "paragraph",
2601
+ text: ""
2602
+ }];
2603
+ const innerEls = inner.map((b) => new Y.XmlElement(blockElName(b)));
2604
+ el.insert(0, innerEls);
2605
+ inner.forEach((b, i) => fillBlock(innerEls[i], b));
2606
+ break;
2607
+ }
2608
+ case "codeTree":
2609
+ el.setAttribute("files", block.files);
2610
+ break;
2611
+ case "accordion": {
2612
+ const itemEls = block.items.map(() => new Y.XmlElement("accordionItem"));
2613
+ el.insert(0, itemEls);
2614
+ block.items.forEach((item, i) => {
2615
+ itemEls[i].setAttribute("label", item.label);
2616
+ if (item.icon) itemEls[i].setAttribute("icon", item.icon);
2617
+ const inner = item.innerBlocks.length ? item.innerBlocks : [{
2618
+ type: "paragraph",
2619
+ text: ""
2620
+ }];
2621
+ const childEls = inner.map((b) => new Y.XmlElement(blockElName(b)));
2622
+ itemEls[i].insert(0, childEls);
2623
+ inner.forEach((b, ci) => fillBlock(childEls[ci], b));
2624
+ });
2625
+ break;
2626
+ }
2627
+ case "tabs": {
2628
+ const itemEls = block.items.map(() => new Y.XmlElement("tabsItem"));
2629
+ el.insert(0, itemEls);
2630
+ block.items.forEach((item, i) => {
2631
+ itemEls[i].setAttribute("label", item.label);
2632
+ if (item.icon) itemEls[i].setAttribute("icon", item.icon);
2633
+ const inner = item.innerBlocks.length ? item.innerBlocks : [{
2634
+ type: "paragraph",
2635
+ text: ""
2636
+ }];
2637
+ const childEls = inner.map((b) => new Y.XmlElement(blockElName(b)));
2638
+ itemEls[i].insert(0, childEls);
2639
+ inner.forEach((b, ci) => fillBlock(childEls[ci], b));
2640
+ });
2641
+ break;
2642
+ }
2643
+ case "field": {
2644
+ if (block.name) el.setAttribute("name", block.name);
2645
+ el.setAttribute("type", block.fieldType);
2646
+ el.setAttribute("required", block.required);
2647
+ const inner = block.innerBlocks.length ? block.innerBlocks : [{
2648
+ type: "paragraph",
2649
+ text: ""
2650
+ }];
2651
+ const innerEls = inner.map((b) => new Y.XmlElement(blockElName(b)));
2652
+ el.insert(0, innerEls);
2653
+ inner.forEach((b, i) => fillBlock(innerEls[i], b));
2654
+ break;
2655
+ }
2656
+ case "fieldGroup": {
2657
+ const fieldEls = block.fields.map((b) => new Y.XmlElement(blockElName(b)));
2658
+ el.insert(0, fieldEls);
2659
+ block.fields.forEach((b, i) => fillBlock(fieldEls[i], b));
2660
+ break;
2661
+ }
2662
+ case "image":
2663
+ el.setAttribute("src", block.src);
2664
+ if (block.alt) el.setAttribute("alt", block.alt);
2665
+ if (block.width) el.setAttribute("width", block.width);
2666
+ if (block.height) el.setAttribute("height", block.height);
2667
+ break;
2668
+ case "docEmbed":
2669
+ el.setAttribute("docId", block.docId);
2670
+ for (const flag of [
2671
+ "collapsed",
2672
+ "tall",
2673
+ "seamless"
2674
+ ]) if (block.props[flag] === "true" || block.props[flag] === "1") el.setAttribute(flag, true);
2675
+ break;
2676
+ case "mathBlock":
2677
+ el.setAttribute("expression", block.expression);
2678
+ break;
2679
+ case "fileBlock":
2680
+ if (block.src) el.setAttribute("src", block.src);
2681
+ if (block.mime) el.setAttribute("mime", block.mime);
2682
+ if (block.uploadId) el.setAttribute("uploadId", block.uploadId);
2683
+ if (block.filename) el.setAttribute("filename", block.filename);
2684
+ break;
2685
+ }
2686
+ }
2687
+ /**
2688
+ * Parses markdown text and writes the result into a Y.XmlFragment that
2689
+ * TipTap's Collaboration extension can read.
2690
+ *
2691
+ * Requires `fragment.doc` to be set (i.e. the fragment must already be
2692
+ * obtained from a live Y.Doc via `ydoc.getXmlFragment('default')`).
2693
+ *
2694
+ * @param fragment The target `Y.Doc.getXmlFragment('default')`
2695
+ * @param markdown Raw markdown string
2696
+ * @param fallbackTitle Used as the title when the markdown has no H1
2697
+ */
2698
+ function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled") {
2699
+ const ydoc = fragment.doc;
2700
+ if (!ydoc) {
2701
+ console.warn("[markdownToYjs] fragment has no doc — skipping population");
2702
+ return;
2703
+ }
2704
+ const fm = parseFrontmatter(markdown);
2705
+ const blocks = parseBlocks(fm.body);
2706
+ let title = fallbackTitle;
2707
+ let titleSource;
2708
+ if (fm.title !== void 0) {
2709
+ title = fm.title;
2710
+ titleSource = "frontmatter";
2711
+ }
2712
+ let contentBlocks = blocks;
2713
+ const h1 = blocks.findIndex((b) => b.type === "heading" && b.level === 1);
2714
+ if (h1 !== -1) {
2715
+ title = blocks[h1].text;
2716
+ contentBlocks = blocks.filter((_, i) => i !== h1);
2717
+ titleSource = "h1";
2718
+ }
2719
+ ydoc.transact(() => {
2720
+ const headerEl = new Y.XmlElement("documentHeader");
2721
+ const metaEl = new Y.XmlElement("documentMeta");
2722
+ const bodyEls = contentBlocks.map((b) => {
2723
+ switch (b.type) {
2724
+ case "heading": return new Y.XmlElement("heading");
2725
+ case "paragraph": return new Y.XmlElement("paragraph");
2726
+ case "bulletList": return new Y.XmlElement("bulletList");
2727
+ case "orderedList": return new Y.XmlElement("orderedList");
2728
+ case "taskList": return new Y.XmlElement("taskList");
2729
+ case "codeBlock": return new Y.XmlElement("codeBlock");
2730
+ case "blockquote": return new Y.XmlElement("blockquote");
2731
+ case "table": return new Y.XmlElement("table");
2732
+ case "hr": return new Y.XmlElement("horizontalRule");
2733
+ case "callout": return new Y.XmlElement("callout");
2734
+ case "collapsible": return new Y.XmlElement("collapsible");
2735
+ case "steps": return new Y.XmlElement("steps");
2736
+ case "card": return new Y.XmlElement("card");
2737
+ case "cardGroup": return new Y.XmlElement("cardGroup");
2738
+ case "codeCollapse": return new Y.XmlElement("codeCollapse");
2739
+ case "codeGroup": return new Y.XmlElement("codeGroup");
2740
+ case "codePreview": return new Y.XmlElement("codePreview");
2741
+ case "codeTree": return new Y.XmlElement("codeTree");
2742
+ case "accordion": return new Y.XmlElement("accordion");
2743
+ case "tabs": return new Y.XmlElement("tabs");
2744
+ case "field": return new Y.XmlElement("field");
2745
+ case "fieldGroup": return new Y.XmlElement("fieldGroup");
2746
+ case "image": return new Y.XmlElement("image");
2747
+ case "docEmbed": return new Y.XmlElement("docEmbed");
2748
+ case "mathBlock": return new Y.XmlElement("mathBlock");
2749
+ case "fileBlock": return new Y.XmlElement("fileBlock");
2750
+ }
2751
+ });
2752
+ fragment.insert(0, [
2753
+ headerEl,
2754
+ metaEl,
2755
+ ...bodyEls
2756
+ ]);
2757
+ if (titleSource) headerEl.setAttribute("titleSource", titleSource);
2758
+ const headerXt = new Y.XmlText();
2759
+ headerEl.insert(0, [headerXt]);
2760
+ headerXt.insert(0, title);
2761
+ for (const k of Object.keys(fm.meta)) {
2762
+ const v = fm.meta[k];
2763
+ if (v === void 0 || v === null) continue;
2764
+ metaEl.setAttribute(k, v);
2765
+ }
2766
+ if (fm.type) metaEl.setAttribute("type", fm.type);
2767
+ contentBlocks.forEach((block, i) => fillBlock(bodyEls[i], block));
2768
+ });
2769
+ }
2770
+
2771
+ //#endregion
2772
+ //#region packages/convert/src/yjs-to-markdown.ts
2773
+ function isXElem(n) {
2774
+ return !!n && typeof n.nodeName === "string";
2775
+ }
2776
+ function isXText(n) {
2777
+ return !!n && typeof n.nodeName !== "string" && typeof n.toDelta === "function";
2778
+ }
2779
+ function localizeFragment(fragment) {
2780
+ return fragment;
2781
+ }
2782
+ function serializeDelta(delta) {
2783
+ let result = "";
2784
+ for (const op of delta) {
2785
+ if (typeof op.insert !== "string") continue;
2786
+ let text = op.insert;
2787
+ const attrs = op.attributes ?? {};
2788
+ if (attrs.code) {
2789
+ result += `\`${text}\``;
2790
+ continue;
2791
+ }
2792
+ if (attrs.badge) {
2793
+ const b = attrs.badge;
2794
+ const props = [];
2795
+ if (b.color && b.color !== "neutral") props.push(`color="${b.color}"`);
2796
+ if (b.variant && b.variant !== "subtle") props.push(`variant="${b.variant}"`);
2797
+ result += `:badge[${b.label || text}]${props.length ? `{${props.join(" ")}}` : ""}`;
2798
+ continue;
2799
+ }
2800
+ if (attrs.proseIcon) {
2801
+ const icon = attrs.proseIcon.name || "i-lucide-star";
2802
+ result += `:icon{name="${icon}"}`;
2803
+ continue;
2804
+ }
2805
+ if (attrs.kbd) {
2806
+ const value = attrs.kbd.value || text;
2807
+ result += `:kbd{value="${value}"}`;
2808
+ continue;
2809
+ }
2810
+ if (attrs.docLink) {
2811
+ const docId = attrs.docLink.docId;
2812
+ if (docId) {
2813
+ result += text === docId ? `[[${docId}]]` : `[[${docId}|${text}]]`;
2814
+ continue;
2815
+ }
2816
+ }
2817
+ if (attrs.mention) {
2818
+ const { userId, label } = attrs.mention;
2819
+ if (userId) {
2820
+ result += `@[${label || text}](user:${userId})`;
2821
+ continue;
2822
+ }
2823
+ }
2824
+ if (attrs.mathInline) {
2825
+ const expr = attrs.mathInline.expression ?? text;
2826
+ result += `$${expr}$`;
2827
+ continue;
2828
+ }
2829
+ if (attrs.bold) text = `**${text}**`;
2830
+ if (attrs.italic) text = `*${text}*`;
2831
+ if (attrs.strike) text = `~~${text}~~`;
2832
+ if (attrs.link) {
2833
+ const href = attrs.link.href ?? "";
2834
+ text = `[${text}](${href})`;
2835
+ }
2836
+ result += text;
2837
+ }
2838
+ return result;
2839
+ }
2840
+ function serializeInline(el) {
2841
+ const parts = [];
2842
+ for (const child of el.toArray()) if (isXText(child)) parts.push(serializeDelta(child.toDelta()));
2843
+ else if (isXElem(child)) if (child.nodeName === "docLink") {
2844
+ const docId = child.getAttribute("docId") ?? "";
2845
+ parts.push(`[[${docId}]]`);
2846
+ } else parts.push(serializeInline(child));
2847
+ return parts.join("");
2848
+ }
2849
+ function serializeBlock(el, indent = "") {
2850
+ if (isXText(el)) return serializeDelta(el.toDelta());
2851
+ switch (el.nodeName) {
2852
+ case "documentHeader":
2853
+ case "documentMeta": return "";
2854
+ case "heading": {
2855
+ const level = Number(el.getAttribute("level") ?? 2);
2856
+ return `${"#".repeat(level)} ${serializeInline(el)}`;
2857
+ }
2858
+ case "paragraph": return serializeInline(el);
2859
+ case "bulletList": return serializeListItems(el, "bullet", indent);
2860
+ case "orderedList": return serializeListItems(el, "ordered", indent);
2861
+ case "taskList": return serializeTaskList(el, indent);
2862
+ case "codeBlock": {
2863
+ const lang = el.getAttribute("language") ?? "";
2864
+ const code = getCodeBlockText(el);
2865
+ if (code === "") return `\`\`\`${lang}\n\`\`\``;
2866
+ return `\`\`\`${lang}\n${code}\n\`\`\``;
2867
+ }
2868
+ case "blockquote": {
2869
+ const lines = [];
2870
+ for (const child of el.toArray()) if (isXElem(child)) {
2871
+ const text = serializeBlock(child);
2872
+ for (const line of text.split("\n")) lines.push(`> ${line}`);
2873
+ }
2874
+ return lines.join("\n");
2875
+ }
2876
+ case "table": return serializeTable(el);
2877
+ case "horizontalRule": return "---";
2878
+ case "image": {
2879
+ const src = el.getAttribute("src") ?? "";
2880
+ const alt = el.getAttribute("alt") ?? "";
2881
+ const width = el.getAttribute("width");
2882
+ const height = el.getAttribute("height");
2883
+ const attrs = [];
2884
+ if (width) attrs.push(`width=${width}`);
2885
+ if (height) attrs.push(`height=${height}`);
2886
+ return `![${alt}](${src})${attrs.length ? `{${attrs.join(" ")}}` : ""}`;
2887
+ }
2888
+ case "docEmbed": {
2889
+ const docId = el.getAttribute("docId") ?? "";
2890
+ const collapsed = el.getAttribute("collapsed");
2891
+ const tall = el.getAttribute("tall");
2892
+ const seamless = el.getAttribute("seamless");
2893
+ const flags = [];
2894
+ if (collapsed === true || collapsed === "true") flags.push("collapsed");
2895
+ if (tall === true || tall === "true") flags.push("tall");
2896
+ if (seamless === true || seamless === "true") flags.push("seamless");
2897
+ return `![[${docId}]]${flags.length ? `{${flags.join(" ")}}` : ""}`;
2898
+ }
2899
+ case "mathBlock": return `\`\`\`math\n${el.getAttribute("expression") ?? ""}\n\`\`\``;
2900
+ case "fileBlock": {
2901
+ const uploadId = el.getAttribute("uploadId") ?? "";
2902
+ const filename = el.getAttribute("filename") ?? "";
2903
+ const mime = el.getAttribute("mime") ?? "";
2904
+ const src = el.getAttribute("src") ?? (uploadId && filename ? `.abracadabra/files/${uploadId}-${filename}` : "");
2905
+ const props = [];
2906
+ if (src) props.push(`src="${src}"`);
2907
+ if (mime) props.push(`mime="${mime}"`);
2908
+ if (uploadId) props.push(`upload-id="${uploadId}"`);
2909
+ if (filename) props.push(`filename="${filename}"`);
2910
+ return `:file{${props.join(" ")}}`;
2911
+ }
2912
+ case "callout": return `::${el.getAttribute("type") ?? "note"}\n${serializeChildren(el)}\n::`;
2913
+ case "collapsible": {
2914
+ const label = el.getAttribute("label") ?? "Details";
2915
+ const open = el.getAttribute("open");
2916
+ const props = [`label="${label}"`];
2917
+ if (open === true || open === "true") props.push("open=\"true\"");
2918
+ return `::collapsible{${props.join(" ")}}\n${serializeChildren(el)}\n::`;
2919
+ }
2920
+ case "steps": return `::steps\n${serializeChildren(el)}\n::`;
2921
+ case "card": {
2922
+ const props = [];
2923
+ const title = el.getAttribute("title");
2924
+ const icon = el.getAttribute("icon");
2925
+ const to = el.getAttribute("to");
2926
+ if (title) props.push(`title="${title}"`);
2927
+ if (icon) props.push(`icon="${icon}"`);
2928
+ if (to) props.push(`to="${to}"`);
2929
+ return `::card${props.length ? `{${props.join(" ")}}` : ""}\n${serializeChildren(el)}\n::`;
2930
+ }
2931
+ case "cardGroup": return `::card-group\n${el.toArray().filter((c) => isXElem(c)).map((c) => serializeBlock(c)).join("\n\n")}\n::`;
2932
+ case "codeCollapse": return `::code-collapse\n${el.toArray().filter((c) => isXElem(c) && c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n")}\n::`;
2933
+ case "codeGroup": return `::code-group\n${el.toArray().filter((c) => isXElem(c) && c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n")}\n::`;
2934
+ case "codePreview": {
2935
+ const children = el.toArray().filter((c) => isXElem(c));
2936
+ const nonCode = children.filter((c) => c.nodeName !== "codeBlock").map((c) => serializeBlock(c)).join("\n\n");
2937
+ const code = children.filter((c) => c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n");
2938
+ const parts = [nonCode];
2939
+ if (code) parts.push(`#code\n${code}`);
2940
+ return `::code-preview\n${parts.filter(Boolean).join("\n\n")}\n::`;
2941
+ }
2942
+ case "codeTree": return `::code-tree{files="${el.getAttribute("files") ?? "[]"}"}\n::`;
2943
+ case "accordion": return serializeSlottedContainer(el, "accordion", "accordionItem", "item");
2944
+ case "tabs": return serializeSlottedContainer(el, "tabs", "tabsItem", "tab");
2945
+ case "field": {
2946
+ const fieldName = el.getAttribute("name") ?? "";
2947
+ const fieldType = el.getAttribute("type") ?? "string";
2948
+ const required = el.getAttribute("required");
2949
+ const props = [`name="${fieldName}"`, `type="${fieldType}"`];
2950
+ if (required === true || required === "true") props.push("required=\"true\"");
2951
+ return `::field{${props.join(" ")}}\n${serializeChildren(el)}\n::`;
2952
+ }
2953
+ case "fieldGroup": return `::field-group\n${el.toArray().filter((c) => isXElem(c)).map((c) => serializeBlock(c)).join("\n\n")}\n::`;
2954
+ default: return serializeChildren(el);
2955
+ }
2956
+ }
2957
+ function serializeChildren(el) {
2958
+ const blocks = [];
2959
+ for (const child of el.toArray()) if (isXElem(child)) {
2960
+ const text = serializeBlock(child);
2961
+ if (text) blocks.push(text);
2962
+ } else if (isXText(child)) {
2963
+ const text = serializeDelta(child.toDelta());
2964
+ if (text) blocks.push(text);
2965
+ }
2966
+ return blocks.join("\n\n");
2967
+ }
2968
+ function serializeListItems(el, type, indent) {
2969
+ const lines = [];
2970
+ let counter = 1;
2971
+ for (const child of el.toArray()) {
2972
+ if (!isXElem(child) || child.nodeName !== "listItem") continue;
2973
+ const prefix = type === "bullet" ? "- " : `${counter++}. `;
2974
+ const subParts = [];
2975
+ for (const sub of child.toArray()) {
2976
+ if (!isXElem(sub)) continue;
2977
+ if (sub.nodeName === "bulletList") subParts.push({
2978
+ text: serializeListItems(sub, "bullet", indent + " "),
2979
+ isList: true
2980
+ });
2981
+ else if (sub.nodeName === "orderedList") subParts.push({
2982
+ text: serializeListItems(sub, "ordered", indent + " "),
2983
+ isList: true
2984
+ });
2985
+ else subParts.push({
2986
+ text: serializeInline(sub),
2987
+ isList: false
2988
+ });
2989
+ }
2990
+ lines.push(`${indent}${prefix}${subParts[0]?.text ?? ""}`);
2991
+ for (let i = 1; i < subParts.length; i++) {
2992
+ const part = subParts[i];
2993
+ lines.push(part.isList ? part.text : `${indent} ${part.text}`);
2994
+ }
2995
+ }
2996
+ return lines.join("\n");
2997
+ }
2998
+ function serializeTaskList(el, indent) {
2999
+ const lines = [];
3000
+ for (const child of el.toArray()) {
3001
+ if (!isXElem(child) || child.nodeName !== "taskItem") continue;
3002
+ const checked = child.getAttribute("checked");
3003
+ const marker = checked === true || checked === "true" ? "[x]" : "[ ]";
3004
+ let header = "";
3005
+ const nestedParts = [];
3006
+ for (const sub of child.toArray()) {
3007
+ if (!isXElem(sub)) continue;
3008
+ if (sub.nodeName === "paragraph" && header === "") header = serializeInline(sub);
3009
+ else if (sub.nodeName === "bulletList") nestedParts.push(serializeListItems(sub, "bullet", indent + " "));
3010
+ else if (sub.nodeName === "orderedList") nestedParts.push(serializeListItems(sub, "ordered", indent + " "));
3011
+ else if (sub.nodeName === "taskList") nestedParts.push(serializeTaskList(sub, indent + " "));
3012
+ else nestedParts.push(indent + " " + serializeBlock(sub, indent + " "));
3013
+ }
3014
+ lines.push(`${indent}- ${marker} ${header}`);
3015
+ for (const part of nestedParts) lines.push(part);
3016
+ }
3017
+ return lines.join("\n");
3018
+ }
3019
+ function getCodeBlockText(el) {
3020
+ for (const child of el.toArray()) if (isXText(child)) return child.toString();
3021
+ return "";
3022
+ }
3023
+ function serializeTable(el) {
3024
+ const rows = el.toArray().filter((c) => isXElem(c));
3025
+ if (!rows.length) return "";
3026
+ const serializedRows = [];
3027
+ for (const row of rows) {
3028
+ const cells = row.toArray().filter((c) => isXElem(c)).map((cell) => {
3029
+ return cell.toArray().filter((c) => isXElem(c)).map((c) => serializeInline(c)).join(" ");
3030
+ });
3031
+ serializedRows.push(cells);
3032
+ }
3033
+ if (!serializedRows.length) return "";
3034
+ const colCount = Math.max(...serializedRows.map((r) => r.length));
3035
+ const headerRow = serializedRows[0];
3036
+ const separator = Array(colCount).fill("---");
3037
+ const dataRows = serializedRows.slice(1);
3038
+ const formatRow = (cells) => {
3039
+ return `| ${Array(colCount).fill("").map((_, i) => cells[i] ?? "").join(" | ")} |`;
3040
+ };
3041
+ return [
3042
+ formatRow(headerRow),
3043
+ formatRow(separator),
3044
+ ...dataRows.map(formatRow)
3045
+ ].join("\n");
3046
+ }
3047
+ function serializeSlottedContainer(el, containerName, childName, slotPrefix) {
3048
+ return `::${containerName}\n${el.toArray().filter((c) => isXElem(c) && c.nodeName === childName).map((item) => {
3049
+ const label = item.getAttribute("label") ?? "";
3050
+ const icon = item.getAttribute("icon") ?? "";
3051
+ const props = [];
3052
+ if (label) props.push(`label="${label}"`);
3053
+ if (icon) props.push(`icon="${icon}"`);
3054
+ const content = serializeChildren(item);
3055
+ return `#${slotPrefix}{${props.join(" ")}}\n${content}`;
3056
+ }).join("\n\n")}\n::`;
3057
+ }
3058
+ /**
3059
+ * Keys the hand-rolled canonical section below emits itself. The generic
3060
+ * spec-driven pass must skip these so each key is serialised exactly once,
3061
+ * in the canonical order existing fixtures depend on.
3062
+ */
3063
+ const CANONICAL_FM_KEYS = new Set([
3064
+ "title",
3065
+ "type",
3066
+ "tags",
3067
+ "color",
3068
+ "icon",
3069
+ "status",
3070
+ "priority",
3071
+ "checked",
3072
+ "language",
3073
+ "fileExtension",
3074
+ "codeTheme",
3075
+ "dateStart",
3076
+ "dateEnd",
3077
+ "subtitle",
3078
+ "url",
3079
+ "rating"
3080
+ ]);
3081
+ /** Render one meta value as a YAML line, or null when it can't be emitted. */
3082
+ function yamlLine(key, v, specType) {
3083
+ if (v === void 0 || v === null) return null;
3084
+ if (Array.isArray(v)) {
3085
+ if (v.length === 0) return null;
3086
+ return `${key}: [${v.map((x) => String(x)).join(", ")}]`;
3087
+ }
3088
+ if (typeof v === "number") return Number.isFinite(v) ? `${key}: ${v}` : null;
3089
+ if (typeof v === "boolean") return `${key}: ${v}`;
3090
+ if (typeof v === "string") return v === "" ? null : `${key}: ${yamlScalar(v)}`;
3091
+ if (specType === "members" || specType === "json" || typeof v === "object") try {
3092
+ const json = JSON.stringify(v);
3093
+ return json.includes("'") ? null : `${key}: '${json}'`;
3094
+ } catch {
3095
+ return null;
3096
+ }
3097
+ return null;
3098
+ }
3099
+ /**
3100
+ * Generate the YAML frontmatter block. Returns '' when there is nothing to
3101
+ * emit, so callers can skip the block entirely — an empty `---\n\n---` shell
3102
+ * is never produced (docs whose meta holds only internal `_`-keys used to
3103
+ * export as exactly that junk).
3104
+ *
3105
+ * Emission order: the long-standing hand-rolled canonical keys first (byte
3106
+ * stability for existing files), then every remaining universal-meta key in
3107
+ * registry order, then custom keys alphabetically. Internal keys (leading
3108
+ * `_`, e.g. `_metaInitialized`) are never serialised.
3109
+ */
3110
+ function generateFrontmatter(label, meta, type) {
3111
+ const lines = [];
3112
+ if (label !== void 0) lines.push(`title: "${escapeYaml(label)}"`);
3113
+ if (type && type !== "doc") lines.push(`type: ${type}`);
3114
+ if (meta) {
3115
+ if (meta.tags?.length) lines.push(`tags: [${meta.tags.join(", ")}]`);
3116
+ if (meta.color) lines.push(`color: ${yamlScalar(meta.color)}`);
3117
+ if (meta.icon) lines.push(`icon: ${yamlScalar(meta.icon)}`);
3118
+ if (meta.status) lines.push(`status: ${yamlScalar(meta.status)}`);
3119
+ if (meta.priority !== void 0 && meta.priority !== 0) lines.push(`priority: ${{
3120
+ 1: "low",
3121
+ 2: "medium",
3122
+ 3: "high",
3123
+ 4: "urgent"
3124
+ }[meta.priority] ?? meta.priority}`);
3125
+ if (meta.checked !== void 0) lines.push(`checked: ${meta.checked}`);
3126
+ if (meta.language) lines.push(`language: ${yamlScalar(meta.language)}`);
3127
+ if (meta.fileExtension) lines.push(`fileExtension: ${yamlScalar(meta.fileExtension)}`);
3128
+ if (meta.codeTheme) lines.push(`codeTheme: ${yamlScalar(meta.codeTheme)}`);
3129
+ if (meta.dateStart) lines.push(`dateStart: "${escapeYaml(meta.dateStart)}"`);
3130
+ if (meta.dateEnd) lines.push(`dateEnd: "${escapeYaml(meta.dateEnd)}"`);
3131
+ if (meta.subtitle) lines.push(`subtitle: "${escapeYaml(meta.subtitle)}"`);
3132
+ if (meta.url) lines.push(`url: ${meta.url}`);
3133
+ if (meta.rating !== void 0 && meta.rating !== 0) lines.push(`rating: ${meta.rating}`);
3134
+ const m = meta;
3135
+ for (const spec of UNIVERSAL_META_KEYS) {
3136
+ if (CANONICAL_FM_KEYS.has(spec.key)) continue;
3137
+ const line = yamlLine(spec.key, m[spec.key], spec.type);
3138
+ if (line) lines.push(line);
3139
+ }
3140
+ const customKeys = Object.keys(m).filter((k) => !k.startsWith("_") && !CANONICAL_FM_KEYS.has(k) && !UNIVERSAL_META_KEY_NAMES.has(k)).sort();
3141
+ for (const k of customKeys) {
3142
+ const line = yamlLine(k, m[k]);
3143
+ if (line) lines.push(line);
3144
+ }
3145
+ }
3146
+ if (lines.length === 0) return "";
3147
+ return `---\n${lines.join("\n")}\n---`;
3148
+ }
3149
+ /**
3150
+ * Render a YAML scalar — bare when safe, double-quoted when the value
3151
+ * needs escaping. YAML treats `#`, `:`, leading whitespace, and a few
3152
+ * other characters as syntactically significant, so anything starting
3153
+ * with one of those gets quoted to stay round-trip safe.
3154
+ */
3155
+ function yamlScalar(s) {
3156
+ if (s === "") return "\"\"";
3157
+ if (/^[#&*!|>%@`]/.test(s)) return `"${escapeYaml(s)}"`;
3158
+ if (/[:"]/.test(s)) return `"${escapeYaml(s)}"`;
3159
+ if (/^\s|\s$/.test(s)) return `"${escapeYaml(s)}"`;
3160
+ return s;
3161
+ }
3162
+ function escapeYaml(s) {
3163
+ return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
3164
+ }
3165
+ function yjsToMarkdown(fragment, label, meta, type) {
3166
+ fragment = localizeFragment(fragment);
3167
+ const { text: headerText, source: titleSource } = readDocumentHeader(fragment);
3168
+ const effectiveTitle = headerText || label;
3169
+ const docMeta = readDocumentMeta(fragment);
3170
+ const effectiveMeta = meta ?? docMeta.meta;
3171
+ const effectiveType = type ?? docMeta.type;
3172
+ const bodyBlocks = collectBodyBlocks(fragment);
3173
+ let body;
3174
+ if (titleSource === "h1" && effectiveTitle) {
3175
+ const tail = serializeBlocksClean(bodyBlocks);
3176
+ body = tail === "" ? `# ${effectiveTitle}` : `# ${effectiveTitle}\n\n${tail}`;
3177
+ } else body = serializeBlocksClean(bodyBlocks);
3178
+ const frontmatter = generateFrontmatter(titleSource === "frontmatter" ? effectiveTitle : void 0, effectiveMeta, effectiveType);
3179
+ if (frontmatter === "") return body === "" ? "" : `${body}\n`;
3180
+ if (body === "") return `${frontmatter}\n`;
3181
+ return `${frontmatter}\n\n${body}\n`;
3182
+ }
3183
+ function readDocumentMeta(fragment) {
3184
+ const meta = {};
3185
+ let type;
3186
+ for (const child of fragment.toArray()) {
3187
+ if (!isXElem(child) || child.nodeName !== "documentMeta") continue;
3188
+ const attrs = child.getAttributes();
3189
+ for (const k of Object.keys(attrs)) {
3190
+ const v = attrs[k];
3191
+ if (v === void 0 || v === null) continue;
3192
+ if (k === "type" && typeof v === "string") {
3193
+ type = v;
3194
+ continue;
3195
+ }
3196
+ meta[k] = v;
3197
+ }
3198
+ break;
3199
+ }
3200
+ return {
3201
+ meta,
3202
+ type
3203
+ };
3204
+ }
3205
+ function readDocumentHeader(fragment) {
3206
+ for (const child of fragment.toArray()) {
3207
+ if (!isXElem(child) || child.nodeName !== "documentHeader") continue;
3208
+ const text = child.toArray().find((c) => isXText(c));
3209
+ const src = child.getAttribute("titleSource");
3210
+ const source = src === "h1" || src === "frontmatter" ? src : void 0;
3211
+ return {
3212
+ text: text ? text.toString() : "",
3213
+ source
3214
+ };
3215
+ }
3216
+ return { text: "" };
3217
+ }
3218
+ function collectBodyBlocks(fragment) {
3219
+ const out = [];
3220
+ for (const child of fragment.toArray()) {
3221
+ if (!isXElem(child)) continue;
3222
+ if (child.nodeName === "documentHeader" || child.nodeName === "documentMeta") continue;
3223
+ out.push(child);
3224
+ }
3225
+ return out;
3226
+ }
3227
+ function serializeBlocksClean(blocks) {
3228
+ const parts = [];
3229
+ for (const block of blocks) {
3230
+ if (block.nodeName === "paragraph" && block.length === 0) {
3231
+ parts.push("");
3232
+ continue;
3233
+ }
3234
+ parts.push(serializeBlock(block));
3235
+ }
3236
+ while (parts.length && parts[parts.length - 1] === "") parts.pop();
3237
+ return parts.join("\n\n");
3238
+ }
3239
+
3240
+ //#endregion
3241
+ //#region packages/convert/src/spec/nodes.ts
3242
+ const BOOL_FALSE_DEFAULT = {
3243
+ key: "",
3244
+ type: "boolean",
3245
+ default: false,
3246
+ optional: true
3247
+ };
3248
+ const bool = (key) => ({
3249
+ ...BOOL_FALSE_DEFAULT,
3250
+ key
3251
+ });
3252
+ const str = (key, def) => ({
3253
+ key,
3254
+ type: "string",
3255
+ default: def,
3256
+ optional: true
3257
+ });
3258
+ const num = (key) => ({
3259
+ key,
3260
+ type: "number",
3261
+ optional: true
3262
+ });
3263
+ const int = (key) => ({
3264
+ key,
3265
+ type: "integer",
3266
+ optional: true
3267
+ });
3268
+ const VANILLA_BLOCKS = [
3168
3269
  {
3169
- name: "buttonGroup",
3270
+ name: "documentHeader",
3170
3271
  group: "block",
3171
- wire: "mdc-slotted",
3172
- mdcTag: "button-group",
3173
- slotChild: "button"
3272
+ wire: "special",
3273
+ doc: "Holds the title; hoisted to frontmatter on serialise."
3174
3274
  },
3175
3275
  {
3176
- name: "timeline",
3276
+ name: "documentMeta",
3177
3277
  group: "block",
3178
- wire: "mdc-slotted",
3179
- slotChild: "timelineItem"
3278
+ wire: "special",
3279
+ doc: "Holds page-level meta; serialised into frontmatter."
3180
3280
  },
3181
3281
  {
3182
- name: "timelineItem",
3282
+ name: "paragraph",
3183
3283
  group: "block",
3184
- wire: "mdc-container",
3185
- mdcTag: "timeline-item",
3186
- attrs: [
3187
- str("label"),
3188
- str("icon"),
3189
- str("date")
3190
- ]
3284
+ wire: "vanilla",
3285
+ contentBearing: true
3191
3286
  },
3192
3287
  {
3193
- name: "diff",
3288
+ name: "heading",
3194
3289
  group: "block",
3195
- wire: "mdc-atom-block",
3196
- attrs: [str("language", ""), {
3197
- key: "value",
3198
- type: "string"
3199
- }]
3200
- }
3201
- ];
3202
- const INLINE_AND_SPECIAL = [
3203
- {
3204
- name: "docLink",
3205
- group: "inline",
3206
- wire: "special",
3207
- attrs: [str("docId")],
3208
- doc: "Wire form `[[uuid|label]]`; label regenerated on export."
3290
+ wire: "vanilla",
3291
+ attrs: [int("level")],
3292
+ contentBearing: true
3209
3293
  },
3210
3294
  {
3211
- name: "docEmbed",
3295
+ name: "blockquote",
3212
3296
  group: "block",
3213
- wire: "special",
3214
- attrs: [
3215
- str("docId"),
3216
- bool("collapsed"),
3217
- bool("tall"),
3218
- bool("seamless")
3219
- ],
3220
- doc: "Wire form `![[uuid|label]]{collapsed tall seamless}`."
3221
- },
3222
- {
3223
- name: "mention",
3224
- group: "inline",
3225
- wire: "special",
3226
- attrs: [str("userId")],
3227
- doc: "Wire form `@[label](user:uuid)`; label regenerated on export."
3228
- },
3229
- {
3230
- name: "mathInline",
3231
- group: "inline",
3232
- wire: "special",
3233
- attrs: [{
3234
- key: "expression",
3235
- type: "string"
3236
- }],
3237
- doc: "Wire form `$expression$`."
3297
+ wire: "vanilla",
3298
+ contentBearing: true
3238
3299
  },
3239
3300
  {
3240
- name: "mathBlock",
3301
+ name: "codeBlock",
3241
3302
  group: "block",
3242
3303
  wire: "fence",
3243
- attrs: [{
3244
- key: "expression",
3245
- type: "string"
3246
- }],
3247
- mdcTag: "math",
3248
- doc: "Wire form ``` ```math\\nexpression\\n``` ```."
3304
+ attrs: [str("language", "")]
3249
3305
  },
3250
3306
  {
3251
- name: "fileBlock",
3307
+ name: "bulletList",
3252
3308
  group: "block",
3253
- wire: "mdc-atom-block",
3254
- mdcTag: "file",
3255
- attrs: [
3256
- str("src"),
3257
- str("mime"),
3258
- str("uploadId"),
3259
- str("filename")
3260
- ],
3261
- doc: "Wire form `:file{src=… mime=… upload-id=… filename=…}`; binary in sidecar."
3262
- },
3263
- {
3264
- name: "badge",
3265
- group: "inline",
3266
- wire: "mdc-atom-inl",
3267
- attrs: [
3268
- str("label"),
3269
- str("color"),
3270
- str("variant", "subtle")
3271
- ],
3272
- doc: "Wire form `:badge[Label]{color=… variant=…}`."
3273
- },
3274
- {
3275
- name: "proseIcon",
3276
- group: "inline",
3277
- wire: "mdc-atom-inl",
3278
- mdcTag: "icon",
3279
- attrs: [str("name")],
3280
- doc: "Wire form `:icon{name=…}`."
3281
- },
3282
- {
3283
- name: "kbd",
3284
- group: "inline",
3285
- wire: "mdc-atom-inl",
3286
- attrs: [str("value")],
3287
- doc: "Wire form `:kbd{value=…}`."
3288
- }
3289
- ];
3290
- const NODE_SPECS = [
3291
- ...VANILLA_BLOCKS,
3292
- ...MDC_CONTAINERS,
3293
- ...INLINE_AND_SPECIAL
3294
- ];
3295
- const NODE_SPEC_BY_NAME = new Map(NODE_SPECS.map((spec) => [spec.name, spec]));
3296
-
3297
- //#endregion
3298
- //#region packages/convert/src/spec/marks.ts
3299
- const MARK_SPECS = [
3300
- {
3301
- name: "bold",
3302
- wire: "delimited",
3303
- delim: "**"
3304
- },
3305
- {
3306
- name: "italic",
3307
- wire: "delimited",
3308
- delim: "*"
3309
- },
3310
- {
3311
- name: "strike",
3312
- wire: "delimited",
3313
- delim: "~~"
3314
- },
3315
- {
3316
- name: "code",
3317
- wire: "delimited",
3318
- delim: "`"
3319
- },
3320
- {
3321
- name: "link",
3322
- wire: "link",
3323
- attrs: [{
3324
- key: "href",
3325
- type: "string"
3326
- }, {
3327
- key: "title",
3328
- type: "string",
3329
- optional: true
3330
- }]
3331
- },
3332
- {
3333
- name: "underline",
3334
- wire: "delimited",
3335
- delim: "__",
3336
- doc: "Disambiguated from bold by delimiter character. Two underscores = underline; two asterisks = bold."
3337
- },
3338
- {
3339
- name: "highlight",
3340
- wire: "delimited",
3341
- delim: "==",
3342
- doc: "Pandoc-style."
3343
- },
3344
- {
3345
- name: "subscript",
3346
- wire: "delimited",
3347
- delim: "~",
3348
- doc: "Single tilde; double tilde is strike."
3309
+ wire: "vanilla"
3349
3310
  },
3350
3311
  {
3351
- name: "superscript",
3352
- wire: "delimited",
3353
- delim: "^"
3312
+ name: "orderedList",
3313
+ group: "block",
3314
+ wire: "vanilla"
3354
3315
  },
3355
3316
  {
3356
- name: "textStyle",
3357
- wire: "mdc-span",
3358
- attrs: [
3359
- {
3360
- key: "color",
3361
- type: "string",
3362
- optional: true
3363
- },
3364
- {
3365
- key: "backgroundColor",
3366
- type: "string",
3367
- optional: true
3368
- },
3369
- {
3370
- key: "fontSize",
3371
- type: "string",
3372
- optional: true
3373
- },
3374
- {
3375
- key: "fontFamily",
3376
- type: "string",
3377
- optional: true
3378
- }
3379
- ],
3380
- doc: "Wire form `:span[text]{color=\"…\" font-size=\"…\"}`. Any of the attrs may be set."
3381
- }
3382
- ];
3383
- const MARK_SPEC_BY_NAME = new Map(MARK_SPECS.map((spec) => [spec.name, spec]));
3384
- const MARK_SPEC_BY_DELIM = new Map(MARK_SPECS.filter((spec) => spec.wire === "delimited" && !!spec.delim).map((spec) => [spec.delim, spec]));
3385
-
3386
- //#endregion
3387
- //#region packages/convert/src/spec/universal-meta.ts
3388
- const UNIVERSAL_META_KEYS = [
3389
- {
3390
- key: "title",
3391
- type: "string",
3392
- doc: "Display title; the first H1 is hoisted into this field on import."
3317
+ name: "listItem",
3318
+ group: "block",
3319
+ wire: "vanilla",
3320
+ contentBearing: true
3393
3321
  },
3394
3322
  {
3395
- key: "type",
3396
- type: "string",
3397
- doc: "Page type (doc, kanban, table, …). Omitted on serialise when \"doc\"."
3323
+ name: "taskList",
3324
+ group: "block",
3325
+ wire: "vanilla"
3398
3326
  },
3399
3327
  {
3400
- key: "color",
3401
- type: "string",
3402
- doc: "Hex or CSS color name."
3328
+ name: "taskItem",
3329
+ group: "block",
3330
+ wire: "vanilla",
3331
+ attrs: [bool("checked")],
3332
+ contentBearing: true
3403
3333
  },
3404
3334
  {
3405
- key: "icon",
3406
- type: "string",
3407
- doc: "Lucide icon name in kebab-case."
3335
+ name: "table",
3336
+ group: "block",
3337
+ wire: "vanilla"
3408
3338
  },
3409
3339
  {
3410
- key: "datetimeStart",
3411
- type: "iso-datetime"
3340
+ name: "tableRow",
3341
+ group: "block",
3342
+ wire: "vanilla"
3412
3343
  },
3413
3344
  {
3414
- key: "datetimeEnd",
3415
- type: "iso-datetime"
3345
+ name: "tableHeader",
3346
+ group: "block",
3347
+ wire: "vanilla",
3348
+ contentBearing: true
3416
3349
  },
3417
3350
  {
3418
- key: "allDay",
3419
- type: "boolean"
3351
+ name: "tableCell",
3352
+ group: "block",
3353
+ wire: "vanilla",
3354
+ contentBearing: true
3420
3355
  },
3421
3356
  {
3422
- key: "dateTaken",
3423
- type: "iso-datetime"
3357
+ name: "horizontalRule",
3358
+ group: "block",
3359
+ wire: "vanilla"
3424
3360
  },
3425
3361
  {
3426
- key: "dateStart",
3427
- type: "iso-date",
3428
- parseAliases: ["date", "created"]
3362
+ name: "image",
3363
+ group: "block",
3364
+ wire: "special",
3365
+ attrs: [
3366
+ str("src"),
3367
+ str("alt", ""),
3368
+ int("width"),
3369
+ int("height")
3370
+ ]
3429
3371
  },
3430
3372
  {
3431
- key: "dateEnd",
3432
- type: "iso-date",
3433
- parseAliases: ["due"]
3434
- },
3373
+ name: "hardBreak",
3374
+ group: "inline",
3375
+ wire: "vanilla"
3376
+ }
3377
+ ];
3378
+ const MDC_CONTAINERS = [
3435
3379
  {
3436
- key: "timeStart",
3437
- type: "hh-mm"
3380
+ name: "callout",
3381
+ group: "block",
3382
+ wire: "mdc-container",
3383
+ attrs: [
3384
+ {
3385
+ key: "type",
3386
+ type: "string",
3387
+ default: "note",
3388
+ optional: true,
3389
+ values: [
3390
+ "note",
3391
+ "tip",
3392
+ "warning",
3393
+ "danger",
3394
+ "info",
3395
+ "caution",
3396
+ "alert",
3397
+ "success",
3398
+ "error"
3399
+ ]
3400
+ },
3401
+ str("title"),
3402
+ str("icon")
3403
+ ]
3438
3404
  },
3439
3405
  {
3440
- key: "timeEnd",
3441
- type: "hh-mm"
3406
+ name: "collapsible",
3407
+ group: "block",
3408
+ wire: "mdc-container",
3409
+ attrs: [str("label", "Details"), bool("open")]
3442
3410
  },
3443
3411
  {
3444
- key: "tags",
3445
- type: "string[]"
3412
+ name: "accordion",
3413
+ group: "block",
3414
+ wire: "mdc-slotted",
3415
+ slotChild: "accordionItem"
3446
3416
  },
3447
3417
  {
3448
- key: "checked",
3449
- type: "boolean",
3450
- parseAliases: ["done"]
3418
+ name: "accordionItem",
3419
+ group: "block",
3420
+ wire: "mdc-container",
3421
+ mdcTag: "accordion-item",
3422
+ attrs: [str("label", "Item"), str("icon")]
3451
3423
  },
3452
3424
  {
3453
- key: "priority",
3454
- type: "integer",
3455
- min: 0,
3456
- max: 4,
3457
- doc: "Numeric or named (low/medium/high/urgent → 1/2/3/4)."
3425
+ name: "tabs",
3426
+ group: "block",
3427
+ wire: "mdc-slotted",
3428
+ slotChild: "tabsItem"
3458
3429
  },
3459
3430
  {
3460
- key: "status",
3461
- type: "string"
3431
+ name: "tabsItem",
3432
+ group: "block",
3433
+ wire: "mdc-container",
3434
+ mdcTag: "tabs-item",
3435
+ attrs: [str("label"), str("icon")]
3462
3436
  },
3463
3437
  {
3464
- key: "rating",
3465
- type: "number",
3466
- min: 0,
3467
- max: 5
3438
+ name: "steps",
3439
+ group: "block",
3440
+ wire: "mdc-container"
3468
3441
  },
3469
3442
  {
3470
- key: "url",
3471
- type: "string"
3443
+ name: "card",
3444
+ group: "block",
3445
+ wire: "mdc-container",
3446
+ attrs: [
3447
+ str("title"),
3448
+ str("icon"),
3449
+ str("to")
3450
+ ]
3472
3451
  },
3473
3452
  {
3474
- key: "email",
3475
- type: "string"
3453
+ name: "cardGroup",
3454
+ group: "block",
3455
+ wire: "mdc-slotted",
3456
+ mdcTag: "card-group",
3457
+ slotChild: "card"
3476
3458
  },
3477
3459
  {
3478
- key: "phone",
3479
- type: "string"
3460
+ name: "field",
3461
+ group: "block",
3462
+ wire: "mdc-container",
3463
+ attrs: [
3464
+ str("name"),
3465
+ str("type", "string"),
3466
+ bool("required")
3467
+ ]
3480
3468
  },
3481
3469
  {
3482
- key: "number",
3483
- type: "number"
3470
+ name: "fieldGroup",
3471
+ group: "block",
3472
+ wire: "mdc-slotted",
3473
+ mdcTag: "field-group",
3474
+ slotChild: "field"
3484
3475
  },
3485
3476
  {
3486
- key: "unit",
3487
- type: "string"
3477
+ name: "codeGroup",
3478
+ group: "block",
3479
+ wire: "mdc-slotted",
3480
+ mdcTag: "code-group",
3481
+ slotChild: "codeBlock"
3488
3482
  },
3489
3483
  {
3490
- key: "subtitle",
3491
- type: "string",
3492
- parseAliases: ["description"]
3484
+ name: "codeCollapse",
3485
+ group: "block",
3486
+ wire: "mdc-container",
3487
+ mdcTag: "code-collapse"
3493
3488
  },
3494
3489
  {
3495
- key: "note",
3496
- type: "string"
3490
+ name: "codePreview",
3491
+ group: "block",
3492
+ wire: "mdc-container",
3493
+ mdcTag: "code-preview"
3497
3494
  },
3498
3495
  {
3499
- key: "taskProgress",
3500
- type: "integer",
3501
- min: 0,
3502
- max: 100
3496
+ name: "codeTree",
3497
+ group: "block",
3498
+ wire: "mdc-atom-block",
3499
+ mdcTag: "code-tree",
3500
+ attrs: [{
3501
+ key: "files",
3502
+ type: "json"
3503
+ }]
3503
3504
  },
3504
3505
  {
3505
- key: "members",
3506
- type: "members"
3506
+ name: "figure",
3507
+ group: "block",
3508
+ wire: "mdc-container",
3509
+ attrs: [
3510
+ str("src"),
3511
+ str("alt", ""),
3512
+ str("caption")
3513
+ ]
3507
3514
  },
3508
3515
  {
3509
- key: "coverUploadId",
3510
- type: "string"
3516
+ name: "video",
3517
+ group: "block",
3518
+ wire: "mdc-atom-block",
3519
+ attrs: [
3520
+ str("src"),
3521
+ str("poster"),
3522
+ bool("autoplay"),
3523
+ bool("loop"),
3524
+ bool("controls")
3525
+ ]
3511
3526
  },
3512
3527
  {
3513
- key: "coverDocId",
3514
- type: "string"
3528
+ name: "embed",
3529
+ group: "block",
3530
+ wire: "mdc-atom-block",
3531
+ attrs: [str("src"), str("title")]
3515
3532
  },
3516
3533
  {
3517
- key: "coverMimeType",
3518
- type: "string"
3534
+ name: "svgEmbed",
3535
+ group: "block",
3536
+ wire: "fence",
3537
+ attrs: [str("title")],
3538
+ mdcTag: "svg",
3539
+ doc: "Serialised as a ```svg fenced block; the SVG markup is the body."
3519
3540
  },
3520
3541
  {
3521
- key: "geoType",
3522
- type: "string-enum",
3523
- values: [
3524
- "marker",
3525
- "line",
3526
- "measure"
3527
- ]
3542
+ name: "divider",
3543
+ group: "block",
3544
+ wire: "mdc-atom-block",
3545
+ attrs: [str("label"), str("icon")]
3528
3546
  },
3529
3547
  {
3530
- key: "geoLat",
3531
- type: "number"
3548
+ name: "quote",
3549
+ group: "block",
3550
+ wire: "mdc-container",
3551
+ attrs: [str("cite")]
3532
3552
  },
3533
3553
  {
3534
- key: "geoLng",
3535
- type: "number"
3554
+ name: "progress",
3555
+ group: "block",
3556
+ wire: "mdc-atom-block",
3557
+ attrs: [
3558
+ num("value"),
3559
+ num("max"),
3560
+ str("label")
3561
+ ]
3536
3562
  },
3537
3563
  {
3538
- key: "geoDescription",
3539
- type: "string"
3564
+ name: "spoiler",
3565
+ group: "block",
3566
+ wire: "mdc-container",
3567
+ attrs: [str("label")]
3540
3568
  },
3541
3569
  {
3542
- key: "deskX",
3543
- type: "number"
3570
+ name: "colorSwatch",
3571
+ group: "block",
3572
+ wire: "mdc-atom-block",
3573
+ mdcTag: "color-swatch",
3574
+ attrs: [str("color"), str("label")]
3544
3575
  },
3545
3576
  {
3546
- key: "deskY",
3547
- type: "number"
3577
+ name: "stat",
3578
+ group: "block",
3579
+ wire: "mdc-container",
3580
+ attrs: [
3581
+ str("label"),
3582
+ str("value"),
3583
+ str("icon")
3584
+ ]
3548
3585
  },
3549
3586
  {
3550
- key: "deskZ",
3551
- type: "number"
3587
+ name: "statGroup",
3588
+ group: "block",
3589
+ wire: "mdc-slotted",
3590
+ mdcTag: "stat-group",
3591
+ slotChild: "stat"
3552
3592
  },
3553
3593
  {
3554
- key: "deskMode",
3555
- type: "string-enum",
3556
- values: [
3557
- "icon",
3558
- "widget-sm",
3559
- "widget-lg"
3594
+ name: "button",
3595
+ group: "block",
3596
+ wire: "mdc-atom-block",
3597
+ attrs: [
3598
+ str("label"),
3599
+ str("to"),
3600
+ str("icon"),
3601
+ str("variant")
3560
3602
  ]
3561
3603
  },
3562
3604
  {
3563
- key: "mmX",
3564
- type: "number"
3605
+ name: "buttonGroup",
3606
+ group: "block",
3607
+ wire: "mdc-slotted",
3608
+ mdcTag: "button-group",
3609
+ slotChild: "button"
3565
3610
  },
3566
3611
  {
3567
- key: "mmY",
3568
- type: "number"
3612
+ name: "timeline",
3613
+ group: "block",
3614
+ wire: "mdc-slotted",
3615
+ slotChild: "timelineItem"
3569
3616
  },
3570
3617
  {
3571
- key: "graphX",
3572
- type: "number"
3618
+ name: "timelineItem",
3619
+ group: "block",
3620
+ wire: "mdc-container",
3621
+ mdcTag: "timeline-item",
3622
+ attrs: [
3623
+ str("label"),
3624
+ str("icon"),
3625
+ str("date")
3626
+ ]
3573
3627
  },
3574
3628
  {
3575
- key: "graphY",
3576
- type: "number"
3629
+ name: "diff",
3630
+ group: "block",
3631
+ wire: "mdc-atom-block",
3632
+ attrs: [str("language", ""), {
3633
+ key: "value",
3634
+ type: "string"
3635
+ }]
3636
+ }
3637
+ ];
3638
+ const INLINE_AND_SPECIAL = [
3639
+ {
3640
+ name: "docLink",
3641
+ group: "inline",
3642
+ wire: "special",
3643
+ attrs: [str("docId")],
3644
+ doc: "Wire form `[[uuid|label]]`; label regenerated on export."
3577
3645
  },
3578
3646
  {
3579
- key: "graphPinned",
3580
- type: "boolean"
3647
+ name: "docEmbed",
3648
+ group: "block",
3649
+ wire: "special",
3650
+ attrs: [
3651
+ str("docId"),
3652
+ bool("collapsed"),
3653
+ bool("tall"),
3654
+ bool("seamless")
3655
+ ],
3656
+ doc: "Wire form `![[uuid|label]]{collapsed tall seamless}`."
3581
3657
  },
3582
3658
  {
3583
- key: "spX",
3584
- type: "number"
3659
+ name: "mention",
3660
+ group: "inline",
3661
+ wire: "special",
3662
+ attrs: [str("userId")],
3663
+ doc: "Wire form `@[label](user:uuid)`; label regenerated on export."
3585
3664
  },
3586
3665
  {
3587
- key: "spY",
3588
- type: "number"
3666
+ name: "mathInline",
3667
+ group: "inline",
3668
+ wire: "special",
3669
+ attrs: [{
3670
+ key: "expression",
3671
+ type: "string"
3672
+ }],
3673
+ doc: "Wire form `$expression$`."
3589
3674
  },
3590
3675
  {
3591
- key: "spZ",
3592
- type: "number"
3676
+ name: "mathBlock",
3677
+ group: "block",
3678
+ wire: "fence",
3679
+ attrs: [{
3680
+ key: "expression",
3681
+ type: "string"
3682
+ }],
3683
+ mdcTag: "math",
3684
+ doc: "Wire form ``` ```math\\nexpression\\n``` ```."
3593
3685
  },
3594
3686
  {
3595
- key: "spRX",
3596
- type: "number"
3687
+ name: "fileBlock",
3688
+ group: "block",
3689
+ wire: "mdc-atom-block",
3690
+ mdcTag: "file",
3691
+ attrs: [
3692
+ str("src"),
3693
+ str("mime"),
3694
+ str("uploadId"),
3695
+ str("filename")
3696
+ ],
3697
+ doc: "Wire form `:file{src=… mime=… upload-id=… filename=…}`; binary in sidecar."
3597
3698
  },
3598
3699
  {
3599
- key: "spRY",
3600
- type: "number"
3700
+ name: "badge",
3701
+ group: "inline",
3702
+ wire: "mdc-atom-inl",
3703
+ attrs: [
3704
+ str("label"),
3705
+ str("color"),
3706
+ str("variant", "subtle")
3707
+ ],
3708
+ doc: "Wire form `:badge[Label]{color=… variant=…}`."
3601
3709
  },
3602
3710
  {
3603
- key: "spRZ",
3604
- type: "number"
3711
+ name: "proseIcon",
3712
+ group: "inline",
3713
+ wire: "mdc-atom-inl",
3714
+ mdcTag: "icon",
3715
+ attrs: [str("name")],
3716
+ doc: "Wire form `:icon{name=…}`."
3605
3717
  },
3606
3718
  {
3607
- key: "spSX",
3608
- type: "number"
3719
+ name: "kbd",
3720
+ group: "inline",
3721
+ wire: "mdc-atom-inl",
3722
+ attrs: [str("value")],
3723
+ doc: "Wire form `:kbd{value=…}`."
3724
+ }
3725
+ ];
3726
+ const NODE_SPECS = [
3727
+ ...VANILLA_BLOCKS,
3728
+ ...MDC_CONTAINERS,
3729
+ ...INLINE_AND_SPECIAL
3730
+ ];
3731
+ const NODE_SPEC_BY_NAME = new Map(NODE_SPECS.map((spec) => [spec.name, spec]));
3732
+
3733
+ //#endregion
3734
+ //#region packages/convert/src/spec/marks.ts
3735
+ const MARK_SPECS = [
3736
+ {
3737
+ name: "bold",
3738
+ wire: "delimited",
3739
+ delim: "**"
3609
3740
  },
3610
3741
  {
3611
- key: "spSY",
3612
- type: "number"
3742
+ name: "italic",
3743
+ wire: "delimited",
3744
+ delim: "*"
3613
3745
  },
3614
3746
  {
3615
- key: "spSZ",
3616
- type: "number"
3747
+ name: "strike",
3748
+ wire: "delimited",
3749
+ delim: "~~"
3617
3750
  },
3618
3751
  {
3619
- key: "spShape",
3620
- type: "string-enum",
3621
- values: [
3622
- "box",
3623
- "sphere",
3624
- "cylinder",
3625
- "cone",
3626
- "plane",
3627
- "torus",
3628
- "glb"
3629
- ]
3752
+ name: "code",
3753
+ wire: "delimited",
3754
+ delim: "`"
3630
3755
  },
3631
3756
  {
3632
- key: "spOpacity",
3633
- type: "integer",
3634
- min: 0,
3635
- max: 100
3757
+ name: "link",
3758
+ wire: "link",
3759
+ attrs: [{
3760
+ key: "href",
3761
+ type: "string"
3762
+ }, {
3763
+ key: "title",
3764
+ type: "string",
3765
+ optional: true
3766
+ }]
3636
3767
  },
3637
3768
  {
3638
- key: "spModelUploadId",
3639
- type: "string"
3769
+ name: "underline",
3770
+ wire: "delimited",
3771
+ delim: "__",
3772
+ doc: "Disambiguated from bold by delimiter character. Two underscores = underline; two asterisks = bold."
3640
3773
  },
3641
3774
  {
3642
- key: "spModelDocId",
3643
- type: "string"
3775
+ name: "highlight",
3776
+ wire: "delimited",
3777
+ delim: "==",
3778
+ doc: "Pandoc-style."
3644
3779
  },
3645
3780
  {
3646
- key: "slidesTransition",
3647
- type: "string-enum",
3648
- values: [
3649
- "none",
3650
- "fade",
3651
- "slide"
3652
- ]
3781
+ name: "subscript",
3782
+ wire: "delimited",
3783
+ delim: "~",
3784
+ doc: "Single tilde; double tilde is strike."
3653
3785
  },
3654
3786
  {
3655
- key: "slidesTheme",
3656
- type: "string-enum",
3657
- values: ["dark", "light"]
3787
+ name: "superscript",
3788
+ wire: "delimited",
3789
+ delim: "^"
3658
3790
  },
3659
3791
  {
3660
- key: "__schemaVersion",
3661
- type: "integer",
3662
- min: 0
3792
+ name: "textStyle",
3793
+ wire: "mdc-span",
3794
+ attrs: [
3795
+ {
3796
+ key: "color",
3797
+ type: "string",
3798
+ optional: true
3799
+ },
3800
+ {
3801
+ key: "backgroundColor",
3802
+ type: "string",
3803
+ optional: true
3804
+ },
3805
+ {
3806
+ key: "fontSize",
3807
+ type: "string",
3808
+ optional: true
3809
+ },
3810
+ {
3811
+ key: "fontFamily",
3812
+ type: "string",
3813
+ optional: true
3814
+ }
3815
+ ],
3816
+ doc: "Wire form `:span[text]{color=\"…\" font-size=\"…\"}`. Any of the attrs may be set."
3663
3817
  }
3664
3818
  ];
3665
- const UNIVERSAL_META_KEY_NAMES = new Set(UNIVERSAL_META_KEYS.map((k) => k.key));
3819
+ const MARK_SPEC_BY_NAME = new Map(MARK_SPECS.map((spec) => [spec.name, spec]));
3820
+ const MARK_SPEC_BY_DELIM = new Map(MARK_SPECS.filter((spec) => spec.wire === "delimited" && !!spec.delim).map((spec) => [spec.delim, spec]));
3666
3821
 
3667
3822
  //#endregion
3668
3823
  //#region packages/cli/src/commands/documents.ts