@ansonlai/docx-redline-js 0.8.0 → 0.8.2

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +553 -486
  2. package/README.md +331 -309
  3. package/dist/docx-redline-js.esm.js +1209 -168
  4. package/dist/docx-redline-js.esm.js.map +4 -4
  5. package/dist/docx-redline-js.esm.min.js +107 -107
  6. package/dist/docx-redline-js.esm.min.js.map +4 -4
  7. package/dist/docx-redline.bundle.cjs +1209 -168
  8. package/dist/docx-redline.bundle.cjs.map +4 -4
  9. package/dist/docx-redline.bundle.js +1209 -168
  10. package/dist/docx-redline.bundle.js.map +4 -4
  11. package/docs/AGENT_KNOWLEDGE_BASE.md +69 -8
  12. package/docs/schemas/document-operations.schema.json +29 -0
  13. package/document/docx-document.js +416 -22
  14. package/engine/reconstruction-mode.js +183 -54
  15. package/engine/reconstruction-writer.js +18 -0
  16. package/index.d.ts +598 -575
  17. package/index.js +349 -348
  18. package/node/cli-help.js +5 -5
  19. package/node/cli.js +760 -754
  20. package/node/index.d.ts +1 -1
  21. package/package.json +2 -1
  22. package/pipeline/diff-engine.js +274 -258
  23. package/scripts/generate-word-comment-edge-case-fixture.ps1 +58 -0
  24. package/scripts/generate-word-comment-thread-fixture.ps1 +36 -0
  25. package/scripts/generate-word-header-footer-fixture.ps1 +32 -0
  26. package/scripts/generate-word-multi-section-fixture.ps1 +27 -0
  27. package/scripts/generate-word-package-integrity-cases.mjs +141 -0
  28. package/scripts/generate-word-thread-state-fixtures.ps1 +50 -0
  29. package/scripts/run-tests.mjs +14 -2
  30. package/scripts/word-com-package-integrity.ps1 +98 -0
  31. package/services/batch-operation-orchestrator.js +262 -259
  32. package/services/comment-replies.js +96 -95
  33. package/services/comment-thread-parts.js +280 -0
  34. package/services/document-inspection.js +366 -365
  35. package/services/document-operation-applier.js +230 -202
  36. package/services/document-operation-contract.js +17 -2
  37. package/services/error-recovery.js +6 -0
  38. package/services/headers-footers.js +206 -0
  39. package/services/operation-batch-compiler.js +1 -0
  40. package/services/operation-preflight.js +231 -218
  41. package/services/package-parts.js +80 -0
  42. package/services/standalone-docx-plumbing.js +631 -569
  43. package/services/standalone-operation-runner.d.ts +19 -3
@@ -1,4 +1,4 @@
1
- // @ansonlai/docx-redline-js standalone CommonJS bundle v0.8.0 — https://github.com/AnsonLai/docx-redline-js
1
+ // @ansonlai/docx-redline-js standalone CommonJS bundle v0.8.2 — 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;
@@ -8080,6 +8080,7 @@ __export(index_exports, {
8080
8080
  rejectTrackedChangesInOoxml: () => rejectTrackedChangesInOoxml,
8081
8081
  remapNumberingPayloadForDocument: () => remapNumberingPayloadForDocument,
8082
8082
  removeFormattingFromRPr: () => removeFormattingFromRPr,
8083
+ repairKnownContentTypes: () => repairKnownContentTypes,
8083
8084
  reserveNextNumberingId: () => reserveNextNumberingId,
8084
8085
  reserveNextNumberingIdPair: () => reserveNextNumberingIdPair,
8085
8086
  resolveParagraphRangeByRefs: () => resolveParagraphRangeByRefs,
@@ -9138,15 +9139,29 @@ function createDiffEngine(options = {}) {
9138
9139
  engine.Diff_Timeout = timeout;
9139
9140
  return engine;
9140
9141
  }
9141
- function tokenize(text2) {
9142
+ function tokenize(text2, atomicChars = null) {
9142
9143
  const tokens = [];
9144
+ const pushWord = (word) => {
9145
+ if (!atomicChars || atomicChars.size === 0) {
9146
+ tokens.push(word);
9147
+ return;
9148
+ }
9149
+ let start = 0;
9150
+ for (let index = 0; index < word.length; index++) {
9151
+ if (!atomicChars.has(word[index])) continue;
9152
+ if (index > start) tokens.push(word.slice(start, index));
9153
+ tokens.push(word[index]);
9154
+ start = index + 1;
9155
+ }
9156
+ if (start < word.length) tokens.push(word.slice(start));
9157
+ };
9143
9158
  const leading = text2.match(/^\s+/);
9144
9159
  if (leading) tokens.push(leading[0]);
9145
9160
  const regex = /(\S+)(\s*)/g;
9146
9161
  regex.lastIndex = leading?.[0].length || 0;
9147
9162
  let match;
9148
9163
  while ((match = regex.exec(text2)) !== null) {
9149
- if (match[1]) tokens.push(match[1]);
9164
+ if (match[1]) pushWord(match[1]);
9150
9165
  if (match[2]) tokens.push(match[2]);
9151
9166
  }
9152
9167
  return tokens;
@@ -9174,8 +9189,8 @@ function wordsToChars(text1, text2, options = {}) {
9174
9189
  }
9175
9190
  return { chars, tokenIds };
9176
9191
  }
9177
- const tokens1 = tokenize(text1);
9178
- const tokens2 = tokenize(text2);
9192
+ const tokens1 = tokenize(text1, options.atomicChars);
9193
+ const tokens2 = tokenize(text2, options.atomicChars);
9179
9194
  const encoded1 = mapTokensToChars(tokens1);
9180
9195
  const encoded2 = mapTokensToChars(tokens2);
9181
9196
  return {
@@ -14333,6 +14348,19 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
14333
14348
  pendingReplacementEvent = null;
14334
14349
  }
14335
14350
  }
14351
+ for (const offset of Array.from(sentinelMapByStart.keys()).sort((a, b) => a - b)) {
14352
+ for (const sentinel of sentinelMapByStart.get(offset)) {
14353
+ if (!sentinel.isCommentMarker || emittedCommentMarkers.has(sentinel.node)) continue;
14354
+ emittedCommentMarkers.add(sentinel.node);
14355
+ if (isWordElement(sentinel.node, "commentReference")) {
14356
+ const run = createWordElement(xmlDoc, "w:r");
14357
+ run.appendChild(sentinel.node.cloneNode(true));
14358
+ currentParagraph.appendChild(run);
14359
+ } else {
14360
+ currentParagraph.appendChild(sentinel.node.cloneNode(true));
14361
+ }
14362
+ }
14363
+ }
14336
14364
  if (generateRedlines) {
14337
14365
  containerFragments.forEach((fragment) => {
14338
14366
  Array.from(fragment.childNodes).forEach((node) => {
@@ -14567,7 +14595,11 @@ function applyReconstructionMode(xmlDoc, originalText, modifiedText, serializer,
14567
14595
  if (mapping.paragraphs.length === 0) {
14568
14596
  return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: false });
14569
14597
  }
14570
- const diffs = computeWordDiffs(mapping.originalFullText, mapping.processedModifiedText, diffOptions);
14598
+ const wordDiffs = computeWordDiffs(mapping.originalFullText, mapping.processedModifiedText, {
14599
+ ...diffOptions,
14600
+ atomicChars: new Set(mapping.referenceMap.keys())
14601
+ });
14602
+ const diffs = keepUnchangedTextOutsideWrappers(anchorSharedReferences(wordDiffs, mapping.referenceMap), mapping);
14571
14603
  return withOoxmlSourceType(applyReconstructionDiffs(
14572
14604
  xmlDoc,
14573
14605
  diffs,
@@ -14579,6 +14611,109 @@ function applyReconstructionMode(xmlDoc, originalText, modifiedText, serializer,
14579
14611
  options
14580
14612
  ));
14581
14613
  }
14614
+ function forEachChangeGroup(diffs, onEqual, onGroup) {
14615
+ let index = 0;
14616
+ while (index < diffs.length) {
14617
+ if (diffs[index][0] === 0) {
14618
+ onEqual(diffs[index]);
14619
+ index++;
14620
+ continue;
14621
+ }
14622
+ const group = [];
14623
+ while (index < diffs.length && diffs[index][0] !== 0) group.push(diffs[index++]);
14624
+ onGroup(
14625
+ group,
14626
+ group.filter(([op]) => op === -1).map(([, text2]) => text2).join(""),
14627
+ group.filter(([op]) => op === 1).map(([, text2]) => text2).join("")
14628
+ );
14629
+ }
14630
+ }
14631
+ function pushChange(result, deleted, inserted) {
14632
+ if (deleted === inserted) {
14633
+ if (deleted) result.push([0, deleted]);
14634
+ return;
14635
+ }
14636
+ if (deleted) result.push([-1, deleted]);
14637
+ if (inserted) result.push([1, inserted]);
14638
+ }
14639
+ function anchorSharedReferences(diffs, referenceMap) {
14640
+ if (referenceMap.size === 0) return diffs;
14641
+ const result = [];
14642
+ forEachChangeGroup(diffs, (part) => result.push(part), (group, deleted, inserted) => {
14643
+ const deletedRefs = [...deleted].filter((char) => referenceMap.has(char) && inserted.includes(char));
14644
+ const insertedRefs = [...inserted].filter((char) => deletedRefs.includes(char));
14645
+ if (deletedRefs.length === 0 || deletedRefs.join("") !== insertedRefs.join("")) {
14646
+ result.push(...group);
14647
+ return;
14648
+ }
14649
+ let deletedStart = 0;
14650
+ let insertedStart = 0;
14651
+ for (const ref of deletedRefs) {
14652
+ const deletedAt = deleted.indexOf(ref, deletedStart);
14653
+ const insertedAt = inserted.indexOf(ref, insertedStart);
14654
+ pushChange(result, deleted.slice(deletedStart, deletedAt), inserted.slice(insertedStart, insertedAt));
14655
+ result.push([0, ref]);
14656
+ deletedStart = deletedAt + 1;
14657
+ insertedStart = insertedAt + 1;
14658
+ }
14659
+ pushChange(result, deleted.slice(deletedStart), inserted.slice(insertedStart));
14660
+ });
14661
+ return result;
14662
+ }
14663
+ function commonPrefixLength(left, right) {
14664
+ const limit = Math.min(left.length, right.length);
14665
+ let length = 0;
14666
+ while (length < limit && left[length] === right[length]) length++;
14667
+ return length;
14668
+ }
14669
+ function commonSuffixLength(left, right, limit) {
14670
+ let length = 0;
14671
+ while (length < limit && left[left.length - 1 - length] === right[right.length - 1 - length]) length++;
14672
+ return length;
14673
+ }
14674
+ function keepUnchangedTextOutsideWrappers(diffs, mapping) {
14675
+ const wrapperAt = (index) => mapping.getRunProperties(index).wrapper || null;
14676
+ const result = [];
14677
+ let originalOffset = 0;
14678
+ forEachChangeGroup(diffs, (part) => {
14679
+ result.push(part);
14680
+ originalOffset += part[1].length;
14681
+ }, (group, deleted, inserted) => {
14682
+ const start = originalOffset;
14683
+ const end = start + deleted.length;
14684
+ originalOffset = end;
14685
+ const isWrapperBoundary = (index) => index > start && index < end && wrapperAt(index - 1) !== wrapperAt(index);
14686
+ let prefix = 0;
14687
+ let suffix = 0;
14688
+ if (deleted && inserted) {
14689
+ const sharedSuffix = commonSuffixLength(deleted, inserted, Math.min(deleted.length, inserted.length));
14690
+ for (let length = sharedSuffix; length > 0; length--) {
14691
+ if (isWrapperBoundary(end - length)) {
14692
+ suffix = length;
14693
+ break;
14694
+ }
14695
+ }
14696
+ const sharedPrefix = Math.min(
14697
+ commonPrefixLength(deleted, inserted),
14698
+ Math.min(deleted.length, inserted.length) - suffix
14699
+ );
14700
+ for (let length = sharedPrefix; length > 0; length--) {
14701
+ if (isWrapperBoundary(start + length)) {
14702
+ prefix = length;
14703
+ break;
14704
+ }
14705
+ }
14706
+ }
14707
+ if (prefix === 0 && suffix === 0) {
14708
+ result.push(...group);
14709
+ return;
14710
+ }
14711
+ if (prefix > 0) result.push([0, deleted.slice(0, prefix)]);
14712
+ pushChange(result, deleted.slice(prefix, deleted.length - suffix), inserted.slice(prefix, inserted.length - suffix));
14713
+ if (suffix > 0) result.push([0, deleted.slice(deleted.length - suffix)]);
14714
+ });
14715
+ return result;
14716
+ }
14582
14717
 
14583
14718
  // engine/table-mode.js
14584
14719
  function noChanges(serializer, xmlDoc) {
@@ -18127,6 +18262,260 @@ function ingestWordOoxmlToMarkdownResult(ooxml) {
18127
18262
  return { text: lines.join("\n\n").trim(), status: "ok", warnings: parsed.warnings };
18128
18263
  }
18129
18264
 
18265
+ // services/comment-builders.js
18266
+ var NS_W14 = "http://schemas.microsoft.com/office/word/2010/wordml";
18267
+ var NS_W15 = "http://schemas.microsoft.com/office/word/2012/wordml";
18268
+ function createCommentParaId(commentId) {
18269
+ const numeric = Number.parseInt(String(commentId), 10);
18270
+ const value = Number.isFinite(numeric) ? 1879048192 + (numeric >>> 0) >>> 0 : 1879048192;
18271
+ return value.toString(16).toUpperCase().padStart(8, "0").slice(-8);
18272
+ }
18273
+ function buildCommentElement(commentId, author, content, date, paraId = createCommentParaId(commentId)) {
18274
+ const initials = author.split(" ").map((word) => word[0]).join("").toUpperCase() || "AI";
18275
+ const escapedContent = escapeXml(content);
18276
+ const escapedAuthor = escapeXml(author);
18277
+ return `<w:comment w:id="${commentId}" w:author="${escapedAuthor}" w:date="${date}" w:initials="${initials}">
18278
+ <w:p w14:paraId="${escapeXml(paraId)}" xmlns:w14="${NS_W14}">
18279
+ <w:r><w:t>${escapedContent}</w:t></w:r>
18280
+ </w:p>
18281
+ </w:comment>`;
18282
+ }
18283
+ function buildCommentsPartXml(comments) {
18284
+ if (!comments || comments.length === 0) {
18285
+ return `<w:comments xmlns:w="${NS_W}"></w:comments>`;
18286
+ }
18287
+ const commentElements = comments.map(
18288
+ (comment) => buildCommentElement(comment.id, comment.author, comment.content, comment.date, comment.paraId)
18289
+ ).join("\n ");
18290
+ return `<w:comments xmlns:w="${NS_W}">
18291
+ ${commentElements}
18292
+ </w:comments>`;
18293
+ }
18294
+ function buildCommentsExtendedPartXml(entries) {
18295
+ const body = (entries || []).map((entry) => {
18296
+ const parent = entry.paraIdParent ? ` w15:paraIdParent="${escapeXml(entry.paraIdParent)}"` : "";
18297
+ return `<w15:commentEx w15:paraId="${escapeXml(entry.paraId)}"${parent} w15:done="${entry.done ? "1" : "0"}"/>`;
18298
+ }).join("");
18299
+ return `<w15:commentsEx xmlns:w15="${NS_W15}">${body}</w15:commentsEx>`;
18300
+ }
18301
+
18302
+ // services/comment-thread-parts.js
18303
+ var attrOf = (node, qualified, local) => node?.getAttribute?.(qualified) || node?.getAttribute?.(local) || "";
18304
+ function commentThreadParagraph(commentElement) {
18305
+ const paragraphs = Array.from(commentElement?.getElementsByTagNameNS?.("*", "p") || []);
18306
+ return paragraphs.length ? paragraphs[paragraphs.length - 1] : null;
18307
+ }
18308
+ function commentParaId(commentElement) {
18309
+ return attrOf(commentThreadParagraph(commentElement), "w14:paraId", "paraId").toUpperCase();
18310
+ }
18311
+ function usedParaIds(commentsDoc, extendedDoc) {
18312
+ const ids = /* @__PURE__ */ new Set();
18313
+ for (const p of Array.from(commentsDoc?.getElementsByTagNameNS("*", "p") || [])) {
18314
+ const id = attrOf(p, "w14:paraId", "paraId");
18315
+ if (id) ids.add(id.toUpperCase());
18316
+ }
18317
+ for (const ex of Array.from(extendedDoc?.getElementsByTagNameNS("*", "commentEx") || [])) {
18318
+ const id = attrOf(ex, "w15:paraId", "paraId");
18319
+ if (id) ids.add(id.toUpperCase());
18320
+ }
18321
+ return ids;
18322
+ }
18323
+ function allocateParaId(commentId, occupied) {
18324
+ let candidate = createCommentParaId(commentId);
18325
+ let value = Number.parseInt(candidate, 16) >>> 0;
18326
+ while (occupied.has(candidate)) {
18327
+ value = value + 1 >>> 0;
18328
+ candidate = value.toString(16).toUpperCase().padStart(8, "0");
18329
+ }
18330
+ occupied.add(candidate);
18331
+ return candidate;
18332
+ }
18333
+ var parseDoc = (xml, partName) => {
18334
+ const parsed = parseOoxmlSafe(xml, "application/xml");
18335
+ if (!parsed.doc || parsed.error) return { error: { code: "PARSE_ERROR", message: `Could not parse ${partName}: ${parsed.error?.message || "invalid XML"}` } };
18336
+ return { doc: parsed.doc };
18337
+ };
18338
+ function applyThreadResolutionToParts({ commentsXml, commentsExtendedXml = null, commentId, resolved = true }) {
18339
+ if (!commentsXml) return { status: "error", error: { code: "COMMENTS_PART_MISSING", message: "Resolving a comment requires an existing word/comments.xml part." } };
18340
+ const commentsParsed = parseDoc(commentsXml, "word/comments.xml");
18341
+ if (commentsParsed.error) return { status: "error", error: commentsParsed.error };
18342
+ const commentsDoc = commentsParsed.doc;
18343
+ const idOf = (node) => attrOf(node, "w:id", "id");
18344
+ const comments = Array.from(commentsDoc.getElementsByTagNameNS("*", "comment"));
18345
+ const target = comments.find((node) => idOf(node) === String(commentId));
18346
+ if (!target) return { status: "error", error: { code: "COMMENT_NOT_FOUND", message: `Comment '${commentId}' was not found.` } };
18347
+ let extendedDoc;
18348
+ if (commentsExtendedXml) {
18349
+ const parsed = parseDoc(commentsExtendedXml, "word/commentsExtended.xml");
18350
+ if (parsed.error) return { status: "error", error: parsed.error };
18351
+ extendedDoc = parsed.doc;
18352
+ } else {
18353
+ extendedDoc = parseDoc(`<w15:commentsEx xmlns:w15="${NS_W15}"/>`, "word/commentsExtended.xml").doc;
18354
+ }
18355
+ const root = extendedDoc.documentElement;
18356
+ const entryFor = (paraId) => Array.from(root.getElementsByTagNameNS("*", "commentEx")).find((node) => attrOf(node, "w15:paraId", "paraId").toUpperCase() === paraId) || null;
18357
+ const occupied = usedParaIds(commentsDoc, extendedDoc);
18358
+ let commentsChanged = false;
18359
+ const keyOf = (node) => {
18360
+ let paraId = commentParaId(node);
18361
+ if (!paraId) {
18362
+ paraId = allocateParaId(idOf(node), occupied);
18363
+ commentThreadParagraph(node)?.setAttributeNS(NS_W14, "w14:paraId", paraId);
18364
+ commentsChanged = true;
18365
+ }
18366
+ return paraId;
18367
+ };
18368
+ const targetKey = keyOf(target);
18369
+ const targetEntry = entryFor(targetKey);
18370
+ const rootKey = targetEntry && attrOf(targetEntry, "w15:paraIdParent", "paraIdParent").toUpperCase() || targetKey;
18371
+ const threadKeys = /* @__PURE__ */ new Set([rootKey, targetKey]);
18372
+ for (const entry of Array.from(root.getElementsByTagNameNS("*", "commentEx"))) {
18373
+ if (attrOf(entry, "w15:paraIdParent", "paraIdParent").toUpperCase() === rootKey) threadKeys.add(attrOf(entry, "w15:paraId", "paraId").toUpperCase());
18374
+ }
18375
+ const doneValue = resolved ? "1" : "0";
18376
+ let extendedChanged = !commentsExtendedXml;
18377
+ for (const key of threadKeys) {
18378
+ let entry = entryFor(key);
18379
+ if (!entry) {
18380
+ entry = extendedDoc.createElementNS(NS_W15, "w15:commentEx");
18381
+ entry.setAttributeNS(NS_W15, "w15:paraId", key);
18382
+ if (key !== rootKey) entry.setAttributeNS(NS_W15, "w15:paraIdParent", rootKey);
18383
+ entry.setAttributeNS(NS_W15, "w15:done", doneValue);
18384
+ root.appendChild(entry);
18385
+ extendedChanged = true;
18386
+ } else if (attrOf(entry, "w15:done", "done") !== doneValue) {
18387
+ entry.setAttributeNS(NS_W15, "w15:done", doneValue);
18388
+ extendedChanged = true;
18389
+ }
18390
+ }
18391
+ const byKey = new Map(comments.map((node) => [commentParaId(node), node]));
18392
+ const commentIds = [...threadKeys].map((key) => byKey.get(key)).filter(Boolean).map(idOf);
18393
+ const rootComment = byKey.get(rootKey);
18394
+ const serializer = createSerializer();
18395
+ const hasChanges = extendedChanged || commentsChanged;
18396
+ return {
18397
+ status: "ok",
18398
+ hasChanges,
18399
+ resolved,
18400
+ threadRootId: rootComment ? idOf(rootComment) : String(commentId),
18401
+ commentIds,
18402
+ ...hasChanges ? { commentsExtendedXml: serializer.serializeToString(extendedDoc) } : {},
18403
+ ...commentsChanged ? { commentsXml: serializer.serializeToString(commentsDoc) } : {}
18404
+ };
18405
+ }
18406
+ var NS_W_MAIN = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
18407
+ function anchorReplyInDocument(xmlDoc, memberIds, newId) {
18408
+ const wanted = new Set(memberIds.map(String));
18409
+ const idOf = (node) => attrOf(node, "w:id", "id");
18410
+ const all = (name) => Array.from(xmlDoc.getElementsByTagNameNS("*", name)).filter((node) => wanted.has(idOf(node)));
18411
+ const starts = all("commentRangeStart");
18412
+ const references = all("commentReference");
18413
+ if (!starts.length || !references.length) return false;
18414
+ const lastStart = starts[starts.length - 1];
18415
+ const lastReference = references[references.length - 1];
18416
+ const referenceRun = lastReference.parentNode?.localName === "r" ? lastReference.parentNode : lastReference;
18417
+ const make = (name, parent) => {
18418
+ const element = xmlDoc.createElementNS(NS_W_MAIN, `w:${name}`);
18419
+ element.setAttributeNS(NS_W_MAIN, "w:id", String(newId));
18420
+ if (parent) parent.appendChild(element);
18421
+ return element;
18422
+ };
18423
+ const insertAfter = (anchor, node) => anchor.parentNode.insertBefore(node, anchor.nextSibling);
18424
+ insertAfter(lastStart, make("commentRangeStart"));
18425
+ const end = make("commentRangeEnd");
18426
+ const run = xmlDoc.createElementNS(NS_W_MAIN, "w:r");
18427
+ run.appendChild(xmlDoc.createElementNS(NS_W_MAIN, "w:rPr"));
18428
+ make("commentReference", run);
18429
+ insertAfter(referenceRun, end);
18430
+ insertAfter(end, run);
18431
+ return true;
18432
+ }
18433
+ var NS_W16CID = "http://schemas.microsoft.com/office/word/2016/wordml/cid";
18434
+ var NS_W16CEX = "http://schemas.microsoft.com/office/word/2018/wordml/cex";
18435
+ var durableIdFor = (paraId, taken) => {
18436
+ let value = (Number.parseInt(paraId, 16) ^ 625341585) & 2147483646;
18437
+ let candidate = value.toString(16).toUpperCase().padStart(8, "0");
18438
+ while (taken.has(candidate)) {
18439
+ value = value + 2 & 2147483646;
18440
+ candidate = value.toString(16).toUpperCase().padStart(8, "0");
18441
+ }
18442
+ taken.add(candidate);
18443
+ return candidate;
18444
+ };
18445
+ function reconcileCommentSiblingParts({ commentsXml, commentsIdsXml = null, commentsExtensibleXml = null }) {
18446
+ const out = {};
18447
+ if (!commentsXml || !commentsIdsXml && !commentsExtensibleXml) return out;
18448
+ const commentsParsed = parseOoxmlSafe(commentsXml, "application/xml");
18449
+ if (!commentsParsed.doc || commentsParsed.error) return out;
18450
+ const serializer = createSerializer();
18451
+ const byKey = /* @__PURE__ */ new Map();
18452
+ for (const node of Array.from(commentsParsed.doc.getElementsByTagNameNS("*", "comment"))) {
18453
+ const key = commentParaId(node);
18454
+ if (key) byKey.set(key, node);
18455
+ }
18456
+ let idsDoc = null;
18457
+ const durableByKey = /* @__PURE__ */ new Map();
18458
+ const taken = /* @__PURE__ */ new Set();
18459
+ let idsChanged = false;
18460
+ if (commentsIdsXml) {
18461
+ const parsed = parseOoxmlSafe(commentsIdsXml, "application/xml");
18462
+ if (!parsed.doc || parsed.error) return out;
18463
+ idsDoc = parsed.doc;
18464
+ const root = idsDoc.documentElement;
18465
+ for (const entry of Array.from(root.getElementsByTagNameNS("*", "commentId"))) {
18466
+ const key = attrOf(entry, "w16cid:paraId", "paraId").toUpperCase();
18467
+ const durable = attrOf(entry, "w16cid:durableId", "durableId").toUpperCase();
18468
+ if (durable) taken.add(durable);
18469
+ if (!byKey.has(key)) {
18470
+ root.removeChild(entry);
18471
+ idsChanged = true;
18472
+ continue;
18473
+ }
18474
+ durableByKey.set(key, durable);
18475
+ }
18476
+ for (const key of byKey.keys()) {
18477
+ if (durableByKey.has(key)) continue;
18478
+ const durable = durableIdFor(key, taken);
18479
+ const entry = idsDoc.createElementNS(NS_W16CID, "w16cid:commentId");
18480
+ entry.setAttributeNS(NS_W16CID, "w16cid:paraId", key);
18481
+ entry.setAttributeNS(NS_W16CID, "w16cid:durableId", durable);
18482
+ root.appendChild(entry);
18483
+ durableByKey.set(key, durable);
18484
+ idsChanged = true;
18485
+ }
18486
+ }
18487
+ let extDoc = null;
18488
+ let extChanged = false;
18489
+ if (commentsExtensibleXml && idsDoc) {
18490
+ const parsed = parseOoxmlSafe(commentsExtensibleXml, "application/xml");
18491
+ if (parsed.doc && !parsed.error) {
18492
+ extDoc = parsed.doc;
18493
+ const root = extDoc.documentElement;
18494
+ const live = new Set(durableByKey.values());
18495
+ const present = /* @__PURE__ */ new Set();
18496
+ for (const entry of Array.from(root.getElementsByTagNameNS("*", "commentExtensible"))) {
18497
+ const durable = attrOf(entry, "w16cex:durableId", "durableId").toUpperCase();
18498
+ if (!live.has(durable)) {
18499
+ root.removeChild(entry);
18500
+ extChanged = true;
18501
+ } else present.add(durable);
18502
+ }
18503
+ for (const [key, durable] of durableByKey) {
18504
+ if (present.has(durable)) continue;
18505
+ const date = attrOf(byKey.get(key), "w:date", "date");
18506
+ const entry = extDoc.createElementNS(NS_W16CEX, "w16cex:commentExtensible");
18507
+ entry.setAttributeNS(NS_W16CEX, "w16cex:durableId", durable);
18508
+ if (date) entry.setAttributeNS(NS_W16CEX, "w16cex:dateUtc", date);
18509
+ root.appendChild(entry);
18510
+ extChanged = true;
18511
+ }
18512
+ }
18513
+ }
18514
+ if (idsChanged) out.commentsIdsXml = serializer.serializeToString(idsDoc);
18515
+ if (extChanged) out.commentsExtensibleXml = serializer.serializeToString(extDoc);
18516
+ return out;
18517
+ }
18518
+
18130
18519
  // core/sha256.js
18131
18520
  var K = new Uint32Array([
18132
18521
  1116352408,
@@ -18619,7 +19008,7 @@ function readCommentDefinitions(commentsDoc) {
18619
19008
  author: attr2(comment, "author") || null,
18620
19009
  date: attr2(comment, "date") || null,
18621
19010
  text: paragraphs.map((p) => extractCanonicalParagraphText(p)).join("\n"),
18622
- paraId: paragraphs[0]?.getAttribute?.("w14:paraId") || paragraphs[0]?.getAttribute?.("paraId") || null
19011
+ paraId: commentParaId(comment) || null
18623
19012
  });
18624
19013
  }
18625
19014
  return result;
@@ -18850,43 +19239,6 @@ function inspectDocumentParts(parts, options = {}) {
18850
19239
  };
18851
19240
  }
18852
19241
 
18853
- // services/comment-builders.js
18854
- var NS_W14 = "http://schemas.microsoft.com/office/word/2010/wordml";
18855
- var NS_W15 = "http://schemas.microsoft.com/office/word/2012/wordml";
18856
- function createCommentParaId(commentId) {
18857
- const numeric = Number.parseInt(String(commentId), 10);
18858
- const value = Number.isFinite(numeric) ? 1879048192 + (numeric >>> 0) >>> 0 : 1879048192;
18859
- return value.toString(16).toUpperCase().padStart(8, "0").slice(-8);
18860
- }
18861
- function buildCommentElement(commentId, author, content, date, paraId = createCommentParaId(commentId)) {
18862
- const initials = author.split(" ").map((word) => word[0]).join("").toUpperCase() || "AI";
18863
- const escapedContent = escapeXml(content);
18864
- const escapedAuthor = escapeXml(author);
18865
- return `<w:comment w:id="${commentId}" w:author="${escapedAuthor}" w:date="${date}" w:initials="${initials}">
18866
- <w:p w14:paraId="${escapeXml(paraId)}" xmlns:w14="${NS_W14}">
18867
- <w:r><w:t>${escapedContent}</w:t></w:r>
18868
- </w:p>
18869
- </w:comment>`;
18870
- }
18871
- function buildCommentsPartXml(comments) {
18872
- if (!comments || comments.length === 0) {
18873
- return `<w:comments xmlns:w="${NS_W}"></w:comments>`;
18874
- }
18875
- const commentElements = comments.map(
18876
- (comment) => buildCommentElement(comment.id, comment.author, comment.content, comment.date, comment.paraId)
18877
- ).join("\n ");
18878
- return `<w:comments xmlns:w="${NS_W}">
18879
- ${commentElements}
18880
- </w:comments>`;
18881
- }
18882
- function buildCommentsExtendedPartXml(entries) {
18883
- const body = (entries || []).map((entry) => {
18884
- const parent = entry.paraIdParent ? ` w15:paraIdParent="${escapeXml(entry.paraIdParent)}"` : "";
18885
- return `<w15:commentEx w15:paraId="${escapeXml(entry.paraId)}"${parent} w15:done="${entry.done ? "1" : "0"}"/>`;
18886
- }).join("");
18887
- return `<w15:commentsEx xmlns:w15="${NS_W15}">${body}</w15:commentsEx>`;
18888
- }
18889
-
18890
19242
  // services/comment-locator.js
18891
19243
  function createParagraphTextIndex(paragraph, options = {}) {
18892
19244
  const revisionView = options.revisionView === "current" ? "accepted" : options.revisionView || "accepted";
@@ -19347,29 +19699,7 @@ function parseRequired(xml, partName) {
19347
19699
  }
19348
19700
  return { doc: parsed.doc };
19349
19701
  }
19350
- function usedParaIds(commentsDoc, extendedDoc) {
19351
- const ids = /* @__PURE__ */ new Set();
19352
- for (const p of Array.from(commentsDoc?.getElementsByTagNameNS("*", "p") || [])) {
19353
- const id = attr3(p, "w14:paraId", "paraId");
19354
- if (id) ids.add(id.toUpperCase());
19355
- }
19356
- for (const ex of Array.from(extendedDoc?.getElementsByTagNameNS("*", "commentEx") || [])) {
19357
- const id = attr3(ex, "w15:paraId", "paraId");
19358
- if (id) ids.add(id.toUpperCase());
19359
- }
19360
- return ids;
19361
- }
19362
- function allocateParaId(commentId, occupied) {
19363
- let candidate = createCommentParaId(commentId);
19364
- let value = Number.parseInt(candidate, 16) >>> 0;
19365
- while (occupied.has(candidate)) {
19366
- value = value + 1 >>> 0;
19367
- candidate = value.toString(16).toUpperCase().padStart(8, "0");
19368
- }
19369
- occupied.add(candidate);
19370
- return candidate;
19371
- }
19372
- function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, parentCommentId, commentId, commentContent, author, date = (/* @__PURE__ */ new Date()).toISOString() }) {
19702
+ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, documentXml = null, parentCommentId, commentId, commentContent, author, date = (/* @__PURE__ */ new Date()).toISOString() }) {
19373
19703
  if (!commentsXml) return { status: "error", error: { code: "COMMENTS_PART_MISSING", message: "A comment reply requires an existing word/comments.xml part." } };
19374
19704
  const commentsParsed = parseRequired(commentsXml, "word/comments.xml");
19375
19705
  if (commentsParsed.error) return { status: "error", error: commentsParsed.error };
@@ -19383,15 +19713,31 @@ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, par
19383
19713
  extendedDoc = parsed.doc;
19384
19714
  }
19385
19715
  const occupied = usedParaIds(commentsDoc, extendedDoc);
19386
- const parentParagraph = Array.from(parent.getElementsByTagNameNS(NS_W6, "p"))[0] || Array.from(parent.getElementsByTagNameNS("*", "p"))[0];
19716
+ if (documentXml) for (const match of String(documentXml).matchAll(/w14:paraId="([0-9A-Fa-f]{8})"/g)) occupied.add(match[1].toUpperCase());
19717
+ const parentParagraph = commentThreadParagraph(parent);
19387
19718
  if (!parentParagraph) return { status: "error", error: { code: "PARENT_COMMENT_INVALID", message: `Parent comment '${parentCommentId}' has no paragraph.` } };
19388
- let parentParaId = attr3(parentParagraph, "w14:paraId", "paraId");
19719
+ let parentParaId = commentParaId(parent);
19389
19720
  if (!parentParaId) {
19390
19721
  parentParaId = allocateParaId(parentCommentId, occupied);
19391
19722
  parentParagraph.setAttributeNS(NS_W14, "w14:paraId", parentParaId);
19392
19723
  } else {
19393
19724
  parentParaId = parentParaId.toUpperCase();
19394
19725
  }
19726
+ for (const node of Array.from(extendedDoc?.getElementsByTagNameNS("*", "commentEx") || [])) {
19727
+ if (attr3(node, "w15:paraId", "paraId").toUpperCase() === parentParaId) {
19728
+ const root2 = attr3(node, "w15:paraIdParent", "paraIdParent").toUpperCase();
19729
+ if (root2) parentParaId = root2;
19730
+ break;
19731
+ }
19732
+ }
19733
+ const threadCommentIds = [];
19734
+ const commentNodes = Array.from(commentsDoc.getElementsByTagNameNS("*", "comment"));
19735
+ const parentOf = new Map(Array.from(extendedDoc?.getElementsByTagNameNS("*", "commentEx") || []).map((node) => [attr3(node, "w15:paraId", "paraId").toUpperCase(), attr3(node, "w15:paraIdParent", "paraIdParent").toUpperCase()]));
19736
+ for (const node of commentNodes) {
19737
+ const key = commentParaId(node);
19738
+ if (key === parentParaId || parentOf.get(key) === parentParaId) threadCommentIds.push(attr3(node, "w:id", "id"));
19739
+ }
19740
+ const threadRootCommentId = attr3(commentNodes.find((node) => commentParaId(node) === parentParaId), "w:id", "id") || String(parentCommentId);
19395
19741
  const replyParaId = allocateParaId(commentId, occupied);
19396
19742
  const replyParsed = parseRequired(`<w:comments xmlns:w="${NS_W6}" xmlns:w14="${NS_W14}">${buildCommentElement(commentId, author, commentContent, date, replyParaId)}</w:comments>`, "reply comment");
19397
19743
  commentsDoc.documentElement.appendChild(commentsDoc.importNode(replyParsed.doc.documentElement.firstChild, true));
@@ -19400,16 +19746,18 @@ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, par
19400
19746
  }
19401
19747
  const root = extendedDoc.documentElement;
19402
19748
  const entries = Array.from(root.getElementsByTagNameNS("*", "commentEx"));
19403
- if (!entries.some((node) => attr3(node, "w15:paraId", "paraId").toUpperCase() === parentParaId)) {
19749
+ const rootEntry = entries.find((node) => attr3(node, "w15:paraId", "paraId").toUpperCase() === parentParaId);
19750
+ if (!rootEntry) {
19404
19751
  const parentEx = extendedDoc.createElementNS(NS_W15, "w15:commentEx");
19405
19752
  parentEx.setAttributeNS(NS_W15, "w15:paraId", parentParaId);
19406
19753
  parentEx.setAttributeNS(NS_W15, "w15:done", "0");
19407
19754
  root.appendChild(parentEx);
19408
19755
  }
19756
+ const threadDone = attr3(rootEntry, "w15:done", "done") === "1" ? "1" : "0";
19409
19757
  const replyEx = extendedDoc.createElementNS(NS_W15, "w15:commentEx");
19410
19758
  replyEx.setAttributeNS(NS_W15, "w15:paraId", replyParaId);
19411
19759
  replyEx.setAttributeNS(NS_W15, "w15:paraIdParent", parentParaId);
19412
- replyEx.setAttributeNS(NS_W15, "w15:done", "0");
19760
+ replyEx.setAttributeNS(NS_W15, "w15:done", threadDone);
19413
19761
  root.appendChild(replyEx);
19414
19762
  const serializer = createSerializer();
19415
19763
  return {
@@ -19422,7 +19770,9 @@ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, par
19422
19770
  commentId,
19423
19771
  parentCommentId: String(parentCommentId),
19424
19772
  paraId: replyParaId,
19425
- parentParaId
19773
+ parentParaId,
19774
+ threadRootCommentId,
19775
+ threadCommentIds: threadCommentIds.length ? threadCommentIds : [threadRootCommentId]
19426
19776
  };
19427
19777
  }
19428
19778
 
@@ -19487,19 +19837,19 @@ function applyFormattingRemovalToOoxml(ooxmlString, targetText, formatTypes) {
19487
19837
  if (!targetText || !ooxmlString) return ooxmlString;
19488
19838
  const doc = parseOoxml(ooxmlString);
19489
19839
  if (!doc) return ooxmlString;
19490
- const NS_W10 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19491
- const runs = doc.getElementsByTagNameNS(NS_W10, "r");
19492
- const insertions = doc.getElementsByTagNameNS(NS_W10, "ins");
19840
+ const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19841
+ const runs = doc.getElementsByTagNameNS(NS_W11, "r");
19842
+ const insertions = doc.getElementsByTagNameNS(NS_W11, "ins");
19493
19843
  const allRuns = [...Array.from(runs)];
19494
19844
  for (const ins of insertions) {
19495
- const insideRuns = ins.getElementsByTagNameNS(NS_W10, "r");
19845
+ const insideRuns = ins.getElementsByTagNameNS(NS_W11, "r");
19496
19846
  allRuns.push(...Array.from(insideRuns));
19497
19847
  }
19498
19848
  for (const run of allRuns) {
19499
- const textNodes = run.getElementsByTagNameNS(NS_W10, "t");
19849
+ const textNodes = run.getElementsByTagNameNS(NS_W11, "t");
19500
19850
  const runText = Array.from(textNodes).map((t) => t.textContent).join("");
19501
19851
  if (runText.includes(targetText) || runText === targetText) {
19502
- const rPrElements = run.getElementsByTagNameNS(NS_W10, "rPr");
19852
+ const rPrElements = run.getElementsByTagNameNS(NS_W11, "rPr");
19503
19853
  if (rPrElements.length > 0) {
19504
19854
  const rPr = rPrElements[0];
19505
19855
  const newRPr = removeFormattingFromRPr(rPr, formatTypes);
@@ -19534,7 +19884,7 @@ var HIGHLIGHT_COLOR_MAP = {
19534
19884
  "white": "white"
19535
19885
  };
19536
19886
  function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
19537
- const NS_W10 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19887
+ const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19538
19888
  const ooxmlColor = HIGHLIGHT_COLOR_MAP[color.toLowerCase()] || "yellow";
19539
19889
  const generateRedlines = options?.generateRedlines ?? false;
19540
19890
  const author = options?.author || getDefaultAuthor();
@@ -19553,10 +19903,10 @@ function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
19553
19903
  }
19554
19904
  });
19555
19905
  }
19556
- const existingHighlight = rPrElement.getElementsByTagNameNS(NS_W10, "highlight");
19906
+ const existingHighlight = rPrElement.getElementsByTagNameNS(NS_W11, "highlight");
19557
19907
  Array.from(existingHighlight).forEach(removeNode2);
19558
19908
  const highlightEl = createWordElement(doc, "w:highlight");
19559
- highlightEl.setAttributeNS(NS_W10, "w:val", ooxmlColor);
19909
+ highlightEl.setAttributeNS(NS_W11, "w:val", ooxmlColor);
19560
19910
  rPrElement.appendChild(highlightEl);
19561
19911
  if (generateRedlines && previousRPrState) {
19562
19912
  const rPrChange = createWordElement(doc, "w:rPrChange");
@@ -19565,7 +19915,7 @@ function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
19565
19915
  rPrChange.setAttribute("w:author", metadata.author);
19566
19916
  rPrChange.setAttribute("w:date", metadata.date);
19567
19917
  rPrChange.appendChild(previousRPrState);
19568
- const existingChange = rPrElement.getElementsByTagNameNS(NS_W10, "rPrChange");
19918
+ const existingChange = rPrElement.getElementsByTagNameNS(NS_W11, "rPrChange");
19569
19919
  Array.from(existingChange).forEach(removeNode2);
19570
19920
  rPrElement.appendChild(rPrChange);
19571
19921
  }
@@ -19582,12 +19932,12 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
19582
19932
  } else {
19583
19933
  revisionIdAllocator = createRevisionIdAllocator(doc);
19584
19934
  }
19585
- const NS_W10 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19935
+ const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19586
19936
  const getRunText = (run) => {
19587
- const textNodes = run.getElementsByTagNameNS(NS_W10, "t");
19937
+ const textNodes = run.getElementsByTagNameNS(NS_W11, "t");
19588
19938
  return Array.from(textNodes).map((t) => t.textContent).join("");
19589
19939
  };
19590
- const allRuns = Array.from(doc.getElementsByTagNameNS(NS_W10, "r"));
19940
+ const allRuns = Array.from(doc.getElementsByTagNameNS(NS_W11, "r"));
19591
19941
  const sourceRunsWithClaimedRevisionIds = /* @__PURE__ */ new WeakSet();
19592
19942
  const cloneRunWithText2 = (sourceRun, text2, shouldHighlight) => {
19593
19943
  const clonedRun = sourceRun.cloneNode(true);
@@ -19596,14 +19946,14 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
19596
19946
  } else {
19597
19947
  sourceRunsWithClaimedRevisionIds.add(sourceRun);
19598
19948
  }
19599
- const textNodes = clonedRun.getElementsByTagNameNS(NS_W10, "t");
19949
+ const textNodes = clonedRun.getElementsByTagNameNS(NS_W11, "t");
19600
19950
  Array.from(textNodes).forEach(removeNode2);
19601
19951
  const newText = createWordElement(doc, "w:t");
19602
19952
  newText.setAttribute("xml:space", "preserve");
19603
19953
  newText.textContent = text2;
19604
19954
  clonedRun.appendChild(newText);
19605
19955
  if (shouldHighlight) {
19606
- const rPrElements = clonedRun.getElementsByTagNameNS(NS_W10, "rPr");
19956
+ const rPrElements = clonedRun.getElementsByTagNameNS(NS_W11, "rPr");
19607
19957
  const existingRPr = rPrElements.length > 0 ? rPrElements[0] : null;
19608
19958
  const newRPr = injectHighlightIntoRPr(doc, existingRPr, color, options);
19609
19959
  if (existingRPr) {
@@ -19652,20 +20002,68 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
19652
20002
  return serializeOoxml(doc);
19653
20003
  }
19654
20004
 
20005
+ // services/package-parts.js
20006
+ var WORD_ML = "application/vnd.openxmlformats-officedocument.wordprocessingml";
20007
+ var REL_OFFICE_DOC = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
20008
+ var PART_SPECS = Object.freeze([
20009
+ { id: "document", path: "word/document.xml", pathPattern: null, contentType: `${WORD_ML}.document.main+xml`, legacyContentTypes: [], relType: `${REL_OFFICE_DOC}/officeDocument`, relTarget: null, verified: true },
20010
+ { id: "numbering", path: "word/numbering.xml", pathPattern: null, contentType: `${WORD_ML}.numbering+xml`, legacyContentTypes: [], relType: `${REL_OFFICE_DOC}/numbering`, relTarget: "numbering.xml", verified: false },
20011
+ { id: "comments", path: "word/comments.xml", pathPattern: null, contentType: `${WORD_ML}.comments+xml`, legacyContentTypes: [], relType: `${REL_OFFICE_DOC}/comments`, relTarget: "comments.xml", verified: true },
20012
+ {
20013
+ id: "commentsExtended",
20014
+ path: "word/commentsExtended.xml",
20015
+ pathPattern: null,
20016
+ contentType: `${WORD_ML}.commentsExtended+xml`,
20017
+ // Written by this package through 0.8.0; Word does not recognize it and offers to repair the file.
20018
+ legacyContentTypes: ["application/vnd.ms-word.commentsExtended+xml"],
20019
+ relType: "http://schemas.microsoft.com/office/2011/relationships/commentsExtended",
20020
+ relTarget: "commentsExtended.xml",
20021
+ verified: true
20022
+ },
20023
+ { id: "commentsIds", path: "word/commentsIds.xml", pathPattern: null, contentType: `${WORD_ML}.commentsIds+xml`, legacyContentTypes: [], relType: "http://schemas.microsoft.com/office/2016/09/relationships/commentsIds", relTarget: "commentsIds.xml", verified: true },
20024
+ { id: "commentsExtensible", path: "word/commentsExtensible.xml", pathPattern: null, contentType: `${WORD_ML}.commentsExtensible+xml`, legacyContentTypes: [], relType: "http://schemas.microsoft.com/office/2018/08/relationships/commentsExtensible", relTarget: "commentsExtensible.xml", verified: true },
20025
+ // Not yet produced by any Word fixture (Word only writes people.xml for signed-in authors): unverified.
20026
+ { id: "people", path: "word/people.xml", pathPattern: null, contentType: `${WORD_ML}.people+xml`, legacyContentTypes: [], relType: "http://schemas.microsoft.com/office/2011/relationships/people", relTarget: "people.xml", verified: false },
20027
+ { id: "header", path: null, pathPattern: /^word\/header\d+\.xml$/i, contentType: `${WORD_ML}.header+xml`, legacyContentTypes: [], relType: `${REL_OFFICE_DOC}/header`, relTarget: null, verified: true },
20028
+ { id: "footer", path: null, pathPattern: /^word\/footer\d+\.xml$/i, contentType: `${WORD_ML}.footer+xml`, legacyContentTypes: [], relType: `${REL_OFFICE_DOC}/footer`, relTarget: null, verified: true }
20029
+ ].map((spec) => Object.freeze(spec)));
20030
+ var BY_ID = new Map(PART_SPECS.map((spec) => [spec.id, spec]));
20031
+ function getPartSpec(id) {
20032
+ const spec = BY_ID.get(id);
20033
+ if (!spec) throw new Error(`Unknown package part: ${id}`);
20034
+ return spec;
20035
+ }
20036
+ function toEntryName(partName) {
20037
+ return String(partName || "").replace(/^\/+/, "");
20038
+ }
20039
+ function findPartSpecByPartName(partName) {
20040
+ const name = toEntryName(partName);
20041
+ const lower = name.toLowerCase();
20042
+ return PART_SPECS.find((spec) => spec.path ? spec.path.toLowerCase() === lower : spec.pathPattern?.test(name)) || null;
20043
+ }
20044
+ function correctedContentTypeFor(partName, actualContentType) {
20045
+ const spec = findPartSpecByPartName(partName);
20046
+ if (!spec || !spec.legacyContentTypes.includes(actualContentType)) return null;
20047
+ return spec.contentType;
20048
+ }
20049
+
19655
20050
  // services/standalone-docx-plumbing.js
19656
20051
  var NS_W7 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19657
20052
  var NS_CT = "http://schemas.openxmlformats.org/package/2006/content-types";
19658
20053
  var NS_RELS = "http://schemas.openxmlformats.org/package/2006/relationships";
19659
- var NUMBERING_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering";
19660
- var NUMBERING_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml";
19661
- var COMMENTS_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
19662
- var COMMENTS_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml";
19663
- var COMMENTS_EXTENDED_REL_TYPE = "http://schemas.microsoft.com/office/2011/relationships/commentsExtended";
19664
- var COMMENTS_EXTENDED_CONTENT_TYPE = "application/vnd.ms-word.commentsExtended+xml";
19665
- var DOCUMENT_PATH = "word/document.xml";
19666
- var NUMBERING_PATH = "word/numbering.xml";
19667
- var COMMENTS_PATH = "word/comments.xml";
19668
- var COMMENTS_EXTENDED_PATH = "word/commentsExtended.xml";
20054
+ var NUMBERING = getPartSpec("numbering");
20055
+ var COMMENTS = getPartSpec("comments");
20056
+ var COMMENTS_EXTENDED = getPartSpec("commentsExtended");
20057
+ var NUMBERING_REL_TYPE = NUMBERING.relType;
20058
+ var NUMBERING_CONTENT_TYPE = NUMBERING.contentType;
20059
+ var COMMENTS_REL_TYPE = COMMENTS.relType;
20060
+ var COMMENTS_CONTENT_TYPE = COMMENTS.contentType;
20061
+ var COMMENTS_EXTENDED_REL_TYPE = COMMENTS_EXTENDED.relType;
20062
+ var COMMENTS_EXTENDED_CONTENT_TYPE = COMMENTS_EXTENDED.contentType;
20063
+ var DOCUMENT_PATH = getPartSpec("document").path;
20064
+ var NUMBERING_PATH = NUMBERING.path;
20065
+ var COMMENTS_PATH = COMMENTS.path;
20066
+ var COMMENTS_EXTENDED_PATH = COMMENTS_EXTENDED.path;
19669
20067
  var CONTENT_TYPES_PATH = "[Content_Types].xml";
19670
20068
  var DOCUMENT_RELS_PATH = "word/_rels/document.xml.rels";
19671
20069
  function parseXmlStrictStandalone(xmlText, label = "xml") {
@@ -19953,6 +20351,22 @@ async function ensureCommentsExtendedArtifactsInZip(zip, commentsExtendedXml, op
19953
20351
  }
19954
20352
  }
19955
20353
  }
20354
+ async function repairKnownContentTypes(zip) {
20355
+ const ctText = await readZipText(zip, CONTENT_TYPES_PATH);
20356
+ if (!ctText) return [];
20357
+ const ctDoc = parseXmlStrictStandalone(ctText, CONTENT_TYPES_PATH);
20358
+ const repairs = [];
20359
+ for (const override of Array.from(ctDoc.getElementsByTagNameNS("*", "Override"))) {
20360
+ const partName = override.getAttribute("PartName") || "";
20361
+ const actual = override.getAttribute("ContentType") || "";
20362
+ const expected = correctedContentTypeFor(partName, actual);
20363
+ if (!expected) continue;
20364
+ override.setAttribute("ContentType", expected);
20365
+ repairs.push({ partName, from: actual, to: expected });
20366
+ }
20367
+ if (repairs.length > 0) zip.file(CONTENT_TYPES_PATH, createSerializer().serializeToString(ctDoc));
20368
+ return repairs;
20369
+ }
19956
20370
  async function validateDocxPackage(zip) {
19957
20371
  const documentXml = await readZipText(zip, DOCUMENT_PATH);
19958
20372
  if (!documentXml) {
@@ -20034,9 +20448,8 @@ async function validateDocxPackage(zip) {
20034
20448
  const paraIdToCommentId = /* @__PURE__ */ new Map();
20035
20449
  for (const comment of Array.from(commentsDoc.getElementsByTagNameNS("*", "comment"))) {
20036
20450
  const id = comment.getAttribute("w:id") || comment.getAttribute("id");
20037
- const paragraph = Array.from(comment.getElementsByTagNameNS("*", "p"))[0];
20038
- const paraId = paragraph?.getAttribute("w14:paraId") || paragraph?.getAttribute("paraId");
20039
- if (paraId) paraIdToCommentId.set(paraId.toUpperCase(), id);
20451
+ const paraId = commentParaId(comment);
20452
+ if (paraId) paraIdToCommentId.set(paraId, id);
20040
20453
  }
20041
20454
  const knownParaIds = new Set(paraIdToCommentId.keys());
20042
20455
  const extendedParaIds = /* @__PURE__ */ new Set();
@@ -20097,10 +20510,39 @@ async function validateDocxPackage(zip) {
20097
20510
  throw new Error("Validation failed: comments rel missing");
20098
20511
  }
20099
20512
  }
20513
+ {
20514
+ const relationshipById = /* @__PURE__ */ new Map();
20515
+ for (const rel of Array.from(relsDoc.getElementsByTagNameNS("*", "Relationship"))) {
20516
+ relationshipById.set(rel.getAttribute("Id") || "", { type: rel.getAttribute("Type") || "", target: rel.getAttribute("Target") || "" });
20517
+ }
20518
+ const headerSpec = getPartSpec("header");
20519
+ const footerSpec = getPartSpec("footer");
20520
+ const overrideTypeByPart = new Map(Array.from(ctDoc.getElementsByTagNameNS("*", "Override")).map((override) => [(override.getAttribute("PartName") || "").toLowerCase(), override.getAttribute("ContentType") || ""]));
20521
+ for (const reference of [
20522
+ ...Array.from(documentDoc.getElementsByTagNameNS(NS_W7, "headerReference")),
20523
+ ...Array.from(documentDoc.getElementsByTagNameNS(NS_W7, "footerReference"))
20524
+ ]) {
20525
+ const relId = reference.getAttributeNS?.("http://schemas.openxmlformats.org/officeDocument/2006/relationships", "id") || reference.getAttribute("r:id") || "";
20526
+ const spec = reference.localName === "headerReference" ? headerSpec : footerSpec;
20527
+ const relationship = relationshipById.get(relId);
20528
+ if (!relationship) throw new Error(`Validation failed: ${reference.localName} ${relId || "(no r:id)"} has no document relationship`);
20529
+ if (relationship.type !== spec.relType) throw new Error(`Validation failed: ${reference.localName} ${relId} points at a relationship of the wrong type (${relationship.type})`);
20530
+ const partName = relationship.target.startsWith("/") ? relationship.target : `/word/${relationship.target}`;
20531
+ const partXml = await readZipText(zip, partName.slice(1));
20532
+ if (!partXml) throw new Error(`Validation failed: ${reference.localName} ${relId} targets a missing part: ${partName}`);
20533
+ parseXmlStrictStandalone(partXml, partName.slice(1));
20534
+ const contentType = overrideTypeByPart.get(partName.toLowerCase());
20535
+ if (contentType !== spec.contentType) throw new Error(`Validation failed: ${partName} content type is ${contentType ? `'${contentType}'` : "missing"}; Word expects '${spec.contentType}'`);
20536
+ }
20537
+ }
20100
20538
  if (commentsExtendedXml) {
20101
- const hasContentType = Array.from(ctDoc.getElementsByTagNameNS("*", "Override")).some(
20102
- (override) => (override.getAttribute("PartName") || "").toLowerCase() === "/word/commentsextended.xml" && (override.getAttribute("ContentType") || "") === COMMENTS_EXTENDED_CONTENT_TYPE
20539
+ const extendedOverride = Array.from(ctDoc.getElementsByTagNameNS("*", "Override")).find(
20540
+ (override) => (override.getAttribute("PartName") || "").toLowerCase() === "/word/commentsextended.xml"
20103
20541
  );
20542
+ if (extendedOverride && extendedOverride.getAttribute("ContentType") !== COMMENTS_EXTENDED_CONTENT_TYPE) {
20543
+ throw new Error(`Validation failed: commentsExtended CT override has wrong content type '${extendedOverride.getAttribute("ContentType")}' (Word expects '${COMMENTS_EXTENDED_CONTENT_TYPE}')`);
20544
+ }
20545
+ const hasContentType = !!extendedOverride;
20104
20546
  const hasRelationship = Array.from(relsDoc.getElementsByTagNameNS("*", "Relationship")).some(
20105
20547
  (rel) => (rel.getAttribute("Type") || "") === COMMENTS_EXTENDED_REL_TYPE
20106
20548
  );
@@ -20464,6 +20906,156 @@ function reconcileReceiptsAgainstOutput(parts, receipts) {
20464
20906
  return { valid: true };
20465
20907
  }
20466
20908
 
20909
+ // services/headers-footers.js
20910
+ var NS_W8 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
20911
+ var NS_R2 = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
20912
+ var REVISION_ELEMENTS = /* @__PURE__ */ new Set(["ins", "del", "moveFrom", "moveTo", "rPrChange", "pPrChange", "sectPrChange", "tblPrChange", "tblGridChange", "tcPrChange", "trPrChange", "numberingChange"]);
20913
+ var REVISION_CONTENT_ELEMENTS = /* @__PURE__ */ new Set(["ins", "del", "moveFrom", "moveTo"]);
20914
+ var attrOf2 = (node, qualified, local) => node?.getAttribute?.(qualified) || node?.getAttribute?.(local) || "";
20915
+ function resolveTarget(target) {
20916
+ const clean = String(target || "");
20917
+ if (clean.startsWith("/")) return clean.replace(/^\/+/, "");
20918
+ const parts = ["word"];
20919
+ for (const segment of clean.split("/")) {
20920
+ if (segment === "..") parts.pop();
20921
+ else if (segment && segment !== ".") parts.push(segment);
20922
+ }
20923
+ return parts.join("/");
20924
+ }
20925
+ function discoverHeaderFooterParts({ documentXml, relsXml = null, settingsXml = null }) {
20926
+ if (!documentXml || !relsXml) return [];
20927
+ const documentParsed = parseOoxmlSafe(documentXml, "application/xml");
20928
+ const relsParsed = parseOoxmlSafe(relsXml, "application/xml");
20929
+ if (!documentParsed.doc || documentParsed.error || !relsParsed.doc || relsParsed.error) return [];
20930
+ const targetByRelId = /* @__PURE__ */ new Map();
20931
+ for (const rel of Array.from(relsParsed.doc.getElementsByTagNameNS("*", "Relationship"))) {
20932
+ targetByRelId.set(rel.getAttribute("Id") || "", rel.getAttribute("Target") || "");
20933
+ }
20934
+ const evenAndOdd = !!settingsXml && /<w:evenAndOddHeaders\b(?![^>]*w:val="(?:0|false|off)")/.test(settingsXml);
20935
+ const byPath = /* @__PURE__ */ new Map();
20936
+ const inForce = /* @__PURE__ */ new Map();
20937
+ const sectPrs = Array.from(documentParsed.doc.getElementsByTagNameNS(NS_W8, "sectPr"));
20938
+ sectPrs.forEach((sectPr, sectionIndex) => {
20939
+ const titlePg = Array.from(sectPr.childNodes || []).some((child) => child.localName === "titlePg" && !/^(0|false|off)$/i.test(attrOf2(child, "w:val", "val")));
20940
+ for (const ref of Array.from(sectPr.childNodes || [])) {
20941
+ if (ref.nodeType !== 1 || ref.localName !== "headerReference" && ref.localName !== "footerReference") continue;
20942
+ const relId = ref.getAttributeNS?.(NS_R2, "id") || ref.getAttribute("r:id") || "";
20943
+ const target = targetByRelId.get(relId);
20944
+ if (!target) continue;
20945
+ const path = resolveTarget(target);
20946
+ const kind = ref.localName === "headerReference" ? "header" : "footer";
20947
+ const type = attrOf2(ref, "w:type", "type") || "default";
20948
+ const entry = byPath.get(path) || { path, kind, type, sections: [], applies: [], titlePgSections: 0 };
20949
+ if (!entry.sections.includes(sectionIndex)) entry.sections.push(sectionIndex);
20950
+ if (titlePg) entry.titlePgSections += 1;
20951
+ byPath.set(path, entry);
20952
+ inForce.set(`${kind}:${type}`, path);
20953
+ }
20954
+ for (const path of inForce.values()) {
20955
+ const entry = byPath.get(path);
20956
+ if (entry && !entry.applies.includes(sectionIndex)) entry.applies.push(sectionIndex);
20957
+ }
20958
+ });
20959
+ return Array.from(byPath.values()).map((entry) => ({
20960
+ path: entry.path,
20961
+ kind: entry.kind,
20962
+ type: entry.type,
20963
+ sections: entry.sections,
20964
+ appliesToSections: entry.applies,
20965
+ sharedBySections: entry.applies.length > 1,
20966
+ // first-page parts only show with w:titlePg; even parts only with w:evenAndOddHeaders in settings.xml.
20967
+ active: entry.type === "first" ? entry.titlePgSections > 0 : entry.type === "even" ? evenAndOdd : true
20968
+ }));
20969
+ }
20970
+ function resolvePartSelector(parts, selector) {
20971
+ const describe = (part) => ({ path: part.path, kind: part.kind, type: part.type, sections: part.sections, appliesToSections: part.appliesToSections });
20972
+ if (typeof selector === "string") {
20973
+ const wanted = selector.replace(/^\/+/, "").toLowerCase();
20974
+ const match = parts.find((part) => part.path.toLowerCase() === wanted || part.path.toLowerCase() === `word/${wanted}`);
20975
+ if (match) return { part: match };
20976
+ return { error: { code: "PART_NOT_FOUND", message: `No header/footer part '${selector}' is referenced by the document.`, candidates: parts.map(describe) } };
20977
+ }
20978
+ if (!selector || typeof selector !== "object" || selector.kind !== "header" && selector.kind !== "footer") {
20979
+ return { error: { code: "INVALID_OPERATION", message: 'part must be a part path or { kind: "header"|"footer", type?: "default"|"first"|"even", section?: number }.' } };
20980
+ }
20981
+ const type = selector.type || "default";
20982
+ if (!["default", "first", "even"].includes(type)) {
20983
+ return { error: { code: "INVALID_OPERATION", message: `part.type must be default, first or even (got '${type}').` } };
20984
+ }
20985
+ let matches = parts.filter((part) => part.kind === selector.kind && part.type === type);
20986
+ if (Number.isInteger(selector.section)) matches = matches.filter((part) => part.appliesToSections.includes(selector.section));
20987
+ if (matches.length === 1) return { part: matches[0] };
20988
+ if (matches.length === 0) {
20989
+ return { error: { code: "PART_NOT_FOUND", message: `The document has no ${type} ${selector.kind}${Number.isInteger(selector.section) ? ` for section ${selector.section}` : ""}.`, candidates: parts.map(describe) } };
20990
+ }
20991
+ return { error: { code: "PART_AMBIGUOUS", message: `Several ${type} ${selector.kind} parts exist; add "section" or use the part path.`, candidates: matches.map(describe) } };
20992
+ }
20993
+ function isHeaderFooterPath(path) {
20994
+ const spec = findPartSpecByPartName(path);
20995
+ return spec?.id === "header" || spec?.id === "footer";
20996
+ }
20997
+ function countRevisionsInsideFields(xml) {
20998
+ const parsed = parseOoxmlSafe(xml, "application/xml");
20999
+ if (!parsed.doc || parsed.error) return 0;
21000
+ let depth = 0;
21001
+ let count = 0;
21002
+ const visit = (node) => {
21003
+ for (const child of Array.from(node.childNodes || [])) {
21004
+ if (child.nodeType !== 1) continue;
21005
+ const name = child.localName;
21006
+ if (name === "fldChar") {
21007
+ const type = attrOf2(child, "w:fldCharType", "fldCharType");
21008
+ if (type === "begin") depth += 1;
21009
+ else if (type === "end") depth = Math.max(0, depth - 1);
21010
+ continue;
21011
+ }
21012
+ if (REVISION_CONTENT_ELEMENTS.has(name) && depth > 0) count += 1;
21013
+ if (name === "fldSimple") {
21014
+ depth += 1;
21015
+ visit(child);
21016
+ depth -= 1;
21017
+ continue;
21018
+ }
21019
+ visit(child);
21020
+ }
21021
+ };
21022
+ visit(parsed.doc.documentElement);
21023
+ return count;
21024
+ }
21025
+ function collectRevisionIds(xml) {
21026
+ const ids = /* @__PURE__ */ new Set();
21027
+ for (const match of String(xml || "").matchAll(/<w:(?:ins|del|moveFrom|moveTo|rPrChange|pPrChange|sectPrChange|tblPrChange|tblGridChange|tcPrChange|trPrChange|numberingChange)\b[^>]*?\bw:id="(\d+)"/g)) ids.add(match[1]);
21028
+ return ids;
21029
+ }
21030
+ function renumberCollidingRevisionIds(xml, taken) {
21031
+ const parsed = parseOoxmlSafe(xml, "application/xml");
21032
+ if (!parsed.doc || parsed.error) return { xml, renumbered: 0, idMap: /* @__PURE__ */ new Map() };
21033
+ const idMap = /* @__PURE__ */ new Map();
21034
+ let next = 1 + Math.max(0, ...Array.from(taken, (id) => Number(id)).filter(Number.isFinite));
21035
+ const used = new Set(taken);
21036
+ let renumbered = 0;
21037
+ const visit = (node) => {
21038
+ for (const child of Array.from(node.childNodes || [])) {
21039
+ if (child.nodeType !== 1) continue;
21040
+ if (REVISION_ELEMENTS.has(child.localName)) {
21041
+ const id = attrOf2(child, "w:id", "id");
21042
+ if (id && used.has(id)) {
21043
+ while (used.has(String(next))) next += 1;
21044
+ child.setAttributeNS(NS_W8, "w:id", String(next));
21045
+ idMap.set(id, String(next));
21046
+ used.add(String(next));
21047
+ renumbered += 1;
21048
+ } else if (id) {
21049
+ used.add(id);
21050
+ }
21051
+ }
21052
+ visit(child);
21053
+ }
21054
+ };
21055
+ visit(parsed.doc.documentElement);
21056
+ return renumbered > 0 ? { xml: createSerializer().serializeToString(parsed.doc), renumbered, idMap } : { xml, renumbered: 0, idMap };
21057
+ }
21058
+
20467
21059
  // services/error-recovery.js
20468
21060
  var ERROR_RECOVERY_VERSION = 1;
20469
21061
  var RULES = Object.freeze({
@@ -20501,6 +21093,12 @@ var RULES = Object.freeze({
20501
21093
  STRUCTURED_CONTENT_INVALID: ["request", "request_fixable", "change_request"],
20502
21094
  UNSUPPORTED_INSERTION_AFFINITY: ["request", "request_fixable", "change_request"],
20503
21095
  EXISTING_REVISIONS: ["target-safety", "policy_choice_required", "set_option"],
21096
+ PART_NOT_FOUND: ["target-resolution", "target_refresh_required", "reinspect"],
21097
+ PART_AMBIGUOUS: ["target-resolution", "candidate_selection_required", "choose_candidate"],
21098
+ COMMENT_IN_HEADER_FOOTER: ["request", "request_fixable", "change_request"],
21099
+ FIELD_EDIT_REFUSED: ["target-safety", "request_fixable", "change_request"],
21100
+ COMMENT_NOT_FOUND: ["target-resolution", "target_refresh_required", "reinspect"],
21101
+ PARENT_ANCHOR_NOT_FOUND: ["target-resolution", "target_refresh_required", "reinspect"],
20504
21102
  COMMENTED_CONTENT_MERGE: ["target-safety", "user_authorization_required", "resolve_comments"],
20505
21103
  COMMENTED_CONTENT_DELETE: ["target-safety", "user_authorization_required", "resolve_comments"],
20506
21104
  OVERLAPPING_SOURCE_TARGETS: ["batch-compilation", "source_conflict", "consolidate_operations"],
@@ -20953,6 +21551,7 @@ var SUPPORTED_OPERATION_TYPES = /* @__PURE__ */ new Set([
20953
21551
  "delete",
20954
21552
  "comment",
20955
21553
  "comment_reply",
21554
+ "comment_resolve",
20956
21555
  "highlight"
20957
21556
  ]);
20958
21557
  var EXISTING_REVISIONS_POLICIES = Object.freeze([
@@ -20971,11 +21570,14 @@ function isRecord(value) {
20971
21570
  function nonEmptyString(value) {
20972
21571
  return typeof value === "string" && value.trim().length > 0;
20973
21572
  }
21573
+ function isCommentThreadOperationKind(kind) {
21574
+ return kind === "comment_reply" || kind === "comment_resolve";
21575
+ }
20974
21576
  function getCanonicalOperationType(operation) {
20975
21577
  const type = operation?.type;
20976
21578
  if (type === "insert" && operation?.target?.revisionView === "rejected") return "rejected-insert";
20977
21579
  if (type === "restore") return "restore";
20978
- if (type === "comment" || type === "comment_reply" || type === "highlight") return type;
21580
+ if (type === "comment" || type === "comment_reply" || type === "comment_resolve" || type === "highlight") return type;
20979
21581
  if (type === "paragraph-format") return "paragraph-format";
20980
21582
  if (type === "character-format" || type === "format" && (operation?.textToFormat != null || operation?.properties != null)) return "format";
20981
21583
  return "redline";
@@ -21095,7 +21697,7 @@ function validateDocumentOperation(operation) {
21095
21697
  };
21096
21698
  }
21097
21699
  const target = normalized.targetDescriptor;
21098
- if (normalized.operationKind !== "comment_reply" && !nonEmptyString(target.text) && target.index == null && !target.paragraphId && !target.fingerprint && !target.captureRef) {
21700
+ if (!isCommentThreadOperationKind(normalized.operationKind) && !nonEmptyString(target.text) && target.index == null && !target.paragraphId && !target.fingerprint && !target.captureRef) {
21099
21701
  return {
21100
21702
  valid: false,
21101
21703
  error: {
@@ -21322,6 +21924,14 @@ function validateDocumentOperation(operation) {
21322
21924
  return { valid: false, error: { code: "INVALID_OPERATION", message: 'Comment reply operations require a non-empty "commentContent" field.' } };
21323
21925
  }
21324
21926
  }
21927
+ if (normalized.operationKind === "comment_resolve") {
21928
+ if (!nonEmptyString(String(normalized.commentId ?? ""))) {
21929
+ return { valid: false, error: { code: "INVALID_OPERATION", message: 'Comment resolve operations require a "commentId".' } };
21930
+ }
21931
+ if (normalized.resolved != null && typeof normalized.resolved !== "boolean") {
21932
+ return { valid: false, error: { code: "INVALID_OPERATION", message: 'Comment resolve "resolved" must be a boolean when provided.' } };
21933
+ }
21934
+ }
21325
21935
  if (normalized.operationKind === "highlight") {
21326
21936
  if (!nonEmptyString(normalized.textToHighlight)) {
21327
21937
  return {
@@ -21801,7 +22411,7 @@ function compileOperationBatch(xmlDoc, operations = [], options = {}) {
21801
22411
  const dynamicOccurrence = operation.targetDescriptor?.occurrence != null && !operation.targetDescriptor?.paragraphId && !operation.targetDescriptor?.fingerprint;
21802
22412
  const compositeTextTarget = typeof operation.targetDescriptor?.text === "string" && /\r|\n/.test(operation.targetDescriptor.text) && !operation.targetEndDescriptor;
21803
22413
  if (dynamicOccurrence || compositeTextTarget) binding.dynamic = true;
21804
- if (validation.valid && operation.operationKind !== "comment_reply" && !operation.targetDescriptor?.captureRef && !binding.dynamic) {
22414
+ if (validation.valid && operation.operationKind !== "comment_reply" && operation.operationKind !== "comment_resolve" && !operation.targetDescriptor?.captureRef && !binding.dynamic) {
21805
22415
  try {
21806
22416
  const start = resolveDescriptor(
21807
22417
  xmlDoc,
@@ -22302,12 +22912,12 @@ function resolveTargetFromCapture(xmlDoc, session, targetDescriptor, _opType = "
22302
22912
  }
22303
22913
 
22304
22914
  // services/document-operation-mutations.js
22305
- var NS_W8 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
22915
+ var NS_W9 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
22306
22916
  var NS_W142 = "http://schemas.microsoft.com/office/word/2010/wordml";
22307
22917
  function getCommentIdsInElement(element) {
22308
22918
  const ids = /* @__PURE__ */ new Set();
22309
22919
  for (const localName2 of ["commentRangeStart", "commentRangeEnd", "commentReference"]) {
22310
- for (const node of Array.from(element?.getElementsByTagNameNS?.(NS_W8, localName2) || [])) {
22920
+ for (const node of Array.from(element?.getElementsByTagNameNS?.(NS_W9, localName2) || [])) {
22311
22921
  const id = node.getAttribute("w:id") || node.getAttribute("id");
22312
22922
  if (id !== "") ids.add(id);
22313
22923
  }
@@ -22521,7 +23131,7 @@ function normalizeBodySectionOrder(xmlDoc) {
22521
23131
  normalizeBodySectionOrderStandalone(xmlDoc);
22522
23132
  }
22523
23133
  function removeProofErrNodes(paragraph) {
22524
- for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W8, "proofErr") || [])) {
23134
+ for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W9, "proofErr") || [])) {
22525
23135
  node.parentNode?.removeChild(node);
22526
23136
  }
22527
23137
  }
@@ -22532,12 +23142,12 @@ function preprocessRedlineTargetParagraph(targetParagraph) {
22532
23142
  function getDirectWordChild4(element, localName2) {
22533
23143
  if (!element) return null;
22534
23144
  return Array.from(element.childNodes || []).find(
22535
- (node) => node && node.nodeType === 1 && node.namespaceURI === NS_W8 && node.localName === localName2
23145
+ (node) => node && node.nodeType === 1 && node.namespaceURI === NS_W9 && node.localName === localName2
22536
23146
  ) || null;
22537
23147
  }
22538
23148
  function computeTableIndexInDocument(xmlDoc, tableElement) {
22539
23149
  if (!xmlDoc || !tableElement) return null;
22540
- const tables = Array.from(xmlDoc.getElementsByTagNameNS(NS_W8, "tbl"));
23150
+ const tables = Array.from(xmlDoc.getElementsByTagNameNS(NS_W9, "tbl"));
22541
23151
  const idx = tables.indexOf(tableElement);
22542
23152
  return idx >= 0 ? idx + 1 : null;
22543
23153
  }
@@ -22587,7 +23197,7 @@ function buildInsertedListParagraph(xmlDoc, anchorParagraph, entry, revisionMeta
22587
23197
  markParagraphMarkInserted(xmlDoc, paragraph, author);
22588
23198
  }
22589
23199
  const run = createWordElement(xmlDoc, "w:r");
22590
- const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W8, "r"))[0] || null;
23200
+ const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W9, "r"))[0] || null;
22591
23201
  const anchorRunPr = anchorFirstRun ? getDirectWordChild4(anchorFirstRun, "rPr") : null;
22592
23202
  if (anchorRunPr) {
22593
23203
  run.appendChild(clonePropertiesWithoutRevisionHistory(anchorRunPr));
@@ -22612,7 +23222,7 @@ function buildInsertedListParagraph(xmlDoc, anchorParagraph, entry, revisionMeta
22612
23222
  }
22613
23223
  function serializeParagraphRangeAsDocument(paragraphs, serializer) {
22614
23224
  const paragraphXml = (paragraphs || []).map((paragraph) => serializer.serializeToString(paragraph)).join("");
22615
- return `<w:document xmlns:w="${NS_W8}"><w:body>${paragraphXml}</w:body></w:document>`;
23225
+ return `<w:document xmlns:w="${NS_W9}"><w:body>${paragraphXml}</w:body></w:document>`;
22616
23226
  }
22617
23227
  function applyExplicitRangeListInsertions({
22618
23228
  xmlDoc,
@@ -22681,7 +23291,7 @@ function buildEmptyParagraphTemplateFromAnchor(xmlDoc, anchorParagraph) {
22681
23291
  const anchorPPr = getDirectWordChild4(anchorParagraph, "pPr");
22682
23292
  if (anchorPPr) paragraph.appendChild(clonePropertiesWithoutRevisionHistory(anchorPPr));
22683
23293
  const run = createWordElement(xmlDoc, "w:r");
22684
- const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W8, "r"))[0] || null;
23294
+ const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W9, "r"))[0] || null;
22685
23295
  const anchorRunPr = anchorFirstRun ? getDirectWordChild4(anchorFirstRun, "rPr") : null;
22686
23296
  if (anchorRunPr) run.appendChild(clonePropertiesWithoutRevisionHistory(anchorRunPr));
22687
23297
  const textNode = createWordElement(xmlDoc, "w:t");
@@ -22704,7 +23314,7 @@ function wrapParagraphContentInInsertion(xmlDoc, paragraph, revisionMetadata, au
22704
23314
  ins.setAttribute("w:author", metadata.author);
22705
23315
  ins.setAttribute("w:date", metadata.date);
22706
23316
  for (const child of Array.from(paragraph.childNodes || [])) {
22707
- if (child?.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName === "pPr") continue;
23317
+ if (child?.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName === "pPr") continue;
22708
23318
  ins.appendChild(child.cloneNode(true));
22709
23319
  }
22710
23320
  wrappedParagraph.appendChild(ins);
@@ -22728,7 +23338,7 @@ async function buildInsertedPlainParagraph(xmlDoc, anchorParagraph, text2, revis
22728
23338
  if (typeof markdownResult?.oxml === "string") {
22729
23339
  const extracted = extractReplacementNodes(markdownResult.oxml);
22730
23340
  sourceParagraph = (extracted.replacementNodes || []).find(
22731
- (node) => node && node.nodeType === 1 && node.namespaceURI === NS_W8 && node.localName === "p"
23341
+ (node) => node && node.nodeType === 1 && node.namespaceURI === NS_W9 && node.localName === "p"
22732
23342
  ) || null;
22733
23343
  }
22734
23344
  if (!sourceParagraph) {
@@ -22760,7 +23370,7 @@ function collectNumberingIdsFromNodes(nodes) {
22760
23370
  }
22761
23371
  function directDeletionCarrierText(carrier) {
22762
23372
  return Array.from(carrier?.childNodes || []).reduce((text2, child) => {
22763
- return text2 + (child?.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName === "r" ? getRunContentPieces(child).map((piece) => piece.text).join("") : "");
23373
+ return text2 + (child?.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName === "r" ? getRunContentPieces(child).map((piece) => piece.text).join("") : "");
22764
23374
  }, "");
22765
23375
  }
22766
23376
  function findOccurrenceOffset(text2, needle, occurrence) {
@@ -22802,9 +23412,9 @@ function hasUnsupportedDeletionSplitMarkup(paragraph, carrier) {
22802
23412
  }
22803
23413
  for (const child of Array.from(carrier?.childNodes || [])) {
22804
23414
  if (child.nodeType !== 1) continue;
22805
- if (!(child.namespaceURI === NS_W8 && child.localName === "r")) return true;
23415
+ if (!(child.namespaceURI === NS_W9 && child.localName === "r")) return true;
22806
23416
  for (const runChild of Array.from(child.childNodes || [])) {
22807
- if (runChild.nodeType !== 1 || runChild.namespaceURI !== NS_W8) continue;
23417
+ if (runChild.nodeType !== 1 || runChild.namespaceURI !== NS_W9) continue;
22808
23418
  if (!["rPr", "delText", "t", "tab", "br", "cr", "noBreakHyphen", "softHyphen"].includes(runChild.localName)) return true;
22809
23419
  }
22810
23420
  }
@@ -22813,7 +23423,7 @@ function hasUnsupportedDeletionSplitMarkup(paragraph, carrier) {
22813
23423
  function insertedRunPropertiesAtDeletionOffset(carrier, localOffset) {
22814
23424
  let offset = 0;
22815
23425
  for (const child of Array.from(carrier?.childNodes || [])) {
22816
- if (!(child?.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName === "r")) continue;
23426
+ if (!(child?.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName === "r")) continue;
22817
23427
  const length = getRunTextLength(getRunContentPieces(child));
22818
23428
  if (localOffset <= offset + length) {
22819
23429
  return clonePropertiesWithoutRevisionHistory(getDirectWordChild4(child, "rPr"));
@@ -22843,7 +23453,7 @@ function normalizedAuthor2(author) {
22843
23453
  function directWordParagraphSibling(paragraph, direction = "nextSibling") {
22844
23454
  let cursor = paragraph?.[direction] || null;
22845
23455
  while (cursor) {
22846
- if (cursor.nodeType === 1 && cursor.namespaceURI === NS_W8 && cursor.localName === "p") return cursor;
23456
+ if (cursor.nodeType === 1 && cursor.namespaceURI === NS_W9 && cursor.localName === "p") return cursor;
22847
23457
  cursor = cursor[direction] || null;
22848
23458
  }
22849
23459
  return null;
@@ -22853,7 +23463,7 @@ function createSanitizedRestorationPPr(xmlDoc, sourceParagraph) {
22853
23463
  const pPr = createWordElement(xmlDoc, "w:pPr");
22854
23464
  if (!sourcePPr) return pPr;
22855
23465
  for (const child of Array.from(sourcePPr.childNodes || [])) {
22856
- if (child.nodeType !== 1 || child.namespaceURI !== NS_W8) continue;
23466
+ if (child.nodeType !== 1 || child.namespaceURI !== NS_W9) continue;
22857
23467
  if (!RESTORATION_PPR_ALLOWLIST.has(child.localName)) continue;
22858
23468
  pPr.appendChild(xmlDoc.importNode(child, true));
22859
23469
  }
@@ -22862,14 +23472,14 @@ function createSanitizedRestorationPPr(xmlDoc, sourceParagraph) {
22862
23472
  function collectRestorationAnchorWarnings(sourceParagraph) {
22863
23473
  const warnings = [];
22864
23474
  const bookmarkNames = /* @__PURE__ */ new Set();
22865
- for (const node of Array.from(sourceParagraph?.getElementsByTagNameNS?.(NS_W8, "bookmarkStart") || [])) {
23475
+ for (const node of Array.from(sourceParagraph?.getElementsByTagNameNS?.(NS_W9, "bookmarkStart") || [])) {
22866
23476
  const name = wordAttribute4(node, "name") || "(unnamed)";
22867
23477
  bookmarkNames.add(name);
22868
23478
  }
22869
23479
  for (const name of bookmarkNames) warnings.push(`RESTORATION_DROPPED_BOOKMARK:${name}`);
22870
23480
  const commentIds = /* @__PURE__ */ new Set();
22871
23481
  for (const localName2 of ["commentRangeStart", "commentRangeEnd", "commentReference"]) {
22872
- for (const node of Array.from(sourceParagraph?.getElementsByTagNameNS?.(NS_W8, localName2) || [])) {
23482
+ for (const node of Array.from(sourceParagraph?.getElementsByTagNameNS?.(NS_W9, localName2) || [])) {
22873
23483
  const id = wordAttribute4(node, "id");
22874
23484
  if (id !== "") commentIds.add(id);
22875
23485
  }
@@ -22886,24 +23496,24 @@ function removeRestorationAnchors(paragraph) {
22886
23496
  "commentReference"
22887
23497
  ];
22888
23498
  for (const localName2 of anchorNames) {
22889
- for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W8, localName2) || [])) {
23499
+ for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W9, localName2) || [])) {
22890
23500
  node.parentNode?.removeChild(node);
22891
23501
  }
22892
23502
  }
22893
- for (const run of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W8, "r") || [])) {
22894
- const meaningful = Array.from(run.childNodes || []).some((child) => child.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName !== "rPr");
23503
+ for (const run of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W9, "r") || [])) {
23504
+ const meaningful = Array.from(run.childNodes || []).some((child) => child.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName !== "rPr");
22895
23505
  if (!meaningful) run.parentNode?.removeChild(run);
22896
23506
  }
22897
23507
  }
22898
23508
  function buildRejectedRestorationTemplate(xmlDoc, sourceParagraph, serializer) {
22899
23509
  const rejected = rejectTrackedChangesInOoxml(serializer.serializeToString(sourceParagraph), { allAuthors: true });
22900
23510
  const parsed = parseOoxmlSafe(rejected.oxml, "application/xml");
22901
- const rejectedParagraph = parsed.doc?.getElementsByTagNameNS?.(NS_W8, "p")?.[0] || null;
23511
+ const rejectedParagraph = parsed.doc?.getElementsByTagNameNS?.(NS_W9, "p")?.[0] || null;
22902
23512
  if (!rejectedParagraph) return null;
22903
23513
  const paragraph = createWordElement(xmlDoc, "w:p");
22904
23514
  paragraph.appendChild(createSanitizedRestorationPPr(xmlDoc, sourceParagraph));
22905
23515
  for (const child of Array.from(rejectedParagraph.childNodes || [])) {
22906
- if (child.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName === "pPr") continue;
23516
+ if (child.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName === "pPr") continue;
22907
23517
  paragraph.appendChild(xmlDoc.importNode(child, true));
22908
23518
  }
22909
23519
  removeRestorationAnchors(paragraph);
@@ -22924,7 +23534,7 @@ async function editRestorationTemplate(xmlDoc, template, modifiedText, author, s
22924
23534
  );
22925
23535
  if (result?.status === "error" || typeof result?.oxml !== "string") return null;
22926
23536
  const extracted = extractReplacementNodes(result.oxml);
22927
- const paragraph = (extracted.replacementNodes || []).find((node) => node?.nodeType === 1 && node.namespaceURI === NS_W8 && node.localName === "p");
23537
+ const paragraph = (extracted.replacementNodes || []).find((node) => node?.nodeType === 1 && node.namespaceURI === NS_W9 && node.localName === "p");
22928
23538
  return paragraph ? xmlDoc.importNode(paragraph, true) : null;
22929
23539
  }
22930
23540
  function allocateFreshParagraphId(xmlDoc, operationSession) {
@@ -23095,7 +23705,7 @@ async function insertIntoRejectedDeletedText(documentXml, targetText, anchor, mo
23095
23705
  }
23096
23706
  const insertionOffset = anchorStart + anchor.offset;
23097
23707
  const carriers = Array.from(paragraph.childNodes || []).filter(
23098
- (node) => node?.nodeType === 1 && node.namespaceURI === NS_W8 && node.localName === "del"
23708
+ (node) => node?.nodeType === 1 && node.namespaceURI === NS_W9 && node.localName === "del"
23099
23709
  );
23100
23710
  if (carriers.some((carrier) => hasUnsupportedDeletionSplitMarkup(paragraph, carrier))) {
23101
23711
  return {
@@ -23752,7 +24362,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
23752
24362
  };
23753
24363
  }
23754
24364
  if (!allSame) {
23755
- const hasMove = targetParagraph.getElementsByTagNameNS(NS_W8, "moveFrom").length > 0 || targetParagraph.getElementsByTagNameNS(NS_W8, "moveTo").length > 0;
24365
+ const hasMove = targetParagraph.getElementsByTagNameNS(NS_W9, "moveFrom").length > 0 || targetParagraph.getElementsByTagNameNS(NS_W9, "moveTo").length > 0;
23756
24366
  if (hasMove) {
23757
24367
  return {
23758
24368
  documentXml,
@@ -23767,8 +24377,8 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
23767
24377
  }
23768
24378
  }
23769
24379
  } else {
23770
- const hasDel = targetParagraph.getElementsByTagNameNS(NS_W8, "del").length > 0;
23771
- const hasMove = targetParagraph.getElementsByTagNameNS(NS_W8, "moveFrom").length > 0 || targetParagraph.getElementsByTagNameNS(NS_W8, "moveTo").length > 0;
24380
+ const hasDel = targetParagraph.getElementsByTagNameNS(NS_W9, "del").length > 0;
24381
+ const hasMove = targetParagraph.getElementsByTagNameNS(NS_W9, "moveFrom").length > 0 || targetParagraph.getElementsByTagNameNS(NS_W9, "moveTo").length > 0;
23772
24382
  if (hasDel) {
23773
24383
  return {
23774
24384
  documentXml,
@@ -24287,7 +24897,7 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
24287
24897
  const targetParagraph = resolved.paragraph;
24288
24898
  const collectVisibleRuns = () => {
24289
24899
  const runEntries2 = [];
24290
- const allRuns = Array.from(targetParagraph.getElementsByTagNameNS(NS_W8, "r"));
24900
+ const allRuns = Array.from(targetParagraph.getElementsByTagNameNS(NS_W9, "r"));
24291
24901
  for (const run of allRuns) {
24292
24902
  let parent = run.parentNode;
24293
24903
  let isDel = false;
@@ -24379,7 +24989,7 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
24379
24989
  const targetRuns = finalRuns.filter((span) => span.start >= matchStart && span.end <= matchEnd);
24380
24990
  let anyChanged = false;
24381
24991
  for (const entry of targetRuns) {
24382
- const rPr = entry.run.getElementsByTagNameNS(NS_W8, "rPr")[0] || null;
24992
+ const rPr = entry.run.getElementsByTagNameNS(NS_W9, "rPr")[0] || null;
24383
24993
  if (checkRunPropertiesChanged(rPr, properties)) {
24384
24994
  anyChanged = true;
24385
24995
  break;
@@ -24390,7 +25000,7 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
24390
25000
  }
24391
25001
  for (const entry of targetRuns) {
24392
25002
  const run = entry.run;
24393
- let rPr = run.getElementsByTagNameNS(NS_W8, "rPr")[0];
25003
+ let rPr = run.getElementsByTagNameNS(NS_W9, "rPr")[0];
24394
25004
  if (!rPr) {
24395
25005
  rPr = createWordElement(xmlDoc, "w:rPr");
24396
25006
  run.insertBefore(rPr, run.firstChild);
@@ -24399,7 +25009,7 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
24399
25009
  let insideSameAuthorIns = false;
24400
25010
  while (insAncestor && insAncestor !== targetParagraph) {
24401
25011
  if (insAncestor.nodeType === 1 && (insAncestor.localName === "ins" || insAncestor.nodeName === "w:ins")) {
24402
- const insAuthor = insAncestor.getAttribute("w:author") || insAncestor.getAttributeNS(NS_W8, "author");
25012
+ const insAuthor = insAncestor.getAttribute("w:author") || insAncestor.getAttributeNS(NS_W9, "author");
24403
25013
  if (insAuthor === (author || getDefaultAuthor())) {
24404
25014
  insideSameAuthorIns = true;
24405
25015
  }
@@ -24447,7 +25057,7 @@ async function applyParagraphFormatToParagraphByExactText(documentXml, targetTex
24447
25057
  return { documentXml, hasChanges: false, status: "error", error: resolved?.error || { code: "TARGET_NOT_FOUND", message: "Target paragraph not found." } };
24448
25058
  }
24449
25059
  const targetParagraph = resolved.paragraph;
24450
- let pPr = targetParagraph.getElementsByTagNameNS(NS_W8, "pPr")[0];
25060
+ let pPr = targetParagraph.getElementsByTagNameNS(NS_W9, "pPr")[0];
24451
25061
  if (!pPr) {
24452
25062
  pPr = createWordElement(xmlDoc, "w:pPr");
24453
25063
  targetParagraph.insertBefore(pPr, targetParagraph.firstChild);
@@ -24495,7 +25105,7 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
24495
25105
  }
24496
25106
  let operation = validation.operation || normalizeDocumentOperation(op);
24497
25107
  const authorUsed = resolveDocumentOperationAuthor(operation, author, getDefaultAuthor());
24498
- if (operation.operationKind !== "comment_reply" && operation.operationKind !== "rejected-insert" && operation.operationKind !== "restore" && (operation.targetDescriptor?.revisionView === "rejected" || operation.targetEndDescriptor?.revisionView === "rejected")) {
25108
+ if (operation.operationKind !== "comment_reply" && operation.operationKind !== "comment_resolve" && operation.operationKind !== "rejected-insert" && operation.operationKind !== "restore" && (operation.targetDescriptor?.revisionView === "rejected" || operation.targetEndDescriptor?.revisionView === "rejected")) {
24499
25109
  return {
24500
25110
  documentXml,
24501
25111
  hasChanges: false,
@@ -24662,6 +25272,7 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
24662
25272
  result = applyCommentReplyToParts({
24663
25273
  commentsXml: existingCommentsXml,
24664
25274
  commentsExtendedXml: existingExtendedXml,
25275
+ documentXml,
24665
25276
  parentCommentId: operation.parentCommentId,
24666
25277
  commentId,
24667
25278
  commentContent: operation.commentContent,
@@ -24669,8 +25280,30 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
24669
25280
  date: operation.date || (/* @__PURE__ */ new Date()).toISOString()
24670
25281
  });
24671
25282
  result.documentXml = documentXml;
24672
- if (result.hasChanges && session.receiptCollector) session.receiptCollector.recordComment(commentId);
25283
+ if (result.status !== "error" && result.hasChanges) {
25284
+ if (!session.document || !anchorReplyInDocument(session.document, result.threadCommentIds, commentId)) {
25285
+ result = { documentXml, hasChanges: false, status: "error", error: { code: "PARENT_ANCHOR_NOT_FOUND", message: `Comment '${operation.parentCommentId}' has no anchor in the document body, so a reply cannot be placed.` } };
25286
+ } else if (session.receiptCollector) {
25287
+ session.receiptCollector.recordComment(commentId);
25288
+ }
25289
+ }
24673
25290
  }
25291
+ } else if (operation.operationKind === "comment_resolve") {
25292
+ const resolution = applyThreadResolutionToParts({
25293
+ commentsXml: session.commentsXml || runtimeContext?.commentsXml || options.commentsXml,
25294
+ commentsExtendedXml: session.commentsExtendedXml || runtimeContext?.commentsExtendedXml || options.commentsExtendedXml,
25295
+ commentId: operation.commentId,
25296
+ resolved: operation.resolved !== false
25297
+ });
25298
+ result = resolution.status === "error" ? { documentXml, hasChanges: false, status: "error", error: resolution.error } : {
25299
+ documentXml,
25300
+ hasChanges: resolution.hasChanges,
25301
+ ...resolution.commentsXml ? { commentsXml: resolution.commentsXml, commentsXmlMode: "replace" } : {},
25302
+ ...resolution.commentsExtendedXml ? { commentsExtendedXml: resolution.commentsExtendedXml, commentsExtendedXmlMode: "replace" } : {},
25303
+ resolved: resolution.resolved,
25304
+ threadRootId: resolution.threadRootId,
25305
+ commentIds: resolution.commentIds
25306
+ };
24674
25307
  } else if (operation.operationKind === "highlight") {
24675
25308
  result = await applyHighlightToParagraphByExactText(
24676
25309
  documentXml,
@@ -24811,7 +25444,7 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
24811
25444
  ...resolutionCapture
24812
25445
  };
24813
25446
  }
24814
- session.markMutationCommitted(operation.operationKind !== "comment_reply");
25447
+ session.markMutationCommitted(true);
24815
25448
  if (operation._compiledSourceId && session.sourceTargetRegistry) {
24816
25449
  session.sourceTargetRegistry.commitMutation(
24817
25450
  operation._compiledSourceId,
@@ -24924,9 +25557,9 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
24924
25557
  }
24925
25558
 
24926
25559
  // services/batch-operation-orchestrator.js
24927
- var NS_W9 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
25560
+ var NS_W10 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
24928
25561
  function operationTargetPriority(op) {
24929
- return op?.type === "comment" || op?.type === "comment_reply" ? 0 : 1;
25562
+ return op?.type === "comment" || op?.type === "comment_reply" || op?.type === "comment_resolve" ? 0 : 1;
24930
25563
  }
24931
25564
  function buildOperationDependencyPlan(operations = []) {
24932
25565
  const list = Array.isArray(operations) ? operations : [];
@@ -25001,7 +25634,7 @@ function buildOperationDependencyPlan(operations = []) {
25001
25634
  inDegrees[i2]++;
25002
25635
  }
25003
25636
  const kind = normalizeDocumentOperation(op).operationKind;
25004
- if (kind !== "comment" && kind !== "comment_reply") {
25637
+ if (kind !== "comment" && kind !== "comment_reply" && kind !== "comment_resolve") {
25005
25638
  if (!mutatingCaptureConsumers.has(ref)) mutatingCaptureConsumers.set(ref, []);
25006
25639
  mutatingCaptureConsumers.get(ref).push({
25007
25640
  index: i2,
@@ -25069,9 +25702,9 @@ function mergeCommentsXml(existingXml, incomingXml) {
25069
25702
  const serializer = createSerializer();
25070
25703
  const existingRoot = existingDoc.documentElement;
25071
25704
  const existingIds = new Set(
25072
- Array.from(existingDoc.getElementsByTagNameNS(NS_W9, "comment")).map((node) => node.getAttribute("w:id") || node.getAttribute("id")).filter(Boolean)
25705
+ Array.from(existingDoc.getElementsByTagNameNS(NS_W10, "comment")).map((node) => node.getAttribute("w:id") || node.getAttribute("id")).filter(Boolean)
25073
25706
  );
25074
- for (const comment of Array.from(incomingDoc.getElementsByTagNameNS(NS_W9, "comment"))) {
25707
+ for (const comment of Array.from(incomingDoc.getElementsByTagNameNS(NS_W10, "comment"))) {
25075
25708
  const id = comment.getAttribute("w:id") || comment.getAttribute("id");
25076
25709
  if (id && existingIds.has(id)) continue;
25077
25710
  existingRoot.appendChild(existingDoc.importNode(comment, true));
@@ -25232,7 +25865,7 @@ async function applyOperationsToDocumentXml(documentXml, operations, author, run
25232
25865
  context.targetRefSnapshot = session.initialTargetReferenceSnapshot;
25233
25866
  }
25234
25867
  session.runtimeContext = context;
25235
- if (sourceOperations.some((op) => op?.type === "comment_reply")) {
25868
+ if (sourceOperations.some((op) => op?.type === "comment_reply" || op?.type === "comment_resolve")) {
25236
25869
  session.commentsXml = context.commentsXml || options.commentsXml || null;
25237
25870
  session.commentsExtendedXml = context.commentsExtendedXml || options.commentsExtendedXml || null;
25238
25871
  session.commentsXmlMode = "replace";
@@ -25305,6 +25938,7 @@ async function applyOperationsToDocumentXml(documentXml, operations, author, run
25305
25938
  ...result.resolvedTarget ? { resolvedTarget: result.resolvedTarget } : {},
25306
25939
  ...result.resolvedAnchor ? { resolvedAnchor: result.resolvedAnchor } : {},
25307
25940
  ...result.change ? { change: result.change } : {},
25941
+ ...result.operationType === "comment_resolve" && !isError ? { resolved: result.resolved, threadRootId: result.threadRootId, commentIds: result.commentIds } : {},
25308
25942
  ...Array.isArray(result.warnings) && result.warnings.length > 0 ? { warnings: result.warnings } : {},
25309
25943
  ...result.error ? { error: result.error } : {},
25310
25944
  ...result.receipt ? { receipt: result.receipt } : {}
@@ -25581,6 +26215,26 @@ function preflightOperations(documentXml, operations, author, options = {}) {
25581
26215
  }
25582
26216
  commentsRequired = commentsRequired || operation.operationKind === "comment" || operation.operationKind === "comment_reply";
25583
26217
  numberingRequired = numberingRequired || operationNeedsNumbering(operation);
26218
+ if (operation.operationKind === "comment_resolve") {
26219
+ const commentId = String(operation.commentId);
26220
+ results.push(options._existingCommentDetails?.[commentId] ? {
26221
+ index: index + 1,
26222
+ type: sourceOperation.type,
26223
+ operationType: operation.operationKind,
26224
+ status: "ready",
26225
+ authorUsed,
26226
+ resolvedBy: "comment_id",
26227
+ commentId
26228
+ } : {
26229
+ index: index + 1,
26230
+ type: sourceOperation.type,
26231
+ operationType: operation.operationKind,
26232
+ status: "error",
26233
+ authorUsed,
26234
+ error: { code: "COMMENT_NOT_FOUND", message: `Comment '${commentId}' was not found.` }
26235
+ });
26236
+ continue;
26237
+ }
25584
26238
  if (operation.operationKind === "comment_reply") {
25585
26239
  const parentId = String(operation.parentCommentId);
25586
26240
  const parent = options._existingCommentDetails?.[parentId];
@@ -26732,6 +27386,15 @@ var text = (entries, path) => {
26732
27386
  if (typeof entry === "string") return entry;
26733
27387
  return textDecoder2.decode(entry);
26734
27388
  };
27389
+ function reconcileCommentSiblings(working) {
27390
+ const updated = reconcileCommentSiblingParts({
27391
+ commentsXml: text(working, "word/comments.xml"),
27392
+ commentsIdsXml: text(working, "word/commentsIds.xml"),
27393
+ commentsExtensibleXml: text(working, "word/commentsExtensible.xml")
27394
+ });
27395
+ if (updated.commentsIdsXml) working.set("word/commentsIds.xml", textEncoder4.encode(updated.commentsIdsXml));
27396
+ if (updated.commentsExtensibleXml) working.set("word/commentsExtensible.xml", textEncoder4.encode(updated.commentsExtensibleXml));
27397
+ }
26735
27398
  var cloneEntries = (entries) => new Map([...entries].map(([name, data]) => [name, new Uint8Array(data)]));
26736
27399
  function toBufferCompatible(uint8Arr) {
26737
27400
  if (typeof Buffer !== "undefined" && typeof Buffer.from === "function") {
@@ -26799,6 +27462,7 @@ function computePackageRevisionToken(input) {
26799
27462
  entries
26800
27463
  });
26801
27464
  }
27465
+ var COMMENT_OPERATION_TYPES = /* @__PURE__ */ new Set(["comment", "comment_reply", "comment_resolve"]);
26802
27466
  function nextCommentId(entries) {
26803
27467
  const ids = `${text(entries, "word/document.xml") || ""} ${text(entries, "word/comments.xml") || ""}`.match(/(?:w:)?id=["'](\d+)["']/g) || [];
26804
27468
  let next = ids.reduce((max2, token) => Math.max(max2, Number(token.match(/\d+/)?.[0] || 0)), 0) + 1;
@@ -26817,7 +27481,7 @@ function existingCommentDetails(entries) {
26817
27481
  id,
26818
27482
  author: comment.getAttribute("w:author") || comment.getAttribute("author") || "",
26819
27483
  text: String(comment.textContent || "").trim(),
26820
- paraId: Array.from(comment.getElementsByTagNameNS("*", "p"))[0]?.getAttribute("w14:paraId") || null
27484
+ paraId: commentParaId(comment) || null
26821
27485
  };
26822
27486
  }
26823
27487
  return details;
@@ -26848,13 +27512,34 @@ var DocxDocument = class {
26848
27512
  }
26849
27513
  inspect(options = {}) {
26850
27514
  const digestFn = options.digestFn || ((bytes) => sha256(bytes));
26851
- return inspectDocumentParts({
27515
+ const inspection = inspectDocumentParts({
26852
27516
  documentXml: text(this.entries, "word/document.xml"),
26853
27517
  commentsXml: text(this.entries, "word/comments.xml"),
26854
27518
  commentsExtendedXml: text(this.entries, "word/commentsExtended.xml"),
26855
27519
  numberingXml: text(this.entries, "word/numbering.xml"),
26856
27520
  stylesXml: text(this.entries, "word/styles.xml")
26857
27521
  }, { ...options, digestFn });
27522
+ if (inspection.status === "error") return inspection;
27523
+ return { ...inspection, headersFooters: this.inspectHeadersFooters() };
27524
+ }
27525
+ /**
27526
+ * Header and footer parts the document uses, with their paragraph text, so operations can target them with
27527
+ * `part`. `hasFields` marks parts containing PAGE/NUMPAGES-style fields (edit around them, not inside).
27528
+ */
27529
+ inspectHeadersFooters() {
27530
+ return discoverHeaderFooterParts({
27531
+ documentXml: text(this.entries, "word/document.xml"),
27532
+ relsXml: text(this.entries, "word/_rels/document.xml.rels"),
27533
+ settingsXml: text(this.entries, "word/settings.xml")
27534
+ }).map((part) => {
27535
+ const xml = text(this.entries, part.path) || "";
27536
+ const partInspection = xml ? inspectDocumentParts({ documentXml: xml }) : { paragraphs: [] };
27537
+ return {
27538
+ ...part,
27539
+ hasFields: /<w:fldChar\b|<w:fldSimple\b/.test(xml),
27540
+ paragraphs: (partInspection.paragraphs || []).map((paragraph) => ({ index: paragraph.index, text: paragraph.text }))
27541
+ };
27542
+ });
26858
27543
  }
26859
27544
  getRevisionToken() {
26860
27545
  return computePackageRevisionToken(this.entries);
@@ -26863,12 +27548,51 @@ var DocxDocument = class {
26863
27548
  return this.getRevisionToken();
26864
27549
  }
26865
27550
  preflight(operations, author = getDefaultAuthor(), options = {}) {
26866
- return preflightOperations(
27551
+ const list = Array.isArray(operations) ? operations : [];
27552
+ const runBody = (ops) => preflightOperations(
26867
27553
  text(this.entries, "word/document.xml"),
26868
- operations,
27554
+ ops,
26869
27555
  author || getDefaultAuthor(),
26870
27556
  { ...options, _existingCommentDetails: existingCommentDetails(this.entries) }
26871
27557
  );
27558
+ if (!list.some((operation) => operation && typeof operation === "object" && operation.part != null)) return runBody(operations);
27559
+ const parts = this.inspectHeadersFooters();
27560
+ const bodyOps = [];
27561
+ const results = [];
27562
+ const conflicts = [];
27563
+ const authorsUsed = /* @__PURE__ */ new Set();
27564
+ let valid = true;
27565
+ list.forEach((operation, i2) => {
27566
+ if (!operation || typeof operation !== "object" || operation.part == null) {
27567
+ bodyOps.push({ operation, origIndex: i2 });
27568
+ return;
27569
+ }
27570
+ const resolved = resolvePartSelector(parts, operation.part);
27571
+ const refusal = COMMENT_OPERATION_TYPES.has(operation.type) ? { code: "COMMENT_IN_HEADER_FOOTER", message: "Word does not support comments in headers or footers." } : resolved.error;
27572
+ if (refusal) {
27573
+ valid = false;
27574
+ results.push({ index: i2 + 1, type: operation.type || "redline", status: "error", error: normalizeErrorWithRecovery({ ...refusal, operationIndex: i2 + 1 }) });
27575
+ return;
27576
+ }
27577
+ const { part: _selector, ...rest } = operation;
27578
+ const partResult = preflightOperations(text(this.entries, resolved.part.path), [rest], author || getDefaultAuthor(), { ...options, _existingCommentDetails: {} });
27579
+ valid = valid && partResult.valid;
27580
+ for (const result of partResult.results || []) results.push({ ...result, index: i2 + 1, part: resolved.part.path });
27581
+ conflicts.push(...partResult.conflicts || []);
27582
+ for (const name of partResult.authorsUsed || []) authorsUsed.add(name);
27583
+ });
27584
+ let requiredArtifacts = { comments: false, numbering: false };
27585
+ if (bodyOps.length) {
27586
+ const body = runBody(bodyOps.map((item) => item.operation));
27587
+ valid = valid && body.valid;
27588
+ for (const result of body.results || []) results.push({ ...result, index: bodyOps[result.index - 1].origIndex + 1 });
27589
+ conflicts.push(...body.conflicts || []);
27590
+ for (const name of body.authorsUsed || []) authorsUsed.add(name);
27591
+ requiredArtifacts = body.requiredArtifacts || requiredArtifacts;
27592
+ }
27593
+ results.sort((a, b) => a.index - b.index);
27594
+ const ok = valid && conflicts.length === 0;
27595
+ return { valid: ok, status: ok ? "ok" : "error", results, conflicts, authorsUsed: [...authorsUsed], requiredArtifacts };
26872
27596
  }
26873
27597
  toUint8Array() {
26874
27598
  return zipDocx(this.entries);
@@ -26876,7 +27600,240 @@ var DocxDocument = class {
26876
27600
  toBuffer() {
26877
27601
  return toBufferCompatible(this.toUint8Array());
26878
27602
  }
27603
+ /**
27604
+ * Applies operations to the document. Operations without `part` target the body (word/document.xml).
27605
+ * An operation with `part` targets one header or footer part:
27606
+ * `{ part: { kind: 'footer', type: 'default', section: 0 } | 'word/footer2.xml', target: { exactText }, ... }`.
27607
+ * Discover parts with `inspect().headersFooters`.
27608
+ */
26879
27609
  async applyOperations(operations, options = {}) {
27610
+ const list = Array.isArray(operations) ? operations : [];
27611
+ if (!list.some((operation) => operation && typeof operation === "object" && operation.part != null)) {
27612
+ return this.applyBodyOperations(operations, options);
27613
+ }
27614
+ return this.applyOperationsWithParts(list, options);
27615
+ }
27616
+ async applyOperationsWithParts(operations, options = {}) {
27617
+ const author = options.author || getDefaultAuthor();
27618
+ const atomic = options.atomic === true;
27619
+ const snapshotEntries = this.entries;
27620
+ const snapshotBytes = this.originalBytes;
27621
+ const total = operations.length;
27622
+ const restore = () => {
27623
+ this.entries = snapshotEntries;
27624
+ this.originalBytes = snapshotBytes;
27625
+ };
27626
+ const original = () => ({
27627
+ uint8Array: snapshotBytes,
27628
+ toUint8Array: () => new Uint8Array(snapshotBytes),
27629
+ buffer: toBufferCompatible(snapshotBytes),
27630
+ toBuffer: () => toBufferCompatible(snapshotBytes)
27631
+ });
27632
+ const refuse = (error2, results2 = [], receipts2 = [], extra = {}) => ({
27633
+ status: "error",
27634
+ hasChanges: false,
27635
+ written: false,
27636
+ rolledBack: true,
27637
+ results: results2,
27638
+ receipts: receipts2,
27639
+ executionOrder: [],
27640
+ authorsUsed: [],
27641
+ artifactsChanged: [],
27642
+ error: normalizeErrorWithRecovery(error2),
27643
+ retryPlan: createRetryPlan({ atomic: true, rolledBack: true, results: results2, receipts: receipts2, operationCount: total }),
27644
+ validation: { originalIssues: [], generatedIssues: [] },
27645
+ ...extra,
27646
+ ...original()
27647
+ });
27648
+ if (options.expectedRevision) {
27649
+ const precondition = await this.applyBodyOperations([], { expectedRevision: options.expectedRevision });
27650
+ if (precondition.status === "error") return precondition;
27651
+ }
27652
+ const parts = discoverHeaderFooterParts({
27653
+ documentXml: text(this.entries, "word/document.xml"),
27654
+ relsXml: text(this.entries, "word/_rels/document.xml.rels"),
27655
+ settingsXml: text(this.entries, "word/settings.xml")
27656
+ });
27657
+ const bodyOps = [];
27658
+ const partOps = [];
27659
+ for (let i2 = 0; i2 < operations.length; i2++) {
27660
+ const operation = operations[i2];
27661
+ if (!operation || typeof operation !== "object" || operation.part == null) {
27662
+ bodyOps.push({ operation, origIndex: i2 });
27663
+ continue;
27664
+ }
27665
+ if (COMMENT_OPERATION_TYPES.has(operation.type)) {
27666
+ return refuse({ code: "COMMENT_IN_HEADER_FOOTER", message: `Operation ${i2 + 1}: Word does not support comments in headers or footers.`, operationIndex: i2 + 1 });
27667
+ }
27668
+ const resolved = resolvePartSelector(parts, operation.part);
27669
+ if (resolved.error) return refuse({ ...resolved.error, message: `Operation ${i2 + 1}: ${resolved.error.message}`, operationIndex: i2 + 1 });
27670
+ const { part: _selector, ...rest } = operation;
27671
+ partOps.push({ operation: rest, origIndex: i2, part: resolved.part });
27672
+ }
27673
+ const results = [];
27674
+ const receipts = [];
27675
+ const executionOrder = [];
27676
+ const authorsUsed = /* @__PURE__ */ new Set();
27677
+ const originalIssues = [];
27678
+ let anyChange = false;
27679
+ let anyFailure = false;
27680
+ let bodyResult = null;
27681
+ if (bodyOps.length) {
27682
+ bodyResult = await this.applyBodyOperations(bodyOps.map((item) => item.operation), { ...options, expectedRevision: void 0 });
27683
+ const toOriginal = (index) => bodyOps[index - 1]?.origIndex + 1;
27684
+ for (const result of bodyResult.results || []) {
27685
+ results.push({
27686
+ ...result,
27687
+ index: toOriginal(result.index),
27688
+ ...result.receipt ? { receipt: { ...result.receipt, operationIndex: toOriginal(result.receipt.operationIndex) } } : {}
27689
+ });
27690
+ }
27691
+ for (const receipt of bodyResult.receipts || []) receipts.push({ ...receipt, operationIndex: toOriginal(receipt.operationIndex) });
27692
+ for (const index of bodyResult.executionOrder || []) executionOrder.push(toOriginal(index));
27693
+ for (const name of bodyResult.authorsUsed || []) authorsUsed.add(name);
27694
+ originalIssues.push(...bodyResult.validation?.originalIssues || []);
27695
+ if (bodyResult.status === "error" || bodyResult.rolledBack) {
27696
+ anyFailure = true;
27697
+ if (atomic) {
27698
+ restore();
27699
+ return { ...bodyResult, results, receipts, executionOrder, written: false, artifactsChanged: [], rolledBack: true, ...original() };
27700
+ }
27701
+ }
27702
+ anyChange = bodyResult.hasChanges === true;
27703
+ }
27704
+ const working = cloneEntries(this.entries);
27705
+ const zip = new MemoryZip(working);
27706
+ const changedParts = /* @__PURE__ */ new Set();
27707
+ const { expectedRevision: _ignored, ...runnerOptions } = options;
27708
+ const failPart = (item, error2) => {
27709
+ anyFailure = true;
27710
+ const failure = normalizeErrorWithRecovery({ ...error2, operationIndex: item.origIndex + 1 });
27711
+ results.push({ index: item.origIndex + 1, type: item.operation.type || "redline", status: "error", authorUsed: author, part: item.part.path, error: failure });
27712
+ };
27713
+ for (const item of partOps) {
27714
+ const path = item.part.path;
27715
+ const xml = text(working, path);
27716
+ if (!xml) {
27717
+ failPart(item, { code: "PART_NOT_FOUND", message: `Part '${path}' is referenced but missing from the package.` });
27718
+ if (atomic) break;
27719
+ continue;
27720
+ }
27721
+ const numberingText = text(working, "word/numbering.xml");
27722
+ const outcome = await applyOperationsToDocumentXml(xml, [item.operation], author, { numberingIdState: createDynamicNumberingIdState(numberingText || void 0) }, {
27723
+ ...runnerOptions,
27724
+ atomic: true,
27725
+ strictTargets: options.strictTargets !== false,
27726
+ _existingCommentDetails: {}
27727
+ });
27728
+ const single = outcome.results?.[0];
27729
+ if (outcome.rolledBack || outcome.status === "error") {
27730
+ failPart(item, single?.error || outcome.error || { code: "OPERATION_FAILED", message: "Operation failed." });
27731
+ if (atomic) break;
27732
+ continue;
27733
+ }
27734
+ if (!outcome.hasChanges) {
27735
+ results.push({ ...single, index: item.origIndex + 1, part: path });
27736
+ continue;
27737
+ }
27738
+ if (countRevisionsInsideFields(outcome.documentXml) > countRevisionsInsideFields(xml)) {
27739
+ failPart(item, { code: "FIELD_EDIT_REFUSED", message: `The edit would change a field (for example PAGE or NUMPAGES) in ${path}. Word recomputes fields; edit the text around them instead.` });
27740
+ if (atomic) break;
27741
+ continue;
27742
+ }
27743
+ const taken = /* @__PURE__ */ new Set();
27744
+ for (const [name, data] of working) {
27745
+ if (name === path || !(name === "word/document.xml" || isHeaderFooterPath(name))) continue;
27746
+ for (const id of collectRevisionIds(textDecoder2.decode(data))) taken.add(id);
27747
+ }
27748
+ const renumbered = renumberCollidingRevisionIds(outcome.documentXml, taken);
27749
+ working.set(path, textEncoder4.encode(renumbered.xml));
27750
+ const describeReceipt = (receipt) => receipt?.revisionItems ? { ...receipt, revisionItems: receipt.revisionItems.map((revision) => ({ ...revision, id: renumbered.idMap.get(String(revision.id)) ?? revision.id, partName: path })) } : receipt;
27751
+ if (outcome.numberingXmlParts?.length) {
27752
+ await ensureNumberingArtifactsInZip(zip, outcome.numberingXmlParts, { mergeNumberingXml: mergeNumberingXmlBySchemaOrder });
27753
+ }
27754
+ changedParts.add(path);
27755
+ anyChange = true;
27756
+ results.push({
27757
+ ...single,
27758
+ index: item.origIndex + 1,
27759
+ part: path,
27760
+ partSections: item.part.appliesToSections,
27761
+ ...single?.receipt ? { receipt: { ...describeReceipt(single.receipt), operationIndex: item.origIndex + 1 } } : {}
27762
+ });
27763
+ if (outcome.receipts?.[0]) receipts.push({ ...describeReceipt(outcome.receipts[0]), operationIndex: item.origIndex + 1 });
27764
+ executionOrder.push(item.origIndex + 1);
27765
+ for (const name of outcome.authorsUsed || []) authorsUsed.add(name);
27766
+ }
27767
+ const generatedIssues = [];
27768
+ if (!(atomic && anyFailure)) {
27769
+ for (const path of changedParts) {
27770
+ const before = validateRedlineOoxml(text(this.entries, path)).issues.map((issue) => ({ source: path, ...issue }));
27771
+ const after = validateRedlineOoxml(text(working, path)).issues.map((issue) => ({ source: path, ...issue }));
27772
+ generatedIssues.push(...validationErrors(subtractValidationIssueMultiset(after, before)));
27773
+ }
27774
+ if (options.validate !== false && changedParts.size) {
27775
+ try {
27776
+ await validateDocxPackage(zip);
27777
+ } catch (error2) {
27778
+ generatedIssues.push({ source: "package", code: "PACKAGE_VALIDATION", severity: "error", message: error2.message });
27779
+ }
27780
+ }
27781
+ }
27782
+ if (atomic && anyFailure || generatedIssues.length) {
27783
+ restore();
27784
+ const error2 = generatedIssues.length ? { code: "PACKAGE_OPERATION_FAILED", message: `Applied operations introduced invalid markup (${[...new Set(generatedIssues.map((issue) => issue.code))].join(", ")}).`, issues: generatedIssues } : { code: "BATCH_OPERATION_FAILED", message: "Atomic batch rolled back because one or more operations failed." };
27785
+ const rolledBackResults = results.map((result) => result.receipt ? { ...result, receipt: { ...result.receipt, committed: false, finalDisposition: "rolled_back" } } : result).sort((a, b) => a.index - b.index);
27786
+ const rolledBackReceipts = receipts.map((receipt) => ({ ...receipt, committed: false, finalDisposition: "rolled_back" }));
27787
+ return refuse(error2, rolledBackResults, rolledBackReceipts, { validation: { originalIssues, generatedIssues }, issues: generatedIssues });
27788
+ }
27789
+ results.sort((a, b) => a.index - b.index);
27790
+ receipts.sort((a, b) => a.operationIndex - b.operationIndex);
27791
+ const status = !anyFailure ? "ok" : anyChange ? "partial" : "error";
27792
+ const failureError = anyFailure ? { error: normalizeErrorWithRecovery({ code: "BATCH_OPERATION_FAILED", message: "One or more operations failed; the others were applied." }) } : {};
27793
+ if (!changedParts.size) {
27794
+ const buf = toBufferCompatible(this.originalBytes);
27795
+ return {
27796
+ ...bodyResult || {},
27797
+ results,
27798
+ receipts,
27799
+ executionOrder,
27800
+ authorsUsed: [...authorsUsed],
27801
+ status,
27802
+ hasChanges: anyChange,
27803
+ written: bodyResult?.written === true,
27804
+ artifactsChanged: bodyResult?.artifactsChanged || [],
27805
+ validation: { originalIssues, generatedIssues: [] },
27806
+ ...failureError,
27807
+ uint8Array: this.originalBytes,
27808
+ toUint8Array: () => new Uint8Array(this.originalBytes),
27809
+ buffer: buf,
27810
+ toBuffer: () => toBufferCompatible(this.originalBytes)
27811
+ };
27812
+ }
27813
+ this.entries = working;
27814
+ const outputBytes = this.toUint8Array();
27815
+ this.originalBytes = outputBytes;
27816
+ const artifactsChanged = [...working].filter(([name, data]) => !snapshotEntries.has(name) || !areByteArraysEqual(data, snapshotEntries.get(name))).map(([name]) => name);
27817
+ return {
27818
+ ...bodyResult || {},
27819
+ status,
27820
+ hasChanges: true,
27821
+ written: true,
27822
+ results,
27823
+ receipts,
27824
+ executionOrder,
27825
+ authorsUsed: [...authorsUsed],
27826
+ artifactsChanged,
27827
+ validation: { originalIssues, generatedIssues: [] },
27828
+ ...failureError,
27829
+ uint8Array: outputBytes,
27830
+ toUint8Array: () => new Uint8Array(outputBytes),
27831
+ buffer: toBufferCompatible(outputBytes),
27832
+ inspection: this.inspect(),
27833
+ toBuffer: () => toBufferCompatible(outputBytes)
27834
+ };
27835
+ }
27836
+ async applyBodyOperations(operations, options = {}) {
26880
27837
  const failedApply = (error2) => {
26881
27838
  const results = [];
26882
27839
  const receipts = [];
@@ -26987,18 +27944,21 @@ var DocxDocument = class {
26987
27944
  toBuffer: () => toBufferCompatible(this.originalBytes)
26988
27945
  };
26989
27946
  }
26990
- working.set("word/document.xml", textEncoder4.encode(result.documentXml));
27947
+ const bodyChanged = (result.results || []).some((item) => item?.status !== "error" && item?.operationType !== "comment_resolve");
27948
+ if (bodyChanged) working.set("word/document.xml", textEncoder4.encode(result.documentXml));
26991
27949
  await ensureNumberingArtifactsInZip(zip, result.numberingXmlParts, { mergeNumberingXml: mergeNumberingXmlBySchemaOrder });
26992
27950
  const existingCommentsXml = text(working, "word/comments.xml");
26993
27951
  const commentsXmlForPackaging = result.commentsXml || existingCommentsXml;
26994
27952
  await ensureCommentsArtifactsInZip(zip, commentsXmlForPackaging, {
26995
27953
  replaceExisting: result.commentsXmlMode === "replace" || !result.commentsXml && !!existingCommentsXml
26996
27954
  });
26997
- const existingCommentsExtendedXml = text(working, "word/commentsExtended.xml");
26998
- const commentsExtendedXmlForPackaging = result.commentsExtendedXml || existingCommentsExtendedXml;
26999
- await ensureCommentsExtendedArtifactsInZip(zip, commentsExtendedXmlForPackaging, {
27000
- replaceExisting: result.commentsExtendedXmlMode === "replace" || !result.commentsExtendedXml && !!existingCommentsExtendedXml
27001
- });
27955
+ if (result.commentsExtendedXml) {
27956
+ await ensureCommentsExtendedArtifactsInZip(zip, result.commentsExtendedXml, {
27957
+ replaceExisting: result.commentsExtendedXmlMode === "replace"
27958
+ });
27959
+ }
27960
+ await repairKnownContentTypes(zip);
27961
+ reconcileCommentSiblings(working);
27002
27962
  if (options.validate !== false) {
27003
27963
  const generated = validateRedlineOoxml(result.documentXml);
27004
27964
  const outputIssues = generated.issues.map((issue) => ({ source: "word/document.xml", ...issue }));
@@ -27067,13 +28027,25 @@ var DocxDocument = class {
27067
28027
  const transform = action === "accept" ? acceptTrackedChangesInOoxml : action === "reject" ? rejectTrackedChangesInOoxml : null;
27068
28028
  if (!transform) return packageFailure(this.originalBytes, "INVALID_ACTION", `Unknown revision action: ${action}`);
27069
28029
  const sourceBytes = this.originalBytes;
28030
+ const originalEntries = this.entries;
27070
28031
  const working = cloneEntries(this.entries);
27071
28032
  const zip = new MemoryZip(working);
27072
28033
  const result = transform(text(working, "word/document.xml"), { author: options.author, allAuthors: options.allAuthors === true });
27073
28034
  if (result.status === "error" || result.error) {
27074
28035
  return packageFailure(sourceBytes, result.error?.code || "REVISION_OPERATION_FAILED", result.error?.message || "Revision operation failed.");
27075
28036
  }
27076
- if (!result.hasChanges) {
28037
+ const partUpdates = /* @__PURE__ */ new Map();
28038
+ for (const part of this.inspectHeadersFooters()) {
28039
+ const partXml = text(working, part.path);
28040
+ if (!partXml) continue;
28041
+ const partResult = transform(partXml, { author: options.author, allAuthors: options.allAuthors === true });
28042
+ if (partResult.status === "error" || partResult.error) {
28043
+ return packageFailure(sourceBytes, partResult.error?.code || "REVISION_OPERATION_FAILED", `${part.path}: ${partResult.error?.message || "Revision operation failed."}`);
28044
+ }
28045
+ if (partResult.hasChanges) partUpdates.set(part.path, partResult.oxml);
28046
+ }
28047
+ const hasChanges = result.hasChanges || partUpdates.size > 0;
28048
+ if (!hasChanges) {
27077
28049
  const buf = toBufferCompatible(sourceBytes);
27078
28050
  return {
27079
28051
  ...result,
@@ -27086,8 +28058,10 @@ var DocxDocument = class {
27086
28058
  toBuffer: () => toBufferCompatible(sourceBytes)
27087
28059
  };
27088
28060
  }
27089
- working.set("word/document.xml", textEncoder4.encode(result.oxml));
28061
+ if (result.hasChanges) working.set("word/document.xml", textEncoder4.encode(result.oxml));
28062
+ for (const [path, xml] of partUpdates) working.set(path, textEncoder4.encode(xml));
27090
28063
  try {
28064
+ await repairKnownContentTypes(zip);
27091
28065
  if (options.validate !== false) await validateDocxPackage(zip);
27092
28066
  } catch (error2) {
27093
28067
  return packageFailure(sourceBytes, "PACKAGE_VALIDATION", error2.message);
@@ -27100,13 +28074,71 @@ var DocxDocument = class {
27100
28074
  ...result,
27101
28075
  status: "ok",
27102
28076
  written: true,
27103
- artifactsChanged: ["word/document.xml"],
28077
+ hasChanges: true,
28078
+ artifactsChanged: [...working].filter(([name, data]) => !originalEntries.has(name) || !areByteArraysEqual(data, originalEntries.get(name))).map(([name]) => name),
27104
28079
  uint8Array: outputBytes,
27105
28080
  toUint8Array: () => new Uint8Array(outputBytes),
27106
28081
  buffer: outputBuf,
27107
28082
  toBuffer: () => toBufferCompatible(outputBytes)
27108
28083
  };
27109
28084
  }
28085
+ /**
28086
+ * Resolves (or reopens) the comment thread containing `commentId`.
28087
+ * Resolved is a thread-level state in Word, so the root and every reply are updated together.
28088
+ */
28089
+ async resolveComment(commentId, options = {}) {
28090
+ const sourceBytes = this.originalBytes;
28091
+ const working = cloneEntries(this.entries);
28092
+ const resolved = options.resolved !== false;
28093
+ const noChange = (extra) => {
28094
+ const buf = toBufferCompatible(sourceBytes);
28095
+ return {
28096
+ status: "ok",
28097
+ hasChanges: false,
28098
+ written: false,
28099
+ artifactsChanged: [],
28100
+ ...extra,
28101
+ uint8Array: sourceBytes,
28102
+ toUint8Array: () => new Uint8Array(sourceBytes),
28103
+ buffer: buf,
28104
+ toBuffer: () => toBufferCompatible(sourceBytes)
28105
+ };
28106
+ };
28107
+ const result = applyThreadResolutionToParts({
28108
+ commentsXml: text(working, "word/comments.xml"),
28109
+ commentsExtendedXml: text(working, "word/commentsExtended.xml"),
28110
+ commentId,
28111
+ resolved
28112
+ });
28113
+ if (result.status === "error") return packageFailure(sourceBytes, result.error.code, result.error.message);
28114
+ if (!result.hasChanges) return noChange({ resolved, threadRootId: result.threadRootId, commentIds: result.commentIds });
28115
+ const zip = new MemoryZip(working);
28116
+ try {
28117
+ if (result.commentsXml) working.set("word/comments.xml", textEncoder4.encode(result.commentsXml));
28118
+ await ensureCommentsExtendedArtifactsInZip(zip, result.commentsExtendedXml, { replaceExisting: true });
28119
+ await repairKnownContentTypes(zip);
28120
+ if (options.validate !== false) await validateDocxPackage(zip);
28121
+ } catch (error2) {
28122
+ return packageFailure(sourceBytes, "PACKAGE_VALIDATION", error2.message);
28123
+ }
28124
+ const artifactsChanged = [...working].filter(([name, data]) => !this.entries.has(name) || !areByteArraysEqual(data, this.entries.get(name))).map(([name]) => name);
28125
+ this.entries = working;
28126
+ const outputBytes = this.toUint8Array();
28127
+ this.originalBytes = outputBytes;
28128
+ return {
28129
+ status: "ok",
28130
+ hasChanges: true,
28131
+ written: true,
28132
+ resolved,
28133
+ threadRootId: result.threadRootId,
28134
+ commentIds: result.commentIds,
28135
+ artifactsChanged,
28136
+ uint8Array: outputBytes,
28137
+ toUint8Array: () => new Uint8Array(outputBytes),
28138
+ buffer: toBufferCompatible(outputBytes),
28139
+ toBuffer: () => toBufferCompatible(outputBytes)
28140
+ };
28141
+ }
27110
28142
  async deleteComments(options = {}) {
27111
28143
  const sourceBytes = this.originalBytes;
27112
28144
  const working = cloneEntries(this.entries);
@@ -27130,7 +28162,13 @@ var DocxDocument = class {
27130
28162
  if (!parsed.doc || parsed.error) {
27131
28163
  return packageFailure(sourceBytes, "PARSE_ERROR", parsed.error?.message || "Could not parse comments.xml.");
27132
28164
  }
27133
- const matches = (comment) => options.allAuthors === true || (comment.getAttribute("w:author") || comment.getAttribute("author")) === options.author;
28165
+ const requestedIds = Array.isArray(options.ids) ? new Set(options.ids.map(String)) : null;
28166
+ if (requestedIds) {
28167
+ const known = new Set(Array.from(parsed.doc.getElementsByTagNameNS("*", "comment")).map((node) => node.getAttribute("w:id") || node.getAttribute("id")));
28168
+ const missing = [...requestedIds].filter((id) => !known.has(id));
28169
+ if (missing.length) return packageFailure(sourceBytes, "COMMENT_NOT_FOUND", `Comment id(s) not found: ${missing.join(", ")}.`);
28170
+ }
28171
+ const matches = (comment) => requestedIds ? requestedIds.has(comment.getAttribute("w:id") || comment.getAttribute("id")) : options.allAuthors === true || (comment.getAttribute("w:author") || comment.getAttribute("author")) === options.author;
27134
28172
  const ids = new Set(
27135
28173
  Array.from(parsed.doc.getElementsByTagNameNS("*", "comment")).filter(matches).map((node) => node.getAttribute("w:id") || node.getAttribute("id")).filter(Boolean)
27136
28174
  );
@@ -27159,9 +28197,8 @@ var DocxDocument = class {
27159
28197
  const idByParaId = /* @__PURE__ */ new Map();
27160
28198
  for (const comment of Array.from(parsed.doc.getElementsByTagNameNS("*", "comment"))) {
27161
28199
  const id = comment.getAttribute("w:id") || comment.getAttribute("id");
27162
- const paragraph = Array.from(comment.getElementsByTagNameNS("*", "p"))[0];
27163
- const paraId = paragraph?.getAttribute("w14:paraId") || paragraph?.getAttribute("paraId");
27164
- if (paraId) idByParaId.set(paraId.toUpperCase(), id);
28200
+ const paraId = commentParaId(comment);
28201
+ if (paraId) idByParaId.set(paraId, id);
27165
28202
  }
27166
28203
  for (const [paraId, id] of idByParaId) {
27167
28204
  if (ids.has(id)) removedParaIds.add(paraId);
@@ -27180,8 +28217,8 @@ var DocxDocument = class {
27180
28217
  }
27181
28218
  }
27182
28219
  }
27183
- const commentsResult = deleteCommentsByAuthorInOoxml(commentsXml, { author: options.author, allAuthors: options.allAuthors === true });
27184
- const remainingParsed = parseOoxmlSafe(commentsResult.oxml, "application/xml");
28220
+ const remainingXml = requestedIds ? commentsXml : deleteCommentsByAuthorInOoxml(commentsXml, { author: options.author, allAuthors: options.allAuthors === true }).oxml;
28221
+ const remainingParsed = parseOoxmlSafe(remainingXml, "application/xml");
27185
28222
  if (!remainingParsed.doc || remainingParsed.error) {
27186
28223
  return packageFailure(sourceBytes, "PARSE_ERROR", remainingParsed.error?.message || "Could not parse updated comments.xml.");
27187
28224
  }
@@ -27217,10 +28254,14 @@ var DocxDocument = class {
27217
28254
  }
27218
28255
  working.set("word/document.xml", textEncoder4.encode(serializer.serializeToString(documentParsed.doc)));
27219
28256
  try {
27220
- if (options.validate !== false) await validateDocxPackage(new MemoryZip(working));
28257
+ reconcileCommentSiblings(working);
28258
+ const finalZip = new MemoryZip(working);
28259
+ await repairKnownContentTypes(finalZip);
28260
+ if (options.validate !== false) await validateDocxPackage(finalZip);
27221
28261
  } catch (error2) {
27222
28262
  return packageFailure(sourceBytes, "PACKAGE_VALIDATION", error2.message);
27223
28263
  }
28264
+ const artifactsChanged = [...working].filter(([name, data]) => !this.entries.has(name) || !areByteArraysEqual(data, this.entries.get(name))).map(([name]) => name);
27224
28265
  this.entries = working;
27225
28266
  const outputBytes = this.toUint8Array();
27226
28267
  this.originalBytes = outputBytes;
@@ -27231,7 +28272,7 @@ var DocxDocument = class {
27231
28272
  written: true,
27232
28273
  commentsRemoved: ids.size,
27233
28274
  referencesRemoved,
27234
- artifactsChanged: ["word/document.xml", "word/comments.xml", ...extendedParsed?.doc ? ["word/commentsExtended.xml"] : []],
28275
+ artifactsChanged,
27235
28276
  uint8Array: outputBytes,
27236
28277
  toUint8Array: () => new Uint8Array(outputBytes),
27237
28278
  buffer: outputBuf,