@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 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 — https://github.com/AnsonLai/docx-redline-js
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]) tokens.push(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 diffs = computeWordDiffs(mapping.originalFullText, mapping.processedModifiedText, diffOptions);
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) {