@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.
@@ -1,4 +1,4 @@
1
- // @ansonlai/docx-redline-js v0.5.2 — https://github.com/AnsonLai/docx-redline-js
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
- for (let i = 0; i < diffs.length; i++) {
6570
- const [op, text] = diffs[i];
6571
- if (op === 0) {
6572
- const len = text.length;
6573
- const startPos = originalPos;
6574
- const endPos = originalPos + len;
6575
- forEachOverlappingSpan(spanIndex, startPos, endPos, (span) => {
6576
- const overlapStartOriginal = Math.max(span.charStart, startPos);
6577
- const overlapEndOriginal = Math.min(span.charEnd, endPos);
6578
- const segmentLen = overlapEndOriginal - overlapStartOriginal;
6579
- const relativeOffset = overlapStartOriginal - startPos;
6580
- const overlapStartNew = newPos + relativeOffset;
6581
- const overlapEndNew = overlapStartNew + segmentLen;
6582
- const applicableHints = getApplicableFormatHints(formatHints, overlapStartNew, overlapEndNew);
6583
- if (reconcileFormattingForTextSpan(xmlDoc, span, overlapStartOriginal, overlapEndOriginal, applicableHints, author, generateRedlines)) {
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
- originalPos += len;
6588
- newPos += len;
6589
- } else if (op === -1) {
6590
- const hasNextInsert = i + 1 < diffs.length && diffs[i + 1][0] === 1;
6591
- let paired = false;
6592
- let delMetadata = null;
6593
- let insMetadata = null;
6594
- if (pairReplacements && generateRedlines && hasNextInsert) {
6595
- const nextText = diffs[i + 1][1];
6596
- const textWithoutNewlines = nextText.replace(/\n/g, " ");
6597
- if (textWithoutNewlines.length > 0) {
6598
- const checkResult = checkSafeAdjacencyForPairing(
6599
- spanIndex,
6600
- originalPos,
6601
- originalPos + text.length,
6602
- options?.existingRevisions === "slice-cross-author"
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
- if (processDelete(xmlDoc, spanIndex, originalPos, originalPos + text.length, author, generateRedlines, delMetadata)) {
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, insMetadata, options?.insertionAffinity || null, options?.existingRevisions || "merge-same-author");
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 += nextText.length;
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 normalizedAuthor = normalizeAuthor(options?.author);
7596
- if (!normalizedAuthor) {
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 localNameOf2(node) {
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) => localNameOf2(el) === name);
10110
+ return Array.from(root.getElementsByTagName("*")).filter((el) => localNameOf3(el) === name);
9875
10111
  }
9876
- function wordAttribute2(node, name) {
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 localNameOf2(node.parentNode) === "rPr";
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 && localNameOf2(el) === "p");
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) => localNameOf2(child) === "sectPr" ? index : -1).filter((index) => index >= 0);
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(localNameOf2(el)));
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 (localNameOf2(revision) !== "ins" || localNameOf2(candidate) !== "del") return true;
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="${wordAttribute2(revision, "id")}") contains invalid nested <${invalidNested.nodeName}>.`
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="${wordAttribute2(del, "id")}") contains <w:t>; deleted text must use <w:delText>.`
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 (!wordAttribute2(revision, "id")) missing.push("w:id");
9961
- if (!wordAttribute2(revision, "author")) missing.push("w:author");
9962
- if (!REVISION_DATE_PATTERN.test(wordAttribute2(revision, "date"))) missing.push("w:date");
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(localNameOf2(node))) continue;
9974
- const id = wordAttribute2(node, "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="${wordAttribute2(revision, "id")}") wraps no content.`
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