@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.
- package/README.md +28 -0
- package/dist/abracadabra-convert.cjs +91 -62
- package/dist/abracadabra-convert.cjs.map +1 -1
- package/dist/abracadabra-convert.esm.js +91 -62
- package/dist/abracadabra-convert.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/file-blocks/paths.ts +26 -3
- package/src/markdown-to-yjs.ts +44 -10
- package/src/yjs-to-markdown.ts +89 -44
|
@@ -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
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
705
|
-
|
|
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
|
-
|
|
708
|
-
|
|
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
|
|
1125
|
-
else if (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
|
|
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
|
|
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
|
|
1211
|
-
case "codeCollapse": return `::code-collapse\n${el.toArray().filter((c) => c
|
|
1212
|
-
case "codeGroup": return `::code-group\n${el.toArray().filter((c) => c
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1299
|
-
return cell.toArray().filter((c) => c
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1406
|
-
else if (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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1441
|
-
return `<${tag}>${cell.toArray().filter((c) => c
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
|
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
|
|
1493
|
-
const text = child.toArray().find((c) => c
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
}
|