@ansonlai/docx-redline-js 0.5.2 → 0.5.4
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 +67 -12
- package/ARCHITECTURE.md +38 -3
- package/CHANGELOG.md +19 -1
- package/README.md +52 -12
- package/core/paragraph-revision-safety.js +215 -0
- package/core/paragraph-targeting.js +19 -0
- package/core/redline-validation.js +20 -4
- package/core/revision-cloning.js +21 -0
- package/core/validation-delta.js +23 -0
- package/dist/docx-redline-js.esm.js +493 -99
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +84 -84
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/docs/plans/2026-09-08-cross-author-revision-slicing.md +861 -37
- package/docs/schemas/document-operations.schema.json +14 -1
- package/engine/oxml-engine.js +24 -3
- package/engine/surgical-diff-application.js +35 -1
- package/engine/surgical-mode.js +203 -4
- package/engine/surgical-run-splitting.js +19 -7
- package/engine/surgical-spans.js +2 -1
- package/index.d.ts +12 -0
- package/node/cli.js +128 -13
- package/node/docx-document.js +17 -14
- package/package.json +1 -1
- package/pipeline/diff-engine.js +15 -0
- package/services/document-operation-applier.js +73 -9
- package/services/document-operation-contract.js +77 -2
- package/services/document-operation-mutations.js +846 -49
- package/services/operation-preflight.js +72 -8
- package/services/standalone-operation-runner.d.ts +26 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @ansonlai/docx-redline-js v0.5.
|
|
1
|
+
// @ansonlai/docx-redline-js v0.5.4 — 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,10 @@ function computeWordDiffs(originalText, newText, options = {}) {
|
|
|
2634
2634
|
}
|
|
2635
2635
|
return decodeBmpDiffs(charDiffs, wordArray);
|
|
2636
2636
|
}
|
|
2637
|
+
function computeCharacterDiffs(originalText, newText, options = {}) {
|
|
2638
|
+
if (originalText === newText) return [[0, originalText]];
|
|
2639
|
+
return createDiffEngine(options).diff_main(originalText, newText);
|
|
2640
|
+
}
|
|
2637
2641
|
function computeInsertionOnlyDiffs(originalText, newText) {
|
|
2638
2642
|
if (originalText === newText) return [[0, originalText]];
|
|
2639
2643
|
if (!originalText) return [[1, newText]];
|
|
@@ -5713,7 +5717,7 @@ function getParagraphId(paragraph) {
|
|
|
5713
5717
|
|
|
5714
5718
|
// engine/surgical-spans.js
|
|
5715
5719
|
function getRunChildText(child) {
|
|
5716
|
-
if (isWordElement(child, "t")) return child.textContent || "";
|
|
5720
|
+
if (isWordElement(child, "t") || isWordElement(child, "delText")) return child.textContent || "";
|
|
5717
5721
|
if (isWordElement(child, "br") || isWordElement(child, "cr")) return "\n";
|
|
5718
5722
|
if (isWordElement(child, "tab")) return " ";
|
|
5719
5723
|
if (isWordElement(child, "noBreakHyphen")) return "\u2011";
|
|
@@ -5721,7 +5725,7 @@ function getRunChildText(child) {
|
|
|
5721
5725
|
return "";
|
|
5722
5726
|
}
|
|
5723
5727
|
function isTextLikeRunChild(child) {
|
|
5724
|
-
return isWordElement(child, "t") || isWordElement(child, "br") || isWordElement(child, "cr") || isWordElement(child, "tab") || isWordElement(child, "noBreakHyphen") || isWordElement(child, "softHyphen");
|
|
5728
|
+
return isWordElement(child, "t") || isWordElement(child, "delText") || isWordElement(child, "br") || isWordElement(child, "cr") || isWordElement(child, "tab") || isWordElement(child, "noBreakHyphen") || isWordElement(child, "softHyphen");
|
|
5725
5729
|
}
|
|
5726
5730
|
function buildSurgicalTextSpans(paragraphs) {
|
|
5727
5731
|
let fullText = "";
|
|
@@ -5904,12 +5908,13 @@ function refreshRunPropertyChangeIds(root, allocator = null) {
|
|
|
5904
5908
|
} else {
|
|
5905
5909
|
node.setAttribute("w:id", nextId);
|
|
5906
5910
|
}
|
|
5911
|
+
resolvedAllocator._receiptCollector?.recordRevision(Number(nextId), "rPrChange");
|
|
5907
5912
|
}
|
|
5908
5913
|
return root;
|
|
5909
5914
|
}
|
|
5910
5915
|
|
|
5911
5916
|
// engine/surgical-run-splitting.js
|
|
5912
|
-
var TRACK_CHANGE_CARRIERS = /* @__PURE__ */ new Set(["ins"]);
|
|
5917
|
+
var TRACK_CHANGE_CARRIERS = /* @__PURE__ */ new Set(["ins", "del"]);
|
|
5913
5918
|
function getRunContentPieces(runElement) {
|
|
5914
5919
|
const pieces = [];
|
|
5915
5920
|
let offset = 0;
|
|
@@ -5961,7 +5966,7 @@ function insertRunPiecesBefore(xmlDoc, parent, referenceNode, pieces, rPr) {
|
|
|
5961
5966
|
function splitTrackChangeCarrier(xmlDoc, carrierElement, splitOffset, allocator = null) {
|
|
5962
5967
|
const carrierName = getLocalName(carrierElement);
|
|
5963
5968
|
if (!TRACK_CHANGE_CARRIERS.has(carrierName)) {
|
|
5964
|
-
throw new TypeError("splitTrackChangeCarrier requires a w:ins carrier.");
|
|
5969
|
+
throw new TypeError("splitTrackChangeCarrier requires a w:ins or w:del carrier.");
|
|
5965
5970
|
}
|
|
5966
5971
|
if (!Number.isInteger(splitOffset) || splitOffset < 0) {
|
|
5967
5972
|
throw new RangeError("splitOffset must be a non-negative integer.");
|
|
@@ -5998,16 +6003,17 @@ function splitTrackChangeCarrier(xmlDoc, carrierElement, splitOffset, allocator
|
|
|
5998
6003
|
} else {
|
|
5999
6004
|
const localOffset = splitOffset - offset;
|
|
6000
6005
|
const rPr = Array.from(child.childNodes || []).find((node) => isWordElement(node, "rPr")) || null;
|
|
6001
|
-
const
|
|
6002
|
-
const
|
|
6006
|
+
const asDeletedText = carrierName === "del";
|
|
6007
|
+
const leftPieces = sliceRunPieces(xmlDoc, pieces, 0, localOffset, asDeletedText);
|
|
6008
|
+
const rightPieces = sliceRunPieces(xmlDoc, pieces, localOffset, runLength, asDeletedText);
|
|
6003
6009
|
leftCarrier.appendChild(createRunFromPieces(xmlDoc, leftPieces, rPr));
|
|
6004
6010
|
const rightRun = createRunFromPieces(xmlDoc, rightPieces, rPr);
|
|
6005
|
-
refreshRunPropertyChangeIds(rightRun, resolveAllocator(xmlDoc, allocator));
|
|
6006
6011
|
rightCarrier.appendChild(rightRun);
|
|
6007
6012
|
}
|
|
6008
6013
|
offset = runEnd;
|
|
6009
6014
|
}
|
|
6010
6015
|
const resolvedAllocator = resolveAllocator(xmlDoc, allocator);
|
|
6016
|
+
refreshRunPropertyChangeIds(rightCarrier, resolvedAllocator);
|
|
6011
6017
|
const nextId = resolvedAllocator.next();
|
|
6012
6018
|
setWordAttribute(rightCarrier, "id", String(nextId));
|
|
6013
6019
|
resolvedAllocator._receiptCollector?.recordRevision(nextId, carrierName);
|
|
@@ -6027,7 +6033,7 @@ function getLocalName(element) {
|
|
|
6027
6033
|
return String(element?.localName || element?.nodeName || "").replace(/^.*:/, "");
|
|
6028
6034
|
}
|
|
6029
6035
|
function cloneRunPiece(xmlDoc, sourceNode, text, asDeletedText) {
|
|
6030
|
-
if (asDeletedText) {
|
|
6036
|
+
if (asDeletedText && (isWordElement(sourceNode, "delText") || isWordElement(sourceNode, "t"))) {
|
|
6031
6037
|
const delText = createWordElement(xmlDoc, "w:delText");
|
|
6032
6038
|
delText.setAttribute("xml:space", "preserve");
|
|
6033
6039
|
delText.textContent = text;
|
|
@@ -6050,6 +6056,15 @@ function cloneRunPiece(xmlDoc, sourceNode, text, asDeletedText) {
|
|
|
6050
6056
|
if (text === "\u2011" && isWordElement(sourceNode, "noBreakHyphen")) {
|
|
6051
6057
|
return sourceNode.cloneNode(true);
|
|
6052
6058
|
}
|
|
6059
|
+
if (text === "\xAD" && isWordElement(sourceNode, "softHyphen")) {
|
|
6060
|
+
return sourceNode.cloneNode(true);
|
|
6061
|
+
}
|
|
6062
|
+
if (asDeletedText) {
|
|
6063
|
+
const delText = createWordElement(xmlDoc, "w:delText");
|
|
6064
|
+
delText.setAttribute("xml:space", "preserve");
|
|
6065
|
+
delText.textContent = text;
|
|
6066
|
+
return delText;
|
|
6067
|
+
}
|
|
6053
6068
|
const textNode = createWordElement(xmlDoc, "w:t");
|
|
6054
6069
|
textNode.setAttribute("xml:space", "preserve");
|
|
6055
6070
|
textNode.textContent = text;
|
|
@@ -6160,7 +6175,15 @@ function processDelete(xmlDoc, spanIndex, startPos, endPos, author, generateRedl
|
|
|
6160
6175
|
if (delWrapper && record.deletedPieces.length > 0) {
|
|
6161
6176
|
delWrapper.appendChild(createRunFromPieces(xmlDoc, record.deletedPieces, record.rPr));
|
|
6162
6177
|
}
|
|
6163
|
-
insertRunPiecesBefore(xmlDoc, parent, runElement, record.afterPieces, record.rPr);
|
|
6178
|
+
const afterRun = insertRunPiecesBefore(xmlDoc, parent, runElement, record.afterPieces, record.rPr);
|
|
6179
|
+
if (record.globalEnd === endPos && !isWordElement(parent, "ins")) {
|
|
6180
|
+
if (!spanIndex.replacementInsertionAnchors) spanIndex.replacementInsertionAnchors = /* @__PURE__ */ new Map();
|
|
6181
|
+
spanIndex.replacementInsertionAnchors.set(endPos, {
|
|
6182
|
+
parent,
|
|
6183
|
+
referenceNode: afterRun || runElement.nextSibling,
|
|
6184
|
+
rPr: record.rPr
|
|
6185
|
+
});
|
|
6186
|
+
}
|
|
6164
6187
|
parent.removeChild(runElement);
|
|
6165
6188
|
changed = true;
|
|
6166
6189
|
}
|
|
@@ -6197,6 +6220,23 @@ function processInsert(xmlDoc, spanIndex, pos, text, author, formatHints = [], i
|
|
|
6197
6220
|
revisionMetadata
|
|
6198
6221
|
);
|
|
6199
6222
|
}
|
|
6223
|
+
const replacementAnchor = spanIndex.replacementInsertionAnchors?.get(pos) || null;
|
|
6224
|
+
if (replacementAnchor && !affinity && isConnected(replacementAnchor.parent) && (!replacementAnchor.referenceNode || replacementAnchor.referenceNode.parentNode === replacementAnchor.parent)) {
|
|
6225
|
+
spanIndex.replacementInsertionAnchors.delete(pos);
|
|
6226
|
+
insertTextRuns(
|
|
6227
|
+
xmlDoc,
|
|
6228
|
+
replacementAnchor.parent,
|
|
6229
|
+
replacementAnchor.referenceNode,
|
|
6230
|
+
text,
|
|
6231
|
+
replacementAnchor.rPr,
|
|
6232
|
+
author,
|
|
6233
|
+
formatHints,
|
|
6234
|
+
insertOffset,
|
|
6235
|
+
generateRedlines,
|
|
6236
|
+
revisionMetadata
|
|
6237
|
+
);
|
|
6238
|
+
return true;
|
|
6239
|
+
}
|
|
6200
6240
|
if (!affinity) {
|
|
6201
6241
|
let targetSpan = findContainingSpan(spanIndex, pos);
|
|
6202
6242
|
if (!targetSpan && pos > 0) {
|
|
@@ -6559,68 +6599,197 @@ function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer, autho
|
|
|
6559
6599
|
const allParagraphs = targetParagraph ? [targetParagraph] : getDocumentParagraphs(xmlDoc);
|
|
6560
6600
|
const { fullText, textSpans } = buildSurgicalTextSpans(allParagraphs);
|
|
6561
6601
|
const insertionOnlyDiffs = options.existingRevisions === "slice-cross-author" ? computeInsertionOnlyDiffs(fullText, modifiedText) : null;
|
|
6562
|
-
const diffs = insertionOnlyDiffs ||
|
|
6602
|
+
const diffs = insertionOnlyDiffs || refineSpaceEquivalentReplacements(
|
|
6603
|
+
computeWordDiffs(fullText, modifiedText, diffOptions),
|
|
6604
|
+
diffOptions
|
|
6605
|
+
);
|
|
6563
6606
|
const spanIndex = buildSpanIndex(textSpans);
|
|
6564
6607
|
const pairReplacements = options.pairReplacements === true;
|
|
6565
6608
|
const warnings = [];
|
|
6566
6609
|
let originalPos = 0;
|
|
6567
6610
|
let newPos = 0;
|
|
6568
6611
|
let hasChanges = false;
|
|
6569
|
-
|
|
6570
|
-
|
|
6571
|
-
|
|
6572
|
-
const
|
|
6573
|
-
const
|
|
6574
|
-
const
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6612
|
+
const insertionOperations = insertionOnlyDiffs ? collectInsertionOperations(insertionOnlyDiffs) : [];
|
|
6613
|
+
if (insertionOperations.length > 1 && formatHints.length === 0) {
|
|
6614
|
+
for (const operation of insertionOperations.slice().reverse()) {
|
|
6615
|
+
const liveSpans = buildSurgicalTextSpans(allParagraphs).textSpans;
|
|
6616
|
+
const liveSpanIndex = buildSpanIndex(liveSpans);
|
|
6617
|
+
const textWithoutNewlines = operation.text.replace(/\n/g, " ");
|
|
6618
|
+
if (textWithoutNewlines.length === 0) continue;
|
|
6619
|
+
const insertResult = processInsert(
|
|
6620
|
+
xmlDoc,
|
|
6621
|
+
liveSpanIndex,
|
|
6622
|
+
operation.originalPos,
|
|
6623
|
+
textWithoutNewlines,
|
|
6624
|
+
author,
|
|
6625
|
+
formatHints,
|
|
6626
|
+
operation.newPos,
|
|
6627
|
+
generateRedlines,
|
|
6628
|
+
allParagraphs[0] || null,
|
|
6629
|
+
null,
|
|
6630
|
+
options?.insertionAffinity || null,
|
|
6631
|
+
options?.existingRevisions || "merge-same-author"
|
|
6632
|
+
);
|
|
6633
|
+
if (insertResult && typeof insertResult === "object" && insertResult.error) {
|
|
6634
|
+
return withOoxmlSourceType({
|
|
6635
|
+
oxml: serializer.serializeToString(xmlDoc),
|
|
6636
|
+
hasChanges: false,
|
|
6637
|
+
status: "error",
|
|
6638
|
+
error: insertResult.error
|
|
6639
|
+
});
|
|
6640
|
+
}
|
|
6641
|
+
if (insertResult === true) hasChanges = true;
|
|
6642
|
+
}
|
|
6643
|
+
} else if (formatHints.length === 0) {
|
|
6644
|
+
const editOperations = collectTextEditOperations(diffs);
|
|
6645
|
+
for (const operation of editOperations.reverse()) {
|
|
6646
|
+
const liveSpanIndex = buildSpanIndex(buildSurgicalTextSpans(allParagraphs).textSpans);
|
|
6647
|
+
if (operation.type === "insert") {
|
|
6648
|
+
const inserted = processInsert(
|
|
6649
|
+
xmlDoc,
|
|
6650
|
+
liveSpanIndex,
|
|
6651
|
+
operation.start,
|
|
6652
|
+
operation.text.replace(/\n/g, " "),
|
|
6653
|
+
author,
|
|
6654
|
+
formatHints,
|
|
6655
|
+
operation.newPos,
|
|
6656
|
+
generateRedlines,
|
|
6657
|
+
allParagraphs[0] || null,
|
|
6658
|
+
null,
|
|
6659
|
+
options?.insertionAffinity || null,
|
|
6660
|
+
options?.existingRevisions || "merge-same-author"
|
|
6661
|
+
);
|
|
6662
|
+
if (inserted && typeof inserted === "object" && inserted.error) {
|
|
6663
|
+
return withOoxmlSourceType({
|
|
6664
|
+
oxml: serializer.serializeToString(xmlDoc),
|
|
6665
|
+
hasChanges: false,
|
|
6666
|
+
status: "error",
|
|
6667
|
+
error: inserted.error
|
|
6668
|
+
});
|
|
6585
6669
|
}
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
} else if (op === -1) {
|
|
6590
|
-
const hasNextInsert = i + 1 < diffs.length && diffs[i + 1][0] === 1;
|
|
6591
|
-
let paired = false;
|
|
6670
|
+
if (inserted === true) hasChanges = true;
|
|
6671
|
+
continue;
|
|
6672
|
+
}
|
|
6592
6673
|
let delMetadata = null;
|
|
6593
6674
|
let insMetadata = null;
|
|
6594
|
-
if (pairReplacements && generateRedlines &&
|
|
6595
|
-
const
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
insMetadata = { id: event.insertionId, author: event.author, date: event.date };
|
|
6608
|
-
paired = true;
|
|
6609
|
-
} else if (checkResult.structuralBoundary) {
|
|
6610
|
-
warnings.push("PAIRING_SKIPPED_STRUCTURAL_BOUNDARY");
|
|
6611
|
-
}
|
|
6675
|
+
if (operation.type === "replace" && pairReplacements && generateRedlines && operation.text.replace(/\n/g, " ")) {
|
|
6676
|
+
const checkResult = checkSafeAdjacencyForPairing(
|
|
6677
|
+
liveSpanIndex,
|
|
6678
|
+
operation.start,
|
|
6679
|
+
operation.end,
|
|
6680
|
+
options?.existingRevisions === "slice-cross-author"
|
|
6681
|
+
);
|
|
6682
|
+
if (checkResult.safe) {
|
|
6683
|
+
const event = createReplacementRevisionEvent(author, xmlDoc);
|
|
6684
|
+
delMetadata = { id: event.deletionId, author: event.author, date: event.date };
|
|
6685
|
+
insMetadata = { id: event.insertionId, author: event.author, date: event.date };
|
|
6686
|
+
} else if (checkResult.structuralBoundary) {
|
|
6687
|
+
warnings.push("PAIRING_SKIPPED_STRUCTURAL_BOUNDARY");
|
|
6612
6688
|
}
|
|
6613
6689
|
}
|
|
6614
|
-
if (processDelete(xmlDoc,
|
|
6690
|
+
if (processDelete(xmlDoc, liveSpanIndex, operation.start, operation.end, author, generateRedlines, delMetadata)) {
|
|
6615
6691
|
hasChanges = true;
|
|
6616
6692
|
}
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6693
|
+
if (operation.type === "replace") {
|
|
6694
|
+
const inserted = processInsert(
|
|
6695
|
+
xmlDoc,
|
|
6696
|
+
liveSpanIndex,
|
|
6697
|
+
operation.end,
|
|
6698
|
+
operation.text.replace(/\n/g, " "),
|
|
6699
|
+
author,
|
|
6700
|
+
formatHints,
|
|
6701
|
+
operation.newPos,
|
|
6702
|
+
generateRedlines,
|
|
6703
|
+
allParagraphs[0] || null,
|
|
6704
|
+
insMetadata,
|
|
6705
|
+
options?.insertionAffinity || null,
|
|
6706
|
+
options?.existingRevisions || "merge-same-author"
|
|
6707
|
+
);
|
|
6708
|
+
if (inserted && typeof inserted === "object" && inserted.error) {
|
|
6709
|
+
return withOoxmlSourceType({
|
|
6710
|
+
oxml: serializer.serializeToString(xmlDoc),
|
|
6711
|
+
hasChanges: false,
|
|
6712
|
+
status: "error",
|
|
6713
|
+
error: inserted.error
|
|
6714
|
+
});
|
|
6715
|
+
}
|
|
6716
|
+
if (inserted === true) hasChanges = true;
|
|
6717
|
+
}
|
|
6718
|
+
}
|
|
6719
|
+
} else {
|
|
6720
|
+
for (let i = 0; i < diffs.length; i++) {
|
|
6721
|
+
const [op, text] = diffs[i];
|
|
6722
|
+
if (op === 0) {
|
|
6723
|
+
const len = text.length;
|
|
6724
|
+
const startPos = originalPos;
|
|
6725
|
+
const endPos = originalPos + len;
|
|
6726
|
+
forEachOverlappingSpan(spanIndex, startPos, endPos, (span) => {
|
|
6727
|
+
const overlapStartOriginal = Math.max(span.charStart, startPos);
|
|
6728
|
+
const overlapEndOriginal = Math.min(span.charEnd, endPos);
|
|
6729
|
+
const segmentLen = overlapEndOriginal - overlapStartOriginal;
|
|
6730
|
+
const relativeOffset = overlapStartOriginal - startPos;
|
|
6731
|
+
const overlapStartNew = newPos + relativeOffset;
|
|
6732
|
+
const overlapEndNew = overlapStartNew + segmentLen;
|
|
6733
|
+
const applicableHints = getApplicableFormatHints(formatHints, overlapStartNew, overlapEndNew);
|
|
6734
|
+
if (reconcileFormattingForTextSpan(xmlDoc, span, overlapStartOriginal, overlapEndOriginal, applicableHints, author, generateRedlines)) {
|
|
6735
|
+
hasChanges = true;
|
|
6736
|
+
}
|
|
6737
|
+
});
|
|
6738
|
+
originalPos += len;
|
|
6739
|
+
newPos += len;
|
|
6740
|
+
} else if (op === -1) {
|
|
6741
|
+
const hasNextInsert = i + 1 < diffs.length && diffs[i + 1][0] === 1;
|
|
6742
|
+
let paired = false;
|
|
6743
|
+
let delMetadata = null;
|
|
6744
|
+
let insMetadata = null;
|
|
6745
|
+
if (pairReplacements && generateRedlines && hasNextInsert) {
|
|
6746
|
+
const nextText = diffs[i + 1][1];
|
|
6747
|
+
const textWithoutNewlines = nextText.replace(/\n/g, " ");
|
|
6748
|
+
if (textWithoutNewlines.length > 0) {
|
|
6749
|
+
const checkResult = checkSafeAdjacencyForPairing(
|
|
6750
|
+
spanIndex,
|
|
6751
|
+
originalPos,
|
|
6752
|
+
originalPos + text.length,
|
|
6753
|
+
options?.existingRevisions === "slice-cross-author"
|
|
6754
|
+
);
|
|
6755
|
+
if (checkResult.safe) {
|
|
6756
|
+
const event = createReplacementRevisionEvent(author, xmlDoc);
|
|
6757
|
+
delMetadata = { id: event.deletionId, author: event.author, date: event.date };
|
|
6758
|
+
insMetadata = { id: event.insertionId, author: event.author, date: event.date };
|
|
6759
|
+
paired = true;
|
|
6760
|
+
} else if (checkResult.structuralBoundary) {
|
|
6761
|
+
warnings.push("PAIRING_SKIPPED_STRUCTURAL_BOUNDARY");
|
|
6762
|
+
}
|
|
6763
|
+
}
|
|
6764
|
+
}
|
|
6765
|
+
if (processDelete(xmlDoc, spanIndex, originalPos, originalPos + text.length, author, generateRedlines, delMetadata)) {
|
|
6766
|
+
hasChanges = true;
|
|
6767
|
+
}
|
|
6768
|
+
originalPos += text.length;
|
|
6769
|
+
if (paired) {
|
|
6770
|
+
i++;
|
|
6771
|
+
const [, nextText] = diffs[i];
|
|
6772
|
+
const textWithoutNewlines = nextText.replace(/\n/g, " ");
|
|
6773
|
+
if (textWithoutNewlines.length > 0) {
|
|
6774
|
+
const insertResult = processInsert(xmlDoc, spanIndex, originalPos, textWithoutNewlines, author, formatHints, newPos, generateRedlines, allParagraphs[0] || null, insMetadata, options?.insertionAffinity || null, options?.existingRevisions || "merge-same-author");
|
|
6775
|
+
if (insertResult && typeof insertResult === "object" && insertResult.error) {
|
|
6776
|
+
return withOoxmlSourceType({
|
|
6777
|
+
oxml: serializer.serializeToString(xmlDoc),
|
|
6778
|
+
hasChanges: false,
|
|
6779
|
+
status: "error",
|
|
6780
|
+
error: insertResult.error
|
|
6781
|
+
});
|
|
6782
|
+
}
|
|
6783
|
+
if (insertResult === true) {
|
|
6784
|
+
hasChanges = true;
|
|
6785
|
+
}
|
|
6786
|
+
}
|
|
6787
|
+
newPos += nextText.length;
|
|
6788
|
+
}
|
|
6789
|
+
} else if (op === 1) {
|
|
6790
|
+
const textWithoutNewlines = text.replace(/\n/g, " ");
|
|
6622
6791
|
if (textWithoutNewlines.length > 0) {
|
|
6623
|
-
const insertResult = processInsert(xmlDoc, spanIndex, originalPos, textWithoutNewlines, author, formatHints, newPos, generateRedlines, allParagraphs[0] || null,
|
|
6792
|
+
const insertResult = processInsert(xmlDoc, spanIndex, originalPos, textWithoutNewlines, author, formatHints, newPos, generateRedlines, allParagraphs[0] || null, null, options?.insertionAffinity || null, options?.existingRevisions || "merge-same-author");
|
|
6624
6793
|
if (insertResult && typeof insertResult === "object" && insertResult.error) {
|
|
6625
6794
|
return withOoxmlSourceType({
|
|
6626
6795
|
oxml: serializer.serializeToString(xmlDoc),
|
|
@@ -6633,25 +6802,8 @@ function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer, autho
|
|
|
6633
6802
|
hasChanges = true;
|
|
6634
6803
|
}
|
|
6635
6804
|
}
|
|
6636
|
-
newPos +=
|
|
6637
|
-
}
|
|
6638
|
-
} else if (op === 1) {
|
|
6639
|
-
const textWithoutNewlines = text.replace(/\n/g, " ");
|
|
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");
|
|
6642
|
-
if (insertResult && typeof insertResult === "object" && insertResult.error) {
|
|
6643
|
-
return withOoxmlSourceType({
|
|
6644
|
-
oxml: serializer.serializeToString(xmlDoc),
|
|
6645
|
-
hasChanges: false,
|
|
6646
|
-
status: "error",
|
|
6647
|
-
error: insertResult.error
|
|
6648
|
-
});
|
|
6649
|
-
}
|
|
6650
|
-
if (insertResult === true) {
|
|
6651
|
-
hasChanges = true;
|
|
6652
|
-
}
|
|
6805
|
+
newPos += text.length;
|
|
6653
6806
|
}
|
|
6654
|
-
newPos += text.length;
|
|
6655
6807
|
}
|
|
6656
6808
|
}
|
|
6657
6809
|
const actualText = allParagraphs.map((paragraph) => extractCanonicalParagraphText(paragraph)).join("\n");
|
|
@@ -6667,7 +6819,9 @@ function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer, autho
|
|
|
6667
6819
|
message: "Generated OOXML accepted-view text does not match the requested modified text; the mutation was rejected.",
|
|
6668
6820
|
mismatchOffset,
|
|
6669
6821
|
expectedExcerpt: excerptAt(expectedText, mismatchOffset),
|
|
6670
|
-
actualExcerpt: excerptAt(actualText, mismatchOffset)
|
|
6822
|
+
actualExcerpt: excerptAt(actualText, mismatchOffset),
|
|
6823
|
+
expectedCodePoint: codePointAtOffset(expectedText, mismatchOffset),
|
|
6824
|
+
actualCodePoint: codePointAtOffset(actualText, mismatchOffset)
|
|
6671
6825
|
},
|
|
6672
6826
|
...warnings.length > 0 ? { warnings: [...new Set(warnings)] } : {}
|
|
6673
6827
|
});
|
|
@@ -6690,6 +6844,76 @@ function excerptAt(text, offset, radius = 40) {
|
|
|
6690
6844
|
const end = Math.min(text.length, offset + radius);
|
|
6691
6845
|
return text.slice(start, end);
|
|
6692
6846
|
}
|
|
6847
|
+
function codePointAtOffset(text, offset) {
|
|
6848
|
+
if (offset >= text.length) return "END";
|
|
6849
|
+
return `U+${text.codePointAt(offset).toString(16).toUpperCase().padStart(4, "0")}`;
|
|
6850
|
+
}
|
|
6851
|
+
function refineSpaceEquivalentReplacements(diffs, diffOptions) {
|
|
6852
|
+
const refined = [];
|
|
6853
|
+
for (let index = 0; index < diffs.length; index++) {
|
|
6854
|
+
const [op, text] = diffs[index];
|
|
6855
|
+
const next = diffs[index + 1];
|
|
6856
|
+
if (op === -1 && next?.[0] === 1 && text !== next[1] && text.length === next[1].length && text.replace(/\u00a0/g, " ") === next[1].replace(/\u00a0/g, " ")) {
|
|
6857
|
+
refined.push(...computeCharacterDiffs(text, next[1], diffOptions));
|
|
6858
|
+
index++;
|
|
6859
|
+
continue;
|
|
6860
|
+
}
|
|
6861
|
+
refined.push([op, text]);
|
|
6862
|
+
}
|
|
6863
|
+
return refined;
|
|
6864
|
+
}
|
|
6865
|
+
function collectTextEditOperations(diffs) {
|
|
6866
|
+
const operations = [];
|
|
6867
|
+
let originalPos = 0;
|
|
6868
|
+
let newPos = 0;
|
|
6869
|
+
for (let index = 0; index < diffs.length; index++) {
|
|
6870
|
+
const [op, text] = diffs[index];
|
|
6871
|
+
if (op === 0) {
|
|
6872
|
+
originalPos += text.length;
|
|
6873
|
+
newPos += text.length;
|
|
6874
|
+
continue;
|
|
6875
|
+
}
|
|
6876
|
+
if (op === -1) {
|
|
6877
|
+
const next = diffs[index + 1];
|
|
6878
|
+
if (next?.[0] === 1) {
|
|
6879
|
+
operations.push({
|
|
6880
|
+
type: "replace",
|
|
6881
|
+
start: originalPos,
|
|
6882
|
+
end: originalPos + text.length,
|
|
6883
|
+
newPos,
|
|
6884
|
+
text: next[1]
|
|
6885
|
+
});
|
|
6886
|
+
originalPos += text.length;
|
|
6887
|
+
newPos += next[1].length;
|
|
6888
|
+
index++;
|
|
6889
|
+
} else {
|
|
6890
|
+
operations.push({ type: "delete", start: originalPos, end: originalPos + text.length, newPos, text: "" });
|
|
6891
|
+
originalPos += text.length;
|
|
6892
|
+
}
|
|
6893
|
+
continue;
|
|
6894
|
+
}
|
|
6895
|
+
operations.push({ type: "insert", start: originalPos, end: originalPos, newPos, text });
|
|
6896
|
+
newPos += text.length;
|
|
6897
|
+
}
|
|
6898
|
+
return operations;
|
|
6899
|
+
}
|
|
6900
|
+
function collectInsertionOperations(diffs) {
|
|
6901
|
+
const operations = [];
|
|
6902
|
+
let originalPos = 0;
|
|
6903
|
+
let newPos = 0;
|
|
6904
|
+
for (const [op, text] of diffs) {
|
|
6905
|
+
if (op === 0) {
|
|
6906
|
+
originalPos += text.length;
|
|
6907
|
+
newPos += text.length;
|
|
6908
|
+
} else if (op === -1) {
|
|
6909
|
+
originalPos += text.length;
|
|
6910
|
+
} else if (op === 1) {
|
|
6911
|
+
operations.push({ originalPos, newPos, text });
|
|
6912
|
+
newPos += text.length;
|
|
6913
|
+
}
|
|
6914
|
+
}
|
|
6915
|
+
return operations;
|
|
6916
|
+
}
|
|
6693
6917
|
|
|
6694
6918
|
// engine/reconstruction-mapper.js
|
|
6695
6919
|
var import_diff_match_patch2 = __toESM(require_diff_match_patch(), 1);
|
|
@@ -7592,8 +7816,8 @@ function resolveAuthorFilter(options = {}) {
|
|
|
7592
7816
|
if (options?.allAuthors === true) {
|
|
7593
7817
|
return { valid: true, allAuthors: true, normalizedAuthor: "" };
|
|
7594
7818
|
}
|
|
7595
|
-
const
|
|
7596
|
-
if (!
|
|
7819
|
+
const normalizedAuthor2 = normalizeAuthor(options?.author);
|
|
7820
|
+
if (!normalizedAuthor2) {
|
|
7597
7821
|
return {
|
|
7598
7822
|
valid: false,
|
|
7599
7823
|
allAuthors: false,
|
|
@@ -7601,7 +7825,7 @@ function resolveAuthorFilter(options = {}) {
|
|
|
7601
7825
|
warning: "No author provided. Pass { author } or set { allAuthors: true }."
|
|
7602
7826
|
};
|
|
7603
7827
|
}
|
|
7604
|
-
return { valid: true, allAuthors: false, normalizedAuthor };
|
|
7828
|
+
return { valid: true, allAuthors: false, normalizedAuthor: normalizedAuthor2 };
|
|
7605
7829
|
}
|
|
7606
7830
|
function authorMatchesNode(node, filter) {
|
|
7607
7831
|
if (filter.allAuthors) return true;
|
|
@@ -8053,6 +8277,133 @@ function recordRouteSelection(options, route, context = {}) {
|
|
|
8053
8277
|
}));
|
|
8054
8278
|
}
|
|
8055
8279
|
|
|
8280
|
+
// core/paragraph-revision-safety.js
|
|
8281
|
+
var NON_CONTENT_CHILDREN = /* @__PURE__ */ new Set([
|
|
8282
|
+
"pPr",
|
|
8283
|
+
"bookmarkStart",
|
|
8284
|
+
"bookmarkEnd",
|
|
8285
|
+
"commentRangeStart",
|
|
8286
|
+
"commentRangeEnd",
|
|
8287
|
+
"commentReference",
|
|
8288
|
+
"customXmlInsRangeStart",
|
|
8289
|
+
"customXmlInsRangeEnd",
|
|
8290
|
+
"customXmlDelRangeStart",
|
|
8291
|
+
"customXmlDelRangeEnd",
|
|
8292
|
+
"moveFromRangeStart",
|
|
8293
|
+
"moveFromRangeEnd",
|
|
8294
|
+
"moveToRangeStart",
|
|
8295
|
+
"moveToRangeEnd",
|
|
8296
|
+
"permStart",
|
|
8297
|
+
"permEnd",
|
|
8298
|
+
"proofErr"
|
|
8299
|
+
]);
|
|
8300
|
+
function localNameOf2(node) {
|
|
8301
|
+
return String(node?.localName || node?.nodeName || "").replace(/^.*:/, "");
|
|
8302
|
+
}
|
|
8303
|
+
function directElementChildren(node) {
|
|
8304
|
+
return Array.from(node?.childNodes || []).filter((child) => child.nodeType === 1);
|
|
8305
|
+
}
|
|
8306
|
+
function directChild(node, localName2) {
|
|
8307
|
+
return directElementChildren(node).find((child) => localNameOf2(child) === localName2) || null;
|
|
8308
|
+
}
|
|
8309
|
+
function wordAttribute2(node, localName2) {
|
|
8310
|
+
return node?.getAttribute?.(`w:${localName2}`) || node?.getAttribute?.(localName2) || "";
|
|
8311
|
+
}
|
|
8312
|
+
function normalizedAuthor(author) {
|
|
8313
|
+
return String(author || "").trim().toLowerCase();
|
|
8314
|
+
}
|
|
8315
|
+
function isAnchorOnlyRun(node) {
|
|
8316
|
+
if (localNameOf2(node) !== "r") return false;
|
|
8317
|
+
return directElementChildren(node).every((child) => [
|
|
8318
|
+
"rPr",
|
|
8319
|
+
"commentReference",
|
|
8320
|
+
"bookmarkStart",
|
|
8321
|
+
"bookmarkEnd",
|
|
8322
|
+
"commentRangeStart",
|
|
8323
|
+
"commentRangeEnd",
|
|
8324
|
+
"proofErr"
|
|
8325
|
+
].includes(localNameOf2(child)));
|
|
8326
|
+
}
|
|
8327
|
+
function isNonContentChild(node) {
|
|
8328
|
+
return NON_CONTENT_CHILDREN.has(localNameOf2(node)) || isAnchorOnlyRun(node);
|
|
8329
|
+
}
|
|
8330
|
+
function isWhollyDeletedContentNode(node) {
|
|
8331
|
+
if (localNameOf2(node) === "del") return true;
|
|
8332
|
+
if (!["customXml", "smartTag", "sdt", "sdtContent"].includes(localNameOf2(node))) return false;
|
|
8333
|
+
const contentChildren = directElementChildren(node).filter((child) => !isNonContentChild(child) && localNameOf2(child) !== "sdtPr");
|
|
8334
|
+
return contentChildren.every(isWhollyDeletedContentNode);
|
|
8335
|
+
}
|
|
8336
|
+
function paragraphMarkDeletion(paragraph) {
|
|
8337
|
+
const pPr = directChild(paragraph, "pPr");
|
|
8338
|
+
const rPr = directChild(pPr, "rPr");
|
|
8339
|
+
return directChild(rPr, "del");
|
|
8340
|
+
}
|
|
8341
|
+
function hasVisibleInsertionContent(insertion) {
|
|
8342
|
+
for (const node of Array.from(insertion?.getElementsByTagName?.("*") || [])) {
|
|
8343
|
+
const localName2 = localNameOf2(node);
|
|
8344
|
+
if (!["t", "tab", "br", "cr", "noBreakHyphen", "softHyphen"].includes(localName2)) continue;
|
|
8345
|
+
let ancestor = node.parentNode;
|
|
8346
|
+
let hidden = false;
|
|
8347
|
+
while (ancestor && ancestor !== insertion) {
|
|
8348
|
+
const ancestorName = localNameOf2(ancestor);
|
|
8349
|
+
if (ancestorName === "del" || ancestorName === "moveFrom") {
|
|
8350
|
+
hidden = true;
|
|
8351
|
+
break;
|
|
8352
|
+
}
|
|
8353
|
+
ancestor = ancestor.parentNode;
|
|
8354
|
+
}
|
|
8355
|
+
if (hidden) continue;
|
|
8356
|
+
if (localName2 !== "t" || (node.textContent || "").length > 0) return true;
|
|
8357
|
+
}
|
|
8358
|
+
return false;
|
|
8359
|
+
}
|
|
8360
|
+
function inspectForeignDeletedParagraphTarget(paragraph, mutationAuthor) {
|
|
8361
|
+
const markDeletion = paragraphMarkDeletion(paragraph);
|
|
8362
|
+
if (!markDeletion) {
|
|
8363
|
+
return {
|
|
8364
|
+
matches: false,
|
|
8365
|
+
hasParagraphMarkDeletion: false,
|
|
8366
|
+
foreignParagraphMarkDeletion: false,
|
|
8367
|
+
allContentDeleted: false,
|
|
8368
|
+
ownerAuthor: null,
|
|
8369
|
+
markDeletion: null
|
|
8370
|
+
};
|
|
8371
|
+
}
|
|
8372
|
+
const ownerAuthor = wordAttribute2(markDeletion, "author") || null;
|
|
8373
|
+
const foreignParagraphMarkDeletion = !ownerAuthor || normalizedAuthor(ownerAuthor) !== normalizedAuthor(mutationAuthor);
|
|
8374
|
+
const contentChildren = directElementChildren(paragraph).filter((child) => !isNonContentChild(child));
|
|
8375
|
+
const allContentDeleted = contentChildren.every(isWhollyDeletedContentNode);
|
|
8376
|
+
return {
|
|
8377
|
+
matches: foreignParagraphMarkDeletion && allContentDeleted,
|
|
8378
|
+
hasParagraphMarkDeletion: true,
|
|
8379
|
+
foreignParagraphMarkDeletion,
|
|
8380
|
+
allContentDeleted,
|
|
8381
|
+
ownerAuthor,
|
|
8382
|
+
markDeletion
|
|
8383
|
+
};
|
|
8384
|
+
}
|
|
8385
|
+
function findForeignDeletedParagraphResurrections(root) {
|
|
8386
|
+
const paragraphs = localNameOf2(root) === "p" ? [root] : Array.from(root?.getElementsByTagName?.("*") || []).filter((node) => localNameOf2(node) === "p");
|
|
8387
|
+
const matches = [];
|
|
8388
|
+
for (const paragraph of paragraphs) {
|
|
8389
|
+
const markDeletion = paragraphMarkDeletion(paragraph);
|
|
8390
|
+
if (!markDeletion) continue;
|
|
8391
|
+
const ownerAuthor = wordAttribute2(markDeletion, "author") || null;
|
|
8392
|
+
const contentChildren = directElementChildren(paragraph).filter((child) => !isNonContentChild(child));
|
|
8393
|
+
const foreignInsertions = contentChildren.filter((child) => {
|
|
8394
|
+
if (localNameOf2(child) !== "ins" || !hasVisibleInsertionContent(child)) return false;
|
|
8395
|
+
return normalizedAuthor(wordAttribute2(child, "author")) !== normalizedAuthor(ownerAuthor);
|
|
8396
|
+
});
|
|
8397
|
+
const onlyDeletedContentAndForeignInsertions = contentChildren.every((child) => {
|
|
8398
|
+
return isWhollyDeletedContentNode(child) || foreignInsertions.includes(child);
|
|
8399
|
+
});
|
|
8400
|
+
if (foreignInsertions.length > 0 && onlyDeletedContentAndForeignInsertions) {
|
|
8401
|
+
matches.push({ paragraph, markDeletion, ownerAuthor, foreignInsertions });
|
|
8402
|
+
}
|
|
8403
|
+
}
|
|
8404
|
+
return matches;
|
|
8405
|
+
}
|
|
8406
|
+
|
|
8056
8407
|
// engine/oxml-engine.js
|
|
8057
8408
|
function getCommentIdsInOoxml(node) {
|
|
8058
8409
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -8133,6 +8484,23 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
8133
8484
|
}
|
|
8134
8485
|
const revisionIdAllocator = options?._revisionIdAllocator instanceof RevisionIdAllocator ? options._revisionIdAllocator : new RevisionIdAllocator();
|
|
8135
8486
|
seedRevisionIdsFromDocument(xmlDoc, revisionIdAllocator);
|
|
8487
|
+
const inputParagraphs = xmlDoc.documentElement && String(xmlDoc.documentElement.localName || "").toLowerCase() === "p" ? [xmlDoc.documentElement] : getDocumentParagraphs(xmlDoc);
|
|
8488
|
+
if (inputParagraphs.length === 1 && modifiedText.length > 0) {
|
|
8489
|
+
const resurrectionTarget = inspectForeignDeletedParagraphTarget(inputParagraphs[0], author);
|
|
8490
|
+
if (resurrectionTarget.matches) {
|
|
8491
|
+
const ownerAuthor = resurrectionTarget.ownerAuthor || "unattributed";
|
|
8492
|
+
return finalize({
|
|
8493
|
+
oxml: inputOoxml,
|
|
8494
|
+
hasChanges: false,
|
|
8495
|
+
status: "error",
|
|
8496
|
+
error: {
|
|
8497
|
+
code: "FOREIGN_PARAGRAPH_MARK_DELETION",
|
|
8498
|
+
message: `Refusing to add visible text to a paragraph whose paragraph mark is deleted by another author (${ownerAuthor}). Use explicit paragraph restoration when supported.`,
|
|
8499
|
+
ownerAuthor
|
|
8500
|
+
}
|
|
8501
|
+
});
|
|
8502
|
+
}
|
|
8503
|
+
}
|
|
8136
8504
|
if (containsTrackedChanges(xmlDoc)) {
|
|
8137
8505
|
if (existingRevisionsPolicy === "merge-same-author" || existingRevisionsPolicy === "slice-cross-author") {
|
|
8138
8506
|
const authors = getTrackedChangeAuthors(xmlDoc);
|
|
@@ -8839,6 +9207,23 @@ function resolveTargetParagraph(xmlDoc, options = {}) {
|
|
|
8839
9207
|
}
|
|
8840
9208
|
return { paragraph: byId, resolvedBy: "paragraph_id" };
|
|
8841
9209
|
}
|
|
9210
|
+
if (descriptor?.fingerprint && !cleanTargetText && !parsedRef) {
|
|
9211
|
+
let fingerprintCandidates = (paragraphMetadataIndex?.entries || []).filter((candidate) => candidate.fingerprint === descriptor.fingerprint);
|
|
9212
|
+
if (typeof descriptor.inTable === "boolean") {
|
|
9213
|
+
fingerprintCandidates = fingerprintCandidates.filter((candidate) => candidate.inTable === descriptor.inTable);
|
|
9214
|
+
}
|
|
9215
|
+
if (fingerprintCandidates.length === 1) {
|
|
9216
|
+
return { paragraph: fingerprintCandidates[0].paragraph, resolvedBy: "fingerprint" };
|
|
9217
|
+
}
|
|
9218
|
+
if (fingerprintCandidates.length > 1) {
|
|
9219
|
+
throw createTargetError(
|
|
9220
|
+
"AMBIGUOUS_TARGET",
|
|
9221
|
+
"Target fingerprint matched multiple paragraphs; provide paragraphId or index.",
|
|
9222
|
+
fingerprintCandidates.map(serializeTargetCandidate)
|
|
9223
|
+
);
|
|
9224
|
+
}
|
|
9225
|
+
throw createTargetError("TARGET_NOT_FOUND", `Target fingerprint not found: "${descriptor.fingerprint}".`);
|
|
9226
|
+
}
|
|
8842
9227
|
let candidates = [];
|
|
8843
9228
|
if (cleanTargetText) {
|
|
8844
9229
|
const unfilteredCandidates = findStrictTargetCandidates(xmlDoc, cleanTargetText, paragraphMetadataIndex);
|
|
@@ -9867,20 +10252,20 @@ async function executeSingleLineListStructuralFallback(plan, options = {}) {
|
|
|
9867
10252
|
// core/redline-validation.js
|
|
9868
10253
|
var REVISION_ID_ELEMENTS = /* @__PURE__ */ new Set(["ins", "del", "rPrChange", "pPrChange"]);
|
|
9869
10254
|
var REVISION_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}T/;
|
|
9870
|
-
function
|
|
10255
|
+
function localNameOf3(node) {
|
|
9871
10256
|
return String(node?.localName || node?.nodeName || "").replace(/^.*:/, "");
|
|
9872
10257
|
}
|
|
9873
10258
|
function elementsByLocalName(root, name) {
|
|
9874
|
-
return Array.from(root.getElementsByTagName("*")).filter((el) =>
|
|
10259
|
+
return Array.from(root.getElementsByTagName("*")).filter((el) => localNameOf3(el) === name);
|
|
9875
10260
|
}
|
|
9876
|
-
function
|
|
10261
|
+
function wordAttribute3(node, name) {
|
|
9877
10262
|
return node.getAttribute(`w:${name}`) || node.getAttribute(name) || "";
|
|
9878
10263
|
}
|
|
9879
10264
|
function xmlSpaceAttribute(node) {
|
|
9880
10265
|
return node.getAttribute("xml:space") || node.getAttribute("space") || node.getAttributeNS?.("http://www.w3.org/XML/1998/namespace", "space") || "";
|
|
9881
10266
|
}
|
|
9882
10267
|
function isParagraphMarkRevision(node) {
|
|
9883
|
-
return
|
|
10268
|
+
return localNameOf3(node.parentNode) === "rPr";
|
|
9884
10269
|
}
|
|
9885
10270
|
function parseOoxmlForValidation(oxml) {
|
|
9886
10271
|
const attempt = (xml) => {
|
|
@@ -9904,11 +10289,12 @@ function parseOoxmlForValidation(oxml) {
|
|
|
9904
10289
|
function validateRedlineOoxml(oxml) {
|
|
9905
10290
|
const issues = [];
|
|
9906
10291
|
const addIssue = (code, severity, message) => issues.push({ code, severity, message });
|
|
9907
|
-
|
|
9908
|
-
|
|
10292
|
+
const isDomNode = oxml && typeof oxml === "object" && (oxml.nodeType === 9 || oxml.nodeType === 1);
|
|
10293
|
+
if (!isDomNode && (typeof oxml !== "string" || oxml.trim() === "")) {
|
|
10294
|
+
addIssue("PARSE_ERROR", "error", "Input is not non-empty OOXML or a parsed XML DOM.");
|
|
9909
10295
|
return { valid: false, issues };
|
|
9910
10296
|
}
|
|
9911
|
-
const { doc, error: error2 } = parseOoxmlForValidation(oxml);
|
|
10297
|
+
const { doc, error: error2 } = isDomNode ? { doc: oxml.nodeType === 9 ? oxml : oxml.ownerDocument } : parseOoxmlForValidation(oxml);
|
|
9912
10298
|
if (!doc) {
|
|
9913
10299
|
addIssue("PARSE_ERROR", "error", `OOXML does not parse as XML: ${error2}`);
|
|
9914
10300
|
return { valid: false, issues };
|
|
@@ -9917,14 +10303,14 @@ function validateRedlineOoxml(oxml) {
|
|
|
9917
10303
|
const delElements = elementsByLocalName(doc, "del");
|
|
9918
10304
|
const revisions = insElements.concat(delElements);
|
|
9919
10305
|
for (const paragraph of elementsByLocalName(doc, "p")) {
|
|
9920
|
-
const nested = Array.from(paragraph.getElementsByTagName("*")).find((el) => el !== paragraph &&
|
|
10306
|
+
const nested = Array.from(paragraph.getElementsByTagName("*")).find((el) => el !== paragraph && localNameOf3(el) === "p");
|
|
9921
10307
|
if (nested) {
|
|
9922
10308
|
addIssue("NESTED_PARAGRAPH", "error", `<${paragraph.nodeName}> contains nested <${nested.nodeName}>.`);
|
|
9923
10309
|
}
|
|
9924
10310
|
}
|
|
9925
10311
|
for (const body of elementsByLocalName(doc, "body")) {
|
|
9926
10312
|
const children = Array.from(body.childNodes || []).filter((child) => child.nodeType === 1);
|
|
9927
|
-
const sectPrIndexes = children.map((child, index) =>
|
|
10313
|
+
const sectPrIndexes = children.map((child, index) => localNameOf3(child) === "sectPr" ? index : -1).filter((index) => index >= 0);
|
|
9928
10314
|
if (sectPrIndexes.length > 1) {
|
|
9929
10315
|
addIssue("MULTIPLE_BODY_SECTPR", "error", "<w:body> contains multiple direct <w:sectPr> elements.");
|
|
9930
10316
|
} else if (sectPrIndexes.length === 1 && sectPrIndexes[0] !== children.length - 1) {
|
|
@@ -9932,16 +10318,16 @@ function validateRedlineOoxml(oxml) {
|
|
|
9932
10318
|
}
|
|
9933
10319
|
}
|
|
9934
10320
|
for (const revision of revisions) {
|
|
9935
|
-
const nested = Array.from(revision.getElementsByTagName("*")).filter((el) => el !== revision && ["ins", "del"].includes(
|
|
10321
|
+
const nested = Array.from(revision.getElementsByTagName("*")).filter((el) => el !== revision && ["ins", "del"].includes(localNameOf3(el)));
|
|
9936
10322
|
const invalidNested = nested.find((candidate) => {
|
|
9937
|
-
if (
|
|
10323
|
+
if (localNameOf3(revision) !== "ins" || localNameOf3(candidate) !== "del") return true;
|
|
9938
10324
|
return candidate.parentNode !== revision;
|
|
9939
10325
|
});
|
|
9940
10326
|
if (invalidNested) {
|
|
9941
10327
|
addIssue(
|
|
9942
10328
|
"NESTED_REVISION",
|
|
9943
10329
|
"error",
|
|
9944
|
-
`<${revision.nodeName}> (w:id="${
|
|
10330
|
+
`<${revision.nodeName}> (w:id="${wordAttribute3(revision, "id")}") contains invalid nested <${invalidNested.nodeName}>.`
|
|
9945
10331
|
);
|
|
9946
10332
|
}
|
|
9947
10333
|
}
|
|
@@ -9951,15 +10337,15 @@ function validateRedlineOoxml(oxml) {
|
|
|
9951
10337
|
addIssue(
|
|
9952
10338
|
"DEL_CONTAINS_T",
|
|
9953
10339
|
"error",
|
|
9954
|
-
`<w:del> (w:id="${
|
|
10340
|
+
`<w:del> (w:id="${wordAttribute3(del, "id")}") contains <w:t>; deleted text must use <w:delText>.`
|
|
9955
10341
|
);
|
|
9956
10342
|
}
|
|
9957
10343
|
}
|
|
9958
10344
|
for (const revision of revisions) {
|
|
9959
10345
|
const missing = [];
|
|
9960
|
-
if (!
|
|
9961
|
-
if (!
|
|
9962
|
-
if (!REVISION_DATE_PATTERN.test(
|
|
10346
|
+
if (!wordAttribute3(revision, "id")) missing.push("w:id");
|
|
10347
|
+
if (!wordAttribute3(revision, "author")) missing.push("w:author");
|
|
10348
|
+
if (!REVISION_DATE_PATTERN.test(wordAttribute3(revision, "date"))) missing.push("w:date");
|
|
9963
10349
|
if (missing.length > 0) {
|
|
9964
10350
|
addIssue(
|
|
9965
10351
|
"MISSING_REVISION_METADATA",
|
|
@@ -9970,8 +10356,8 @@ function validateRedlineOoxml(oxml) {
|
|
|
9970
10356
|
}
|
|
9971
10357
|
const seenIds = /* @__PURE__ */ new Set();
|
|
9972
10358
|
for (const node of Array.from(doc.getElementsByTagName("*"))) {
|
|
9973
|
-
if (!REVISION_ID_ELEMENTS.has(
|
|
9974
|
-
const id =
|
|
10359
|
+
if (!REVISION_ID_ELEMENTS.has(localNameOf3(node))) continue;
|
|
10360
|
+
const id = wordAttribute3(node, "id");
|
|
9975
10361
|
if (!id) continue;
|
|
9976
10362
|
if (seenIds.has(id)) {
|
|
9977
10363
|
addIssue("DUPLICATE_REVISION_ID", "error", `Revision id ${id} appears more than once.`);
|
|
@@ -9999,10 +10385,18 @@ function validateRedlineOoxml(oxml) {
|
|
|
9999
10385
|
addIssue(
|
|
10000
10386
|
"EMPTY_REVISION_WRAPPER",
|
|
10001
10387
|
"warning",
|
|
10002
|
-
`<${revision.nodeName}> (w:id="${
|
|
10388
|
+
`<${revision.nodeName}> (w:id="${wordAttribute3(revision, "id")}") wraps no content.`
|
|
10003
10389
|
);
|
|
10004
10390
|
}
|
|
10005
10391
|
}
|
|
10392
|
+
for (const resurrection of findForeignDeletedParagraphResurrections(doc)) {
|
|
10393
|
+
const ownerAuthor = resurrection.ownerAuthor || "unattributed";
|
|
10394
|
+
addIssue(
|
|
10395
|
+
"FOREIGN_PARAGRAPH_MARK_DELETION",
|
|
10396
|
+
"warning",
|
|
10397
|
+
`Paragraph deleted by ${ownerAuthor} also contains non-empty insertion content from another author; Accept/Reject lifecycle may not preserve the apparent restoration.`
|
|
10398
|
+
);
|
|
10399
|
+
}
|
|
10006
10400
|
return { valid: !issues.some((issue) => issue.severity === "error"), issues };
|
|
10007
10401
|
}
|
|
10008
10402
|
|