@ansonlai/docx-redline-js 0.5.0 → 0.5.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/AGENTS.md +76 -15
- package/CHANGELOG.md +24 -1
- package/README.md +43 -11
- package/core/redline-validation.js +11 -5
- package/dist/docx-redline-js.esm.js +363 -55
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +77 -74
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/docs/TESTING.md +18 -0
- package/docs/plans/2026-09-08-cross-author-revision-slicing.md +575 -0
- package/docs/schemas/document-operations.schema.json +1 -1
- package/engine/oxml-engine.js +39 -16
- package/engine/surgical-diff-application.js +203 -19
- package/engine/surgical-mode.js +55 -10
- package/engine/surgical-run-splitting.js +103 -0
- package/index.d.ts +1 -1
- package/node/cli.js +12 -7
- package/package.json +123 -123
- package/pipeline/diff-engine.js +22 -0
- package/scripts/generate-cross-author-slicing-fixtures.ps1 +256 -0
- package/services/batch-operation-orchestrator.js +24 -5
- package/services/document-operation-contract.js +22 -0
- package/services/document-operation-mutations.js +19 -3
- package/services/operation-preflight.js +32 -8
- package/services/revision-comment-management.js +40 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @ansonlai/docx-redline-js v0.5.
|
|
1
|
+
// @ansonlai/docx-redline-js v0.5.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;
|
|
@@ -2634,6 +2634,17 @@ function computeWordDiffs(originalText, newText, options = {}) {
|
|
|
2634
2634
|
}
|
|
2635
2635
|
return decodeBmpDiffs(charDiffs, wordArray);
|
|
2636
2636
|
}
|
|
2637
|
+
function computeInsertionOnlyDiffs(originalText, newText) {
|
|
2638
|
+
if (originalText === newText) return [[0, originalText]];
|
|
2639
|
+
if (!originalText) return [[1, newText]];
|
|
2640
|
+
let originalIndex = 0;
|
|
2641
|
+
for (let modifiedIndex = 0; modifiedIndex < newText.length && originalIndex < originalText.length; modifiedIndex++) {
|
|
2642
|
+
if (newText[modifiedIndex] === originalText[originalIndex]) originalIndex++;
|
|
2643
|
+
}
|
|
2644
|
+
if (originalIndex !== originalText.length) return null;
|
|
2645
|
+
const diffs = createDiffEngine().diff_main(originalText, newText);
|
|
2646
|
+
return diffs.some(([op]) => op === -1) ? null : diffs;
|
|
2647
|
+
}
|
|
2637
2648
|
function computeWordLevelDiffOps(originalText, newText, options = {}) {
|
|
2638
2649
|
if (originalText === newText) {
|
|
2639
2650
|
return [{
|
|
@@ -5879,7 +5890,26 @@ function describeInsertionBoundary(spanIndex, pos, fallbackParagraph = null) {
|
|
|
5879
5890
|
};
|
|
5880
5891
|
}
|
|
5881
5892
|
|
|
5893
|
+
// core/revision-cloning.js
|
|
5894
|
+
function refreshRunPropertyChangeIds(root, allocator = null) {
|
|
5895
|
+
if (!root) return root;
|
|
5896
|
+
const xmlDoc = root.nodeType === 9 ? root : root.ownerDocument;
|
|
5897
|
+
const resolvedAllocator = allocator instanceof RevisionIdAllocator ? allocator : getRevisionIdAllocatorForDocument(xmlDoc) || createRevisionIdAllocator(xmlDoc);
|
|
5898
|
+
const candidates = [root, ...Array.from(root.getElementsByTagName?.("*") || [])];
|
|
5899
|
+
for (const node of candidates) {
|
|
5900
|
+
if (!isWordElement(node, "rPrChange")) continue;
|
|
5901
|
+
const nextId = String(resolvedAllocator.next());
|
|
5902
|
+
if (typeof node.setAttributeNS === "function") {
|
|
5903
|
+
node.setAttributeNS(NS_W, "w:id", nextId);
|
|
5904
|
+
} else {
|
|
5905
|
+
node.setAttribute("w:id", nextId);
|
|
5906
|
+
}
|
|
5907
|
+
}
|
|
5908
|
+
return root;
|
|
5909
|
+
}
|
|
5910
|
+
|
|
5882
5911
|
// engine/surgical-run-splitting.js
|
|
5912
|
+
var TRACK_CHANGE_CARRIERS = /* @__PURE__ */ new Set(["ins"]);
|
|
5883
5913
|
function getRunContentPieces(runElement) {
|
|
5884
5914
|
const pieces = [];
|
|
5885
5915
|
let offset = 0;
|
|
@@ -5928,6 +5958,74 @@ function insertRunPiecesBefore(xmlDoc, parent, referenceNode, pieces, rPr) {
|
|
|
5928
5958
|
parent.insertBefore(run, referenceNode);
|
|
5929
5959
|
return run;
|
|
5930
5960
|
}
|
|
5961
|
+
function splitTrackChangeCarrier(xmlDoc, carrierElement, splitOffset, allocator = null) {
|
|
5962
|
+
const carrierName = getLocalName(carrierElement);
|
|
5963
|
+
if (!TRACK_CHANGE_CARRIERS.has(carrierName)) {
|
|
5964
|
+
throw new TypeError("splitTrackChangeCarrier requires a w:ins carrier.");
|
|
5965
|
+
}
|
|
5966
|
+
if (!Number.isInteger(splitOffset) || splitOffset < 0) {
|
|
5967
|
+
throw new RangeError("splitOffset must be a non-negative integer.");
|
|
5968
|
+
}
|
|
5969
|
+
const children = Array.from(carrierElement.childNodes || []);
|
|
5970
|
+
const totalLength = children.reduce((length, child) => {
|
|
5971
|
+
return length + (isWordElement(child, "r") ? getRunTextLength(getRunContentPieces(child)) : 0);
|
|
5972
|
+
}, 0);
|
|
5973
|
+
if (splitOffset > totalLength) {
|
|
5974
|
+
throw new RangeError(`splitOffset ${splitOffset} exceeds carrier text length ${totalLength}.`);
|
|
5975
|
+
}
|
|
5976
|
+
if (splitOffset === 0) {
|
|
5977
|
+
return { leftCarrier: null, rightCarrier: carrierElement.cloneNode(true) };
|
|
5978
|
+
}
|
|
5979
|
+
if (splitOffset === totalLength) {
|
|
5980
|
+
return { leftCarrier: carrierElement.cloneNode(true), rightCarrier: null };
|
|
5981
|
+
}
|
|
5982
|
+
const leftCarrier = carrierElement.cloneNode(false);
|
|
5983
|
+
const rightCarrier = carrierElement.cloneNode(false);
|
|
5984
|
+
let offset = 0;
|
|
5985
|
+
for (const child of children) {
|
|
5986
|
+
if (!isWordElement(child, "r")) {
|
|
5987
|
+
const destination = offset <= splitOffset ? leftCarrier : rightCarrier;
|
|
5988
|
+
destination.appendChild(child.cloneNode(true));
|
|
5989
|
+
continue;
|
|
5990
|
+
}
|
|
5991
|
+
const pieces = getRunContentPieces(child);
|
|
5992
|
+
const runLength = getRunTextLength(pieces);
|
|
5993
|
+
const runEnd = offset + runLength;
|
|
5994
|
+
if (runEnd <= splitOffset) {
|
|
5995
|
+
leftCarrier.appendChild(child.cloneNode(true));
|
|
5996
|
+
} else if (offset >= splitOffset) {
|
|
5997
|
+
rightCarrier.appendChild(child.cloneNode(true));
|
|
5998
|
+
} else {
|
|
5999
|
+
const localOffset = splitOffset - offset;
|
|
6000
|
+
const rPr = Array.from(child.childNodes || []).find((node) => isWordElement(node, "rPr")) || null;
|
|
6001
|
+
const leftPieces = sliceRunPieces(xmlDoc, pieces, 0, localOffset, false);
|
|
6002
|
+
const rightPieces = sliceRunPieces(xmlDoc, pieces, localOffset, runLength, false);
|
|
6003
|
+
leftCarrier.appendChild(createRunFromPieces(xmlDoc, leftPieces, rPr));
|
|
6004
|
+
const rightRun = createRunFromPieces(xmlDoc, rightPieces, rPr);
|
|
6005
|
+
refreshRunPropertyChangeIds(rightRun, resolveAllocator(xmlDoc, allocator));
|
|
6006
|
+
rightCarrier.appendChild(rightRun);
|
|
6007
|
+
}
|
|
6008
|
+
offset = runEnd;
|
|
6009
|
+
}
|
|
6010
|
+
const resolvedAllocator = resolveAllocator(xmlDoc, allocator);
|
|
6011
|
+
const nextId = resolvedAllocator.next();
|
|
6012
|
+
setWordAttribute(rightCarrier, "id", String(nextId));
|
|
6013
|
+
resolvedAllocator._receiptCollector?.recordRevision(nextId, carrierName);
|
|
6014
|
+
return { leftCarrier, rightCarrier };
|
|
6015
|
+
}
|
|
6016
|
+
function resolveAllocator(xmlDoc, allocator) {
|
|
6017
|
+
return allocator instanceof RevisionIdAllocator ? allocator : getRevisionIdAllocatorForDocument(xmlDoc) || createRevisionIdAllocator(xmlDoc);
|
|
6018
|
+
}
|
|
6019
|
+
function setWordAttribute(element, localName2, value) {
|
|
6020
|
+
if (typeof element.setAttributeNS === "function") {
|
|
6021
|
+
element.setAttributeNS(NS_W, `w:${localName2}`, value);
|
|
6022
|
+
} else {
|
|
6023
|
+
element.setAttribute(`w:${localName2}`, value);
|
|
6024
|
+
}
|
|
6025
|
+
}
|
|
6026
|
+
function getLocalName(element) {
|
|
6027
|
+
return String(element?.localName || element?.nodeName || "").replace(/^.*:/, "");
|
|
6028
|
+
}
|
|
5931
6029
|
function cloneRunPiece(xmlDoc, sourceNode, text, asDeletedText) {
|
|
5932
6030
|
if (asDeletedText) {
|
|
5933
6031
|
const delText = createWordElement(xmlDoc, "w:delText");
|
|
@@ -6003,8 +6101,7 @@ function processDelete(xmlDoc, spanIndex, startPos, endPos, author, generateRedl
|
|
|
6003
6101
|
if (!spansByRun.has(span.runElement)) spansByRun.set(span.runElement, []);
|
|
6004
6102
|
spansByRun.get(span.runElement).push(span);
|
|
6005
6103
|
});
|
|
6006
|
-
|
|
6007
|
-
let usedDelMetadata = false;
|
|
6104
|
+
const records = [];
|
|
6008
6105
|
spansByRun.forEach((runSpans, runElement) => {
|
|
6009
6106
|
const parent = runElement.parentNode;
|
|
6010
6107
|
if (!parent) return;
|
|
@@ -6022,24 +6119,84 @@ function processDelete(xmlDoc, spanIndex, startPos, endPos, author, generateRedl
|
|
|
6022
6119
|
deleteEnd = Math.max(deleteEnd, piece.start + spanDeleteEnd);
|
|
6023
6120
|
});
|
|
6024
6121
|
if (!Number.isFinite(deleteStart) || deleteEnd <= deleteStart) return;
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6122
|
+
records.push({
|
|
6123
|
+
runElement,
|
|
6124
|
+
parent,
|
|
6125
|
+
rPr: runSpans[0].rPr,
|
|
6126
|
+
beforePieces: sliceRunPieces(xmlDoc, pieces, 0, deleteStart, false),
|
|
6127
|
+
deletedPieces: sliceRunPieces(xmlDoc, pieces, deleteStart, deleteEnd, true),
|
|
6128
|
+
afterPieces: sliceRunPieces(xmlDoc, pieces, deleteEnd, getRunTextLength(pieces), false),
|
|
6129
|
+
globalStart: Math.max(startPos, Math.min(...runSpans.map((span) => span.charStart))),
|
|
6130
|
+
globalEnd: Math.min(endPos, Math.max(...runSpans.map((span) => span.charEnd))),
|
|
6131
|
+
carrierGlobalStart: isWordElement(parent, "ins") ? getCarrierGlobalStart(spanIndex, parent) : null
|
|
6132
|
+
});
|
|
6133
|
+
});
|
|
6134
|
+
const groups = [];
|
|
6135
|
+
for (const record of records) {
|
|
6136
|
+
const previousGroup = groups[groups.length - 1];
|
|
6137
|
+
const previousRecord = previousGroup?.[previousGroup.length - 1];
|
|
6138
|
+
if (previousRecord && previousRecord.parent === record.parent && nextElementSibling(previousRecord.runElement) === record.runElement) {
|
|
6139
|
+
previousGroup.push(record);
|
|
6140
|
+
} else {
|
|
6141
|
+
groups.push([record]);
|
|
6142
|
+
}
|
|
6143
|
+
}
|
|
6144
|
+
let changed = false;
|
|
6145
|
+
let usedDelMetadata = false;
|
|
6146
|
+
for (const group of groups) {
|
|
6147
|
+
const firstRecord = group[0];
|
|
6148
|
+
let delWrapper = null;
|
|
6149
|
+
if (generateRedlines && group.some((record) => record.deletedPieces.length > 0)) {
|
|
6150
|
+
const metadata = revisionMetadata ? usedDelMetadata ? { ...revisionMetadata, id: createRevisionMetadata(author, xmlDoc, "del").id } : revisionMetadata : null;
|
|
6032
6151
|
usedDelMetadata = true;
|
|
6033
|
-
|
|
6034
|
-
|
|
6152
|
+
delWrapper = createTrackChange(xmlDoc, "del", null, author, metadata);
|
|
6153
|
+
}
|
|
6154
|
+
for (const record of group) {
|
|
6155
|
+
const { parent, runElement } = record;
|
|
6156
|
+
insertRunPiecesBefore(xmlDoc, parent, runElement, record.beforePieces, record.rPr);
|
|
6157
|
+
if (delWrapper && record === firstRecord) {
|
|
6158
|
+
parent.insertBefore(delWrapper, runElement);
|
|
6159
|
+
}
|
|
6160
|
+
if (delWrapper && record.deletedPieces.length > 0) {
|
|
6161
|
+
delWrapper.appendChild(createRunFromPieces(xmlDoc, record.deletedPieces, record.rPr));
|
|
6162
|
+
}
|
|
6163
|
+
insertRunPiecesBefore(xmlDoc, parent, runElement, record.afterPieces, record.rPr);
|
|
6164
|
+
parent.removeChild(runElement);
|
|
6165
|
+
changed = true;
|
|
6166
|
+
}
|
|
6167
|
+
const carrier = isWordElement(firstRecord.parent, "ins") ? firstRecord.parent : null;
|
|
6168
|
+
const groupEnd = Math.max(...group.map((record) => record.globalEnd));
|
|
6169
|
+
if (carrier && groupEnd === endPos) {
|
|
6170
|
+
const carrierStart = firstRecord.carrierGlobalStart;
|
|
6171
|
+
const deletedBeforeEnd = group.filter((record) => record.globalStart < endPos).reduce((sum, record) => sum + record.deletedPieces.reduce((n, piece) => n + (piece.textContent || "").length, 0), 0);
|
|
6172
|
+
const currentOffset = Math.max(0, endPos - carrierStart - deletedBeforeEnd);
|
|
6173
|
+
if (!spanIndex.revisionInsertionAnchors) spanIndex.revisionInsertionAnchors = /* @__PURE__ */ new Map();
|
|
6174
|
+
spanIndex.revisionInsertionAnchors.set(endPos, {
|
|
6175
|
+
carrier,
|
|
6176
|
+
splitOffset: currentOffset,
|
|
6177
|
+
rPr: firstRecord.rPr
|
|
6178
|
+
});
|
|
6035
6179
|
}
|
|
6036
|
-
|
|
6037
|
-
parent.removeChild(runElement);
|
|
6038
|
-
changed = true;
|
|
6039
|
-
});
|
|
6180
|
+
}
|
|
6040
6181
|
return changed;
|
|
6041
6182
|
}
|
|
6042
|
-
function processInsert(xmlDoc, spanIndex, pos, text, author, formatHints = [], insertOffset = 0, generateRedlines = true, fallbackParagraph = null, revisionMetadata = null, affinity = null) {
|
|
6183
|
+
function processInsert(xmlDoc, spanIndex, pos, text, author, formatHints = [], insertOffset = 0, generateRedlines = true, fallbackParagraph = null, revisionMetadata = null, affinity = null, existingRevisions = "merge-same-author") {
|
|
6184
|
+
const mutationAnchor = spanIndex.revisionInsertionAnchors?.get(pos) || null;
|
|
6185
|
+
if (mutationAnchor && existingRevisions === "slice-cross-author" && isConnected(mutationAnchor.carrier) && isForeignInsertion(mutationAnchor.carrier, author)) {
|
|
6186
|
+
spanIndex.revisionInsertionAnchors.delete(pos);
|
|
6187
|
+
return spliceInsertionAtCarrierOffset(
|
|
6188
|
+
xmlDoc,
|
|
6189
|
+
mutationAnchor.carrier,
|
|
6190
|
+
mutationAnchor.splitOffset,
|
|
6191
|
+
text,
|
|
6192
|
+
mutationAnchor.rPr,
|
|
6193
|
+
author,
|
|
6194
|
+
formatHints,
|
|
6195
|
+
insertOffset,
|
|
6196
|
+
generateRedlines,
|
|
6197
|
+
revisionMetadata
|
|
6198
|
+
);
|
|
6199
|
+
}
|
|
6043
6200
|
if (!affinity) {
|
|
6044
6201
|
let targetSpan = findContainingSpan(spanIndex, pos);
|
|
6045
6202
|
if (!targetSpan && pos > 0) {
|
|
@@ -6062,6 +6219,21 @@ function processInsert(xmlDoc, spanIndex, pos, text, author, formatHints = [], i
|
|
|
6062
6219
|
insertTextRuns(xmlDoc, fallbackParagraph, null, text, targetSpan.rPr, author, formatHints, insertOffset, generateRedlines, revisionMetadata);
|
|
6063
6220
|
return true;
|
|
6064
6221
|
}
|
|
6222
|
+
const generateNestedRevision = !(generateRedlines && existingRevisions === "slice-cross-author" && isSameAuthorInsertion(parent2, author));
|
|
6223
|
+
if (generateRedlines && existingRevisions === "slice-cross-author" && isForeignInsertion(parent2, author)) {
|
|
6224
|
+
return spliceInsertionAtCarrierOffset(
|
|
6225
|
+
xmlDoc,
|
|
6226
|
+
parent2,
|
|
6227
|
+
getCarrierSplitOffset(spanIndex, parent2, pos),
|
|
6228
|
+
text,
|
|
6229
|
+
targetSpan.rPr,
|
|
6230
|
+
author,
|
|
6231
|
+
formatHints,
|
|
6232
|
+
insertOffset,
|
|
6233
|
+
generateRedlines,
|
|
6234
|
+
revisionMetadata
|
|
6235
|
+
);
|
|
6236
|
+
}
|
|
6065
6237
|
const pieces = getRunContentPieces(targetSpan.runElement);
|
|
6066
6238
|
const targetPiece = pieces.find((piece) => piece.node === targetSpan.textElement);
|
|
6067
6239
|
const localInsertPos = targetPiece ? targetPiece.start + Math.max(0, Math.min(pos - targetSpan.charStart, targetSpan.charEnd - targetSpan.charStart)) : pos <= targetSpan.charStart ? 0 : getRunTextLength(pieces);
|
|
@@ -6069,13 +6241,13 @@ function processInsert(xmlDoc, spanIndex, pos, text, author, formatHints = [], i
|
|
|
6069
6241
|
const beforePieces = sliceRunPieces(xmlDoc, pieces, 0, localInsertPos, false);
|
|
6070
6242
|
const afterPieces = sliceRunPieces(xmlDoc, pieces, localInsertPos, getRunTextLength(pieces), false);
|
|
6071
6243
|
insertRunPiecesBefore(xmlDoc, parent2, targetSpan.runElement, beforePieces, targetSpan.rPr);
|
|
6072
|
-
insertTextRuns(xmlDoc, parent2, targetSpan.runElement, text, targetSpan.rPr, author, formatHints, insertOffset,
|
|
6244
|
+
insertTextRuns(xmlDoc, parent2, targetSpan.runElement, text, targetSpan.rPr, author, formatHints, insertOffset, generateNestedRevision, revisionMetadata);
|
|
6073
6245
|
insertRunPiecesBefore(xmlDoc, parent2, targetSpan.runElement, afterPieces, targetSpan.rPr);
|
|
6074
6246
|
parent2.removeChild(targetSpan.runElement);
|
|
6075
6247
|
return true;
|
|
6076
6248
|
}
|
|
6077
6249
|
const referenceNode2 = pos <= targetSpan.charStart ? targetSpan.runElement : targetSpan.runElement.nextSibling;
|
|
6078
|
-
insertTextRuns(xmlDoc, parent2, referenceNode2, text, targetSpan.rPr, author, formatHints, insertOffset,
|
|
6250
|
+
insertTextRuns(xmlDoc, parent2, referenceNode2, text, targetSpan.rPr, author, formatHints, insertOffset, generateNestedRevision, revisionMetadata);
|
|
6079
6251
|
return true;
|
|
6080
6252
|
}
|
|
6081
6253
|
const boundary = describeInsertionBoundary(spanIndex, pos, fallbackParagraph);
|
|
@@ -6224,9 +6396,78 @@ function processInsert(xmlDoc, spanIndex, pos, text, author, formatHints = [], i
|
|
|
6224
6396
|
return true;
|
|
6225
6397
|
}
|
|
6226
6398
|
}
|
|
6399
|
+
if (generateRedlines && existingRevisions === "slice-cross-author" && isForeignInsertion(parent, author)) {
|
|
6400
|
+
return spliceInsertionAtCarrierOffset(
|
|
6401
|
+
xmlDoc,
|
|
6402
|
+
parent,
|
|
6403
|
+
getCarrierSplitOffset(spanIndex, parent, pos),
|
|
6404
|
+
text,
|
|
6405
|
+
baseRPr,
|
|
6406
|
+
author,
|
|
6407
|
+
formatHints,
|
|
6408
|
+
insertOffset,
|
|
6409
|
+
generateRedlines,
|
|
6410
|
+
revisionMetadata
|
|
6411
|
+
);
|
|
6412
|
+
}
|
|
6227
6413
|
insertTextRuns(xmlDoc, parent, referenceNode, text, baseRPr, author, formatHints, insertOffset, generateRedlines, revisionMetadata);
|
|
6228
6414
|
return true;
|
|
6229
6415
|
}
|
|
6416
|
+
function spliceInsertionAtCarrierOffset(xmlDoc, carrier, splitOffset, text, baseRPr, author, formatHints, insertOffset, generateRedlines, revisionMetadata) {
|
|
6417
|
+
const parent = carrier.parentNode;
|
|
6418
|
+
if (!parent) return false;
|
|
6419
|
+
const { leftCarrier, rightCarrier } = splitTrackChangeCarrier(xmlDoc, carrier, splitOffset);
|
|
6420
|
+
if (leftCarrier) parent.insertBefore(leftCarrier, carrier);
|
|
6421
|
+
insertTextRuns(
|
|
6422
|
+
xmlDoc,
|
|
6423
|
+
parent,
|
|
6424
|
+
carrier,
|
|
6425
|
+
text,
|
|
6426
|
+
withoutRunPropertyChanges(baseRPr),
|
|
6427
|
+
author,
|
|
6428
|
+
formatHints,
|
|
6429
|
+
insertOffset,
|
|
6430
|
+
generateRedlines,
|
|
6431
|
+
revisionMetadata
|
|
6432
|
+
);
|
|
6433
|
+
if (rightCarrier) parent.insertBefore(rightCarrier, carrier);
|
|
6434
|
+
parent.removeChild(carrier);
|
|
6435
|
+
return true;
|
|
6436
|
+
}
|
|
6437
|
+
function withoutRunPropertyChanges(rPr) {
|
|
6438
|
+
if (!rPr) return null;
|
|
6439
|
+
const clone = rPr.cloneNode(true);
|
|
6440
|
+
const changes = Array.from(clone.getElementsByTagName?.("*") || []).filter((node) => isWordElement(node, "rPrChange"));
|
|
6441
|
+
changes.forEach((node) => node.parentNode?.removeChild(node));
|
|
6442
|
+
return clone;
|
|
6443
|
+
}
|
|
6444
|
+
function getCarrierSplitOffset(spanIndex, carrier, pos) {
|
|
6445
|
+
const carrierStart = getCarrierGlobalStart(spanIndex, carrier);
|
|
6446
|
+
const carrierLength = spanIndex.spans.filter((span) => span.runElement?.parentNode === carrier).reduce((length, span) => length + (span.charEnd - span.charStart), 0);
|
|
6447
|
+
return Math.max(0, Math.min(pos - carrierStart, carrierLength));
|
|
6448
|
+
}
|
|
6449
|
+
function getCarrierGlobalStart(spanIndex, carrier) {
|
|
6450
|
+
const carrierSpans = spanIndex.spans.filter((span) => span.runElement?.parentNode === carrier);
|
|
6451
|
+
return carrierSpans.length > 0 ? Math.min(...carrierSpans.map((span) => span.charStart)) : 0;
|
|
6452
|
+
}
|
|
6453
|
+
function isForeignInsertion(node, author) {
|
|
6454
|
+
if (!isWordElement(node, "ins")) return false;
|
|
6455
|
+
const carrierAuthor = node.getAttribute("w:author") || node.getAttributeNS?.(NS_W, "author") || "";
|
|
6456
|
+
return carrierAuthor.trim().toLowerCase() !== String(author || "").trim().toLowerCase();
|
|
6457
|
+
}
|
|
6458
|
+
function isSameAuthorInsertion(node, author) {
|
|
6459
|
+
if (!isWordElement(node, "ins")) return false;
|
|
6460
|
+
const carrierAuthor = node.getAttribute("w:author") || node.getAttributeNS?.(NS_W, "author") || "";
|
|
6461
|
+
return carrierAuthor.trim().toLowerCase() === String(author || "").trim().toLowerCase();
|
|
6462
|
+
}
|
|
6463
|
+
function nextElementSibling(node) {
|
|
6464
|
+
let sibling = node?.nextSibling || null;
|
|
6465
|
+
while (sibling && sibling.nodeType !== 1) sibling = sibling.nextSibling;
|
|
6466
|
+
return sibling;
|
|
6467
|
+
}
|
|
6468
|
+
function isConnected(node) {
|
|
6469
|
+
return !!node?.parentNode;
|
|
6470
|
+
}
|
|
6230
6471
|
function insertTextRuns(xmlDoc, parent, referenceNode, text, baseRPr, author, formatHints, insertOffset, generateRedlines, revisionMetadata = null) {
|
|
6231
6472
|
const applicableHints = getApplicableFormatHints(formatHints, insertOffset, insertOffset + text.length);
|
|
6232
6473
|
if (applicableHints.length === 0) {
|
|
@@ -6250,7 +6491,7 @@ function insertTextRuns(xmlDoc, parent, referenceNode, text, baseRPr, author, fo
|
|
|
6250
6491
|
}
|
|
6251
6492
|
|
|
6252
6493
|
// engine/surgical-mode.js
|
|
6253
|
-
function checkSafeAdjacencyForPairing(spanIndex, startPos, endPos) {
|
|
6494
|
+
function checkSafeAdjacencyForPairing(spanIndex, startPos, endPos, allowInsertionCarrier = false) {
|
|
6254
6495
|
const spans = [];
|
|
6255
6496
|
forEachOverlappingSpan(spanIndex, startPos, endPos, (span) => spans.push(span));
|
|
6256
6497
|
if (spans.length === 0) return { safe: false };
|
|
@@ -6260,7 +6501,7 @@ function checkSafeAdjacencyForPairing(spanIndex, startPos, endPos) {
|
|
|
6260
6501
|
const sameParent = spans.every((s) => s.runElement?.parentNode === parent);
|
|
6261
6502
|
if (!sameParent) return { safe: false, structuralBoundary: true };
|
|
6262
6503
|
const parentLocal = parent.localName || parent.nodeName.replace(/^.*:/, "");
|
|
6263
|
-
if (["hyperlink", "sdt", "
|
|
6504
|
+
if (["hyperlink", "sdt", "del", "moveFrom", "moveTo"].includes(parentLocal) || parentLocal === "ins" && !allowInsertionCarrier) {
|
|
6264
6505
|
return { safe: false, structuralBoundary: true };
|
|
6265
6506
|
}
|
|
6266
6507
|
const structuralTags = /* @__PURE__ */ new Set([
|
|
@@ -6317,7 +6558,8 @@ function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer, autho
|
|
|
6317
6558
|
void originalText;
|
|
6318
6559
|
const allParagraphs = targetParagraph ? [targetParagraph] : getDocumentParagraphs(xmlDoc);
|
|
6319
6560
|
const { fullText, textSpans } = buildSurgicalTextSpans(allParagraphs);
|
|
6320
|
-
const
|
|
6561
|
+
const insertionOnlyDiffs = options.existingRevisions === "slice-cross-author" ? computeInsertionOnlyDiffs(fullText, modifiedText) : null;
|
|
6562
|
+
const diffs = insertionOnlyDiffs || computeWordDiffs(fullText, modifiedText, diffOptions);
|
|
6321
6563
|
const spanIndex = buildSpanIndex(textSpans);
|
|
6322
6564
|
const pairReplacements = options.pairReplacements === true;
|
|
6323
6565
|
const warnings = [];
|
|
@@ -6352,8 +6594,13 @@ function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer, autho
|
|
|
6352
6594
|
if (pairReplacements && generateRedlines && hasNextInsert) {
|
|
6353
6595
|
const nextText = diffs[i + 1][1];
|
|
6354
6596
|
const textWithoutNewlines = nextText.replace(/\n/g, " ");
|
|
6355
|
-
if (textWithoutNewlines.
|
|
6356
|
-
const checkResult = checkSafeAdjacencyForPairing(
|
|
6597
|
+
if (textWithoutNewlines.length > 0) {
|
|
6598
|
+
const checkResult = checkSafeAdjacencyForPairing(
|
|
6599
|
+
spanIndex,
|
|
6600
|
+
originalPos,
|
|
6601
|
+
originalPos + text.length,
|
|
6602
|
+
options?.existingRevisions === "slice-cross-author"
|
|
6603
|
+
);
|
|
6357
6604
|
if (checkResult.safe) {
|
|
6358
6605
|
const event = createReplacementRevisionEvent(author, xmlDoc);
|
|
6359
6606
|
delMetadata = { id: event.deletionId, author: event.author, date: event.date };
|
|
@@ -6372,8 +6619,8 @@ function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer, autho
|
|
|
6372
6619
|
i++;
|
|
6373
6620
|
const [, nextText] = diffs[i];
|
|
6374
6621
|
const textWithoutNewlines = nextText.replace(/\n/g, " ");
|
|
6375
|
-
if (textWithoutNewlines.
|
|
6376
|
-
const insertResult = processInsert(xmlDoc, spanIndex, originalPos, textWithoutNewlines, author, formatHints, newPos, generateRedlines, allParagraphs[0] || null, insMetadata, options?.insertionAffinity || null);
|
|
6622
|
+
if (textWithoutNewlines.length > 0) {
|
|
6623
|
+
const insertResult = processInsert(xmlDoc, spanIndex, originalPos, textWithoutNewlines, author, formatHints, newPos, generateRedlines, allParagraphs[0] || null, insMetadata, options?.insertionAffinity || null, options?.existingRevisions || "merge-same-author");
|
|
6377
6624
|
if (insertResult && typeof insertResult === "object" && insertResult.error) {
|
|
6378
6625
|
return withOoxmlSourceType({
|
|
6379
6626
|
oxml: serializer.serializeToString(xmlDoc),
|
|
@@ -6390,8 +6637,8 @@ function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer, autho
|
|
|
6390
6637
|
}
|
|
6391
6638
|
} else if (op === 1) {
|
|
6392
6639
|
const textWithoutNewlines = text.replace(/\n/g, " ");
|
|
6393
|
-
if (textWithoutNewlines.
|
|
6394
|
-
const insertResult = processInsert(xmlDoc, spanIndex, originalPos, textWithoutNewlines, author, formatHints, newPos, generateRedlines, allParagraphs[0] || null, null, options?.insertionAffinity || null);
|
|
6640
|
+
if (textWithoutNewlines.length > 0) {
|
|
6641
|
+
const insertResult = processInsert(xmlDoc, spanIndex, originalPos, textWithoutNewlines, author, formatHints, newPos, generateRedlines, allParagraphs[0] || null, null, options?.insertionAffinity || null, options?.existingRevisions || "merge-same-author");
|
|
6395
6642
|
if (insertResult && typeof insertResult === "object" && insertResult.error) {
|
|
6396
6643
|
return withOoxmlSourceType({
|
|
6397
6644
|
oxml: serializer.serializeToString(xmlDoc),
|
|
@@ -6407,12 +6654,42 @@ function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer, autho
|
|
|
6407
6654
|
newPos += text.length;
|
|
6408
6655
|
}
|
|
6409
6656
|
}
|
|
6657
|
+
const actualText = allParagraphs.map((paragraph) => extractCanonicalParagraphText(paragraph)).join("\n");
|
|
6658
|
+
const expectedText = String(modifiedText).replace(/\r\n/g, "\n");
|
|
6659
|
+
if (options.existingRevisions === "slice-cross-author" && actualText !== expectedText) {
|
|
6660
|
+
const mismatchOffset = firstMismatchOffset(expectedText, actualText);
|
|
6661
|
+
return withOoxmlSourceType({
|
|
6662
|
+
oxml: serializer.serializeToString(xmlDoc),
|
|
6663
|
+
hasChanges: false,
|
|
6664
|
+
status: "error",
|
|
6665
|
+
error: {
|
|
6666
|
+
code: "PATCH_ROUNDTRIP_MISMATCH",
|
|
6667
|
+
message: "Generated OOXML accepted-view text does not match the requested modified text; the mutation was rejected.",
|
|
6668
|
+
mismatchOffset,
|
|
6669
|
+
expectedExcerpt: excerptAt(expectedText, mismatchOffset),
|
|
6670
|
+
actualExcerpt: excerptAt(actualText, mismatchOffset)
|
|
6671
|
+
},
|
|
6672
|
+
...warnings.length > 0 ? { warnings: [...new Set(warnings)] } : {}
|
|
6673
|
+
});
|
|
6674
|
+
}
|
|
6410
6675
|
return withOoxmlSourceType({
|
|
6411
6676
|
oxml: serializer.serializeToString(xmlDoc),
|
|
6412
6677
|
hasChanges,
|
|
6413
6678
|
...warnings.length > 0 ? { warnings: [...new Set(warnings)] } : {}
|
|
6414
6679
|
});
|
|
6415
6680
|
}
|
|
6681
|
+
function firstMismatchOffset(expected, actual) {
|
|
6682
|
+
const limit = Math.min(expected.length, actual.length);
|
|
6683
|
+
for (let index = 0; index < limit; index++) {
|
|
6684
|
+
if (expected[index] !== actual[index]) return index;
|
|
6685
|
+
}
|
|
6686
|
+
return limit;
|
|
6687
|
+
}
|
|
6688
|
+
function excerptAt(text, offset, radius = 40) {
|
|
6689
|
+
const start = Math.max(0, offset - radius);
|
|
6690
|
+
const end = Math.min(text.length, offset + radius);
|
|
6691
|
+
return text.slice(start, end);
|
|
6692
|
+
}
|
|
6416
6693
|
|
|
6417
6694
|
// engine/reconstruction-mapper.js
|
|
6418
6695
|
var import_diff_match_patch2 = __toESM(require_diff_match_patch(), 1);
|
|
@@ -7380,6 +7657,30 @@ function unwrapNode(node) {
|
|
|
7380
7657
|
parent.removeChild(node);
|
|
7381
7658
|
return true;
|
|
7382
7659
|
}
|
|
7660
|
+
function nextElementSibling2(node) {
|
|
7661
|
+
let sibling = node?.nextSibling || null;
|
|
7662
|
+
while (sibling && sibling.nodeType !== 1) sibling = sibling.nextSibling;
|
|
7663
|
+
return sibling;
|
|
7664
|
+
}
|
|
7665
|
+
function revisionMetadataWithoutId(node) {
|
|
7666
|
+
return Array.from(node?.attributes || []).filter((attribute) => String(attribute.localName || attribute.name || "").toLowerCase() !== "id").map((attribute) => `${attribute.namespaceURI || ""}|${attribute.localName || attribute.name}=${attribute.value}`).sort().join("\n");
|
|
7667
|
+
}
|
|
7668
|
+
function coalesceAdjacentCompatibleInsertions(xmlDoc) {
|
|
7669
|
+
let coalesced = 0;
|
|
7670
|
+
const insertions = getWordElementsByLocalName(xmlDoc, "ins");
|
|
7671
|
+
for (const insertion of insertions) {
|
|
7672
|
+
if (!insertion.parentNode || isParagraphMarkRevisionMarker(insertion)) continue;
|
|
7673
|
+
let sibling = nextElementSibling2(insertion);
|
|
7674
|
+
while (isWordElement5(sibling, "ins") && normalizeAuthor(getAttributeByLocalName(sibling, "author")) === normalizeAuthor(getAttributeByLocalName(insertion, "author")) && revisionMetadataWithoutId(sibling) === revisionMetadataWithoutId(insertion)) {
|
|
7675
|
+
const next = nextElementSibling2(sibling);
|
|
7676
|
+
while (sibling.firstChild) insertion.appendChild(sibling.firstChild);
|
|
7677
|
+
sibling.parentNode?.removeChild(sibling);
|
|
7678
|
+
coalesced += 1;
|
|
7679
|
+
sibling = next;
|
|
7680
|
+
}
|
|
7681
|
+
}
|
|
7682
|
+
return coalesced;
|
|
7683
|
+
}
|
|
7383
7684
|
function isTableRowRevisionMarker(node) {
|
|
7384
7685
|
const parent = node?.parentNode;
|
|
7385
7686
|
return isWordElement5(parent, "trPr") && isWordElement5(parent?.parentNode, "tr");
|
|
@@ -7627,6 +7928,7 @@ function rejectTrackedChangesInOoxml(oxml, options = {}) {
|
|
|
7627
7928
|
if (rejectPropertyChangeNode(changeNode, localName2)) rejectedCount += 1;
|
|
7628
7929
|
}
|
|
7629
7930
|
}
|
|
7931
|
+
coalesceAdjacentCompatibleInsertions(xmlDoc);
|
|
7630
7932
|
const serializedOxml = parseResult.isFragmentWrapped ? Array.from(xmlDoc.documentElement.childNodes).map((n) => serializer.serializeToString(n)).join("") : serializer.serializeToString(xmlDoc);
|
|
7631
7933
|
return {
|
|
7632
7934
|
oxml: serializedOxml,
|
|
@@ -7778,7 +8080,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
7778
8080
|
const finalize = (result) => {
|
|
7779
8081
|
const withStatus = { ...result };
|
|
7780
8082
|
if (normalizedExistingRevisions && withStatus.hasChanges === false && withStatus.status !== "error") {
|
|
7781
|
-
if (existingRevisionsPolicy === "merge-same-author") {
|
|
8083
|
+
if (existingRevisionsPolicy === "merge-same-author" || existingRevisionsPolicy === "slice-cross-author") {
|
|
7782
8084
|
withStatus.oxml = workingOoxml;
|
|
7783
8085
|
withStatus.hasChanges = true;
|
|
7784
8086
|
withStatus.warnings = [
|
|
@@ -7832,7 +8134,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
7832
8134
|
const revisionIdAllocator = options?._revisionIdAllocator instanceof RevisionIdAllocator ? options._revisionIdAllocator : new RevisionIdAllocator();
|
|
7833
8135
|
seedRevisionIdsFromDocument(xmlDoc, revisionIdAllocator);
|
|
7834
8136
|
if (containsTrackedChanges(xmlDoc)) {
|
|
7835
|
-
if (existingRevisionsPolicy === "merge-same-author") {
|
|
8137
|
+
if (existingRevisionsPolicy === "merge-same-author" || existingRevisionsPolicy === "slice-cross-author") {
|
|
7836
8138
|
const authors = getTrackedChangeAuthors(xmlDoc);
|
|
7837
8139
|
const currentAuthor = String(author || "").trim().toLowerCase();
|
|
7838
8140
|
const isSameAuthor = authors.length > 0 && authors.every((a) => a.trim().toLowerCase() === currentAuthor);
|
|
@@ -7887,7 +8189,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
7887
8189
|
const paragraphsInDoc = xmlDoc.documentElement && String(xmlDoc.documentElement.localName || "").toLowerCase() === "p" ? [xmlDoc.documentElement] : getDocumentParagraphs(xmlDoc);
|
|
7888
8190
|
const baselineText = paragraphsInDoc.length > 0 ? paragraphsInDoc.map((p) => extractCanonicalParagraphText(p)).join("\n") : "";
|
|
7889
8191
|
originalText = baselineText;
|
|
7890
|
-
} else {
|
|
8192
|
+
} else if (existingRevisionsPolicy === "merge-same-author") {
|
|
7891
8193
|
log("[OxmlEngine] Existing revisions detected from another/unattributed author; refusing per merge-same-author policy");
|
|
7892
8194
|
return finalize({
|
|
7893
8195
|
oxml: inputOoxml,
|
|
@@ -7898,6 +8200,22 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
7898
8200
|
message: `Input OOXML contains tracked changes from another author (${authors.length ? authors.join(", ") : "unattributed"}). Pass existingRevisions: "accept-all-first" or resolve revisions first.`
|
|
7899
8201
|
}
|
|
7900
8202
|
});
|
|
8203
|
+
} else {
|
|
8204
|
+
const hasMoveRevision = ["moveFrom", "moveTo"].some((localName2) => {
|
|
8205
|
+
return getElementsByTagNSOrTag(xmlDoc, NS_W, localName2).length > 0;
|
|
8206
|
+
});
|
|
8207
|
+
if (hasMoveRevision) {
|
|
8208
|
+
return finalize({
|
|
8209
|
+
oxml: inputOoxml,
|
|
8210
|
+
hasChanges: false,
|
|
8211
|
+
status: "error",
|
|
8212
|
+
error: {
|
|
8213
|
+
code: "UNSAFE_REVISION_NESTING",
|
|
8214
|
+
message: "Cross-author slicing does not support pending move revisions."
|
|
8215
|
+
}
|
|
8216
|
+
});
|
|
8217
|
+
}
|
|
8218
|
+
log("[OxmlEngine] Existing revisions retained for cross-author surgical slicing");
|
|
7901
8219
|
}
|
|
7902
8220
|
} else if (existingRevisionsPolicy === "accept-all-first" || existingRevisionsPolicy === "accept-all-first-keep-normalized") {
|
|
7903
8221
|
log("[OxmlEngine] Existing revisions detected; accepting all input revisions before redlining");
|
|
@@ -7973,7 +8291,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
7973
8291
|
}
|
|
7974
8292
|
}
|
|
7975
8293
|
const { cleanText: cleanModifiedText, formatHints } = preprocessMarkdown(sanitizedText);
|
|
7976
|
-
const hasTextChanges = cleanModifiedText.trim() !== originalText.trim();
|
|
8294
|
+
const hasTextChanges = existingRevisionsPolicy === "slice-cross-author" ? cleanModifiedText !== originalText : cleanModifiedText.trim() !== originalText.trim();
|
|
7977
8295
|
const hasFormatHints = formatHints.length > 0;
|
|
7978
8296
|
const { existingFormatHints, textSpans, paragraphs } = extractFormattingFromOoxml(xmlDoc);
|
|
7979
8297
|
const hasExistingFormatting = existingFormatHints.length > 0;
|
|
@@ -8096,7 +8414,8 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
8096
8414
|
const isTargetList = isListTargetLoose(cleanModifiedText);
|
|
8097
8415
|
const isStructuredContent = options.structuredContent !== false && structuredAnalysis?.requiresStructuredContent === true;
|
|
8098
8416
|
const tableCellContext = initialTableCellContext;
|
|
8099
|
-
|
|
8417
|
+
const usesSurgicalTextMode = hasTables || existingRevisionsPolicy === "slice-cross-author";
|
|
8418
|
+
log(`[OxmlEngine] Mode: ${usesSurgicalTextMode ? "SURGICAL" : "RECONSTRUCTION"}, formatHints: ${formatHints.length}, isMarkdownTable: ${isMarkdownTable}, isTargetList: ${isTargetList}, isTableCellParagraph: ${tableCellContext.isTableCellParagraph}`);
|
|
8100
8419
|
try {
|
|
8101
8420
|
if (isMarkdownTable && !hasTables) {
|
|
8102
8421
|
recordRouteSelection(options, "table", { transformation: "text-to-table" });
|
|
@@ -8107,8 +8426,8 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
8107
8426
|
recordRouteSelection(options, "table", { transformation: "table-reconciliation" });
|
|
8108
8427
|
return finalize(applyTableReconciliation(xmlDoc, cleanModifiedText, serializer, null, author, generateRedlines));
|
|
8109
8428
|
}
|
|
8110
|
-
if (
|
|
8111
|
-
recordRouteSelection(options, "surgical", { tableScoped:
|
|
8429
|
+
if (usesSurgicalTextMode) {
|
|
8430
|
+
recordRouteSelection(options, "surgical", { tableScoped: hasTables });
|
|
8112
8431
|
const surgicalTarget = tableCellContext.hasTableWrapper && tableCellContext.targetParagraph ? tableCellContext.targetParagraph : null;
|
|
8113
8432
|
if (surgicalTarget) {
|
|
8114
8433
|
log("[OxmlEngine] Table cell edit: scoping surgical mode to target paragraph");
|
|
@@ -8125,6 +8444,9 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
8125
8444
|
{},
|
|
8126
8445
|
options
|
|
8127
8446
|
);
|
|
8447
|
+
if (result.status === "error" && result.error?.code === "PATCH_ROUNDTRIP_MISMATCH") {
|
|
8448
|
+
return finalize({ ...result, oxml: inputOoxml, hasChanges: false });
|
|
8449
|
+
}
|
|
8128
8450
|
if (tableCellContext.hasTableWrapper && result.hasChanges && tableCellContext.targetParagraph) {
|
|
8129
8451
|
log("[OxmlEngine] Stripping table wrapper for table cell paragraph (surgical mode)");
|
|
8130
8452
|
return finalize({ oxml: serializeParagraphOnly(xmlDoc, tableCellContext.targetParagraph, serializer), hasChanges: true });
|
|
@@ -9611,11 +9933,15 @@ function validateRedlineOoxml(oxml) {
|
|
|
9611
9933
|
}
|
|
9612
9934
|
for (const revision of revisions) {
|
|
9613
9935
|
const nested = Array.from(revision.getElementsByTagName("*")).filter((el) => el !== revision && ["ins", "del"].includes(localNameOf2(el)));
|
|
9614
|
-
|
|
9936
|
+
const invalidNested = nested.find((candidate) => {
|
|
9937
|
+
if (localNameOf2(revision) !== "ins" || localNameOf2(candidate) !== "del") return true;
|
|
9938
|
+
return candidate.parentNode !== revision;
|
|
9939
|
+
});
|
|
9940
|
+
if (invalidNested) {
|
|
9615
9941
|
addIssue(
|
|
9616
9942
|
"NESTED_REVISION",
|
|
9617
9943
|
"error",
|
|
9618
|
-
`<${revision.nodeName}> (w:id="${wordAttribute2(revision, "id")}") contains nested <${
|
|
9944
|
+
`<${revision.nodeName}> (w:id="${wordAttribute2(revision, "id")}") contains invalid nested <${invalidNested.nodeName}>.`
|
|
9619
9945
|
);
|
|
9620
9946
|
}
|
|
9621
9947
|
}
|
|
@@ -10804,24 +11130,6 @@ function buildCommentsExtendedPartXml(entries) {
|
|
|
10804
11130
|
return `<w15:commentsEx xmlns:w15="${NS_W15}">${body}</w15:commentsEx>`;
|
|
10805
11131
|
}
|
|
10806
11132
|
|
|
10807
|
-
// core/revision-cloning.js
|
|
10808
|
-
function refreshRunPropertyChangeIds(root, allocator = null) {
|
|
10809
|
-
if (!root) return root;
|
|
10810
|
-
const xmlDoc = root.nodeType === 9 ? root : root.ownerDocument;
|
|
10811
|
-
const resolvedAllocator = allocator instanceof RevisionIdAllocator ? allocator : getRevisionIdAllocatorForDocument(xmlDoc) || createRevisionIdAllocator(xmlDoc);
|
|
10812
|
-
const candidates = [root, ...Array.from(root.getElementsByTagName?.("*") || [])];
|
|
10813
|
-
for (const node of candidates) {
|
|
10814
|
-
if (!isWordElement(node, "rPrChange")) continue;
|
|
10815
|
-
const nextId = String(resolvedAllocator.next());
|
|
10816
|
-
if (typeof node.setAttributeNS === "function") {
|
|
10817
|
-
node.setAttributeNS(NS_W, "w:id", nextId);
|
|
10818
|
-
} else {
|
|
10819
|
-
node.setAttribute("w:id", nextId);
|
|
10820
|
-
}
|
|
10821
|
-
}
|
|
10822
|
-
return root;
|
|
10823
|
-
}
|
|
10824
|
-
|
|
10825
11133
|
// services/comment-locator.js
|
|
10826
11134
|
function createParagraphTextIndex(paragraph, options = {}) {
|
|
10827
11135
|
const revisionView = options.revisionView === "current" ? "accepted" : options.revisionView || "accepted";
|