docx-diff-editor 1.0.61 → 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 +747 -628
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +747 -628
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1776,241 +1776,6 @@ function alignListItems(listA, listB, listPathA, listPathB) {
|
|
|
1776
1776
|
}
|
|
1777
1777
|
return alignNodes(itemsA, itemsB);
|
|
1778
1778
|
}
|
|
1779
|
-
function cloneNode(node) {
|
|
1780
|
-
return JSON.parse(JSON.stringify(node));
|
|
1781
|
-
}
|
|
1782
|
-
function getMarkSpansForRange(spansB, start, end) {
|
|
1783
|
-
const result = [];
|
|
1784
|
-
for (const span of spansB) {
|
|
1785
|
-
if (span.to > start && span.from < end) {
|
|
1786
|
-
const overlapStart = Math.max(span.from, start);
|
|
1787
|
-
const overlapEnd = Math.min(span.to, end);
|
|
1788
|
-
result.push({
|
|
1789
|
-
relStart: overlapStart - start,
|
|
1790
|
-
relEnd: overlapEnd - start,
|
|
1791
|
-
marks: span.marks || []
|
|
1792
|
-
});
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
|
-
return result;
|
|
1796
|
-
}
|
|
1797
|
-
function createInsertedTextNodes(text, posB, spansB, author, replacementId) {
|
|
1798
|
-
const result = [];
|
|
1799
|
-
const trackMark = createTrackInsertMark(author, replacementId);
|
|
1800
|
-
if (posB === void 0 || spansB.length === 0) {
|
|
1801
|
-
return [{
|
|
1802
|
-
type: "text",
|
|
1803
|
-
text,
|
|
1804
|
-
marks: [trackMark]
|
|
1805
|
-
}];
|
|
1806
|
-
}
|
|
1807
|
-
const markSpans = getMarkSpansForRange(spansB, posB, posB + text.length);
|
|
1808
|
-
if (markSpans.length === 0) {
|
|
1809
|
-
return [{
|
|
1810
|
-
type: "text",
|
|
1811
|
-
text,
|
|
1812
|
-
marks: [trackMark]
|
|
1813
|
-
}];
|
|
1814
|
-
}
|
|
1815
|
-
markSpans.sort((a, b) => a.relStart - b.relStart);
|
|
1816
|
-
let processedUpTo = 0;
|
|
1817
|
-
for (const span of markSpans) {
|
|
1818
|
-
if (span.relStart > processedUpTo) {
|
|
1819
|
-
result.push({
|
|
1820
|
-
type: "text",
|
|
1821
|
-
text: text.substring(processedUpTo, span.relStart),
|
|
1822
|
-
marks: [trackMark]
|
|
1823
|
-
});
|
|
1824
|
-
}
|
|
1825
|
-
if (span.relEnd > span.relStart) {
|
|
1826
|
-
const spanText = text.substring(span.relStart, span.relEnd);
|
|
1827
|
-
const normalizedSpanMarks = normalizeMarksForRendering(span.marks);
|
|
1828
|
-
const marks = [...normalizedSpanMarks, trackMark];
|
|
1829
|
-
result.push({
|
|
1830
|
-
type: "text",
|
|
1831
|
-
text: spanText,
|
|
1832
|
-
marks
|
|
1833
|
-
});
|
|
1834
|
-
processedUpTo = span.relEnd;
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1837
|
-
if (processedUpTo < text.length) {
|
|
1838
|
-
result.push({
|
|
1839
|
-
type: "text",
|
|
1840
|
-
text: text.substring(processedUpTo),
|
|
1841
|
-
marks: [trackMark]
|
|
1842
|
-
});
|
|
1843
|
-
}
|
|
1844
|
-
return result;
|
|
1845
|
-
}
|
|
1846
|
-
function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
|
|
1847
|
-
const merged = cloneNode(docA);
|
|
1848
|
-
const charStates = [];
|
|
1849
|
-
let insertions = [];
|
|
1850
|
-
const formatChanges = diffResult.formatChanges || [];
|
|
1851
|
-
function getFormatChangeAt(pos) {
|
|
1852
|
-
for (const fc of formatChanges) {
|
|
1853
|
-
if (pos >= fc.from && pos < fc.to) {
|
|
1854
|
-
return fc;
|
|
1855
|
-
}
|
|
1856
|
-
}
|
|
1857
|
-
return null;
|
|
1858
|
-
}
|
|
1859
|
-
let docAOffset = 0;
|
|
1860
|
-
const segments = diffResult.segments;
|
|
1861
|
-
for (let segIdx = 0; segIdx < segments.length; segIdx++) {
|
|
1862
|
-
const segment = segments[segIdx];
|
|
1863
|
-
if (segment.type === "equal") {
|
|
1864
|
-
for (let i = 0; i < segment.text.length; i++) {
|
|
1865
|
-
charStates[docAOffset + i] = { type: "equal" };
|
|
1866
|
-
}
|
|
1867
|
-
docAOffset += segment.text.length;
|
|
1868
|
-
} else if (segment.type === "delete") {
|
|
1869
|
-
const nextSegment = segments[segIdx + 1];
|
|
1870
|
-
const isReplacement = nextSegment && nextSegment.type === "insert";
|
|
1871
|
-
const replacementId = isReplacement ? v4() : void 0;
|
|
1872
|
-
for (let i = 0; i < segment.text.length; i++) {
|
|
1873
|
-
charStates[docAOffset + i] = { type: "delete", replacementId };
|
|
1874
|
-
}
|
|
1875
|
-
docAOffset += segment.text.length;
|
|
1876
|
-
if (isReplacement && nextSegment) {
|
|
1877
|
-
insertions.push({
|
|
1878
|
-
afterOffset: docAOffset,
|
|
1879
|
-
text: nextSegment.text,
|
|
1880
|
-
replacementId,
|
|
1881
|
-
posB: nextSegment.posB
|
|
1882
|
-
// Capture docB position for mark lookup
|
|
1883
|
-
});
|
|
1884
|
-
segIdx++;
|
|
1885
|
-
}
|
|
1886
|
-
} else if (segment.type === "insert") {
|
|
1887
|
-
insertions.push({
|
|
1888
|
-
afterOffset: docAOffset,
|
|
1889
|
-
text: segment.text,
|
|
1890
|
-
posB: segment.posB
|
|
1891
|
-
// Capture docB position for mark lookup
|
|
1892
|
-
});
|
|
1893
|
-
}
|
|
1894
|
-
}
|
|
1895
|
-
const spansB = diffResult.spansB || [];
|
|
1896
|
-
function transformNode(node, nodeOffset, path) {
|
|
1897
|
-
if (node.type === "text" && node.text) {
|
|
1898
|
-
const text = node.text;
|
|
1899
|
-
const result = [];
|
|
1900
|
-
let i = 0;
|
|
1901
|
-
while (i < text.length) {
|
|
1902
|
-
const charOffset = nodeOffset + i;
|
|
1903
|
-
const charState = charStates[charOffset] || { type: "equal" };
|
|
1904
|
-
const insertionsHere = insertions.filter((ins) => ins.afterOffset === charOffset);
|
|
1905
|
-
for (const ins of insertionsHere) {
|
|
1906
|
-
const insertedNodes = createInsertedTextNodes(
|
|
1907
|
-
ins.text,
|
|
1908
|
-
ins.posB,
|
|
1909
|
-
spansB,
|
|
1910
|
-
author,
|
|
1911
|
-
ins.replacementId
|
|
1912
|
-
);
|
|
1913
|
-
result.push(...insertedNodes);
|
|
1914
|
-
}
|
|
1915
|
-
const currentFormatChange = getFormatChangeAt(nodeOffset + i);
|
|
1916
|
-
let j = i + 1;
|
|
1917
|
-
while (j < text.length) {
|
|
1918
|
-
const nextState = charStates[nodeOffset + j] || { type: "equal" };
|
|
1919
|
-
if (nextState.type !== charState.type) break;
|
|
1920
|
-
if (insertions.some((ins) => ins.afterOffset === nodeOffset + j)) break;
|
|
1921
|
-
const nextFormatChange = getFormatChangeAt(nodeOffset + j);
|
|
1922
|
-
if (currentFormatChange !== nextFormatChange) break;
|
|
1923
|
-
j++;
|
|
1924
|
-
}
|
|
1925
|
-
const chunk = text.substring(i, j);
|
|
1926
|
-
let marks = [...node.marks || []];
|
|
1927
|
-
if (charState.type === "delete") {
|
|
1928
|
-
marks.push(createTrackDeleteMark(author, charState.replacementId));
|
|
1929
|
-
} else if (charState.type === "equal") {
|
|
1930
|
-
if (currentFormatChange) {
|
|
1931
|
-
const trackFormatMark = createTrackFormatMark(
|
|
1932
|
-
currentFormatChange.before,
|
|
1933
|
-
currentFormatChange.after,
|
|
1934
|
-
author
|
|
1935
|
-
);
|
|
1936
|
-
const normalizedAfterMarks = normalizeMarksForRendering(currentFormatChange.after);
|
|
1937
|
-
marks = [...normalizedAfterMarks, trackFormatMark];
|
|
1938
|
-
}
|
|
1939
|
-
}
|
|
1940
|
-
result.push({
|
|
1941
|
-
type: "text",
|
|
1942
|
-
text: chunk,
|
|
1943
|
-
marks: marks.length > 0 ? marks : void 0
|
|
1944
|
-
});
|
|
1945
|
-
i = j;
|
|
1946
|
-
}
|
|
1947
|
-
const endOffset = nodeOffset + text.length;
|
|
1948
|
-
const endInsertions = insertions.filter((ins) => ins.afterOffset === endOffset);
|
|
1949
|
-
for (const ins of endInsertions) {
|
|
1950
|
-
const insertedNodes = createInsertedTextNodes(
|
|
1951
|
-
ins.text,
|
|
1952
|
-
ins.posB,
|
|
1953
|
-
spansB,
|
|
1954
|
-
author,
|
|
1955
|
-
ins.replacementId
|
|
1956
|
-
);
|
|
1957
|
-
result.push(...insertedNodes);
|
|
1958
|
-
}
|
|
1959
|
-
insertions = insertions.filter(
|
|
1960
|
-
(ins) => ins.afterOffset < nodeOffset || ins.afterOffset > endOffset
|
|
1961
|
-
);
|
|
1962
|
-
return { nodes: result, consumedLength: text.length };
|
|
1963
|
-
}
|
|
1964
|
-
if (node.content && Array.isArray(node.content)) {
|
|
1965
|
-
const newContent = [];
|
|
1966
|
-
let offset = nodeOffset;
|
|
1967
|
-
for (const child of node.content) {
|
|
1968
|
-
const { nodes, consumedLength } = transformNode(child, offset);
|
|
1969
|
-
newContent.push(...nodes);
|
|
1970
|
-
offset += consumedLength;
|
|
1971
|
-
}
|
|
1972
|
-
return {
|
|
1973
|
-
nodes: [{ ...node, content: newContent }],
|
|
1974
|
-
consumedLength: offset - nodeOffset
|
|
1975
|
-
};
|
|
1976
|
-
}
|
|
1977
|
-
return { nodes: [node], consumedLength: 0 };
|
|
1978
|
-
}
|
|
1979
|
-
if (merged.content && Array.isArray(merged.content)) {
|
|
1980
|
-
const newContent = [];
|
|
1981
|
-
let offset = 0;
|
|
1982
|
-
for (let i = 0; i < merged.content.length; i++) {
|
|
1983
|
-
const child = merged.content[i];
|
|
1984
|
-
const { nodes, consumedLength } = transformNode(child, offset);
|
|
1985
|
-
newContent.push(...nodes);
|
|
1986
|
-
offset += consumedLength;
|
|
1987
|
-
}
|
|
1988
|
-
merged.content = newContent;
|
|
1989
|
-
}
|
|
1990
|
-
if (insertions.length > 0) {
|
|
1991
|
-
for (const ins of insertions) {
|
|
1992
|
-
const insertedNodes = createInsertedTextNodes(
|
|
1993
|
-
ins.text,
|
|
1994
|
-
ins.posB,
|
|
1995
|
-
spansB,
|
|
1996
|
-
author,
|
|
1997
|
-
ins.replacementId
|
|
1998
|
-
);
|
|
1999
|
-
const insertNode = {
|
|
2000
|
-
type: "paragraph",
|
|
2001
|
-
content: [
|
|
2002
|
-
{
|
|
2003
|
-
type: "run",
|
|
2004
|
-
content: insertedNodes
|
|
2005
|
-
}
|
|
2006
|
-
]
|
|
2007
|
-
};
|
|
2008
|
-
if (!merged.content) merged.content = [];
|
|
2009
|
-
merged.content.push(insertNode);
|
|
2010
|
-
}
|
|
2011
|
-
}
|
|
2012
|
-
return merged;
|
|
2013
|
-
}
|
|
2014
1779
|
|
|
2015
1780
|
// src/services/attrComparer.ts
|
|
2016
1781
|
var KNOWN_DEFAULTS = {
|
|
@@ -2517,10 +2282,7 @@ function getImagePreview(node) {
|
|
|
2517
2282
|
return "(image)";
|
|
2518
2283
|
}
|
|
2519
2284
|
|
|
2520
|
-
// src/services/
|
|
2521
|
-
function cloneNode2(node) {
|
|
2522
|
-
return JSON.parse(JSON.stringify(node));
|
|
2523
|
-
}
|
|
2285
|
+
// src/services/blockLevelMerger.ts
|
|
2524
2286
|
function markAllTextAsInserted(node, sharedId, author) {
|
|
2525
2287
|
if (node.type === "text") {
|
|
2526
2288
|
const existingMarks = normalizeMarksForRendering(node.marks || []);
|
|
@@ -2537,7 +2299,7 @@ function markAllTextAsInserted(node, sharedId, author) {
|
|
|
2537
2299
|
)
|
|
2538
2300
|
};
|
|
2539
2301
|
}
|
|
2540
|
-
return
|
|
2302
|
+
return node;
|
|
2541
2303
|
}
|
|
2542
2304
|
function markAllTextAsDeleted(node, sharedId, author) {
|
|
2543
2305
|
if (node.type === "text") {
|
|
@@ -2555,7 +2317,10 @@ function markAllTextAsDeleted(node, sharedId, author) {
|
|
|
2555
2317
|
)
|
|
2556
2318
|
};
|
|
2557
2319
|
}
|
|
2558
|
-
return
|
|
2320
|
+
return node;
|
|
2321
|
+
}
|
|
2322
|
+
function cloneNode(node) {
|
|
2323
|
+
return JSON.parse(JSON.stringify(node));
|
|
2559
2324
|
}
|
|
2560
2325
|
function extractTextPreview(node, maxLength = 50) {
|
|
2561
2326
|
const texts = [];
|
|
@@ -2576,17 +2341,504 @@ function extractTextPreview(node, maxLength = 50) {
|
|
|
2576
2341
|
}
|
|
2577
2342
|
return text || "(empty)";
|
|
2578
2343
|
}
|
|
2579
|
-
function
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
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;
|
|
2351
|
+
for (const inserted of alignment.insertions) {
|
|
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,
|
|
2378
|
+
path: inserted.path,
|
|
2379
|
+
node: markAllTextAsInserted(cloneNode(node), sharedId, author)
|
|
2380
|
+
});
|
|
2381
|
+
infos.push({
|
|
2382
|
+
id: sharedId,
|
|
2383
|
+
type,
|
|
2384
|
+
nodeType: node.type,
|
|
2385
|
+
location,
|
|
2386
|
+
preview,
|
|
2387
|
+
author,
|
|
2388
|
+
date
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
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
|
|
2446
|
+
});
|
|
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
|
|
2481
|
+
});
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
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);
|
|
2499
|
+
}
|
|
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 };
|
|
2515
|
+
}
|
|
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;
|
|
2535
|
+
}
|
|
2536
|
+
function cloneNode2(node) {
|
|
2537
|
+
return JSON.parse(JSON.stringify(node));
|
|
2538
|
+
}
|
|
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
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
return result;
|
|
2553
|
+
}
|
|
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
|
+
}];
|
|
2563
|
+
}
|
|
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
|
+
});
|
|
2581
|
+
}
|
|
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
|
|
2590
|
+
});
|
|
2591
|
+
processedUpTo = span.relEnd;
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
if (processedUpTo < text.length) {
|
|
2595
|
+
result.push({
|
|
2596
|
+
type: "text",
|
|
2597
|
+
text: text.substring(processedUpTo),
|
|
2598
|
+
marks: [trackMark]
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
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
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
return null;
|
|
2615
|
+
}
|
|
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
|
|
2649
|
+
});
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
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 };
|
|
2735
|
+
}
|
|
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;
|
|
2746
|
+
}
|
|
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);
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
return merged;
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
// src/services/structuralMerger.ts
|
|
2773
|
+
function cloneNode3(node) {
|
|
2774
|
+
return JSON.parse(JSON.stringify(node));
|
|
2775
|
+
}
|
|
2776
|
+
function markAllTextAsInserted2(node, sharedId, author) {
|
|
2777
|
+
if (node.type === "text") {
|
|
2778
|
+
const existingMarks = normalizeMarksForRendering(node.marks || []);
|
|
2779
|
+
return {
|
|
2780
|
+
...node,
|
|
2781
|
+
marks: [...existingMarks, createTrackInsertMark(author, sharedId)]
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
if (node.content && Array.isArray(node.content)) {
|
|
2785
|
+
return {
|
|
2786
|
+
...node,
|
|
2787
|
+
content: node.content.map(
|
|
2788
|
+
(child) => markAllTextAsInserted2(child, sharedId, author)
|
|
2789
|
+
)
|
|
2790
|
+
};
|
|
2791
|
+
}
|
|
2792
|
+
return { ...node };
|
|
2793
|
+
}
|
|
2794
|
+
function markAllTextAsDeleted2(node, sharedId, author) {
|
|
2795
|
+
if (node.type === "text") {
|
|
2796
|
+
const existingMarks = normalizeMarksForRendering(node.marks || []);
|
|
2797
|
+
return {
|
|
2798
|
+
...node,
|
|
2799
|
+
marks: [...existingMarks, createTrackDeleteMark(author, sharedId)]
|
|
2800
|
+
};
|
|
2801
|
+
}
|
|
2802
|
+
if (node.content && Array.isArray(node.content)) {
|
|
2803
|
+
return {
|
|
2804
|
+
...node,
|
|
2805
|
+
content: node.content.map(
|
|
2806
|
+
(child) => markAllTextAsDeleted2(child, sharedId, author)
|
|
2807
|
+
)
|
|
2808
|
+
};
|
|
2809
|
+
}
|
|
2810
|
+
return { ...node };
|
|
2811
|
+
}
|
|
2812
|
+
function extractTextPreview2(node, maxLength = 50) {
|
|
2813
|
+
const texts = [];
|
|
2814
|
+
function extract(n) {
|
|
2815
|
+
if (n.type === "text") {
|
|
2816
|
+
texts.push(n.text || "");
|
|
2817
|
+
}
|
|
2818
|
+
if (n.content) {
|
|
2819
|
+
for (const child of n.content) {
|
|
2820
|
+
extract(child);
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
extract(node);
|
|
2825
|
+
const text = texts.join("").trim();
|
|
2826
|
+
if (text.length > maxLength) {
|
|
2827
|
+
return text.substring(0, maxLength - 3) + "...";
|
|
2828
|
+
}
|
|
2829
|
+
return text || "(empty)";
|
|
2830
|
+
}
|
|
2831
|
+
function getNodeTypeDescription(node) {
|
|
2832
|
+
if (isTable(node)) return "Table";
|
|
2833
|
+
if (isList(node)) return "List";
|
|
2834
|
+
if (isListItem(node)) return "List item";
|
|
2835
|
+
if (isTableRow(node)) return "Table row";
|
|
2836
|
+
if (isImage(node)) return "Image";
|
|
2837
|
+
if (node.type === "heading") return `Heading ${node.attrs?.level || 1}`;
|
|
2838
|
+
if (node.type === "paragraph") return "Paragraph";
|
|
2839
|
+
if (node.type === "blockquote") return "Blockquote";
|
|
2840
|
+
if (node.type === "codeBlock") return "Code block";
|
|
2841
|
+
return node.type || "Block";
|
|
2590
2842
|
}
|
|
2591
2843
|
function mergeWithStructuralAwareness(docA, docB, author = DEFAULT_AUTHOR) {
|
|
2592
2844
|
const structuralInfos = [];
|
|
@@ -2743,7 +2995,7 @@ function mergeMatchedBlock(nodeA, nodeB, blockIndex, author) {
|
|
|
2743
2995
|
diff,
|
|
2744
2996
|
author
|
|
2745
2997
|
);
|
|
2746
|
-
const mergedNode = merged.content?.[0] ||
|
|
2998
|
+
const mergedNode = merged.content?.[0] || cloneNode3(nodeB);
|
|
2747
2999
|
return { mergedNode, infos, changes };
|
|
2748
3000
|
}
|
|
2749
3001
|
function mergeMatchedTable(tableA, tableB, tableIndex, author) {
|
|
@@ -2772,13 +3024,13 @@ function mergeMatchedTable(tableA, tableB, tableIndex, author) {
|
|
|
2772
3024
|
if (deletedIndices.has(checkIdx) && !processedDeletions.has(checkIdx)) {
|
|
2773
3025
|
const deletedRow = rowsA[checkIdx];
|
|
2774
3026
|
const changeId = v4();
|
|
2775
|
-
mergedRows.push(
|
|
3027
|
+
mergedRows.push(markAllTextAsDeleted2(cloneNode3(deletedRow), changeId, author));
|
|
2776
3028
|
tableInfos.push({
|
|
2777
3029
|
id: changeId,
|
|
2778
3030
|
type: "rowDelete",
|
|
2779
3031
|
nodeType: "tableRow",
|
|
2780
3032
|
location: `Table ${tableIndex}, Row ${checkIdx + 1}`,
|
|
2781
|
-
preview:
|
|
3033
|
+
preview: extractTextPreview2(deletedRow),
|
|
2782
3034
|
author,
|
|
2783
3035
|
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2784
3036
|
});
|
|
@@ -2790,13 +3042,13 @@ function mergeMatchedTable(tableA, tableB, tableIndex, author) {
|
|
|
2790
3042
|
changeCount += changes;
|
|
2791
3043
|
} else {
|
|
2792
3044
|
const changeId = v4();
|
|
2793
|
-
mergedRows.push(
|
|
3045
|
+
mergedRows.push(markAllTextAsInserted2(cloneNode3(rowB), changeId, author));
|
|
2794
3046
|
tableInfos.push({
|
|
2795
3047
|
id: changeId,
|
|
2796
3048
|
type: "rowInsert",
|
|
2797
3049
|
nodeType: "tableRow",
|
|
2798
3050
|
location: `Table ${tableIndex}, Row ${idxB + 1}`,
|
|
2799
|
-
preview:
|
|
3051
|
+
preview: extractTextPreview2(rowB),
|
|
2800
3052
|
author,
|
|
2801
3053
|
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2802
3054
|
});
|
|
@@ -2806,13 +3058,13 @@ function mergeMatchedTable(tableA, tableB, tableIndex, author) {
|
|
|
2806
3058
|
const idxA = deletion.path[deletion.path.length - 1];
|
|
2807
3059
|
if (!processedDeletions.has(idxA)) {
|
|
2808
3060
|
const changeId = v4();
|
|
2809
|
-
mergedRows.push(
|
|
3061
|
+
mergedRows.push(markAllTextAsDeleted2(cloneNode3(deletion.node), changeId, author));
|
|
2810
3062
|
tableInfos.push({
|
|
2811
3063
|
id: changeId,
|
|
2812
3064
|
type: "rowDelete",
|
|
2813
3065
|
nodeType: "tableRow",
|
|
2814
3066
|
location: `Table ${tableIndex}, Row ${idxA + 1}`,
|
|
2815
|
-
preview:
|
|
3067
|
+
preview: extractTextPreview2(deletion.node),
|
|
2816
3068
|
author,
|
|
2817
3069
|
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2818
3070
|
});
|
|
@@ -2850,13 +3102,13 @@ function mergeMatchedList(listA, listB, listIndex, author) {
|
|
|
2850
3102
|
if (deletedIndices.has(checkIdx) && !processedDeletions.has(checkIdx)) {
|
|
2851
3103
|
const deletedItem = itemsA[checkIdx];
|
|
2852
3104
|
const changeId = v4();
|
|
2853
|
-
mergedItems.push(
|
|
3105
|
+
mergedItems.push(markAllTextAsDeleted2(cloneNode3(deletedItem), changeId, author));
|
|
2854
3106
|
listInfos.push({
|
|
2855
3107
|
id: changeId,
|
|
2856
3108
|
type: "listItemDelete",
|
|
2857
3109
|
nodeType: "listItem",
|
|
2858
3110
|
location: `List ${listIndex}, Item ${checkIdx + 1}`,
|
|
2859
|
-
preview:
|
|
3111
|
+
preview: extractTextPreview2(deletedItem),
|
|
2860
3112
|
author,
|
|
2861
3113
|
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2862
3114
|
});
|
|
@@ -2868,13 +3120,13 @@ function mergeMatchedList(listA, listB, listIndex, author) {
|
|
|
2868
3120
|
changeCount += changes;
|
|
2869
3121
|
} else {
|
|
2870
3122
|
const changeId = v4();
|
|
2871
|
-
mergedItems.push(
|
|
3123
|
+
mergedItems.push(markAllTextAsInserted2(cloneNode3(itemB), changeId, author));
|
|
2872
3124
|
listInfos.push({
|
|
2873
3125
|
id: changeId,
|
|
2874
3126
|
type: "listItemInsert",
|
|
2875
3127
|
nodeType: "listItem",
|
|
2876
3128
|
location: `List ${listIndex}, Item ${idxB + 1}`,
|
|
2877
|
-
preview:
|
|
3129
|
+
preview: extractTextPreview2(itemB),
|
|
2878
3130
|
author,
|
|
2879
3131
|
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2880
3132
|
});
|
|
@@ -2884,13 +3136,13 @@ function mergeMatchedList(listA, listB, listIndex, author) {
|
|
|
2884
3136
|
const idxA = deletion.path[deletion.path.length - 1];
|
|
2885
3137
|
if (!processedDeletions.has(idxA)) {
|
|
2886
3138
|
const changeId = v4();
|
|
2887
|
-
mergedItems.push(
|
|
3139
|
+
mergedItems.push(markAllTextAsDeleted2(cloneNode3(deletion.node), changeId, author));
|
|
2888
3140
|
listInfos.push({
|
|
2889
3141
|
id: changeId,
|
|
2890
3142
|
type: "listItemDelete",
|
|
2891
3143
|
nodeType: "listItem",
|
|
2892
3144
|
location: `List ${listIndex}, Item ${idxA + 1}`,
|
|
2893
|
-
preview:
|
|
3145
|
+
preview: extractTextPreview2(deletion.node),
|
|
2894
3146
|
author,
|
|
2895
3147
|
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2896
3148
|
});
|
|
@@ -2904,14 +3156,14 @@ function mergeMatchedList(listA, listB, listIndex, author) {
|
|
|
2904
3156
|
}
|
|
2905
3157
|
function createInsertedBlock(node, blockIndex, author) {
|
|
2906
3158
|
const changeId = v4();
|
|
2907
|
-
const markedNode =
|
|
3159
|
+
const markedNode = markAllTextAsInserted2(cloneNode3(node), changeId, author);
|
|
2908
3160
|
const nodeDesc = getNodeTypeDescription(node);
|
|
2909
3161
|
const info = {
|
|
2910
3162
|
id: changeId,
|
|
2911
3163
|
type: isTable(node) ? "rowInsert" : isList(node) ? "listItemInsert" : isImage(node) ? "imageInsert" : "paragraphInsert",
|
|
2912
3164
|
nodeType: node.type || "unknown",
|
|
2913
3165
|
location: `${nodeDesc} inserted at position ${blockIndex}`,
|
|
2914
|
-
preview:
|
|
3166
|
+
preview: extractTextPreview2(node),
|
|
2915
3167
|
author,
|
|
2916
3168
|
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2917
3169
|
};
|
|
@@ -2919,14 +3171,14 @@ function createInsertedBlock(node, blockIndex, author) {
|
|
|
2919
3171
|
}
|
|
2920
3172
|
function createDeletedBlock(node, blockIndex, author) {
|
|
2921
3173
|
const changeId = v4();
|
|
2922
|
-
const markedNode =
|
|
3174
|
+
const markedNode = markAllTextAsDeleted2(cloneNode3(node), changeId, author);
|
|
2923
3175
|
const nodeDesc = getNodeTypeDescription(node);
|
|
2924
3176
|
const info = {
|
|
2925
3177
|
id: changeId,
|
|
2926
3178
|
type: isTable(node) ? "rowDelete" : isList(node) ? "listItemDelete" : isImage(node) ? "imageDelete" : "paragraphDelete",
|
|
2927
3179
|
nodeType: node.type || "unknown",
|
|
2928
3180
|
location: `${nodeDesc} deleted from position ${blockIndex}`,
|
|
2929
|
-
preview:
|
|
3181
|
+
preview: extractTextPreview2(node),
|
|
2930
3182
|
author,
|
|
2931
3183
|
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2932
3184
|
};
|
|
@@ -2985,8 +3237,9 @@ var DocxDiffEditor = forwardRef(
|
|
|
2985
3237
|
const mountedRef = useRef(true);
|
|
2986
3238
|
const initRef = useRef(false);
|
|
2987
3239
|
const readyRef = useRef(false);
|
|
3240
|
+
const rollbackJsonRef = useRef(null);
|
|
2988
3241
|
const [isLoading, setIsLoading] = useState(true);
|
|
2989
|
-
const [
|
|
3242
|
+
const [fatalError, setFatalError] = useState(null);
|
|
2990
3243
|
const [sourceJson, setSourceJson] = useState(null);
|
|
2991
3244
|
const [mergedJson, setMergedJson] = useState(null);
|
|
2992
3245
|
const [diffResult, setDiffResult] = useState(null);
|
|
@@ -3043,11 +3296,31 @@ var DocxDiffEditor = forwardRef(
|
|
|
3043
3296
|
sd.setTrackedChangesPreferences({ mode: "off", enabled: false });
|
|
3044
3297
|
}
|
|
3045
3298
|
}, []);
|
|
3046
|
-
const
|
|
3047
|
-
(err) => {
|
|
3048
|
-
const
|
|
3049
|
-
|
|
3050
|
-
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
|
+
});
|
|
3051
3324
|
},
|
|
3052
3325
|
[onError]
|
|
3053
3326
|
);
|
|
@@ -3205,7 +3478,7 @@ var DocxDiffEditor = forwardRef(
|
|
|
3205
3478
|
return;
|
|
3206
3479
|
}
|
|
3207
3480
|
setIsLoading(true);
|
|
3208
|
-
|
|
3481
|
+
setFatalError(null);
|
|
3209
3482
|
destroySuperdoc();
|
|
3210
3483
|
try {
|
|
3211
3484
|
const { SuperDoc } = await import('superdoc');
|
|
@@ -3228,17 +3501,19 @@ var DocxDiffEditor = forwardRef(
|
|
|
3228
3501
|
if (sd?.activeEditor && isProseMirrorJSON(initialSource)) {
|
|
3229
3502
|
setEditorContent(sd.activeEditor, initialSource);
|
|
3230
3503
|
setSourceJson(initialSource);
|
|
3504
|
+
rollbackJsonRef.current = initialSource;
|
|
3231
3505
|
onSourceLoaded?.(initialSource);
|
|
3232
3506
|
}
|
|
3233
3507
|
} else {
|
|
3234
3508
|
setSourceJson(json);
|
|
3509
|
+
rollbackJsonRef.current = json;
|
|
3235
3510
|
onSourceLoaded?.(json);
|
|
3236
3511
|
}
|
|
3237
3512
|
setIsLoading(false);
|
|
3238
3513
|
onReady?.();
|
|
3239
3514
|
} catch (err) {
|
|
3240
3515
|
console.error("Failed to initialize SuperDoc:", err);
|
|
3241
|
-
|
|
3516
|
+
handleFatalError(err instanceof Error ? err : new Error("Failed to load editor"), "init");
|
|
3242
3517
|
setIsLoading(false);
|
|
3243
3518
|
initRef.current = false;
|
|
3244
3519
|
}
|
|
@@ -3252,7 +3527,7 @@ var DocxDiffEditor = forwardRef(
|
|
|
3252
3527
|
destroySuperdoc,
|
|
3253
3528
|
createSuperdoc,
|
|
3254
3529
|
setEditorContent,
|
|
3255
|
-
|
|
3530
|
+
handleFatalError
|
|
3256
3531
|
]);
|
|
3257
3532
|
useEffect(() => {
|
|
3258
3533
|
mountedRef.current = true;
|
|
@@ -3287,42 +3562,98 @@ var DocxDiffEditor = forwardRef(
|
|
|
3287
3562
|
* Accepts File (DOCX), HTML string, or ProseMirror JSON.
|
|
3288
3563
|
* Note: This destroys and recreates the SuperDoc instance.
|
|
3289
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.
|
|
3290
3568
|
*/
|
|
3291
3569
|
async setSource(content) {
|
|
3292
3570
|
if (!SuperDocRef.current) {
|
|
3293
|
-
|
|
3571
|
+
const error = new Error("Editor not initialized");
|
|
3572
|
+
handleOperationError(error, "setSource");
|
|
3573
|
+
return { success: false, error, message: error.message };
|
|
3294
3574
|
}
|
|
3575
|
+
const currentRollback = superdocRef.current?.activeEditor ? superdocRef.current.activeEditor.getJSON() : rollbackJsonRef.current;
|
|
3295
3576
|
setIsLoading(true);
|
|
3296
|
-
|
|
3577
|
+
setFatalError(null);
|
|
3297
3578
|
try {
|
|
3298
3579
|
const contentType = detectContentType(content);
|
|
3299
|
-
let
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
const result = await createSuperdoc({ html: content });
|
|
3306
|
-
json = result.json;
|
|
3307
|
-
} else {
|
|
3308
|
-
const result = await createSuperdoc(templateDocx ? { document: templateDocx } : {});
|
|
3309
|
-
if (result.superdoc?.activeEditor && isProseMirrorJSON(content)) {
|
|
3310
|
-
setEditorContent(result.superdoc.activeEditor, content);
|
|
3311
|
-
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);
|
|
3312
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 });
|
|
3313
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 };
|
|
3314
3642
|
}
|
|
3315
3643
|
}
|
|
3316
3644
|
setSourceJson(json);
|
|
3317
3645
|
setMergedJson(null);
|
|
3318
3646
|
setDiffResult(null);
|
|
3319
3647
|
setEditingMode(superdocRef.current);
|
|
3648
|
+
rollbackJsonRef.current = json;
|
|
3320
3649
|
onSourceLoaded?.(json);
|
|
3650
|
+
setIsLoading(false);
|
|
3651
|
+
return;
|
|
3321
3652
|
} catch (err) {
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
} finally {
|
|
3653
|
+
const error = err instanceof Error ? err : new Error("Failed to set source");
|
|
3654
|
+
handleFatalError(error, "setSource");
|
|
3325
3655
|
setIsLoading(false);
|
|
3656
|
+
return { success: false, error, message: error.message };
|
|
3326
3657
|
}
|
|
3327
3658
|
},
|
|
3328
3659
|
/**
|
|
@@ -3334,21 +3665,27 @@ var DocxDiffEditor = forwardRef(
|
|
|
3334
3665
|
*
|
|
3335
3666
|
* To compare against the original source document, call setSource() again
|
|
3336
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
|
|
3337
3672
|
*/
|
|
3338
3673
|
async compareWith(content) {
|
|
3339
3674
|
if (!SuperDocRef.current) {
|
|
3340
|
-
|
|
3675
|
+
const error = new Error("Editor not initialized");
|
|
3676
|
+
handleOperationError(error, "compareWith", "parsing");
|
|
3677
|
+
return { success: false, error, message: error.message, phase: "parsing" };
|
|
3341
3678
|
}
|
|
3342
3679
|
if (!superdocRef.current?.activeEditor) {
|
|
3343
|
-
|
|
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" };
|
|
3344
3683
|
}
|
|
3684
|
+
const rollbackJson = superdocRef.current.activeEditor.getJSON();
|
|
3345
3685
|
setIsLoading(true);
|
|
3686
|
+
let newJson;
|
|
3687
|
+
const contentType = detectContentType(content);
|
|
3346
3688
|
try {
|
|
3347
|
-
const currentEditorJson = superdocRef.current.activeEditor.getJSON();
|
|
3348
|
-
const cleanBaseline = acceptAllChangesInJson(currentEditorJson) || { type: "doc", content: [] };
|
|
3349
|
-
setSourceJson(cleanBaseline);
|
|
3350
|
-
const contentType = detectContentType(content);
|
|
3351
|
-
let newJson;
|
|
3352
3689
|
if (contentType === "file") {
|
|
3353
3690
|
newJson = await parseDocxFile(content, SuperDocRef.current);
|
|
3354
3691
|
} else if (contentType === "html") {
|
|
@@ -3403,91 +3740,122 @@ var DocxDiffEditor = forwardRef(
|
|
|
3403
3740
|
}
|
|
3404
3741
|
newJson = content;
|
|
3405
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: [] };
|
|
3406
3757
|
const cleanNewJson = acceptAllChangesInJson(newJson) || { type: "doc", content: [] };
|
|
3407
|
-
|
|
3758
|
+
normalizedNewJson = normalizeRunProperties(cleanNewJson);
|
|
3408
3759
|
const structuralResult = mergeWithStructuralAwareness(
|
|
3409
3760
|
cleanBaseline,
|
|
3410
3761
|
normalizedNewJson,
|
|
3411
3762
|
author
|
|
3412
3763
|
);
|
|
3413
3764
|
const merged = structuralResult.mergedDoc;
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
editor: sd.activeEditor,
|
|
3431
|
-
comments: [],
|
|
3432
|
-
// Empty array - we just want to trigger createCommentForTrackChanges
|
|
3433
|
-
documentId: sd.activeEditor?.options?.documentId || "primary"
|
|
3434
|
-
});
|
|
3435
|
-
} catch (err) {
|
|
3436
|
-
console.warn("[DocxDiffEditor] Failed to process track changes for bubbles:", err);
|
|
3437
|
-
}
|
|
3438
|
-
}, 50);
|
|
3439
|
-
}
|
|
3440
|
-
} catch (contentErr) {
|
|
3441
|
-
console.warn(
|
|
3442
|
-
"[DocxDiffEditor] Failed to apply merged content with track changes. Falling back to direct content update without track bubbles.",
|
|
3443
|
-
contentErr
|
|
3444
|
-
);
|
|
3445
|
-
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(() => {
|
|
3446
3781
|
try {
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
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);
|
|
3452
3790
|
}
|
|
3453
|
-
}
|
|
3454
|
-
}
|
|
3455
|
-
if (!usedFallback) {
|
|
3456
|
-
setStructuralChanges(structInfos);
|
|
3457
|
-
setIsPaneDismissed(false);
|
|
3458
|
-
} else {
|
|
3459
|
-
setStructuralChanges([]);
|
|
3791
|
+
}, 50);
|
|
3460
3792
|
}
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
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
|
+
}
|
|
3468
3818
|
}
|
|
3469
|
-
|
|
3470
|
-
|
|
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);
|
|
3471
3839
|
}
|
|
3472
|
-
const result = {
|
|
3473
|
-
totalChanges: insertions + deletions + formatChanges + structuralChangeCount,
|
|
3474
|
-
insertions,
|
|
3475
|
-
deletions,
|
|
3476
|
-
formatChanges,
|
|
3477
|
-
structuralChanges: structuralChangeCount,
|
|
3478
|
-
summary: combinedSummary,
|
|
3479
|
-
mergedJson: usedFallback ? normalizedNewJson : merged,
|
|
3480
|
-
structuralChangeInfos: usedFallback ? [] : structInfos,
|
|
3481
|
-
usedFallback
|
|
3482
|
-
};
|
|
3483
|
-
onComparisonComplete?.(result);
|
|
3484
|
-
return result;
|
|
3485
|
-
} catch (err) {
|
|
3486
|
-
handleError(err instanceof Error ? err : new Error("Comparison failed"));
|
|
3487
|
-
throw err;
|
|
3488
|
-
} finally {
|
|
3489
|
-
setIsLoading(false);
|
|
3490
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;
|
|
3491
3859
|
},
|
|
3492
3860
|
/**
|
|
3493
3861
|
* Get raw diff segments
|
|
@@ -3815,7 +4183,8 @@ var DocxDiffEditor = forwardRef(
|
|
|
3815
4183
|
setEditingMode,
|
|
3816
4184
|
onSourceLoaded,
|
|
3817
4185
|
onComparisonComplete,
|
|
3818
|
-
|
|
4186
|
+
handleFatalError,
|
|
4187
|
+
handleOperationError
|
|
3819
4188
|
]
|
|
3820
4189
|
);
|
|
3821
4190
|
return /* @__PURE__ */ jsxs("div", { className: `dde-container ${className}`.trim(), children: [
|
|
@@ -3823,7 +4192,7 @@ var DocxDiffEditor = forwardRef(
|
|
|
3823
4192
|
/* @__PURE__ */ jsx("div", { className: "dde-loading__spinner" }),
|
|
3824
4193
|
/* @__PURE__ */ jsx("p", { className: "dde-loading__text", children: "Loading document..." })
|
|
3825
4194
|
] }),
|
|
3826
|
-
|
|
4195
|
+
fatalError && /* @__PURE__ */ jsxs("div", { className: "dde-error", children: [
|
|
3827
4196
|
/* @__PURE__ */ jsx("div", { className: "dde-error__icon", children: /* @__PURE__ */ jsx(
|
|
3828
4197
|
"svg",
|
|
3829
4198
|
{
|
|
@@ -3843,7 +4212,7 @@ var DocxDiffEditor = forwardRef(
|
|
|
3843
4212
|
}
|
|
3844
4213
|
) }),
|
|
3845
4214
|
/* @__PURE__ */ jsx("p", { className: "dde-error__title", children: "Failed to load document" }),
|
|
3846
|
-
/* @__PURE__ */ jsx("p", { className: "dde-error__message", children:
|
|
4215
|
+
/* @__PURE__ */ jsx("p", { className: "dde-error__message", children: fatalError })
|
|
3847
4216
|
] }),
|
|
3848
4217
|
showToolbar && /* @__PURE__ */ jsx(
|
|
3849
4218
|
"div",
|
|
@@ -3880,256 +4249,6 @@ var DocxDiffEditor_default = DocxDiffEditor;
|
|
|
3880
4249
|
|
|
3881
4250
|
// src/services/index.ts
|
|
3882
4251
|
init_nodeFingerprint();
|
|
3883
|
-
function markAllTextAsInserted2(node, sharedId, author) {
|
|
3884
|
-
if (node.type === "text") {
|
|
3885
|
-
const existingMarks = normalizeMarksForRendering(node.marks || []);
|
|
3886
|
-
return {
|
|
3887
|
-
...node,
|
|
3888
|
-
marks: [...existingMarks, createTrackInsertMark(author, sharedId)]
|
|
3889
|
-
};
|
|
3890
|
-
}
|
|
3891
|
-
if (node.content && Array.isArray(node.content)) {
|
|
3892
|
-
return {
|
|
3893
|
-
...node,
|
|
3894
|
-
content: node.content.map(
|
|
3895
|
-
(child) => markAllTextAsInserted2(child, sharedId, author)
|
|
3896
|
-
)
|
|
3897
|
-
};
|
|
3898
|
-
}
|
|
3899
|
-
return node;
|
|
3900
|
-
}
|
|
3901
|
-
function markAllTextAsDeleted2(node, sharedId, author) {
|
|
3902
|
-
if (node.type === "text") {
|
|
3903
|
-
const existingMarks = normalizeMarksForRendering(node.marks || []);
|
|
3904
|
-
return {
|
|
3905
|
-
...node,
|
|
3906
|
-
marks: [...existingMarks, createTrackDeleteMark(author, sharedId)]
|
|
3907
|
-
};
|
|
3908
|
-
}
|
|
3909
|
-
if (node.content && Array.isArray(node.content)) {
|
|
3910
|
-
return {
|
|
3911
|
-
...node,
|
|
3912
|
-
content: node.content.map(
|
|
3913
|
-
(child) => markAllTextAsDeleted2(child, sharedId, author)
|
|
3914
|
-
)
|
|
3915
|
-
};
|
|
3916
|
-
}
|
|
3917
|
-
return node;
|
|
3918
|
-
}
|
|
3919
|
-
function cloneNode3(node) {
|
|
3920
|
-
return JSON.parse(JSON.stringify(node));
|
|
3921
|
-
}
|
|
3922
|
-
function extractTextPreview2(node, maxLength = 50) {
|
|
3923
|
-
const texts = [];
|
|
3924
|
-
function extract(n) {
|
|
3925
|
-
if (n.type === "text") {
|
|
3926
|
-
texts.push(n.text || "");
|
|
3927
|
-
}
|
|
3928
|
-
if (n.content) {
|
|
3929
|
-
for (const child of n.content) {
|
|
3930
|
-
extract(child);
|
|
3931
|
-
}
|
|
3932
|
-
}
|
|
3933
|
-
}
|
|
3934
|
-
extract(node);
|
|
3935
|
-
const text = texts.join("").trim();
|
|
3936
|
-
if (text.length > maxLength) {
|
|
3937
|
-
return text.substring(0, maxLength - 3) + "...";
|
|
3938
|
-
}
|
|
3939
|
-
return text || "(empty)";
|
|
3940
|
-
}
|
|
3941
|
-
function processStructuralChanges(docA, docB, author = DEFAULT_AUTHOR) {
|
|
3942
|
-
const changes = [];
|
|
3943
|
-
const infos = [];
|
|
3944
|
-
const alignment = alignDocuments(docA, docB);
|
|
3945
|
-
let tableIndex = 0;
|
|
3946
|
-
let listIndex = 0;
|
|
3947
|
-
let paragraphIndex = 0;
|
|
3948
|
-
for (const inserted of alignment.insertions) {
|
|
3949
|
-
const node = inserted.node;
|
|
3950
|
-
const sharedId = v4();
|
|
3951
|
-
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
3952
|
-
let type = "paragraphInsert";
|
|
3953
|
-
let location = "";
|
|
3954
|
-
let preview = "";
|
|
3955
|
-
if (isTable(node)) {
|
|
3956
|
-
type = "rowInsert";
|
|
3957
|
-
location = `New table at position ${inserted.path[0] + 1}`;
|
|
3958
|
-
preview = `Table with ${node.content?.length || 0} rows`;
|
|
3959
|
-
tableIndex++;
|
|
3960
|
-
} else if (isList(node)) {
|
|
3961
|
-
type = "listItemInsert";
|
|
3962
|
-
location = `New list at position ${inserted.path[0] + 1}`;
|
|
3963
|
-
preview = `List with ${node.content?.length || 0} items`;
|
|
3964
|
-
listIndex++;
|
|
3965
|
-
} else {
|
|
3966
|
-
type = "paragraphInsert";
|
|
3967
|
-
paragraphIndex++;
|
|
3968
|
-
location = `Paragraph ${paragraphIndex}`;
|
|
3969
|
-
preview = extractTextPreview2(node);
|
|
3970
|
-
}
|
|
3971
|
-
changes.push({
|
|
3972
|
-
id: sharedId,
|
|
3973
|
-
type,
|
|
3974
|
-
nodeType: node.type,
|
|
3975
|
-
path: inserted.path,
|
|
3976
|
-
node: markAllTextAsInserted2(cloneNode3(node), sharedId, author)
|
|
3977
|
-
});
|
|
3978
|
-
infos.push({
|
|
3979
|
-
id: sharedId,
|
|
3980
|
-
type,
|
|
3981
|
-
nodeType: node.type,
|
|
3982
|
-
location,
|
|
3983
|
-
preview,
|
|
3984
|
-
author,
|
|
3985
|
-
date
|
|
3986
|
-
});
|
|
3987
|
-
}
|
|
3988
|
-
for (const deleted of alignment.deletions) {
|
|
3989
|
-
const node = deleted.node;
|
|
3990
|
-
const sharedId = v4();
|
|
3991
|
-
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
3992
|
-
let type = "paragraphDelete";
|
|
3993
|
-
let location = "";
|
|
3994
|
-
let preview = "";
|
|
3995
|
-
if (isTable(node)) {
|
|
3996
|
-
type = "rowDelete";
|
|
3997
|
-
location = `Deleted table at position ${deleted.path[0] + 1}`;
|
|
3998
|
-
preview = `Table with ${node.content?.length || 0} rows`;
|
|
3999
|
-
} else if (isList(node)) {
|
|
4000
|
-
type = "listItemDelete";
|
|
4001
|
-
location = `Deleted list at position ${deleted.path[0] + 1}`;
|
|
4002
|
-
preview = `List with ${node.content?.length || 0} items`;
|
|
4003
|
-
} else {
|
|
4004
|
-
type = "paragraphDelete";
|
|
4005
|
-
location = `Deleted paragraph`;
|
|
4006
|
-
preview = extractTextPreview2(node);
|
|
4007
|
-
}
|
|
4008
|
-
changes.push({
|
|
4009
|
-
id: sharedId,
|
|
4010
|
-
type,
|
|
4011
|
-
nodeType: node.type,
|
|
4012
|
-
path: deleted.path,
|
|
4013
|
-
node: markAllTextAsDeleted2(cloneNode3(node), sharedId, author)
|
|
4014
|
-
});
|
|
4015
|
-
infos.push({
|
|
4016
|
-
id: sharedId,
|
|
4017
|
-
type,
|
|
4018
|
-
nodeType: node.type,
|
|
4019
|
-
location,
|
|
4020
|
-
preview,
|
|
4021
|
-
author,
|
|
4022
|
-
date
|
|
4023
|
-
});
|
|
4024
|
-
}
|
|
4025
|
-
for (const match of alignment.matched) {
|
|
4026
|
-
const nodeA = docA.content?.[match.pathA[0]];
|
|
4027
|
-
const nodeB = docB.content?.[match.pathB[0]];
|
|
4028
|
-
if (!nodeA || !nodeB) continue;
|
|
4029
|
-
if (isTable(nodeA) && isTable(nodeB)) {
|
|
4030
|
-
tableIndex++;
|
|
4031
|
-
const tableResult = diffTables(nodeA, nodeB, match.pathA, match.pathB);
|
|
4032
|
-
for (const rowChange of tableResult.rowChanges) {
|
|
4033
|
-
const sharedId = rowChange.id;
|
|
4034
|
-
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
4035
|
-
const rowIndex = rowChange.path[rowChange.path.length - 1];
|
|
4036
|
-
const isInsert = rowChange.type === "rowInsert";
|
|
4037
|
-
const location = getRowLocation(rowChange.path, rowIndex, tableIndex - 1);
|
|
4038
|
-
const preview = getRowPreview(rowChange.node);
|
|
4039
|
-
const markedNode = isInsert ? markAllTextAsInserted2(cloneNode3(rowChange.node), sharedId, author) : markAllTextAsDeleted2(cloneNode3(rowChange.node), sharedId, author);
|
|
4040
|
-
changes.push({
|
|
4041
|
-
...rowChange,
|
|
4042
|
-
node: markedNode
|
|
4043
|
-
});
|
|
4044
|
-
infos.push({
|
|
4045
|
-
id: sharedId,
|
|
4046
|
-
type: rowChange.type,
|
|
4047
|
-
nodeType: "tableRow",
|
|
4048
|
-
location,
|
|
4049
|
-
preview,
|
|
4050
|
-
author,
|
|
4051
|
-
date
|
|
4052
|
-
});
|
|
4053
|
-
}
|
|
4054
|
-
}
|
|
4055
|
-
if (isList(nodeA) && isList(nodeB)) {
|
|
4056
|
-
listIndex++;
|
|
4057
|
-
const listResult = diffLists(nodeA, nodeB, match.pathA, match.pathB);
|
|
4058
|
-
for (const itemChange of listResult.itemChanges) {
|
|
4059
|
-
const sharedId = itemChange.id;
|
|
4060
|
-
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
4061
|
-
const itemIndex = itemChange.path[itemChange.path.length - 1];
|
|
4062
|
-
const isInsert = itemChange.type === "listItemInsert";
|
|
4063
|
-
const location = getListItemLocation(itemChange.path, itemIndex, listIndex - 1);
|
|
4064
|
-
const preview = getListItemPreview(itemChange.node);
|
|
4065
|
-
const markedNode = isInsert ? markAllTextAsInserted2(cloneNode3(itemChange.node), sharedId, author) : markAllTextAsDeleted2(cloneNode3(itemChange.node), sharedId, author);
|
|
4066
|
-
changes.push({
|
|
4067
|
-
...itemChange,
|
|
4068
|
-
node: markedNode
|
|
4069
|
-
});
|
|
4070
|
-
infos.push({
|
|
4071
|
-
id: sharedId,
|
|
4072
|
-
type: itemChange.type,
|
|
4073
|
-
nodeType: "listItem",
|
|
4074
|
-
location,
|
|
4075
|
-
preview,
|
|
4076
|
-
author,
|
|
4077
|
-
date
|
|
4078
|
-
});
|
|
4079
|
-
}
|
|
4080
|
-
}
|
|
4081
|
-
}
|
|
4082
|
-
const imageChanges = diffImages(docA, docB);
|
|
4083
|
-
for (const imgInsert of imageChanges.inserted) {
|
|
4084
|
-
const sharedId = imgInsert.id;
|
|
4085
|
-
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
4086
|
-
infos.push({
|
|
4087
|
-
id: sharedId,
|
|
4088
|
-
type: "imageInsert",
|
|
4089
|
-
nodeType: "image",
|
|
4090
|
-
location: getImageLocation(imgInsert.path),
|
|
4091
|
-
preview: getImagePreview(imgInsert.node),
|
|
4092
|
-
author,
|
|
4093
|
-
date
|
|
4094
|
-
});
|
|
4095
|
-
changes.push(imgInsert);
|
|
4096
|
-
}
|
|
4097
|
-
for (const imgDelete of imageChanges.deleted) {
|
|
4098
|
-
const sharedId = imgDelete.id;
|
|
4099
|
-
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
4100
|
-
infos.push({
|
|
4101
|
-
id: sharedId,
|
|
4102
|
-
type: "imageDelete",
|
|
4103
|
-
nodeType: "image",
|
|
4104
|
-
location: getImageLocation(imgDelete.path),
|
|
4105
|
-
preview: getImagePreview(imgDelete.node),
|
|
4106
|
-
author,
|
|
4107
|
-
date
|
|
4108
|
-
});
|
|
4109
|
-
changes.push(imgDelete);
|
|
4110
|
-
}
|
|
4111
|
-
return { changes, infos };
|
|
4112
|
-
}
|
|
4113
|
-
function generateStructuralChangeSummary(infos) {
|
|
4114
|
-
const summary = [];
|
|
4115
|
-
const rowInserts = infos.filter((i) => i.type === "rowInsert").length;
|
|
4116
|
-
const rowDeletes = infos.filter((i) => i.type === "rowDelete").length;
|
|
4117
|
-
const paragraphInserts = infos.filter((i) => i.type === "paragraphInsert").length;
|
|
4118
|
-
const paragraphDeletes = infos.filter((i) => i.type === "paragraphDelete").length;
|
|
4119
|
-
const listItemInserts = infos.filter((i) => i.type === "listItemInsert").length;
|
|
4120
|
-
const listItemDeletes = infos.filter((i) => i.type === "listItemDelete").length;
|
|
4121
|
-
const imageInserts = infos.filter((i) => i.type === "imageInsert").length;
|
|
4122
|
-
const imageDeletes = infos.filter((i) => i.type === "imageDelete").length;
|
|
4123
|
-
if (rowInserts > 0) summary.push(`${rowInserts} row(s) inserted`);
|
|
4124
|
-
if (rowDeletes > 0) summary.push(`${rowDeletes} row(s) deleted`);
|
|
4125
|
-
if (paragraphInserts > 0) summary.push(`${paragraphInserts} paragraph(s) inserted`);
|
|
4126
|
-
if (paragraphDeletes > 0) summary.push(`${paragraphDeletes} paragraph(s) deleted`);
|
|
4127
|
-
if (listItemInserts > 0) summary.push(`${listItemInserts} list item(s) inserted`);
|
|
4128
|
-
if (listItemDeletes > 0) summary.push(`${listItemDeletes} list item(s) deleted`);
|
|
4129
|
-
if (imageInserts > 0) summary.push(`${imageInserts} image(s) inserted`);
|
|
4130
|
-
if (imageDeletes > 0) summary.push(`${imageDeletes} image(s) deleted`);
|
|
4131
|
-
return summary;
|
|
4132
|
-
}
|
|
4133
4252
|
|
|
4134
4253
|
// src/blankTemplate.ts
|
|
4135
4254
|
var BLANK_DOCX_BASE64 = `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`;
|