docx-diff-editor 1.0.42 → 1.0.44
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 +35 -3
- package/dist/index.d.mts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +849 -412
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +849 -412
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +3 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -495,8 +495,10 @@ async function parseHtmlToJson(html, SuperDoc) {
|
|
|
495
495
|
try {
|
|
496
496
|
superdoc = new SuperDoc({
|
|
497
497
|
selector: container,
|
|
498
|
-
html,
|
|
499
|
-
|
|
498
|
+
html: "<p></p>",
|
|
499
|
+
// Minimal empty document
|
|
500
|
+
documentMode: "editing",
|
|
501
|
+
// Need editing mode to use paste
|
|
500
502
|
rulers: false,
|
|
501
503
|
user: { name: "Parser", email: "parser@local" },
|
|
502
504
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -507,10 +509,26 @@ async function parseHtmlToJson(html, SuperDoc) {
|
|
|
507
509
|
if (!editor) {
|
|
508
510
|
throw new Error("No active editor found");
|
|
509
511
|
}
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
512
|
+
const view = editor.view;
|
|
513
|
+
if (!view) {
|
|
514
|
+
throw new Error("No editor view found");
|
|
515
|
+
}
|
|
516
|
+
editor.commands.selectAll();
|
|
517
|
+
editor.commands.deleteSelection();
|
|
518
|
+
view.pasteHTML(html);
|
|
519
|
+
setTimeout(() => {
|
|
520
|
+
if (resolved) return;
|
|
521
|
+
try {
|
|
522
|
+
const json = editor.getJSON();
|
|
523
|
+
resolved = true;
|
|
524
|
+
cleanup();
|
|
525
|
+
resolve(json);
|
|
526
|
+
} catch (err) {
|
|
527
|
+
resolved = true;
|
|
528
|
+
cleanup();
|
|
529
|
+
reject(err);
|
|
530
|
+
}
|
|
531
|
+
}, 50);
|
|
514
532
|
} catch (err) {
|
|
515
533
|
resolved = true;
|
|
516
534
|
cleanup();
|
|
@@ -781,204 +799,6 @@ function diffDocuments(docA, docB) {
|
|
|
781
799
|
summary
|
|
782
800
|
};
|
|
783
801
|
}
|
|
784
|
-
function createTrackInsertMark(author = DEFAULT_AUTHOR, id) {
|
|
785
|
-
return {
|
|
786
|
-
type: "trackInsert",
|
|
787
|
-
attrs: {
|
|
788
|
-
id: id ?? uuid.v4(),
|
|
789
|
-
author: author.name,
|
|
790
|
-
authorEmail: author.email,
|
|
791
|
-
authorImage: "",
|
|
792
|
-
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
793
|
-
}
|
|
794
|
-
};
|
|
795
|
-
}
|
|
796
|
-
function createTrackDeleteMark(author = DEFAULT_AUTHOR, id) {
|
|
797
|
-
return {
|
|
798
|
-
type: "trackDelete",
|
|
799
|
-
attrs: {
|
|
800
|
-
id: id ?? uuid.v4(),
|
|
801
|
-
author: author.name,
|
|
802
|
-
authorEmail: author.email,
|
|
803
|
-
authorImage: "",
|
|
804
|
-
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
805
|
-
}
|
|
806
|
-
};
|
|
807
|
-
}
|
|
808
|
-
function createTrackFormatMark(before, after, author = DEFAULT_AUTHOR) {
|
|
809
|
-
return {
|
|
810
|
-
type: "trackFormat",
|
|
811
|
-
attrs: {
|
|
812
|
-
id: uuid.v4(),
|
|
813
|
-
author: author.name,
|
|
814
|
-
authorEmail: author.email,
|
|
815
|
-
authorImage: "",
|
|
816
|
-
date: (/* @__PURE__ */ new Date()).toISOString(),
|
|
817
|
-
before,
|
|
818
|
-
after
|
|
819
|
-
}
|
|
820
|
-
};
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
// src/services/mergeDocuments.ts
|
|
824
|
-
function cloneNode(node) {
|
|
825
|
-
return JSON.parse(JSON.stringify(node));
|
|
826
|
-
}
|
|
827
|
-
function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
|
|
828
|
-
const merged = cloneNode(docA);
|
|
829
|
-
const charStates = [];
|
|
830
|
-
let insertions = [];
|
|
831
|
-
const formatChanges = diffResult.formatChanges || [];
|
|
832
|
-
function getFormatChangeAt(pos) {
|
|
833
|
-
for (const fc of formatChanges) {
|
|
834
|
-
if (pos >= fc.from && pos < fc.to) {
|
|
835
|
-
return fc;
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
return null;
|
|
839
|
-
}
|
|
840
|
-
let docAOffset = 0;
|
|
841
|
-
const segments = diffResult.segments;
|
|
842
|
-
for (let segIdx = 0; segIdx < segments.length; segIdx++) {
|
|
843
|
-
const segment = segments[segIdx];
|
|
844
|
-
if (segment.type === "equal") {
|
|
845
|
-
for (let i = 0; i < segment.text.length; i++) {
|
|
846
|
-
charStates[docAOffset + i] = { type: "equal" };
|
|
847
|
-
}
|
|
848
|
-
docAOffset += segment.text.length;
|
|
849
|
-
} else if (segment.type === "delete") {
|
|
850
|
-
const nextSegment = segments[segIdx + 1];
|
|
851
|
-
const isReplacement = nextSegment && nextSegment.type === "insert";
|
|
852
|
-
const replacementId = isReplacement ? uuid.v4() : void 0;
|
|
853
|
-
for (let i = 0; i < segment.text.length; i++) {
|
|
854
|
-
charStates[docAOffset + i] = { type: "delete", replacementId };
|
|
855
|
-
}
|
|
856
|
-
docAOffset += segment.text.length;
|
|
857
|
-
if (isReplacement && nextSegment) {
|
|
858
|
-
insertions.push({
|
|
859
|
-
afterOffset: docAOffset,
|
|
860
|
-
text: nextSegment.text,
|
|
861
|
-
replacementId
|
|
862
|
-
});
|
|
863
|
-
segIdx++;
|
|
864
|
-
}
|
|
865
|
-
} else if (segment.type === "insert") {
|
|
866
|
-
insertions.push({
|
|
867
|
-
afterOffset: docAOffset,
|
|
868
|
-
text: segment.text
|
|
869
|
-
});
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
function transformNode(node, nodeOffset, path) {
|
|
873
|
-
if (node.type === "text" && node.text) {
|
|
874
|
-
const text = node.text;
|
|
875
|
-
const result = [];
|
|
876
|
-
let i = 0;
|
|
877
|
-
while (i < text.length) {
|
|
878
|
-
const charOffset = nodeOffset + i;
|
|
879
|
-
const charState = charStates[charOffset] || { type: "equal" };
|
|
880
|
-
const insertionsHere = insertions.filter((ins) => ins.afterOffset === charOffset);
|
|
881
|
-
for (const ins of insertionsHere) {
|
|
882
|
-
result.push({
|
|
883
|
-
type: "text",
|
|
884
|
-
text: ins.text,
|
|
885
|
-
marks: [...node.marks || [], createTrackInsertMark(author, ins.replacementId)]
|
|
886
|
-
});
|
|
887
|
-
}
|
|
888
|
-
const currentFormatChange = getFormatChangeAt(nodeOffset + i);
|
|
889
|
-
let j = i + 1;
|
|
890
|
-
while (j < text.length) {
|
|
891
|
-
const nextState = charStates[nodeOffset + j] || { type: "equal" };
|
|
892
|
-
if (nextState.type !== charState.type) break;
|
|
893
|
-
if (insertions.some((ins) => ins.afterOffset === nodeOffset + j)) break;
|
|
894
|
-
const nextFormatChange = getFormatChangeAt(nodeOffset + j);
|
|
895
|
-
if (currentFormatChange !== nextFormatChange) break;
|
|
896
|
-
j++;
|
|
897
|
-
}
|
|
898
|
-
const chunk = text.substring(i, j);
|
|
899
|
-
let marks = [...node.marks || []];
|
|
900
|
-
if (charState.type === "delete") {
|
|
901
|
-
marks.push(createTrackDeleteMark(author, charState.replacementId));
|
|
902
|
-
} else if (charState.type === "equal") {
|
|
903
|
-
if (currentFormatChange) {
|
|
904
|
-
const trackFormatMark = createTrackFormatMark(
|
|
905
|
-
currentFormatChange.before,
|
|
906
|
-
currentFormatChange.after,
|
|
907
|
-
author
|
|
908
|
-
);
|
|
909
|
-
marks = [...currentFormatChange.after, trackFormatMark];
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
result.push({
|
|
913
|
-
type: "text",
|
|
914
|
-
text: chunk,
|
|
915
|
-
marks: marks.length > 0 ? marks : void 0
|
|
916
|
-
});
|
|
917
|
-
i = j;
|
|
918
|
-
}
|
|
919
|
-
const endOffset = nodeOffset + text.length;
|
|
920
|
-
const endInsertions = insertions.filter((ins) => ins.afterOffset === endOffset);
|
|
921
|
-
for (const ins of endInsertions) {
|
|
922
|
-
result.push({
|
|
923
|
-
type: "text",
|
|
924
|
-
text: ins.text,
|
|
925
|
-
marks: [...node.marks || [], createTrackInsertMark(author, ins.replacementId)]
|
|
926
|
-
});
|
|
927
|
-
}
|
|
928
|
-
insertions = insertions.filter(
|
|
929
|
-
(ins) => ins.afterOffset < nodeOffset || ins.afterOffset > endOffset
|
|
930
|
-
);
|
|
931
|
-
return { nodes: result, consumedLength: text.length };
|
|
932
|
-
}
|
|
933
|
-
if (node.content && Array.isArray(node.content)) {
|
|
934
|
-
const newContent = [];
|
|
935
|
-
let offset = nodeOffset;
|
|
936
|
-
for (const child of node.content) {
|
|
937
|
-
const { nodes, consumedLength } = transformNode(child, offset);
|
|
938
|
-
newContent.push(...nodes);
|
|
939
|
-
offset += consumedLength;
|
|
940
|
-
}
|
|
941
|
-
return {
|
|
942
|
-
nodes: [{ ...node, content: newContent }],
|
|
943
|
-
consumedLength: offset - nodeOffset
|
|
944
|
-
};
|
|
945
|
-
}
|
|
946
|
-
return { nodes: [node], consumedLength: 0 };
|
|
947
|
-
}
|
|
948
|
-
if (merged.content && Array.isArray(merged.content)) {
|
|
949
|
-
const newContent = [];
|
|
950
|
-
let offset = 0;
|
|
951
|
-
for (let i = 0; i < merged.content.length; i++) {
|
|
952
|
-
const child = merged.content[i];
|
|
953
|
-
const { nodes, consumedLength } = transformNode(child, offset);
|
|
954
|
-
newContent.push(...nodes);
|
|
955
|
-
offset += consumedLength;
|
|
956
|
-
}
|
|
957
|
-
merged.content = newContent;
|
|
958
|
-
}
|
|
959
|
-
if (insertions.length > 0) {
|
|
960
|
-
for (const ins of insertions) {
|
|
961
|
-
const insertNode = {
|
|
962
|
-
type: "paragraph",
|
|
963
|
-
content: [
|
|
964
|
-
{
|
|
965
|
-
type: "run",
|
|
966
|
-
content: [
|
|
967
|
-
{
|
|
968
|
-
type: "text",
|
|
969
|
-
text: ins.text,
|
|
970
|
-
marks: [createTrackInsertMark(author, ins.replacementId)]
|
|
971
|
-
}
|
|
972
|
-
]
|
|
973
|
-
}
|
|
974
|
-
]
|
|
975
|
-
};
|
|
976
|
-
if (!merged.content) merged.content = [];
|
|
977
|
-
merged.content.push(insertNode);
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
return merged;
|
|
981
|
-
}
|
|
982
802
|
|
|
983
803
|
// src/services/changeContextExtractor.ts
|
|
984
804
|
function extractEnrichedChanges(mergedJson) {
|
|
@@ -1237,6 +1057,44 @@ function groupReplacements(changes) {
|
|
|
1237
1057
|
}
|
|
1238
1058
|
return result;
|
|
1239
1059
|
}
|
|
1060
|
+
function createTrackInsertMark(author = DEFAULT_AUTHOR, id) {
|
|
1061
|
+
return {
|
|
1062
|
+
type: "trackInsert",
|
|
1063
|
+
attrs: {
|
|
1064
|
+
id: id ?? uuid.v4(),
|
|
1065
|
+
author: author.name,
|
|
1066
|
+
authorEmail: author.email,
|
|
1067
|
+
authorImage: "",
|
|
1068
|
+
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
function createTrackDeleteMark(author = DEFAULT_AUTHOR, id) {
|
|
1073
|
+
return {
|
|
1074
|
+
type: "trackDelete",
|
|
1075
|
+
attrs: {
|
|
1076
|
+
id: id ?? uuid.v4(),
|
|
1077
|
+
author: author.name,
|
|
1078
|
+
authorEmail: author.email,
|
|
1079
|
+
authorImage: "",
|
|
1080
|
+
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
function createTrackFormatMark(before, after, author = DEFAULT_AUTHOR) {
|
|
1085
|
+
return {
|
|
1086
|
+
type: "trackFormat",
|
|
1087
|
+
attrs: {
|
|
1088
|
+
id: uuid.v4(),
|
|
1089
|
+
author: author.name,
|
|
1090
|
+
authorEmail: author.email,
|
|
1091
|
+
authorImage: "",
|
|
1092
|
+
date: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1093
|
+
before,
|
|
1094
|
+
after
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1240
1098
|
|
|
1241
1099
|
// src/services/nodeAligner.ts
|
|
1242
1100
|
init_nodeFingerprint();
|
|
@@ -1417,19 +1275,177 @@ function alignListItems(listA, listB, listPathA, listPathB) {
|
|
|
1417
1275
|
}
|
|
1418
1276
|
return alignNodes(itemsA, itemsB);
|
|
1419
1277
|
}
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1278
|
+
function cloneNode(node) {
|
|
1279
|
+
return JSON.parse(JSON.stringify(node));
|
|
1280
|
+
}
|
|
1281
|
+
function mergeDocuments(docA, docB, diffResult, author = DEFAULT_AUTHOR) {
|
|
1282
|
+
const merged = cloneNode(docA);
|
|
1283
|
+
const charStates = [];
|
|
1284
|
+
let insertions = [];
|
|
1285
|
+
const formatChanges = diffResult.formatChanges || [];
|
|
1286
|
+
function getFormatChangeAt(pos) {
|
|
1287
|
+
for (const fc of formatChanges) {
|
|
1288
|
+
if (pos >= fc.from && pos < fc.to) {
|
|
1289
|
+
return fc;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return null;
|
|
1293
|
+
}
|
|
1294
|
+
let docAOffset = 0;
|
|
1295
|
+
const segments = diffResult.segments;
|
|
1296
|
+
for (let segIdx = 0; segIdx < segments.length; segIdx++) {
|
|
1297
|
+
const segment = segments[segIdx];
|
|
1298
|
+
if (segment.type === "equal") {
|
|
1299
|
+
for (let i = 0; i < segment.text.length; i++) {
|
|
1300
|
+
charStates[docAOffset + i] = { type: "equal" };
|
|
1301
|
+
}
|
|
1302
|
+
docAOffset += segment.text.length;
|
|
1303
|
+
} else if (segment.type === "delete") {
|
|
1304
|
+
const nextSegment = segments[segIdx + 1];
|
|
1305
|
+
const isReplacement = nextSegment && nextSegment.type === "insert";
|
|
1306
|
+
const replacementId = isReplacement ? uuid.v4() : void 0;
|
|
1307
|
+
for (let i = 0; i < segment.text.length; i++) {
|
|
1308
|
+
charStates[docAOffset + i] = { type: "delete", replacementId };
|
|
1309
|
+
}
|
|
1310
|
+
docAOffset += segment.text.length;
|
|
1311
|
+
if (isReplacement && nextSegment) {
|
|
1312
|
+
insertions.push({
|
|
1313
|
+
afterOffset: docAOffset,
|
|
1314
|
+
text: nextSegment.text,
|
|
1315
|
+
replacementId
|
|
1316
|
+
});
|
|
1317
|
+
segIdx++;
|
|
1318
|
+
}
|
|
1319
|
+
} else if (segment.type === "insert") {
|
|
1320
|
+
insertions.push({
|
|
1321
|
+
afterOffset: docAOffset,
|
|
1322
|
+
text: segment.text
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
function transformNode(node, nodeOffset, path) {
|
|
1327
|
+
if (node.type === "text" && node.text) {
|
|
1328
|
+
const text = node.text;
|
|
1329
|
+
const result = [];
|
|
1330
|
+
let i = 0;
|
|
1331
|
+
while (i < text.length) {
|
|
1332
|
+
const charOffset = nodeOffset + i;
|
|
1333
|
+
const charState = charStates[charOffset] || { type: "equal" };
|
|
1334
|
+
const insertionsHere = insertions.filter((ins) => ins.afterOffset === charOffset);
|
|
1335
|
+
for (const ins of insertionsHere) {
|
|
1336
|
+
result.push({
|
|
1337
|
+
type: "text",
|
|
1338
|
+
text: ins.text,
|
|
1339
|
+
marks: [...node.marks || [], createTrackInsertMark(author, ins.replacementId)]
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
const currentFormatChange = getFormatChangeAt(nodeOffset + i);
|
|
1343
|
+
let j = i + 1;
|
|
1344
|
+
while (j < text.length) {
|
|
1345
|
+
const nextState = charStates[nodeOffset + j] || { type: "equal" };
|
|
1346
|
+
if (nextState.type !== charState.type) break;
|
|
1347
|
+
if (insertions.some((ins) => ins.afterOffset === nodeOffset + j)) break;
|
|
1348
|
+
const nextFormatChange = getFormatChangeAt(nodeOffset + j);
|
|
1349
|
+
if (currentFormatChange !== nextFormatChange) break;
|
|
1350
|
+
j++;
|
|
1351
|
+
}
|
|
1352
|
+
const chunk = text.substring(i, j);
|
|
1353
|
+
let marks = [...node.marks || []];
|
|
1354
|
+
if (charState.type === "delete") {
|
|
1355
|
+
marks.push(createTrackDeleteMark(author, charState.replacementId));
|
|
1356
|
+
} else if (charState.type === "equal") {
|
|
1357
|
+
if (currentFormatChange) {
|
|
1358
|
+
const trackFormatMark = createTrackFormatMark(
|
|
1359
|
+
currentFormatChange.before,
|
|
1360
|
+
currentFormatChange.after,
|
|
1361
|
+
author
|
|
1362
|
+
);
|
|
1363
|
+
marks = [...currentFormatChange.after, trackFormatMark];
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
result.push({
|
|
1367
|
+
type: "text",
|
|
1368
|
+
text: chunk,
|
|
1369
|
+
marks: marks.length > 0 ? marks : void 0
|
|
1370
|
+
});
|
|
1371
|
+
i = j;
|
|
1372
|
+
}
|
|
1373
|
+
const endOffset = nodeOffset + text.length;
|
|
1374
|
+
const endInsertions = insertions.filter((ins) => ins.afterOffset === endOffset);
|
|
1375
|
+
for (const ins of endInsertions) {
|
|
1376
|
+
result.push({
|
|
1377
|
+
type: "text",
|
|
1378
|
+
text: ins.text,
|
|
1379
|
+
marks: [...node.marks || [], createTrackInsertMark(author, ins.replacementId)]
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
insertions = insertions.filter(
|
|
1383
|
+
(ins) => ins.afterOffset < nodeOffset || ins.afterOffset > endOffset
|
|
1384
|
+
);
|
|
1385
|
+
return { nodes: result, consumedLength: text.length };
|
|
1386
|
+
}
|
|
1387
|
+
if (node.content && Array.isArray(node.content)) {
|
|
1388
|
+
const newContent = [];
|
|
1389
|
+
let offset = nodeOffset;
|
|
1390
|
+
for (const child of node.content) {
|
|
1391
|
+
const { nodes, consumedLength } = transformNode(child, offset);
|
|
1392
|
+
newContent.push(...nodes);
|
|
1393
|
+
offset += consumedLength;
|
|
1394
|
+
}
|
|
1395
|
+
return {
|
|
1396
|
+
nodes: [{ ...node, content: newContent }],
|
|
1397
|
+
consumedLength: offset - nodeOffset
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
return { nodes: [node], consumedLength: 0 };
|
|
1401
|
+
}
|
|
1402
|
+
if (merged.content && Array.isArray(merged.content)) {
|
|
1403
|
+
const newContent = [];
|
|
1404
|
+
let offset = 0;
|
|
1405
|
+
for (let i = 0; i < merged.content.length; i++) {
|
|
1406
|
+
const child = merged.content[i];
|
|
1407
|
+
const { nodes, consumedLength } = transformNode(child, offset);
|
|
1408
|
+
newContent.push(...nodes);
|
|
1409
|
+
offset += consumedLength;
|
|
1410
|
+
}
|
|
1411
|
+
merged.content = newContent;
|
|
1412
|
+
}
|
|
1413
|
+
if (insertions.length > 0) {
|
|
1414
|
+
for (const ins of insertions) {
|
|
1415
|
+
const insertNode = {
|
|
1416
|
+
type: "paragraph",
|
|
1417
|
+
content: [
|
|
1418
|
+
{
|
|
1419
|
+
type: "run",
|
|
1420
|
+
content: [
|
|
1421
|
+
{
|
|
1422
|
+
type: "text",
|
|
1423
|
+
text: ins.text,
|
|
1424
|
+
marks: [createTrackInsertMark(author, ins.replacementId)]
|
|
1425
|
+
}
|
|
1426
|
+
]
|
|
1427
|
+
}
|
|
1428
|
+
]
|
|
1429
|
+
};
|
|
1430
|
+
if (!merged.content) merged.content = [];
|
|
1431
|
+
merged.content.push(insertNode);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
return merged;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// src/services/attrComparer.ts
|
|
1438
|
+
var KNOWN_DEFAULTS = {
|
|
1439
|
+
paragraph: {
|
|
1440
|
+
textAlign: "left",
|
|
1441
|
+
indent: 0,
|
|
1442
|
+
lineSpacing: 1
|
|
1443
|
+
},
|
|
1444
|
+
heading: {
|
|
1445
|
+
level: 1,
|
|
1446
|
+
textAlign: "left"
|
|
1447
|
+
},
|
|
1448
|
+
table: {
|
|
1433
1449
|
alignment: "left",
|
|
1434
1450
|
borderStyle: "single"
|
|
1435
1451
|
},
|
|
@@ -1649,6 +1665,9 @@ function diffTables(tableA, tableB, tablePathA, tablePathB) {
|
|
|
1649
1665
|
function isTable(node) {
|
|
1650
1666
|
return node?.type === "table";
|
|
1651
1667
|
}
|
|
1668
|
+
function isTableRow(node) {
|
|
1669
|
+
return node?.type === "tableRow";
|
|
1670
|
+
}
|
|
1652
1671
|
function getRowLocation(tablePath, rowIndex, tableIndex) {
|
|
1653
1672
|
return `Table ${tableIndex + 1}, Row ${rowIndex + 1}`;
|
|
1654
1673
|
}
|
|
@@ -1685,6 +1704,9 @@ function extractCellText(cell) {
|
|
|
1685
1704
|
function isList(node) {
|
|
1686
1705
|
return node?.type === "bulletList" || node?.type === "orderedList";
|
|
1687
1706
|
}
|
|
1707
|
+
function isListItem(node) {
|
|
1708
|
+
return node?.type === "listItem";
|
|
1709
|
+
}
|
|
1688
1710
|
function extractListItemText(item) {
|
|
1689
1711
|
const texts = [];
|
|
1690
1712
|
function extract(node) {
|
|
@@ -1917,7 +1939,10 @@ function getImagePreview(node) {
|
|
|
1917
1939
|
return "(image)";
|
|
1918
1940
|
}
|
|
1919
1941
|
|
|
1920
|
-
// src/services/
|
|
1942
|
+
// src/services/structuralMerger.ts
|
|
1943
|
+
function cloneNode2(node) {
|
|
1944
|
+
return JSON.parse(JSON.stringify(node));
|
|
1945
|
+
}
|
|
1921
1946
|
function markAllTextAsInserted(node, sharedId, author) {
|
|
1922
1947
|
if (node.type === "text") {
|
|
1923
1948
|
return {
|
|
@@ -1933,7 +1958,7 @@ function markAllTextAsInserted(node, sharedId, author) {
|
|
|
1933
1958
|
)
|
|
1934
1959
|
};
|
|
1935
1960
|
}
|
|
1936
|
-
return node;
|
|
1961
|
+
return { ...node };
|
|
1937
1962
|
}
|
|
1938
1963
|
function markAllTextAsDeleted(node, sharedId, author) {
|
|
1939
1964
|
if (node.type === "text") {
|
|
@@ -1950,10 +1975,7 @@ function markAllTextAsDeleted(node, sharedId, author) {
|
|
|
1950
1975
|
)
|
|
1951
1976
|
};
|
|
1952
1977
|
}
|
|
1953
|
-
return node;
|
|
1954
|
-
}
|
|
1955
|
-
function cloneNode2(node) {
|
|
1956
|
-
return JSON.parse(JSON.stringify(node));
|
|
1978
|
+
return { ...node };
|
|
1957
1979
|
}
|
|
1958
1980
|
function extractTextPreview(node, maxLength = 50) {
|
|
1959
1981
|
const texts = [];
|
|
@@ -1974,197 +1996,361 @@ function extractTextPreview(node, maxLength = 50) {
|
|
|
1974
1996
|
}
|
|
1975
1997
|
return text || "(empty)";
|
|
1976
1998
|
}
|
|
1977
|
-
function
|
|
1978
|
-
|
|
1979
|
-
|
|
1999
|
+
function getNodeTypeDescription(node) {
|
|
2000
|
+
if (isTable(node)) return "Table";
|
|
2001
|
+
if (isList(node)) return "List";
|
|
2002
|
+
if (isListItem(node)) return "List item";
|
|
2003
|
+
if (isTableRow(node)) return "Table row";
|
|
2004
|
+
if (isImage(node)) return "Image";
|
|
2005
|
+
if (node.type === "heading") return `Heading ${node.attrs?.level || 1}`;
|
|
2006
|
+
if (node.type === "paragraph") return "Paragraph";
|
|
2007
|
+
if (node.type === "blockquote") return "Blockquote";
|
|
2008
|
+
if (node.type === "codeBlock") return "Code block";
|
|
2009
|
+
return node.type || "Block";
|
|
2010
|
+
}
|
|
2011
|
+
function mergeWithStructuralAwareness(docA, docB, author = DEFAULT_AUTHOR) {
|
|
2012
|
+
const structuralInfos = [];
|
|
2013
|
+
const summary = [];
|
|
2014
|
+
let textChangeCount = 0;
|
|
1980
2015
|
const alignment = alignDocuments(docA, docB);
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
let
|
|
1984
|
-
for (const
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2016
|
+
const operations = buildMergeOperations(alignment, docA, docB);
|
|
2017
|
+
const mergedContent = [];
|
|
2018
|
+
let blockIndex = 0;
|
|
2019
|
+
for (const op of operations) {
|
|
2020
|
+
blockIndex++;
|
|
2021
|
+
switch (op.type) {
|
|
2022
|
+
case "matched": {
|
|
2023
|
+
const { mergedNode, infos, changes } = mergeMatchedBlock(
|
|
2024
|
+
op.nodeA,
|
|
2025
|
+
op.nodeB,
|
|
2026
|
+
blockIndex,
|
|
2027
|
+
author
|
|
2028
|
+
);
|
|
2029
|
+
mergedContent.push(mergedNode);
|
|
2030
|
+
structuralInfos.push(...infos);
|
|
2031
|
+
textChangeCount += changes;
|
|
2032
|
+
break;
|
|
2033
|
+
}
|
|
2034
|
+
case "inserted": {
|
|
2035
|
+
const { markedNode, info } = createInsertedBlock(
|
|
2036
|
+
op.nodeB,
|
|
2037
|
+
blockIndex,
|
|
2038
|
+
author
|
|
2039
|
+
);
|
|
2040
|
+
mergedContent.push(markedNode);
|
|
2041
|
+
if (info) {
|
|
2042
|
+
structuralInfos.push(info);
|
|
2043
|
+
}
|
|
2044
|
+
break;
|
|
2045
|
+
}
|
|
2046
|
+
case "deleted": {
|
|
2047
|
+
const { markedNode, info } = createDeletedBlock(
|
|
2048
|
+
op.nodeA,
|
|
2049
|
+
blockIndex,
|
|
2050
|
+
author
|
|
2051
|
+
);
|
|
2052
|
+
mergedContent.push(markedNode);
|
|
2053
|
+
if (info) {
|
|
2054
|
+
structuralInfos.push(info);
|
|
2055
|
+
}
|
|
2056
|
+
break;
|
|
2057
|
+
}
|
|
2006
2058
|
}
|
|
2007
|
-
changes.push({
|
|
2008
|
-
id: sharedId,
|
|
2009
|
-
type,
|
|
2010
|
-
nodeType: node.type,
|
|
2011
|
-
path: inserted.path,
|
|
2012
|
-
node: markAllTextAsInserted(cloneNode2(node), sharedId, author)
|
|
2013
|
-
});
|
|
2014
|
-
infos.push({
|
|
2015
|
-
id: sharedId,
|
|
2016
|
-
type,
|
|
2017
|
-
nodeType: node.type,
|
|
2018
|
-
location,
|
|
2019
|
-
preview,
|
|
2020
|
-
author,
|
|
2021
|
-
date
|
|
2022
|
-
});
|
|
2023
2059
|
}
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2060
|
+
const mergedDoc = {
|
|
2061
|
+
type: "doc",
|
|
2062
|
+
content: mergedContent
|
|
2063
|
+
};
|
|
2064
|
+
const insertCount = structuralInfos.filter((i) => i.type.includes("Insert")).length;
|
|
2065
|
+
const deleteCount = structuralInfos.filter((i) => i.type.includes("Delete")).length;
|
|
2066
|
+
if (insertCount > 0) summary.push(`${insertCount} block(s) inserted`);
|
|
2067
|
+
if (deleteCount > 0) summary.push(`${deleteCount} block(s) deleted`);
|
|
2068
|
+
if (textChangeCount > 0) summary.push(`${textChangeCount} text change(s)`);
|
|
2069
|
+
return {
|
|
2070
|
+
mergedDoc,
|
|
2071
|
+
structuralInfos,
|
|
2072
|
+
summary,
|
|
2073
|
+
textChangeCount
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
function buildMergeOperations(alignment, docA, docB) {
|
|
2077
|
+
const operations = [];
|
|
2078
|
+
const matchedFromA = /* @__PURE__ */ new Map();
|
|
2079
|
+
const matchedFromB = /* @__PURE__ */ new Map();
|
|
2080
|
+
for (const match of alignment.matched) {
|
|
2081
|
+
const idxA = match.pathA[0];
|
|
2082
|
+
const idxB = match.pathB[0];
|
|
2083
|
+
matchedFromA.set(idxA, { pathB: match.pathB, similarity: match.similarity });
|
|
2084
|
+
matchedFromB.set(idxB, { pathA: match.pathA, similarity: match.similarity });
|
|
2085
|
+
}
|
|
2086
|
+
const deletedIndices = new Set(alignment.deletions.map((d) => d.path[0]));
|
|
2087
|
+
const processedDeletions = /* @__PURE__ */ new Set();
|
|
2088
|
+
const contentB = docB.content || [];
|
|
2089
|
+
const contentA = docA.content || [];
|
|
2090
|
+
for (let idxB = 0; idxB < contentB.length; idxB++) {
|
|
2091
|
+
const nodeB = contentB[idxB];
|
|
2092
|
+
const match = matchedFromB.get(idxB);
|
|
2093
|
+
if (match) {
|
|
2094
|
+
const idxA = match.pathA[0];
|
|
2095
|
+
const nodeA = contentA[idxA];
|
|
2096
|
+
for (let checkIdx = 0; checkIdx < idxA; checkIdx++) {
|
|
2097
|
+
if (deletedIndices.has(checkIdx) && !processedDeletions.has(checkIdx)) {
|
|
2098
|
+
operations.push({
|
|
2099
|
+
type: "deleted",
|
|
2100
|
+
nodeA: contentA[checkIdx],
|
|
2101
|
+
pathA: [checkIdx]
|
|
2102
|
+
});
|
|
2103
|
+
processedDeletions.add(checkIdx);
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
operations.push({
|
|
2107
|
+
type: "matched",
|
|
2108
|
+
nodeA,
|
|
2109
|
+
nodeB,
|
|
2110
|
+
pathA: match.pathA,
|
|
2111
|
+
pathB: [idxB]
|
|
2112
|
+
});
|
|
2039
2113
|
} else {
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2114
|
+
operations.push({
|
|
2115
|
+
type: "inserted",
|
|
2116
|
+
nodeB,
|
|
2117
|
+
pathB: [idxB]
|
|
2118
|
+
});
|
|
2043
2119
|
}
|
|
2044
|
-
changes.push({
|
|
2045
|
-
id: sharedId,
|
|
2046
|
-
type,
|
|
2047
|
-
nodeType: node.type,
|
|
2048
|
-
path: deleted.path,
|
|
2049
|
-
node: markAllTextAsDeleted(cloneNode2(node), sharedId, author)
|
|
2050
|
-
});
|
|
2051
|
-
infos.push({
|
|
2052
|
-
id: sharedId,
|
|
2053
|
-
type,
|
|
2054
|
-
nodeType: node.type,
|
|
2055
|
-
location,
|
|
2056
|
-
preview,
|
|
2057
|
-
author,
|
|
2058
|
-
date
|
|
2059
|
-
});
|
|
2060
2120
|
}
|
|
2061
|
-
for (const
|
|
2062
|
-
const
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
const sharedId = rowChange.id;
|
|
2070
|
-
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
2071
|
-
const rowIndex = rowChange.path[rowChange.path.length - 1];
|
|
2072
|
-
const isInsert = rowChange.type === "rowInsert";
|
|
2073
|
-
const location = getRowLocation(rowChange.path, rowIndex, tableIndex - 1);
|
|
2074
|
-
const preview = getRowPreview(rowChange.node);
|
|
2075
|
-
const markedNode = isInsert ? markAllTextAsInserted(cloneNode2(rowChange.node), sharedId, author) : markAllTextAsDeleted(cloneNode2(rowChange.node), sharedId, author);
|
|
2076
|
-
changes.push({
|
|
2077
|
-
...rowChange,
|
|
2078
|
-
node: markedNode
|
|
2079
|
-
});
|
|
2080
|
-
infos.push({
|
|
2081
|
-
id: sharedId,
|
|
2082
|
-
type: rowChange.type,
|
|
2083
|
-
nodeType: "tableRow",
|
|
2084
|
-
location,
|
|
2085
|
-
preview,
|
|
2086
|
-
author,
|
|
2087
|
-
date
|
|
2088
|
-
});
|
|
2089
|
-
}
|
|
2121
|
+
for (const deletion of alignment.deletions) {
|
|
2122
|
+
const idxA = deletion.path[0];
|
|
2123
|
+
if (!processedDeletions.has(idxA)) {
|
|
2124
|
+
operations.push({
|
|
2125
|
+
type: "deleted",
|
|
2126
|
+
nodeA: deletion.node,
|
|
2127
|
+
pathA: deletion.path
|
|
2128
|
+
});
|
|
2090
2129
|
}
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2130
|
+
}
|
|
2131
|
+
return operations;
|
|
2132
|
+
}
|
|
2133
|
+
function mergeMatchedBlock(nodeA, nodeB, blockIndex, author) {
|
|
2134
|
+
const infos = [];
|
|
2135
|
+
let changes = 0;
|
|
2136
|
+
if (isTable(nodeA) && isTable(nodeB)) {
|
|
2137
|
+
const { mergedTable, tableInfos, changeCount } = mergeMatchedTable(
|
|
2138
|
+
nodeA,
|
|
2139
|
+
nodeB,
|
|
2140
|
+
blockIndex,
|
|
2141
|
+
author
|
|
2142
|
+
);
|
|
2143
|
+
return { mergedNode: mergedTable, infos: tableInfos, changes: changeCount };
|
|
2144
|
+
}
|
|
2145
|
+
if (isList(nodeA) && isList(nodeB)) {
|
|
2146
|
+
const { mergedList, listInfos, changeCount } = mergeMatchedList(
|
|
2147
|
+
nodeA,
|
|
2148
|
+
nodeB,
|
|
2149
|
+
blockIndex,
|
|
2150
|
+
author
|
|
2151
|
+
);
|
|
2152
|
+
return { mergedNode: mergedList, infos: listInfos, changes: changeCount };
|
|
2153
|
+
}
|
|
2154
|
+
const diff = diffDocuments(
|
|
2155
|
+
{ type: "doc", content: [nodeA] },
|
|
2156
|
+
{ type: "doc", content: [nodeB] }
|
|
2157
|
+
);
|
|
2158
|
+
changes = diff.segments.filter((s) => s.type !== "equal").length;
|
|
2159
|
+
changes += diff.formatChanges?.length || 0;
|
|
2160
|
+
const merged = mergeDocuments(
|
|
2161
|
+
{ type: "doc", content: [nodeA] },
|
|
2162
|
+
{ },
|
|
2163
|
+
diff,
|
|
2164
|
+
author
|
|
2165
|
+
);
|
|
2166
|
+
const mergedNode = merged.content?.[0] || cloneNode2(nodeB);
|
|
2167
|
+
return { mergedNode, infos, changes };
|
|
2168
|
+
}
|
|
2169
|
+
function mergeMatchedTable(tableA, tableB, tableIndex, author) {
|
|
2170
|
+
const tableInfos = [];
|
|
2171
|
+
let changeCount = 0;
|
|
2172
|
+
const rowAlignment = alignTableRows(tableA, tableB, [tableIndex - 1], [tableIndex - 1]);
|
|
2173
|
+
const mergedRows = [];
|
|
2174
|
+
const matchedFromA = /* @__PURE__ */ new Map();
|
|
2175
|
+
const matchedFromB = /* @__PURE__ */ new Map();
|
|
2176
|
+
for (const match of rowAlignment.matched) {
|
|
2177
|
+
const idxA = match.pathA[match.pathA.length - 1];
|
|
2178
|
+
const idxB = match.pathB[match.pathB.length - 1];
|
|
2179
|
+
matchedFromA.set(idxA, idxB);
|
|
2180
|
+
matchedFromB.set(idxB, idxA);
|
|
2181
|
+
}
|
|
2182
|
+
const deletedIndices = new Set(rowAlignment.deletions.map((d) => d.path[d.path.length - 1]));
|
|
2183
|
+
const processedDeletions = /* @__PURE__ */ new Set();
|
|
2184
|
+
const rowsA = tableA.content || [];
|
|
2185
|
+
const rowsB = tableB.content || [];
|
|
2186
|
+
for (let idxB = 0; idxB < rowsB.length; idxB++) {
|
|
2187
|
+
const rowB = rowsB[idxB];
|
|
2188
|
+
const matchedIdxA = matchedFromB.get(idxB);
|
|
2189
|
+
if (matchedIdxA !== void 0) {
|
|
2190
|
+
const rowA = rowsA[matchedIdxA];
|
|
2191
|
+
for (let checkIdx = 0; checkIdx < matchedIdxA; checkIdx++) {
|
|
2192
|
+
if (deletedIndices.has(checkIdx) && !processedDeletions.has(checkIdx)) {
|
|
2193
|
+
const deletedRow = rowsA[checkIdx];
|
|
2194
|
+
const changeId = uuid.v4();
|
|
2195
|
+
mergedRows.push(markAllTextAsDeleted(cloneNode2(deletedRow), changeId, author));
|
|
2196
|
+
tableInfos.push({
|
|
2197
|
+
id: changeId,
|
|
2198
|
+
type: "rowDelete",
|
|
2199
|
+
nodeType: "tableRow",
|
|
2200
|
+
location: `Table ${tableIndex}, Row ${checkIdx + 1}`,
|
|
2201
|
+
preview: extractTextPreview(deletedRow),
|
|
2202
|
+
author,
|
|
2203
|
+
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2204
|
+
});
|
|
2205
|
+
processedDeletions.add(checkIdx);
|
|
2206
|
+
}
|
|
2115
2207
|
}
|
|
2208
|
+
const { mergedNode, changes } = mergeMatchedBlock(rowA, rowB, idxB, author);
|
|
2209
|
+
mergedRows.push(mergedNode);
|
|
2210
|
+
changeCount += changes;
|
|
2211
|
+
} else {
|
|
2212
|
+
const changeId = uuid.v4();
|
|
2213
|
+
mergedRows.push(markAllTextAsInserted(cloneNode2(rowB), changeId, author));
|
|
2214
|
+
tableInfos.push({
|
|
2215
|
+
id: changeId,
|
|
2216
|
+
type: "rowInsert",
|
|
2217
|
+
nodeType: "tableRow",
|
|
2218
|
+
location: `Table ${tableIndex}, Row ${idxB + 1}`,
|
|
2219
|
+
preview: extractTextPreview(rowB),
|
|
2220
|
+
author,
|
|
2221
|
+
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2222
|
+
});
|
|
2116
2223
|
}
|
|
2117
2224
|
}
|
|
2118
|
-
const
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
for (const imgDelete of imageChanges.deleted) {
|
|
2134
|
-
const sharedId = imgDelete.id;
|
|
2135
|
-
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
2136
|
-
infos.push({
|
|
2137
|
-
id: sharedId,
|
|
2138
|
-
type: "imageDelete",
|
|
2139
|
-
nodeType: "image",
|
|
2140
|
-
location: getImageLocation(imgDelete.path),
|
|
2141
|
-
preview: getImagePreview(imgDelete.node),
|
|
2142
|
-
author,
|
|
2143
|
-
date
|
|
2144
|
-
});
|
|
2145
|
-
changes.push(imgDelete);
|
|
2225
|
+
for (const deletion of rowAlignment.deletions) {
|
|
2226
|
+
const idxA = deletion.path[deletion.path.length - 1];
|
|
2227
|
+
if (!processedDeletions.has(idxA)) {
|
|
2228
|
+
const changeId = uuid.v4();
|
|
2229
|
+
mergedRows.push(markAllTextAsDeleted(cloneNode2(deletion.node), changeId, author));
|
|
2230
|
+
tableInfos.push({
|
|
2231
|
+
id: changeId,
|
|
2232
|
+
type: "rowDelete",
|
|
2233
|
+
nodeType: "tableRow",
|
|
2234
|
+
location: `Table ${tableIndex}, Row ${idxA + 1}`,
|
|
2235
|
+
preview: extractTextPreview(deletion.node),
|
|
2236
|
+
author,
|
|
2237
|
+
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2238
|
+
});
|
|
2239
|
+
}
|
|
2146
2240
|
}
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
const
|
|
2155
|
-
|
|
2156
|
-
const
|
|
2157
|
-
const
|
|
2158
|
-
const
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2241
|
+
const mergedTable = {
|
|
2242
|
+
...tableB,
|
|
2243
|
+
content: mergedRows
|
|
2244
|
+
};
|
|
2245
|
+
return { mergedTable, tableInfos, changeCount };
|
|
2246
|
+
}
|
|
2247
|
+
function mergeMatchedList(listA, listB, listIndex, author) {
|
|
2248
|
+
const listInfos = [];
|
|
2249
|
+
let changeCount = 0;
|
|
2250
|
+
const itemAlignment = alignListItems(listA, listB, [listIndex - 1], [listIndex - 1]);
|
|
2251
|
+
const mergedItems = [];
|
|
2252
|
+
const matchedFromA = /* @__PURE__ */ new Map();
|
|
2253
|
+
const matchedFromB = /* @__PURE__ */ new Map();
|
|
2254
|
+
for (const match of itemAlignment.matched) {
|
|
2255
|
+
const idxA = match.pathA[match.pathA.length - 1];
|
|
2256
|
+
const idxB = match.pathB[match.pathB.length - 1];
|
|
2257
|
+
matchedFromA.set(idxA, idxB);
|
|
2258
|
+
matchedFromB.set(idxB, idxA);
|
|
2259
|
+
}
|
|
2260
|
+
const deletedIndices = new Set(itemAlignment.deletions.map((d) => d.path[d.path.length - 1]));
|
|
2261
|
+
const processedDeletions = /* @__PURE__ */ new Set();
|
|
2262
|
+
const itemsA = listA.content || [];
|
|
2263
|
+
const itemsB = listB.content || [];
|
|
2264
|
+
for (let idxB = 0; idxB < itemsB.length; idxB++) {
|
|
2265
|
+
const itemB = itemsB[idxB];
|
|
2266
|
+
const matchedIdxA = matchedFromB.get(idxB);
|
|
2267
|
+
if (matchedIdxA !== void 0) {
|
|
2268
|
+
const itemA = itemsA[matchedIdxA];
|
|
2269
|
+
for (let checkIdx = 0; checkIdx < matchedIdxA; checkIdx++) {
|
|
2270
|
+
if (deletedIndices.has(checkIdx) && !processedDeletions.has(checkIdx)) {
|
|
2271
|
+
const deletedItem = itemsA[checkIdx];
|
|
2272
|
+
const changeId = uuid.v4();
|
|
2273
|
+
mergedItems.push(markAllTextAsDeleted(cloneNode2(deletedItem), changeId, author));
|
|
2274
|
+
listInfos.push({
|
|
2275
|
+
id: changeId,
|
|
2276
|
+
type: "listItemDelete",
|
|
2277
|
+
nodeType: "listItem",
|
|
2278
|
+
location: `List ${listIndex}, Item ${checkIdx + 1}`,
|
|
2279
|
+
preview: extractTextPreview(deletedItem),
|
|
2280
|
+
author,
|
|
2281
|
+
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2282
|
+
});
|
|
2283
|
+
processedDeletions.add(checkIdx);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
const { mergedNode, changes } = mergeMatchedBlock(itemA, itemB, idxB, author);
|
|
2287
|
+
mergedItems.push(mergedNode);
|
|
2288
|
+
changeCount += changes;
|
|
2289
|
+
} else {
|
|
2290
|
+
const changeId = uuid.v4();
|
|
2291
|
+
mergedItems.push(markAllTextAsInserted(cloneNode2(itemB), changeId, author));
|
|
2292
|
+
listInfos.push({
|
|
2293
|
+
id: changeId,
|
|
2294
|
+
type: "listItemInsert",
|
|
2295
|
+
nodeType: "listItem",
|
|
2296
|
+
location: `List ${listIndex}, Item ${idxB + 1}`,
|
|
2297
|
+
preview: extractTextPreview(itemB),
|
|
2298
|
+
author,
|
|
2299
|
+
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
for (const deletion of itemAlignment.deletions) {
|
|
2304
|
+
const idxA = deletion.path[deletion.path.length - 1];
|
|
2305
|
+
if (!processedDeletions.has(idxA)) {
|
|
2306
|
+
const changeId = uuid.v4();
|
|
2307
|
+
mergedItems.push(markAllTextAsDeleted(cloneNode2(deletion.node), changeId, author));
|
|
2308
|
+
listInfos.push({
|
|
2309
|
+
id: changeId,
|
|
2310
|
+
type: "listItemDelete",
|
|
2311
|
+
nodeType: "listItem",
|
|
2312
|
+
location: `List ${listIndex}, Item ${idxA + 1}`,
|
|
2313
|
+
preview: extractTextPreview(deletion.node),
|
|
2314
|
+
author,
|
|
2315
|
+
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2316
|
+
});
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
const mergedList = {
|
|
2320
|
+
...listB,
|
|
2321
|
+
content: mergedItems
|
|
2322
|
+
};
|
|
2323
|
+
return { mergedList, listInfos, changeCount };
|
|
2324
|
+
}
|
|
2325
|
+
function createInsertedBlock(node, blockIndex, author) {
|
|
2326
|
+
const changeId = uuid.v4();
|
|
2327
|
+
const markedNode = markAllTextAsInserted(cloneNode2(node), changeId, author);
|
|
2328
|
+
const nodeDesc = getNodeTypeDescription(node);
|
|
2329
|
+
const info = {
|
|
2330
|
+
id: changeId,
|
|
2331
|
+
type: isTable(node) ? "rowInsert" : isList(node) ? "listItemInsert" : isImage(node) ? "imageInsert" : "paragraphInsert",
|
|
2332
|
+
nodeType: node.type || "unknown",
|
|
2333
|
+
location: `${nodeDesc} inserted at position ${blockIndex}`,
|
|
2334
|
+
preview: extractTextPreview(node),
|
|
2335
|
+
author,
|
|
2336
|
+
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2337
|
+
};
|
|
2338
|
+
return { markedNode, info };
|
|
2339
|
+
}
|
|
2340
|
+
function createDeletedBlock(node, blockIndex, author) {
|
|
2341
|
+
const changeId = uuid.v4();
|
|
2342
|
+
const markedNode = markAllTextAsDeleted(cloneNode2(node), changeId, author);
|
|
2343
|
+
const nodeDesc = getNodeTypeDescription(node);
|
|
2344
|
+
const info = {
|
|
2345
|
+
id: changeId,
|
|
2346
|
+
type: isTable(node) ? "rowDelete" : isList(node) ? "listItemDelete" : isImage(node) ? "imageDelete" : "paragraphDelete",
|
|
2347
|
+
nodeType: node.type || "unknown",
|
|
2348
|
+
location: `${nodeDesc} deleted from position ${blockIndex}`,
|
|
2349
|
+
preview: extractTextPreview(node),
|
|
2350
|
+
author,
|
|
2351
|
+
date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2352
|
+
};
|
|
2353
|
+
return { markedNode, info };
|
|
2168
2354
|
}
|
|
2169
2355
|
var permissionResolver = ({ permission }) => {
|
|
2170
2356
|
return TRACK_CHANGE_PERMISSIONS.includes(permission) ? true : void 0;
|
|
@@ -2603,10 +2789,16 @@ var DocxDiffEditor = react.forwardRef(
|
|
|
2603
2789
|
}
|
|
2604
2790
|
newJson = content;
|
|
2605
2791
|
}
|
|
2792
|
+
const structuralResult = mergeWithStructuralAwareness(
|
|
2793
|
+
sourceJson,
|
|
2794
|
+
newJson,
|
|
2795
|
+
author
|
|
2796
|
+
);
|
|
2797
|
+
const merged = structuralResult.mergedDoc;
|
|
2798
|
+
const structInfos = structuralResult.structuralInfos;
|
|
2799
|
+
setMergedJson(merged);
|
|
2606
2800
|
const diff = diffDocuments(sourceJson, newJson);
|
|
2607
2801
|
setDiffResult(diff);
|
|
2608
|
-
const merged = mergeDocuments(sourceJson, newJson, diff, author);
|
|
2609
|
-
setMergedJson(merged);
|
|
2610
2802
|
if (superdocRef.current?.activeEditor) {
|
|
2611
2803
|
setEditorContent(superdocRef.current.activeEditor, merged);
|
|
2612
2804
|
enableReviewMode(superdocRef.current);
|
|
@@ -2627,19 +2819,16 @@ var DocxDiffEditor = react.forwardRef(
|
|
|
2627
2819
|
}, 50);
|
|
2628
2820
|
}
|
|
2629
2821
|
}
|
|
2630
|
-
const { changes: structChanges, infos: structInfos } = processStructuralChanges(
|
|
2631
|
-
sourceJson,
|
|
2632
|
-
newJson,
|
|
2633
|
-
author
|
|
2634
|
-
);
|
|
2635
2822
|
setStructuralChanges(structInfos);
|
|
2636
2823
|
setIsPaneDismissed(false);
|
|
2637
2824
|
const insertions = diff.segments.filter((s) => s.type === "insert").length;
|
|
2638
2825
|
const deletions = diff.segments.filter((s) => s.type === "delete").length;
|
|
2639
2826
|
const formatChanges = diff.formatChanges?.length || 0;
|
|
2640
2827
|
const structuralChangeCount = structInfos.length;
|
|
2641
|
-
const
|
|
2642
|
-
|
|
2828
|
+
const combinedSummary = [...structuralResult.summary];
|
|
2829
|
+
if (diff.summary.length > 0 && structuralResult.summary.length === 0) {
|
|
2830
|
+
combinedSummary.push(...diff.summary);
|
|
2831
|
+
}
|
|
2643
2832
|
const result = {
|
|
2644
2833
|
totalChanges: insertions + deletions + formatChanges + structuralChangeCount,
|
|
2645
2834
|
insertions,
|
|
@@ -3033,6 +3222,254 @@ var DocxDiffEditor_default = DocxDiffEditor;
|
|
|
3033
3222
|
|
|
3034
3223
|
// src/services/index.ts
|
|
3035
3224
|
init_nodeFingerprint();
|
|
3225
|
+
function markAllTextAsInserted2(node, sharedId, author) {
|
|
3226
|
+
if (node.type === "text") {
|
|
3227
|
+
return {
|
|
3228
|
+
...node,
|
|
3229
|
+
marks: [...node.marks || [], createTrackInsertMark(author, sharedId)]
|
|
3230
|
+
};
|
|
3231
|
+
}
|
|
3232
|
+
if (node.content && Array.isArray(node.content)) {
|
|
3233
|
+
return {
|
|
3234
|
+
...node,
|
|
3235
|
+
content: node.content.map(
|
|
3236
|
+
(child) => markAllTextAsInserted2(child, sharedId, author)
|
|
3237
|
+
)
|
|
3238
|
+
};
|
|
3239
|
+
}
|
|
3240
|
+
return node;
|
|
3241
|
+
}
|
|
3242
|
+
function markAllTextAsDeleted2(node, sharedId, author) {
|
|
3243
|
+
if (node.type === "text") {
|
|
3244
|
+
return {
|
|
3245
|
+
...node,
|
|
3246
|
+
marks: [...node.marks || [], createTrackDeleteMark(author, sharedId)]
|
|
3247
|
+
};
|
|
3248
|
+
}
|
|
3249
|
+
if (node.content && Array.isArray(node.content)) {
|
|
3250
|
+
return {
|
|
3251
|
+
...node,
|
|
3252
|
+
content: node.content.map(
|
|
3253
|
+
(child) => markAllTextAsDeleted2(child, sharedId, author)
|
|
3254
|
+
)
|
|
3255
|
+
};
|
|
3256
|
+
}
|
|
3257
|
+
return node;
|
|
3258
|
+
}
|
|
3259
|
+
function cloneNode3(node) {
|
|
3260
|
+
return JSON.parse(JSON.stringify(node));
|
|
3261
|
+
}
|
|
3262
|
+
function extractTextPreview2(node, maxLength = 50) {
|
|
3263
|
+
const texts = [];
|
|
3264
|
+
function extract(n) {
|
|
3265
|
+
if (n.type === "text") {
|
|
3266
|
+
texts.push(n.text || "");
|
|
3267
|
+
}
|
|
3268
|
+
if (n.content) {
|
|
3269
|
+
for (const child of n.content) {
|
|
3270
|
+
extract(child);
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
extract(node);
|
|
3275
|
+
const text = texts.join("").trim();
|
|
3276
|
+
if (text.length > maxLength) {
|
|
3277
|
+
return text.substring(0, maxLength - 3) + "...";
|
|
3278
|
+
}
|
|
3279
|
+
return text || "(empty)";
|
|
3280
|
+
}
|
|
3281
|
+
function processStructuralChanges(docA, docB, author = DEFAULT_AUTHOR) {
|
|
3282
|
+
const changes = [];
|
|
3283
|
+
const infos = [];
|
|
3284
|
+
const alignment = alignDocuments(docA, docB);
|
|
3285
|
+
let tableIndex = 0;
|
|
3286
|
+
let listIndex = 0;
|
|
3287
|
+
let paragraphIndex = 0;
|
|
3288
|
+
for (const inserted of alignment.insertions) {
|
|
3289
|
+
const node = inserted.node;
|
|
3290
|
+
const sharedId = uuid.v4();
|
|
3291
|
+
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
3292
|
+
let type = "paragraphInsert";
|
|
3293
|
+
let location = "";
|
|
3294
|
+
let preview = "";
|
|
3295
|
+
if (isTable(node)) {
|
|
3296
|
+
type = "rowInsert";
|
|
3297
|
+
location = `New table at position ${inserted.path[0] + 1}`;
|
|
3298
|
+
preview = `Table with ${node.content?.length || 0} rows`;
|
|
3299
|
+
tableIndex++;
|
|
3300
|
+
} else if (isList(node)) {
|
|
3301
|
+
type = "listItemInsert";
|
|
3302
|
+
location = `New list at position ${inserted.path[0] + 1}`;
|
|
3303
|
+
preview = `List with ${node.content?.length || 0} items`;
|
|
3304
|
+
listIndex++;
|
|
3305
|
+
} else {
|
|
3306
|
+
type = "paragraphInsert";
|
|
3307
|
+
paragraphIndex++;
|
|
3308
|
+
location = `Paragraph ${paragraphIndex}`;
|
|
3309
|
+
preview = extractTextPreview2(node);
|
|
3310
|
+
}
|
|
3311
|
+
changes.push({
|
|
3312
|
+
id: sharedId,
|
|
3313
|
+
type,
|
|
3314
|
+
nodeType: node.type,
|
|
3315
|
+
path: inserted.path,
|
|
3316
|
+
node: markAllTextAsInserted2(cloneNode3(node), sharedId, author)
|
|
3317
|
+
});
|
|
3318
|
+
infos.push({
|
|
3319
|
+
id: sharedId,
|
|
3320
|
+
type,
|
|
3321
|
+
nodeType: node.type,
|
|
3322
|
+
location,
|
|
3323
|
+
preview,
|
|
3324
|
+
author,
|
|
3325
|
+
date
|
|
3326
|
+
});
|
|
3327
|
+
}
|
|
3328
|
+
for (const deleted of alignment.deletions) {
|
|
3329
|
+
const node = deleted.node;
|
|
3330
|
+
const sharedId = uuid.v4();
|
|
3331
|
+
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
3332
|
+
let type = "paragraphDelete";
|
|
3333
|
+
let location = "";
|
|
3334
|
+
let preview = "";
|
|
3335
|
+
if (isTable(node)) {
|
|
3336
|
+
type = "rowDelete";
|
|
3337
|
+
location = `Deleted table at position ${deleted.path[0] + 1}`;
|
|
3338
|
+
preview = `Table with ${node.content?.length || 0} rows`;
|
|
3339
|
+
} else if (isList(node)) {
|
|
3340
|
+
type = "listItemDelete";
|
|
3341
|
+
location = `Deleted list at position ${deleted.path[0] + 1}`;
|
|
3342
|
+
preview = `List with ${node.content?.length || 0} items`;
|
|
3343
|
+
} else {
|
|
3344
|
+
type = "paragraphDelete";
|
|
3345
|
+
location = `Deleted paragraph`;
|
|
3346
|
+
preview = extractTextPreview2(node);
|
|
3347
|
+
}
|
|
3348
|
+
changes.push({
|
|
3349
|
+
id: sharedId,
|
|
3350
|
+
type,
|
|
3351
|
+
nodeType: node.type,
|
|
3352
|
+
path: deleted.path,
|
|
3353
|
+
node: markAllTextAsDeleted2(cloneNode3(node), sharedId, author)
|
|
3354
|
+
});
|
|
3355
|
+
infos.push({
|
|
3356
|
+
id: sharedId,
|
|
3357
|
+
type,
|
|
3358
|
+
nodeType: node.type,
|
|
3359
|
+
location,
|
|
3360
|
+
preview,
|
|
3361
|
+
author,
|
|
3362
|
+
date
|
|
3363
|
+
});
|
|
3364
|
+
}
|
|
3365
|
+
for (const match of alignment.matched) {
|
|
3366
|
+
const nodeA = docA.content?.[match.pathA[0]];
|
|
3367
|
+
const nodeB = docB.content?.[match.pathB[0]];
|
|
3368
|
+
if (!nodeA || !nodeB) continue;
|
|
3369
|
+
if (isTable(nodeA) && isTable(nodeB)) {
|
|
3370
|
+
tableIndex++;
|
|
3371
|
+
const tableResult = diffTables(nodeA, nodeB, match.pathA, match.pathB);
|
|
3372
|
+
for (const rowChange of tableResult.rowChanges) {
|
|
3373
|
+
const sharedId = rowChange.id;
|
|
3374
|
+
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
3375
|
+
const rowIndex = rowChange.path[rowChange.path.length - 1];
|
|
3376
|
+
const isInsert = rowChange.type === "rowInsert";
|
|
3377
|
+
const location = getRowLocation(rowChange.path, rowIndex, tableIndex - 1);
|
|
3378
|
+
const preview = getRowPreview(rowChange.node);
|
|
3379
|
+
const markedNode = isInsert ? markAllTextAsInserted2(cloneNode3(rowChange.node), sharedId, author) : markAllTextAsDeleted2(cloneNode3(rowChange.node), sharedId, author);
|
|
3380
|
+
changes.push({
|
|
3381
|
+
...rowChange,
|
|
3382
|
+
node: markedNode
|
|
3383
|
+
});
|
|
3384
|
+
infos.push({
|
|
3385
|
+
id: sharedId,
|
|
3386
|
+
type: rowChange.type,
|
|
3387
|
+
nodeType: "tableRow",
|
|
3388
|
+
location,
|
|
3389
|
+
preview,
|
|
3390
|
+
author,
|
|
3391
|
+
date
|
|
3392
|
+
});
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
if (isList(nodeA) && isList(nodeB)) {
|
|
3396
|
+
listIndex++;
|
|
3397
|
+
const listResult = diffLists(nodeA, nodeB, match.pathA, match.pathB);
|
|
3398
|
+
for (const itemChange of listResult.itemChanges) {
|
|
3399
|
+
const sharedId = itemChange.id;
|
|
3400
|
+
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
3401
|
+
const itemIndex = itemChange.path[itemChange.path.length - 1];
|
|
3402
|
+
const isInsert = itemChange.type === "listItemInsert";
|
|
3403
|
+
const location = getListItemLocation(itemChange.path, itemIndex, listIndex - 1);
|
|
3404
|
+
const preview = getListItemPreview(itemChange.node);
|
|
3405
|
+
const markedNode = isInsert ? markAllTextAsInserted2(cloneNode3(itemChange.node), sharedId, author) : markAllTextAsDeleted2(cloneNode3(itemChange.node), sharedId, author);
|
|
3406
|
+
changes.push({
|
|
3407
|
+
...itemChange,
|
|
3408
|
+
node: markedNode
|
|
3409
|
+
});
|
|
3410
|
+
infos.push({
|
|
3411
|
+
id: sharedId,
|
|
3412
|
+
type: itemChange.type,
|
|
3413
|
+
nodeType: "listItem",
|
|
3414
|
+
location,
|
|
3415
|
+
preview,
|
|
3416
|
+
author,
|
|
3417
|
+
date
|
|
3418
|
+
});
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
const imageChanges = diffImages(docA, docB);
|
|
3423
|
+
for (const imgInsert of imageChanges.inserted) {
|
|
3424
|
+
const sharedId = imgInsert.id;
|
|
3425
|
+
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
3426
|
+
infos.push({
|
|
3427
|
+
id: sharedId,
|
|
3428
|
+
type: "imageInsert",
|
|
3429
|
+
nodeType: "image",
|
|
3430
|
+
location: getImageLocation(imgInsert.path),
|
|
3431
|
+
preview: getImagePreview(imgInsert.node),
|
|
3432
|
+
author,
|
|
3433
|
+
date
|
|
3434
|
+
});
|
|
3435
|
+
changes.push(imgInsert);
|
|
3436
|
+
}
|
|
3437
|
+
for (const imgDelete of imageChanges.deleted) {
|
|
3438
|
+
const sharedId = imgDelete.id;
|
|
3439
|
+
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
3440
|
+
infos.push({
|
|
3441
|
+
id: sharedId,
|
|
3442
|
+
type: "imageDelete",
|
|
3443
|
+
nodeType: "image",
|
|
3444
|
+
location: getImageLocation(imgDelete.path),
|
|
3445
|
+
preview: getImagePreview(imgDelete.node),
|
|
3446
|
+
author,
|
|
3447
|
+
date
|
|
3448
|
+
});
|
|
3449
|
+
changes.push(imgDelete);
|
|
3450
|
+
}
|
|
3451
|
+
return { changes, infos };
|
|
3452
|
+
}
|
|
3453
|
+
function generateStructuralChangeSummary(infos) {
|
|
3454
|
+
const summary = [];
|
|
3455
|
+
const rowInserts = infos.filter((i) => i.type === "rowInsert").length;
|
|
3456
|
+
const rowDeletes = infos.filter((i) => i.type === "rowDelete").length;
|
|
3457
|
+
const paragraphInserts = infos.filter((i) => i.type === "paragraphInsert").length;
|
|
3458
|
+
const paragraphDeletes = infos.filter((i) => i.type === "paragraphDelete").length;
|
|
3459
|
+
const listItemInserts = infos.filter((i) => i.type === "listItemInsert").length;
|
|
3460
|
+
const listItemDeletes = infos.filter((i) => i.type === "listItemDelete").length;
|
|
3461
|
+
const imageInserts = infos.filter((i) => i.type === "imageInsert").length;
|
|
3462
|
+
const imageDeletes = infos.filter((i) => i.type === "imageDelete").length;
|
|
3463
|
+
if (rowInserts > 0) summary.push(`${rowInserts} row(s) inserted`);
|
|
3464
|
+
if (rowDeletes > 0) summary.push(`${rowDeletes} row(s) deleted`);
|
|
3465
|
+
if (paragraphInserts > 0) summary.push(`${paragraphInserts} paragraph(s) inserted`);
|
|
3466
|
+
if (paragraphDeletes > 0) summary.push(`${paragraphDeletes} paragraph(s) deleted`);
|
|
3467
|
+
if (listItemInserts > 0) summary.push(`${listItemInserts} list item(s) inserted`);
|
|
3468
|
+
if (listItemDeletes > 0) summary.push(`${listItemDeletes} list item(s) deleted`);
|
|
3469
|
+
if (imageInserts > 0) summary.push(`${imageInserts} image(s) inserted`);
|
|
3470
|
+
if (imageDeletes > 0) summary.push(`${imageDeletes} image(s) deleted`);
|
|
3471
|
+
return summary;
|
|
3472
|
+
}
|
|
3036
3473
|
|
|
3037
3474
|
// src/blankTemplate.ts
|
|
3038
3475
|
var BLANK_DOCX_BASE64 = `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`;
|