@ansonlai/docx-redline-js 0.9.0 → 0.10.0

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.9.0 — https://github.com/AnsonLai/docx-redline-js
1
+ // @ansonlai/docx-redline-js v0.10.0 — 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;
@@ -1663,7 +1663,10 @@ var MARKDOWN_PATTERNS = [
1663
1663
  { regex: /\*\*\+\+(.+?)\+\+\*\*/, format: { bold: true, underline: true } },
1664
1664
  // Bold: **text** or __text__
1665
1665
  { regex: /\*\*(.+?)\*\*/, format: { bold: true } },
1666
- { regex: /__(.+?)__/, format: { bold: true } },
1666
+ // Underscore delimiters follow CommonMark flanking rules: not intraword, not part of a
1667
+ // longer underscore run, and never around whitespace. So signature lines ("_____")
1668
+ // and snake_case identifiers stay literal text.
1669
+ { regex: /(?<![A-Za-z0-9_])__(?=[^_\s])(.+?)(?<=[^_\s])__(?![A-Za-z0-9_])/, format: { bold: true } },
1667
1670
  // Underline: ++text++
1668
1671
  { regex: /\+\+(.+?)\+\+/, format: { underline: true } },
1669
1672
  // Strikethrough: ~~text~~ or ~text~
@@ -1671,7 +1674,7 @@ var MARKDOWN_PATTERNS = [
1671
1674
  { regex: /~(.+?)~/, format: { strikethrough: true } },
1672
1675
  // Italic: *text* or _text_ (using lookahead only for compatibility)
1673
1676
  { regex: /\*(?!\*)(.+?)\*(?!\*)/, format: { italic: true } },
1674
- { regex: /_(?!_)(.+?)_(?!_)/, format: { italic: true } }
1677
+ { regex: /(?<![A-Za-z0-9_])_(?=[^_\s])(.+?)(?<=[^_\s])_(?![A-Za-z0-9_])/, format: { italic: true } }
1675
1678
  ];
1676
1679
  function preprocessMarkdown(text2) {
1677
1680
  if (!text2) {
@@ -3842,6 +3845,23 @@ function withOoxmlSourceType(result) {
3842
3845
  }
3843
3846
  return { ...result, sourceType: classifyOoxmlSourceType(result.oxml) };
3844
3847
  }
3848
+ function hasUnattributedTrackedChange(node) {
3849
+ if (!node) return false;
3850
+ for (const localName4 of TRACKED_CHANGE_NAMES) {
3851
+ for (const change of wordElementsByLocalName(node, localName4)) {
3852
+ const author = change.getAttribute?.("w:author") || change.getAttribute?.("author") || (typeof change.getAttributeNS === "function" ? change.getAttributeNS(NS_W, "author") : null);
3853
+ if (!author || !String(author).trim()) return true;
3854
+ }
3855
+ }
3856
+ return false;
3857
+ }
3858
+ function resolveExistingRevisionsPolicy(policy, node, author) {
3859
+ if (policy !== "auto") return policy;
3860
+ const nodes = (Array.isArray(node) ? node : [node]).filter(Boolean);
3861
+ const own = String(author || "").trim().toLowerCase();
3862
+ const foreign = nodes.some((item) => containsTrackedChanges(item) && (hasUnattributedTrackedChange(item) || getTrackedChangeAuthors(item).some((name) => name.trim().toLowerCase() !== own)));
3863
+ return foreign ? "slice-cross-author" : "merge-same-author";
3864
+ }
3845
3865
 
3846
3866
  // engine/paragraph-mark-revisions.js
3847
3867
  function localNameOf(node) {
@@ -9352,7 +9372,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
9352
9372
  const operationWarnings = [];
9353
9373
  let normalizedExistingRevisions = false;
9354
9374
  let preservedInsertedParagraphMark = null;
9355
- const existingRevisionsPolicy = options.existingRevisions || "merge-same-author";
9375
+ let existingRevisionsPolicy = options.existingRevisions || "merge-same-author";
9356
9376
  const keepNormalizedNoOp = existingRevisionsPolicy === "accept-all-first-keep-normalized";
9357
9377
  const finalize = (result) => {
9358
9378
  const withStatus = { ...result };
@@ -9400,6 +9420,10 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
9400
9420
  const parsed = parseOoxmlSafe(inputOoxml, "text/xml");
9401
9421
  parseWarnings = parsed.warnings;
9402
9422
  let xmlDoc = parsed.doc;
9423
+ if (existingRevisionsPolicy === "auto") {
9424
+ existingRevisionsPolicy = resolveExistingRevisionsPolicy("auto", xmlDoc, author);
9425
+ options = { ...options, existingRevisions: existingRevisionsPolicy };
9426
+ }
9403
9427
  const parseError = xmlDoc ? getXmlParseError(xmlDoc) : null;
9404
9428
  if (parsed.error || parseError) {
9405
9429
  const message = parsed.error?.message || parseError?.textContent || "Could not parse OOXML input.";
@@ -9434,7 +9458,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
9434
9458
  if (existingRevisionsPolicy === "merge-same-author" || existingRevisionsPolicy === "slice-cross-author") {
9435
9459
  const authors = getTrackedChangeAuthors(xmlDoc);
9436
9460
  const currentAuthor = String(author || "").trim().toLowerCase();
9437
- const isSameAuthor = authors.length > 0 && authors.every((a) => a.trim().toLowerCase() === currentAuthor);
9461
+ const isSameAuthor = authors.length > 0 && !hasUnattributedTrackedChange(xmlDoc) && authors.every((a) => a.trim().toLowerCase() === currentAuthor);
9438
9462
  if (isSameAuthor) {
9439
9463
  const commentIds = getCommentIdsInOoxml(xmlDoc);
9440
9464
  if (commentIds.length > 0) {
@@ -9591,7 +9615,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
9591
9615
  sanitizedText = structuredAnalysis.normalizedMarkdown;
9592
9616
  }
9593
9617
  }
9594
- const { cleanText: cleanModifiedText, formatHints } = preprocessMarkdown(sanitizedText);
9618
+ const { cleanText: cleanModifiedText, formatHints } = options.plainTextContent === true ? { cleanText: sanitizedText, formatHints: [] } : preprocessMarkdown(sanitizedText);
9595
9619
  const { existingFormatHints, textSpans, paragraphs } = extractFormattingFromOoxml(xmlDoc);
9596
9620
  const hasLiteralTextChanges = existingRevisionsPolicy === "slice-cross-author" ? cleanModifiedText !== originalText : cleanModifiedText.trim() !== originalText.trim();
9597
9621
  const hasListStructureChange = !hasLiteralTextChanges && options.explicitStructuredContent === true && isListTargetLoose(cleanModifiedText) && !sourceMatchesListShape(paragraphs, cleanModifiedText);
@@ -13423,6 +13447,70 @@ function inspectDocumentParts(parts, options = {}) {
13423
13447
  };
13424
13448
  }
13425
13449
 
13450
+ // services/inspection-comparison.js
13451
+ var commentIdentity = (comment) => [comment.author ?? null, comment.text ?? "", comment.anchoredText || ""];
13452
+ function reviewSegments(segments = []) {
13453
+ return segments.filter((segment) => segment.kind !== "baseline").reduce((result, segment) => {
13454
+ const next = {
13455
+ kind: segment.kind,
13456
+ author: segment.author || null,
13457
+ text: segment.text,
13458
+ acceptedStart: segment.acceptedStart ?? null,
13459
+ rejectedStart: segment.rejectedStart ?? null
13460
+ };
13461
+ const previous = result.at(-1);
13462
+ const adjacent = (key) => previous[key] == null && next[key] == null || previous[key] != null && previous[key] + previous.text.length === next[key];
13463
+ if (previous && previous.kind === next.kind && previous.author === next.author && adjacent("acceptedStart") && adjacent("rejectedStart")) previous.text += next.text;
13464
+ else result.push(next);
13465
+ return result;
13466
+ }, []);
13467
+ }
13468
+ function inspectionReviewShape(inspection) {
13469
+ const comments = inspection?.comments || [];
13470
+ return {
13471
+ paragraphs: (inspection?.paragraphs || []).map((paragraph) => ({
13472
+ text: paragraph.exactText,
13473
+ inTable: paragraph.inTable === true,
13474
+ table: paragraph.table ? [paragraph.table.rowIndex, paragraph.table.cellIndex] : null,
13475
+ list: paragraph.list ? [paragraph.list.format, paragraph.list.level, paragraph.list.label] : null,
13476
+ review: {
13477
+ present: paragraph.hasRevisions === true,
13478
+ authors: [...paragraph.revisionAuthors || []].sort(),
13479
+ segments: reviewSegments(paragraph.segments)
13480
+ }
13481
+ })),
13482
+ comments: comments.map((comment) => {
13483
+ const parent = comment.parentCommentId != null ? comments.find((item) => String(item.id) === String(comment.parentCommentId)) : null;
13484
+ return [
13485
+ ...commentIdentity(comment),
13486
+ comment.done === true,
13487
+ parent ? commentIdentity(parent) : comment.parentCommentId != null || comment.parentParaId ? "unresolved parent" : null
13488
+ ];
13489
+ }).map((item) => JSON.stringify(item)).sort()
13490
+ };
13491
+ }
13492
+ function compareDocumentInspections(expected, actual) {
13493
+ const left = inspectionReviewShape(expected);
13494
+ const right = inspectionReviewShape(actual);
13495
+ const differences = [];
13496
+ const note = (path, a, b) => {
13497
+ if (differences.length < 10 && JSON.stringify(a) !== JSON.stringify(b)) differences.push({ path, expected: a, actual: b });
13498
+ };
13499
+ note("paragraphs.length", left.paragraphs.length, right.paragraphs.length);
13500
+ const count = Math.max(left.paragraphs.length, right.paragraphs.length);
13501
+ for (let index = 0; index < count; index++) {
13502
+ const a = left.paragraphs[index] ?? null;
13503
+ const b = right.paragraphs[index] ?? null;
13504
+ if (!a || !b) {
13505
+ note(`paragraphs[${index}]`, a, b);
13506
+ continue;
13507
+ }
13508
+ for (const key of ["text", "inTable", "table", "list", "review"]) note(`paragraphs[${index}].${key}`, a[key], b[key]);
13509
+ }
13510
+ note("comments", left.comments, right.comments);
13511
+ return { matches: differences.length === 0, differences };
13512
+ }
13513
+
13426
13514
  // services/comment-locator.js
13427
13515
  function createParagraphTextIndex(paragraph, options = {}) {
13428
13516
  const revisionView = options.revisionView === "current" ? "accepted" : options.revisionView || "accepted";
@@ -13897,7 +13985,7 @@ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, doc
13897
13985
  extendedDoc = parsed.doc;
13898
13986
  }
13899
13987
  const occupied = usedParaIds(commentsDoc, extendedDoc);
13900
- if (documentXml) for (const match of String(documentXml).matchAll(/w14:paraId="([0-9A-Fa-f]{8})"/g)) occupied.add(match[1].toUpperCase());
13988
+ if (documentXml) for (const match of String(documentXml).matchAll(/\bw14:paraId="([0-9A-Fa-f]{8})"/g)) occupied.add(match[1].toUpperCase());
13901
13989
  const parentParagraph = commentThreadParagraph(parent);
13902
13990
  if (!parentParagraph) return { status: "error", error: { code: "PARENT_COMMENT_INVALID", message: `Parent comment '${parentCommentId}' has no paragraph.` } };
13903
13991
  let parentParaId = commentParaId(parent);
@@ -16199,6 +16287,7 @@ var SUPPORTED_OPERATION_TYPES = /* @__PURE__ */ new Set([
16199
16287
  "highlight"
16200
16288
  ]);
16201
16289
  var EXISTING_REVISIONS_POLICIES = Object.freeze([
16290
+ "auto",
16202
16291
  "merge-same-author",
16203
16292
  "slice-cross-author",
16204
16293
  "reject-input",
@@ -20253,13 +20342,17 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
20253
20342
  }
20254
20343
  };
20255
20344
  }
20345
+ const autoExistingRevisions = options.existingRevisions === "auto";
20346
+ if (autoExistingRevisions) {
20347
+ options = { ...options, existingRevisions: resolveExistingRevisionsPolicy("auto", targetParagraph, author) };
20348
+ }
20256
20349
  const hasRevisions = containsTrackedChanges(targetParagraph);
20257
20350
  const existingPolicy = options.existingRevisions || "merge-same-author";
20258
20351
  if (hasRevisions && existingPolicy !== "accept-all-first" && existingPolicy !== "accept-all-first-keep-normalized") {
20259
20352
  if (existingPolicy === "merge-same-author" || existingPolicy === "slice-cross-author") {
20260
20353
  const authors = getTrackedChangeAuthors(targetParagraph);
20261
20354
  const opAuthor = String(author || "").trim().toLowerCase();
20262
- const allSame = authors.length > 0 && authors.every((a) => a.trim().toLowerCase() === opAuthor);
20355
+ const allSame = authors.length > 0 && !hasUnattributedTrackedChange(targetParagraph) && authors.every((a) => a.trim().toLowerCase() === opAuthor);
20263
20356
  if (!allSame && existingPolicy === "merge-same-author") {
20264
20357
  return {
20265
20358
  documentXml,
@@ -20268,7 +20361,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
20268
20361
  status: "error",
20269
20362
  error: {
20270
20363
  code: "EXISTING_REVISIONS",
20271
- message: `Target paragraph contains tracked changes from another author (${authors.length ? authors.join(", ") : "unattributed"}). Use existingRevisions: "slice-cross-author" for a surgical edit that preserves reviewer history; accepting or rejecting revisions requires separate authorization.`,
20364
+ message: `Target paragraph contains tracked changes from another author (${[...authors, ...hasUnattributedTrackedChange(targetParagraph) ? ["unattributed"] : []].join(", ")}). Use existingRevisions: "slice-cross-author" for a surgical edit that preserves reviewer history; accepting or rejecting revisions requires separate authorization.`,
20272
20365
  revisionAuthors: authors,
20273
20366
  currentPolicy: existingPolicy
20274
20367
  }
@@ -20410,6 +20503,9 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
20410
20503
  onWarn
20411
20504
  }) : typeof targetText === "string" && /\r?\n/.test(targetText) ? findReconstructionParagraphRange(xmlDoc, targetText) : null;
20412
20505
  const hasExplicitRangeScope = Array.isArray(explicitRangeParagraphs) && explicitRangeParagraphs.length > 0;
20506
+ if (autoExistingRevisions && hasExplicitRangeScope) {
20507
+ options = { ...options, existingRevisions: resolveExistingRevisionsPolicy("auto", explicitRangeParagraphs, author) };
20508
+ }
20413
20509
  if (!useTableScope && hasExplicitRangeScope) {
20414
20510
  const insertionEntries = buildExplicitRangeInsertionEntries(explicitRangeParagraphs, effectiveModifiedText);
20415
20511
  if (insertionEntries && insertionEntries.length > 0) {
@@ -20735,6 +20831,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
20735
20831
  pairReplacements: options.pairReplacements === true,
20736
20832
  insertionAffinity: options.insertionAffinity || null,
20737
20833
  literalTextEdit: options.literalTextEdit === true,
20834
+ plainTextContent: options.plainTextContent === true,
20738
20835
  _revisionIdAllocator: revisionIdAllocator,
20739
20836
  _isolatedTableCell: useTableScope
20740
20837
  });
@@ -21745,7 +21842,31 @@ function verifyUntrackedLocality({ before, after, targetSourceIndex, beforeViews
21745
21842
  }
21746
21843
  return violations.length ? { ok: false, violations } : { ok: true };
21747
21844
  }
21748
- function verifyNumberingReferences({ before, after, numberingXmls, beforeViews = null, afterViews = null }) {
21845
+ var numberingDefinitionCache = /* @__PURE__ */ new Map();
21846
+ var NUMBERING_DEFINITION_CACHE_SIZE = 8;
21847
+ function parseNumberingDefinitions(text2) {
21848
+ const cached = numberingDefinitionCache.get(text2);
21849
+ if (cached) return cached;
21850
+ const abstractLevels = /* @__PURE__ */ new Map();
21851
+ for (const match of text2.matchAll(/<(?:\w+:)?abstractNum\b([^>]*)>([\s\S]*?)<\/(?:\w+:)?abstractNum>/g)) {
21852
+ const id = /abstractNumId="([^"]*)"/.exec(match[1])?.[1];
21853
+ if (id == null) continue;
21854
+ const levels = abstractLevels.get(id) || /* @__PURE__ */ new Set();
21855
+ for (const lvl of match[2].matchAll(/<(?:\w+:)?lvl\b[^>]*?ilvl="([^"]*)"/g)) levels.add(lvl[1]);
21856
+ abstractLevels.set(id, levels);
21857
+ }
21858
+ const numToAbstract = /* @__PURE__ */ new Map();
21859
+ for (const match of text2.matchAll(/<(?:\w+:)?num\b([^>]*)>([\s\S]*?)<\/(?:\w+:)?num>/g)) {
21860
+ const id = /numId="([^"]*)"/.exec(match[1])?.[1];
21861
+ const abstractId = /abstractNumId[^>]*?val="([^"]*)"/.exec(match[2])?.[1];
21862
+ if (id != null && abstractId != null) numToAbstract.set(id, abstractId);
21863
+ }
21864
+ const definitions = { abstractLevels, numToAbstract };
21865
+ if (numberingDefinitionCache.size >= NUMBERING_DEFINITION_CACHE_SIZE) numberingDefinitionCache.delete(numberingDefinitionCache.keys().next().value);
21866
+ numberingDefinitionCache.set(text2, definitions);
21867
+ return definitions;
21868
+ }
21869
+ function verifyNumberingReferences({ before, after, numberingXmls }) {
21749
21870
  const references = (documentNode) => {
21750
21871
  const found = /* @__PURE__ */ new Map();
21751
21872
  for (const numIdNode of getAllReferencedNumIdElements(documentNode)) {
@@ -21761,21 +21882,7 @@ function verifyNumberingReferences({ before, after, numberingXmls, beforeViews =
21761
21882
  const used = references(after);
21762
21883
  if (used.size === 0) return { ok: true };
21763
21884
  const known = references(before);
21764
- const text2 = (numberingXmls || []).filter(Boolean).join("\n");
21765
- const abstractLevels = /* @__PURE__ */ new Map();
21766
- for (const match of text2.matchAll(/<(?:\w+:)?abstractNum\b([^>]*)>([\s\S]*?)<\/(?:\w+:)?abstractNum>/g)) {
21767
- const id = /abstractNumId="([^"]*)"/.exec(match[1])?.[1];
21768
- if (id == null) continue;
21769
- const levels = abstractLevels.get(id) || /* @__PURE__ */ new Set();
21770
- for (const lvl of match[2].matchAll(/<(?:\w+:)?lvl\b[^>]*?ilvl="([^"]*)"/g)) levels.add(lvl[1]);
21771
- abstractLevels.set(id, levels);
21772
- }
21773
- const numToAbstract = /* @__PURE__ */ new Map();
21774
- for (const match of text2.matchAll(/<(?:\w+:)?num\b([^>]*)>([\s\S]*?)<\/(?:\w+:)?num>/g)) {
21775
- const id = /numId="([^"]*)"/.exec(match[1])?.[1];
21776
- const abstractId = /abstractNumId[^>]*?val="([^"]*)"/.exec(match[2])?.[1];
21777
- if (id != null && abstractId != null) numToAbstract.set(id, abstractId);
21778
- }
21885
+ const { abstractLevels, numToAbstract } = parseNumberingDefinitions((numberingXmls || []).filter(Boolean).join("\n"));
21779
21886
  const violations = [];
21780
21887
  for (const [numId, levels] of used) {
21781
21888
  if (known.has(numId)) continue;
@@ -22124,6 +22231,7 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
22124
22231
  ...operation.existingRevisions ? { existingRevisions: operation.existingRevisions } : {},
22125
22232
  structuredContent: typeof operation.structuredContent === "boolean" ? operation.structuredContent : options.structuredContent !== false,
22126
22233
  explicitStructuredContent: operation.structuredContent === true,
22234
+ plainTextContent: operation.structuredContent === false || operation.structuredContent == null && options.structuredContent === false,
22127
22235
  ...operation._localizedReplacementCompilation && operation.structuredContent !== true ? { literalTextEdit: true } : {},
22128
22236
  pairReplacements: typeof operation.pairReplacements === "boolean" ? operation.pairReplacements : options.pairReplacements !== false,
22129
22237
  ...operation.insertionAffinity ? { insertionAffinity: operation.insertionAffinity } : {},
@@ -23378,35 +23486,44 @@ function preflightOperations(documentXml, operations, author, options = {}) {
23378
23486
  const anchor = operation.operationKind === "comment" ? operation.textToComment || paragraphText : operation.operationKind === "highlight" ? operation.textToHighlight : operation.operationKind === "format" ? operation.textToFormat : null;
23379
23487
  const anchorResolution = operation.operationKind === "comment" && anchor != null ? resolveTextInParagraphIndex(createParagraphTextIndex(paragraph, { revisionView: targetView }), anchor) : null;
23380
23488
  const anchorFound = anchor == null || (anchorResolution ? anchorResolution.found : paragraphText.includes(anchor));
23489
+ const rangeParagraphs = (opType) => {
23490
+ if (operation.targetEndRef) {
23491
+ return resolveParagraphRangeByRefs(xmlDoc, operation.targetRef, operation.targetEndRef, {
23492
+ opType,
23493
+ targetRefSnapshot: options.targetRefSnapshot || null,
23494
+ onInfo: options.onInfo,
23495
+ onWarn: options.onWarn
23496
+ });
23497
+ }
23498
+ if (!operation.targetEndDescriptor) return [paragraph];
23499
+ const endResolved = resolveTargetParagraph(xmlDoc, {
23500
+ targetText: operation.targetEndDescriptor.text,
23501
+ targetRef: operation.targetEndDescriptor.index,
23502
+ targetDescriptor: operation.targetEndDescriptor,
23503
+ opType,
23504
+ strictAmbiguity: strictTargets,
23505
+ paragraphMetadataIndex: currentMetadataIndex,
23506
+ metadataIndices,
23507
+ onInfo: options.onInfo,
23508
+ onWarn: options.onWarn
23509
+ });
23510
+ const allParagraphs = getDocumentParagraphNodes(xmlDoc);
23511
+ const startIndex = allParagraphs.indexOf(paragraph);
23512
+ const endIndex = allParagraphs.indexOf(endResolved?.paragraph);
23513
+ return startIndex >= 0 && endIndex >= startIndex ? allParagraphs.slice(startIndex, endIndex + 1) : null;
23514
+ };
23381
23515
  const hasRevisions = containsTrackedChanges(paragraph);
23382
- const existingPolicy = operation.existingRevisions || options.existingRevisions || "merge-same-author";
23516
+ const requestedPolicy = operation.existingRevisions || options.existingRevisions || "merge-same-author";
23517
+ const existingPolicy = resolveExistingRevisionsPolicy(
23518
+ requestedPolicy,
23519
+ requestedPolicy === "auto" && operation.operationKind === "redline" && (operation.targetEndRef || operation.targetEndDescriptor) ? rangeParagraphs("redline") || paragraph : paragraph,
23520
+ authorUsed
23521
+ );
23383
23522
  const deletingWholeParagraph = operation.operationKind === "redline" && operation.modified === "";
23384
23523
  const commentIds = deletingWholeParagraph ? getCommentIdsInParagraph(paragraph) : [];
23385
23524
  let error2 = null;
23386
23525
  if (operation.operationKind === "restore") {
23387
- let restorationParagraphs = operation.targetEndRef ? resolveParagraphRangeByRefs(xmlDoc, operation.targetRef, operation.targetEndRef, {
23388
- opType: "restore",
23389
- targetRefSnapshot: options.targetRefSnapshot || null,
23390
- onInfo: options.onInfo,
23391
- onWarn: options.onWarn
23392
- }) : [paragraph];
23393
- if (!operation.targetEndRef && operation.targetEndDescriptor) {
23394
- const endResolved = resolveTargetParagraph(xmlDoc, {
23395
- targetText: operation.targetEndDescriptor.text,
23396
- targetRef: operation.targetEndDescriptor.index,
23397
- targetDescriptor: operation.targetEndDescriptor,
23398
- opType: "restore",
23399
- strictAmbiguity: strictTargets,
23400
- paragraphMetadataIndex: currentMetadataIndex,
23401
- metadataIndices,
23402
- onInfo: options.onInfo,
23403
- onWarn: options.onWarn
23404
- });
23405
- const allParagraphs = getDocumentParagraphNodes(xmlDoc);
23406
- const startIndex = allParagraphs.indexOf(paragraph);
23407
- const endIndex = allParagraphs.indexOf(endResolved?.paragraph);
23408
- restorationParagraphs = startIndex >= 0 && endIndex >= startIndex ? allParagraphs.slice(startIndex, endIndex + 1) : null;
23409
- }
23526
+ const restorationParagraphs = rangeParagraphs("restore");
23410
23527
  const replacements = Array.isArray(operation.modified) ? operation.modified : [operation.modified];
23411
23528
  if (!restorationParagraphs || restorationParagraphs[0] !== paragraph || replacements.length !== restorationParagraphs.length) {
23412
23529
  error2 = {
@@ -23453,11 +23570,11 @@ function preflightOperations(documentXml, operations, author, options = {}) {
23453
23570
  if (existingPolicy === "merge-same-author" || existingPolicy === "slice-cross-author") {
23454
23571
  const authors = getTrackedChangeAuthors(paragraph);
23455
23572
  const opAuthor = String(authorUsed || "").trim().toLowerCase();
23456
- const allSame = authors.length > 0 && authors.every((a) => a.trim().toLowerCase() === opAuthor);
23573
+ const allSame = authors.length > 0 && !hasUnattributedTrackedChange(paragraph) && authors.every((a) => a.trim().toLowerCase() === opAuthor);
23457
23574
  if (!allSame && existingPolicy === "merge-same-author") {
23458
23575
  error2 = {
23459
23576
  code: "EXISTING_REVISIONS",
23460
- message: `Target paragraph contains tracked changes from another author (${authors.length ? authors.join(", ") : "unattributed"}). Use existingRevisions: "slice-cross-author" for a surgical edit that preserves reviewer history; accepting or rejecting revisions requires separate authorization.`,
23577
+ message: `Target paragraph contains tracked changes from another author (${[...authors, ...hasUnattributedTrackedChange(paragraph) ? ["unattributed"] : []].join(", ")}). Use existingRevisions: "slice-cross-author" for a surgical edit that preserves reviewer history; accepting or rejecting revisions requires separate authorization.`,
23461
23578
  revisionAuthors: authors,
23462
23579
  currentPolicy: existingPolicy
23463
23580
  };
@@ -25543,6 +25660,7 @@ export {
25543
25660
  canonicalNumberingDefinition,
25544
25661
  clearSingleLineListFallbackExplicitSequence,
25545
25662
  collectContiguousListParagraphBlock,
25663
+ compareDocumentInspections,
25546
25664
  computeDocumentPartsRevisionToken,
25547
25665
  computePackageRevisionToken,
25548
25666
  computeRevisionToken,
@@ -25594,6 +25712,7 @@ export {
25594
25712
  injectCommentsIntoPackage2 as injectCommentsIntoPackage,
25595
25713
  insertBodyElementBeforeSectPr,
25596
25714
  inspectDocumentParts,
25715
+ inspectionReviewShape,
25597
25716
  isLikelyStructuredTableSourceParagraph,
25598
25717
  isMarkdownTableText,
25599
25718
  isNodeVisibleInRevisionView,
@@ -25623,6 +25742,7 @@ export {
25623
25742
  repairKnownContentTypes,
25624
25743
  reserveNextNumberingId,
25625
25744
  reserveNextNumberingIdPair,
25745
+ resolveExistingRevisionsPolicy,
25626
25746
  resolveParagraphRangeByRefs,
25627
25747
  resolveSingleLineListFallbackNumberingAction,
25628
25748
  resolveTargetParagraph,