@ansonlai/docx-redline-js 0.8.1 → 0.8.2
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/CHANGELOG.md +14 -0
- package/dist/docx-redline-js.esm.js +127 -6
- package/dist/docx-redline-js.esm.js.map +3 -3
- package/dist/docx-redline-js.esm.min.js +74 -74
- package/dist/docx-redline-js.esm.min.js.map +3 -3
- package/dist/docx-redline.bundle.cjs +127 -6
- package/dist/docx-redline.bundle.cjs.map +3 -3
- package/dist/docx-redline.bundle.js +127 -6
- package/dist/docx-redline.bundle.js.map +3 -3
- package/engine/reconstruction-mode.js +183 -54
- package/package.json +1 -1
- package/pipeline/diff-engine.js +274 -258
- package/scripts/run-tests.mjs +14 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Replacement next to a hyperlink no longer pulls adjacent plain text into the link** ([#3](https://github.com/AnsonLai/docx-redline-js/issues/3)):
|
|
8
|
+
replacing `example.org` with `example.net` (localized `replacements` or full `modified`) in a hyperlink followed by a
|
|
9
|
+
plain `.` run produced a link showing `example.net.` after Accept All. The reconstruction-mode word diff treated
|
|
10
|
+
`example.org.` as one token spanning the hyperlink edge. Where a replacement's shared prefix or suffix reaches a wrapper
|
|
11
|
+
boundary inside the deleted text, it is now kept unchanged.
|
|
12
|
+
- **Reject All no longer reorders text around a manual line break** ([#4](https://github.com/AnsonLai/docx-redline-js/issues/4)):
|
|
13
|
+
replacing `tabbed` with `aligned` in `<tab>tabbed<w:br/>Line with ` rejected to `<tab>tabbedLine<break> with `, moving
|
|
14
|
+
deleted text across the break. Break and note-reference placeholders are now standalone word-diff tokens (new internal
|
|
15
|
+
`atomicChars` option), and change groups are split at references present on both sides. Covered by `tests/reconstruction_boundary_fidelity_tests.mjs`.
|
|
16
|
+
|
|
17
|
+
## 0.8.1
|
|
18
|
+
|
|
5
19
|
### Added
|
|
6
20
|
|
|
7
21
|
- **Header and footer editing:** add `part` to an operation (a selector `{ kind, type?, section? }` or a part path) to edit a
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @ansonlai/docx-redline-js v0.8.
|
|
1
|
+
// @ansonlai/docx-redline-js v0.8.2 — https://github.com/AnsonLai/docx-redline-js
|
|
2
2
|
var __create = Object.create;
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -2522,15 +2522,29 @@ function createDiffEngine(options = {}) {
|
|
|
2522
2522
|
engine.Diff_Timeout = timeout;
|
|
2523
2523
|
return engine;
|
|
2524
2524
|
}
|
|
2525
|
-
function tokenize(text2) {
|
|
2525
|
+
function tokenize(text2, atomicChars = null) {
|
|
2526
2526
|
const tokens = [];
|
|
2527
|
+
const pushWord = (word) => {
|
|
2528
|
+
if (!atomicChars || atomicChars.size === 0) {
|
|
2529
|
+
tokens.push(word);
|
|
2530
|
+
return;
|
|
2531
|
+
}
|
|
2532
|
+
let start = 0;
|
|
2533
|
+
for (let index = 0; index < word.length; index++) {
|
|
2534
|
+
if (!atomicChars.has(word[index])) continue;
|
|
2535
|
+
if (index > start) tokens.push(word.slice(start, index));
|
|
2536
|
+
tokens.push(word[index]);
|
|
2537
|
+
start = index + 1;
|
|
2538
|
+
}
|
|
2539
|
+
if (start < word.length) tokens.push(word.slice(start));
|
|
2540
|
+
};
|
|
2527
2541
|
const leading = text2.match(/^\s+/);
|
|
2528
2542
|
if (leading) tokens.push(leading[0]);
|
|
2529
2543
|
const regex = /(\S+)(\s*)/g;
|
|
2530
2544
|
regex.lastIndex = leading?.[0].length || 0;
|
|
2531
2545
|
let match;
|
|
2532
2546
|
while ((match = regex.exec(text2)) !== null) {
|
|
2533
|
-
if (match[1])
|
|
2547
|
+
if (match[1]) pushWord(match[1]);
|
|
2534
2548
|
if (match[2]) tokens.push(match[2]);
|
|
2535
2549
|
}
|
|
2536
2550
|
return tokens;
|
|
@@ -2558,8 +2572,8 @@ function wordsToChars(text1, text2, options = {}) {
|
|
|
2558
2572
|
}
|
|
2559
2573
|
return { chars, tokenIds };
|
|
2560
2574
|
}
|
|
2561
|
-
const tokens1 = tokenize(text1);
|
|
2562
|
-
const tokens2 = tokenize(text2);
|
|
2575
|
+
const tokens1 = tokenize(text1, options.atomicChars);
|
|
2576
|
+
const tokens2 = tokenize(text2, options.atomicChars);
|
|
2563
2577
|
const encoded1 = mapTokensToChars(tokens1);
|
|
2564
2578
|
const encoded2 = mapTokensToChars(tokens2);
|
|
2565
2579
|
return {
|
|
@@ -7964,7 +7978,11 @@ function applyReconstructionMode(xmlDoc, originalText, modifiedText, serializer,
|
|
|
7964
7978
|
if (mapping.paragraphs.length === 0) {
|
|
7965
7979
|
return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: false });
|
|
7966
7980
|
}
|
|
7967
|
-
const
|
|
7981
|
+
const wordDiffs = computeWordDiffs(mapping.originalFullText, mapping.processedModifiedText, {
|
|
7982
|
+
...diffOptions,
|
|
7983
|
+
atomicChars: new Set(mapping.referenceMap.keys())
|
|
7984
|
+
});
|
|
7985
|
+
const diffs = keepUnchangedTextOutsideWrappers(anchorSharedReferences(wordDiffs, mapping.referenceMap), mapping);
|
|
7968
7986
|
return withOoxmlSourceType(applyReconstructionDiffs(
|
|
7969
7987
|
xmlDoc,
|
|
7970
7988
|
diffs,
|
|
@@ -7976,6 +7994,109 @@ function applyReconstructionMode(xmlDoc, originalText, modifiedText, serializer,
|
|
|
7976
7994
|
options
|
|
7977
7995
|
));
|
|
7978
7996
|
}
|
|
7997
|
+
function forEachChangeGroup(diffs, onEqual, onGroup) {
|
|
7998
|
+
let index = 0;
|
|
7999
|
+
while (index < diffs.length) {
|
|
8000
|
+
if (diffs[index][0] === 0) {
|
|
8001
|
+
onEqual(diffs[index]);
|
|
8002
|
+
index++;
|
|
8003
|
+
continue;
|
|
8004
|
+
}
|
|
8005
|
+
const group = [];
|
|
8006
|
+
while (index < diffs.length && diffs[index][0] !== 0) group.push(diffs[index++]);
|
|
8007
|
+
onGroup(
|
|
8008
|
+
group,
|
|
8009
|
+
group.filter(([op]) => op === -1).map(([, text2]) => text2).join(""),
|
|
8010
|
+
group.filter(([op]) => op === 1).map(([, text2]) => text2).join("")
|
|
8011
|
+
);
|
|
8012
|
+
}
|
|
8013
|
+
}
|
|
8014
|
+
function pushChange(result, deleted, inserted) {
|
|
8015
|
+
if (deleted === inserted) {
|
|
8016
|
+
if (deleted) result.push([0, deleted]);
|
|
8017
|
+
return;
|
|
8018
|
+
}
|
|
8019
|
+
if (deleted) result.push([-1, deleted]);
|
|
8020
|
+
if (inserted) result.push([1, inserted]);
|
|
8021
|
+
}
|
|
8022
|
+
function anchorSharedReferences(diffs, referenceMap) {
|
|
8023
|
+
if (referenceMap.size === 0) return diffs;
|
|
8024
|
+
const result = [];
|
|
8025
|
+
forEachChangeGroup(diffs, (part) => result.push(part), (group, deleted, inserted) => {
|
|
8026
|
+
const deletedRefs = [...deleted].filter((char) => referenceMap.has(char) && inserted.includes(char));
|
|
8027
|
+
const insertedRefs = [...inserted].filter((char) => deletedRefs.includes(char));
|
|
8028
|
+
if (deletedRefs.length === 0 || deletedRefs.join("") !== insertedRefs.join("")) {
|
|
8029
|
+
result.push(...group);
|
|
8030
|
+
return;
|
|
8031
|
+
}
|
|
8032
|
+
let deletedStart = 0;
|
|
8033
|
+
let insertedStart = 0;
|
|
8034
|
+
for (const ref of deletedRefs) {
|
|
8035
|
+
const deletedAt = deleted.indexOf(ref, deletedStart);
|
|
8036
|
+
const insertedAt = inserted.indexOf(ref, insertedStart);
|
|
8037
|
+
pushChange(result, deleted.slice(deletedStart, deletedAt), inserted.slice(insertedStart, insertedAt));
|
|
8038
|
+
result.push([0, ref]);
|
|
8039
|
+
deletedStart = deletedAt + 1;
|
|
8040
|
+
insertedStart = insertedAt + 1;
|
|
8041
|
+
}
|
|
8042
|
+
pushChange(result, deleted.slice(deletedStart), inserted.slice(insertedStart));
|
|
8043
|
+
});
|
|
8044
|
+
return result;
|
|
8045
|
+
}
|
|
8046
|
+
function commonPrefixLength(left, right) {
|
|
8047
|
+
const limit = Math.min(left.length, right.length);
|
|
8048
|
+
let length = 0;
|
|
8049
|
+
while (length < limit && left[length] === right[length]) length++;
|
|
8050
|
+
return length;
|
|
8051
|
+
}
|
|
8052
|
+
function commonSuffixLength(left, right, limit) {
|
|
8053
|
+
let length = 0;
|
|
8054
|
+
while (length < limit && left[left.length - 1 - length] === right[right.length - 1 - length]) length++;
|
|
8055
|
+
return length;
|
|
8056
|
+
}
|
|
8057
|
+
function keepUnchangedTextOutsideWrappers(diffs, mapping) {
|
|
8058
|
+
const wrapperAt = (index) => mapping.getRunProperties(index).wrapper || null;
|
|
8059
|
+
const result = [];
|
|
8060
|
+
let originalOffset = 0;
|
|
8061
|
+
forEachChangeGroup(diffs, (part) => {
|
|
8062
|
+
result.push(part);
|
|
8063
|
+
originalOffset += part[1].length;
|
|
8064
|
+
}, (group, deleted, inserted) => {
|
|
8065
|
+
const start = originalOffset;
|
|
8066
|
+
const end = start + deleted.length;
|
|
8067
|
+
originalOffset = end;
|
|
8068
|
+
const isWrapperBoundary = (index) => index > start && index < end && wrapperAt(index - 1) !== wrapperAt(index);
|
|
8069
|
+
let prefix = 0;
|
|
8070
|
+
let suffix = 0;
|
|
8071
|
+
if (deleted && inserted) {
|
|
8072
|
+
const sharedSuffix = commonSuffixLength(deleted, inserted, Math.min(deleted.length, inserted.length));
|
|
8073
|
+
for (let length = sharedSuffix; length > 0; length--) {
|
|
8074
|
+
if (isWrapperBoundary(end - length)) {
|
|
8075
|
+
suffix = length;
|
|
8076
|
+
break;
|
|
8077
|
+
}
|
|
8078
|
+
}
|
|
8079
|
+
const sharedPrefix = Math.min(
|
|
8080
|
+
commonPrefixLength(deleted, inserted),
|
|
8081
|
+
Math.min(deleted.length, inserted.length) - suffix
|
|
8082
|
+
);
|
|
8083
|
+
for (let length = sharedPrefix; length > 0; length--) {
|
|
8084
|
+
if (isWrapperBoundary(start + length)) {
|
|
8085
|
+
prefix = length;
|
|
8086
|
+
break;
|
|
8087
|
+
}
|
|
8088
|
+
}
|
|
8089
|
+
}
|
|
8090
|
+
if (prefix === 0 && suffix === 0) {
|
|
8091
|
+
result.push(...group);
|
|
8092
|
+
return;
|
|
8093
|
+
}
|
|
8094
|
+
if (prefix > 0) result.push([0, deleted.slice(0, prefix)]);
|
|
8095
|
+
pushChange(result, deleted.slice(prefix, deleted.length - suffix), inserted.slice(prefix, inserted.length - suffix));
|
|
8096
|
+
if (suffix > 0) result.push([0, deleted.slice(deleted.length - suffix)]);
|
|
8097
|
+
});
|
|
8098
|
+
return result;
|
|
8099
|
+
}
|
|
7979
8100
|
|
|
7980
8101
|
// engine/table-mode.js
|
|
7981
8102
|
function noChanges(serializer, xmlDoc) {
|