@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 standalone bundle v0.9.0 — https://github.com/AnsonLai/docx-redline-js
1
+ // @ansonlai/docx-redline-js standalone bundle 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;
@@ -8142,7 +8142,10 @@ var MARKDOWN_PATTERNS = [
8142
8142
  { regex: /\*\*\+\+(.+?)\+\+\*\*/, format: { bold: true, underline: true } },
8143
8143
  // Bold: **text** or __text__
8144
8144
  { regex: /\*\*(.+?)\*\*/, format: { bold: true } },
8145
- { regex: /__(.+?)__/, format: { bold: true } },
8145
+ // Underscore delimiters follow CommonMark flanking rules: not intraword, not part of a
8146
+ // longer underscore run, and never around whitespace. So signature lines ("_____")
8147
+ // and snake_case identifiers stay literal text.
8148
+ { regex: /(?<![A-Za-z0-9_])__(?=[^_\s])(.+?)(?<=[^_\s])__(?![A-Za-z0-9_])/, format: { bold: true } },
8146
8149
  // Underline: ++text++
8147
8150
  { regex: /\+\+(.+?)\+\+/, format: { underline: true } },
8148
8151
  // Strikethrough: ~~text~~ or ~text~
@@ -8150,7 +8153,7 @@ var MARKDOWN_PATTERNS = [
8150
8153
  { regex: /~(.+?)~/, format: { strikethrough: true } },
8151
8154
  // Italic: *text* or _text_ (using lookahead only for compatibility)
8152
8155
  { regex: /\*(?!\*)(.+?)\*(?!\*)/, format: { italic: true } },
8153
- { regex: /_(?!_)(.+?)_(?!_)/, format: { italic: true } }
8156
+ { regex: /(?<![A-Za-z0-9_])_(?=[^_\s])(.+?)(?<=[^_\s])_(?![A-Za-z0-9_])/, format: { italic: true } }
8154
8157
  ];
8155
8158
  function preprocessMarkdown(text2) {
8156
8159
  if (!text2) {
@@ -10321,6 +10324,23 @@ function withOoxmlSourceType(result) {
10321
10324
  }
10322
10325
  return { ...result, sourceType: classifyOoxmlSourceType(result.oxml) };
10323
10326
  }
10327
+ function hasUnattributedTrackedChange(node) {
10328
+ if (!node) return false;
10329
+ for (const localName4 of TRACKED_CHANGE_NAMES) {
10330
+ for (const change of wordElementsByLocalName(node, localName4)) {
10331
+ const author = change.getAttribute?.("w:author") || change.getAttribute?.("author") || (typeof change.getAttributeNS === "function" ? change.getAttributeNS(NS_W, "author") : null);
10332
+ if (!author || !String(author).trim()) return true;
10333
+ }
10334
+ }
10335
+ return false;
10336
+ }
10337
+ function resolveExistingRevisionsPolicy(policy, node, author) {
10338
+ if (policy !== "auto") return policy;
10339
+ const nodes = (Array.isArray(node) ? node : [node]).filter(Boolean);
10340
+ const own = String(author || "").trim().toLowerCase();
10341
+ const foreign = nodes.some((item) => containsTrackedChanges(item) && (hasUnattributedTrackedChange(item) || getTrackedChangeAuthors(item).some((name) => name.trim().toLowerCase() !== own)));
10342
+ return foreign ? "slice-cross-author" : "merge-same-author";
10343
+ }
10324
10344
 
10325
10345
  // engine/paragraph-mark-revisions.js
10326
10346
  function localNameOf(node) {
@@ -15831,7 +15851,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
15831
15851
  const operationWarnings = [];
15832
15852
  let normalizedExistingRevisions = false;
15833
15853
  let preservedInsertedParagraphMark = null;
15834
- const existingRevisionsPolicy = options.existingRevisions || "merge-same-author";
15854
+ let existingRevisionsPolicy = options.existingRevisions || "merge-same-author";
15835
15855
  const keepNormalizedNoOp = existingRevisionsPolicy === "accept-all-first-keep-normalized";
15836
15856
  const finalize = (result) => {
15837
15857
  const withStatus = { ...result };
@@ -15879,6 +15899,10 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
15879
15899
  const parsed = parseOoxmlSafe(inputOoxml, "text/xml");
15880
15900
  parseWarnings = parsed.warnings;
15881
15901
  let xmlDoc = parsed.doc;
15902
+ if (existingRevisionsPolicy === "auto") {
15903
+ existingRevisionsPolicy = resolveExistingRevisionsPolicy("auto", xmlDoc, author);
15904
+ options = { ...options, existingRevisions: existingRevisionsPolicy };
15905
+ }
15882
15906
  const parseError = xmlDoc ? getXmlParseError(xmlDoc) : null;
15883
15907
  if (parsed.error || parseError) {
15884
15908
  const message = parsed.error?.message || parseError?.textContent || "Could not parse OOXML input.";
@@ -15913,7 +15937,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
15913
15937
  if (existingRevisionsPolicy === "merge-same-author" || existingRevisionsPolicy === "slice-cross-author") {
15914
15938
  const authors = getTrackedChangeAuthors(xmlDoc);
15915
15939
  const currentAuthor = String(author || "").trim().toLowerCase();
15916
- const isSameAuthor = authors.length > 0 && authors.every((a) => a.trim().toLowerCase() === currentAuthor);
15940
+ const isSameAuthor = authors.length > 0 && !hasUnattributedTrackedChange(xmlDoc) && authors.every((a) => a.trim().toLowerCase() === currentAuthor);
15917
15941
  if (isSameAuthor) {
15918
15942
  const commentIds = getCommentIdsInOoxml(xmlDoc);
15919
15943
  if (commentIds.length > 0) {
@@ -16070,7 +16094,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
16070
16094
  sanitizedText = structuredAnalysis.normalizedMarkdown;
16071
16095
  }
16072
16096
  }
16073
- const { cleanText: cleanModifiedText, formatHints } = preprocessMarkdown(sanitizedText);
16097
+ const { cleanText: cleanModifiedText, formatHints } = options.plainTextContent === true ? { cleanText: sanitizedText, formatHints: [] } : preprocessMarkdown(sanitizedText);
16074
16098
  const { existingFormatHints, textSpans, paragraphs } = extractFormattingFromOoxml(xmlDoc);
16075
16099
  const hasLiteralTextChanges = existingRevisionsPolicy === "slice-cross-author" ? cleanModifiedText !== originalText : cleanModifiedText.trim() !== originalText.trim();
16076
16100
  const hasListStructureChange = !hasLiteralTextChanges && options.explicitStructuredContent === true && isListTargetLoose(cleanModifiedText) && !sourceMatchesListShape(paragraphs, cleanModifiedText);
@@ -19902,6 +19926,70 @@ function inspectDocumentParts(parts, options = {}) {
19902
19926
  };
19903
19927
  }
19904
19928
 
19929
+ // services/inspection-comparison.js
19930
+ var commentIdentity = (comment) => [comment.author ?? null, comment.text ?? "", comment.anchoredText || ""];
19931
+ function reviewSegments(segments = []) {
19932
+ return segments.filter((segment) => segment.kind !== "baseline").reduce((result, segment) => {
19933
+ const next = {
19934
+ kind: segment.kind,
19935
+ author: segment.author || null,
19936
+ text: segment.text,
19937
+ acceptedStart: segment.acceptedStart ?? null,
19938
+ rejectedStart: segment.rejectedStart ?? null
19939
+ };
19940
+ const previous = result.at(-1);
19941
+ const adjacent = (key) => previous[key] == null && next[key] == null || previous[key] != null && previous[key] + previous.text.length === next[key];
19942
+ if (previous && previous.kind === next.kind && previous.author === next.author && adjacent("acceptedStart") && adjacent("rejectedStart")) previous.text += next.text;
19943
+ else result.push(next);
19944
+ return result;
19945
+ }, []);
19946
+ }
19947
+ function inspectionReviewShape(inspection) {
19948
+ const comments = inspection?.comments || [];
19949
+ return {
19950
+ paragraphs: (inspection?.paragraphs || []).map((paragraph) => ({
19951
+ text: paragraph.exactText,
19952
+ inTable: paragraph.inTable === true,
19953
+ table: paragraph.table ? [paragraph.table.rowIndex, paragraph.table.cellIndex] : null,
19954
+ list: paragraph.list ? [paragraph.list.format, paragraph.list.level, paragraph.list.label] : null,
19955
+ review: {
19956
+ present: paragraph.hasRevisions === true,
19957
+ authors: [...paragraph.revisionAuthors || []].sort(),
19958
+ segments: reviewSegments(paragraph.segments)
19959
+ }
19960
+ })),
19961
+ comments: comments.map((comment) => {
19962
+ const parent = comment.parentCommentId != null ? comments.find((item) => String(item.id) === String(comment.parentCommentId)) : null;
19963
+ return [
19964
+ ...commentIdentity(comment),
19965
+ comment.done === true,
19966
+ parent ? commentIdentity(parent) : comment.parentCommentId != null || comment.parentParaId ? "unresolved parent" : null
19967
+ ];
19968
+ }).map((item) => JSON.stringify(item)).sort()
19969
+ };
19970
+ }
19971
+ function compareDocumentInspections(expected, actual) {
19972
+ const left = inspectionReviewShape(expected);
19973
+ const right = inspectionReviewShape(actual);
19974
+ const differences = [];
19975
+ const note = (path, a, b) => {
19976
+ if (differences.length < 10 && JSON.stringify(a) !== JSON.stringify(b)) differences.push({ path, expected: a, actual: b });
19977
+ };
19978
+ note("paragraphs.length", left.paragraphs.length, right.paragraphs.length);
19979
+ const count = Math.max(left.paragraphs.length, right.paragraphs.length);
19980
+ for (let index = 0; index < count; index++) {
19981
+ const a = left.paragraphs[index] ?? null;
19982
+ const b = right.paragraphs[index] ?? null;
19983
+ if (!a || !b) {
19984
+ note(`paragraphs[${index}]`, a, b);
19985
+ continue;
19986
+ }
19987
+ for (const key of ["text", "inTable", "table", "list", "review"]) note(`paragraphs[${index}].${key}`, a[key], b[key]);
19988
+ }
19989
+ note("comments", left.comments, right.comments);
19990
+ return { matches: differences.length === 0, differences };
19991
+ }
19992
+
19905
19993
  // services/comment-locator.js
19906
19994
  function createParagraphTextIndex(paragraph, options = {}) {
19907
19995
  const revisionView = options.revisionView === "current" ? "accepted" : options.revisionView || "accepted";
@@ -20376,7 +20464,7 @@ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, doc
20376
20464
  extendedDoc = parsed.doc;
20377
20465
  }
20378
20466
  const occupied = usedParaIds(commentsDoc, extendedDoc);
20379
- if (documentXml) for (const match of String(documentXml).matchAll(/w14:paraId="([0-9A-Fa-f]{8})"/g)) occupied.add(match[1].toUpperCase());
20467
+ if (documentXml) for (const match of String(documentXml).matchAll(/\bw14:paraId="([0-9A-Fa-f]{8})"/g)) occupied.add(match[1].toUpperCase());
20380
20468
  const parentParagraph = commentThreadParagraph(parent);
20381
20469
  if (!parentParagraph) return { status: "error", error: { code: "PARENT_COMMENT_INVALID", message: `Parent comment '${parentCommentId}' has no paragraph.` } };
20382
20470
  let parentParaId = commentParaId(parent);
@@ -22678,6 +22766,7 @@ var SUPPORTED_OPERATION_TYPES = /* @__PURE__ */ new Set([
22678
22766
  "highlight"
22679
22767
  ]);
22680
22768
  var EXISTING_REVISIONS_POLICIES = Object.freeze([
22769
+ "auto",
22681
22770
  "merge-same-author",
22682
22771
  "slice-cross-author",
22683
22772
  "reject-input",
@@ -26732,13 +26821,17 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
26732
26821
  }
26733
26822
  };
26734
26823
  }
26824
+ const autoExistingRevisions = options.existingRevisions === "auto";
26825
+ if (autoExistingRevisions) {
26826
+ options = { ...options, existingRevisions: resolveExistingRevisionsPolicy("auto", targetParagraph, author) };
26827
+ }
26735
26828
  const hasRevisions = containsTrackedChanges(targetParagraph);
26736
26829
  const existingPolicy = options.existingRevisions || "merge-same-author";
26737
26830
  if (hasRevisions && existingPolicy !== "accept-all-first" && existingPolicy !== "accept-all-first-keep-normalized") {
26738
26831
  if (existingPolicy === "merge-same-author" || existingPolicy === "slice-cross-author") {
26739
26832
  const authors = getTrackedChangeAuthors(targetParagraph);
26740
26833
  const opAuthor = String(author || "").trim().toLowerCase();
26741
- const allSame = authors.length > 0 && authors.every((a) => a.trim().toLowerCase() === opAuthor);
26834
+ const allSame = authors.length > 0 && !hasUnattributedTrackedChange(targetParagraph) && authors.every((a) => a.trim().toLowerCase() === opAuthor);
26742
26835
  if (!allSame && existingPolicy === "merge-same-author") {
26743
26836
  return {
26744
26837
  documentXml,
@@ -26747,7 +26840,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
26747
26840
  status: "error",
26748
26841
  error: {
26749
26842
  code: "EXISTING_REVISIONS",
26750
- 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.`,
26843
+ 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.`,
26751
26844
  revisionAuthors: authors,
26752
26845
  currentPolicy: existingPolicy
26753
26846
  }
@@ -26889,6 +26982,9 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
26889
26982
  onWarn
26890
26983
  }) : typeof targetText === "string" && /\r?\n/.test(targetText) ? findReconstructionParagraphRange(xmlDoc, targetText) : null;
26891
26984
  const hasExplicitRangeScope = Array.isArray(explicitRangeParagraphs) && explicitRangeParagraphs.length > 0;
26985
+ if (autoExistingRevisions && hasExplicitRangeScope) {
26986
+ options = { ...options, existingRevisions: resolveExistingRevisionsPolicy("auto", explicitRangeParagraphs, author) };
26987
+ }
26892
26988
  if (!useTableScope && hasExplicitRangeScope) {
26893
26989
  const insertionEntries = buildExplicitRangeInsertionEntries(explicitRangeParagraphs, effectiveModifiedText);
26894
26990
  if (insertionEntries && insertionEntries.length > 0) {
@@ -27214,6 +27310,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
27214
27310
  pairReplacements: options.pairReplacements === true,
27215
27311
  insertionAffinity: options.insertionAffinity || null,
27216
27312
  literalTextEdit: options.literalTextEdit === true,
27313
+ plainTextContent: options.plainTextContent === true,
27217
27314
  _revisionIdAllocator: revisionIdAllocator,
27218
27315
  _isolatedTableCell: useTableScope
27219
27316
  });
@@ -28224,7 +28321,31 @@ function verifyUntrackedLocality({ before, after, targetSourceIndex, beforeViews
28224
28321
  }
28225
28322
  return violations.length ? { ok: false, violations } : { ok: true };
28226
28323
  }
28227
- function verifyNumberingReferences({ before, after, numberingXmls, beforeViews = null, afterViews = null }) {
28324
+ var numberingDefinitionCache = /* @__PURE__ */ new Map();
28325
+ var NUMBERING_DEFINITION_CACHE_SIZE = 8;
28326
+ function parseNumberingDefinitions(text2) {
28327
+ const cached = numberingDefinitionCache.get(text2);
28328
+ if (cached) return cached;
28329
+ const abstractLevels = /* @__PURE__ */ new Map();
28330
+ for (const match of text2.matchAll(/<(?:\w+:)?abstractNum\b([^>]*)>([\s\S]*?)<\/(?:\w+:)?abstractNum>/g)) {
28331
+ const id = /abstractNumId="([^"]*)"/.exec(match[1])?.[1];
28332
+ if (id == null) continue;
28333
+ const levels = abstractLevels.get(id) || /* @__PURE__ */ new Set();
28334
+ for (const lvl of match[2].matchAll(/<(?:\w+:)?lvl\b[^>]*?ilvl="([^"]*)"/g)) levels.add(lvl[1]);
28335
+ abstractLevels.set(id, levels);
28336
+ }
28337
+ const numToAbstract = /* @__PURE__ */ new Map();
28338
+ for (const match of text2.matchAll(/<(?:\w+:)?num\b([^>]*)>([\s\S]*?)<\/(?:\w+:)?num>/g)) {
28339
+ const id = /numId="([^"]*)"/.exec(match[1])?.[1];
28340
+ const abstractId = /abstractNumId[^>]*?val="([^"]*)"/.exec(match[2])?.[1];
28341
+ if (id != null && abstractId != null) numToAbstract.set(id, abstractId);
28342
+ }
28343
+ const definitions = { abstractLevels, numToAbstract };
28344
+ if (numberingDefinitionCache.size >= NUMBERING_DEFINITION_CACHE_SIZE) numberingDefinitionCache.delete(numberingDefinitionCache.keys().next().value);
28345
+ numberingDefinitionCache.set(text2, definitions);
28346
+ return definitions;
28347
+ }
28348
+ function verifyNumberingReferences({ before, after, numberingXmls }) {
28228
28349
  const references = (documentNode) => {
28229
28350
  const found = /* @__PURE__ */ new Map();
28230
28351
  for (const numIdNode of getAllReferencedNumIdElements(documentNode)) {
@@ -28240,21 +28361,7 @@ function verifyNumberingReferences({ before, after, numberingXmls, beforeViews =
28240
28361
  const used = references(after);
28241
28362
  if (used.size === 0) return { ok: true };
28242
28363
  const known = references(before);
28243
- const text2 = (numberingXmls || []).filter(Boolean).join("\n");
28244
- const abstractLevels = /* @__PURE__ */ new Map();
28245
- for (const match of text2.matchAll(/<(?:\w+:)?abstractNum\b([^>]*)>([\s\S]*?)<\/(?:\w+:)?abstractNum>/g)) {
28246
- const id = /abstractNumId="([^"]*)"/.exec(match[1])?.[1];
28247
- if (id == null) continue;
28248
- const levels = abstractLevels.get(id) || /* @__PURE__ */ new Set();
28249
- for (const lvl of match[2].matchAll(/<(?:\w+:)?lvl\b[^>]*?ilvl="([^"]*)"/g)) levels.add(lvl[1]);
28250
- abstractLevels.set(id, levels);
28251
- }
28252
- const numToAbstract = /* @__PURE__ */ new Map();
28253
- for (const match of text2.matchAll(/<(?:\w+:)?num\b([^>]*)>([\s\S]*?)<\/(?:\w+:)?num>/g)) {
28254
- const id = /numId="([^"]*)"/.exec(match[1])?.[1];
28255
- const abstractId = /abstractNumId[^>]*?val="([^"]*)"/.exec(match[2])?.[1];
28256
- if (id != null && abstractId != null) numToAbstract.set(id, abstractId);
28257
- }
28364
+ const { abstractLevels, numToAbstract } = parseNumberingDefinitions((numberingXmls || []).filter(Boolean).join("\n"));
28258
28365
  const violations = [];
28259
28366
  for (const [numId, levels] of used) {
28260
28367
  if (known.has(numId)) continue;
@@ -28603,6 +28710,7 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
28603
28710
  ...operation.existingRevisions ? { existingRevisions: operation.existingRevisions } : {},
28604
28711
  structuredContent: typeof operation.structuredContent === "boolean" ? operation.structuredContent : options.structuredContent !== false,
28605
28712
  explicitStructuredContent: operation.structuredContent === true,
28713
+ plainTextContent: operation.structuredContent === false || operation.structuredContent == null && options.structuredContent === false,
28606
28714
  ...operation._localizedReplacementCompilation && operation.structuredContent !== true ? { literalTextEdit: true } : {},
28607
28715
  pairReplacements: typeof operation.pairReplacements === "boolean" ? operation.pairReplacements : options.pairReplacements !== false,
28608
28716
  ...operation.insertionAffinity ? { insertionAffinity: operation.insertionAffinity } : {},
@@ -29857,35 +29965,44 @@ function preflightOperations(documentXml, operations, author, options = {}) {
29857
29965
  const anchor = operation.operationKind === "comment" ? operation.textToComment || paragraphText : operation.operationKind === "highlight" ? operation.textToHighlight : operation.operationKind === "format" ? operation.textToFormat : null;
29858
29966
  const anchorResolution = operation.operationKind === "comment" && anchor != null ? resolveTextInParagraphIndex(createParagraphTextIndex(paragraph, { revisionView: targetView }), anchor) : null;
29859
29967
  const anchorFound = anchor == null || (anchorResolution ? anchorResolution.found : paragraphText.includes(anchor));
29968
+ const rangeParagraphs = (opType) => {
29969
+ if (operation.targetEndRef) {
29970
+ return resolveParagraphRangeByRefs(xmlDoc, operation.targetRef, operation.targetEndRef, {
29971
+ opType,
29972
+ targetRefSnapshot: options.targetRefSnapshot || null,
29973
+ onInfo: options.onInfo,
29974
+ onWarn: options.onWarn
29975
+ });
29976
+ }
29977
+ if (!operation.targetEndDescriptor) return [paragraph];
29978
+ const endResolved = resolveTargetParagraph(xmlDoc, {
29979
+ targetText: operation.targetEndDescriptor.text,
29980
+ targetRef: operation.targetEndDescriptor.index,
29981
+ targetDescriptor: operation.targetEndDescriptor,
29982
+ opType,
29983
+ strictAmbiguity: strictTargets,
29984
+ paragraphMetadataIndex: currentMetadataIndex,
29985
+ metadataIndices,
29986
+ onInfo: options.onInfo,
29987
+ onWarn: options.onWarn
29988
+ });
29989
+ const allParagraphs = getDocumentParagraphNodes(xmlDoc);
29990
+ const startIndex = allParagraphs.indexOf(paragraph);
29991
+ const endIndex = allParagraphs.indexOf(endResolved?.paragraph);
29992
+ return startIndex >= 0 && endIndex >= startIndex ? allParagraphs.slice(startIndex, endIndex + 1) : null;
29993
+ };
29860
29994
  const hasRevisions = containsTrackedChanges(paragraph);
29861
- const existingPolicy = operation.existingRevisions || options.existingRevisions || "merge-same-author";
29995
+ const requestedPolicy = operation.existingRevisions || options.existingRevisions || "merge-same-author";
29996
+ const existingPolicy = resolveExistingRevisionsPolicy(
29997
+ requestedPolicy,
29998
+ requestedPolicy === "auto" && operation.operationKind === "redline" && (operation.targetEndRef || operation.targetEndDescriptor) ? rangeParagraphs("redline") || paragraph : paragraph,
29999
+ authorUsed
30000
+ );
29862
30001
  const deletingWholeParagraph = operation.operationKind === "redline" && operation.modified === "";
29863
30002
  const commentIds = deletingWholeParagraph ? getCommentIdsInParagraph(paragraph) : [];
29864
30003
  let error2 = null;
29865
30004
  if (operation.operationKind === "restore") {
29866
- let restorationParagraphs = operation.targetEndRef ? resolveParagraphRangeByRefs(xmlDoc, operation.targetRef, operation.targetEndRef, {
29867
- opType: "restore",
29868
- targetRefSnapshot: options.targetRefSnapshot || null,
29869
- onInfo: options.onInfo,
29870
- onWarn: options.onWarn
29871
- }) : [paragraph];
29872
- if (!operation.targetEndRef && operation.targetEndDescriptor) {
29873
- const endResolved = resolveTargetParagraph(xmlDoc, {
29874
- targetText: operation.targetEndDescriptor.text,
29875
- targetRef: operation.targetEndDescriptor.index,
29876
- targetDescriptor: operation.targetEndDescriptor,
29877
- opType: "restore",
29878
- strictAmbiguity: strictTargets,
29879
- paragraphMetadataIndex: currentMetadataIndex,
29880
- metadataIndices,
29881
- onInfo: options.onInfo,
29882
- onWarn: options.onWarn
29883
- });
29884
- const allParagraphs = getDocumentParagraphNodes(xmlDoc);
29885
- const startIndex = allParagraphs.indexOf(paragraph);
29886
- const endIndex = allParagraphs.indexOf(endResolved?.paragraph);
29887
- restorationParagraphs = startIndex >= 0 && endIndex >= startIndex ? allParagraphs.slice(startIndex, endIndex + 1) : null;
29888
- }
30005
+ const restorationParagraphs = rangeParagraphs("restore");
29889
30006
  const replacements = Array.isArray(operation.modified) ? operation.modified : [operation.modified];
29890
30007
  if (!restorationParagraphs || restorationParagraphs[0] !== paragraph || replacements.length !== restorationParagraphs.length) {
29891
30008
  error2 = {
@@ -29932,11 +30049,11 @@ function preflightOperations(documentXml, operations, author, options = {}) {
29932
30049
  if (existingPolicy === "merge-same-author" || existingPolicy === "slice-cross-author") {
29933
30050
  const authors = getTrackedChangeAuthors(paragraph);
29934
30051
  const opAuthor = String(authorUsed || "").trim().toLowerCase();
29935
- const allSame = authors.length > 0 && authors.every((a) => a.trim().toLowerCase() === opAuthor);
30052
+ const allSame = authors.length > 0 && !hasUnattributedTrackedChange(paragraph) && authors.every((a) => a.trim().toLowerCase() === opAuthor);
29936
30053
  if (!allSame && existingPolicy === "merge-same-author") {
29937
30054
  error2 = {
29938
30055
  code: "EXISTING_REVISIONS",
29939
- 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.`,
30056
+ 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.`,
29940
30057
  revisionAuthors: authors,
29941
30058
  currentPolicy: existingPolicy
29942
30059
  };
@@ -32022,6 +32139,7 @@ export {
32022
32139
  canonicalNumberingDefinition,
32023
32140
  clearSingleLineListFallbackExplicitSequence,
32024
32141
  collectContiguousListParagraphBlock,
32142
+ compareDocumentInspections,
32025
32143
  computeDocumentPartsRevisionToken,
32026
32144
  computePackageRevisionToken,
32027
32145
  computeRevisionToken,
@@ -32073,6 +32191,7 @@ export {
32073
32191
  injectCommentsIntoPackage2 as injectCommentsIntoPackage,
32074
32192
  insertBodyElementBeforeSectPr,
32075
32193
  inspectDocumentParts,
32194
+ inspectionReviewShape,
32076
32195
  isLikelyStructuredTableSourceParagraph,
32077
32196
  isMarkdownTableText,
32078
32197
  isNodeVisibleInRevisionView,
@@ -32102,6 +32221,7 @@ export {
32102
32221
  repairKnownContentTypes,
32103
32222
  reserveNextNumberingId,
32104
32223
  reserveNextNumberingIdPair,
32224
+ resolveExistingRevisionsPolicy,
32105
32225
  resolveParagraphRangeByRefs,
32106
32226
  resolveSingleLineListFallbackNumberingAction,
32107
32227
  resolveTargetParagraph,