@abraca/convert 2.4.0 → 2.5.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.
@@ -102,6 +102,23 @@ function parseFrontmatter(markdown) {
102
102
  body
103
103
  };
104
104
  }
105
+ function pushNested(out, inner, wrap) {
106
+ const children = parseInline(inner);
107
+ if (children.length === 0) {
108
+ out.push({
109
+ text: inner,
110
+ attrs: { ...wrap }
111
+ });
112
+ return;
113
+ }
114
+ for (const child of children) out.push({
115
+ text: child.text,
116
+ attrs: {
117
+ ...child.attrs ?? {},
118
+ ...wrap
119
+ }
120
+ });
121
+ }
105
122
  function parseInline(text) {
106
123
  const stripped = text.replace(/\{lang="[^"]*"\}/g, "").replace(/:(?!badge|icon|kbd)(\w[\w-]*)\[([^\]]*)\](\{[^}]*\})?/g, "$2").replace(/:(?!badge|icon|kbd)(\w[\w-]*)(\{[^}]*\})/g, "");
107
124
  const tokens = [];
@@ -150,22 +167,10 @@ function parseInline(text) {
150
167
  text: label,
151
168
  attrs: { docLink: { docId } }
152
169
  });
153
- } else if (match[10] !== void 0) tokens.push({
154
- text: match[10],
155
- attrs: { strike: true }
156
- });
157
- else if (match[11] !== void 0) tokens.push({
158
- text: match[11],
159
- attrs: { bold: true }
160
- });
161
- else if (match[12] !== void 0) tokens.push({
162
- text: match[12],
163
- attrs: { italic: true }
164
- });
165
- else if (match[13] !== void 0) tokens.push({
166
- text: match[13],
167
- attrs: { italic: true }
168
- });
170
+ } else if (match[10] !== void 0) pushNested(tokens, match[10], { strike: true });
171
+ else if (match[11] !== void 0) pushNested(tokens, match[11], { bold: true });
172
+ else if (match[12] !== void 0) pushNested(tokens, match[12], { italic: true });
173
+ else if (match[13] !== void 0) pushNested(tokens, match[13], { italic: true });
169
174
  else if (match[14] !== void 0) tokens.push({
170
175
  text: match[14],
171
176
  attrs: { code: true }
@@ -701,11 +706,19 @@ function parseBlocks(markdown) {
701
706
  function fillTextInto(el, tokens) {
702
707
  const filtered = tokens.filter((t) => t.text.length > 0);
703
708
  if (!filtered.length) return;
704
- const xtNodes = filtered.map(() => new Y.XmlText());
705
- el.insert(0, xtNodes);
709
+ const children = filtered.map((tok) => {
710
+ return (tok.attrs?.docLink)?.docId ? new Y.XmlElement("docLink") : new Y.XmlText();
711
+ });
712
+ el.insert(0, children);
706
713
  filtered.forEach((tok, i) => {
707
- if (tok.attrs) xtNodes[i].insert(0, tok.text, tok.attrs);
708
- else xtNodes[i].insert(0, tok.text);
714
+ const node = children[i];
715
+ if (node instanceof Y.XmlElement) {
716
+ const dl = tok.attrs.docLink;
717
+ node.setAttribute("docId", dl.docId);
718
+ return;
719
+ }
720
+ if (tok.attrs) node.insert(0, tok.text, tok.attrs);
721
+ else node.insert(0, tok.text);
709
722
  });
710
723
  }
711
724
  function blockElName(b) {
@@ -1061,6 +1074,15 @@ function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled"
1061
1074
 
1062
1075
  //#endregion
1063
1076
  //#region packages/convert/src/yjs-to-markdown.ts
1077
+ function isXElem(n) {
1078
+ return !!n && typeof n.nodeName === "string";
1079
+ }
1080
+ function isXText(n) {
1081
+ return !!n && typeof n.nodeName !== "string" && typeof n.toDelta === "function";
1082
+ }
1083
+ function localizeFragment(fragment) {
1084
+ return fragment;
1085
+ }
1064
1086
  function serializeDelta(delta) {
1065
1087
  let result = "";
1066
1088
  for (const op of delta) {
@@ -1121,12 +1143,15 @@ function serializeDelta(delta) {
1121
1143
  }
1122
1144
  function serializeInline(el) {
1123
1145
  const parts = [];
1124
- for (const child of el.toArray()) if (child instanceof Y.XmlText) parts.push(serializeDelta(child.toDelta()));
1125
- else if (child instanceof Y.XmlElement) parts.push(serializeInline(child));
1146
+ for (const child of el.toArray()) if (isXText(child)) parts.push(serializeDelta(child.toDelta()));
1147
+ else if (isXElem(child)) if (child.nodeName === "docLink") {
1148
+ const docId = child.getAttribute("docId") ?? "";
1149
+ parts.push(`[[${docId}]]`);
1150
+ } else parts.push(serializeInline(child));
1126
1151
  return parts.join("");
1127
1152
  }
1128
1153
  function serializeBlock(el, indent = "") {
1129
- if (el instanceof Y.XmlText) return serializeDelta(el.toDelta());
1154
+ if (isXText(el)) return serializeDelta(el.toDelta());
1130
1155
  switch (el.nodeName) {
1131
1156
  case "documentHeader":
1132
1157
  case "documentMeta": return "";
@@ -1146,7 +1171,7 @@ function serializeBlock(el, indent = "") {
1146
1171
  }
1147
1172
  case "blockquote": {
1148
1173
  const lines = [];
1149
- for (const child of el.toArray()) if (child instanceof Y.XmlElement) {
1174
+ for (const child of el.toArray()) if (isXElem(child)) {
1150
1175
  const text = serializeBlock(child);
1151
1176
  for (const line of text.split("\n")) lines.push(`> ${line}`);
1152
1177
  }
@@ -1207,11 +1232,11 @@ function serializeBlock(el, indent = "") {
1207
1232
  if (to) props.push(`to="${to}"`);
1208
1233
  return `::card${props.length ? `{${props.join(" ")}}` : ""}\n${serializeChildren(el)}\n::`;
1209
1234
  }
1210
- case "cardGroup": return `::card-group\n${el.toArray().filter((c) => c instanceof Y.XmlElement).map((c) => serializeBlock(c)).join("\n\n")}\n::`;
1211
- case "codeCollapse": return `::code-collapse\n${el.toArray().filter((c) => c instanceof Y.XmlElement && c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n")}\n::`;
1212
- case "codeGroup": return `::code-group\n${el.toArray().filter((c) => c instanceof Y.XmlElement && c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n")}\n::`;
1235
+ case "cardGroup": return `::card-group\n${el.toArray().filter((c) => isXElem(c)).map((c) => serializeBlock(c)).join("\n\n")}\n::`;
1236
+ case "codeCollapse": return `::code-collapse\n${el.toArray().filter((c) => isXElem(c) && c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n")}\n::`;
1237
+ case "codeGroup": return `::code-group\n${el.toArray().filter((c) => isXElem(c) && c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n")}\n::`;
1213
1238
  case "codePreview": {
1214
- const children = el.toArray().filter((c) => c instanceof Y.XmlElement);
1239
+ const children = el.toArray().filter((c) => isXElem(c));
1215
1240
  const nonCode = children.filter((c) => c.nodeName !== "codeBlock").map((c) => serializeBlock(c)).join("\n\n");
1216
1241
  const code = children.filter((c) => c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n");
1217
1242
  const parts = [nonCode];
@@ -1229,16 +1254,16 @@ function serializeBlock(el, indent = "") {
1229
1254
  if (required === true || required === "true") props.push("required=\"true\"");
1230
1255
  return `::field{${props.join(" ")}}\n${serializeChildren(el)}\n::`;
1231
1256
  }
1232
- case "fieldGroup": return `::field-group\n${el.toArray().filter((c) => c instanceof Y.XmlElement).map((c) => serializeBlock(c)).join("\n\n")}\n::`;
1257
+ case "fieldGroup": return `::field-group\n${el.toArray().filter((c) => isXElem(c)).map((c) => serializeBlock(c)).join("\n\n")}\n::`;
1233
1258
  default: return serializeChildren(el);
1234
1259
  }
1235
1260
  }
1236
1261
  function serializeChildren(el) {
1237
1262
  const blocks = [];
1238
- for (const child of el.toArray()) if (child instanceof Y.XmlElement) {
1263
+ for (const child of el.toArray()) if (isXElem(child)) {
1239
1264
  const text = serializeBlock(child);
1240
1265
  if (text) blocks.push(text);
1241
- } else if (child instanceof Y.XmlText) {
1266
+ } else if (isXText(child)) {
1242
1267
  const text = serializeDelta(child.toDelta());
1243
1268
  if (text) blocks.push(text);
1244
1269
  }
@@ -1248,11 +1273,11 @@ function serializeListItems(el, type, indent) {
1248
1273
  const lines = [];
1249
1274
  let counter = 1;
1250
1275
  for (const child of el.toArray()) {
1251
- if (!(child instanceof Y.XmlElement) || child.nodeName !== "listItem") continue;
1276
+ if (!isXElem(child) || child.nodeName !== "listItem") continue;
1252
1277
  const prefix = type === "bullet" ? "- " : `${counter++}. `;
1253
1278
  const subParts = [];
1254
1279
  for (const sub of child.toArray()) {
1255
- if (!(sub instanceof Y.XmlElement)) continue;
1280
+ if (!isXElem(sub)) continue;
1256
1281
  if (sub.nodeName === "bulletList") subParts.push(serializeListItems(sub, "bullet", indent + " "));
1257
1282
  else if (sub.nodeName === "orderedList") subParts.push(serializeListItems(sub, "ordered", indent + " "));
1258
1283
  else subParts.push(serializeInline(sub));
@@ -1268,13 +1293,13 @@ function serializeListItems(el, type, indent) {
1268
1293
  function serializeTaskList(el, indent) {
1269
1294
  const lines = [];
1270
1295
  for (const child of el.toArray()) {
1271
- if (!(child instanceof Y.XmlElement) || child.nodeName !== "taskItem") continue;
1296
+ if (!isXElem(child) || child.nodeName !== "taskItem") continue;
1272
1297
  const checked = child.getAttribute("checked");
1273
1298
  const marker = checked === true || checked === "true" ? "[x]" : "[ ]";
1274
1299
  let header = "";
1275
1300
  const nestedParts = [];
1276
1301
  for (const sub of child.toArray()) {
1277
- if (!(sub instanceof Y.XmlElement)) continue;
1302
+ if (!isXElem(sub)) continue;
1278
1303
  if (sub.nodeName === "paragraph" && header === "") header = serializeInline(sub);
1279
1304
  else if (sub.nodeName === "bulletList") nestedParts.push(serializeListItems(sub, "bullet", indent + " "));
1280
1305
  else if (sub.nodeName === "orderedList") nestedParts.push(serializeListItems(sub, "ordered", indent + " "));
@@ -1287,16 +1312,16 @@ function serializeTaskList(el, indent) {
1287
1312
  return lines.join("\n");
1288
1313
  }
1289
1314
  function getCodeBlockText(el) {
1290
- for (const child of el.toArray()) if (child instanceof Y.XmlText) return child.toString();
1315
+ for (const child of el.toArray()) if (isXText(child)) return child.toString();
1291
1316
  return "";
1292
1317
  }
1293
1318
  function serializeTable(el) {
1294
- const rows = el.toArray().filter((c) => c instanceof Y.XmlElement);
1319
+ const rows = el.toArray().filter((c) => isXElem(c));
1295
1320
  if (!rows.length) return "";
1296
1321
  const serializedRows = [];
1297
1322
  for (const row of rows) {
1298
- const cells = row.toArray().filter((c) => c instanceof Y.XmlElement).map((cell) => {
1299
- return cell.toArray().filter((c) => c instanceof Y.XmlElement).map((c) => serializeInline(c)).join(" ");
1323
+ const cells = row.toArray().filter((c) => isXElem(c)).map((cell) => {
1324
+ return cell.toArray().filter((c) => isXElem(c)).map((c) => serializeInline(c)).join(" ");
1300
1325
  });
1301
1326
  serializedRows.push(cells);
1302
1327
  }
@@ -1315,7 +1340,7 @@ function serializeTable(el) {
1315
1340
  ].join("\n");
1316
1341
  }
1317
1342
  function serializeSlottedContainer(el, containerName, childName, slotPrefix) {
1318
- return `::${containerName}\n${el.toArray().filter((c) => c instanceof Y.XmlElement && c.nodeName === childName).map((item) => {
1343
+ return `::${containerName}\n${el.toArray().filter((c) => isXElem(c) && c.nodeName === childName).map((item) => {
1319
1344
  const label = item.getAttribute("label") ?? "";
1320
1345
  const icon = item.getAttribute("icon") ?? "";
1321
1346
  const props = [];
@@ -1365,7 +1390,7 @@ function escapeYaml(s) {
1365
1390
  return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
1366
1391
  }
1367
1392
  function serializeBlockToHtml(el) {
1368
- if (el instanceof Y.XmlText) return serializeDeltaToHtml(el.toDelta());
1393
+ if (isXText(el)) return serializeDeltaToHtml(el.toDelta());
1369
1394
  const name = el.nodeName;
1370
1395
  switch (name) {
1371
1396
  case "documentHeader":
@@ -1379,11 +1404,11 @@ function serializeBlockToHtml(el) {
1379
1404
  case "orderedList": return `<ol>${serializeListHtml(el)}</ol>`;
1380
1405
  case "taskList": return `<ul>${serializeTaskListHtml(el)}</ul>`;
1381
1406
  case "codeBlock": {
1382
- const lang = el.getAttribute("language") ?? "";
1407
+ const lang = (el.getAttribute("language") ?? "").replace(/[^\w.-]/g, "");
1383
1408
  const code = escapeHtml(getCodeBlockText(el));
1384
1409
  return lang ? `<pre><code class="language-${lang}">${code}</code></pre>` : `<pre><code>${code}</code></pre>`;
1385
1410
  }
1386
- case "blockquote": return `<blockquote>\n${el.toArray().filter((c) => c instanceof Y.XmlElement).map((c) => serializeBlockToHtml(c)).join("\n")}\n</blockquote>`;
1411
+ case "blockquote": return `<blockquote>\n${el.toArray().filter((c) => isXElem(c)).map((c) => serializeBlockToHtml(c)).join("\n")}\n</blockquote>`;
1387
1412
  case "table": return serializeTableHtml(el);
1388
1413
  case "horizontalRule": return "<hr>";
1389
1414
  case "image": {
@@ -1397,13 +1422,13 @@ function serializeBlockToHtml(el) {
1397
1422
  if (uploadId) return `<!--fileblock:${uploadId}:${filename}-->`;
1398
1423
  return `<!-- file: ${filename} -->`;
1399
1424
  }
1400
- default: return `<div data-type="${name}">\n${el.toArray().filter((c) => c instanceof Y.XmlElement || c instanceof Y.XmlText).map((c) => c instanceof Y.XmlElement ? serializeBlockToHtml(c) : serializeDeltaToHtml(c.toDelta())).join("\n")}\n</div>`;
1425
+ default: return `<div data-type="${name}">\n${el.toArray().filter((c) => isXElem(c) || isXText(c)).map((c) => isXElem(c) ? serializeBlockToHtml(c) : serializeDeltaToHtml(c.toDelta())).join("\n")}\n</div>`;
1401
1426
  }
1402
1427
  }
1403
1428
  function serializeInlineHtml(el) {
1404
1429
  const parts = [];
1405
- for (const child of el.toArray()) if (child instanceof Y.XmlText) parts.push(serializeDeltaToHtml(child.toDelta()));
1406
- else if (child instanceof Y.XmlElement) parts.push(serializeInlineHtml(child));
1430
+ for (const child of el.toArray()) if (isXText(child)) parts.push(serializeDeltaToHtml(child.toDelta()));
1431
+ else if (isXElem(child)) parts.push(serializeInlineHtml(child));
1407
1432
  return parts.join("");
1408
1433
  }
1409
1434
  function serializeDeltaToHtml(delta) {
@@ -1422,23 +1447,23 @@ function serializeDeltaToHtml(delta) {
1422
1447
  return result;
1423
1448
  }
1424
1449
  function serializeListHtml(el) {
1425
- return el.toArray().filter((c) => c instanceof Y.XmlElement && c.nodeName === "listItem").map((li) => `<li>${li.toArray().filter((c) => c instanceof Y.XmlElement).map((c) => serializeBlockToHtml(c)).join("")}</li>`).join("\n");
1450
+ return el.toArray().filter((c) => isXElem(c) && c.nodeName === "listItem").map((li) => `<li>${li.toArray().filter((c) => isXElem(c)).map((c) => serializeBlockToHtml(c)).join("")}</li>`).join("\n");
1426
1451
  }
1427
1452
  function serializeTaskListHtml(el) {
1428
- return el.toArray().filter((c) => c instanceof Y.XmlElement && c.nodeName === "taskItem").map((ti) => {
1453
+ return el.toArray().filter((c) => isXElem(c) && c.nodeName === "taskItem").map((ti) => {
1429
1454
  const rawChecked = ti.getAttribute("checked");
1430
1455
  const checked = rawChecked === true || rawChecked === "true";
1431
- const text = ti.toArray().filter((c) => c instanceof Y.XmlElement).map((c) => serializeInlineHtml(c)).join("");
1456
+ const text = ti.toArray().filter((c) => isXElem(c)).map((c) => serializeInlineHtml(c)).join("");
1432
1457
  return `<li><input type="checkbox"${checked ? " checked" : ""} disabled> ${text}</li>`;
1433
1458
  }).join("\n");
1434
1459
  }
1435
1460
  function serializeTableHtml(el) {
1436
- const rows = el.toArray().filter((c) => c instanceof Y.XmlElement);
1461
+ const rows = el.toArray().filter((c) => isXElem(c));
1437
1462
  if (!rows.length) return "";
1438
1463
  return `<table>\n${rows.map((row, ri) => {
1439
1464
  const tag = ri === 0 ? "th" : "td";
1440
- return `<tr>${row.toArray().filter((c) => c instanceof Y.XmlElement).map((cell) => {
1441
- return `<${tag}>${cell.toArray().filter((c) => c instanceof Y.XmlElement).map((c) => serializeInlineHtml(c)).join("")}</${tag}>`;
1465
+ return `<tr>${row.toArray().filter((c) => isXElem(c)).map((cell) => {
1466
+ return `<${tag}>${cell.toArray().filter((c) => isXElem(c)).map((c) => serializeInlineHtml(c)).join("")}</${tag}>`;
1442
1467
  }).join("")}</tr>`;
1443
1468
  }).join("\n")}\n</table>`;
1444
1469
  }
@@ -1446,6 +1471,7 @@ function escapeHtml(s) {
1446
1471
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1447
1472
  }
1448
1473
  function yjsToMarkdown(fragment, label, meta, type) {
1474
+ fragment = localizeFragment(fragment);
1449
1475
  const { text: headerText, source: titleSource } = readDocumentHeader(fragment);
1450
1476
  const effectiveTitle = headerText || label;
1451
1477
  const docMeta = readDocumentMeta(fragment);
@@ -1469,7 +1495,7 @@ function readDocumentMeta(fragment) {
1469
1495
  const meta = {};
1470
1496
  let type;
1471
1497
  for (const child of fragment.toArray()) {
1472
- if (!(child instanceof Y.XmlElement) || child.nodeName !== "documentMeta") continue;
1498
+ if (!isXElem(child) || child.nodeName !== "documentMeta") continue;
1473
1499
  const attrs = child.getAttributes();
1474
1500
  for (const k of Object.keys(attrs)) {
1475
1501
  const v = attrs[k];
@@ -1489,8 +1515,8 @@ function readDocumentMeta(fragment) {
1489
1515
  }
1490
1516
  function readDocumentHeader(fragment) {
1491
1517
  for (const child of fragment.toArray()) {
1492
- if (!(child instanceof Y.XmlElement) || child.nodeName !== "documentHeader") continue;
1493
- const text = child.toArray().find((c) => c instanceof Y.XmlText);
1518
+ if (!isXElem(child) || child.nodeName !== "documentHeader") continue;
1519
+ const text = child.toArray().find((c) => isXText(c));
1494
1520
  const src = child.getAttribute("titleSource");
1495
1521
  const source = src === "h1" || src === "frontmatter" ? src : void 0;
1496
1522
  return {
@@ -1503,7 +1529,7 @@ function readDocumentHeader(fragment) {
1503
1529
  function collectBodyBlocks(fragment) {
1504
1530
  const out = [];
1505
1531
  for (const child of fragment.toArray()) {
1506
- if (!(child instanceof Y.XmlElement)) continue;
1532
+ if (!isXElem(child)) continue;
1507
1533
  if (child.nodeName === "documentHeader" || child.nodeName === "documentMeta") continue;
1508
1534
  out.push(child);
1509
1535
  }
@@ -1538,9 +1564,10 @@ function isMetaEmpty(meta) {
1538
1564
  * accessibility tooling, search indexing, and snippet previews.
1539
1565
  */
1540
1566
  function yjsToPlainText(fragment) {
1567
+ fragment = localizeFragment(fragment);
1541
1568
  const out = [];
1542
1569
  const visit = (node) => {
1543
- if (node instanceof Y.XmlText) {
1570
+ if (isXText(node)) {
1544
1571
  out.push(node.toString());
1545
1572
  return;
1546
1573
  }
@@ -1550,17 +1577,18 @@ function yjsToPlainText(fragment) {
1550
1577
  if (alt) out.push(alt);
1551
1578
  return;
1552
1579
  }
1553
- for (const child of node.toArray()) if (child instanceof Y.XmlText || child instanceof Y.XmlElement) visit(child);
1580
+ for (const child of node.toArray()) if (isXText(child) || isXElem(child)) visit(child);
1554
1581
  if (node.nodeName !== "paragraph" && node.length === 0) return;
1555
1582
  out.push("\n");
1556
1583
  };
1557
- for (const child of fragment.toArray()) if (child instanceof Y.XmlText || child instanceof Y.XmlElement) visit(child);
1584
+ for (const child of fragment.toArray()) if (isXText(child) || isXElem(child)) visit(child);
1558
1585
  return out.join("").replace(/\n+$/, "").replace(/\n{3,}/g, "\n\n");
1559
1586
  }
1560
1587
  function yjsToHtml(fragment, label) {
1588
+ fragment = localizeFragment(fragment);
1561
1589
  const title = escapeHtml(label);
1562
1590
  const bodyParts = [];
1563
- for (const child of fragment.toArray()) if (child instanceof Y.XmlElement) {
1591
+ for (const child of fragment.toArray()) if (isXElem(child)) {
1564
1592
  const html = serializeBlockToHtml(child);
1565
1593
  if (html) bodyParts.push(html);
1566
1594
  }
@@ -3046,7 +3074,7 @@ function buildReverseLookup(manifest) {
3046
3074
  * e.g. "My Project!" -> "my-project"
3047
3075
  */
3048
3076
  function labelToFilename(label) {
3049
- return label.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "untitled";
3077
+ return String(label ?? "").toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "untitled";
3050
3078
  }
3051
3079
  /**
3052
3080
  * Convert a filename back to a label (best-effort).
@@ -3145,7 +3173,8 @@ function simpleHash(str) {
3145
3173
  function getTreeData(treeMap) {
3146
3174
  const data = {};
3147
3175
  treeMap.forEach((val, key) => {
3148
- if (val && typeof val === "object") data[key] = val;
3176
+ const plain = val instanceof Y.Map || !!val && typeof val === "object" && typeof val.toJSON === "function" && typeof val.get === "function" ? val.toJSON() : val;
3177
+ if (plain && typeof plain === "object") data[key] = plain;
3149
3178
  });
3150
3179
  return data;
3151
3180
  }