@ansonlai/docx-redline-js 0.5.2 → 0.5.3
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 +24 -3
- package/CHANGELOG.md +13 -1
- package/README.md +16 -3
- package/core/paragraph-revision-safety.js +213 -0
- package/core/paragraph-targeting.js +19 -0
- package/core/redline-validation.js +13 -0
- package/dist/docx-redline-js.esm.js +333 -89
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +79 -79
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/docs/plans/2026-09-08-cross-author-revision-slicing.md +282 -1
- package/docs/schemas/document-operations.schema.json +3 -0
- package/engine/oxml-engine.js +24 -3
- package/engine/surgical-diff-application.js +35 -1
- package/engine/surgical-mode.js +55 -1
- package/index.d.ts +12 -0
- package/package.json +1 -1
- package/services/document-operation-applier.js +24 -5
- package/services/document-operation-contract.js +37 -2
- package/services/document-operation-mutations.js +509 -29
- package/services/operation-preflight.js +72 -8
- package/services/standalone-operation-runner.d.ts +8 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @ansonlai/docx-redline-js v0.5.
|
|
1
|
+
// @ansonlai/docx-redline-js v0.5.3 — 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;
|
|
@@ -6160,7 +6160,15 @@ function processDelete(xmlDoc, spanIndex, startPos, endPos, author, generateRedl
|
|
|
6160
6160
|
if (delWrapper && record.deletedPieces.length > 0) {
|
|
6161
6161
|
delWrapper.appendChild(createRunFromPieces(xmlDoc, record.deletedPieces, record.rPr));
|
|
6162
6162
|
}
|
|
6163
|
-
insertRunPiecesBefore(xmlDoc, parent, runElement, record.afterPieces, record.rPr);
|
|
6163
|
+
const afterRun = insertRunPiecesBefore(xmlDoc, parent, runElement, record.afterPieces, record.rPr);
|
|
6164
|
+
if (record.globalEnd === endPos && !isWordElement(parent, "ins")) {
|
|
6165
|
+
if (!spanIndex.replacementInsertionAnchors) spanIndex.replacementInsertionAnchors = /* @__PURE__ */ new Map();
|
|
6166
|
+
spanIndex.replacementInsertionAnchors.set(endPos, {
|
|
6167
|
+
parent,
|
|
6168
|
+
referenceNode: afterRun || runElement.nextSibling,
|
|
6169
|
+
rPr: record.rPr
|
|
6170
|
+
});
|
|
6171
|
+
}
|
|
6164
6172
|
parent.removeChild(runElement);
|
|
6165
6173
|
changed = true;
|
|
6166
6174
|
}
|
|
@@ -6197,6 +6205,23 @@ function processInsert(xmlDoc, spanIndex, pos, text, author, formatHints = [], i
|
|
|
6197
6205
|
revisionMetadata
|
|
6198
6206
|
);
|
|
6199
6207
|
}
|
|
6208
|
+
const replacementAnchor = spanIndex.replacementInsertionAnchors?.get(pos) || null;
|
|
6209
|
+
if (replacementAnchor && !affinity && isConnected(replacementAnchor.parent) && (!replacementAnchor.referenceNode || replacementAnchor.referenceNode.parentNode === replacementAnchor.parent)) {
|
|
6210
|
+
spanIndex.replacementInsertionAnchors.delete(pos);
|
|
6211
|
+
insertTextRuns(
|
|
6212
|
+
xmlDoc,
|
|
6213
|
+
replacementAnchor.parent,
|
|
6214
|
+
replacementAnchor.referenceNode,
|
|
6215
|
+
text,
|
|
6216
|
+
replacementAnchor.rPr,
|
|
6217
|
+
author,
|
|
6218
|
+
formatHints,
|
|
6219
|
+
insertOffset,
|
|
6220
|
+
generateRedlines,
|
|
6221
|
+
revisionMetadata
|
|
6222
|
+
);
|
|
6223
|
+
return true;
|
|
6224
|
+
}
|
|
6200
6225
|
if (!affinity) {
|
|
6201
6226
|
let targetSpan = findContainingSpan(spanIndex, pos);
|
|
6202
6227
|
if (!targetSpan && pos > 0) {
|
|
@@ -6566,61 +6591,111 @@ function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer, autho
|
|
|
6566
6591
|
let originalPos = 0;
|
|
6567
6592
|
let newPos = 0;
|
|
6568
6593
|
let hasChanges = false;
|
|
6569
|
-
|
|
6570
|
-
|
|
6571
|
-
|
|
6572
|
-
const
|
|
6573
|
-
const
|
|
6574
|
-
const
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6594
|
+
const insertionOperations = insertionOnlyDiffs ? collectInsertionOperations(insertionOnlyDiffs) : [];
|
|
6595
|
+
if (insertionOperations.length > 1 && formatHints.length === 0) {
|
|
6596
|
+
for (const operation of insertionOperations.slice().reverse()) {
|
|
6597
|
+
const liveSpans = buildSurgicalTextSpans(allParagraphs).textSpans;
|
|
6598
|
+
const liveSpanIndex = buildSpanIndex(liveSpans);
|
|
6599
|
+
const textWithoutNewlines = operation.text.replace(/\n/g, " ");
|
|
6600
|
+
if (textWithoutNewlines.length === 0) continue;
|
|
6601
|
+
const insertResult = processInsert(
|
|
6602
|
+
xmlDoc,
|
|
6603
|
+
liveSpanIndex,
|
|
6604
|
+
operation.originalPos,
|
|
6605
|
+
textWithoutNewlines,
|
|
6606
|
+
author,
|
|
6607
|
+
formatHints,
|
|
6608
|
+
operation.newPos,
|
|
6609
|
+
generateRedlines,
|
|
6610
|
+
allParagraphs[0] || null,
|
|
6611
|
+
null,
|
|
6612
|
+
options?.insertionAffinity || null,
|
|
6613
|
+
options?.existingRevisions || "merge-same-author"
|
|
6614
|
+
);
|
|
6615
|
+
if (insertResult && typeof insertResult === "object" && insertResult.error) {
|
|
6616
|
+
return withOoxmlSourceType({
|
|
6617
|
+
oxml: serializer.serializeToString(xmlDoc),
|
|
6618
|
+
hasChanges: false,
|
|
6619
|
+
status: "error",
|
|
6620
|
+
error: insertResult.error
|
|
6621
|
+
});
|
|
6622
|
+
}
|
|
6623
|
+
if (insertResult === true) hasChanges = true;
|
|
6624
|
+
}
|
|
6625
|
+
} else {
|
|
6626
|
+
for (let i = 0; i < diffs.length; i++) {
|
|
6627
|
+
const [op, text] = diffs[i];
|
|
6628
|
+
if (op === 0) {
|
|
6629
|
+
const len = text.length;
|
|
6630
|
+
const startPos = originalPos;
|
|
6631
|
+
const endPos = originalPos + len;
|
|
6632
|
+
forEachOverlappingSpan(spanIndex, startPos, endPos, (span) => {
|
|
6633
|
+
const overlapStartOriginal = Math.max(span.charStart, startPos);
|
|
6634
|
+
const overlapEndOriginal = Math.min(span.charEnd, endPos);
|
|
6635
|
+
const segmentLen = overlapEndOriginal - overlapStartOriginal;
|
|
6636
|
+
const relativeOffset = overlapStartOriginal - startPos;
|
|
6637
|
+
const overlapStartNew = newPos + relativeOffset;
|
|
6638
|
+
const overlapEndNew = overlapStartNew + segmentLen;
|
|
6639
|
+
const applicableHints = getApplicableFormatHints(formatHints, overlapStartNew, overlapEndNew);
|
|
6640
|
+
if (reconcileFormattingForTextSpan(xmlDoc, span, overlapStartOriginal, overlapEndOriginal, applicableHints, author, generateRedlines)) {
|
|
6641
|
+
hasChanges = true;
|
|
6642
|
+
}
|
|
6643
|
+
});
|
|
6644
|
+
originalPos += len;
|
|
6645
|
+
newPos += len;
|
|
6646
|
+
} else if (op === -1) {
|
|
6647
|
+
const hasNextInsert = i + 1 < diffs.length && diffs[i + 1][0] === 1;
|
|
6648
|
+
let paired = false;
|
|
6649
|
+
let delMetadata = null;
|
|
6650
|
+
let insMetadata = null;
|
|
6651
|
+
if (pairReplacements && generateRedlines && hasNextInsert) {
|
|
6652
|
+
const nextText = diffs[i + 1][1];
|
|
6653
|
+
const textWithoutNewlines = nextText.replace(/\n/g, " ");
|
|
6654
|
+
if (textWithoutNewlines.length > 0) {
|
|
6655
|
+
const checkResult = checkSafeAdjacencyForPairing(
|
|
6656
|
+
spanIndex,
|
|
6657
|
+
originalPos,
|
|
6658
|
+
originalPos + text.length,
|
|
6659
|
+
options?.existingRevisions === "slice-cross-author"
|
|
6660
|
+
);
|
|
6661
|
+
if (checkResult.safe) {
|
|
6662
|
+
const event = createReplacementRevisionEvent(author, xmlDoc);
|
|
6663
|
+
delMetadata = { id: event.deletionId, author: event.author, date: event.date };
|
|
6664
|
+
insMetadata = { id: event.insertionId, author: event.author, date: event.date };
|
|
6665
|
+
paired = true;
|
|
6666
|
+
} else if (checkResult.structuralBoundary) {
|
|
6667
|
+
warnings.push("PAIRING_SKIPPED_STRUCTURAL_BOUNDARY");
|
|
6668
|
+
}
|
|
6669
|
+
}
|
|
6670
|
+
}
|
|
6671
|
+
if (processDelete(xmlDoc, spanIndex, originalPos, originalPos + text.length, author, generateRedlines, delMetadata)) {
|
|
6584
6672
|
hasChanges = true;
|
|
6585
6673
|
}
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
if (checkResult.safe) {
|
|
6605
|
-
const event = createReplacementRevisionEvent(author, xmlDoc);
|
|
6606
|
-
delMetadata = { id: event.deletionId, author: event.author, date: event.date };
|
|
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");
|
|
6674
|
+
originalPos += text.length;
|
|
6675
|
+
if (paired) {
|
|
6676
|
+
i++;
|
|
6677
|
+
const [, nextText] = diffs[i];
|
|
6678
|
+
const textWithoutNewlines = nextText.replace(/\n/g, " ");
|
|
6679
|
+
if (textWithoutNewlines.length > 0) {
|
|
6680
|
+
const insertResult = processInsert(xmlDoc, spanIndex, originalPos, textWithoutNewlines, author, formatHints, newPos, generateRedlines, allParagraphs[0] || null, insMetadata, options?.insertionAffinity || null, options?.existingRevisions || "merge-same-author");
|
|
6681
|
+
if (insertResult && typeof insertResult === "object" && insertResult.error) {
|
|
6682
|
+
return withOoxmlSourceType({
|
|
6683
|
+
oxml: serializer.serializeToString(xmlDoc),
|
|
6684
|
+
hasChanges: false,
|
|
6685
|
+
status: "error",
|
|
6686
|
+
error: insertResult.error
|
|
6687
|
+
});
|
|
6688
|
+
}
|
|
6689
|
+
if (insertResult === true) {
|
|
6690
|
+
hasChanges = true;
|
|
6691
|
+
}
|
|
6611
6692
|
}
|
|
6693
|
+
newPos += nextText.length;
|
|
6612
6694
|
}
|
|
6613
|
-
}
|
|
6614
|
-
|
|
6615
|
-
hasChanges = true;
|
|
6616
|
-
}
|
|
6617
|
-
originalPos += text.length;
|
|
6618
|
-
if (paired) {
|
|
6619
|
-
i++;
|
|
6620
|
-
const [, nextText] = diffs[i];
|
|
6621
|
-
const textWithoutNewlines = nextText.replace(/\n/g, " ");
|
|
6695
|
+
} else if (op === 1) {
|
|
6696
|
+
const textWithoutNewlines = text.replace(/\n/g, " ");
|
|
6622
6697
|
if (textWithoutNewlines.length > 0) {
|
|
6623
|
-
const insertResult = processInsert(xmlDoc, spanIndex, originalPos, textWithoutNewlines, author, formatHints, newPos, generateRedlines, allParagraphs[0] || null,
|
|
6698
|
+
const insertResult = processInsert(xmlDoc, spanIndex, originalPos, textWithoutNewlines, author, formatHints, newPos, generateRedlines, allParagraphs[0] || null, null, options?.insertionAffinity || null, options?.existingRevisions || "merge-same-author");
|
|
6624
6699
|
if (insertResult && typeof insertResult === "object" && insertResult.error) {
|
|
6625
6700
|
return withOoxmlSourceType({
|
|
6626
6701
|
oxml: serializer.serializeToString(xmlDoc),
|
|
@@ -6633,25 +6708,8 @@ function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer, autho
|
|
|
6633
6708
|
hasChanges = true;
|
|
6634
6709
|
}
|
|
6635
6710
|
}
|
|
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
|
-
}
|
|
6711
|
+
newPos += text.length;
|
|
6653
6712
|
}
|
|
6654
|
-
newPos += text.length;
|
|
6655
6713
|
}
|
|
6656
6714
|
}
|
|
6657
6715
|
const actualText = allParagraphs.map((paragraph) => extractCanonicalParagraphText(paragraph)).join("\n");
|
|
@@ -6690,6 +6748,23 @@ function excerptAt(text, offset, radius = 40) {
|
|
|
6690
6748
|
const end = Math.min(text.length, offset + radius);
|
|
6691
6749
|
return text.slice(start, end);
|
|
6692
6750
|
}
|
|
6751
|
+
function collectInsertionOperations(diffs) {
|
|
6752
|
+
const operations = [];
|
|
6753
|
+
let originalPos = 0;
|
|
6754
|
+
let newPos = 0;
|
|
6755
|
+
for (const [op, text] of diffs) {
|
|
6756
|
+
if (op === 0) {
|
|
6757
|
+
originalPos += text.length;
|
|
6758
|
+
newPos += text.length;
|
|
6759
|
+
} else if (op === -1) {
|
|
6760
|
+
originalPos += text.length;
|
|
6761
|
+
} else if (op === 1) {
|
|
6762
|
+
operations.push({ originalPos, newPos, text });
|
|
6763
|
+
newPos += text.length;
|
|
6764
|
+
}
|
|
6765
|
+
}
|
|
6766
|
+
return operations;
|
|
6767
|
+
}
|
|
6693
6768
|
|
|
6694
6769
|
// engine/reconstruction-mapper.js
|
|
6695
6770
|
var import_diff_match_patch2 = __toESM(require_diff_match_patch(), 1);
|
|
@@ -7592,8 +7667,8 @@ function resolveAuthorFilter(options = {}) {
|
|
|
7592
7667
|
if (options?.allAuthors === true) {
|
|
7593
7668
|
return { valid: true, allAuthors: true, normalizedAuthor: "" };
|
|
7594
7669
|
}
|
|
7595
|
-
const
|
|
7596
|
-
if (!
|
|
7670
|
+
const normalizedAuthor2 = normalizeAuthor(options?.author);
|
|
7671
|
+
if (!normalizedAuthor2) {
|
|
7597
7672
|
return {
|
|
7598
7673
|
valid: false,
|
|
7599
7674
|
allAuthors: false,
|
|
@@ -7601,7 +7676,7 @@ function resolveAuthorFilter(options = {}) {
|
|
|
7601
7676
|
warning: "No author provided. Pass { author } or set { allAuthors: true }."
|
|
7602
7677
|
};
|
|
7603
7678
|
}
|
|
7604
|
-
return { valid: true, allAuthors: false, normalizedAuthor };
|
|
7679
|
+
return { valid: true, allAuthors: false, normalizedAuthor: normalizedAuthor2 };
|
|
7605
7680
|
}
|
|
7606
7681
|
function authorMatchesNode(node, filter) {
|
|
7607
7682
|
if (filter.allAuthors) return true;
|
|
@@ -8053,6 +8128,133 @@ function recordRouteSelection(options, route, context = {}) {
|
|
|
8053
8128
|
}));
|
|
8054
8129
|
}
|
|
8055
8130
|
|
|
8131
|
+
// core/paragraph-revision-safety.js
|
|
8132
|
+
var NON_CONTENT_CHILDREN = /* @__PURE__ */ new Set([
|
|
8133
|
+
"pPr",
|
|
8134
|
+
"bookmarkStart",
|
|
8135
|
+
"bookmarkEnd",
|
|
8136
|
+
"commentRangeStart",
|
|
8137
|
+
"commentRangeEnd",
|
|
8138
|
+
"commentReference",
|
|
8139
|
+
"customXmlInsRangeStart",
|
|
8140
|
+
"customXmlInsRangeEnd",
|
|
8141
|
+
"customXmlDelRangeStart",
|
|
8142
|
+
"customXmlDelRangeEnd",
|
|
8143
|
+
"moveFromRangeStart",
|
|
8144
|
+
"moveFromRangeEnd",
|
|
8145
|
+
"moveToRangeStart",
|
|
8146
|
+
"moveToRangeEnd",
|
|
8147
|
+
"permStart",
|
|
8148
|
+
"permEnd",
|
|
8149
|
+
"proofErr"
|
|
8150
|
+
]);
|
|
8151
|
+
function localNameOf2(node) {
|
|
8152
|
+
return String(node?.localName || node?.nodeName || "").replace(/^.*:/, "");
|
|
8153
|
+
}
|
|
8154
|
+
function directElementChildren(node) {
|
|
8155
|
+
return Array.from(node?.childNodes || []).filter((child) => child.nodeType === 1);
|
|
8156
|
+
}
|
|
8157
|
+
function directChild(node, localName2) {
|
|
8158
|
+
return directElementChildren(node).find((child) => localNameOf2(child) === localName2) || null;
|
|
8159
|
+
}
|
|
8160
|
+
function wordAttribute2(node, localName2) {
|
|
8161
|
+
return node?.getAttribute?.(`w:${localName2}`) || node?.getAttribute?.(localName2) || "";
|
|
8162
|
+
}
|
|
8163
|
+
function normalizedAuthor(author) {
|
|
8164
|
+
return String(author || "").trim().toLowerCase();
|
|
8165
|
+
}
|
|
8166
|
+
function isAnchorOnlyRun(node) {
|
|
8167
|
+
if (localNameOf2(node) !== "r") return false;
|
|
8168
|
+
return directElementChildren(node).every((child) => [
|
|
8169
|
+
"rPr",
|
|
8170
|
+
"commentReference",
|
|
8171
|
+
"bookmarkStart",
|
|
8172
|
+
"bookmarkEnd",
|
|
8173
|
+
"commentRangeStart",
|
|
8174
|
+
"commentRangeEnd",
|
|
8175
|
+
"proofErr"
|
|
8176
|
+
].includes(localNameOf2(child)));
|
|
8177
|
+
}
|
|
8178
|
+
function isNonContentChild(node) {
|
|
8179
|
+
return NON_CONTENT_CHILDREN.has(localNameOf2(node)) || isAnchorOnlyRun(node);
|
|
8180
|
+
}
|
|
8181
|
+
function isWhollyDeletedContentNode(node) {
|
|
8182
|
+
if (localNameOf2(node) === "del") return true;
|
|
8183
|
+
if (!["customXml", "smartTag", "sdt", "sdtContent"].includes(localNameOf2(node))) return false;
|
|
8184
|
+
const contentChildren = directElementChildren(node).filter((child) => !isNonContentChild(child) && localNameOf2(child) !== "sdtPr");
|
|
8185
|
+
return contentChildren.every(isWhollyDeletedContentNode);
|
|
8186
|
+
}
|
|
8187
|
+
function paragraphMarkDeletion(paragraph) {
|
|
8188
|
+
const pPr = directChild(paragraph, "pPr");
|
|
8189
|
+
const rPr = directChild(pPr, "rPr");
|
|
8190
|
+
return directChild(rPr, "del");
|
|
8191
|
+
}
|
|
8192
|
+
function hasVisibleInsertionContent(insertion) {
|
|
8193
|
+
for (const node of Array.from(insertion?.getElementsByTagName?.("*") || [])) {
|
|
8194
|
+
const localName2 = localNameOf2(node);
|
|
8195
|
+
if (!["t", "tab", "br", "cr", "noBreakHyphen", "softHyphen"].includes(localName2)) continue;
|
|
8196
|
+
let ancestor = node.parentNode;
|
|
8197
|
+
let hidden = false;
|
|
8198
|
+
while (ancestor && ancestor !== insertion) {
|
|
8199
|
+
const ancestorName = localNameOf2(ancestor);
|
|
8200
|
+
if (ancestorName === "del" || ancestorName === "moveFrom") {
|
|
8201
|
+
hidden = true;
|
|
8202
|
+
break;
|
|
8203
|
+
}
|
|
8204
|
+
ancestor = ancestor.parentNode;
|
|
8205
|
+
}
|
|
8206
|
+
if (hidden) continue;
|
|
8207
|
+
if (localName2 !== "t" || (node.textContent || "").length > 0) return true;
|
|
8208
|
+
}
|
|
8209
|
+
return false;
|
|
8210
|
+
}
|
|
8211
|
+
function inspectForeignDeletedParagraphTarget(paragraph, mutationAuthor) {
|
|
8212
|
+
const markDeletion = paragraphMarkDeletion(paragraph);
|
|
8213
|
+
if (!markDeletion) {
|
|
8214
|
+
return {
|
|
8215
|
+
matches: false,
|
|
8216
|
+
hasParagraphMarkDeletion: false,
|
|
8217
|
+
foreignParagraphMarkDeletion: false,
|
|
8218
|
+
allContentDeleted: false,
|
|
8219
|
+
ownerAuthor: null,
|
|
8220
|
+
markDeletion: null
|
|
8221
|
+
};
|
|
8222
|
+
}
|
|
8223
|
+
const ownerAuthor = wordAttribute2(markDeletion, "author") || null;
|
|
8224
|
+
const foreignParagraphMarkDeletion = !ownerAuthor || normalizedAuthor(ownerAuthor) !== normalizedAuthor(mutationAuthor);
|
|
8225
|
+
const contentChildren = directElementChildren(paragraph).filter((child) => !isNonContentChild(child));
|
|
8226
|
+
const allContentDeleted = contentChildren.every(isWhollyDeletedContentNode);
|
|
8227
|
+
return {
|
|
8228
|
+
matches: foreignParagraphMarkDeletion && allContentDeleted,
|
|
8229
|
+
hasParagraphMarkDeletion: true,
|
|
8230
|
+
foreignParagraphMarkDeletion,
|
|
8231
|
+
allContentDeleted,
|
|
8232
|
+
ownerAuthor,
|
|
8233
|
+
markDeletion
|
|
8234
|
+
};
|
|
8235
|
+
}
|
|
8236
|
+
function findForeignDeletedParagraphResurrections(root) {
|
|
8237
|
+
const paragraphs = localNameOf2(root) === "p" ? [root] : Array.from(root?.getElementsByTagName?.("*") || []).filter((node) => localNameOf2(node) === "p");
|
|
8238
|
+
const matches = [];
|
|
8239
|
+
for (const paragraph of paragraphs) {
|
|
8240
|
+
const markDeletion = paragraphMarkDeletion(paragraph);
|
|
8241
|
+
if (!markDeletion) continue;
|
|
8242
|
+
const ownerAuthor = wordAttribute2(markDeletion, "author") || null;
|
|
8243
|
+
const contentChildren = directElementChildren(paragraph).filter((child) => !isNonContentChild(child));
|
|
8244
|
+
const foreignInsertions = contentChildren.filter((child) => {
|
|
8245
|
+
if (localNameOf2(child) !== "ins" || !hasVisibleInsertionContent(child)) return false;
|
|
8246
|
+
return normalizedAuthor(wordAttribute2(child, "author")) !== normalizedAuthor(ownerAuthor);
|
|
8247
|
+
});
|
|
8248
|
+
const onlyDeletedContentAndForeignInsertions = contentChildren.every((child) => {
|
|
8249
|
+
return isWhollyDeletedContentNode(child) || foreignInsertions.includes(child);
|
|
8250
|
+
});
|
|
8251
|
+
if (foreignInsertions.length > 0 && onlyDeletedContentAndForeignInsertions) {
|
|
8252
|
+
matches.push({ paragraph, markDeletion, ownerAuthor, foreignInsertions });
|
|
8253
|
+
}
|
|
8254
|
+
}
|
|
8255
|
+
return matches;
|
|
8256
|
+
}
|
|
8257
|
+
|
|
8056
8258
|
// engine/oxml-engine.js
|
|
8057
8259
|
function getCommentIdsInOoxml(node) {
|
|
8058
8260
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -8133,6 +8335,23 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
8133
8335
|
}
|
|
8134
8336
|
const revisionIdAllocator = options?._revisionIdAllocator instanceof RevisionIdAllocator ? options._revisionIdAllocator : new RevisionIdAllocator();
|
|
8135
8337
|
seedRevisionIdsFromDocument(xmlDoc, revisionIdAllocator);
|
|
8338
|
+
const inputParagraphs = xmlDoc.documentElement && String(xmlDoc.documentElement.localName || "").toLowerCase() === "p" ? [xmlDoc.documentElement] : getDocumentParagraphs(xmlDoc);
|
|
8339
|
+
if (inputParagraphs.length === 1 && modifiedText.length > 0) {
|
|
8340
|
+
const resurrectionTarget = inspectForeignDeletedParagraphTarget(inputParagraphs[0], author);
|
|
8341
|
+
if (resurrectionTarget.matches) {
|
|
8342
|
+
const ownerAuthor = resurrectionTarget.ownerAuthor || "unattributed";
|
|
8343
|
+
return finalize({
|
|
8344
|
+
oxml: inputOoxml,
|
|
8345
|
+
hasChanges: false,
|
|
8346
|
+
status: "error",
|
|
8347
|
+
error: {
|
|
8348
|
+
code: "FOREIGN_PARAGRAPH_MARK_DELETION",
|
|
8349
|
+
message: `Refusing to add visible text to a paragraph whose paragraph mark is deleted by another author (${ownerAuthor}). Use explicit paragraph restoration when supported.`,
|
|
8350
|
+
ownerAuthor
|
|
8351
|
+
}
|
|
8352
|
+
});
|
|
8353
|
+
}
|
|
8354
|
+
}
|
|
8136
8355
|
if (containsTrackedChanges(xmlDoc)) {
|
|
8137
8356
|
if (existingRevisionsPolicy === "merge-same-author" || existingRevisionsPolicy === "slice-cross-author") {
|
|
8138
8357
|
const authors = getTrackedChangeAuthors(xmlDoc);
|
|
@@ -8839,6 +9058,23 @@ function resolveTargetParagraph(xmlDoc, options = {}) {
|
|
|
8839
9058
|
}
|
|
8840
9059
|
return { paragraph: byId, resolvedBy: "paragraph_id" };
|
|
8841
9060
|
}
|
|
9061
|
+
if (descriptor?.fingerprint && !cleanTargetText && !parsedRef) {
|
|
9062
|
+
let fingerprintCandidates = (paragraphMetadataIndex?.entries || []).filter((candidate) => candidate.fingerprint === descriptor.fingerprint);
|
|
9063
|
+
if (typeof descriptor.inTable === "boolean") {
|
|
9064
|
+
fingerprintCandidates = fingerprintCandidates.filter((candidate) => candidate.inTable === descriptor.inTable);
|
|
9065
|
+
}
|
|
9066
|
+
if (fingerprintCandidates.length === 1) {
|
|
9067
|
+
return { paragraph: fingerprintCandidates[0].paragraph, resolvedBy: "fingerprint" };
|
|
9068
|
+
}
|
|
9069
|
+
if (fingerprintCandidates.length > 1) {
|
|
9070
|
+
throw createTargetError(
|
|
9071
|
+
"AMBIGUOUS_TARGET",
|
|
9072
|
+
"Target fingerprint matched multiple paragraphs; provide paragraphId or index.",
|
|
9073
|
+
fingerprintCandidates.map(serializeTargetCandidate)
|
|
9074
|
+
);
|
|
9075
|
+
}
|
|
9076
|
+
throw createTargetError("TARGET_NOT_FOUND", `Target fingerprint not found: "${descriptor.fingerprint}".`);
|
|
9077
|
+
}
|
|
8842
9078
|
let candidates = [];
|
|
8843
9079
|
if (cleanTargetText) {
|
|
8844
9080
|
const unfilteredCandidates = findStrictTargetCandidates(xmlDoc, cleanTargetText, paragraphMetadataIndex);
|
|
@@ -9867,20 +10103,20 @@ async function executeSingleLineListStructuralFallback(plan, options = {}) {
|
|
|
9867
10103
|
// core/redline-validation.js
|
|
9868
10104
|
var REVISION_ID_ELEMENTS = /* @__PURE__ */ new Set(["ins", "del", "rPrChange", "pPrChange"]);
|
|
9869
10105
|
var REVISION_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}T/;
|
|
9870
|
-
function
|
|
10106
|
+
function localNameOf3(node) {
|
|
9871
10107
|
return String(node?.localName || node?.nodeName || "").replace(/^.*:/, "");
|
|
9872
10108
|
}
|
|
9873
10109
|
function elementsByLocalName(root, name) {
|
|
9874
|
-
return Array.from(root.getElementsByTagName("*")).filter((el) =>
|
|
10110
|
+
return Array.from(root.getElementsByTagName("*")).filter((el) => localNameOf3(el) === name);
|
|
9875
10111
|
}
|
|
9876
|
-
function
|
|
10112
|
+
function wordAttribute3(node, name) {
|
|
9877
10113
|
return node.getAttribute(`w:${name}`) || node.getAttribute(name) || "";
|
|
9878
10114
|
}
|
|
9879
10115
|
function xmlSpaceAttribute(node) {
|
|
9880
10116
|
return node.getAttribute("xml:space") || node.getAttribute("space") || node.getAttributeNS?.("http://www.w3.org/XML/1998/namespace", "space") || "";
|
|
9881
10117
|
}
|
|
9882
10118
|
function isParagraphMarkRevision(node) {
|
|
9883
|
-
return
|
|
10119
|
+
return localNameOf3(node.parentNode) === "rPr";
|
|
9884
10120
|
}
|
|
9885
10121
|
function parseOoxmlForValidation(oxml) {
|
|
9886
10122
|
const attempt = (xml) => {
|
|
@@ -9917,14 +10153,14 @@ function validateRedlineOoxml(oxml) {
|
|
|
9917
10153
|
const delElements = elementsByLocalName(doc, "del");
|
|
9918
10154
|
const revisions = insElements.concat(delElements);
|
|
9919
10155
|
for (const paragraph of elementsByLocalName(doc, "p")) {
|
|
9920
|
-
const nested = Array.from(paragraph.getElementsByTagName("*")).find((el) => el !== paragraph &&
|
|
10156
|
+
const nested = Array.from(paragraph.getElementsByTagName("*")).find((el) => el !== paragraph && localNameOf3(el) === "p");
|
|
9921
10157
|
if (nested) {
|
|
9922
10158
|
addIssue("NESTED_PARAGRAPH", "error", `<${paragraph.nodeName}> contains nested <${nested.nodeName}>.`);
|
|
9923
10159
|
}
|
|
9924
10160
|
}
|
|
9925
10161
|
for (const body of elementsByLocalName(doc, "body")) {
|
|
9926
10162
|
const children = Array.from(body.childNodes || []).filter((child) => child.nodeType === 1);
|
|
9927
|
-
const sectPrIndexes = children.map((child, index) =>
|
|
10163
|
+
const sectPrIndexes = children.map((child, index) => localNameOf3(child) === "sectPr" ? index : -1).filter((index) => index >= 0);
|
|
9928
10164
|
if (sectPrIndexes.length > 1) {
|
|
9929
10165
|
addIssue("MULTIPLE_BODY_SECTPR", "error", "<w:body> contains multiple direct <w:sectPr> elements.");
|
|
9930
10166
|
} else if (sectPrIndexes.length === 1 && sectPrIndexes[0] !== children.length - 1) {
|
|
@@ -9932,16 +10168,16 @@ function validateRedlineOoxml(oxml) {
|
|
|
9932
10168
|
}
|
|
9933
10169
|
}
|
|
9934
10170
|
for (const revision of revisions) {
|
|
9935
|
-
const nested = Array.from(revision.getElementsByTagName("*")).filter((el) => el !== revision && ["ins", "del"].includes(
|
|
10171
|
+
const nested = Array.from(revision.getElementsByTagName("*")).filter((el) => el !== revision && ["ins", "del"].includes(localNameOf3(el)));
|
|
9936
10172
|
const invalidNested = nested.find((candidate) => {
|
|
9937
|
-
if (
|
|
10173
|
+
if (localNameOf3(revision) !== "ins" || localNameOf3(candidate) !== "del") return true;
|
|
9938
10174
|
return candidate.parentNode !== revision;
|
|
9939
10175
|
});
|
|
9940
10176
|
if (invalidNested) {
|
|
9941
10177
|
addIssue(
|
|
9942
10178
|
"NESTED_REVISION",
|
|
9943
10179
|
"error",
|
|
9944
|
-
`<${revision.nodeName}> (w:id="${
|
|
10180
|
+
`<${revision.nodeName}> (w:id="${wordAttribute3(revision, "id")}") contains invalid nested <${invalidNested.nodeName}>.`
|
|
9945
10181
|
);
|
|
9946
10182
|
}
|
|
9947
10183
|
}
|
|
@@ -9951,15 +10187,15 @@ function validateRedlineOoxml(oxml) {
|
|
|
9951
10187
|
addIssue(
|
|
9952
10188
|
"DEL_CONTAINS_T",
|
|
9953
10189
|
"error",
|
|
9954
|
-
`<w:del> (w:id="${
|
|
10190
|
+
`<w:del> (w:id="${wordAttribute3(del, "id")}") contains <w:t>; deleted text must use <w:delText>.`
|
|
9955
10191
|
);
|
|
9956
10192
|
}
|
|
9957
10193
|
}
|
|
9958
10194
|
for (const revision of revisions) {
|
|
9959
10195
|
const missing = [];
|
|
9960
|
-
if (!
|
|
9961
|
-
if (!
|
|
9962
|
-
if (!REVISION_DATE_PATTERN.test(
|
|
10196
|
+
if (!wordAttribute3(revision, "id")) missing.push("w:id");
|
|
10197
|
+
if (!wordAttribute3(revision, "author")) missing.push("w:author");
|
|
10198
|
+
if (!REVISION_DATE_PATTERN.test(wordAttribute3(revision, "date"))) missing.push("w:date");
|
|
9963
10199
|
if (missing.length > 0) {
|
|
9964
10200
|
addIssue(
|
|
9965
10201
|
"MISSING_REVISION_METADATA",
|
|
@@ -9970,8 +10206,8 @@ function validateRedlineOoxml(oxml) {
|
|
|
9970
10206
|
}
|
|
9971
10207
|
const seenIds = /* @__PURE__ */ new Set();
|
|
9972
10208
|
for (const node of Array.from(doc.getElementsByTagName("*"))) {
|
|
9973
|
-
if (!REVISION_ID_ELEMENTS.has(
|
|
9974
|
-
const id =
|
|
10209
|
+
if (!REVISION_ID_ELEMENTS.has(localNameOf3(node))) continue;
|
|
10210
|
+
const id = wordAttribute3(node, "id");
|
|
9975
10211
|
if (!id) continue;
|
|
9976
10212
|
if (seenIds.has(id)) {
|
|
9977
10213
|
addIssue("DUPLICATE_REVISION_ID", "error", `Revision id ${id} appears more than once.`);
|
|
@@ -9999,10 +10235,18 @@ function validateRedlineOoxml(oxml) {
|
|
|
9999
10235
|
addIssue(
|
|
10000
10236
|
"EMPTY_REVISION_WRAPPER",
|
|
10001
10237
|
"warning",
|
|
10002
|
-
`<${revision.nodeName}> (w:id="${
|
|
10238
|
+
`<${revision.nodeName}> (w:id="${wordAttribute3(revision, "id")}") wraps no content.`
|
|
10003
10239
|
);
|
|
10004
10240
|
}
|
|
10005
10241
|
}
|
|
10242
|
+
for (const resurrection of findForeignDeletedParagraphResurrections(doc)) {
|
|
10243
|
+
const ownerAuthor = resurrection.ownerAuthor || "unattributed";
|
|
10244
|
+
addIssue(
|
|
10245
|
+
"FOREIGN_PARAGRAPH_MARK_DELETION",
|
|
10246
|
+
"warning",
|
|
10247
|
+
`Paragraph deleted by ${ownerAuthor} also contains non-empty insertion content from another author; Accept/Reject lifecycle may not preserve the apparent restoration.`
|
|
10248
|
+
);
|
|
10249
|
+
}
|
|
10006
10250
|
return { valid: !issues.some((issue) => issue.severity === "error"), issues };
|
|
10007
10251
|
}
|
|
10008
10252
|
|