docx-diff-editor 1.0.60 → 1.0.62
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 +76 -5
- package/dist/index.d.mts +88 -8
- package/dist/index.d.ts +88 -8
- package/dist/index.js +1168 -1024
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1168 -1024
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -822,17 +822,6 @@ async function parseHtmlToJson(html, SuperDoc) {
|
|
|
822
822
|
}
|
|
823
823
|
}, TIMEOUTS.CLEANUP_DELAY);
|
|
824
824
|
};
|
|
825
|
-
const createMockPasteEvent = (htmlContent) => {
|
|
826
|
-
const dataTransfer = new DataTransfer();
|
|
827
|
-
dataTransfer.setData("text/html", htmlContent);
|
|
828
|
-
dataTransfer.setData("text/plain", "");
|
|
829
|
-
const event = new ClipboardEvent("paste", {
|
|
830
|
-
bubbles: true,
|
|
831
|
-
cancelable: true,
|
|
832
|
-
clipboardData: dataTransfer
|
|
833
|
-
});
|
|
834
|
-
return event;
|
|
835
|
-
};
|
|
836
825
|
const tryPasteApproach = (sd, onSuccess, onFail) => {
|
|
837
826
|
try {
|
|
838
827
|
const editor = sd?.activeEditor;
|
|
@@ -986,6 +975,16 @@ function syncNumberingToParent(childEditor, parentEditor) {
|
|
|
986
975
|
console.warn("[syncNumberingToParent] Failed to sync numbering definitions:", err);
|
|
987
976
|
}
|
|
988
977
|
}
|
|
978
|
+
function createMockPasteEvent(htmlContent) {
|
|
979
|
+
const dataTransfer = new DataTransfer();
|
|
980
|
+
dataTransfer.setData("text/html", htmlContent);
|
|
981
|
+
dataTransfer.setData("text/plain", "");
|
|
982
|
+
return new ClipboardEvent("paste", {
|
|
983
|
+
bubbles: true,
|
|
984
|
+
cancelable: true,
|
|
985
|
+
clipboardData: dataTransfer
|
|
986
|
+
});
|
|
987
|
+
}
|
|
989
988
|
async function parseHtmlWithLinkedEditor(html, mainEditor) {
|
|
990
989
|
const container = document.createElement("div");
|
|
991
990
|
container.style.cssText = "position:absolute;top:-9999px;left:-9999px;width:800px;height:600px;visibility:hidden;";
|
|
@@ -1007,17 +1006,26 @@ async function parseHtmlWithLinkedEditor(html, mainEditor) {
|
|
|
1007
1006
|
}
|
|
1008
1007
|
}, TIMEOUTS.CLEANUP_DELAY);
|
|
1009
1008
|
};
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1009
|
+
const pasteAndExtract = (editor) => {
|
|
1010
|
+
try {
|
|
1011
|
+
if (!editor?.view?.pasteHTML) {
|
|
1012
|
+
throw new Error("pasteHTML not available on child editor");
|
|
1013
|
+
}
|
|
1014
|
+
editor.commands?.focus?.();
|
|
1015
|
+
if (editor.commands?.selectAll && editor.commands?.deleteSelection) {
|
|
1016
|
+
editor.commands.selectAll();
|
|
1017
|
+
editor.commands.deleteSelection();
|
|
1018
|
+
}
|
|
1019
|
+
const mockEvent = createMockPasteEvent(html);
|
|
1020
|
+
editor.view.pasteHTML(html, mockEvent);
|
|
1021
|
+
setTimeout(() => {
|
|
1016
1022
|
if (resolved) return;
|
|
1017
1023
|
try {
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1024
|
+
syncNumberingToParent(editor, mainEditor);
|
|
1025
|
+
const json = editor.getJSON();
|
|
1026
|
+
if (!json?.content?.length) {
|
|
1027
|
+
throw new Error("Paste produced empty document");
|
|
1028
|
+
}
|
|
1021
1029
|
const normalizedJson = normalizeRunProperties(json);
|
|
1022
1030
|
resolved = true;
|
|
1023
1031
|
cleanup();
|
|
@@ -1027,6 +1035,23 @@ async function parseHtmlWithLinkedEditor(html, mainEditor) {
|
|
|
1027
1035
|
cleanup();
|
|
1028
1036
|
reject(err);
|
|
1029
1037
|
}
|
|
1038
|
+
}, 100);
|
|
1039
|
+
} catch (err) {
|
|
1040
|
+
resolved = true;
|
|
1041
|
+
cleanup();
|
|
1042
|
+
reject(err);
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
try {
|
|
1046
|
+
mainEditor.createChildEditor({
|
|
1047
|
+
element: container,
|
|
1048
|
+
html: "<p></p>",
|
|
1049
|
+
// Minimal empty document - actual HTML pasted via pasteHTML
|
|
1050
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1051
|
+
onCreate: ({ editor: localEditor }) => {
|
|
1052
|
+
if (resolved) return;
|
|
1053
|
+
childEditor = localEditor;
|
|
1054
|
+
pasteAndExtract(localEditor);
|
|
1030
1055
|
},
|
|
1031
1056
|
onError: (error) => {
|
|
1032
1057
|
if (resolved) return;
|
|
@@ -1751,410 +1776,175 @@ function alignListItems(listA, listB, listPathA, listPathB) {
|
|
|
1751
1776
|
}
|
|
1752
1777
|
return alignNodes(itemsA, itemsB);
|
|
1753
1778
|
}
|
|
1754
|
-
|
|
1755
|
-
|
|
1779
|
+
|
|
1780
|
+
// src/services/attrComparer.ts
|
|
1781
|
+
var KNOWN_DEFAULTS = {
|
|
1782
|
+
paragraph: {
|
|
1783
|
+
textAlign: "left",
|
|
1784
|
+
indent: 0,
|
|
1785
|
+
lineSpacing: 1
|
|
1786
|
+
},
|
|
1787
|
+
heading: {
|
|
1788
|
+
level: 1,
|
|
1789
|
+
textAlign: "left"
|
|
1790
|
+
},
|
|
1791
|
+
table: {
|
|
1792
|
+
alignment: "left",
|
|
1793
|
+
borderStyle: "single"
|
|
1794
|
+
},
|
|
1795
|
+
tableCell: {
|
|
1796
|
+
verticalAlign: "top",
|
|
1797
|
+
colspan: 1,
|
|
1798
|
+
rowspan: 1
|
|
1799
|
+
},
|
|
1800
|
+
listItem: {
|
|
1801
|
+
indent: 0
|
|
1802
|
+
},
|
|
1803
|
+
image: {
|
|
1804
|
+
width: "auto",
|
|
1805
|
+
height: "auto"
|
|
1806
|
+
}
|
|
1807
|
+
};
|
|
1808
|
+
var IGNORED_ATTRS = /* @__PURE__ */ new Set([
|
|
1809
|
+
"id",
|
|
1810
|
+
"class",
|
|
1811
|
+
"data-id",
|
|
1812
|
+
"data-pm-slice",
|
|
1813
|
+
"__trackAttrChanges"
|
|
1814
|
+
]);
|
|
1815
|
+
function isPlainObject(value) {
|
|
1816
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.prototype.toString.call(value) === "[object Object]";
|
|
1756
1817
|
}
|
|
1757
|
-
function
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1818
|
+
function deepEqual2(a, b) {
|
|
1819
|
+
if (a === b) return true;
|
|
1820
|
+
if (typeof a !== typeof b) return false;
|
|
1821
|
+
if (a === null || b === null) return a === b;
|
|
1822
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
1823
|
+
if (a.length !== b.length) return false;
|
|
1824
|
+
return a.every((val, i) => deepEqual2(val, b[i]));
|
|
1825
|
+
}
|
|
1826
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
1827
|
+
const keysA = Object.keys(a);
|
|
1828
|
+
const keysB = Object.keys(b);
|
|
1829
|
+
if (keysA.length !== keysB.length) return false;
|
|
1830
|
+
return keysA.every((key) => deepEqual2(a[key], b[key]));
|
|
1831
|
+
}
|
|
1832
|
+
return false;
|
|
1833
|
+
}
|
|
1834
|
+
function normalizeValue(value, key, nodeType) {
|
|
1835
|
+
if (value !== void 0 && value !== null) {
|
|
1836
|
+
return value;
|
|
1837
|
+
}
|
|
1838
|
+
const defaults = KNOWN_DEFAULTS[nodeType];
|
|
1839
|
+
if (defaults && key in defaults) {
|
|
1840
|
+
return defaults[key];
|
|
1841
|
+
}
|
|
1842
|
+
return value;
|
|
1843
|
+
}
|
|
1844
|
+
function compareAttrs(attrsA, attrsB, nodeType = "", prefix = "") {
|
|
1845
|
+
const diffs = [];
|
|
1846
|
+
const a = attrsA || {};
|
|
1847
|
+
const b = attrsB || {};
|
|
1848
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
1849
|
+
for (const key of allKeys) {
|
|
1850
|
+
if (IGNORED_ATTRS.has(key)) continue;
|
|
1851
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
1852
|
+
const valueA = normalizeValue(a[key], key, nodeType);
|
|
1853
|
+
const valueB = normalizeValue(b[key], key, nodeType);
|
|
1854
|
+
if (isPlainObject(valueA) && isPlainObject(valueB)) {
|
|
1855
|
+
const nestedDiffs = compareAttrs(
|
|
1856
|
+
valueA,
|
|
1857
|
+
valueB,
|
|
1858
|
+
nodeType,
|
|
1859
|
+
fullKey
|
|
1860
|
+
);
|
|
1861
|
+
diffs.push(...nestedDiffs);
|
|
1862
|
+
continue;
|
|
1863
|
+
}
|
|
1864
|
+
if (!deepEqual2(valueA, valueB)) {
|
|
1865
|
+
diffs.push({
|
|
1866
|
+
key: fullKey,
|
|
1867
|
+
before: valueA,
|
|
1868
|
+
after: valueB
|
|
1767
1869
|
});
|
|
1768
1870
|
}
|
|
1769
1871
|
}
|
|
1770
|
-
return
|
|
1872
|
+
return diffs;
|
|
1771
1873
|
}
|
|
1772
|
-
function
|
|
1773
|
-
const
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
const
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1874
|
+
function compareNodeAttrs(nodeA, nodeB) {
|
|
1875
|
+
const nodeType = nodeA.type || nodeB.type || "";
|
|
1876
|
+
return compareAttrs(nodeA.attrs, nodeB.attrs, nodeType);
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// src/services/tableBlockDiffer.ts
|
|
1880
|
+
function detectColumnChanges(matchedRows, tableA, tableB, tablePathA, tablePathB) {
|
|
1881
|
+
const changes = [];
|
|
1882
|
+
if (matchedRows.length === 0) return changes;
|
|
1883
|
+
const firstMatch = matchedRows[0];
|
|
1884
|
+
const rowIdxA = firstMatch.pathA[firstMatch.pathA.length - 1];
|
|
1885
|
+
const rowIdxB = firstMatch.pathB[firstMatch.pathB.length - 1];
|
|
1886
|
+
const rowA = tableA.content?.[rowIdxA];
|
|
1887
|
+
const rowB = tableB.content?.[rowIdxB];
|
|
1888
|
+
if (!rowA || !rowB) return changes;
|
|
1889
|
+
const cellCountA = rowA.content?.length || 0;
|
|
1890
|
+
const cellCountB = rowB.content?.length || 0;
|
|
1891
|
+
const diff = cellCountB - cellCountA;
|
|
1892
|
+
if (diff === 0) return changes;
|
|
1893
|
+
let consistent = true;
|
|
1894
|
+
for (const match of matchedRows) {
|
|
1895
|
+
const idxA = match.pathA[match.pathA.length - 1];
|
|
1896
|
+
const idxB = match.pathB[match.pathB.length - 1];
|
|
1897
|
+
const rA = tableA.content?.[idxA];
|
|
1898
|
+
const rB = tableB.content?.[idxB];
|
|
1899
|
+
if (!rA || !rB) continue;
|
|
1900
|
+
const countA = rA.content?.length || 0;
|
|
1901
|
+
const countB = rB.content?.length || 0;
|
|
1902
|
+
if (countB - countA !== diff) {
|
|
1903
|
+
consistent = false;
|
|
1904
|
+
break;
|
|
1905
|
+
}
|
|
1789
1906
|
}
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
type: "
|
|
1796
|
-
|
|
1797
|
-
|
|
1907
|
+
if (!consistent) return changes;
|
|
1908
|
+
if (diff > 0) {
|
|
1909
|
+
for (let i = 0; i < diff; i++) {
|
|
1910
|
+
changes.push({
|
|
1911
|
+
id: v4(),
|
|
1912
|
+
type: "columnInsert",
|
|
1913
|
+
nodeType: "tableColumn",
|
|
1914
|
+
path: [...tablePathB],
|
|
1915
|
+
node: { type: "column", position: cellCountA + i }
|
|
1798
1916
|
});
|
|
1799
1917
|
}
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1918
|
+
} else {
|
|
1919
|
+
for (let i = 0; i < Math.abs(diff); i++) {
|
|
1920
|
+
changes.push({
|
|
1921
|
+
id: v4(),
|
|
1922
|
+
type: "columnDelete",
|
|
1923
|
+
nodeType: "tableColumn",
|
|
1924
|
+
path: [...tablePathA],
|
|
1925
|
+
node: { type: "column", position: cellCountB + i }
|
|
1808
1926
|
});
|
|
1809
|
-
processedUpTo = span.relEnd;
|
|
1810
1927
|
}
|
|
1811
1928
|
}
|
|
1812
|
-
|
|
1813
|
-
result.push({
|
|
1814
|
-
type: "text",
|
|
1815
|
-
text: text.substring(processedUpTo),
|
|
1816
|
-
marks: [trackMark]
|
|
1817
|
-
});
|
|
1818
|
-
}
|
|
1819
|
-
return result;
|
|
1929
|
+
return changes;
|
|
1820
1930
|
}
|
|
1821
|
-
function
|
|
1822
|
-
const
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
if (segment.type === "equal") {
|
|
1839
|
-
for (let i = 0; i < segment.text.length; i++) {
|
|
1840
|
-
charStates[docAOffset + i] = { type: "equal" };
|
|
1841
|
-
}
|
|
1842
|
-
docAOffset += segment.text.length;
|
|
1843
|
-
} else if (segment.type === "delete") {
|
|
1844
|
-
const nextSegment = segments[segIdx + 1];
|
|
1845
|
-
const isReplacement = nextSegment && nextSegment.type === "insert";
|
|
1846
|
-
const replacementId = isReplacement ? v4() : void 0;
|
|
1847
|
-
for (let i = 0; i < segment.text.length; i++) {
|
|
1848
|
-
charStates[docAOffset + i] = { type: "delete", replacementId };
|
|
1849
|
-
}
|
|
1850
|
-
docAOffset += segment.text.length;
|
|
1851
|
-
if (isReplacement && nextSegment) {
|
|
1852
|
-
insertions.push({
|
|
1853
|
-
afterOffset: docAOffset,
|
|
1854
|
-
text: nextSegment.text,
|
|
1855
|
-
replacementId,
|
|
1856
|
-
posB: nextSegment.posB
|
|
1857
|
-
// Capture docB position for mark lookup
|
|
1858
|
-
});
|
|
1859
|
-
segIdx++;
|
|
1860
|
-
}
|
|
1861
|
-
} else if (segment.type === "insert") {
|
|
1862
|
-
insertions.push({
|
|
1863
|
-
afterOffset: docAOffset,
|
|
1864
|
-
text: segment.text,
|
|
1865
|
-
posB: segment.posB
|
|
1866
|
-
// Capture docB position for mark lookup
|
|
1867
|
-
});
|
|
1868
|
-
}
|
|
1869
|
-
}
|
|
1870
|
-
const spansB = diffResult.spansB || [];
|
|
1871
|
-
function transformNode(node, nodeOffset, path) {
|
|
1872
|
-
if (node.type === "text" && node.text) {
|
|
1873
|
-
const text = node.text;
|
|
1874
|
-
const result = [];
|
|
1875
|
-
let i = 0;
|
|
1876
|
-
while (i < text.length) {
|
|
1877
|
-
const charOffset = nodeOffset + i;
|
|
1878
|
-
const charState = charStates[charOffset] || { type: "equal" };
|
|
1879
|
-
const insertionsHere = insertions.filter((ins) => ins.afterOffset === charOffset);
|
|
1880
|
-
for (const ins of insertionsHere) {
|
|
1881
|
-
const insertedNodes = createInsertedTextNodes(
|
|
1882
|
-
ins.text,
|
|
1883
|
-
ins.posB,
|
|
1884
|
-
spansB,
|
|
1885
|
-
author,
|
|
1886
|
-
ins.replacementId
|
|
1887
|
-
);
|
|
1888
|
-
result.push(...insertedNodes);
|
|
1889
|
-
}
|
|
1890
|
-
const currentFormatChange = getFormatChangeAt(nodeOffset + i);
|
|
1891
|
-
let j = i + 1;
|
|
1892
|
-
while (j < text.length) {
|
|
1893
|
-
const nextState = charStates[nodeOffset + j] || { type: "equal" };
|
|
1894
|
-
if (nextState.type !== charState.type) break;
|
|
1895
|
-
if (insertions.some((ins) => ins.afterOffset === nodeOffset + j)) break;
|
|
1896
|
-
const nextFormatChange = getFormatChangeAt(nodeOffset + j);
|
|
1897
|
-
if (currentFormatChange !== nextFormatChange) break;
|
|
1898
|
-
j++;
|
|
1899
|
-
}
|
|
1900
|
-
const chunk = text.substring(i, j);
|
|
1901
|
-
let marks = [...node.marks || []];
|
|
1902
|
-
if (charState.type === "delete") {
|
|
1903
|
-
marks.push(createTrackDeleteMark(author, charState.replacementId));
|
|
1904
|
-
} else if (charState.type === "equal") {
|
|
1905
|
-
if (currentFormatChange) {
|
|
1906
|
-
const trackFormatMark = createTrackFormatMark(
|
|
1907
|
-
currentFormatChange.before,
|
|
1908
|
-
currentFormatChange.after,
|
|
1909
|
-
author
|
|
1910
|
-
);
|
|
1911
|
-
const normalizedAfterMarks = normalizeMarksForRendering(currentFormatChange.after);
|
|
1912
|
-
marks = [...normalizedAfterMarks, trackFormatMark];
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
1915
|
-
result.push({
|
|
1916
|
-
type: "text",
|
|
1917
|
-
text: chunk,
|
|
1918
|
-
marks: marks.length > 0 ? marks : void 0
|
|
1919
|
-
});
|
|
1920
|
-
i = j;
|
|
1921
|
-
}
|
|
1922
|
-
const endOffset = nodeOffset + text.length;
|
|
1923
|
-
const endInsertions = insertions.filter((ins) => ins.afterOffset === endOffset);
|
|
1924
|
-
for (const ins of endInsertions) {
|
|
1925
|
-
const insertedNodes = createInsertedTextNodes(
|
|
1926
|
-
ins.text,
|
|
1927
|
-
ins.posB,
|
|
1928
|
-
spansB,
|
|
1929
|
-
author,
|
|
1930
|
-
ins.replacementId
|
|
1931
|
-
);
|
|
1932
|
-
result.push(...insertedNodes);
|
|
1933
|
-
}
|
|
1934
|
-
insertions = insertions.filter(
|
|
1935
|
-
(ins) => ins.afterOffset < nodeOffset || ins.afterOffset > endOffset
|
|
1936
|
-
);
|
|
1937
|
-
return { nodes: result, consumedLength: text.length };
|
|
1938
|
-
}
|
|
1939
|
-
if (node.content && Array.isArray(node.content)) {
|
|
1940
|
-
const newContent = [];
|
|
1941
|
-
let offset = nodeOffset;
|
|
1942
|
-
for (const child of node.content) {
|
|
1943
|
-
const { nodes, consumedLength } = transformNode(child, offset);
|
|
1944
|
-
newContent.push(...nodes);
|
|
1945
|
-
offset += consumedLength;
|
|
1946
|
-
}
|
|
1947
|
-
return {
|
|
1948
|
-
nodes: [{ ...node, content: newContent }],
|
|
1949
|
-
consumedLength: offset - nodeOffset
|
|
1950
|
-
};
|
|
1951
|
-
}
|
|
1952
|
-
return { nodes: [node], consumedLength: 0 };
|
|
1953
|
-
}
|
|
1954
|
-
if (merged.content && Array.isArray(merged.content)) {
|
|
1955
|
-
const newContent = [];
|
|
1956
|
-
let offset = 0;
|
|
1957
|
-
for (let i = 0; i < merged.content.length; i++) {
|
|
1958
|
-
const child = merged.content[i];
|
|
1959
|
-
const { nodes, consumedLength } = transformNode(child, offset);
|
|
1960
|
-
newContent.push(...nodes);
|
|
1961
|
-
offset += consumedLength;
|
|
1962
|
-
}
|
|
1963
|
-
merged.content = newContent;
|
|
1964
|
-
}
|
|
1965
|
-
if (insertions.length > 0) {
|
|
1966
|
-
for (const ins of insertions) {
|
|
1967
|
-
const insertedNodes = createInsertedTextNodes(
|
|
1968
|
-
ins.text,
|
|
1969
|
-
ins.posB,
|
|
1970
|
-
spansB,
|
|
1971
|
-
author,
|
|
1972
|
-
ins.replacementId
|
|
1973
|
-
);
|
|
1974
|
-
const insertNode = {
|
|
1975
|
-
type: "paragraph",
|
|
1976
|
-
content: [
|
|
1977
|
-
{
|
|
1978
|
-
type: "run",
|
|
1979
|
-
content: insertedNodes
|
|
1980
|
-
}
|
|
1981
|
-
]
|
|
1982
|
-
};
|
|
1983
|
-
if (!merged.content) merged.content = [];
|
|
1984
|
-
merged.content.push(insertNode);
|
|
1985
|
-
}
|
|
1986
|
-
}
|
|
1987
|
-
return merged;
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
|
-
// src/services/attrComparer.ts
|
|
1991
|
-
var KNOWN_DEFAULTS = {
|
|
1992
|
-
paragraph: {
|
|
1993
|
-
textAlign: "left",
|
|
1994
|
-
indent: 0,
|
|
1995
|
-
lineSpacing: 1
|
|
1996
|
-
},
|
|
1997
|
-
heading: {
|
|
1998
|
-
level: 1,
|
|
1999
|
-
textAlign: "left"
|
|
2000
|
-
},
|
|
2001
|
-
table: {
|
|
2002
|
-
alignment: "left",
|
|
2003
|
-
borderStyle: "single"
|
|
2004
|
-
},
|
|
2005
|
-
tableCell: {
|
|
2006
|
-
verticalAlign: "top",
|
|
2007
|
-
colspan: 1,
|
|
2008
|
-
rowspan: 1
|
|
2009
|
-
},
|
|
2010
|
-
listItem: {
|
|
2011
|
-
indent: 0
|
|
2012
|
-
},
|
|
2013
|
-
image: {
|
|
2014
|
-
width: "auto",
|
|
2015
|
-
height: "auto"
|
|
2016
|
-
}
|
|
2017
|
-
};
|
|
2018
|
-
var IGNORED_ATTRS = /* @__PURE__ */ new Set([
|
|
2019
|
-
"id",
|
|
2020
|
-
"class",
|
|
2021
|
-
"data-id",
|
|
2022
|
-
"data-pm-slice",
|
|
2023
|
-
"__trackAttrChanges"
|
|
2024
|
-
]);
|
|
2025
|
-
function isPlainObject(value) {
|
|
2026
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.prototype.toString.call(value) === "[object Object]";
|
|
2027
|
-
}
|
|
2028
|
-
function deepEqual2(a, b) {
|
|
2029
|
-
if (a === b) return true;
|
|
2030
|
-
if (typeof a !== typeof b) return false;
|
|
2031
|
-
if (a === null || b === null) return a === b;
|
|
2032
|
-
if (Array.isArray(a) && Array.isArray(b)) {
|
|
2033
|
-
if (a.length !== b.length) return false;
|
|
2034
|
-
return a.every((val, i) => deepEqual2(val, b[i]));
|
|
2035
|
-
}
|
|
2036
|
-
if (isPlainObject(a) && isPlainObject(b)) {
|
|
2037
|
-
const keysA = Object.keys(a);
|
|
2038
|
-
const keysB = Object.keys(b);
|
|
2039
|
-
if (keysA.length !== keysB.length) return false;
|
|
2040
|
-
return keysA.every((key) => deepEqual2(a[key], b[key]));
|
|
2041
|
-
}
|
|
2042
|
-
return false;
|
|
2043
|
-
}
|
|
2044
|
-
function normalizeValue(value, key, nodeType) {
|
|
2045
|
-
if (value !== void 0 && value !== null) {
|
|
2046
|
-
return value;
|
|
2047
|
-
}
|
|
2048
|
-
const defaults = KNOWN_DEFAULTS[nodeType];
|
|
2049
|
-
if (defaults && key in defaults) {
|
|
2050
|
-
return defaults[key];
|
|
2051
|
-
}
|
|
2052
|
-
return value;
|
|
2053
|
-
}
|
|
2054
|
-
function compareAttrs(attrsA, attrsB, nodeType = "", prefix = "") {
|
|
2055
|
-
const diffs = [];
|
|
2056
|
-
const a = attrsA || {};
|
|
2057
|
-
const b = attrsB || {};
|
|
2058
|
-
const allKeys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
2059
|
-
for (const key of allKeys) {
|
|
2060
|
-
if (IGNORED_ATTRS.has(key)) continue;
|
|
2061
|
-
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
2062
|
-
const valueA = normalizeValue(a[key], key, nodeType);
|
|
2063
|
-
const valueB = normalizeValue(b[key], key, nodeType);
|
|
2064
|
-
if (isPlainObject(valueA) && isPlainObject(valueB)) {
|
|
2065
|
-
const nestedDiffs = compareAttrs(
|
|
2066
|
-
valueA,
|
|
2067
|
-
valueB,
|
|
2068
|
-
nodeType,
|
|
2069
|
-
fullKey
|
|
2070
|
-
);
|
|
2071
|
-
diffs.push(...nestedDiffs);
|
|
2072
|
-
continue;
|
|
2073
|
-
}
|
|
2074
|
-
if (!deepEqual2(valueA, valueB)) {
|
|
2075
|
-
diffs.push({
|
|
2076
|
-
key: fullKey,
|
|
2077
|
-
before: valueA,
|
|
2078
|
-
after: valueB
|
|
2079
|
-
});
|
|
2080
|
-
}
|
|
2081
|
-
}
|
|
2082
|
-
return diffs;
|
|
2083
|
-
}
|
|
2084
|
-
function compareNodeAttrs(nodeA, nodeB) {
|
|
2085
|
-
const nodeType = nodeA.type || nodeB.type || "";
|
|
2086
|
-
return compareAttrs(nodeA.attrs, nodeB.attrs, nodeType);
|
|
2087
|
-
}
|
|
2088
|
-
|
|
2089
|
-
// src/services/tableBlockDiffer.ts
|
|
2090
|
-
function detectColumnChanges(matchedRows, tableA, tableB, tablePathA, tablePathB) {
|
|
2091
|
-
const changes = [];
|
|
2092
|
-
if (matchedRows.length === 0) return changes;
|
|
2093
|
-
const firstMatch = matchedRows[0];
|
|
2094
|
-
const rowIdxA = firstMatch.pathA[firstMatch.pathA.length - 1];
|
|
2095
|
-
const rowIdxB = firstMatch.pathB[firstMatch.pathB.length - 1];
|
|
2096
|
-
const rowA = tableA.content?.[rowIdxA];
|
|
2097
|
-
const rowB = tableB.content?.[rowIdxB];
|
|
2098
|
-
if (!rowA || !rowB) return changes;
|
|
2099
|
-
const cellCountA = rowA.content?.length || 0;
|
|
2100
|
-
const cellCountB = rowB.content?.length || 0;
|
|
2101
|
-
const diff = cellCountB - cellCountA;
|
|
2102
|
-
if (diff === 0) return changes;
|
|
2103
|
-
let consistent = true;
|
|
2104
|
-
for (const match of matchedRows) {
|
|
2105
|
-
const idxA = match.pathA[match.pathA.length - 1];
|
|
2106
|
-
const idxB = match.pathB[match.pathB.length - 1];
|
|
2107
|
-
const rA = tableA.content?.[idxA];
|
|
2108
|
-
const rB = tableB.content?.[idxB];
|
|
2109
|
-
if (!rA || !rB) continue;
|
|
2110
|
-
const countA = rA.content?.length || 0;
|
|
2111
|
-
const countB = rB.content?.length || 0;
|
|
2112
|
-
if (countB - countA !== diff) {
|
|
2113
|
-
consistent = false;
|
|
2114
|
-
break;
|
|
2115
|
-
}
|
|
2116
|
-
}
|
|
2117
|
-
if (!consistent) return changes;
|
|
2118
|
-
if (diff > 0) {
|
|
2119
|
-
for (let i = 0; i < diff; i++) {
|
|
2120
|
-
changes.push({
|
|
2121
|
-
id: v4(),
|
|
2122
|
-
type: "columnInsert",
|
|
2123
|
-
nodeType: "tableColumn",
|
|
2124
|
-
path: [...tablePathB],
|
|
2125
|
-
node: { type: "column", position: cellCountA + i }
|
|
2126
|
-
});
|
|
2127
|
-
}
|
|
2128
|
-
} else {
|
|
2129
|
-
for (let i = 0; i < Math.abs(diff); i++) {
|
|
2130
|
-
changes.push({
|
|
2131
|
-
id: v4(),
|
|
2132
|
-
type: "columnDelete",
|
|
2133
|
-
nodeType: "tableColumn",
|
|
2134
|
-
path: [...tablePathA],
|
|
2135
|
-
node: { type: "column", position: cellCountB + i }
|
|
2136
|
-
});
|
|
2137
|
-
}
|
|
2138
|
-
}
|
|
2139
|
-
return changes;
|
|
2140
|
-
}
|
|
2141
|
-
function diffTables(tableA, tableB, tablePathA, tablePathB) {
|
|
2142
|
-
const result = {
|
|
2143
|
-
rowChanges: [],
|
|
2144
|
-
columnChanges: [],
|
|
2145
|
-
cellMatches: [],
|
|
2146
|
-
tableAttrChanges: null,
|
|
2147
|
-
cellAttrChanges: []
|
|
2148
|
-
};
|
|
2149
|
-
const tableAttrDiffs = compareNodeAttrs(tableA, tableB);
|
|
2150
|
-
if (tableAttrDiffs.length > 0) {
|
|
2151
|
-
result.tableAttrChanges = {
|
|
2152
|
-
id: v4(),
|
|
2153
|
-
nodeType: "table",
|
|
2154
|
-
pathA: tablePathA,
|
|
2155
|
-
pathB: tablePathB,
|
|
2156
|
-
changes: tableAttrDiffs
|
|
2157
|
-
};
|
|
1931
|
+
function diffTables(tableA, tableB, tablePathA, tablePathB) {
|
|
1932
|
+
const result = {
|
|
1933
|
+
rowChanges: [],
|
|
1934
|
+
columnChanges: [],
|
|
1935
|
+
cellMatches: [],
|
|
1936
|
+
tableAttrChanges: null,
|
|
1937
|
+
cellAttrChanges: []
|
|
1938
|
+
};
|
|
1939
|
+
const tableAttrDiffs = compareNodeAttrs(tableA, tableB);
|
|
1940
|
+
if (tableAttrDiffs.length > 0) {
|
|
1941
|
+
result.tableAttrChanges = {
|
|
1942
|
+
id: v4(),
|
|
1943
|
+
nodeType: "table",
|
|
1944
|
+
pathA: tablePathA,
|
|
1945
|
+
pathB: tablePathB,
|
|
1946
|
+
changes: tableAttrDiffs
|
|
1947
|
+
};
|
|
2158
1948
|
}
|
|
2159
1949
|
const rowAlignment = alignTableRows(tableA, tableB, tablePathA, tablePathB);
|
|
2160
1950
|
for (const inserted of rowAlignment.insertions) {
|
|
@@ -2213,290 +2003,777 @@ function diffTables(tableA, tableB, tablePathA, tablePathB) {
|
|
|
2213
2003
|
}
|
|
2214
2004
|
}
|
|
2215
2005
|
}
|
|
2216
|
-
return result;
|
|
2217
|
-
}
|
|
2218
|
-
function isTable(node) {
|
|
2219
|
-
return node?.type === "table";
|
|
2220
|
-
}
|
|
2221
|
-
function isTableRow(node) {
|
|
2222
|
-
return node?.type === "tableRow";
|
|
2223
|
-
}
|
|
2224
|
-
function getRowLocation(tablePath, rowIndex, tableIndex) {
|
|
2225
|
-
return `Table ${tableIndex + 1}, Row ${rowIndex + 1}`;
|
|
2226
|
-
}
|
|
2227
|
-
function getRowPreview(row, maxLength = 50) {
|
|
2228
|
-
const cells = [];
|
|
2229
|
-
for (const cell of row.content || []) {
|
|
2230
|
-
const cellText = extractCellText(cell);
|
|
2231
|
-
if (cellText) {
|
|
2232
|
-
cells.push(cellText);
|
|
2006
|
+
return result;
|
|
2007
|
+
}
|
|
2008
|
+
function isTable(node) {
|
|
2009
|
+
return node?.type === "table";
|
|
2010
|
+
}
|
|
2011
|
+
function isTableRow(node) {
|
|
2012
|
+
return node?.type === "tableRow";
|
|
2013
|
+
}
|
|
2014
|
+
function getRowLocation(tablePath, rowIndex, tableIndex) {
|
|
2015
|
+
return `Table ${tableIndex + 1}, Row ${rowIndex + 1}`;
|
|
2016
|
+
}
|
|
2017
|
+
function getRowPreview(row, maxLength = 50) {
|
|
2018
|
+
const cells = [];
|
|
2019
|
+
for (const cell of row.content || []) {
|
|
2020
|
+
const cellText = extractCellText(cell);
|
|
2021
|
+
if (cellText) {
|
|
2022
|
+
cells.push(cellText);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
const preview = cells.join(" | ");
|
|
2026
|
+
if (preview.length > maxLength) {
|
|
2027
|
+
return preview.substring(0, maxLength - 3) + "...";
|
|
2028
|
+
}
|
|
2029
|
+
return preview;
|
|
2030
|
+
}
|
|
2031
|
+
function extractCellText(cell) {
|
|
2032
|
+
if (!cell.content) return "";
|
|
2033
|
+
const texts = [];
|
|
2034
|
+
for (const child of cell.content) {
|
|
2035
|
+
if (child.type === "text") {
|
|
2036
|
+
texts.push(child.text || "");
|
|
2037
|
+
} else if (child.type === "paragraph" && child.content) {
|
|
2038
|
+
for (const pChild of child.content) {
|
|
2039
|
+
if (pChild.type === "text") {
|
|
2040
|
+
texts.push(pChild.text || "");
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
return texts.join("").trim();
|
|
2046
|
+
}
|
|
2047
|
+
function isList(node) {
|
|
2048
|
+
return node?.type === "bulletList" || node?.type === "orderedList";
|
|
2049
|
+
}
|
|
2050
|
+
function isListItem(node) {
|
|
2051
|
+
return node?.type === "listItem";
|
|
2052
|
+
}
|
|
2053
|
+
function extractListItemText(item) {
|
|
2054
|
+
const texts = [];
|
|
2055
|
+
function extract(node) {
|
|
2056
|
+
if (!node) return;
|
|
2057
|
+
if (node.type === "text") {
|
|
2058
|
+
texts.push(node.text || "");
|
|
2059
|
+
}
|
|
2060
|
+
if (node.content && Array.isArray(node.content)) {
|
|
2061
|
+
for (const child of node.content) {
|
|
2062
|
+
if (!isList(child)) {
|
|
2063
|
+
extract(child);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
extract(item);
|
|
2069
|
+
return texts.join("").trim();
|
|
2070
|
+
}
|
|
2071
|
+
function findNestedLists(item) {
|
|
2072
|
+
const lists = [];
|
|
2073
|
+
if (!item.content) return lists;
|
|
2074
|
+
for (const child of item.content) {
|
|
2075
|
+
if (isList(child)) {
|
|
2076
|
+
lists.push(child);
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
return lists;
|
|
2080
|
+
}
|
|
2081
|
+
function diffLists(listA, listB, listPathA, listPathB, depth = 0) {
|
|
2082
|
+
const result = {
|
|
2083
|
+
itemChanges: [],
|
|
2084
|
+
itemMatches: [],
|
|
2085
|
+
nestedChanges: []
|
|
2086
|
+
};
|
|
2087
|
+
const alignment = alignListItems(listA, listB, listPathA, listPathB);
|
|
2088
|
+
for (const inserted of alignment.insertions) {
|
|
2089
|
+
result.itemChanges.push({
|
|
2090
|
+
id: v4(),
|
|
2091
|
+
type: "listItemInsert",
|
|
2092
|
+
nodeType: "listItem",
|
|
2093
|
+
path: inserted.path,
|
|
2094
|
+
node: inserted.node
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
for (const deleted of alignment.deletions) {
|
|
2098
|
+
result.itemChanges.push({
|
|
2099
|
+
id: v4(),
|
|
2100
|
+
type: "listItemDelete",
|
|
2101
|
+
nodeType: "listItem",
|
|
2102
|
+
path: deleted.path,
|
|
2103
|
+
node: deleted.node
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
2106
|
+
result.itemMatches = alignment.matched;
|
|
2107
|
+
for (const match of alignment.matched) {
|
|
2108
|
+
const itemIdxA = match.pathA[match.pathA.length - 1];
|
|
2109
|
+
const itemIdxB = match.pathB[match.pathB.length - 1];
|
|
2110
|
+
const itemA = listA.content?.[itemIdxA];
|
|
2111
|
+
const itemB = listB.content?.[itemIdxB];
|
|
2112
|
+
if (!itemA || !itemB) continue;
|
|
2113
|
+
const nestedA = findNestedLists(itemA);
|
|
2114
|
+
const nestedB = findNestedLists(itemB);
|
|
2115
|
+
const maxNested = Math.max(nestedA.length, nestedB.length);
|
|
2116
|
+
for (let i = 0; i < maxNested; i++) {
|
|
2117
|
+
const nA = nestedA[i];
|
|
2118
|
+
const nB = nestedB[i];
|
|
2119
|
+
if (nA && nB) {
|
|
2120
|
+
const nestedResult = diffLists(
|
|
2121
|
+
nA,
|
|
2122
|
+
nB,
|
|
2123
|
+
[...match.pathA, i],
|
|
2124
|
+
[...match.pathB, i],
|
|
2125
|
+
depth + 1
|
|
2126
|
+
);
|
|
2127
|
+
result.nestedChanges.push(nestedResult);
|
|
2128
|
+
} else if (!nA && nB) {
|
|
2129
|
+
result.itemChanges.push({
|
|
2130
|
+
id: v4(),
|
|
2131
|
+
type: "listItemInsert",
|
|
2132
|
+
nodeType: "nestedList",
|
|
2133
|
+
path: [...match.pathB, i],
|
|
2134
|
+
node: nB
|
|
2135
|
+
});
|
|
2136
|
+
} else if (nA && !nB) {
|
|
2137
|
+
result.itemChanges.push({
|
|
2138
|
+
id: v4(),
|
|
2139
|
+
type: "listItemDelete",
|
|
2140
|
+
nodeType: "nestedList",
|
|
2141
|
+
path: [...match.pathA, i],
|
|
2142
|
+
node: nA
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
return result;
|
|
2148
|
+
}
|
|
2149
|
+
function getListItemLocation(listPath, itemIndex, listIndex, depth = 0) {
|
|
2150
|
+
const depthStr = depth > 0 ? ` (nested, level ${depth + 1})` : "";
|
|
2151
|
+
return `List ${listIndex + 1}, Item ${itemIndex + 1}${depthStr}`;
|
|
2152
|
+
}
|
|
2153
|
+
function getListItemPreview(item, maxLength = 50) {
|
|
2154
|
+
const text = extractListItemText(item);
|
|
2155
|
+
if (text.length > maxLength) {
|
|
2156
|
+
return text.substring(0, maxLength - 3) + "...";
|
|
2157
|
+
}
|
|
2158
|
+
return text || "(empty item)";
|
|
2159
|
+
}
|
|
2160
|
+
function isImage(node) {
|
|
2161
|
+
return node?.type === "image";
|
|
2162
|
+
}
|
|
2163
|
+
function isHorizontalRule(node) {
|
|
2164
|
+
return node?.type === "horizontalRule" || node?.type === "hr";
|
|
2165
|
+
}
|
|
2166
|
+
function isHardBreak(node) {
|
|
2167
|
+
return node?.type === "hardBreak";
|
|
2168
|
+
}
|
|
2169
|
+
function isPageBreak(node) {
|
|
2170
|
+
return node?.type === "pageBreak";
|
|
2171
|
+
}
|
|
2172
|
+
function isEmbedded(node) {
|
|
2173
|
+
const embeddedTypes = [
|
|
2174
|
+
"equation",
|
|
2175
|
+
"math",
|
|
2176
|
+
"embed",
|
|
2177
|
+
"chart",
|
|
2178
|
+
"drawing",
|
|
2179
|
+
"shape"
|
|
2180
|
+
];
|
|
2181
|
+
return embeddedTypes.includes(node?.type);
|
|
2182
|
+
}
|
|
2183
|
+
function isAtomicNode(node) {
|
|
2184
|
+
return isImage(node) || isHorizontalRule(node) || isHardBreak(node) || isPageBreak(node) || isEmbedded(node);
|
|
2185
|
+
}
|
|
2186
|
+
function getImageIdentifier(node) {
|
|
2187
|
+
if (!isImage(node)) return "";
|
|
2188
|
+
const attrs = node.attrs || {};
|
|
2189
|
+
if (attrs.src) {
|
|
2190
|
+
return `src:${attrs.src}`;
|
|
2191
|
+
}
|
|
2192
|
+
if (attrs.data) {
|
|
2193
|
+
return `data:${simpleHash2(attrs.data)}`;
|
|
2194
|
+
}
|
|
2195
|
+
if (attrs.alt) {
|
|
2196
|
+
return `alt:${attrs.alt}`;
|
|
2197
|
+
}
|
|
2198
|
+
return "unknown";
|
|
2199
|
+
}
|
|
2200
|
+
function simpleHash2(str) {
|
|
2201
|
+
let hash = 5381;
|
|
2202
|
+
const sample = str.substring(0, 1e3);
|
|
2203
|
+
for (let i = 0; i < sample.length; i++) {
|
|
2204
|
+
hash = (hash << 5) + hash ^ sample.charCodeAt(i);
|
|
2205
|
+
}
|
|
2206
|
+
return (hash >>> 0).toString(16);
|
|
2207
|
+
}
|
|
2208
|
+
function findImages(doc, basePath = []) {
|
|
2209
|
+
const images = [];
|
|
2210
|
+
function traverse(node, path) {
|
|
2211
|
+
if (!node) return;
|
|
2212
|
+
if (isImage(node)) {
|
|
2213
|
+
images.push({ node, path: [...path] });
|
|
2214
|
+
}
|
|
2215
|
+
if (node.content && Array.isArray(node.content)) {
|
|
2216
|
+
node.content.forEach((child, i) => {
|
|
2217
|
+
traverse(child, [...path, i]);
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
traverse(doc, basePath);
|
|
2222
|
+
return images;
|
|
2223
|
+
}
|
|
2224
|
+
function diffImages(docA, docB) {
|
|
2225
|
+
const imagesA = findImages(docA);
|
|
2226
|
+
const imagesB = findImages(docB);
|
|
2227
|
+
const inserted = [];
|
|
2228
|
+
const deleted = [];
|
|
2229
|
+
const idsA = /* @__PURE__ */ new Map();
|
|
2230
|
+
const idsB = /* @__PURE__ */ new Map();
|
|
2231
|
+
for (const img of imagesA) {
|
|
2232
|
+
const id = getImageIdentifier(img.node);
|
|
2233
|
+
idsA.set(id, img);
|
|
2234
|
+
}
|
|
2235
|
+
for (const img of imagesB) {
|
|
2236
|
+
const id = getImageIdentifier(img.node);
|
|
2237
|
+
idsB.set(id, img);
|
|
2238
|
+
}
|
|
2239
|
+
for (const [id, img] of idsA) {
|
|
2240
|
+
if (!idsB.has(id)) {
|
|
2241
|
+
deleted.push({
|
|
2242
|
+
id: v4(),
|
|
2243
|
+
type: "imageDelete",
|
|
2244
|
+
nodeType: "image",
|
|
2245
|
+
path: img.path,
|
|
2246
|
+
node: img.node
|
|
2247
|
+
});
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
for (const [id, img] of idsB) {
|
|
2251
|
+
if (!idsA.has(id)) {
|
|
2252
|
+
inserted.push({
|
|
2253
|
+
id: v4(),
|
|
2254
|
+
type: "imageInsert",
|
|
2255
|
+
nodeType: "image",
|
|
2256
|
+
path: img.path,
|
|
2257
|
+
node: img.node
|
|
2258
|
+
});
|
|
2233
2259
|
}
|
|
2234
2260
|
}
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2261
|
+
return { inserted, deleted };
|
|
2262
|
+
}
|
|
2263
|
+
function getImageLocation(path) {
|
|
2264
|
+
if (path.length <= 1) {
|
|
2265
|
+
return `Image at position ${path[0] + 1}`;
|
|
2238
2266
|
}
|
|
2239
|
-
return
|
|
2267
|
+
return `Image (nested at depth ${path.length})`;
|
|
2240
2268
|
}
|
|
2241
|
-
function
|
|
2242
|
-
if (!
|
|
2243
|
-
const
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
}
|
|
2269
|
+
function getImagePreview(node) {
|
|
2270
|
+
if (!isImage(node)) return "";
|
|
2271
|
+
const attrs = node.attrs || {};
|
|
2272
|
+
if (attrs.alt) {
|
|
2273
|
+
return `"${attrs.alt}"`;
|
|
2274
|
+
}
|
|
2275
|
+
if (attrs.src) {
|
|
2276
|
+
const src = attrs.src;
|
|
2277
|
+
const filename = src.split("/").pop()?.split("?")[0];
|
|
2278
|
+
if (filename) {
|
|
2279
|
+
return filename;
|
|
2253
2280
|
}
|
|
2254
2281
|
}
|
|
2255
|
-
return
|
|
2282
|
+
return "(image)";
|
|
2256
2283
|
}
|
|
2257
|
-
|
|
2258
|
-
|
|
2284
|
+
|
|
2285
|
+
// src/services/blockLevelMerger.ts
|
|
2286
|
+
function markAllTextAsInserted(node, sharedId, author) {
|
|
2287
|
+
if (node.type === "text") {
|
|
2288
|
+
const existingMarks = normalizeMarksForRendering(node.marks || []);
|
|
2289
|
+
return {
|
|
2290
|
+
...node,
|
|
2291
|
+
marks: [...existingMarks, createTrackInsertMark(author, sharedId)]
|
|
2292
|
+
};
|
|
2293
|
+
}
|
|
2294
|
+
if (node.content && Array.isArray(node.content)) {
|
|
2295
|
+
return {
|
|
2296
|
+
...node,
|
|
2297
|
+
content: node.content.map(
|
|
2298
|
+
(child) => markAllTextAsInserted(child, sharedId, author)
|
|
2299
|
+
)
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
return node;
|
|
2259
2303
|
}
|
|
2260
|
-
function
|
|
2261
|
-
|
|
2304
|
+
function markAllTextAsDeleted(node, sharedId, author) {
|
|
2305
|
+
if (node.type === "text") {
|
|
2306
|
+
const existingMarks = normalizeMarksForRendering(node.marks || []);
|
|
2307
|
+
return {
|
|
2308
|
+
...node,
|
|
2309
|
+
marks: [...existingMarks, createTrackDeleteMark(author, sharedId)]
|
|
2310
|
+
};
|
|
2311
|
+
}
|
|
2312
|
+
if (node.content && Array.isArray(node.content)) {
|
|
2313
|
+
return {
|
|
2314
|
+
...node,
|
|
2315
|
+
content: node.content.map(
|
|
2316
|
+
(child) => markAllTextAsDeleted(child, sharedId, author)
|
|
2317
|
+
)
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
return node;
|
|
2262
2321
|
}
|
|
2263
|
-
function
|
|
2322
|
+
function cloneNode(node) {
|
|
2323
|
+
return JSON.parse(JSON.stringify(node));
|
|
2324
|
+
}
|
|
2325
|
+
function extractTextPreview(node, maxLength = 50) {
|
|
2264
2326
|
const texts = [];
|
|
2265
|
-
function extract(
|
|
2266
|
-
if (
|
|
2267
|
-
|
|
2268
|
-
texts.push(node.text || "");
|
|
2327
|
+
function extract(n) {
|
|
2328
|
+
if (n.type === "text") {
|
|
2329
|
+
texts.push(n.text || "");
|
|
2269
2330
|
}
|
|
2270
|
-
if (
|
|
2271
|
-
for (const child of
|
|
2272
|
-
|
|
2273
|
-
extract(child);
|
|
2274
|
-
}
|
|
2331
|
+
if (n.content) {
|
|
2332
|
+
for (const child of n.content) {
|
|
2333
|
+
extract(child);
|
|
2275
2334
|
}
|
|
2276
2335
|
}
|
|
2277
2336
|
}
|
|
2278
|
-
extract(
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
const lists = [];
|
|
2283
|
-
if (!item.content) return lists;
|
|
2284
|
-
for (const child of item.content) {
|
|
2285
|
-
if (isList(child)) {
|
|
2286
|
-
lists.push(child);
|
|
2287
|
-
}
|
|
2337
|
+
extract(node);
|
|
2338
|
+
const text = texts.join("").trim();
|
|
2339
|
+
if (text.length > maxLength) {
|
|
2340
|
+
return text.substring(0, maxLength - 3) + "...";
|
|
2288
2341
|
}
|
|
2289
|
-
return
|
|
2342
|
+
return text || "(empty)";
|
|
2290
2343
|
}
|
|
2291
|
-
function
|
|
2292
|
-
const
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2344
|
+
function processStructuralChanges(docA, docB, author = DEFAULT_AUTHOR) {
|
|
2345
|
+
const changes = [];
|
|
2346
|
+
const infos = [];
|
|
2347
|
+
const alignment = alignDocuments(docA, docB);
|
|
2348
|
+
let tableIndex = 0;
|
|
2349
|
+
let listIndex = 0;
|
|
2350
|
+
let paragraphIndex = 0;
|
|
2298
2351
|
for (const inserted of alignment.insertions) {
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2352
|
+
const node = inserted.node;
|
|
2353
|
+
const sharedId = v4();
|
|
2354
|
+
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
2355
|
+
let type = "paragraphInsert";
|
|
2356
|
+
let location = "";
|
|
2357
|
+
let preview = "";
|
|
2358
|
+
if (isTable(node)) {
|
|
2359
|
+
type = "rowInsert";
|
|
2360
|
+
location = `New table at position ${inserted.path[0] + 1}`;
|
|
2361
|
+
preview = `Table with ${node.content?.length || 0} rows`;
|
|
2362
|
+
tableIndex++;
|
|
2363
|
+
} else if (isList(node)) {
|
|
2364
|
+
type = "listItemInsert";
|
|
2365
|
+
location = `New list at position ${inserted.path[0] + 1}`;
|
|
2366
|
+
preview = `List with ${node.content?.length || 0} items`;
|
|
2367
|
+
listIndex++;
|
|
2368
|
+
} else {
|
|
2369
|
+
type = "paragraphInsert";
|
|
2370
|
+
paragraphIndex++;
|
|
2371
|
+
location = `Paragraph ${paragraphIndex}`;
|
|
2372
|
+
preview = extractTextPreview(node);
|
|
2373
|
+
}
|
|
2374
|
+
changes.push({
|
|
2375
|
+
id: sharedId,
|
|
2376
|
+
type,
|
|
2377
|
+
nodeType: node.type,
|
|
2303
2378
|
path: inserted.path,
|
|
2304
|
-
node:
|
|
2379
|
+
node: markAllTextAsInserted(cloneNode(node), sharedId, author)
|
|
2305
2380
|
});
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2381
|
+
infos.push({
|
|
2382
|
+
id: sharedId,
|
|
2383
|
+
type,
|
|
2384
|
+
nodeType: node.type,
|
|
2385
|
+
location,
|
|
2386
|
+
preview,
|
|
2387
|
+
author,
|
|
2388
|
+
date
|
|
2314
2389
|
});
|
|
2315
2390
|
}
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
const
|
|
2319
|
-
const
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2391
|
+
for (const deleted of alignment.deletions) {
|
|
2392
|
+
const node = deleted.node;
|
|
2393
|
+
const sharedId = v4();
|
|
2394
|
+
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
2395
|
+
let type = "paragraphDelete";
|
|
2396
|
+
let location = "";
|
|
2397
|
+
let preview = "";
|
|
2398
|
+
if (isTable(node)) {
|
|
2399
|
+
type = "rowDelete";
|
|
2400
|
+
location = `Deleted table at position ${deleted.path[0] + 1}`;
|
|
2401
|
+
preview = `Table with ${node.content?.length || 0} rows`;
|
|
2402
|
+
} else if (isList(node)) {
|
|
2403
|
+
type = "listItemDelete";
|
|
2404
|
+
location = `Deleted list at position ${deleted.path[0] + 1}`;
|
|
2405
|
+
preview = `List with ${node.content?.length || 0} items`;
|
|
2406
|
+
} else {
|
|
2407
|
+
type = "paragraphDelete";
|
|
2408
|
+
location = `Deleted paragraph`;
|
|
2409
|
+
preview = extractTextPreview(node);
|
|
2410
|
+
}
|
|
2411
|
+
changes.push({
|
|
2412
|
+
id: sharedId,
|
|
2413
|
+
type,
|
|
2414
|
+
nodeType: node.type,
|
|
2415
|
+
path: deleted.path,
|
|
2416
|
+
node: markAllTextAsDeleted(cloneNode(node), sharedId, author)
|
|
2417
|
+
});
|
|
2418
|
+
infos.push({
|
|
2419
|
+
id: sharedId,
|
|
2420
|
+
type,
|
|
2421
|
+
nodeType: node.type,
|
|
2422
|
+
location,
|
|
2423
|
+
preview,
|
|
2424
|
+
author,
|
|
2425
|
+
date
|
|
2426
|
+
});
|
|
2427
|
+
}
|
|
2428
|
+
for (const match of alignment.matched) {
|
|
2429
|
+
const nodeA = docA.content?.[match.pathA[0]];
|
|
2430
|
+
const nodeB = docB.content?.[match.pathB[0]];
|
|
2431
|
+
if (!nodeA || !nodeB) continue;
|
|
2432
|
+
if (isTable(nodeA) && isTable(nodeB)) {
|
|
2433
|
+
tableIndex++;
|
|
2434
|
+
const tableResult = diffTables(nodeA, nodeB, match.pathA, match.pathB);
|
|
2435
|
+
for (const rowChange of tableResult.rowChanges) {
|
|
2436
|
+
const sharedId = rowChange.id;
|
|
2437
|
+
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
2438
|
+
const rowIndex = rowChange.path[rowChange.path.length - 1];
|
|
2439
|
+
const isInsert = rowChange.type === "rowInsert";
|
|
2440
|
+
const location = getRowLocation(rowChange.path, rowIndex, tableIndex - 1);
|
|
2441
|
+
const preview = getRowPreview(rowChange.node);
|
|
2442
|
+
const markedNode = isInsert ? markAllTextAsInserted(cloneNode(rowChange.node), sharedId, author) : markAllTextAsDeleted(cloneNode(rowChange.node), sharedId, author);
|
|
2443
|
+
changes.push({
|
|
2444
|
+
...rowChange,
|
|
2445
|
+
node: markedNode
|
|
2345
2446
|
});
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2447
|
+
infos.push({
|
|
2448
|
+
id: sharedId,
|
|
2449
|
+
type: rowChange.type,
|
|
2450
|
+
nodeType: "tableRow",
|
|
2451
|
+
location,
|
|
2452
|
+
preview,
|
|
2453
|
+
author,
|
|
2454
|
+
date
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
if (isList(nodeA) && isList(nodeB)) {
|
|
2459
|
+
listIndex++;
|
|
2460
|
+
const listResult = diffLists(nodeA, nodeB, match.pathA, match.pathB);
|
|
2461
|
+
for (const itemChange of listResult.itemChanges) {
|
|
2462
|
+
const sharedId = itemChange.id;
|
|
2463
|
+
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
2464
|
+
const itemIndex = itemChange.path[itemChange.path.length - 1];
|
|
2465
|
+
const isInsert = itemChange.type === "listItemInsert";
|
|
2466
|
+
const location = getListItemLocation(itemChange.path, itemIndex, listIndex - 1);
|
|
2467
|
+
const preview = getListItemPreview(itemChange.node);
|
|
2468
|
+
const markedNode = isInsert ? markAllTextAsInserted(cloneNode(itemChange.node), sharedId, author) : markAllTextAsDeleted(cloneNode(itemChange.node), sharedId, author);
|
|
2469
|
+
changes.push({
|
|
2470
|
+
...itemChange,
|
|
2471
|
+
node: markedNode
|
|
2472
|
+
});
|
|
2473
|
+
infos.push({
|
|
2474
|
+
id: sharedId,
|
|
2475
|
+
type: itemChange.type,
|
|
2476
|
+
nodeType: "listItem",
|
|
2477
|
+
location,
|
|
2478
|
+
preview,
|
|
2479
|
+
author,
|
|
2480
|
+
date
|
|
2353
2481
|
});
|
|
2354
2482
|
}
|
|
2355
2483
|
}
|
|
2356
2484
|
}
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2485
|
+
const imageChanges = diffImages(docA, docB);
|
|
2486
|
+
for (const imgInsert of imageChanges.inserted) {
|
|
2487
|
+
const sharedId = imgInsert.id;
|
|
2488
|
+
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
2489
|
+
infos.push({
|
|
2490
|
+
id: sharedId,
|
|
2491
|
+
type: "imageInsert",
|
|
2492
|
+
nodeType: "image",
|
|
2493
|
+
location: getImageLocation(imgInsert.path),
|
|
2494
|
+
preview: getImagePreview(imgInsert.node),
|
|
2495
|
+
author,
|
|
2496
|
+
date
|
|
2497
|
+
});
|
|
2498
|
+
changes.push(imgInsert);
|
|
2367
2499
|
}
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2500
|
+
for (const imgDelete of imageChanges.deleted) {
|
|
2501
|
+
const sharedId = imgDelete.id;
|
|
2502
|
+
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
2503
|
+
infos.push({
|
|
2504
|
+
id: sharedId,
|
|
2505
|
+
type: "imageDelete",
|
|
2506
|
+
nodeType: "image",
|
|
2507
|
+
location: getImageLocation(imgDelete.path),
|
|
2508
|
+
preview: getImagePreview(imgDelete.node),
|
|
2509
|
+
author,
|
|
2510
|
+
date
|
|
2511
|
+
});
|
|
2512
|
+
changes.push(imgDelete);
|
|
2513
|
+
}
|
|
2514
|
+
return { changes, infos };
|
|
2381
2515
|
}
|
|
2382
|
-
function
|
|
2383
|
-
const
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2516
|
+
function generateStructuralChangeSummary(infos) {
|
|
2517
|
+
const summary = [];
|
|
2518
|
+
const rowInserts = infos.filter((i) => i.type === "rowInsert").length;
|
|
2519
|
+
const rowDeletes = infos.filter((i) => i.type === "rowDelete").length;
|
|
2520
|
+
const paragraphInserts = infos.filter((i) => i.type === "paragraphInsert").length;
|
|
2521
|
+
const paragraphDeletes = infos.filter((i) => i.type === "paragraphDelete").length;
|
|
2522
|
+
const listItemInserts = infos.filter((i) => i.type === "listItemInsert").length;
|
|
2523
|
+
const listItemDeletes = infos.filter((i) => i.type === "listItemDelete").length;
|
|
2524
|
+
const imageInserts = infos.filter((i) => i.type === "imageInsert").length;
|
|
2525
|
+
const imageDeletes = infos.filter((i) => i.type === "imageDelete").length;
|
|
2526
|
+
if (rowInserts > 0) summary.push(`${rowInserts} row(s) inserted`);
|
|
2527
|
+
if (rowDeletes > 0) summary.push(`${rowDeletes} row(s) deleted`);
|
|
2528
|
+
if (paragraphInserts > 0) summary.push(`${paragraphInserts} paragraph(s) inserted`);
|
|
2529
|
+
if (paragraphDeletes > 0) summary.push(`${paragraphDeletes} paragraph(s) deleted`);
|
|
2530
|
+
if (listItemInserts > 0) summary.push(`${listItemInserts} list item(s) inserted`);
|
|
2531
|
+
if (listItemDeletes > 0) summary.push(`${listItemDeletes} list item(s) deleted`);
|
|
2532
|
+
if (imageInserts > 0) summary.push(`${imageInserts} image(s) inserted`);
|
|
2533
|
+
if (imageDeletes > 0) summary.push(`${imageDeletes} image(s) deleted`);
|
|
2534
|
+
return summary;
|
|
2392
2535
|
}
|
|
2393
|
-
function
|
|
2394
|
-
return
|
|
2536
|
+
function cloneNode2(node) {
|
|
2537
|
+
return JSON.parse(JSON.stringify(node));
|
|
2395
2538
|
}
|
|
2396
|
-
function
|
|
2397
|
-
|
|
2398
|
-
const
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2539
|
+
function getMarkSpansForRange(spansB, start, end) {
|
|
2540
|
+
const result = [];
|
|
2541
|
+
for (const span of spansB) {
|
|
2542
|
+
if (span.to > start && span.from < end) {
|
|
2543
|
+
const overlapStart = Math.max(span.from, start);
|
|
2544
|
+
const overlapEnd = Math.min(span.to, end);
|
|
2545
|
+
result.push({
|
|
2546
|
+
relStart: overlapStart - start,
|
|
2547
|
+
relEnd: overlapEnd - start,
|
|
2548
|
+
marks: span.marks || []
|
|
2549
|
+
});
|
|
2550
|
+
}
|
|
2407
2551
|
}
|
|
2408
|
-
return
|
|
2552
|
+
return result;
|
|
2409
2553
|
}
|
|
2410
|
-
function
|
|
2411
|
-
|
|
2412
|
-
const
|
|
2413
|
-
|
|
2414
|
-
|
|
2554
|
+
function createInsertedTextNodes(text, posB, spansB, author, replacementId) {
|
|
2555
|
+
const result = [];
|
|
2556
|
+
const trackMark = createTrackInsertMark(author, replacementId);
|
|
2557
|
+
if (posB === void 0 || spansB.length === 0) {
|
|
2558
|
+
return [{
|
|
2559
|
+
type: "text",
|
|
2560
|
+
text,
|
|
2561
|
+
marks: [trackMark]
|
|
2562
|
+
}];
|
|
2415
2563
|
}
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2564
|
+
const markSpans = getMarkSpansForRange(spansB, posB, posB + text.length);
|
|
2565
|
+
if (markSpans.length === 0) {
|
|
2566
|
+
return [{
|
|
2567
|
+
type: "text",
|
|
2568
|
+
text,
|
|
2569
|
+
marks: [trackMark]
|
|
2570
|
+
}];
|
|
2571
|
+
}
|
|
2572
|
+
markSpans.sort((a, b) => a.relStart - b.relStart);
|
|
2573
|
+
let processedUpTo = 0;
|
|
2574
|
+
for (const span of markSpans) {
|
|
2575
|
+
if (span.relStart > processedUpTo) {
|
|
2576
|
+
result.push({
|
|
2577
|
+
type: "text",
|
|
2578
|
+
text: text.substring(processedUpTo, span.relStart),
|
|
2579
|
+
marks: [trackMark]
|
|
2580
|
+
});
|
|
2424
2581
|
}
|
|
2425
|
-
if (
|
|
2426
|
-
|
|
2427
|
-
|
|
2582
|
+
if (span.relEnd > span.relStart) {
|
|
2583
|
+
const spanText = text.substring(span.relStart, span.relEnd);
|
|
2584
|
+
const normalizedSpanMarks = normalizeMarksForRendering(span.marks);
|
|
2585
|
+
const marks = [...normalizedSpanMarks, trackMark];
|
|
2586
|
+
result.push({
|
|
2587
|
+
type: "text",
|
|
2588
|
+
text: spanText,
|
|
2589
|
+
marks
|
|
2428
2590
|
});
|
|
2591
|
+
processedUpTo = span.relEnd;
|
|
2429
2592
|
}
|
|
2430
2593
|
}
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
const inserted = [];
|
|
2438
|
-
const deleted = [];
|
|
2439
|
-
const idsA = /* @__PURE__ */ new Map();
|
|
2440
|
-
const idsB = /* @__PURE__ */ new Map();
|
|
2441
|
-
for (const img of imagesA) {
|
|
2442
|
-
const id = getImageIdentifier(img.node);
|
|
2443
|
-
idsA.set(id, img);
|
|
2444
|
-
}
|
|
2445
|
-
for (const img of imagesB) {
|
|
2446
|
-
const id = getImageIdentifier(img.node);
|
|
2447
|
-
idsB.set(id, img);
|
|
2594
|
+
if (processedUpTo < text.length) {
|
|
2595
|
+
result.push({
|
|
2596
|
+
type: "text",
|
|
2597
|
+
text: text.substring(processedUpTo),
|
|
2598
|
+
marks: [trackMark]
|
|
2599
|
+
});
|
|
2448
2600
|
}
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2601
|
+
return result;
|
|
2602
|
+
}
|
|
2603
|
+
function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
|
|
2604
|
+
const merged = cloneNode2(docA);
|
|
2605
|
+
const charStates = [];
|
|
2606
|
+
let insertions = [];
|
|
2607
|
+
const formatChanges = diffResult.formatChanges || [];
|
|
2608
|
+
function getFormatChangeAt(pos) {
|
|
2609
|
+
for (const fc of formatChanges) {
|
|
2610
|
+
if (pos >= fc.from && pos < fc.to) {
|
|
2611
|
+
return fc;
|
|
2612
|
+
}
|
|
2458
2613
|
}
|
|
2614
|
+
return null;
|
|
2459
2615
|
}
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2616
|
+
let docAOffset = 0;
|
|
2617
|
+
const segments = diffResult.segments;
|
|
2618
|
+
for (let segIdx = 0; segIdx < segments.length; segIdx++) {
|
|
2619
|
+
const segment = segments[segIdx];
|
|
2620
|
+
if (segment.type === "equal") {
|
|
2621
|
+
for (let i = 0; i < segment.text.length; i++) {
|
|
2622
|
+
charStates[docAOffset + i] = { type: "equal" };
|
|
2623
|
+
}
|
|
2624
|
+
docAOffset += segment.text.length;
|
|
2625
|
+
} else if (segment.type === "delete") {
|
|
2626
|
+
const nextSegment = segments[segIdx + 1];
|
|
2627
|
+
const isReplacement = nextSegment && nextSegment.type === "insert";
|
|
2628
|
+
const replacementId = isReplacement ? v4() : void 0;
|
|
2629
|
+
for (let i = 0; i < segment.text.length; i++) {
|
|
2630
|
+
charStates[docAOffset + i] = { type: "delete", replacementId };
|
|
2631
|
+
}
|
|
2632
|
+
docAOffset += segment.text.length;
|
|
2633
|
+
if (isReplacement && nextSegment) {
|
|
2634
|
+
insertions.push({
|
|
2635
|
+
afterOffset: docAOffset,
|
|
2636
|
+
text: nextSegment.text,
|
|
2637
|
+
replacementId,
|
|
2638
|
+
posB: nextSegment.posB
|
|
2639
|
+
// Capture docB position for mark lookup
|
|
2640
|
+
});
|
|
2641
|
+
segIdx++;
|
|
2642
|
+
}
|
|
2643
|
+
} else if (segment.type === "insert") {
|
|
2644
|
+
insertions.push({
|
|
2645
|
+
afterOffset: docAOffset,
|
|
2646
|
+
text: segment.text,
|
|
2647
|
+
posB: segment.posB
|
|
2648
|
+
// Capture docB position for mark lookup
|
|
2468
2649
|
});
|
|
2469
2650
|
}
|
|
2470
2651
|
}
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2652
|
+
const spansB = diffResult.spansB || [];
|
|
2653
|
+
function transformNode(node, nodeOffset, path) {
|
|
2654
|
+
if (node.type === "text" && node.text) {
|
|
2655
|
+
const text = node.text;
|
|
2656
|
+
const result = [];
|
|
2657
|
+
let i = 0;
|
|
2658
|
+
while (i < text.length) {
|
|
2659
|
+
const charOffset = nodeOffset + i;
|
|
2660
|
+
const charState = charStates[charOffset] || { type: "equal" };
|
|
2661
|
+
const insertionsHere = insertions.filter((ins) => ins.afterOffset === charOffset);
|
|
2662
|
+
for (const ins of insertionsHere) {
|
|
2663
|
+
const insertedNodes = createInsertedTextNodes(
|
|
2664
|
+
ins.text,
|
|
2665
|
+
ins.posB,
|
|
2666
|
+
spansB,
|
|
2667
|
+
author,
|
|
2668
|
+
ins.replacementId
|
|
2669
|
+
);
|
|
2670
|
+
result.push(...insertedNodes);
|
|
2671
|
+
}
|
|
2672
|
+
const currentFormatChange = getFormatChangeAt(nodeOffset + i);
|
|
2673
|
+
let j = i + 1;
|
|
2674
|
+
while (j < text.length) {
|
|
2675
|
+
const nextState = charStates[nodeOffset + j] || { type: "equal" };
|
|
2676
|
+
if (nextState.type !== charState.type) break;
|
|
2677
|
+
if (insertions.some((ins) => ins.afterOffset === nodeOffset + j)) break;
|
|
2678
|
+
const nextFormatChange = getFormatChangeAt(nodeOffset + j);
|
|
2679
|
+
if (currentFormatChange !== nextFormatChange) break;
|
|
2680
|
+
j++;
|
|
2681
|
+
}
|
|
2682
|
+
const chunk = text.substring(i, j);
|
|
2683
|
+
let marks = [...node.marks || []];
|
|
2684
|
+
if (charState.type === "delete") {
|
|
2685
|
+
marks.push(createTrackDeleteMark(author, charState.replacementId));
|
|
2686
|
+
} else if (charState.type === "equal") {
|
|
2687
|
+
if (currentFormatChange) {
|
|
2688
|
+
const trackFormatMark = createTrackFormatMark(
|
|
2689
|
+
currentFormatChange.before,
|
|
2690
|
+
currentFormatChange.after,
|
|
2691
|
+
author
|
|
2692
|
+
);
|
|
2693
|
+
const normalizedAfterMarks = normalizeMarksForRendering(currentFormatChange.after);
|
|
2694
|
+
marks = [...normalizedAfterMarks, trackFormatMark];
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
result.push({
|
|
2698
|
+
type: "text",
|
|
2699
|
+
text: chunk,
|
|
2700
|
+
marks: marks.length > 0 ? marks : void 0
|
|
2701
|
+
});
|
|
2702
|
+
i = j;
|
|
2703
|
+
}
|
|
2704
|
+
const endOffset = nodeOffset + text.length;
|
|
2705
|
+
const endInsertions = insertions.filter((ins) => ins.afterOffset === endOffset);
|
|
2706
|
+
for (const ins of endInsertions) {
|
|
2707
|
+
const insertedNodes = createInsertedTextNodes(
|
|
2708
|
+
ins.text,
|
|
2709
|
+
ins.posB,
|
|
2710
|
+
spansB,
|
|
2711
|
+
author,
|
|
2712
|
+
ins.replacementId
|
|
2713
|
+
);
|
|
2714
|
+
result.push(...insertedNodes);
|
|
2715
|
+
}
|
|
2716
|
+
insertions = insertions.filter(
|
|
2717
|
+
(ins) => ins.afterOffset < nodeOffset || ins.afterOffset > endOffset
|
|
2718
|
+
);
|
|
2719
|
+
return { nodes: result, consumedLength: text.length };
|
|
2720
|
+
}
|
|
2721
|
+
if (node.content && Array.isArray(node.content)) {
|
|
2722
|
+
const newContent = [];
|
|
2723
|
+
let offset = nodeOffset;
|
|
2724
|
+
for (const child of node.content) {
|
|
2725
|
+
const { nodes, consumedLength } = transformNode(child, offset);
|
|
2726
|
+
newContent.push(...nodes);
|
|
2727
|
+
offset += consumedLength;
|
|
2728
|
+
}
|
|
2729
|
+
return {
|
|
2730
|
+
nodes: [{ ...node, content: newContent }],
|
|
2731
|
+
consumedLength: offset - nodeOffset
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
return { nodes: [node], consumedLength: 0 };
|
|
2476
2735
|
}
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2736
|
+
if (merged.content && Array.isArray(merged.content)) {
|
|
2737
|
+
const newContent = [];
|
|
2738
|
+
let offset = 0;
|
|
2739
|
+
for (let i = 0; i < merged.content.length; i++) {
|
|
2740
|
+
const child = merged.content[i];
|
|
2741
|
+
const { nodes, consumedLength } = transformNode(child, offset);
|
|
2742
|
+
newContent.push(...nodes);
|
|
2743
|
+
offset += consumedLength;
|
|
2744
|
+
}
|
|
2745
|
+
merged.content = newContent;
|
|
2484
2746
|
}
|
|
2485
|
-
if (
|
|
2486
|
-
const
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2747
|
+
if (insertions.length > 0) {
|
|
2748
|
+
for (const ins of insertions) {
|
|
2749
|
+
const insertedNodes = createInsertedTextNodes(
|
|
2750
|
+
ins.text,
|
|
2751
|
+
ins.posB,
|
|
2752
|
+
spansB,
|
|
2753
|
+
author,
|
|
2754
|
+
ins.replacementId
|
|
2755
|
+
);
|
|
2756
|
+
const insertNode = {
|
|
2757
|
+
type: "paragraph",
|
|
2758
|
+
content: [
|
|
2759
|
+
{
|
|
2760
|
+
type: "run",
|
|
2761
|
+
content: insertedNodes
|
|
2762
|
+
}
|
|
2763
|
+
]
|
|
2764
|
+
};
|
|
2765
|
+
if (!merged.content) merged.content = [];
|
|
2766
|
+
merged.content.push(insertNode);
|
|
2490
2767
|
}
|
|
2491
2768
|
}
|
|
2492
|
-
return
|
|
2769
|
+
return merged;
|
|
2493
2770
|
}
|
|
2494
2771
|
|
|
2495
2772
|
// src/services/structuralMerger.ts
|
|
2496
|
-
function
|
|
2773
|
+
function cloneNode3(node) {
|
|
2497
2774
|
return JSON.parse(JSON.stringify(node));
|
|
2498
2775
|
}
|
|
2499
|
-
function
|
|
2776
|
+
function markAllTextAsInserted2(node, sharedId, author) {
|
|
2500
2777
|
if (node.type === "text") {
|
|
2501
2778
|
const existingMarks = normalizeMarksForRendering(node.marks || []);
|
|
2502
2779
|
return {
|
|
@@ -2508,13 +2785,13 @@ function markAllTextAsInserted(node, sharedId, author) {
|
|
|
2508
2785
|
return {
|
|
2509
2786
|
...node,
|
|
2510
2787
|
content: node.content.map(
|
|
2511
|
-
(child) =>
|
|
2788
|
+
(child) => markAllTextAsInserted2(child, sharedId, author)
|
|
2512
2789
|
)
|
|
2513
2790
|
};
|
|
2514
2791
|
}
|
|
2515
2792
|
return { ...node };
|
|
2516
2793
|
}
|
|
2517
|
-
function
|
|
2794
|
+
function markAllTextAsDeleted2(node, sharedId, author) {
|
|
2518
2795
|
if (node.type === "text") {
|
|
2519
2796
|
const existingMarks = normalizeMarksForRendering(node.marks || []);
|
|
2520
2797
|
return {
|
|
@@ -2526,13 +2803,13 @@ function markAllTextAsDeleted(node, sharedId, author) {
|
|
|
2526
2803
|
return {
|
|
2527
2804
|
...node,
|
|
2528
2805
|
content: node.content.map(
|
|
2529
|
-
(child) =>
|
|
2806
|
+
(child) => markAllTextAsDeleted2(child, sharedId, author)
|
|
2530
2807
|
)
|
|
2531
2808
|
};
|
|
2532
2809
|
}
|
|
2533
2810
|
return { ...node };
|
|
2534
2811
|
}
|
|
2535
|
-
function
|
|
2812
|
+
function extractTextPreview2(node, maxLength = 50) {
|
|
2536
2813
|
const texts = [];
|
|
2537
2814
|
function extract(n) {
|
|
2538
2815
|
if (n.type === "text") {
|
|
@@ -2718,7 +2995,7 @@ function mergeMatchedBlock(nodeA, nodeB, blockIndex, author) {
|
|
|
2718
2995
|
diff,
|
|
2719
2996
|
author
|
|
2720
2997
|
);
|
|
2721
|
-
const mergedNode = merged.content?.[0] ||
|
|
2998
|
+
const mergedNode = merged.content?.[0] || cloneNode3(nodeB);
|
|
2722
2999
|
return { mergedNode, infos, changes };
|
|
2723
3000
|
}
|
|
2724
3001
|
function mergeMatchedTable(tableA, tableB, tableIndex, author) {
|
|
@@ -2747,13 +3024,13 @@ function mergeMatchedTable(tableA, tableB, tableIndex, author) {
|
|
|
2747
3024
|
if (deletedIndices.has(checkIdx) && !processedDeletions.has(checkIdx)) {
|
|
2748
3025
|
const deletedRow = rowsA[checkIdx];
|
|
2749
3026
|
const changeId = v4();
|
|
2750
|
-
mergedRows.push(
|
|
3027
|
+
mergedRows.push(markAllTextAsDeleted2(cloneNode3(deletedRow), changeId, author));
|
|
2751
3028
|
tableInfos.push({
|
|
2752
3029
|
id: changeId,
|
|
2753
3030
|
type: "rowDelete",
|
|
2754
3031
|
nodeType: "tableRow",
|
|
2755
3032
|
location: `Table ${tableIndex}, Row ${checkIdx + 1}`,
|
|
2756
|
-
preview:
|
|
3033
|
+
preview: extractTextPreview2(deletedRow),
|
|
2757
3034
|
author,
|
|
2758
3035
|
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2759
3036
|
});
|
|
@@ -2765,13 +3042,13 @@ function mergeMatchedTable(tableA, tableB, tableIndex, author) {
|
|
|
2765
3042
|
changeCount += changes;
|
|
2766
3043
|
} else {
|
|
2767
3044
|
const changeId = v4();
|
|
2768
|
-
mergedRows.push(
|
|
3045
|
+
mergedRows.push(markAllTextAsInserted2(cloneNode3(rowB), changeId, author));
|
|
2769
3046
|
tableInfos.push({
|
|
2770
3047
|
id: changeId,
|
|
2771
3048
|
type: "rowInsert",
|
|
2772
3049
|
nodeType: "tableRow",
|
|
2773
3050
|
location: `Table ${tableIndex}, Row ${idxB + 1}`,
|
|
2774
|
-
preview:
|
|
3051
|
+
preview: extractTextPreview2(rowB),
|
|
2775
3052
|
author,
|
|
2776
3053
|
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2777
3054
|
});
|
|
@@ -2781,13 +3058,13 @@ function mergeMatchedTable(tableA, tableB, tableIndex, author) {
|
|
|
2781
3058
|
const idxA = deletion.path[deletion.path.length - 1];
|
|
2782
3059
|
if (!processedDeletions.has(idxA)) {
|
|
2783
3060
|
const changeId = v4();
|
|
2784
|
-
mergedRows.push(
|
|
3061
|
+
mergedRows.push(markAllTextAsDeleted2(cloneNode3(deletion.node), changeId, author));
|
|
2785
3062
|
tableInfos.push({
|
|
2786
3063
|
id: changeId,
|
|
2787
3064
|
type: "rowDelete",
|
|
2788
3065
|
nodeType: "tableRow",
|
|
2789
3066
|
location: `Table ${tableIndex}, Row ${idxA + 1}`,
|
|
2790
|
-
preview:
|
|
3067
|
+
preview: extractTextPreview2(deletion.node),
|
|
2791
3068
|
author,
|
|
2792
3069
|
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2793
3070
|
});
|
|
@@ -2825,13 +3102,13 @@ function mergeMatchedList(listA, listB, listIndex, author) {
|
|
|
2825
3102
|
if (deletedIndices.has(checkIdx) && !processedDeletions.has(checkIdx)) {
|
|
2826
3103
|
const deletedItem = itemsA[checkIdx];
|
|
2827
3104
|
const changeId = v4();
|
|
2828
|
-
mergedItems.push(
|
|
3105
|
+
mergedItems.push(markAllTextAsDeleted2(cloneNode3(deletedItem), changeId, author));
|
|
2829
3106
|
listInfos.push({
|
|
2830
3107
|
id: changeId,
|
|
2831
3108
|
type: "listItemDelete",
|
|
2832
3109
|
nodeType: "listItem",
|
|
2833
3110
|
location: `List ${listIndex}, Item ${checkIdx + 1}`,
|
|
2834
|
-
preview:
|
|
3111
|
+
preview: extractTextPreview2(deletedItem),
|
|
2835
3112
|
author,
|
|
2836
3113
|
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2837
3114
|
});
|
|
@@ -2843,13 +3120,13 @@ function mergeMatchedList(listA, listB, listIndex, author) {
|
|
|
2843
3120
|
changeCount += changes;
|
|
2844
3121
|
} else {
|
|
2845
3122
|
const changeId = v4();
|
|
2846
|
-
mergedItems.push(
|
|
3123
|
+
mergedItems.push(markAllTextAsInserted2(cloneNode3(itemB), changeId, author));
|
|
2847
3124
|
listInfos.push({
|
|
2848
3125
|
id: changeId,
|
|
2849
3126
|
type: "listItemInsert",
|
|
2850
3127
|
nodeType: "listItem",
|
|
2851
3128
|
location: `List ${listIndex}, Item ${idxB + 1}`,
|
|
2852
|
-
preview:
|
|
3129
|
+
preview: extractTextPreview2(itemB),
|
|
2853
3130
|
author,
|
|
2854
3131
|
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2855
3132
|
});
|
|
@@ -2859,13 +3136,13 @@ function mergeMatchedList(listA, listB, listIndex, author) {
|
|
|
2859
3136
|
const idxA = deletion.path[deletion.path.length - 1];
|
|
2860
3137
|
if (!processedDeletions.has(idxA)) {
|
|
2861
3138
|
const changeId = v4();
|
|
2862
|
-
mergedItems.push(
|
|
3139
|
+
mergedItems.push(markAllTextAsDeleted2(cloneNode3(deletion.node), changeId, author));
|
|
2863
3140
|
listInfos.push({
|
|
2864
3141
|
id: changeId,
|
|
2865
3142
|
type: "listItemDelete",
|
|
2866
3143
|
nodeType: "listItem",
|
|
2867
3144
|
location: `List ${listIndex}, Item ${idxA + 1}`,
|
|
2868
|
-
preview:
|
|
3145
|
+
preview: extractTextPreview2(deletion.node),
|
|
2869
3146
|
author,
|
|
2870
3147
|
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2871
3148
|
});
|
|
@@ -2879,14 +3156,14 @@ function mergeMatchedList(listA, listB, listIndex, author) {
|
|
|
2879
3156
|
}
|
|
2880
3157
|
function createInsertedBlock(node, blockIndex, author) {
|
|
2881
3158
|
const changeId = v4();
|
|
2882
|
-
const markedNode =
|
|
3159
|
+
const markedNode = markAllTextAsInserted2(cloneNode3(node), changeId, author);
|
|
2883
3160
|
const nodeDesc = getNodeTypeDescription(node);
|
|
2884
3161
|
const info = {
|
|
2885
3162
|
id: changeId,
|
|
2886
3163
|
type: isTable(node) ? "rowInsert" : isList(node) ? "listItemInsert" : isImage(node) ? "imageInsert" : "paragraphInsert",
|
|
2887
3164
|
nodeType: node.type || "unknown",
|
|
2888
3165
|
location: `${nodeDesc} inserted at position ${blockIndex}`,
|
|
2889
|
-
preview:
|
|
3166
|
+
preview: extractTextPreview2(node),
|
|
2890
3167
|
author,
|
|
2891
3168
|
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2892
3169
|
};
|
|
@@ -2894,14 +3171,14 @@ function createInsertedBlock(node, blockIndex, author) {
|
|
|
2894
3171
|
}
|
|
2895
3172
|
function createDeletedBlock(node, blockIndex, author) {
|
|
2896
3173
|
const changeId = v4();
|
|
2897
|
-
const markedNode =
|
|
3174
|
+
const markedNode = markAllTextAsDeleted2(cloneNode3(node), changeId, author);
|
|
2898
3175
|
const nodeDesc = getNodeTypeDescription(node);
|
|
2899
3176
|
const info = {
|
|
2900
3177
|
id: changeId,
|
|
2901
3178
|
type: isTable(node) ? "rowDelete" : isList(node) ? "listItemDelete" : isImage(node) ? "imageDelete" : "paragraphDelete",
|
|
2902
3179
|
nodeType: node.type || "unknown",
|
|
2903
3180
|
location: `${nodeDesc} deleted from position ${blockIndex}`,
|
|
2904
|
-
preview:
|
|
3181
|
+
preview: extractTextPreview2(node),
|
|
2905
3182
|
author,
|
|
2906
3183
|
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2907
3184
|
};
|
|
@@ -2960,8 +3237,9 @@ var DocxDiffEditor = forwardRef(
|
|
|
2960
3237
|
const mountedRef = useRef(true);
|
|
2961
3238
|
const initRef = useRef(false);
|
|
2962
3239
|
const readyRef = useRef(false);
|
|
3240
|
+
const rollbackJsonRef = useRef(null);
|
|
2963
3241
|
const [isLoading, setIsLoading] = useState(true);
|
|
2964
|
-
const [
|
|
3242
|
+
const [fatalError, setFatalError] = useState(null);
|
|
2965
3243
|
const [sourceJson, setSourceJson] = useState(null);
|
|
2966
3244
|
const [mergedJson, setMergedJson] = useState(null);
|
|
2967
3245
|
const [diffResult, setDiffResult] = useState(null);
|
|
@@ -3018,11 +3296,31 @@ var DocxDiffEditor = forwardRef(
|
|
|
3018
3296
|
sd.setTrackedChangesPreferences({ mode: "off", enabled: false });
|
|
3019
3297
|
}
|
|
3020
3298
|
}, []);
|
|
3021
|
-
const
|
|
3022
|
-
(err) => {
|
|
3023
|
-
const
|
|
3024
|
-
|
|
3025
|
-
onError?.(
|
|
3299
|
+
const handleFatalError = useCallback(
|
|
3300
|
+
(err, operation) => {
|
|
3301
|
+
const error = err instanceof Error ? err : new Error(err);
|
|
3302
|
+
setFatalError(error.message);
|
|
3303
|
+
onError?.({
|
|
3304
|
+
error,
|
|
3305
|
+
type: "fatal",
|
|
3306
|
+
operation,
|
|
3307
|
+
recoverable: false,
|
|
3308
|
+
message: error.message
|
|
3309
|
+
});
|
|
3310
|
+
},
|
|
3311
|
+
[onError]
|
|
3312
|
+
);
|
|
3313
|
+
const handleOperationError = useCallback(
|
|
3314
|
+
(err, operation, phase) => {
|
|
3315
|
+
const error = err instanceof Error ? err : new Error(err);
|
|
3316
|
+
onError?.({
|
|
3317
|
+
error,
|
|
3318
|
+
type: "operation",
|
|
3319
|
+
operation,
|
|
3320
|
+
recoverable: true,
|
|
3321
|
+
message: error.message,
|
|
3322
|
+
phase
|
|
3323
|
+
});
|
|
3026
3324
|
},
|
|
3027
3325
|
[onError]
|
|
3028
3326
|
);
|
|
@@ -3180,7 +3478,7 @@ var DocxDiffEditor = forwardRef(
|
|
|
3180
3478
|
return;
|
|
3181
3479
|
}
|
|
3182
3480
|
setIsLoading(true);
|
|
3183
|
-
|
|
3481
|
+
setFatalError(null);
|
|
3184
3482
|
destroySuperdoc();
|
|
3185
3483
|
try {
|
|
3186
3484
|
const { SuperDoc } = await import('superdoc');
|
|
@@ -3203,17 +3501,19 @@ var DocxDiffEditor = forwardRef(
|
|
|
3203
3501
|
if (sd?.activeEditor && isProseMirrorJSON(initialSource)) {
|
|
3204
3502
|
setEditorContent(sd.activeEditor, initialSource);
|
|
3205
3503
|
setSourceJson(initialSource);
|
|
3504
|
+
rollbackJsonRef.current = initialSource;
|
|
3206
3505
|
onSourceLoaded?.(initialSource);
|
|
3207
3506
|
}
|
|
3208
3507
|
} else {
|
|
3209
3508
|
setSourceJson(json);
|
|
3509
|
+
rollbackJsonRef.current = json;
|
|
3210
3510
|
onSourceLoaded?.(json);
|
|
3211
3511
|
}
|
|
3212
3512
|
setIsLoading(false);
|
|
3213
3513
|
onReady?.();
|
|
3214
3514
|
} catch (err) {
|
|
3215
3515
|
console.error("Failed to initialize SuperDoc:", err);
|
|
3216
|
-
|
|
3516
|
+
handleFatalError(err instanceof Error ? err : new Error("Failed to load editor"), "init");
|
|
3217
3517
|
setIsLoading(false);
|
|
3218
3518
|
initRef.current = false;
|
|
3219
3519
|
}
|
|
@@ -3227,7 +3527,7 @@ var DocxDiffEditor = forwardRef(
|
|
|
3227
3527
|
destroySuperdoc,
|
|
3228
3528
|
createSuperdoc,
|
|
3229
3529
|
setEditorContent,
|
|
3230
|
-
|
|
3530
|
+
handleFatalError
|
|
3231
3531
|
]);
|
|
3232
3532
|
useEffect(() => {
|
|
3233
3533
|
mountedRef.current = true;
|
|
@@ -3262,42 +3562,98 @@ var DocxDiffEditor = forwardRef(
|
|
|
3262
3562
|
* Accepts File (DOCX), HTML string, or ProseMirror JSON.
|
|
3263
3563
|
* Note: This destroys and recreates the SuperDoc instance.
|
|
3264
3564
|
* For JSON content updates, prefer updateContent() to preserve the existing template.
|
|
3565
|
+
*
|
|
3566
|
+
* On failure, attempts to preserve the previous editor state.
|
|
3567
|
+
* Returns void on success, or SetSourceError on recoverable failure.
|
|
3265
3568
|
*/
|
|
3266
3569
|
async setSource(content) {
|
|
3267
3570
|
if (!SuperDocRef.current) {
|
|
3268
|
-
|
|
3571
|
+
const error = new Error("Editor not initialized");
|
|
3572
|
+
handleOperationError(error, "setSource");
|
|
3573
|
+
return { success: false, error, message: error.message };
|
|
3269
3574
|
}
|
|
3575
|
+
const currentRollback = superdocRef.current?.activeEditor ? superdocRef.current.activeEditor.getJSON() : rollbackJsonRef.current;
|
|
3270
3576
|
setIsLoading(true);
|
|
3271
|
-
|
|
3577
|
+
setFatalError(null);
|
|
3272
3578
|
try {
|
|
3273
3579
|
const contentType = detectContentType(content);
|
|
3274
|
-
let
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
const result = await createSuperdoc({ html: content });
|
|
3281
|
-
json = result.json;
|
|
3282
|
-
} else {
|
|
3283
|
-
const result = await createSuperdoc(templateDocx ? { document: templateDocx } : {});
|
|
3284
|
-
if (result.superdoc?.activeEditor && isProseMirrorJSON(content)) {
|
|
3285
|
-
setEditorContent(result.superdoc.activeEditor, content);
|
|
3286
|
-
json = content;
|
|
3580
|
+
let validatedJson = null;
|
|
3581
|
+
try {
|
|
3582
|
+
if (contentType === "file") {
|
|
3583
|
+
validatedJson = await parseDocxFile(content, SuperDocRef.current);
|
|
3584
|
+
} else if (contentType === "html") {
|
|
3585
|
+
validatedJson = await parseHtmlToJson(content, SuperDocRef.current);
|
|
3287
3586
|
} else {
|
|
3587
|
+
if (!isProseMirrorJSON(content)) {
|
|
3588
|
+
throw new Error("Invalid ProseMirror JSON structure");
|
|
3589
|
+
}
|
|
3590
|
+
validatedJson = content;
|
|
3591
|
+
}
|
|
3592
|
+
} catch (parseErr) {
|
|
3593
|
+
const error = parseErr instanceof Error ? parseErr : new Error("Failed to parse content");
|
|
3594
|
+
handleOperationError(error, "setSource");
|
|
3595
|
+
setIsLoading(false);
|
|
3596
|
+
return { success: false, error, message: error.message };
|
|
3597
|
+
}
|
|
3598
|
+
destroySuperdoc();
|
|
3599
|
+
let json;
|
|
3600
|
+
try {
|
|
3601
|
+
if (contentType === "file") {
|
|
3602
|
+
const result = await createSuperdoc({ document: content });
|
|
3603
|
+
json = result.json;
|
|
3604
|
+
} else if (contentType === "html") {
|
|
3605
|
+
const result = await createSuperdoc({ html: content });
|
|
3288
3606
|
json = result.json;
|
|
3607
|
+
} else {
|
|
3608
|
+
const result = await createSuperdoc(templateDocx ? { document: templateDocx } : {});
|
|
3609
|
+
if (result.superdoc?.activeEditor && validatedJson) {
|
|
3610
|
+
setEditorContent(result.superdoc.activeEditor, validatedJson);
|
|
3611
|
+
json = validatedJson;
|
|
3612
|
+
} else {
|
|
3613
|
+
json = result.json;
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
} catch (createErr) {
|
|
3617
|
+
console.warn("[DocxDiffEditor] Failed to create new editor, attempting recovery:", createErr);
|
|
3618
|
+
if (currentRollback) {
|
|
3619
|
+
try {
|
|
3620
|
+
const recoveryResult = await createSuperdoc(templateDocx ? { document: templateDocx } : {});
|
|
3621
|
+
if (recoveryResult.superdoc?.activeEditor) {
|
|
3622
|
+
setEditorContent(recoveryResult.superdoc.activeEditor, currentRollback);
|
|
3623
|
+
setSourceJson(currentRollback);
|
|
3624
|
+
setEditingMode(superdocRef.current);
|
|
3625
|
+
}
|
|
3626
|
+
const error = createErr instanceof Error ? createErr : new Error("Failed to set source");
|
|
3627
|
+
handleOperationError(error, "setSource");
|
|
3628
|
+
setIsLoading(false);
|
|
3629
|
+
return { success: false, error, message: error.message };
|
|
3630
|
+
} catch (recoveryErr) {
|
|
3631
|
+
console.error("[DocxDiffEditor] Recovery failed:", recoveryErr);
|
|
3632
|
+
const error = createErr instanceof Error ? createErr : new Error("Failed to set source");
|
|
3633
|
+
handleFatalError(error, "setSource");
|
|
3634
|
+
setIsLoading(false);
|
|
3635
|
+
return { success: false, error, message: error.message };
|
|
3636
|
+
}
|
|
3637
|
+
} else {
|
|
3638
|
+
const error = createErr instanceof Error ? createErr : new Error("Failed to set source");
|
|
3639
|
+
handleFatalError(error, "setSource");
|
|
3640
|
+
setIsLoading(false);
|
|
3641
|
+
return { success: false, error, message: error.message };
|
|
3289
3642
|
}
|
|
3290
3643
|
}
|
|
3291
3644
|
setSourceJson(json);
|
|
3292
3645
|
setMergedJson(null);
|
|
3293
3646
|
setDiffResult(null);
|
|
3294
3647
|
setEditingMode(superdocRef.current);
|
|
3648
|
+
rollbackJsonRef.current = json;
|
|
3295
3649
|
onSourceLoaded?.(json);
|
|
3650
|
+
setIsLoading(false);
|
|
3651
|
+
return;
|
|
3296
3652
|
} catch (err) {
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
} finally {
|
|
3653
|
+
const error = err instanceof Error ? err : new Error("Failed to set source");
|
|
3654
|
+
handleFatalError(error, "setSource");
|
|
3300
3655
|
setIsLoading(false);
|
|
3656
|
+
return { success: false, error, message: error.message };
|
|
3301
3657
|
}
|
|
3302
3658
|
},
|
|
3303
3659
|
/**
|
|
@@ -3309,21 +3665,27 @@ var DocxDiffEditor = forwardRef(
|
|
|
3309
3665
|
*
|
|
3310
3666
|
* To compare against the original source document, call setSource() again
|
|
3311
3667
|
* before compareWith().
|
|
3668
|
+
*
|
|
3669
|
+
* Returns a union type - check `result.success` to determine outcome:
|
|
3670
|
+
* - `success: true` - Comparison succeeded
|
|
3671
|
+
* - `success: false` - Comparison failed, editor unchanged
|
|
3312
3672
|
*/
|
|
3313
3673
|
async compareWith(content) {
|
|
3314
3674
|
if (!SuperDocRef.current) {
|
|
3315
|
-
|
|
3675
|
+
const error = new Error("Editor not initialized");
|
|
3676
|
+
handleOperationError(error, "compareWith", "parsing");
|
|
3677
|
+
return { success: false, error, message: error.message, phase: "parsing" };
|
|
3316
3678
|
}
|
|
3317
3679
|
if (!superdocRef.current?.activeEditor) {
|
|
3318
|
-
|
|
3680
|
+
const error = new Error("Editor not ready. Ensure a document is loaded first.");
|
|
3681
|
+
handleOperationError(error, "compareWith", "parsing");
|
|
3682
|
+
return { success: false, error, message: error.message, phase: "parsing" };
|
|
3319
3683
|
}
|
|
3684
|
+
const rollbackJson = superdocRef.current.activeEditor.getJSON();
|
|
3320
3685
|
setIsLoading(true);
|
|
3686
|
+
let newJson;
|
|
3687
|
+
const contentType = detectContentType(content);
|
|
3321
3688
|
try {
|
|
3322
|
-
const currentEditorJson = superdocRef.current.activeEditor.getJSON();
|
|
3323
|
-
const cleanBaseline = acceptAllChangesInJson(currentEditorJson) || { type: "doc", content: [] };
|
|
3324
|
-
setSourceJson(cleanBaseline);
|
|
3325
|
-
const contentType = detectContentType(content);
|
|
3326
|
-
let newJson;
|
|
3327
3689
|
if (contentType === "file") {
|
|
3328
3690
|
newJson = await parseDocxFile(content, SuperDocRef.current);
|
|
3329
3691
|
} else if (contentType === "html") {
|
|
@@ -3378,91 +3740,122 @@ var DocxDiffEditor = forwardRef(
|
|
|
3378
3740
|
}
|
|
3379
3741
|
newJson = content;
|
|
3380
3742
|
}
|
|
3743
|
+
} catch (parseErr) {
|
|
3744
|
+
const error = parseErr instanceof Error ? parseErr : new Error("Failed to parse content");
|
|
3745
|
+
handleOperationError(error, "compareWith", "parsing");
|
|
3746
|
+
setIsLoading(false);
|
|
3747
|
+
return { success: false, error, message: error.message, phase: "parsing" };
|
|
3748
|
+
}
|
|
3749
|
+
let normalizedMerged;
|
|
3750
|
+
let normalizedNewJson;
|
|
3751
|
+
let structInfos;
|
|
3752
|
+
let diff;
|
|
3753
|
+
let cleanBaseline;
|
|
3754
|
+
try {
|
|
3755
|
+
const currentEditorJson = superdocRef.current.activeEditor.getJSON();
|
|
3756
|
+
cleanBaseline = acceptAllChangesInJson(currentEditorJson) || { type: "doc", content: [] };
|
|
3381
3757
|
const cleanNewJson = acceptAllChangesInJson(newJson) || { type: "doc", content: [] };
|
|
3382
|
-
|
|
3758
|
+
normalizedNewJson = normalizeRunProperties(cleanNewJson);
|
|
3383
3759
|
const structuralResult = mergeWithStructuralAwareness(
|
|
3384
3760
|
cleanBaseline,
|
|
3385
3761
|
normalizedNewJson,
|
|
3386
3762
|
author
|
|
3387
3763
|
);
|
|
3388
3764
|
const merged = structuralResult.mergedDoc;
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
editor: sd.activeEditor,
|
|
3406
|
-
comments: [],
|
|
3407
|
-
// Empty array - we just want to trigger createCommentForTrackChanges
|
|
3408
|
-
documentId: sd.activeEditor?.options?.documentId || "primary"
|
|
3409
|
-
});
|
|
3410
|
-
} catch (err) {
|
|
3411
|
-
console.warn("[DocxDiffEditor] Failed to process track changes for bubbles:", err);
|
|
3412
|
-
}
|
|
3413
|
-
}, 50);
|
|
3414
|
-
}
|
|
3415
|
-
} catch (contentErr) {
|
|
3416
|
-
console.warn(
|
|
3417
|
-
"[DocxDiffEditor] Failed to apply merged content with track changes. Falling back to direct content update without track bubbles.",
|
|
3418
|
-
contentErr
|
|
3419
|
-
);
|
|
3420
|
-
usedFallback = true;
|
|
3765
|
+
structInfos = structuralResult.structuralInfos;
|
|
3766
|
+
normalizedMerged = normalizeRunProperties(merged);
|
|
3767
|
+
diff = diffDocuments(cleanBaseline, cleanNewJson);
|
|
3768
|
+
} catch (mergeErr) {
|
|
3769
|
+
const error = mergeErr instanceof Error ? mergeErr : new Error("Failed to merge documents");
|
|
3770
|
+
handleOperationError(error, "compareWith", "merging");
|
|
3771
|
+
setIsLoading(false);
|
|
3772
|
+
return { success: false, error, message: error.message, phase: "merging" };
|
|
3773
|
+
}
|
|
3774
|
+
let usedFallback = false;
|
|
3775
|
+
try {
|
|
3776
|
+
setEditorContent(superdocRef.current.activeEditor, normalizedMerged);
|
|
3777
|
+
enableReviewMode(superdocRef.current);
|
|
3778
|
+
const sd = superdocRef.current;
|
|
3779
|
+
if (sd.commentsStore?.processLoadedDocxComments) {
|
|
3780
|
+
setTimeout(() => {
|
|
3421
3781
|
try {
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3782
|
+
sd.commentsStore.processLoadedDocxComments({
|
|
3783
|
+
superdoc: sd,
|
|
3784
|
+
editor: sd.activeEditor,
|
|
3785
|
+
comments: [],
|
|
3786
|
+
documentId: sd.activeEditor?.options?.documentId || "primary"
|
|
3787
|
+
});
|
|
3788
|
+
} catch (err) {
|
|
3789
|
+
console.warn("[DocxDiffEditor] Failed to process track changes for bubbles:", err);
|
|
3427
3790
|
}
|
|
3428
|
-
}
|
|
3429
|
-
}
|
|
3430
|
-
if (!usedFallback) {
|
|
3431
|
-
setStructuralChanges(structInfos);
|
|
3432
|
-
setIsPaneDismissed(false);
|
|
3433
|
-
} else {
|
|
3434
|
-
setStructuralChanges([]);
|
|
3791
|
+
}, 50);
|
|
3435
3792
|
}
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3793
|
+
} catch (contentErr) {
|
|
3794
|
+
console.warn(
|
|
3795
|
+
"[DocxDiffEditor] Failed to apply merged content with track changes. Falling back to direct content update without track bubbles.",
|
|
3796
|
+
contentErr
|
|
3797
|
+
);
|
|
3798
|
+
usedFallback = true;
|
|
3799
|
+
try {
|
|
3800
|
+
setEditorContent(superdocRef.current.activeEditor, normalizedNewJson);
|
|
3801
|
+
setEditingMode(superdocRef.current);
|
|
3802
|
+
} catch (fallbackErr) {
|
|
3803
|
+
console.warn("[DocxDiffEditor] Fallback content update failed, attempting rollback:", fallbackErr);
|
|
3804
|
+
try {
|
|
3805
|
+
setEditorContent(superdocRef.current.activeEditor, rollbackJson);
|
|
3806
|
+
setEditingMode(superdocRef.current);
|
|
3807
|
+
const error = contentErr instanceof Error ? contentErr : new Error("Failed to apply comparison");
|
|
3808
|
+
handleOperationError(error, "compareWith", "applying");
|
|
3809
|
+
setIsLoading(false);
|
|
3810
|
+
return { success: false, error, message: error.message, phase: "applying" };
|
|
3811
|
+
} catch (rollbackErr) {
|
|
3812
|
+
console.error("[DocxDiffEditor] All recovery attempts failed:", rollbackErr);
|
|
3813
|
+
const error = contentErr instanceof Error ? contentErr : new Error("Failed to apply comparison");
|
|
3814
|
+
handleFatalError(error, "compareWith");
|
|
3815
|
+
setIsLoading(false);
|
|
3816
|
+
return { success: false, error, message: error.message, phase: "applying" };
|
|
3817
|
+
}
|
|
3443
3818
|
}
|
|
3444
|
-
|
|
3445
|
-
|
|
3819
|
+
}
|
|
3820
|
+
setSourceJson(cleanBaseline);
|
|
3821
|
+
setMergedJson(normalizedMerged);
|
|
3822
|
+
setDiffResult(diff);
|
|
3823
|
+
rollbackJsonRef.current = usedFallback ? normalizedNewJson : normalizedMerged;
|
|
3824
|
+
if (!usedFallback) {
|
|
3825
|
+
setStructuralChanges(structInfos);
|
|
3826
|
+
setIsPaneDismissed(false);
|
|
3827
|
+
} else {
|
|
3828
|
+
setStructuralChanges([]);
|
|
3829
|
+
}
|
|
3830
|
+
const insertions = diff.segments.filter((s) => s.type === "insert").length;
|
|
3831
|
+
const deletions = diff.segments.filter((s) => s.type === "delete").length;
|
|
3832
|
+
const formatChanges = diff.formatChanges?.length || 0;
|
|
3833
|
+
const structuralChangeCount = usedFallback ? 0 : structInfos.length;
|
|
3834
|
+
const combinedSummary = [...diff.summary || []];
|
|
3835
|
+
if (structInfos.length > 0 && !usedFallback) {
|
|
3836
|
+
const structSummaries = generateStructuralChangeSummary(structInfos);
|
|
3837
|
+
if (structSummaries.length > 0) {
|
|
3838
|
+
combinedSummary.unshift(...structSummaries);
|
|
3446
3839
|
}
|
|
3447
|
-
const result = {
|
|
3448
|
-
totalChanges: insertions + deletions + formatChanges + structuralChangeCount,
|
|
3449
|
-
insertions,
|
|
3450
|
-
deletions,
|
|
3451
|
-
formatChanges,
|
|
3452
|
-
structuralChanges: structuralChangeCount,
|
|
3453
|
-
summary: combinedSummary,
|
|
3454
|
-
mergedJson: usedFallback ? normalizedNewJson : merged,
|
|
3455
|
-
structuralChangeInfos: usedFallback ? [] : structInfos,
|
|
3456
|
-
usedFallback
|
|
3457
|
-
};
|
|
3458
|
-
onComparisonComplete?.(result);
|
|
3459
|
-
return result;
|
|
3460
|
-
} catch (err) {
|
|
3461
|
-
handleError(err instanceof Error ? err : new Error("Comparison failed"));
|
|
3462
|
-
throw err;
|
|
3463
|
-
} finally {
|
|
3464
|
-
setIsLoading(false);
|
|
3465
3840
|
}
|
|
3841
|
+
if (usedFallback) {
|
|
3842
|
+
combinedSummary.push("Note: Track change visualization unavailable for this content");
|
|
3843
|
+
}
|
|
3844
|
+
const result = {
|
|
3845
|
+
success: true,
|
|
3846
|
+
totalChanges: insertions + deletions + formatChanges + structuralChangeCount,
|
|
3847
|
+
insertions,
|
|
3848
|
+
deletions,
|
|
3849
|
+
formatChanges,
|
|
3850
|
+
structuralChanges: structuralChangeCount,
|
|
3851
|
+
summary: combinedSummary,
|
|
3852
|
+
mergedJson: usedFallback ? normalizedNewJson : normalizedMerged,
|
|
3853
|
+
structuralChangeInfos: usedFallback ? [] : structInfos,
|
|
3854
|
+
usedFallback
|
|
3855
|
+
};
|
|
3856
|
+
onComparisonComplete?.(result);
|
|
3857
|
+
setIsLoading(false);
|
|
3858
|
+
return result;
|
|
3466
3859
|
},
|
|
3467
3860
|
/**
|
|
3468
3861
|
* Get raw diff segments
|
|
@@ -3790,7 +4183,8 @@ var DocxDiffEditor = forwardRef(
|
|
|
3790
4183
|
setEditingMode,
|
|
3791
4184
|
onSourceLoaded,
|
|
3792
4185
|
onComparisonComplete,
|
|
3793
|
-
|
|
4186
|
+
handleFatalError,
|
|
4187
|
+
handleOperationError
|
|
3794
4188
|
]
|
|
3795
4189
|
);
|
|
3796
4190
|
return /* @__PURE__ */ jsxs("div", { className: `dde-container ${className}`.trim(), children: [
|
|
@@ -3798,7 +4192,7 @@ var DocxDiffEditor = forwardRef(
|
|
|
3798
4192
|
/* @__PURE__ */ jsx("div", { className: "dde-loading__spinner" }),
|
|
3799
4193
|
/* @__PURE__ */ jsx("p", { className: "dde-loading__text", children: "Loading document..." })
|
|
3800
4194
|
] }),
|
|
3801
|
-
|
|
4195
|
+
fatalError && /* @__PURE__ */ jsxs("div", { className: "dde-error", children: [
|
|
3802
4196
|
/* @__PURE__ */ jsx("div", { className: "dde-error__icon", children: /* @__PURE__ */ jsx(
|
|
3803
4197
|
"svg",
|
|
3804
4198
|
{
|
|
@@ -3818,7 +4212,7 @@ var DocxDiffEditor = forwardRef(
|
|
|
3818
4212
|
}
|
|
3819
4213
|
) }),
|
|
3820
4214
|
/* @__PURE__ */ jsx("p", { className: "dde-error__title", children: "Failed to load document" }),
|
|
3821
|
-
/* @__PURE__ */ jsx("p", { className: "dde-error__message", children:
|
|
4215
|
+
/* @__PURE__ */ jsx("p", { className: "dde-error__message", children: fatalError })
|
|
3822
4216
|
] }),
|
|
3823
4217
|
showToolbar && /* @__PURE__ */ jsx(
|
|
3824
4218
|
"div",
|
|
@@ -3855,256 +4249,6 @@ var DocxDiffEditor_default = DocxDiffEditor;
|
|
|
3855
4249
|
|
|
3856
4250
|
// src/services/index.ts
|
|
3857
4251
|
init_nodeFingerprint();
|
|
3858
|
-
function markAllTextAsInserted2(node, sharedId, author) {
|
|
3859
|
-
if (node.type === "text") {
|
|
3860
|
-
const existingMarks = normalizeMarksForRendering(node.marks || []);
|
|
3861
|
-
return {
|
|
3862
|
-
...node,
|
|
3863
|
-
marks: [...existingMarks, createTrackInsertMark(author, sharedId)]
|
|
3864
|
-
};
|
|
3865
|
-
}
|
|
3866
|
-
if (node.content && Array.isArray(node.content)) {
|
|
3867
|
-
return {
|
|
3868
|
-
...node,
|
|
3869
|
-
content: node.content.map(
|
|
3870
|
-
(child) => markAllTextAsInserted2(child, sharedId, author)
|
|
3871
|
-
)
|
|
3872
|
-
};
|
|
3873
|
-
}
|
|
3874
|
-
return node;
|
|
3875
|
-
}
|
|
3876
|
-
function markAllTextAsDeleted2(node, sharedId, author) {
|
|
3877
|
-
if (node.type === "text") {
|
|
3878
|
-
const existingMarks = normalizeMarksForRendering(node.marks || []);
|
|
3879
|
-
return {
|
|
3880
|
-
...node,
|
|
3881
|
-
marks: [...existingMarks, createTrackDeleteMark(author, sharedId)]
|
|
3882
|
-
};
|
|
3883
|
-
}
|
|
3884
|
-
if (node.content && Array.isArray(node.content)) {
|
|
3885
|
-
return {
|
|
3886
|
-
...node,
|
|
3887
|
-
content: node.content.map(
|
|
3888
|
-
(child) => markAllTextAsDeleted2(child, sharedId, author)
|
|
3889
|
-
)
|
|
3890
|
-
};
|
|
3891
|
-
}
|
|
3892
|
-
return node;
|
|
3893
|
-
}
|
|
3894
|
-
function cloneNode3(node) {
|
|
3895
|
-
return JSON.parse(JSON.stringify(node));
|
|
3896
|
-
}
|
|
3897
|
-
function extractTextPreview2(node, maxLength = 50) {
|
|
3898
|
-
const texts = [];
|
|
3899
|
-
function extract(n) {
|
|
3900
|
-
if (n.type === "text") {
|
|
3901
|
-
texts.push(n.text || "");
|
|
3902
|
-
}
|
|
3903
|
-
if (n.content) {
|
|
3904
|
-
for (const child of n.content) {
|
|
3905
|
-
extract(child);
|
|
3906
|
-
}
|
|
3907
|
-
}
|
|
3908
|
-
}
|
|
3909
|
-
extract(node);
|
|
3910
|
-
const text = texts.join("").trim();
|
|
3911
|
-
if (text.length > maxLength) {
|
|
3912
|
-
return text.substring(0, maxLength - 3) + "...";
|
|
3913
|
-
}
|
|
3914
|
-
return text || "(empty)";
|
|
3915
|
-
}
|
|
3916
|
-
function processStructuralChanges(docA, docB, author = DEFAULT_AUTHOR) {
|
|
3917
|
-
const changes = [];
|
|
3918
|
-
const infos = [];
|
|
3919
|
-
const alignment = alignDocuments(docA, docB);
|
|
3920
|
-
let tableIndex = 0;
|
|
3921
|
-
let listIndex = 0;
|
|
3922
|
-
let paragraphIndex = 0;
|
|
3923
|
-
for (const inserted of alignment.insertions) {
|
|
3924
|
-
const node = inserted.node;
|
|
3925
|
-
const sharedId = v4();
|
|
3926
|
-
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
3927
|
-
let type = "paragraphInsert";
|
|
3928
|
-
let location = "";
|
|
3929
|
-
let preview = "";
|
|
3930
|
-
if (isTable(node)) {
|
|
3931
|
-
type = "rowInsert";
|
|
3932
|
-
location = `New table at position ${inserted.path[0] + 1}`;
|
|
3933
|
-
preview = `Table with ${node.content?.length || 0} rows`;
|
|
3934
|
-
tableIndex++;
|
|
3935
|
-
} else if (isList(node)) {
|
|
3936
|
-
type = "listItemInsert";
|
|
3937
|
-
location = `New list at position ${inserted.path[0] + 1}`;
|
|
3938
|
-
preview = `List with ${node.content?.length || 0} items`;
|
|
3939
|
-
listIndex++;
|
|
3940
|
-
} else {
|
|
3941
|
-
type = "paragraphInsert";
|
|
3942
|
-
paragraphIndex++;
|
|
3943
|
-
location = `Paragraph ${paragraphIndex}`;
|
|
3944
|
-
preview = extractTextPreview2(node);
|
|
3945
|
-
}
|
|
3946
|
-
changes.push({
|
|
3947
|
-
id: sharedId,
|
|
3948
|
-
type,
|
|
3949
|
-
nodeType: node.type,
|
|
3950
|
-
path: inserted.path,
|
|
3951
|
-
node: markAllTextAsInserted2(cloneNode3(node), sharedId, author)
|
|
3952
|
-
});
|
|
3953
|
-
infos.push({
|
|
3954
|
-
id: sharedId,
|
|
3955
|
-
type,
|
|
3956
|
-
nodeType: node.type,
|
|
3957
|
-
location,
|
|
3958
|
-
preview,
|
|
3959
|
-
author,
|
|
3960
|
-
date
|
|
3961
|
-
});
|
|
3962
|
-
}
|
|
3963
|
-
for (const deleted of alignment.deletions) {
|
|
3964
|
-
const node = deleted.node;
|
|
3965
|
-
const sharedId = v4();
|
|
3966
|
-
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
3967
|
-
let type = "paragraphDelete";
|
|
3968
|
-
let location = "";
|
|
3969
|
-
let preview = "";
|
|
3970
|
-
if (isTable(node)) {
|
|
3971
|
-
type = "rowDelete";
|
|
3972
|
-
location = `Deleted table at position ${deleted.path[0] + 1}`;
|
|
3973
|
-
preview = `Table with ${node.content?.length || 0} rows`;
|
|
3974
|
-
} else if (isList(node)) {
|
|
3975
|
-
type = "listItemDelete";
|
|
3976
|
-
location = `Deleted list at position ${deleted.path[0] + 1}`;
|
|
3977
|
-
preview = `List with ${node.content?.length || 0} items`;
|
|
3978
|
-
} else {
|
|
3979
|
-
type = "paragraphDelete";
|
|
3980
|
-
location = `Deleted paragraph`;
|
|
3981
|
-
preview = extractTextPreview2(node);
|
|
3982
|
-
}
|
|
3983
|
-
changes.push({
|
|
3984
|
-
id: sharedId,
|
|
3985
|
-
type,
|
|
3986
|
-
nodeType: node.type,
|
|
3987
|
-
path: deleted.path,
|
|
3988
|
-
node: markAllTextAsDeleted2(cloneNode3(node), sharedId, author)
|
|
3989
|
-
});
|
|
3990
|
-
infos.push({
|
|
3991
|
-
id: sharedId,
|
|
3992
|
-
type,
|
|
3993
|
-
nodeType: node.type,
|
|
3994
|
-
location,
|
|
3995
|
-
preview,
|
|
3996
|
-
author,
|
|
3997
|
-
date
|
|
3998
|
-
});
|
|
3999
|
-
}
|
|
4000
|
-
for (const match of alignment.matched) {
|
|
4001
|
-
const nodeA = docA.content?.[match.pathA[0]];
|
|
4002
|
-
const nodeB = docB.content?.[match.pathB[0]];
|
|
4003
|
-
if (!nodeA || !nodeB) continue;
|
|
4004
|
-
if (isTable(nodeA) && isTable(nodeB)) {
|
|
4005
|
-
tableIndex++;
|
|
4006
|
-
const tableResult = diffTables(nodeA, nodeB, match.pathA, match.pathB);
|
|
4007
|
-
for (const rowChange of tableResult.rowChanges) {
|
|
4008
|
-
const sharedId = rowChange.id;
|
|
4009
|
-
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
4010
|
-
const rowIndex = rowChange.path[rowChange.path.length - 1];
|
|
4011
|
-
const isInsert = rowChange.type === "rowInsert";
|
|
4012
|
-
const location = getRowLocation(rowChange.path, rowIndex, tableIndex - 1);
|
|
4013
|
-
const preview = getRowPreview(rowChange.node);
|
|
4014
|
-
const markedNode = isInsert ? markAllTextAsInserted2(cloneNode3(rowChange.node), sharedId, author) : markAllTextAsDeleted2(cloneNode3(rowChange.node), sharedId, author);
|
|
4015
|
-
changes.push({
|
|
4016
|
-
...rowChange,
|
|
4017
|
-
node: markedNode
|
|
4018
|
-
});
|
|
4019
|
-
infos.push({
|
|
4020
|
-
id: sharedId,
|
|
4021
|
-
type: rowChange.type,
|
|
4022
|
-
nodeType: "tableRow",
|
|
4023
|
-
location,
|
|
4024
|
-
preview,
|
|
4025
|
-
author,
|
|
4026
|
-
date
|
|
4027
|
-
});
|
|
4028
|
-
}
|
|
4029
|
-
}
|
|
4030
|
-
if (isList(nodeA) && isList(nodeB)) {
|
|
4031
|
-
listIndex++;
|
|
4032
|
-
const listResult = diffLists(nodeA, nodeB, match.pathA, match.pathB);
|
|
4033
|
-
for (const itemChange of listResult.itemChanges) {
|
|
4034
|
-
const sharedId = itemChange.id;
|
|
4035
|
-
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
4036
|
-
const itemIndex = itemChange.path[itemChange.path.length - 1];
|
|
4037
|
-
const isInsert = itemChange.type === "listItemInsert";
|
|
4038
|
-
const location = getListItemLocation(itemChange.path, itemIndex, listIndex - 1);
|
|
4039
|
-
const preview = getListItemPreview(itemChange.node);
|
|
4040
|
-
const markedNode = isInsert ? markAllTextAsInserted2(cloneNode3(itemChange.node), sharedId, author) : markAllTextAsDeleted2(cloneNode3(itemChange.node), sharedId, author);
|
|
4041
|
-
changes.push({
|
|
4042
|
-
...itemChange,
|
|
4043
|
-
node: markedNode
|
|
4044
|
-
});
|
|
4045
|
-
infos.push({
|
|
4046
|
-
id: sharedId,
|
|
4047
|
-
type: itemChange.type,
|
|
4048
|
-
nodeType: "listItem",
|
|
4049
|
-
location,
|
|
4050
|
-
preview,
|
|
4051
|
-
author,
|
|
4052
|
-
date
|
|
4053
|
-
});
|
|
4054
|
-
}
|
|
4055
|
-
}
|
|
4056
|
-
}
|
|
4057
|
-
const imageChanges = diffImages(docA, docB);
|
|
4058
|
-
for (const imgInsert of imageChanges.inserted) {
|
|
4059
|
-
const sharedId = imgInsert.id;
|
|
4060
|
-
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
4061
|
-
infos.push({
|
|
4062
|
-
id: sharedId,
|
|
4063
|
-
type: "imageInsert",
|
|
4064
|
-
nodeType: "image",
|
|
4065
|
-
location: getImageLocation(imgInsert.path),
|
|
4066
|
-
preview: getImagePreview(imgInsert.node),
|
|
4067
|
-
author,
|
|
4068
|
-
date
|
|
4069
|
-
});
|
|
4070
|
-
changes.push(imgInsert);
|
|
4071
|
-
}
|
|
4072
|
-
for (const imgDelete of imageChanges.deleted) {
|
|
4073
|
-
const sharedId = imgDelete.id;
|
|
4074
|
-
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
4075
|
-
infos.push({
|
|
4076
|
-
id: sharedId,
|
|
4077
|
-
type: "imageDelete",
|
|
4078
|
-
nodeType: "image",
|
|
4079
|
-
location: getImageLocation(imgDelete.path),
|
|
4080
|
-
preview: getImagePreview(imgDelete.node),
|
|
4081
|
-
author,
|
|
4082
|
-
date
|
|
4083
|
-
});
|
|
4084
|
-
changes.push(imgDelete);
|
|
4085
|
-
}
|
|
4086
|
-
return { changes, infos };
|
|
4087
|
-
}
|
|
4088
|
-
function generateStructuralChangeSummary(infos) {
|
|
4089
|
-
const summary = [];
|
|
4090
|
-
const rowInserts = infos.filter((i) => i.type === "rowInsert").length;
|
|
4091
|
-
const rowDeletes = infos.filter((i) => i.type === "rowDelete").length;
|
|
4092
|
-
const paragraphInserts = infos.filter((i) => i.type === "paragraphInsert").length;
|
|
4093
|
-
const paragraphDeletes = infos.filter((i) => i.type === "paragraphDelete").length;
|
|
4094
|
-
const listItemInserts = infos.filter((i) => i.type === "listItemInsert").length;
|
|
4095
|
-
const listItemDeletes = infos.filter((i) => i.type === "listItemDelete").length;
|
|
4096
|
-
const imageInserts = infos.filter((i) => i.type === "imageInsert").length;
|
|
4097
|
-
const imageDeletes = infos.filter((i) => i.type === "imageDelete").length;
|
|
4098
|
-
if (rowInserts > 0) summary.push(`${rowInserts} row(s) inserted`);
|
|
4099
|
-
if (rowDeletes > 0) summary.push(`${rowDeletes} row(s) deleted`);
|
|
4100
|
-
if (paragraphInserts > 0) summary.push(`${paragraphInserts} paragraph(s) inserted`);
|
|
4101
|
-
if (paragraphDeletes > 0) summary.push(`${paragraphDeletes} paragraph(s) deleted`);
|
|
4102
|
-
if (listItemInserts > 0) summary.push(`${listItemInserts} list item(s) inserted`);
|
|
4103
|
-
if (listItemDeletes > 0) summary.push(`${listItemDeletes} list item(s) deleted`);
|
|
4104
|
-
if (imageInserts > 0) summary.push(`${imageInserts} image(s) inserted`);
|
|
4105
|
-
if (imageDeletes > 0) summary.push(`${imageDeletes} image(s) deleted`);
|
|
4106
|
-
return summary;
|
|
4107
|
-
}
|
|
4108
4252
|
|
|
4109
4253
|
// src/blankTemplate.ts
|
|
4110
4254
|
var BLANK_DOCX_BASE64 = `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`;
|