@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 CommonJS bundle v0.9.0 — https://github.com/AnsonLai/docx-redline-js
1
+ // @ansonlai/docx-redline-js standalone CommonJS 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;
@@ -8009,6 +8009,7 @@ __export(index_exports, {
8009
8009
  canonicalNumberingDefinition: () => canonicalNumberingDefinition,
8010
8010
  clearSingleLineListFallbackExplicitSequence: () => clearSingleLineListFallbackExplicitSequence,
8011
8011
  collectContiguousListParagraphBlock: () => collectContiguousListParagraphBlock,
8012
+ compareDocumentInspections: () => compareDocumentInspections,
8012
8013
  computeDocumentPartsRevisionToken: () => computeDocumentPartsRevisionToken,
8013
8014
  computePackageRevisionToken: () => computePackageRevisionToken,
8014
8015
  computeRevisionToken: () => computeRevisionToken,
@@ -8060,6 +8061,7 @@ __export(index_exports, {
8060
8061
  injectCommentsIntoPackage: () => injectCommentsIntoPackage2,
8061
8062
  insertBodyElementBeforeSectPr: () => insertBodyElementBeforeSectPr,
8062
8063
  inspectDocumentParts: () => inspectDocumentParts,
8064
+ inspectionReviewShape: () => inspectionReviewShape,
8063
8065
  isLikelyStructuredTableSourceParagraph: () => isLikelyStructuredTableSourceParagraph,
8064
8066
  isMarkdownTableText: () => isMarkdownTableText,
8065
8067
  isNodeVisibleInRevisionView: () => isNodeVisibleInRevisionView,
@@ -8089,6 +8091,7 @@ __export(index_exports, {
8089
8091
  repairKnownContentTypes: () => repairKnownContentTypes,
8090
8092
  reserveNextNumberingId: () => reserveNextNumberingId,
8091
8093
  reserveNextNumberingIdPair: () => reserveNextNumberingIdPair,
8094
+ resolveExistingRevisionsPolicy: () => resolveExistingRevisionsPolicy,
8092
8095
  resolveParagraphRangeByRefs: () => resolveParagraphRangeByRefs,
8093
8096
  resolveSingleLineListFallbackNumberingAction: () => resolveSingleLineListFallbackNumberingAction,
8094
8097
  resolveTargetParagraph: () => resolveTargetParagraph,
@@ -8287,7 +8290,10 @@ var MARKDOWN_PATTERNS = [
8287
8290
  { regex: /\*\*\+\+(.+?)\+\+\*\*/, format: { bold: true, underline: true } },
8288
8291
  // Bold: **text** or __text__
8289
8292
  { regex: /\*\*(.+?)\*\*/, format: { bold: true } },
8290
- { regex: /__(.+?)__/, format: { bold: true } },
8293
+ // Underscore delimiters follow CommonMark flanking rules: not intraword, not part of a
8294
+ // longer underscore run, and never around whitespace. So signature lines ("_____")
8295
+ // and snake_case identifiers stay literal text.
8296
+ { regex: /(?<![A-Za-z0-9_])__(?=[^_\s])(.+?)(?<=[^_\s])__(?![A-Za-z0-9_])/, format: { bold: true } },
8291
8297
  // Underline: ++text++
8292
8298
  { regex: /\+\+(.+?)\+\+/, format: { underline: true } },
8293
8299
  // Strikethrough: ~~text~~ or ~text~
@@ -8295,7 +8301,7 @@ var MARKDOWN_PATTERNS = [
8295
8301
  { regex: /~(.+?)~/, format: { strikethrough: true } },
8296
8302
  // Italic: *text* or _text_ (using lookahead only for compatibility)
8297
8303
  { regex: /\*(?!\*)(.+?)\*(?!\*)/, format: { italic: true } },
8298
- { regex: /_(?!_)(.+?)_(?!_)/, format: { italic: true } }
8304
+ { regex: /(?<![A-Za-z0-9_])_(?=[^_\s])(.+?)(?<=[^_\s])_(?![A-Za-z0-9_])/, format: { italic: true } }
8299
8305
  ];
8300
8306
  function preprocessMarkdown(text2) {
8301
8307
  if (!text2) {
@@ -10466,6 +10472,23 @@ function withOoxmlSourceType(result) {
10466
10472
  }
10467
10473
  return { ...result, sourceType: classifyOoxmlSourceType(result.oxml) };
10468
10474
  }
10475
+ function hasUnattributedTrackedChange(node) {
10476
+ if (!node) return false;
10477
+ for (const localName4 of TRACKED_CHANGE_NAMES) {
10478
+ for (const change of wordElementsByLocalName(node, localName4)) {
10479
+ const author = change.getAttribute?.("w:author") || change.getAttribute?.("author") || (typeof change.getAttributeNS === "function" ? change.getAttributeNS(NS_W, "author") : null);
10480
+ if (!author || !String(author).trim()) return true;
10481
+ }
10482
+ }
10483
+ return false;
10484
+ }
10485
+ function resolveExistingRevisionsPolicy(policy, node, author) {
10486
+ if (policy !== "auto") return policy;
10487
+ const nodes = (Array.isArray(node) ? node : [node]).filter(Boolean);
10488
+ const own = String(author || "").trim().toLowerCase();
10489
+ const foreign = nodes.some((item) => containsTrackedChanges(item) && (hasUnattributedTrackedChange(item) || getTrackedChangeAuthors(item).some((name) => name.trim().toLowerCase() !== own)));
10490
+ return foreign ? "slice-cross-author" : "merge-same-author";
10491
+ }
10469
10492
 
10470
10493
  // engine/paragraph-mark-revisions.js
10471
10494
  function localNameOf(node) {
@@ -15976,7 +15999,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
15976
15999
  const operationWarnings = [];
15977
16000
  let normalizedExistingRevisions = false;
15978
16001
  let preservedInsertedParagraphMark = null;
15979
- const existingRevisionsPolicy = options.existingRevisions || "merge-same-author";
16002
+ let existingRevisionsPolicy = options.existingRevisions || "merge-same-author";
15980
16003
  const keepNormalizedNoOp = existingRevisionsPolicy === "accept-all-first-keep-normalized";
15981
16004
  const finalize = (result) => {
15982
16005
  const withStatus = { ...result };
@@ -16024,6 +16047,10 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
16024
16047
  const parsed = parseOoxmlSafe(inputOoxml, "text/xml");
16025
16048
  parseWarnings = parsed.warnings;
16026
16049
  let xmlDoc = parsed.doc;
16050
+ if (existingRevisionsPolicy === "auto") {
16051
+ existingRevisionsPolicy = resolveExistingRevisionsPolicy("auto", xmlDoc, author);
16052
+ options = { ...options, existingRevisions: existingRevisionsPolicy };
16053
+ }
16027
16054
  const parseError = xmlDoc ? getXmlParseError(xmlDoc) : null;
16028
16055
  if (parsed.error || parseError) {
16029
16056
  const message = parsed.error?.message || parseError?.textContent || "Could not parse OOXML input.";
@@ -16058,7 +16085,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
16058
16085
  if (existingRevisionsPolicy === "merge-same-author" || existingRevisionsPolicy === "slice-cross-author") {
16059
16086
  const authors = getTrackedChangeAuthors(xmlDoc);
16060
16087
  const currentAuthor = String(author || "").trim().toLowerCase();
16061
- const isSameAuthor = authors.length > 0 && authors.every((a) => a.trim().toLowerCase() === currentAuthor);
16088
+ const isSameAuthor = authors.length > 0 && !hasUnattributedTrackedChange(xmlDoc) && authors.every((a) => a.trim().toLowerCase() === currentAuthor);
16062
16089
  if (isSameAuthor) {
16063
16090
  const commentIds = getCommentIdsInOoxml(xmlDoc);
16064
16091
  if (commentIds.length > 0) {
@@ -16215,7 +16242,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
16215
16242
  sanitizedText = structuredAnalysis.normalizedMarkdown;
16216
16243
  }
16217
16244
  }
16218
- const { cleanText: cleanModifiedText, formatHints } = preprocessMarkdown(sanitizedText);
16245
+ const { cleanText: cleanModifiedText, formatHints } = options.plainTextContent === true ? { cleanText: sanitizedText, formatHints: [] } : preprocessMarkdown(sanitizedText);
16219
16246
  const { existingFormatHints, textSpans, paragraphs } = extractFormattingFromOoxml(xmlDoc);
16220
16247
  const hasLiteralTextChanges = existingRevisionsPolicy === "slice-cross-author" ? cleanModifiedText !== originalText : cleanModifiedText.trim() !== originalText.trim();
16221
16248
  const hasListStructureChange = !hasLiteralTextChanges && options.explicitStructuredContent === true && isListTargetLoose(cleanModifiedText) && !sourceMatchesListShape(paragraphs, cleanModifiedText);
@@ -20047,6 +20074,70 @@ function inspectDocumentParts(parts, options = {}) {
20047
20074
  };
20048
20075
  }
20049
20076
 
20077
+ // services/inspection-comparison.js
20078
+ var commentIdentity = (comment) => [comment.author ?? null, comment.text ?? "", comment.anchoredText || ""];
20079
+ function reviewSegments(segments = []) {
20080
+ return segments.filter((segment) => segment.kind !== "baseline").reduce((result, segment) => {
20081
+ const next = {
20082
+ kind: segment.kind,
20083
+ author: segment.author || null,
20084
+ text: segment.text,
20085
+ acceptedStart: segment.acceptedStart ?? null,
20086
+ rejectedStart: segment.rejectedStart ?? null
20087
+ };
20088
+ const previous = result.at(-1);
20089
+ const adjacent = (key) => previous[key] == null && next[key] == null || previous[key] != null && previous[key] + previous.text.length === next[key];
20090
+ if (previous && previous.kind === next.kind && previous.author === next.author && adjacent("acceptedStart") && adjacent("rejectedStart")) previous.text += next.text;
20091
+ else result.push(next);
20092
+ return result;
20093
+ }, []);
20094
+ }
20095
+ function inspectionReviewShape(inspection) {
20096
+ const comments = inspection?.comments || [];
20097
+ return {
20098
+ paragraphs: (inspection?.paragraphs || []).map((paragraph) => ({
20099
+ text: paragraph.exactText,
20100
+ inTable: paragraph.inTable === true,
20101
+ table: paragraph.table ? [paragraph.table.rowIndex, paragraph.table.cellIndex] : null,
20102
+ list: paragraph.list ? [paragraph.list.format, paragraph.list.level, paragraph.list.label] : null,
20103
+ review: {
20104
+ present: paragraph.hasRevisions === true,
20105
+ authors: [...paragraph.revisionAuthors || []].sort(),
20106
+ segments: reviewSegments(paragraph.segments)
20107
+ }
20108
+ })),
20109
+ comments: comments.map((comment) => {
20110
+ const parent = comment.parentCommentId != null ? comments.find((item) => String(item.id) === String(comment.parentCommentId)) : null;
20111
+ return [
20112
+ ...commentIdentity(comment),
20113
+ comment.done === true,
20114
+ parent ? commentIdentity(parent) : comment.parentCommentId != null || comment.parentParaId ? "unresolved parent" : null
20115
+ ];
20116
+ }).map((item) => JSON.stringify(item)).sort()
20117
+ };
20118
+ }
20119
+ function compareDocumentInspections(expected, actual) {
20120
+ const left = inspectionReviewShape(expected);
20121
+ const right = inspectionReviewShape(actual);
20122
+ const differences = [];
20123
+ const note = (path, a, b) => {
20124
+ if (differences.length < 10 && JSON.stringify(a) !== JSON.stringify(b)) differences.push({ path, expected: a, actual: b });
20125
+ };
20126
+ note("paragraphs.length", left.paragraphs.length, right.paragraphs.length);
20127
+ const count = Math.max(left.paragraphs.length, right.paragraphs.length);
20128
+ for (let index = 0; index < count; index++) {
20129
+ const a = left.paragraphs[index] ?? null;
20130
+ const b = right.paragraphs[index] ?? null;
20131
+ if (!a || !b) {
20132
+ note(`paragraphs[${index}]`, a, b);
20133
+ continue;
20134
+ }
20135
+ for (const key of ["text", "inTable", "table", "list", "review"]) note(`paragraphs[${index}].${key}`, a[key], b[key]);
20136
+ }
20137
+ note("comments", left.comments, right.comments);
20138
+ return { matches: differences.length === 0, differences };
20139
+ }
20140
+
20050
20141
  // services/comment-locator.js
20051
20142
  function createParagraphTextIndex(paragraph, options = {}) {
20052
20143
  const revisionView = options.revisionView === "current" ? "accepted" : options.revisionView || "accepted";
@@ -20521,7 +20612,7 @@ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, doc
20521
20612
  extendedDoc = parsed.doc;
20522
20613
  }
20523
20614
  const occupied = usedParaIds(commentsDoc, extendedDoc);
20524
- if (documentXml) for (const match of String(documentXml).matchAll(/w14:paraId="([0-9A-Fa-f]{8})"/g)) occupied.add(match[1].toUpperCase());
20615
+ if (documentXml) for (const match of String(documentXml).matchAll(/\bw14:paraId="([0-9A-Fa-f]{8})"/g)) occupied.add(match[1].toUpperCase());
20525
20616
  const parentParagraph = commentThreadParagraph(parent);
20526
20617
  if (!parentParagraph) return { status: "error", error: { code: "PARENT_COMMENT_INVALID", message: `Parent comment '${parentCommentId}' has no paragraph.` } };
20527
20618
  let parentParaId = commentParaId(parent);
@@ -22823,6 +22914,7 @@ var SUPPORTED_OPERATION_TYPES = /* @__PURE__ */ new Set([
22823
22914
  "highlight"
22824
22915
  ]);
22825
22916
  var EXISTING_REVISIONS_POLICIES = Object.freeze([
22917
+ "auto",
22826
22918
  "merge-same-author",
22827
22919
  "slice-cross-author",
22828
22920
  "reject-input",
@@ -26877,13 +26969,17 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
26877
26969
  }
26878
26970
  };
26879
26971
  }
26972
+ const autoExistingRevisions = options.existingRevisions === "auto";
26973
+ if (autoExistingRevisions) {
26974
+ options = { ...options, existingRevisions: resolveExistingRevisionsPolicy("auto", targetParagraph, author) };
26975
+ }
26880
26976
  const hasRevisions = containsTrackedChanges(targetParagraph);
26881
26977
  const existingPolicy = options.existingRevisions || "merge-same-author";
26882
26978
  if (hasRevisions && existingPolicy !== "accept-all-first" && existingPolicy !== "accept-all-first-keep-normalized") {
26883
26979
  if (existingPolicy === "merge-same-author" || existingPolicy === "slice-cross-author") {
26884
26980
  const authors = getTrackedChangeAuthors(targetParagraph);
26885
26981
  const opAuthor = String(author || "").trim().toLowerCase();
26886
- const allSame = authors.length > 0 && authors.every((a) => a.trim().toLowerCase() === opAuthor);
26982
+ const allSame = authors.length > 0 && !hasUnattributedTrackedChange(targetParagraph) && authors.every((a) => a.trim().toLowerCase() === opAuthor);
26887
26983
  if (!allSame && existingPolicy === "merge-same-author") {
26888
26984
  return {
26889
26985
  documentXml,
@@ -26892,7 +26988,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
26892
26988
  status: "error",
26893
26989
  error: {
26894
26990
  code: "EXISTING_REVISIONS",
26895
- 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.`,
26991
+ 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.`,
26896
26992
  revisionAuthors: authors,
26897
26993
  currentPolicy: existingPolicy
26898
26994
  }
@@ -27034,6 +27130,9 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
27034
27130
  onWarn
27035
27131
  }) : typeof targetText === "string" && /\r?\n/.test(targetText) ? findReconstructionParagraphRange(xmlDoc, targetText) : null;
27036
27132
  const hasExplicitRangeScope = Array.isArray(explicitRangeParagraphs) && explicitRangeParagraphs.length > 0;
27133
+ if (autoExistingRevisions && hasExplicitRangeScope) {
27134
+ options = { ...options, existingRevisions: resolveExistingRevisionsPolicy("auto", explicitRangeParagraphs, author) };
27135
+ }
27037
27136
  if (!useTableScope && hasExplicitRangeScope) {
27038
27137
  const insertionEntries = buildExplicitRangeInsertionEntries(explicitRangeParagraphs, effectiveModifiedText);
27039
27138
  if (insertionEntries && insertionEntries.length > 0) {
@@ -27359,6 +27458,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
27359
27458
  pairReplacements: options.pairReplacements === true,
27360
27459
  insertionAffinity: options.insertionAffinity || null,
27361
27460
  literalTextEdit: options.literalTextEdit === true,
27461
+ plainTextContent: options.plainTextContent === true,
27362
27462
  _revisionIdAllocator: revisionIdAllocator,
27363
27463
  _isolatedTableCell: useTableScope
27364
27464
  });
@@ -28369,7 +28469,31 @@ function verifyUntrackedLocality({ before, after, targetSourceIndex, beforeViews
28369
28469
  }
28370
28470
  return violations.length ? { ok: false, violations } : { ok: true };
28371
28471
  }
28372
- function verifyNumberingReferences({ before, after, numberingXmls, beforeViews = null, afterViews = null }) {
28472
+ var numberingDefinitionCache = /* @__PURE__ */ new Map();
28473
+ var NUMBERING_DEFINITION_CACHE_SIZE = 8;
28474
+ function parseNumberingDefinitions(text2) {
28475
+ const cached = numberingDefinitionCache.get(text2);
28476
+ if (cached) return cached;
28477
+ const abstractLevels = /* @__PURE__ */ new Map();
28478
+ for (const match of text2.matchAll(/<(?:\w+:)?abstractNum\b([^>]*)>([\s\S]*?)<\/(?:\w+:)?abstractNum>/g)) {
28479
+ const id = /abstractNumId="([^"]*)"/.exec(match[1])?.[1];
28480
+ if (id == null) continue;
28481
+ const levels = abstractLevels.get(id) || /* @__PURE__ */ new Set();
28482
+ for (const lvl of match[2].matchAll(/<(?:\w+:)?lvl\b[^>]*?ilvl="([^"]*)"/g)) levels.add(lvl[1]);
28483
+ abstractLevels.set(id, levels);
28484
+ }
28485
+ const numToAbstract = /* @__PURE__ */ new Map();
28486
+ for (const match of text2.matchAll(/<(?:\w+:)?num\b([^>]*)>([\s\S]*?)<\/(?:\w+:)?num>/g)) {
28487
+ const id = /numId="([^"]*)"/.exec(match[1])?.[1];
28488
+ const abstractId = /abstractNumId[^>]*?val="([^"]*)"/.exec(match[2])?.[1];
28489
+ if (id != null && abstractId != null) numToAbstract.set(id, abstractId);
28490
+ }
28491
+ const definitions = { abstractLevels, numToAbstract };
28492
+ if (numberingDefinitionCache.size >= NUMBERING_DEFINITION_CACHE_SIZE) numberingDefinitionCache.delete(numberingDefinitionCache.keys().next().value);
28493
+ numberingDefinitionCache.set(text2, definitions);
28494
+ return definitions;
28495
+ }
28496
+ function verifyNumberingReferences({ before, after, numberingXmls }) {
28373
28497
  const references = (documentNode) => {
28374
28498
  const found = /* @__PURE__ */ new Map();
28375
28499
  for (const numIdNode of getAllReferencedNumIdElements(documentNode)) {
@@ -28385,21 +28509,7 @@ function verifyNumberingReferences({ before, after, numberingXmls, beforeViews =
28385
28509
  const used = references(after);
28386
28510
  if (used.size === 0) return { ok: true };
28387
28511
  const known = references(before);
28388
- const text2 = (numberingXmls || []).filter(Boolean).join("\n");
28389
- const abstractLevels = /* @__PURE__ */ new Map();
28390
- for (const match of text2.matchAll(/<(?:\w+:)?abstractNum\b([^>]*)>([\s\S]*?)<\/(?:\w+:)?abstractNum>/g)) {
28391
- const id = /abstractNumId="([^"]*)"/.exec(match[1])?.[1];
28392
- if (id == null) continue;
28393
- const levels = abstractLevels.get(id) || /* @__PURE__ */ new Set();
28394
- for (const lvl of match[2].matchAll(/<(?:\w+:)?lvl\b[^>]*?ilvl="([^"]*)"/g)) levels.add(lvl[1]);
28395
- abstractLevels.set(id, levels);
28396
- }
28397
- const numToAbstract = /* @__PURE__ */ new Map();
28398
- for (const match of text2.matchAll(/<(?:\w+:)?num\b([^>]*)>([\s\S]*?)<\/(?:\w+:)?num>/g)) {
28399
- const id = /numId="([^"]*)"/.exec(match[1])?.[1];
28400
- const abstractId = /abstractNumId[^>]*?val="([^"]*)"/.exec(match[2])?.[1];
28401
- if (id != null && abstractId != null) numToAbstract.set(id, abstractId);
28402
- }
28512
+ const { abstractLevels, numToAbstract } = parseNumberingDefinitions((numberingXmls || []).filter(Boolean).join("\n"));
28403
28513
  const violations = [];
28404
28514
  for (const [numId, levels] of used) {
28405
28515
  if (known.has(numId)) continue;
@@ -28748,6 +28858,7 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
28748
28858
  ...operation.existingRevisions ? { existingRevisions: operation.existingRevisions } : {},
28749
28859
  structuredContent: typeof operation.structuredContent === "boolean" ? operation.structuredContent : options.structuredContent !== false,
28750
28860
  explicitStructuredContent: operation.structuredContent === true,
28861
+ plainTextContent: operation.structuredContent === false || operation.structuredContent == null && options.structuredContent === false,
28751
28862
  ...operation._localizedReplacementCompilation && operation.structuredContent !== true ? { literalTextEdit: true } : {},
28752
28863
  pairReplacements: typeof operation.pairReplacements === "boolean" ? operation.pairReplacements : options.pairReplacements !== false,
28753
28864
  ...operation.insertionAffinity ? { insertionAffinity: operation.insertionAffinity } : {},
@@ -30002,35 +30113,44 @@ function preflightOperations(documentXml, operations, author, options = {}) {
30002
30113
  const anchor = operation.operationKind === "comment" ? operation.textToComment || paragraphText : operation.operationKind === "highlight" ? operation.textToHighlight : operation.operationKind === "format" ? operation.textToFormat : null;
30003
30114
  const anchorResolution = operation.operationKind === "comment" && anchor != null ? resolveTextInParagraphIndex(createParagraphTextIndex(paragraph, { revisionView: targetView }), anchor) : null;
30004
30115
  const anchorFound = anchor == null || (anchorResolution ? anchorResolution.found : paragraphText.includes(anchor));
30116
+ const rangeParagraphs = (opType) => {
30117
+ if (operation.targetEndRef) {
30118
+ return resolveParagraphRangeByRefs(xmlDoc, operation.targetRef, operation.targetEndRef, {
30119
+ opType,
30120
+ targetRefSnapshot: options.targetRefSnapshot || null,
30121
+ onInfo: options.onInfo,
30122
+ onWarn: options.onWarn
30123
+ });
30124
+ }
30125
+ if (!operation.targetEndDescriptor) return [paragraph];
30126
+ const endResolved = resolveTargetParagraph(xmlDoc, {
30127
+ targetText: operation.targetEndDescriptor.text,
30128
+ targetRef: operation.targetEndDescriptor.index,
30129
+ targetDescriptor: operation.targetEndDescriptor,
30130
+ opType,
30131
+ strictAmbiguity: strictTargets,
30132
+ paragraphMetadataIndex: currentMetadataIndex,
30133
+ metadataIndices,
30134
+ onInfo: options.onInfo,
30135
+ onWarn: options.onWarn
30136
+ });
30137
+ const allParagraphs = getDocumentParagraphNodes(xmlDoc);
30138
+ const startIndex = allParagraphs.indexOf(paragraph);
30139
+ const endIndex = allParagraphs.indexOf(endResolved?.paragraph);
30140
+ return startIndex >= 0 && endIndex >= startIndex ? allParagraphs.slice(startIndex, endIndex + 1) : null;
30141
+ };
30005
30142
  const hasRevisions = containsTrackedChanges(paragraph);
30006
- const existingPolicy = operation.existingRevisions || options.existingRevisions || "merge-same-author";
30143
+ const requestedPolicy = operation.existingRevisions || options.existingRevisions || "merge-same-author";
30144
+ const existingPolicy = resolveExistingRevisionsPolicy(
30145
+ requestedPolicy,
30146
+ requestedPolicy === "auto" && operation.operationKind === "redline" && (operation.targetEndRef || operation.targetEndDescriptor) ? rangeParagraphs("redline") || paragraph : paragraph,
30147
+ authorUsed
30148
+ );
30007
30149
  const deletingWholeParagraph = operation.operationKind === "redline" && operation.modified === "";
30008
30150
  const commentIds = deletingWholeParagraph ? getCommentIdsInParagraph(paragraph) : [];
30009
30151
  let error2 = null;
30010
30152
  if (operation.operationKind === "restore") {
30011
- let restorationParagraphs = operation.targetEndRef ? resolveParagraphRangeByRefs(xmlDoc, operation.targetRef, operation.targetEndRef, {
30012
- opType: "restore",
30013
- targetRefSnapshot: options.targetRefSnapshot || null,
30014
- onInfo: options.onInfo,
30015
- onWarn: options.onWarn
30016
- }) : [paragraph];
30017
- if (!operation.targetEndRef && operation.targetEndDescriptor) {
30018
- const endResolved = resolveTargetParagraph(xmlDoc, {
30019
- targetText: operation.targetEndDescriptor.text,
30020
- targetRef: operation.targetEndDescriptor.index,
30021
- targetDescriptor: operation.targetEndDescriptor,
30022
- opType: "restore",
30023
- strictAmbiguity: strictTargets,
30024
- paragraphMetadataIndex: currentMetadataIndex,
30025
- metadataIndices,
30026
- onInfo: options.onInfo,
30027
- onWarn: options.onWarn
30028
- });
30029
- const allParagraphs = getDocumentParagraphNodes(xmlDoc);
30030
- const startIndex = allParagraphs.indexOf(paragraph);
30031
- const endIndex = allParagraphs.indexOf(endResolved?.paragraph);
30032
- restorationParagraphs = startIndex >= 0 && endIndex >= startIndex ? allParagraphs.slice(startIndex, endIndex + 1) : null;
30033
- }
30153
+ const restorationParagraphs = rangeParagraphs("restore");
30034
30154
  const replacements = Array.isArray(operation.modified) ? operation.modified : [operation.modified];
30035
30155
  if (!restorationParagraphs || restorationParagraphs[0] !== paragraph || replacements.length !== restorationParagraphs.length) {
30036
30156
  error2 = {
@@ -30077,11 +30197,11 @@ function preflightOperations(documentXml, operations, author, options = {}) {
30077
30197
  if (existingPolicy === "merge-same-author" || existingPolicy === "slice-cross-author") {
30078
30198
  const authors = getTrackedChangeAuthors(paragraph);
30079
30199
  const opAuthor = String(authorUsed || "").trim().toLowerCase();
30080
- const allSame = authors.length > 0 && authors.every((a) => a.trim().toLowerCase() === opAuthor);
30200
+ const allSame = authors.length > 0 && !hasUnattributedTrackedChange(paragraph) && authors.every((a) => a.trim().toLowerCase() === opAuthor);
30081
30201
  if (!allSame && existingPolicy === "merge-same-author") {
30082
30202
  error2 = {
30083
30203
  code: "EXISTING_REVISIONS",
30084
- 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.`,
30204
+ 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.`,
30085
30205
  revisionAuthors: authors,
30086
30206
  currentPolicy: existingPolicy
30087
30207
  };