docx-diff-editor 1.0.46 → 1.0.47
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 +19 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +106 -23
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +106 -23
- 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
|
|
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
|
|
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
|
|
|
@@ -1516,6 +1524,69 @@ function alignListItems(listA, listB, listPathA, listPathB) {
|
|
|
1516
1524
|
function cloneNode(node) {
|
|
1517
1525
|
return JSON.parse(JSON.stringify(node));
|
|
1518
1526
|
}
|
|
1527
|
+
function getMarkSpansForRange(spansB, start, end) {
|
|
1528
|
+
const result = [];
|
|
1529
|
+
for (const span of spansB) {
|
|
1530
|
+
if (span.to > start && span.from < end) {
|
|
1531
|
+
const overlapStart = Math.max(span.from, start);
|
|
1532
|
+
const overlapEnd = Math.min(span.to, end);
|
|
1533
|
+
result.push({
|
|
1534
|
+
relStart: overlapStart - start,
|
|
1535
|
+
relEnd: overlapEnd - start,
|
|
1536
|
+
marks: span.marks || []
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
return result;
|
|
1541
|
+
}
|
|
1542
|
+
function createInsertedTextNodes(text, posB, spansB, author, replacementId) {
|
|
1543
|
+
const result = [];
|
|
1544
|
+
const trackMark = createTrackInsertMark(author, replacementId);
|
|
1545
|
+
if (posB === void 0 || spansB.length === 0) {
|
|
1546
|
+
return [{
|
|
1547
|
+
type: "text",
|
|
1548
|
+
text,
|
|
1549
|
+
marks: [trackMark]
|
|
1550
|
+
}];
|
|
1551
|
+
}
|
|
1552
|
+
const markSpans = getMarkSpansForRange(spansB, posB, posB + text.length);
|
|
1553
|
+
if (markSpans.length === 0) {
|
|
1554
|
+
return [{
|
|
1555
|
+
type: "text",
|
|
1556
|
+
text,
|
|
1557
|
+
marks: [trackMark]
|
|
1558
|
+
}];
|
|
1559
|
+
}
|
|
1560
|
+
markSpans.sort((a, b) => a.relStart - b.relStart);
|
|
1561
|
+
let processedUpTo = 0;
|
|
1562
|
+
for (const span of markSpans) {
|
|
1563
|
+
if (span.relStart > processedUpTo) {
|
|
1564
|
+
result.push({
|
|
1565
|
+
type: "text",
|
|
1566
|
+
text: text.substring(processedUpTo, span.relStart),
|
|
1567
|
+
marks: [trackMark]
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
if (span.relEnd > span.relStart) {
|
|
1571
|
+
const spanText = text.substring(span.relStart, span.relEnd);
|
|
1572
|
+
const marks = [...span.marks, trackMark];
|
|
1573
|
+
result.push({
|
|
1574
|
+
type: "text",
|
|
1575
|
+
text: spanText,
|
|
1576
|
+
marks
|
|
1577
|
+
});
|
|
1578
|
+
processedUpTo = span.relEnd;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
if (processedUpTo < text.length) {
|
|
1582
|
+
result.push({
|
|
1583
|
+
type: "text",
|
|
1584
|
+
text: text.substring(processedUpTo),
|
|
1585
|
+
marks: [trackMark]
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
return result;
|
|
1589
|
+
}
|
|
1519
1590
|
function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
|
|
1520
1591
|
const merged = cloneNode(docA);
|
|
1521
1592
|
const charStates = [];
|
|
@@ -1550,17 +1621,22 @@ function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
|
|
|
1550
1621
|
insertions.push({
|
|
1551
1622
|
afterOffset: docAOffset,
|
|
1552
1623
|
text: nextSegment.text,
|
|
1553
|
-
replacementId
|
|
1624
|
+
replacementId,
|
|
1625
|
+
posB: nextSegment.posB
|
|
1626
|
+
// Capture docB position for mark lookup
|
|
1554
1627
|
});
|
|
1555
1628
|
segIdx++;
|
|
1556
1629
|
}
|
|
1557
1630
|
} else if (segment.type === "insert") {
|
|
1558
1631
|
insertions.push({
|
|
1559
1632
|
afterOffset: docAOffset,
|
|
1560
|
-
text: segment.text
|
|
1633
|
+
text: segment.text,
|
|
1634
|
+
posB: segment.posB
|
|
1635
|
+
// Capture docB position for mark lookup
|
|
1561
1636
|
});
|
|
1562
1637
|
}
|
|
1563
1638
|
}
|
|
1639
|
+
const spansB = diffResult.spansB || [];
|
|
1564
1640
|
function transformNode(node, nodeOffset, path) {
|
|
1565
1641
|
if (node.type === "text" && node.text) {
|
|
1566
1642
|
const text = node.text;
|
|
@@ -1571,11 +1647,14 @@ function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
|
|
|
1571
1647
|
const charState = charStates[charOffset] || { type: "equal" };
|
|
1572
1648
|
const insertionsHere = insertions.filter((ins) => ins.afterOffset === charOffset);
|
|
1573
1649
|
for (const ins of insertionsHere) {
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1650
|
+
const insertedNodes = createInsertedTextNodes(
|
|
1651
|
+
ins.text,
|
|
1652
|
+
ins.posB,
|
|
1653
|
+
spansB,
|
|
1654
|
+
author,
|
|
1655
|
+
ins.replacementId
|
|
1656
|
+
);
|
|
1657
|
+
result.push(...insertedNodes);
|
|
1579
1658
|
}
|
|
1580
1659
|
const currentFormatChange = getFormatChangeAt(nodeOffset + i);
|
|
1581
1660
|
let j = i + 1;
|
|
@@ -1611,11 +1690,14 @@ function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
|
|
|
1611
1690
|
const endOffset = nodeOffset + text.length;
|
|
1612
1691
|
const endInsertions = insertions.filter((ins) => ins.afterOffset === endOffset);
|
|
1613
1692
|
for (const ins of endInsertions) {
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1693
|
+
const insertedNodes = createInsertedTextNodes(
|
|
1694
|
+
ins.text,
|
|
1695
|
+
ins.posB,
|
|
1696
|
+
spansB,
|
|
1697
|
+
author,
|
|
1698
|
+
ins.replacementId
|
|
1699
|
+
);
|
|
1700
|
+
result.push(...insertedNodes);
|
|
1619
1701
|
}
|
|
1620
1702
|
insertions = insertions.filter(
|
|
1621
1703
|
(ins) => ins.afterOffset < nodeOffset || ins.afterOffset > endOffset
|
|
@@ -1650,18 +1732,19 @@ function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
|
|
|
1650
1732
|
}
|
|
1651
1733
|
if (insertions.length > 0) {
|
|
1652
1734
|
for (const ins of insertions) {
|
|
1735
|
+
const insertedNodes = createInsertedTextNodes(
|
|
1736
|
+
ins.text,
|
|
1737
|
+
ins.posB,
|
|
1738
|
+
spansB,
|
|
1739
|
+
author,
|
|
1740
|
+
ins.replacementId
|
|
1741
|
+
);
|
|
1653
1742
|
const insertNode = {
|
|
1654
1743
|
type: "paragraph",
|
|
1655
1744
|
content: [
|
|
1656
1745
|
{
|
|
1657
1746
|
type: "run",
|
|
1658
|
-
content:
|
|
1659
|
-
{
|
|
1660
|
-
type: "text",
|
|
1661
|
-
text: ins.text,
|
|
1662
|
-
marks: [createTrackInsertMark(author, ins.replacementId)]
|
|
1663
|
-
}
|
|
1664
|
-
]
|
|
1747
|
+
content: insertedNodes
|
|
1665
1748
|
}
|
|
1666
1749
|
]
|
|
1667
1750
|
};
|