@ansonlai/docx-redline-js 0.5.1 → 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 +39 -13
- package/CHANGELOG.md +35 -0
- package/README.md +59 -14
- 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 +389 -94
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +79 -77
- 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 +351 -0
- package/docs/schemas/document-operations.schema.json +3 -0
- package/engine/oxml-engine.js +36 -9
- package/engine/surgical-diff-application.js +49 -3
- package/engine/surgical-mode.js +97 -6
- package/index.d.ts +12 -0
- package/package.json +1 -1
- package/pipeline/diff-engine.js +22 -0
- 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;
|
|
@@ -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 [{
|
|
@@ -6149,7 +6160,15 @@ function processDelete(xmlDoc, spanIndex, startPos, endPos, author, generateRedl
|
|
|
6149
6160
|
if (delWrapper && record.deletedPieces.length > 0) {
|
|
6150
6161
|
delWrapper.appendChild(createRunFromPieces(xmlDoc, record.deletedPieces, record.rPr));
|
|
6151
6162
|
}
|
|
6152
|
-
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
|
+
}
|
|
6153
6172
|
parent.removeChild(runElement);
|
|
6154
6173
|
changed = true;
|
|
6155
6174
|
}
|
|
@@ -6186,6 +6205,23 @@ function processInsert(xmlDoc, spanIndex, pos, text, author, formatHints = [], i
|
|
|
6186
6205
|
revisionMetadata
|
|
6187
6206
|
);
|
|
6188
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
|
+
}
|
|
6189
6225
|
if (!affinity) {
|
|
6190
6226
|
let targetSpan = findContainingSpan(spanIndex, pos);
|
|
6191
6227
|
if (!targetSpan && pos > 0) {
|
|
@@ -6208,6 +6244,7 @@ function processInsert(xmlDoc, spanIndex, pos, text, author, formatHints = [], i
|
|
|
6208
6244
|
insertTextRuns(xmlDoc, fallbackParagraph, null, text, targetSpan.rPr, author, formatHints, insertOffset, generateRedlines, revisionMetadata);
|
|
6209
6245
|
return true;
|
|
6210
6246
|
}
|
|
6247
|
+
const generateNestedRevision = !(generateRedlines && existingRevisions === "slice-cross-author" && isSameAuthorInsertion(parent2, author));
|
|
6211
6248
|
if (generateRedlines && existingRevisions === "slice-cross-author" && isForeignInsertion(parent2, author)) {
|
|
6212
6249
|
return spliceInsertionAtCarrierOffset(
|
|
6213
6250
|
xmlDoc,
|
|
@@ -6229,13 +6266,13 @@ function processInsert(xmlDoc, spanIndex, pos, text, author, formatHints = [], i
|
|
|
6229
6266
|
const beforePieces = sliceRunPieces(xmlDoc, pieces, 0, localInsertPos, false);
|
|
6230
6267
|
const afterPieces = sliceRunPieces(xmlDoc, pieces, localInsertPos, getRunTextLength(pieces), false);
|
|
6231
6268
|
insertRunPiecesBefore(xmlDoc, parent2, targetSpan.runElement, beforePieces, targetSpan.rPr);
|
|
6232
|
-
insertTextRuns(xmlDoc, parent2, targetSpan.runElement, text, targetSpan.rPr, author, formatHints, insertOffset,
|
|
6269
|
+
insertTextRuns(xmlDoc, parent2, targetSpan.runElement, text, targetSpan.rPr, author, formatHints, insertOffset, generateNestedRevision, revisionMetadata);
|
|
6233
6270
|
insertRunPiecesBefore(xmlDoc, parent2, targetSpan.runElement, afterPieces, targetSpan.rPr);
|
|
6234
6271
|
parent2.removeChild(targetSpan.runElement);
|
|
6235
6272
|
return true;
|
|
6236
6273
|
}
|
|
6237
6274
|
const referenceNode2 = pos <= targetSpan.charStart ? targetSpan.runElement : targetSpan.runElement.nextSibling;
|
|
6238
|
-
insertTextRuns(xmlDoc, parent2, referenceNode2, text, targetSpan.rPr, author, formatHints, insertOffset,
|
|
6275
|
+
insertTextRuns(xmlDoc, parent2, referenceNode2, text, targetSpan.rPr, author, formatHints, insertOffset, generateNestedRevision, revisionMetadata);
|
|
6239
6276
|
return true;
|
|
6240
6277
|
}
|
|
6241
6278
|
const boundary = describeInsertionBoundary(spanIndex, pos, fallbackParagraph);
|
|
@@ -6443,6 +6480,11 @@ function isForeignInsertion(node, author) {
|
|
|
6443
6480
|
const carrierAuthor = node.getAttribute("w:author") || node.getAttributeNS?.(NS_W, "author") || "";
|
|
6444
6481
|
return carrierAuthor.trim().toLowerCase() !== String(author || "").trim().toLowerCase();
|
|
6445
6482
|
}
|
|
6483
|
+
function isSameAuthorInsertion(node, author) {
|
|
6484
|
+
if (!isWordElement(node, "ins")) return false;
|
|
6485
|
+
const carrierAuthor = node.getAttribute("w:author") || node.getAttributeNS?.(NS_W, "author") || "";
|
|
6486
|
+
return carrierAuthor.trim().toLowerCase() === String(author || "").trim().toLowerCase();
|
|
6487
|
+
}
|
|
6446
6488
|
function nextElementSibling(node) {
|
|
6447
6489
|
let sibling = node?.nextSibling || null;
|
|
6448
6490
|
while (sibling && sibling.nodeType !== 1) sibling = sibling.nextSibling;
|
|
@@ -6541,68 +6583,119 @@ function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer, autho
|
|
|
6541
6583
|
void originalText;
|
|
6542
6584
|
const allParagraphs = targetParagraph ? [targetParagraph] : getDocumentParagraphs(xmlDoc);
|
|
6543
6585
|
const { fullText, textSpans } = buildSurgicalTextSpans(allParagraphs);
|
|
6544
|
-
const
|
|
6586
|
+
const insertionOnlyDiffs = options.existingRevisions === "slice-cross-author" ? computeInsertionOnlyDiffs(fullText, modifiedText) : null;
|
|
6587
|
+
const diffs = insertionOnlyDiffs || computeWordDiffs(fullText, modifiedText, diffOptions);
|
|
6545
6588
|
const spanIndex = buildSpanIndex(textSpans);
|
|
6546
6589
|
const pairReplacements = options.pairReplacements === true;
|
|
6547
6590
|
const warnings = [];
|
|
6548
6591
|
let originalPos = 0;
|
|
6549
6592
|
let newPos = 0;
|
|
6550
6593
|
let hasChanges = false;
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
const
|
|
6555
|
-
const
|
|
6556
|
-
const
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
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)) {
|
|
6566
6672
|
hasChanges = true;
|
|
6567
6673
|
}
|
|
6568
|
-
|
|
6569
|
-
|
|
6570
|
-
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
if (checkResult.safe) {
|
|
6587
|
-
const event = createReplacementRevisionEvent(author, xmlDoc);
|
|
6588
|
-
delMetadata = { id: event.deletionId, author: event.author, date: event.date };
|
|
6589
|
-
insMetadata = { id: event.insertionId, author: event.author, date: event.date };
|
|
6590
|
-
paired = true;
|
|
6591
|
-
} else if (checkResult.structuralBoundary) {
|
|
6592
|
-
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
|
+
}
|
|
6593
6692
|
}
|
|
6693
|
+
newPos += nextText.length;
|
|
6594
6694
|
}
|
|
6595
|
-
}
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
originalPos += text.length;
|
|
6600
|
-
if (paired) {
|
|
6601
|
-
i++;
|
|
6602
|
-
const [, nextText] = diffs[i];
|
|
6603
|
-
const textWithoutNewlines = nextText.replace(/\n/g, " ");
|
|
6604
|
-
if (textWithoutNewlines.trim().length > 0) {
|
|
6605
|
-
const insertResult = processInsert(xmlDoc, spanIndex, originalPos, textWithoutNewlines, author, formatHints, newPos, generateRedlines, allParagraphs[0] || null, insMetadata, options?.insertionAffinity || null, options?.existingRevisions || "merge-same-author");
|
|
6695
|
+
} else if (op === 1) {
|
|
6696
|
+
const textWithoutNewlines = text.replace(/\n/g, " ");
|
|
6697
|
+
if (textWithoutNewlines.length > 0) {
|
|
6698
|
+
const insertResult = processInsert(xmlDoc, spanIndex, originalPos, textWithoutNewlines, author, formatHints, newPos, generateRedlines, allParagraphs[0] || null, null, options?.insertionAffinity || null, options?.existingRevisions || "merge-same-author");
|
|
6606
6699
|
if (insertResult && typeof insertResult === "object" && insertResult.error) {
|
|
6607
6700
|
return withOoxmlSourceType({
|
|
6608
6701
|
oxml: serializer.serializeToString(xmlDoc),
|
|
@@ -6615,33 +6708,63 @@ function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer, autho
|
|
|
6615
6708
|
hasChanges = true;
|
|
6616
6709
|
}
|
|
6617
6710
|
}
|
|
6618
|
-
newPos +=
|
|
6619
|
-
}
|
|
6620
|
-
} else if (op === 1) {
|
|
6621
|
-
const textWithoutNewlines = text.replace(/\n/g, " ");
|
|
6622
|
-
if (textWithoutNewlines.trim().length > 0) {
|
|
6623
|
-
const insertResult = processInsert(xmlDoc, spanIndex, originalPos, textWithoutNewlines, author, formatHints, newPos, generateRedlines, allParagraphs[0] || null, null, options?.insertionAffinity || null, options?.existingRevisions || "merge-same-author");
|
|
6624
|
-
if (insertResult && typeof insertResult === "object" && insertResult.error) {
|
|
6625
|
-
return withOoxmlSourceType({
|
|
6626
|
-
oxml: serializer.serializeToString(xmlDoc),
|
|
6627
|
-
hasChanges: false,
|
|
6628
|
-
status: "error",
|
|
6629
|
-
error: insertResult.error
|
|
6630
|
-
});
|
|
6631
|
-
}
|
|
6632
|
-
if (insertResult === true) {
|
|
6633
|
-
hasChanges = true;
|
|
6634
|
-
}
|
|
6711
|
+
newPos += text.length;
|
|
6635
6712
|
}
|
|
6636
|
-
newPos += text.length;
|
|
6637
6713
|
}
|
|
6638
6714
|
}
|
|
6715
|
+
const actualText = allParagraphs.map((paragraph) => extractCanonicalParagraphText(paragraph)).join("\n");
|
|
6716
|
+
const expectedText = String(modifiedText).replace(/\r\n/g, "\n");
|
|
6717
|
+
if (options.existingRevisions === "slice-cross-author" && actualText !== expectedText) {
|
|
6718
|
+
const mismatchOffset = firstMismatchOffset(expectedText, actualText);
|
|
6719
|
+
return withOoxmlSourceType({
|
|
6720
|
+
oxml: serializer.serializeToString(xmlDoc),
|
|
6721
|
+
hasChanges: false,
|
|
6722
|
+
status: "error",
|
|
6723
|
+
error: {
|
|
6724
|
+
code: "PATCH_ROUNDTRIP_MISMATCH",
|
|
6725
|
+
message: "Generated OOXML accepted-view text does not match the requested modified text; the mutation was rejected.",
|
|
6726
|
+
mismatchOffset,
|
|
6727
|
+
expectedExcerpt: excerptAt(expectedText, mismatchOffset),
|
|
6728
|
+
actualExcerpt: excerptAt(actualText, mismatchOffset)
|
|
6729
|
+
},
|
|
6730
|
+
...warnings.length > 0 ? { warnings: [...new Set(warnings)] } : {}
|
|
6731
|
+
});
|
|
6732
|
+
}
|
|
6639
6733
|
return withOoxmlSourceType({
|
|
6640
6734
|
oxml: serializer.serializeToString(xmlDoc),
|
|
6641
6735
|
hasChanges,
|
|
6642
6736
|
...warnings.length > 0 ? { warnings: [...new Set(warnings)] } : {}
|
|
6643
6737
|
});
|
|
6644
6738
|
}
|
|
6739
|
+
function firstMismatchOffset(expected, actual) {
|
|
6740
|
+
const limit = Math.min(expected.length, actual.length);
|
|
6741
|
+
for (let index = 0; index < limit; index++) {
|
|
6742
|
+
if (expected[index] !== actual[index]) return index;
|
|
6743
|
+
}
|
|
6744
|
+
return limit;
|
|
6745
|
+
}
|
|
6746
|
+
function excerptAt(text, offset, radius = 40) {
|
|
6747
|
+
const start = Math.max(0, offset - radius);
|
|
6748
|
+
const end = Math.min(text.length, offset + radius);
|
|
6749
|
+
return text.slice(start, end);
|
|
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
|
+
}
|
|
6645
6768
|
|
|
6646
6769
|
// engine/reconstruction-mapper.js
|
|
6647
6770
|
var import_diff_match_patch2 = __toESM(require_diff_match_patch(), 1);
|
|
@@ -7544,8 +7667,8 @@ function resolveAuthorFilter(options = {}) {
|
|
|
7544
7667
|
if (options?.allAuthors === true) {
|
|
7545
7668
|
return { valid: true, allAuthors: true, normalizedAuthor: "" };
|
|
7546
7669
|
}
|
|
7547
|
-
const
|
|
7548
|
-
if (!
|
|
7670
|
+
const normalizedAuthor2 = normalizeAuthor(options?.author);
|
|
7671
|
+
if (!normalizedAuthor2) {
|
|
7549
7672
|
return {
|
|
7550
7673
|
valid: false,
|
|
7551
7674
|
allAuthors: false,
|
|
@@ -7553,7 +7676,7 @@ function resolveAuthorFilter(options = {}) {
|
|
|
7553
7676
|
warning: "No author provided. Pass { author } or set { allAuthors: true }."
|
|
7554
7677
|
};
|
|
7555
7678
|
}
|
|
7556
|
-
return { valid: true, allAuthors: false, normalizedAuthor };
|
|
7679
|
+
return { valid: true, allAuthors: false, normalizedAuthor: normalizedAuthor2 };
|
|
7557
7680
|
}
|
|
7558
7681
|
function authorMatchesNode(node, filter) {
|
|
7559
7682
|
if (filter.allAuthors) return true;
|
|
@@ -8005,6 +8128,133 @@ function recordRouteSelection(options, route, context = {}) {
|
|
|
8005
8128
|
}));
|
|
8006
8129
|
}
|
|
8007
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
|
+
|
|
8008
8258
|
// engine/oxml-engine.js
|
|
8009
8259
|
function getCommentIdsInOoxml(node) {
|
|
8010
8260
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -8085,6 +8335,23 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
8085
8335
|
}
|
|
8086
8336
|
const revisionIdAllocator = options?._revisionIdAllocator instanceof RevisionIdAllocator ? options._revisionIdAllocator : new RevisionIdAllocator();
|
|
8087
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
|
+
}
|
|
8088
8355
|
if (containsTrackedChanges(xmlDoc)) {
|
|
8089
8356
|
if (existingRevisionsPolicy === "merge-same-author" || existingRevisionsPolicy === "slice-cross-author") {
|
|
8090
8357
|
const authors = getTrackedChangeAuthors(xmlDoc);
|
|
@@ -8243,7 +8510,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
8243
8510
|
}
|
|
8244
8511
|
}
|
|
8245
8512
|
const { cleanText: cleanModifiedText, formatHints } = preprocessMarkdown(sanitizedText);
|
|
8246
|
-
const hasTextChanges = cleanModifiedText.trim() !== originalText.trim();
|
|
8513
|
+
const hasTextChanges = existingRevisionsPolicy === "slice-cross-author" ? cleanModifiedText !== originalText : cleanModifiedText.trim() !== originalText.trim();
|
|
8247
8514
|
const hasFormatHints = formatHints.length > 0;
|
|
8248
8515
|
const { existingFormatHints, textSpans, paragraphs } = extractFormattingFromOoxml(xmlDoc);
|
|
8249
8516
|
const hasExistingFormatting = existingFormatHints.length > 0;
|
|
@@ -8396,6 +8663,9 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
8396
8663
|
{},
|
|
8397
8664
|
options
|
|
8398
8665
|
);
|
|
8666
|
+
if (result.status === "error" && result.error?.code === "PATCH_ROUNDTRIP_MISMATCH") {
|
|
8667
|
+
return finalize({ ...result, oxml: inputOoxml, hasChanges: false });
|
|
8668
|
+
}
|
|
8399
8669
|
if (tableCellContext.hasTableWrapper && result.hasChanges && tableCellContext.targetParagraph) {
|
|
8400
8670
|
log("[OxmlEngine] Stripping table wrapper for table cell paragraph (surgical mode)");
|
|
8401
8671
|
return finalize({ oxml: serializeParagraphOnly(xmlDoc, tableCellContext.targetParagraph, serializer), hasChanges: true });
|
|
@@ -8788,6 +9058,23 @@ function resolveTargetParagraph(xmlDoc, options = {}) {
|
|
|
8788
9058
|
}
|
|
8789
9059
|
return { paragraph: byId, resolvedBy: "paragraph_id" };
|
|
8790
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
|
+
}
|
|
8791
9078
|
let candidates = [];
|
|
8792
9079
|
if (cleanTargetText) {
|
|
8793
9080
|
const unfilteredCandidates = findStrictTargetCandidates(xmlDoc, cleanTargetText, paragraphMetadataIndex);
|
|
@@ -9816,20 +10103,20 @@ async function executeSingleLineListStructuralFallback(plan, options = {}) {
|
|
|
9816
10103
|
// core/redline-validation.js
|
|
9817
10104
|
var REVISION_ID_ELEMENTS = /* @__PURE__ */ new Set(["ins", "del", "rPrChange", "pPrChange"]);
|
|
9818
10105
|
var REVISION_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}T/;
|
|
9819
|
-
function
|
|
10106
|
+
function localNameOf3(node) {
|
|
9820
10107
|
return String(node?.localName || node?.nodeName || "").replace(/^.*:/, "");
|
|
9821
10108
|
}
|
|
9822
10109
|
function elementsByLocalName(root, name) {
|
|
9823
|
-
return Array.from(root.getElementsByTagName("*")).filter((el) =>
|
|
10110
|
+
return Array.from(root.getElementsByTagName("*")).filter((el) => localNameOf3(el) === name);
|
|
9824
10111
|
}
|
|
9825
|
-
function
|
|
10112
|
+
function wordAttribute3(node, name) {
|
|
9826
10113
|
return node.getAttribute(`w:${name}`) || node.getAttribute(name) || "";
|
|
9827
10114
|
}
|
|
9828
10115
|
function xmlSpaceAttribute(node) {
|
|
9829
10116
|
return node.getAttribute("xml:space") || node.getAttribute("space") || node.getAttributeNS?.("http://www.w3.org/XML/1998/namespace", "space") || "";
|
|
9830
10117
|
}
|
|
9831
10118
|
function isParagraphMarkRevision(node) {
|
|
9832
|
-
return
|
|
10119
|
+
return localNameOf3(node.parentNode) === "rPr";
|
|
9833
10120
|
}
|
|
9834
10121
|
function parseOoxmlForValidation(oxml) {
|
|
9835
10122
|
const attempt = (xml) => {
|
|
@@ -9866,14 +10153,14 @@ function validateRedlineOoxml(oxml) {
|
|
|
9866
10153
|
const delElements = elementsByLocalName(doc, "del");
|
|
9867
10154
|
const revisions = insElements.concat(delElements);
|
|
9868
10155
|
for (const paragraph of elementsByLocalName(doc, "p")) {
|
|
9869
|
-
const nested = Array.from(paragraph.getElementsByTagName("*")).find((el) => el !== paragraph &&
|
|
10156
|
+
const nested = Array.from(paragraph.getElementsByTagName("*")).find((el) => el !== paragraph && localNameOf3(el) === "p");
|
|
9870
10157
|
if (nested) {
|
|
9871
10158
|
addIssue("NESTED_PARAGRAPH", "error", `<${paragraph.nodeName}> contains nested <${nested.nodeName}>.`);
|
|
9872
10159
|
}
|
|
9873
10160
|
}
|
|
9874
10161
|
for (const body of elementsByLocalName(doc, "body")) {
|
|
9875
10162
|
const children = Array.from(body.childNodes || []).filter((child) => child.nodeType === 1);
|
|
9876
|
-
const sectPrIndexes = children.map((child, index) =>
|
|
10163
|
+
const sectPrIndexes = children.map((child, index) => localNameOf3(child) === "sectPr" ? index : -1).filter((index) => index >= 0);
|
|
9877
10164
|
if (sectPrIndexes.length > 1) {
|
|
9878
10165
|
addIssue("MULTIPLE_BODY_SECTPR", "error", "<w:body> contains multiple direct <w:sectPr> elements.");
|
|
9879
10166
|
} else if (sectPrIndexes.length === 1 && sectPrIndexes[0] !== children.length - 1) {
|
|
@@ -9881,16 +10168,16 @@ function validateRedlineOoxml(oxml) {
|
|
|
9881
10168
|
}
|
|
9882
10169
|
}
|
|
9883
10170
|
for (const revision of revisions) {
|
|
9884
|
-
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)));
|
|
9885
10172
|
const invalidNested = nested.find((candidate) => {
|
|
9886
|
-
if (
|
|
10173
|
+
if (localNameOf3(revision) !== "ins" || localNameOf3(candidate) !== "del") return true;
|
|
9887
10174
|
return candidate.parentNode !== revision;
|
|
9888
10175
|
});
|
|
9889
10176
|
if (invalidNested) {
|
|
9890
10177
|
addIssue(
|
|
9891
10178
|
"NESTED_REVISION",
|
|
9892
10179
|
"error",
|
|
9893
|
-
`<${revision.nodeName}> (w:id="${
|
|
10180
|
+
`<${revision.nodeName}> (w:id="${wordAttribute3(revision, "id")}") contains invalid nested <${invalidNested.nodeName}>.`
|
|
9894
10181
|
);
|
|
9895
10182
|
}
|
|
9896
10183
|
}
|
|
@@ -9900,15 +10187,15 @@ function validateRedlineOoxml(oxml) {
|
|
|
9900
10187
|
addIssue(
|
|
9901
10188
|
"DEL_CONTAINS_T",
|
|
9902
10189
|
"error",
|
|
9903
|
-
`<w:del> (w:id="${
|
|
10190
|
+
`<w:del> (w:id="${wordAttribute3(del, "id")}") contains <w:t>; deleted text must use <w:delText>.`
|
|
9904
10191
|
);
|
|
9905
10192
|
}
|
|
9906
10193
|
}
|
|
9907
10194
|
for (const revision of revisions) {
|
|
9908
10195
|
const missing = [];
|
|
9909
|
-
if (!
|
|
9910
|
-
if (!
|
|
9911
|
-
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");
|
|
9912
10199
|
if (missing.length > 0) {
|
|
9913
10200
|
addIssue(
|
|
9914
10201
|
"MISSING_REVISION_METADATA",
|
|
@@ -9919,8 +10206,8 @@ function validateRedlineOoxml(oxml) {
|
|
|
9919
10206
|
}
|
|
9920
10207
|
const seenIds = /* @__PURE__ */ new Set();
|
|
9921
10208
|
for (const node of Array.from(doc.getElementsByTagName("*"))) {
|
|
9922
|
-
if (!REVISION_ID_ELEMENTS.has(
|
|
9923
|
-
const id =
|
|
10209
|
+
if (!REVISION_ID_ELEMENTS.has(localNameOf3(node))) continue;
|
|
10210
|
+
const id = wordAttribute3(node, "id");
|
|
9924
10211
|
if (!id) continue;
|
|
9925
10212
|
if (seenIds.has(id)) {
|
|
9926
10213
|
addIssue("DUPLICATE_REVISION_ID", "error", `Revision id ${id} appears more than once.`);
|
|
@@ -9948,10 +10235,18 @@ function validateRedlineOoxml(oxml) {
|
|
|
9948
10235
|
addIssue(
|
|
9949
10236
|
"EMPTY_REVISION_WRAPPER",
|
|
9950
10237
|
"warning",
|
|
9951
|
-
`<${revision.nodeName}> (w:id="${
|
|
10238
|
+
`<${revision.nodeName}> (w:id="${wordAttribute3(revision, "id")}") wraps no content.`
|
|
9952
10239
|
);
|
|
9953
10240
|
}
|
|
9954
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
|
+
}
|
|
9955
10250
|
return { valid: !issues.some((issue) => issue.severity === "error"), issues };
|
|
9956
10251
|
}
|
|
9957
10252
|
|