docx-diff-editor 1.0.46 → 1.0.48
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/dist/index.d.mts +24 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +149 -30
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +149 -30
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -29,6 +29,10 @@ type DocxContent = File | ProseMirrorJSON | string;
|
|
|
29
29
|
interface DiffSegment {
|
|
30
30
|
type: 'equal' | 'insert' | 'delete';
|
|
31
31
|
text: string;
|
|
32
|
+
/** Position in docA where this segment starts (for equal/delete segments) */
|
|
33
|
+
posA?: number;
|
|
34
|
+
/** Position in docB where this segment starts (for equal/insert segments) */
|
|
35
|
+
posB?: number;
|
|
32
36
|
}
|
|
33
37
|
/**
|
|
34
38
|
* A format change on unchanged text
|
|
@@ -54,6 +58,8 @@ interface DiffResult {
|
|
|
54
58
|
textB: string;
|
|
55
59
|
/** Human-readable summary */
|
|
56
60
|
summary: string[];
|
|
61
|
+
/** Text spans from docB with marks (for mark preservation during merge) */
|
|
62
|
+
spansB?: TextSpan[];
|
|
57
63
|
}
|
|
58
64
|
/**
|
|
59
65
|
* Type of structural change
|
|
@@ -365,6 +371,15 @@ interface DocxDiffEditorRef {
|
|
|
365
371
|
/** Parse HTML string to ProseMirror JSON (uses hidden SuperDoc instance) */
|
|
366
372
|
parseHtml(html: string): Promise<ProseMirrorJSON>;
|
|
367
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* Text span with position and marks (used in diffing)
|
|
376
|
+
*/
|
|
377
|
+
interface TextSpan {
|
|
378
|
+
text: string;
|
|
379
|
+
from: number;
|
|
380
|
+
to: number;
|
|
381
|
+
marks: ProseMirrorMark[];
|
|
382
|
+
}
|
|
368
383
|
|
|
369
384
|
/**
|
|
370
385
|
* DocxDiffEditor Component
|
|
@@ -454,6 +469,10 @@ declare function parseDocxFile(file: File, SuperDoc: SuperDocConstructor): Promi
|
|
|
454
469
|
/**
|
|
455
470
|
* Diff two ProseMirror JSON documents at the character level.
|
|
456
471
|
* Detects both text changes and formatting changes.
|
|
472
|
+
*
|
|
473
|
+
* Now also tracks positions in both documents for mark preservation:
|
|
474
|
+
* - posA: position in docA (for equal/delete segments)
|
|
475
|
+
* - posB: position in docB (for equal/insert segments)
|
|
457
476
|
*/
|
|
458
477
|
declare function diffDocuments(docA: ProseMirrorJSON, docB: ProseMirrorJSON): DiffResult;
|
|
459
478
|
|
|
@@ -494,6 +513,11 @@ declare function createTrackInsertMark(author?: TrackChangeAuthor, id?: string):
|
|
|
494
513
|
declare function createTrackDeleteMark(author?: TrackChangeAuthor, id?: string): ProseMirrorMark;
|
|
495
514
|
/**
|
|
496
515
|
* Create a trackFormat mark.
|
|
516
|
+
*
|
|
517
|
+
* Note: SuperDoc's parseFormatList requires all marks in before/after arrays
|
|
518
|
+
* to have both `type` and `attrs` properties. Marks without `attrs` get filtered out,
|
|
519
|
+
* causing empty values in track change bubbles. We normalize marks here to ensure
|
|
520
|
+
* all have at least an empty `attrs` object.
|
|
497
521
|
*/
|
|
498
522
|
declare function createTrackFormatMark(before: ProseMirrorMark[], after: ProseMirrorMark[], author?: TrackChangeAuthor): ProseMirrorMark;
|
|
499
523
|
|
package/dist/index.d.ts
CHANGED
|
@@ -29,6 +29,10 @@ type DocxContent = File | ProseMirrorJSON | string;
|
|
|
29
29
|
interface DiffSegment {
|
|
30
30
|
type: 'equal' | 'insert' | 'delete';
|
|
31
31
|
text: string;
|
|
32
|
+
/** Position in docA where this segment starts (for equal/delete segments) */
|
|
33
|
+
posA?: number;
|
|
34
|
+
/** Position in docB where this segment starts (for equal/insert segments) */
|
|
35
|
+
posB?: number;
|
|
32
36
|
}
|
|
33
37
|
/**
|
|
34
38
|
* A format change on unchanged text
|
|
@@ -54,6 +58,8 @@ interface DiffResult {
|
|
|
54
58
|
textB: string;
|
|
55
59
|
/** Human-readable summary */
|
|
56
60
|
summary: string[];
|
|
61
|
+
/** Text spans from docB with marks (for mark preservation during merge) */
|
|
62
|
+
spansB?: TextSpan[];
|
|
57
63
|
}
|
|
58
64
|
/**
|
|
59
65
|
* Type of structural change
|
|
@@ -365,6 +371,15 @@ interface DocxDiffEditorRef {
|
|
|
365
371
|
/** Parse HTML string to ProseMirror JSON (uses hidden SuperDoc instance) */
|
|
366
372
|
parseHtml(html: string): Promise<ProseMirrorJSON>;
|
|
367
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* Text span with position and marks (used in diffing)
|
|
376
|
+
*/
|
|
377
|
+
interface TextSpan {
|
|
378
|
+
text: string;
|
|
379
|
+
from: number;
|
|
380
|
+
to: number;
|
|
381
|
+
marks: ProseMirrorMark[];
|
|
382
|
+
}
|
|
368
383
|
|
|
369
384
|
/**
|
|
370
385
|
* DocxDiffEditor Component
|
|
@@ -454,6 +469,10 @@ declare function parseDocxFile(file: File, SuperDoc: SuperDocConstructor): Promi
|
|
|
454
469
|
/**
|
|
455
470
|
* Diff two ProseMirror JSON documents at the character level.
|
|
456
471
|
* Detects both text changes and formatting changes.
|
|
472
|
+
*
|
|
473
|
+
* Now also tracks positions in both documents for mark preservation:
|
|
474
|
+
* - posA: position in docA (for equal/delete segments)
|
|
475
|
+
* - posB: position in docB (for equal/insert segments)
|
|
457
476
|
*/
|
|
458
477
|
declare function diffDocuments(docA: ProseMirrorJSON, docB: ProseMirrorJSON): DiffResult;
|
|
459
478
|
|
|
@@ -494,6 +513,11 @@ declare function createTrackInsertMark(author?: TrackChangeAuthor, id?: string):
|
|
|
494
513
|
declare function createTrackDeleteMark(author?: TrackChangeAuthor, id?: string): ProseMirrorMark;
|
|
495
514
|
/**
|
|
496
515
|
* Create a trackFormat mark.
|
|
516
|
+
*
|
|
517
|
+
* Note: SuperDoc's parseFormatList requires all marks in before/after arrays
|
|
518
|
+
* to have both `type` and `attrs` properties. Marks without `attrs` get filtered out,
|
|
519
|
+
* causing empty values in track change bubbles. We normalize marks here to ensure
|
|
520
|
+
* all have at least an empty `attrs` object.
|
|
497
521
|
*/
|
|
498
522
|
declare function createTrackFormatMark(before: ProseMirrorMark[], after: ProseMirrorMark[], author?: TrackChangeAuthor): ProseMirrorMark;
|
|
499
523
|
|
package/dist/index.js
CHANGED
|
@@ -1002,14 +1002,20 @@ function diffDocuments(docA, docB) {
|
|
|
1002
1002
|
const segments = [];
|
|
1003
1003
|
let insertCount = 0;
|
|
1004
1004
|
let deleteCount = 0;
|
|
1005
|
+
let posA = 0;
|
|
1006
|
+
let posB = 0;
|
|
1005
1007
|
for (const [op, text] of diffs) {
|
|
1006
1008
|
if (op === DIFF_EQUAL) {
|
|
1007
|
-
segments.push({ type: "equal", text });
|
|
1009
|
+
segments.push({ type: "equal", text, posA, posB });
|
|
1010
|
+
posA += text.length;
|
|
1011
|
+
posB += text.length;
|
|
1008
1012
|
} else if (op === DIFF_INSERT) {
|
|
1009
|
-
segments.push({ type: "insert", text });
|
|
1013
|
+
segments.push({ type: "insert", text, posB });
|
|
1014
|
+
posB += text.length;
|
|
1010
1015
|
insertCount++;
|
|
1011
1016
|
} else if (op === DIFF_DELETE) {
|
|
1012
|
-
segments.push({ type: "delete", text });
|
|
1017
|
+
segments.push({ type: "delete", text, posA });
|
|
1018
|
+
posA += text.length;
|
|
1013
1019
|
deleteCount++;
|
|
1014
1020
|
}
|
|
1015
1021
|
}
|
|
@@ -1034,7 +1040,9 @@ function diffDocuments(docA, docB) {
|
|
|
1034
1040
|
formatChanges,
|
|
1035
1041
|
textA,
|
|
1036
1042
|
textB,
|
|
1037
|
-
summary
|
|
1043
|
+
summary,
|
|
1044
|
+
spansB
|
|
1045
|
+
// Include docB spans for mark preservation during merge
|
|
1038
1046
|
};
|
|
1039
1047
|
}
|
|
1040
1048
|
|
|
@@ -1295,6 +1303,34 @@ function groupReplacements(changes) {
|
|
|
1295
1303
|
}
|
|
1296
1304
|
return result;
|
|
1297
1305
|
}
|
|
1306
|
+
function ensureValidCssColor(color) {
|
|
1307
|
+
if (typeof color !== "string" || !color) {
|
|
1308
|
+
return void 0;
|
|
1309
|
+
}
|
|
1310
|
+
if (/^[0-9a-fA-F]{6}$/.test(color)) {
|
|
1311
|
+
return `#${color}`;
|
|
1312
|
+
}
|
|
1313
|
+
if (/^[0-9a-fA-F]{3}$/.test(color)) {
|
|
1314
|
+
return `#${color}`;
|
|
1315
|
+
}
|
|
1316
|
+
return color;
|
|
1317
|
+
}
|
|
1318
|
+
function normalizeMark(mark) {
|
|
1319
|
+
const attrs = { ...mark.attrs || {} };
|
|
1320
|
+
if (attrs.color !== void 0) {
|
|
1321
|
+
attrs.color = ensureValidCssColor(attrs.color);
|
|
1322
|
+
}
|
|
1323
|
+
return {
|
|
1324
|
+
type: mark.type,
|
|
1325
|
+
attrs
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
function normalizeMarks(marks) {
|
|
1329
|
+
return marks.map(normalizeMark);
|
|
1330
|
+
}
|
|
1331
|
+
function normalizeMarksForRendering(marks) {
|
|
1332
|
+
return normalizeMarks(marks);
|
|
1333
|
+
}
|
|
1298
1334
|
function createTrackInsertMark(author = DEFAULT_AUTHOR, id) {
|
|
1299
1335
|
return {
|
|
1300
1336
|
type: "trackInsert",
|
|
@@ -1320,6 +1356,8 @@ function createTrackDeleteMark(author = DEFAULT_AUTHOR, id) {
|
|
|
1320
1356
|
};
|
|
1321
1357
|
}
|
|
1322
1358
|
function createTrackFormatMark(before, after, author = DEFAULT_AUTHOR) {
|
|
1359
|
+
const normalizedBefore = normalizeMarks(before);
|
|
1360
|
+
const normalizedAfter = normalizeMarks(after);
|
|
1323
1361
|
return {
|
|
1324
1362
|
type: "trackFormat",
|
|
1325
1363
|
attrs: {
|
|
@@ -1328,8 +1366,8 @@ function createTrackFormatMark(before, after, author = DEFAULT_AUTHOR) {
|
|
|
1328
1366
|
authorEmail: author.email,
|
|
1329
1367
|
authorImage: "",
|
|
1330
1368
|
date: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1331
|
-
before,
|
|
1332
|
-
after
|
|
1369
|
+
before: normalizedBefore,
|
|
1370
|
+
after: normalizedAfter
|
|
1333
1371
|
}
|
|
1334
1372
|
};
|
|
1335
1373
|
}
|
|
@@ -1516,6 +1554,70 @@ function alignListItems(listA, listB, listPathA, listPathB) {
|
|
|
1516
1554
|
function cloneNode(node) {
|
|
1517
1555
|
return JSON.parse(JSON.stringify(node));
|
|
1518
1556
|
}
|
|
1557
|
+
function getMarkSpansForRange(spansB, start, end) {
|
|
1558
|
+
const result = [];
|
|
1559
|
+
for (const span of spansB) {
|
|
1560
|
+
if (span.to > start && span.from < end) {
|
|
1561
|
+
const overlapStart = Math.max(span.from, start);
|
|
1562
|
+
const overlapEnd = Math.min(span.to, end);
|
|
1563
|
+
result.push({
|
|
1564
|
+
relStart: overlapStart - start,
|
|
1565
|
+
relEnd: overlapEnd - start,
|
|
1566
|
+
marks: span.marks || []
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
return result;
|
|
1571
|
+
}
|
|
1572
|
+
function createInsertedTextNodes(text, posB, spansB, author, replacementId) {
|
|
1573
|
+
const result = [];
|
|
1574
|
+
const trackMark = createTrackInsertMark(author, replacementId);
|
|
1575
|
+
if (posB === void 0 || spansB.length === 0) {
|
|
1576
|
+
return [{
|
|
1577
|
+
type: "text",
|
|
1578
|
+
text,
|
|
1579
|
+
marks: [trackMark]
|
|
1580
|
+
}];
|
|
1581
|
+
}
|
|
1582
|
+
const markSpans = getMarkSpansForRange(spansB, posB, posB + text.length);
|
|
1583
|
+
if (markSpans.length === 0) {
|
|
1584
|
+
return [{
|
|
1585
|
+
type: "text",
|
|
1586
|
+
text,
|
|
1587
|
+
marks: [trackMark]
|
|
1588
|
+
}];
|
|
1589
|
+
}
|
|
1590
|
+
markSpans.sort((a, b) => a.relStart - b.relStart);
|
|
1591
|
+
let processedUpTo = 0;
|
|
1592
|
+
for (const span of markSpans) {
|
|
1593
|
+
if (span.relStart > processedUpTo) {
|
|
1594
|
+
result.push({
|
|
1595
|
+
type: "text",
|
|
1596
|
+
text: text.substring(processedUpTo, span.relStart),
|
|
1597
|
+
marks: [trackMark]
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
if (span.relEnd > span.relStart) {
|
|
1601
|
+
const spanText = text.substring(span.relStart, span.relEnd);
|
|
1602
|
+
const normalizedSpanMarks = normalizeMarksForRendering(span.marks);
|
|
1603
|
+
const marks = [...normalizedSpanMarks, trackMark];
|
|
1604
|
+
result.push({
|
|
1605
|
+
type: "text",
|
|
1606
|
+
text: spanText,
|
|
1607
|
+
marks
|
|
1608
|
+
});
|
|
1609
|
+
processedUpTo = span.relEnd;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
if (processedUpTo < text.length) {
|
|
1613
|
+
result.push({
|
|
1614
|
+
type: "text",
|
|
1615
|
+
text: text.substring(processedUpTo),
|
|
1616
|
+
marks: [trackMark]
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
return result;
|
|
1620
|
+
}
|
|
1519
1621
|
function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
|
|
1520
1622
|
const merged = cloneNode(docA);
|
|
1521
1623
|
const charStates = [];
|
|
@@ -1550,17 +1652,22 @@ function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
|
|
|
1550
1652
|
insertions.push({
|
|
1551
1653
|
afterOffset: docAOffset,
|
|
1552
1654
|
text: nextSegment.text,
|
|
1553
|
-
replacementId
|
|
1655
|
+
replacementId,
|
|
1656
|
+
posB: nextSegment.posB
|
|
1657
|
+
// Capture docB position for mark lookup
|
|
1554
1658
|
});
|
|
1555
1659
|
segIdx++;
|
|
1556
1660
|
}
|
|
1557
1661
|
} else if (segment.type === "insert") {
|
|
1558
1662
|
insertions.push({
|
|
1559
1663
|
afterOffset: docAOffset,
|
|
1560
|
-
text: segment.text
|
|
1664
|
+
text: segment.text,
|
|
1665
|
+
posB: segment.posB
|
|
1666
|
+
// Capture docB position for mark lookup
|
|
1561
1667
|
});
|
|
1562
1668
|
}
|
|
1563
1669
|
}
|
|
1670
|
+
const spansB = diffResult.spansB || [];
|
|
1564
1671
|
function transformNode(node, nodeOffset, path) {
|
|
1565
1672
|
if (node.type === "text" && node.text) {
|
|
1566
1673
|
const text = node.text;
|
|
@@ -1571,11 +1678,14 @@ function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
|
|
|
1571
1678
|
const charState = charStates[charOffset] || { type: "equal" };
|
|
1572
1679
|
const insertionsHere = insertions.filter((ins) => ins.afterOffset === charOffset);
|
|
1573
1680
|
for (const ins of insertionsHere) {
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1681
|
+
const insertedNodes = createInsertedTextNodes(
|
|
1682
|
+
ins.text,
|
|
1683
|
+
ins.posB,
|
|
1684
|
+
spansB,
|
|
1685
|
+
author,
|
|
1686
|
+
ins.replacementId
|
|
1687
|
+
);
|
|
1688
|
+
result.push(...insertedNodes);
|
|
1579
1689
|
}
|
|
1580
1690
|
const currentFormatChange = getFormatChangeAt(nodeOffset + i);
|
|
1581
1691
|
let j = i + 1;
|
|
@@ -1598,7 +1708,8 @@ function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
|
|
|
1598
1708
|
currentFormatChange.after,
|
|
1599
1709
|
author
|
|
1600
1710
|
);
|
|
1601
|
-
|
|
1711
|
+
const normalizedAfterMarks = normalizeMarksForRendering(currentFormatChange.after);
|
|
1712
|
+
marks = [...normalizedAfterMarks, trackFormatMark];
|
|
1602
1713
|
}
|
|
1603
1714
|
}
|
|
1604
1715
|
result.push({
|
|
@@ -1611,11 +1722,14 @@ function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
|
|
|
1611
1722
|
const endOffset = nodeOffset + text.length;
|
|
1612
1723
|
const endInsertions = insertions.filter((ins) => ins.afterOffset === endOffset);
|
|
1613
1724
|
for (const ins of endInsertions) {
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1725
|
+
const insertedNodes = createInsertedTextNodes(
|
|
1726
|
+
ins.text,
|
|
1727
|
+
ins.posB,
|
|
1728
|
+
spansB,
|
|
1729
|
+
author,
|
|
1730
|
+
ins.replacementId
|
|
1731
|
+
);
|
|
1732
|
+
result.push(...insertedNodes);
|
|
1619
1733
|
}
|
|
1620
1734
|
insertions = insertions.filter(
|
|
1621
1735
|
(ins) => ins.afterOffset < nodeOffset || ins.afterOffset > endOffset
|
|
@@ -1650,18 +1764,19 @@ function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
|
|
|
1650
1764
|
}
|
|
1651
1765
|
if (insertions.length > 0) {
|
|
1652
1766
|
for (const ins of insertions) {
|
|
1767
|
+
const insertedNodes = createInsertedTextNodes(
|
|
1768
|
+
ins.text,
|
|
1769
|
+
ins.posB,
|
|
1770
|
+
spansB,
|
|
1771
|
+
author,
|
|
1772
|
+
ins.replacementId
|
|
1773
|
+
);
|
|
1653
1774
|
const insertNode = {
|
|
1654
1775
|
type: "paragraph",
|
|
1655
1776
|
content: [
|
|
1656
1777
|
{
|
|
1657
1778
|
type: "run",
|
|
1658
|
-
content:
|
|
1659
|
-
{
|
|
1660
|
-
type: "text",
|
|
1661
|
-
text: ins.text,
|
|
1662
|
-
marks: [createTrackInsertMark(author, ins.replacementId)]
|
|
1663
|
-
}
|
|
1664
|
-
]
|
|
1779
|
+
content: insertedNodes
|
|
1665
1780
|
}
|
|
1666
1781
|
]
|
|
1667
1782
|
};
|
|
@@ -2183,9 +2298,10 @@ function cloneNode2(node) {
|
|
|
2183
2298
|
}
|
|
2184
2299
|
function markAllTextAsInserted(node, sharedId, author) {
|
|
2185
2300
|
if (node.type === "text") {
|
|
2301
|
+
const existingMarks = normalizeMarksForRendering(node.marks || []);
|
|
2186
2302
|
return {
|
|
2187
2303
|
...node,
|
|
2188
|
-
marks: [...
|
|
2304
|
+
marks: [...existingMarks, createTrackInsertMark(author, sharedId)]
|
|
2189
2305
|
};
|
|
2190
2306
|
}
|
|
2191
2307
|
if (node.content && Array.isArray(node.content)) {
|
|
@@ -2200,9 +2316,10 @@ function markAllTextAsInserted(node, sharedId, author) {
|
|
|
2200
2316
|
}
|
|
2201
2317
|
function markAllTextAsDeleted(node, sharedId, author) {
|
|
2202
2318
|
if (node.type === "text") {
|
|
2319
|
+
const existingMarks = normalizeMarksForRendering(node.marks || []);
|
|
2203
2320
|
return {
|
|
2204
2321
|
...node,
|
|
2205
|
-
marks: [...
|
|
2322
|
+
marks: [...existingMarks, createTrackDeleteMark(author, sharedId)]
|
|
2206
2323
|
};
|
|
2207
2324
|
}
|
|
2208
2325
|
if (node.content && Array.isArray(node.content)) {
|
|
@@ -3463,9 +3580,10 @@ var DocxDiffEditor_default = DocxDiffEditor;
|
|
|
3463
3580
|
init_nodeFingerprint();
|
|
3464
3581
|
function markAllTextAsInserted2(node, sharedId, author) {
|
|
3465
3582
|
if (node.type === "text") {
|
|
3583
|
+
const existingMarks = normalizeMarksForRendering(node.marks || []);
|
|
3466
3584
|
return {
|
|
3467
3585
|
...node,
|
|
3468
|
-
marks: [...
|
|
3586
|
+
marks: [...existingMarks, createTrackInsertMark(author, sharedId)]
|
|
3469
3587
|
};
|
|
3470
3588
|
}
|
|
3471
3589
|
if (node.content && Array.isArray(node.content)) {
|
|
@@ -3480,9 +3598,10 @@ function markAllTextAsInserted2(node, sharedId, author) {
|
|
|
3480
3598
|
}
|
|
3481
3599
|
function markAllTextAsDeleted2(node, sharedId, author) {
|
|
3482
3600
|
if (node.type === "text") {
|
|
3601
|
+
const existingMarks = normalizeMarksForRendering(node.marks || []);
|
|
3483
3602
|
return {
|
|
3484
3603
|
...node,
|
|
3485
|
-
marks: [...
|
|
3604
|
+
marks: [...existingMarks, createTrackDeleteMark(author, sharedId)]
|
|
3486
3605
|
};
|
|
3487
3606
|
}
|
|
3488
3607
|
if (node.content && Array.isArray(node.content)) {
|