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