@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 v0.8.0 — https://github.com/AnsonLai/docx-redline-js
1
+ // @ansonlai/docx-redline-js 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;
@@ -2522,15 +2522,29 @@ function createDiffEngine(options = {}) {
2522
2522
  engine.Diff_Timeout = timeout;
2523
2523
  return engine;
2524
2524
  }
2525
- function tokenize(text2) {
2525
+ function tokenize(text2, atomicChars = null) {
2526
2526
  const tokens = [];
2527
+ const pushWord = (word) => {
2528
+ if (!atomicChars || atomicChars.size === 0) {
2529
+ tokens.push(word);
2530
+ return;
2531
+ }
2532
+ let start = 0;
2533
+ for (let index = 0; index < word.length; index++) {
2534
+ if (!atomicChars.has(word[index])) continue;
2535
+ if (index > start) tokens.push(word.slice(start, index));
2536
+ tokens.push(word[index]);
2537
+ start = index + 1;
2538
+ }
2539
+ if (start < word.length) tokens.push(word.slice(start));
2540
+ };
2527
2541
  const leading = text2.match(/^\s+/);
2528
2542
  if (leading) tokens.push(leading[0]);
2529
2543
  const regex = /(\S+)(\s*)/g;
2530
2544
  regex.lastIndex = leading?.[0].length || 0;
2531
2545
  let match;
2532
2546
  while ((match = regex.exec(text2)) !== null) {
2533
- if (match[1]) tokens.push(match[1]);
2547
+ if (match[1]) pushWord(match[1]);
2534
2548
  if (match[2]) tokens.push(match[2]);
2535
2549
  }
2536
2550
  return tokens;
@@ -2558,8 +2572,8 @@ function wordsToChars(text1, text2, options = {}) {
2558
2572
  }
2559
2573
  return { chars, tokenIds };
2560
2574
  }
2561
- const tokens1 = tokenize(text1);
2562
- const tokens2 = tokenize(text2);
2575
+ const tokens1 = tokenize(text1, options.atomicChars);
2576
+ const tokens2 = tokenize(text2, options.atomicChars);
2563
2577
  const encoded1 = mapTokensToChars(tokens1);
2564
2578
  const encoded2 = mapTokensToChars(tokens2);
2565
2579
  return {
@@ -7717,6 +7731,19 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
7717
7731
  pendingReplacementEvent = null;
7718
7732
  }
7719
7733
  }
7734
+ for (const offset of Array.from(sentinelMapByStart.keys()).sort((a, b) => a - b)) {
7735
+ for (const sentinel of sentinelMapByStart.get(offset)) {
7736
+ if (!sentinel.isCommentMarker || emittedCommentMarkers.has(sentinel.node)) continue;
7737
+ emittedCommentMarkers.add(sentinel.node);
7738
+ if (isWordElement(sentinel.node, "commentReference")) {
7739
+ const run = createWordElement(xmlDoc, "w:r");
7740
+ run.appendChild(sentinel.node.cloneNode(true));
7741
+ currentParagraph.appendChild(run);
7742
+ } else {
7743
+ currentParagraph.appendChild(sentinel.node.cloneNode(true));
7744
+ }
7745
+ }
7746
+ }
7720
7747
  if (generateRedlines) {
7721
7748
  containerFragments.forEach((fragment) => {
7722
7749
  Array.from(fragment.childNodes).forEach((node) => {
@@ -7951,7 +7978,11 @@ function applyReconstructionMode(xmlDoc, originalText, modifiedText, serializer,
7951
7978
  if (mapping.paragraphs.length === 0) {
7952
7979
  return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: false });
7953
7980
  }
7954
- const diffs = computeWordDiffs(mapping.originalFullText, mapping.processedModifiedText, diffOptions);
7981
+ const wordDiffs = computeWordDiffs(mapping.originalFullText, mapping.processedModifiedText, {
7982
+ ...diffOptions,
7983
+ atomicChars: new Set(mapping.referenceMap.keys())
7984
+ });
7985
+ const diffs = keepUnchangedTextOutsideWrappers(anchorSharedReferences(wordDiffs, mapping.referenceMap), mapping);
7955
7986
  return withOoxmlSourceType(applyReconstructionDiffs(
7956
7987
  xmlDoc,
7957
7988
  diffs,
@@ -7963,6 +7994,109 @@ function applyReconstructionMode(xmlDoc, originalText, modifiedText, serializer,
7963
7994
  options
7964
7995
  ));
7965
7996
  }
7997
+ function forEachChangeGroup(diffs, onEqual, onGroup) {
7998
+ let index = 0;
7999
+ while (index < diffs.length) {
8000
+ if (diffs[index][0] === 0) {
8001
+ onEqual(diffs[index]);
8002
+ index++;
8003
+ continue;
8004
+ }
8005
+ const group = [];
8006
+ while (index < diffs.length && diffs[index][0] !== 0) group.push(diffs[index++]);
8007
+ onGroup(
8008
+ group,
8009
+ group.filter(([op]) => op === -1).map(([, text2]) => text2).join(""),
8010
+ group.filter(([op]) => op === 1).map(([, text2]) => text2).join("")
8011
+ );
8012
+ }
8013
+ }
8014
+ function pushChange(result, deleted, inserted) {
8015
+ if (deleted === inserted) {
8016
+ if (deleted) result.push([0, deleted]);
8017
+ return;
8018
+ }
8019
+ if (deleted) result.push([-1, deleted]);
8020
+ if (inserted) result.push([1, inserted]);
8021
+ }
8022
+ function anchorSharedReferences(diffs, referenceMap) {
8023
+ if (referenceMap.size === 0) return diffs;
8024
+ const result = [];
8025
+ forEachChangeGroup(diffs, (part) => result.push(part), (group, deleted, inserted) => {
8026
+ const deletedRefs = [...deleted].filter((char) => referenceMap.has(char) && inserted.includes(char));
8027
+ const insertedRefs = [...inserted].filter((char) => deletedRefs.includes(char));
8028
+ if (deletedRefs.length === 0 || deletedRefs.join("") !== insertedRefs.join("")) {
8029
+ result.push(...group);
8030
+ return;
8031
+ }
8032
+ let deletedStart = 0;
8033
+ let insertedStart = 0;
8034
+ for (const ref of deletedRefs) {
8035
+ const deletedAt = deleted.indexOf(ref, deletedStart);
8036
+ const insertedAt = inserted.indexOf(ref, insertedStart);
8037
+ pushChange(result, deleted.slice(deletedStart, deletedAt), inserted.slice(insertedStart, insertedAt));
8038
+ result.push([0, ref]);
8039
+ deletedStart = deletedAt + 1;
8040
+ insertedStart = insertedAt + 1;
8041
+ }
8042
+ pushChange(result, deleted.slice(deletedStart), inserted.slice(insertedStart));
8043
+ });
8044
+ return result;
8045
+ }
8046
+ function commonPrefixLength(left, right) {
8047
+ const limit = Math.min(left.length, right.length);
8048
+ let length = 0;
8049
+ while (length < limit && left[length] === right[length]) length++;
8050
+ return length;
8051
+ }
8052
+ function commonSuffixLength(left, right, limit) {
8053
+ let length = 0;
8054
+ while (length < limit && left[left.length - 1 - length] === right[right.length - 1 - length]) length++;
8055
+ return length;
8056
+ }
8057
+ function keepUnchangedTextOutsideWrappers(diffs, mapping) {
8058
+ const wrapperAt = (index) => mapping.getRunProperties(index).wrapper || null;
8059
+ const result = [];
8060
+ let originalOffset = 0;
8061
+ forEachChangeGroup(diffs, (part) => {
8062
+ result.push(part);
8063
+ originalOffset += part[1].length;
8064
+ }, (group, deleted, inserted) => {
8065
+ const start = originalOffset;
8066
+ const end = start + deleted.length;
8067
+ originalOffset = end;
8068
+ const isWrapperBoundary = (index) => index > start && index < end && wrapperAt(index - 1) !== wrapperAt(index);
8069
+ let prefix = 0;
8070
+ let suffix = 0;
8071
+ if (deleted && inserted) {
8072
+ const sharedSuffix = commonSuffixLength(deleted, inserted, Math.min(deleted.length, inserted.length));
8073
+ for (let length = sharedSuffix; length > 0; length--) {
8074
+ if (isWrapperBoundary(end - length)) {
8075
+ suffix = length;
8076
+ break;
8077
+ }
8078
+ }
8079
+ const sharedPrefix = Math.min(
8080
+ commonPrefixLength(deleted, inserted),
8081
+ Math.min(deleted.length, inserted.length) - suffix
8082
+ );
8083
+ for (let length = sharedPrefix; length > 0; length--) {
8084
+ if (isWrapperBoundary(start + length)) {
8085
+ prefix = length;
8086
+ break;
8087
+ }
8088
+ }
8089
+ }
8090
+ if (prefix === 0 && suffix === 0) {
8091
+ result.push(...group);
8092
+ return;
8093
+ }
8094
+ if (prefix > 0) result.push([0, deleted.slice(0, prefix)]);
8095
+ pushChange(result, deleted.slice(prefix, deleted.length - suffix), inserted.slice(prefix, inserted.length - suffix));
8096
+ if (suffix > 0) result.push([0, deleted.slice(deleted.length - suffix)]);
8097
+ });
8098
+ return result;
8099
+ }
7966
8100
 
7967
8101
  // engine/table-mode.js
7968
8102
  function noChanges(serializer, xmlDoc) {
@@ -11511,6 +11645,260 @@ function ingestWordOoxmlToMarkdownResult(ooxml) {
11511
11645
  return { text: lines.join("\n\n").trim(), status: "ok", warnings: parsed.warnings };
11512
11646
  }
11513
11647
 
11648
+ // services/comment-builders.js
11649
+ var NS_W14 = "http://schemas.microsoft.com/office/word/2010/wordml";
11650
+ var NS_W15 = "http://schemas.microsoft.com/office/word/2012/wordml";
11651
+ function createCommentParaId(commentId) {
11652
+ const numeric = Number.parseInt(String(commentId), 10);
11653
+ const value = Number.isFinite(numeric) ? 1879048192 + (numeric >>> 0) >>> 0 : 1879048192;
11654
+ return value.toString(16).toUpperCase().padStart(8, "0").slice(-8);
11655
+ }
11656
+ function buildCommentElement(commentId, author, content, date, paraId = createCommentParaId(commentId)) {
11657
+ const initials = author.split(" ").map((word) => word[0]).join("").toUpperCase() || "AI";
11658
+ const escapedContent = escapeXml(content);
11659
+ const escapedAuthor = escapeXml(author);
11660
+ return `<w:comment w:id="${commentId}" w:author="${escapedAuthor}" w:date="${date}" w:initials="${initials}">
11661
+ <w:p w14:paraId="${escapeXml(paraId)}" xmlns:w14="${NS_W14}">
11662
+ <w:r><w:t>${escapedContent}</w:t></w:r>
11663
+ </w:p>
11664
+ </w:comment>`;
11665
+ }
11666
+ function buildCommentsPartXml(comments) {
11667
+ if (!comments || comments.length === 0) {
11668
+ return `<w:comments xmlns:w="${NS_W}"></w:comments>`;
11669
+ }
11670
+ const commentElements = comments.map(
11671
+ (comment) => buildCommentElement(comment.id, comment.author, comment.content, comment.date, comment.paraId)
11672
+ ).join("\n ");
11673
+ return `<w:comments xmlns:w="${NS_W}">
11674
+ ${commentElements}
11675
+ </w:comments>`;
11676
+ }
11677
+ function buildCommentsExtendedPartXml(entries) {
11678
+ const body = (entries || []).map((entry) => {
11679
+ const parent = entry.paraIdParent ? ` w15:paraIdParent="${escapeXml(entry.paraIdParent)}"` : "";
11680
+ return `<w15:commentEx w15:paraId="${escapeXml(entry.paraId)}"${parent} w15:done="${entry.done ? "1" : "0"}"/>`;
11681
+ }).join("");
11682
+ return `<w15:commentsEx xmlns:w15="${NS_W15}">${body}</w15:commentsEx>`;
11683
+ }
11684
+
11685
+ // services/comment-thread-parts.js
11686
+ var attrOf = (node, qualified, local) => node?.getAttribute?.(qualified) || node?.getAttribute?.(local) || "";
11687
+ function commentThreadParagraph(commentElement) {
11688
+ const paragraphs = Array.from(commentElement?.getElementsByTagNameNS?.("*", "p") || []);
11689
+ return paragraphs.length ? paragraphs[paragraphs.length - 1] : null;
11690
+ }
11691
+ function commentParaId(commentElement) {
11692
+ return attrOf(commentThreadParagraph(commentElement), "w14:paraId", "paraId").toUpperCase();
11693
+ }
11694
+ function usedParaIds(commentsDoc, extendedDoc) {
11695
+ const ids = /* @__PURE__ */ new Set();
11696
+ for (const p of Array.from(commentsDoc?.getElementsByTagNameNS("*", "p") || [])) {
11697
+ const id = attrOf(p, "w14:paraId", "paraId");
11698
+ if (id) ids.add(id.toUpperCase());
11699
+ }
11700
+ for (const ex of Array.from(extendedDoc?.getElementsByTagNameNS("*", "commentEx") || [])) {
11701
+ const id = attrOf(ex, "w15:paraId", "paraId");
11702
+ if (id) ids.add(id.toUpperCase());
11703
+ }
11704
+ return ids;
11705
+ }
11706
+ function allocateParaId(commentId, occupied) {
11707
+ let candidate = createCommentParaId(commentId);
11708
+ let value = Number.parseInt(candidate, 16) >>> 0;
11709
+ while (occupied.has(candidate)) {
11710
+ value = value + 1 >>> 0;
11711
+ candidate = value.toString(16).toUpperCase().padStart(8, "0");
11712
+ }
11713
+ occupied.add(candidate);
11714
+ return candidate;
11715
+ }
11716
+ var parseDoc = (xml, partName) => {
11717
+ const parsed = parseOoxmlSafe(xml, "application/xml");
11718
+ if (!parsed.doc || parsed.error) return { error: { code: "PARSE_ERROR", message: `Could not parse ${partName}: ${parsed.error?.message || "invalid XML"}` } };
11719
+ return { doc: parsed.doc };
11720
+ };
11721
+ function applyThreadResolutionToParts({ commentsXml, commentsExtendedXml = null, commentId, resolved = true }) {
11722
+ if (!commentsXml) return { status: "error", error: { code: "COMMENTS_PART_MISSING", message: "Resolving a comment requires an existing word/comments.xml part." } };
11723
+ const commentsParsed = parseDoc(commentsXml, "word/comments.xml");
11724
+ if (commentsParsed.error) return { status: "error", error: commentsParsed.error };
11725
+ const commentsDoc = commentsParsed.doc;
11726
+ const idOf = (node) => attrOf(node, "w:id", "id");
11727
+ const comments = Array.from(commentsDoc.getElementsByTagNameNS("*", "comment"));
11728
+ const target = comments.find((node) => idOf(node) === String(commentId));
11729
+ if (!target) return { status: "error", error: { code: "COMMENT_NOT_FOUND", message: `Comment '${commentId}' was not found.` } };
11730
+ let extendedDoc;
11731
+ if (commentsExtendedXml) {
11732
+ const parsed = parseDoc(commentsExtendedXml, "word/commentsExtended.xml");
11733
+ if (parsed.error) return { status: "error", error: parsed.error };
11734
+ extendedDoc = parsed.doc;
11735
+ } else {
11736
+ extendedDoc = parseDoc(`<w15:commentsEx xmlns:w15="${NS_W15}"/>`, "word/commentsExtended.xml").doc;
11737
+ }
11738
+ const root = extendedDoc.documentElement;
11739
+ const entryFor = (paraId) => Array.from(root.getElementsByTagNameNS("*", "commentEx")).find((node) => attrOf(node, "w15:paraId", "paraId").toUpperCase() === paraId) || null;
11740
+ const occupied = usedParaIds(commentsDoc, extendedDoc);
11741
+ let commentsChanged = false;
11742
+ const keyOf = (node) => {
11743
+ let paraId = commentParaId(node);
11744
+ if (!paraId) {
11745
+ paraId = allocateParaId(idOf(node), occupied);
11746
+ commentThreadParagraph(node)?.setAttributeNS(NS_W14, "w14:paraId", paraId);
11747
+ commentsChanged = true;
11748
+ }
11749
+ return paraId;
11750
+ };
11751
+ const targetKey = keyOf(target);
11752
+ const targetEntry = entryFor(targetKey);
11753
+ const rootKey = targetEntry && attrOf(targetEntry, "w15:paraIdParent", "paraIdParent").toUpperCase() || targetKey;
11754
+ const threadKeys = /* @__PURE__ */ new Set([rootKey, targetKey]);
11755
+ for (const entry of Array.from(root.getElementsByTagNameNS("*", "commentEx"))) {
11756
+ if (attrOf(entry, "w15:paraIdParent", "paraIdParent").toUpperCase() === rootKey) threadKeys.add(attrOf(entry, "w15:paraId", "paraId").toUpperCase());
11757
+ }
11758
+ const doneValue = resolved ? "1" : "0";
11759
+ let extendedChanged = !commentsExtendedXml;
11760
+ for (const key of threadKeys) {
11761
+ let entry = entryFor(key);
11762
+ if (!entry) {
11763
+ entry = extendedDoc.createElementNS(NS_W15, "w15:commentEx");
11764
+ entry.setAttributeNS(NS_W15, "w15:paraId", key);
11765
+ if (key !== rootKey) entry.setAttributeNS(NS_W15, "w15:paraIdParent", rootKey);
11766
+ entry.setAttributeNS(NS_W15, "w15:done", doneValue);
11767
+ root.appendChild(entry);
11768
+ extendedChanged = true;
11769
+ } else if (attrOf(entry, "w15:done", "done") !== doneValue) {
11770
+ entry.setAttributeNS(NS_W15, "w15:done", doneValue);
11771
+ extendedChanged = true;
11772
+ }
11773
+ }
11774
+ const byKey = new Map(comments.map((node) => [commentParaId(node), node]));
11775
+ const commentIds = [...threadKeys].map((key) => byKey.get(key)).filter(Boolean).map(idOf);
11776
+ const rootComment = byKey.get(rootKey);
11777
+ const serializer = createSerializer();
11778
+ const hasChanges = extendedChanged || commentsChanged;
11779
+ return {
11780
+ status: "ok",
11781
+ hasChanges,
11782
+ resolved,
11783
+ threadRootId: rootComment ? idOf(rootComment) : String(commentId),
11784
+ commentIds,
11785
+ ...hasChanges ? { commentsExtendedXml: serializer.serializeToString(extendedDoc) } : {},
11786
+ ...commentsChanged ? { commentsXml: serializer.serializeToString(commentsDoc) } : {}
11787
+ };
11788
+ }
11789
+ var NS_W_MAIN = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
11790
+ function anchorReplyInDocument(xmlDoc, memberIds, newId) {
11791
+ const wanted = new Set(memberIds.map(String));
11792
+ const idOf = (node) => attrOf(node, "w:id", "id");
11793
+ const all = (name) => Array.from(xmlDoc.getElementsByTagNameNS("*", name)).filter((node) => wanted.has(idOf(node)));
11794
+ const starts = all("commentRangeStart");
11795
+ const references = all("commentReference");
11796
+ if (!starts.length || !references.length) return false;
11797
+ const lastStart = starts[starts.length - 1];
11798
+ const lastReference = references[references.length - 1];
11799
+ const referenceRun = lastReference.parentNode?.localName === "r" ? lastReference.parentNode : lastReference;
11800
+ const make = (name, parent) => {
11801
+ const element = xmlDoc.createElementNS(NS_W_MAIN, `w:${name}`);
11802
+ element.setAttributeNS(NS_W_MAIN, "w:id", String(newId));
11803
+ if (parent) parent.appendChild(element);
11804
+ return element;
11805
+ };
11806
+ const insertAfter = (anchor, node) => anchor.parentNode.insertBefore(node, anchor.nextSibling);
11807
+ insertAfter(lastStart, make("commentRangeStart"));
11808
+ const end = make("commentRangeEnd");
11809
+ const run = xmlDoc.createElementNS(NS_W_MAIN, "w:r");
11810
+ run.appendChild(xmlDoc.createElementNS(NS_W_MAIN, "w:rPr"));
11811
+ make("commentReference", run);
11812
+ insertAfter(referenceRun, end);
11813
+ insertAfter(end, run);
11814
+ return true;
11815
+ }
11816
+ var NS_W16CID = "http://schemas.microsoft.com/office/word/2016/wordml/cid";
11817
+ var NS_W16CEX = "http://schemas.microsoft.com/office/word/2018/wordml/cex";
11818
+ var durableIdFor = (paraId, taken) => {
11819
+ let value = (Number.parseInt(paraId, 16) ^ 625341585) & 2147483646;
11820
+ let candidate = value.toString(16).toUpperCase().padStart(8, "0");
11821
+ while (taken.has(candidate)) {
11822
+ value = value + 2 & 2147483646;
11823
+ candidate = value.toString(16).toUpperCase().padStart(8, "0");
11824
+ }
11825
+ taken.add(candidate);
11826
+ return candidate;
11827
+ };
11828
+ function reconcileCommentSiblingParts({ commentsXml, commentsIdsXml = null, commentsExtensibleXml = null }) {
11829
+ const out = {};
11830
+ if (!commentsXml || !commentsIdsXml && !commentsExtensibleXml) return out;
11831
+ const commentsParsed = parseOoxmlSafe(commentsXml, "application/xml");
11832
+ if (!commentsParsed.doc || commentsParsed.error) return out;
11833
+ const serializer = createSerializer();
11834
+ const byKey = /* @__PURE__ */ new Map();
11835
+ for (const node of Array.from(commentsParsed.doc.getElementsByTagNameNS("*", "comment"))) {
11836
+ const key = commentParaId(node);
11837
+ if (key) byKey.set(key, node);
11838
+ }
11839
+ let idsDoc = null;
11840
+ const durableByKey = /* @__PURE__ */ new Map();
11841
+ const taken = /* @__PURE__ */ new Set();
11842
+ let idsChanged = false;
11843
+ if (commentsIdsXml) {
11844
+ const parsed = parseOoxmlSafe(commentsIdsXml, "application/xml");
11845
+ if (!parsed.doc || parsed.error) return out;
11846
+ idsDoc = parsed.doc;
11847
+ const root = idsDoc.documentElement;
11848
+ for (const entry of Array.from(root.getElementsByTagNameNS("*", "commentId"))) {
11849
+ const key = attrOf(entry, "w16cid:paraId", "paraId").toUpperCase();
11850
+ const durable = attrOf(entry, "w16cid:durableId", "durableId").toUpperCase();
11851
+ if (durable) taken.add(durable);
11852
+ if (!byKey.has(key)) {
11853
+ root.removeChild(entry);
11854
+ idsChanged = true;
11855
+ continue;
11856
+ }
11857
+ durableByKey.set(key, durable);
11858
+ }
11859
+ for (const key of byKey.keys()) {
11860
+ if (durableByKey.has(key)) continue;
11861
+ const durable = durableIdFor(key, taken);
11862
+ const entry = idsDoc.createElementNS(NS_W16CID, "w16cid:commentId");
11863
+ entry.setAttributeNS(NS_W16CID, "w16cid:paraId", key);
11864
+ entry.setAttributeNS(NS_W16CID, "w16cid:durableId", durable);
11865
+ root.appendChild(entry);
11866
+ durableByKey.set(key, durable);
11867
+ idsChanged = true;
11868
+ }
11869
+ }
11870
+ let extDoc = null;
11871
+ let extChanged = false;
11872
+ if (commentsExtensibleXml && idsDoc) {
11873
+ const parsed = parseOoxmlSafe(commentsExtensibleXml, "application/xml");
11874
+ if (parsed.doc && !parsed.error) {
11875
+ extDoc = parsed.doc;
11876
+ const root = extDoc.documentElement;
11877
+ const live = new Set(durableByKey.values());
11878
+ const present = /* @__PURE__ */ new Set();
11879
+ for (const entry of Array.from(root.getElementsByTagNameNS("*", "commentExtensible"))) {
11880
+ const durable = attrOf(entry, "w16cex:durableId", "durableId").toUpperCase();
11881
+ if (!live.has(durable)) {
11882
+ root.removeChild(entry);
11883
+ extChanged = true;
11884
+ } else present.add(durable);
11885
+ }
11886
+ for (const [key, durable] of durableByKey) {
11887
+ if (present.has(durable)) continue;
11888
+ const date = attrOf(byKey.get(key), "w:date", "date");
11889
+ const entry = extDoc.createElementNS(NS_W16CEX, "w16cex:commentExtensible");
11890
+ entry.setAttributeNS(NS_W16CEX, "w16cex:durableId", durable);
11891
+ if (date) entry.setAttributeNS(NS_W16CEX, "w16cex:dateUtc", date);
11892
+ root.appendChild(entry);
11893
+ extChanged = true;
11894
+ }
11895
+ }
11896
+ }
11897
+ if (idsChanged) out.commentsIdsXml = serializer.serializeToString(idsDoc);
11898
+ if (extChanged) out.commentsExtensibleXml = serializer.serializeToString(extDoc);
11899
+ return out;
11900
+ }
11901
+
11514
11902
  // core/sha256.js
11515
11903
  var K = new Uint32Array([
11516
11904
  1116352408,
@@ -12003,7 +12391,7 @@ function readCommentDefinitions(commentsDoc) {
12003
12391
  author: attr2(comment, "author") || null,
12004
12392
  date: attr2(comment, "date") || null,
12005
12393
  text: paragraphs.map((p) => extractCanonicalParagraphText(p)).join("\n"),
12006
- paraId: paragraphs[0]?.getAttribute?.("w14:paraId") || paragraphs[0]?.getAttribute?.("paraId") || null
12394
+ paraId: commentParaId(comment) || null
12007
12395
  });
12008
12396
  }
12009
12397
  return result;
@@ -12234,43 +12622,6 @@ function inspectDocumentParts(parts, options = {}) {
12234
12622
  };
12235
12623
  }
12236
12624
 
12237
- // services/comment-builders.js
12238
- var NS_W14 = "http://schemas.microsoft.com/office/word/2010/wordml";
12239
- var NS_W15 = "http://schemas.microsoft.com/office/word/2012/wordml";
12240
- function createCommentParaId(commentId) {
12241
- const numeric = Number.parseInt(String(commentId), 10);
12242
- const value = Number.isFinite(numeric) ? 1879048192 + (numeric >>> 0) >>> 0 : 1879048192;
12243
- return value.toString(16).toUpperCase().padStart(8, "0").slice(-8);
12244
- }
12245
- function buildCommentElement(commentId, author, content, date, paraId = createCommentParaId(commentId)) {
12246
- const initials = author.split(" ").map((word) => word[0]).join("").toUpperCase() || "AI";
12247
- const escapedContent = escapeXml(content);
12248
- const escapedAuthor = escapeXml(author);
12249
- return `<w:comment w:id="${commentId}" w:author="${escapedAuthor}" w:date="${date}" w:initials="${initials}">
12250
- <w:p w14:paraId="${escapeXml(paraId)}" xmlns:w14="${NS_W14}">
12251
- <w:r><w:t>${escapedContent}</w:t></w:r>
12252
- </w:p>
12253
- </w:comment>`;
12254
- }
12255
- function buildCommentsPartXml(comments) {
12256
- if (!comments || comments.length === 0) {
12257
- return `<w:comments xmlns:w="${NS_W}"></w:comments>`;
12258
- }
12259
- const commentElements = comments.map(
12260
- (comment) => buildCommentElement(comment.id, comment.author, comment.content, comment.date, comment.paraId)
12261
- ).join("\n ");
12262
- return `<w:comments xmlns:w="${NS_W}">
12263
- ${commentElements}
12264
- </w:comments>`;
12265
- }
12266
- function buildCommentsExtendedPartXml(entries) {
12267
- const body = (entries || []).map((entry) => {
12268
- const parent = entry.paraIdParent ? ` w15:paraIdParent="${escapeXml(entry.paraIdParent)}"` : "";
12269
- return `<w15:commentEx w15:paraId="${escapeXml(entry.paraId)}"${parent} w15:done="${entry.done ? "1" : "0"}"/>`;
12270
- }).join("");
12271
- return `<w15:commentsEx xmlns:w15="${NS_W15}">${body}</w15:commentsEx>`;
12272
- }
12273
-
12274
12625
  // services/comment-locator.js
12275
12626
  function createParagraphTextIndex(paragraph, options = {}) {
12276
12627
  const revisionView = options.revisionView === "current" ? "accepted" : options.revisionView || "accepted";
@@ -12731,29 +13082,7 @@ function parseRequired(xml, partName) {
12731
13082
  }
12732
13083
  return { doc: parsed.doc };
12733
13084
  }
12734
- function usedParaIds(commentsDoc, extendedDoc) {
12735
- const ids = /* @__PURE__ */ new Set();
12736
- for (const p of Array.from(commentsDoc?.getElementsByTagNameNS("*", "p") || [])) {
12737
- const id = attr3(p, "w14:paraId", "paraId");
12738
- if (id) ids.add(id.toUpperCase());
12739
- }
12740
- for (const ex of Array.from(extendedDoc?.getElementsByTagNameNS("*", "commentEx") || [])) {
12741
- const id = attr3(ex, "w15:paraId", "paraId");
12742
- if (id) ids.add(id.toUpperCase());
12743
- }
12744
- return ids;
12745
- }
12746
- function allocateParaId(commentId, occupied) {
12747
- let candidate = createCommentParaId(commentId);
12748
- let value = Number.parseInt(candidate, 16) >>> 0;
12749
- while (occupied.has(candidate)) {
12750
- value = value + 1 >>> 0;
12751
- candidate = value.toString(16).toUpperCase().padStart(8, "0");
12752
- }
12753
- occupied.add(candidate);
12754
- return candidate;
12755
- }
12756
- function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, parentCommentId, commentId, commentContent, author, date = (/* @__PURE__ */ new Date()).toISOString() }) {
13085
+ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, documentXml = null, parentCommentId, commentId, commentContent, author, date = (/* @__PURE__ */ new Date()).toISOString() }) {
12757
13086
  if (!commentsXml) return { status: "error", error: { code: "COMMENTS_PART_MISSING", message: "A comment reply requires an existing word/comments.xml part." } };
12758
13087
  const commentsParsed = parseRequired(commentsXml, "word/comments.xml");
12759
13088
  if (commentsParsed.error) return { status: "error", error: commentsParsed.error };
@@ -12767,15 +13096,31 @@ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, par
12767
13096
  extendedDoc = parsed.doc;
12768
13097
  }
12769
13098
  const occupied = usedParaIds(commentsDoc, extendedDoc);
12770
- const parentParagraph = Array.from(parent.getElementsByTagNameNS(NS_W6, "p"))[0] || Array.from(parent.getElementsByTagNameNS("*", "p"))[0];
13099
+ if (documentXml) for (const match of String(documentXml).matchAll(/w14:paraId="([0-9A-Fa-f]{8})"/g)) occupied.add(match[1].toUpperCase());
13100
+ const parentParagraph = commentThreadParagraph(parent);
12771
13101
  if (!parentParagraph) return { status: "error", error: { code: "PARENT_COMMENT_INVALID", message: `Parent comment '${parentCommentId}' has no paragraph.` } };
12772
- let parentParaId = attr3(parentParagraph, "w14:paraId", "paraId");
13102
+ let parentParaId = commentParaId(parent);
12773
13103
  if (!parentParaId) {
12774
13104
  parentParaId = allocateParaId(parentCommentId, occupied);
12775
13105
  parentParagraph.setAttributeNS(NS_W14, "w14:paraId", parentParaId);
12776
13106
  } else {
12777
13107
  parentParaId = parentParaId.toUpperCase();
12778
13108
  }
13109
+ for (const node of Array.from(extendedDoc?.getElementsByTagNameNS("*", "commentEx") || [])) {
13110
+ if (attr3(node, "w15:paraId", "paraId").toUpperCase() === parentParaId) {
13111
+ const root2 = attr3(node, "w15:paraIdParent", "paraIdParent").toUpperCase();
13112
+ if (root2) parentParaId = root2;
13113
+ break;
13114
+ }
13115
+ }
13116
+ const threadCommentIds = [];
13117
+ const commentNodes = Array.from(commentsDoc.getElementsByTagNameNS("*", "comment"));
13118
+ const parentOf = new Map(Array.from(extendedDoc?.getElementsByTagNameNS("*", "commentEx") || []).map((node) => [attr3(node, "w15:paraId", "paraId").toUpperCase(), attr3(node, "w15:paraIdParent", "paraIdParent").toUpperCase()]));
13119
+ for (const node of commentNodes) {
13120
+ const key = commentParaId(node);
13121
+ if (key === parentParaId || parentOf.get(key) === parentParaId) threadCommentIds.push(attr3(node, "w:id", "id"));
13122
+ }
13123
+ const threadRootCommentId = attr3(commentNodes.find((node) => commentParaId(node) === parentParaId), "w:id", "id") || String(parentCommentId);
12779
13124
  const replyParaId = allocateParaId(commentId, occupied);
12780
13125
  const replyParsed = parseRequired(`<w:comments xmlns:w="${NS_W6}" xmlns:w14="${NS_W14}">${buildCommentElement(commentId, author, commentContent, date, replyParaId)}</w:comments>`, "reply comment");
12781
13126
  commentsDoc.documentElement.appendChild(commentsDoc.importNode(replyParsed.doc.documentElement.firstChild, true));
@@ -12784,16 +13129,18 @@ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, par
12784
13129
  }
12785
13130
  const root = extendedDoc.documentElement;
12786
13131
  const entries = Array.from(root.getElementsByTagNameNS("*", "commentEx"));
12787
- if (!entries.some((node) => attr3(node, "w15:paraId", "paraId").toUpperCase() === parentParaId)) {
13132
+ const rootEntry = entries.find((node) => attr3(node, "w15:paraId", "paraId").toUpperCase() === parentParaId);
13133
+ if (!rootEntry) {
12788
13134
  const parentEx = extendedDoc.createElementNS(NS_W15, "w15:commentEx");
12789
13135
  parentEx.setAttributeNS(NS_W15, "w15:paraId", parentParaId);
12790
13136
  parentEx.setAttributeNS(NS_W15, "w15:done", "0");
12791
13137
  root.appendChild(parentEx);
12792
13138
  }
13139
+ const threadDone = attr3(rootEntry, "w15:done", "done") === "1" ? "1" : "0";
12793
13140
  const replyEx = extendedDoc.createElementNS(NS_W15, "w15:commentEx");
12794
13141
  replyEx.setAttributeNS(NS_W15, "w15:paraId", replyParaId);
12795
13142
  replyEx.setAttributeNS(NS_W15, "w15:paraIdParent", parentParaId);
12796
- replyEx.setAttributeNS(NS_W15, "w15:done", "0");
13143
+ replyEx.setAttributeNS(NS_W15, "w15:done", threadDone);
12797
13144
  root.appendChild(replyEx);
12798
13145
  const serializer = createSerializer();
12799
13146
  return {
@@ -12806,7 +13153,9 @@ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, par
12806
13153
  commentId,
12807
13154
  parentCommentId: String(parentCommentId),
12808
13155
  paraId: replyParaId,
12809
- parentParaId
13156
+ parentParaId,
13157
+ threadRootCommentId,
13158
+ threadCommentIds: threadCommentIds.length ? threadCommentIds : [threadRootCommentId]
12810
13159
  };
12811
13160
  }
12812
13161
 
@@ -12871,19 +13220,19 @@ function applyFormattingRemovalToOoxml(ooxmlString, targetText, formatTypes) {
12871
13220
  if (!targetText || !ooxmlString) return ooxmlString;
12872
13221
  const doc = parseOoxml(ooxmlString);
12873
13222
  if (!doc) return ooxmlString;
12874
- const NS_W10 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
12875
- const runs = doc.getElementsByTagNameNS(NS_W10, "r");
12876
- const insertions = doc.getElementsByTagNameNS(NS_W10, "ins");
13223
+ const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
13224
+ const runs = doc.getElementsByTagNameNS(NS_W11, "r");
13225
+ const insertions = doc.getElementsByTagNameNS(NS_W11, "ins");
12877
13226
  const allRuns = [...Array.from(runs)];
12878
13227
  for (const ins of insertions) {
12879
- const insideRuns = ins.getElementsByTagNameNS(NS_W10, "r");
13228
+ const insideRuns = ins.getElementsByTagNameNS(NS_W11, "r");
12880
13229
  allRuns.push(...Array.from(insideRuns));
12881
13230
  }
12882
13231
  for (const run of allRuns) {
12883
- const textNodes = run.getElementsByTagNameNS(NS_W10, "t");
13232
+ const textNodes = run.getElementsByTagNameNS(NS_W11, "t");
12884
13233
  const runText = Array.from(textNodes).map((t) => t.textContent).join("");
12885
13234
  if (runText.includes(targetText) || runText === targetText) {
12886
- const rPrElements = run.getElementsByTagNameNS(NS_W10, "rPr");
13235
+ const rPrElements = run.getElementsByTagNameNS(NS_W11, "rPr");
12887
13236
  if (rPrElements.length > 0) {
12888
13237
  const rPr = rPrElements[0];
12889
13238
  const newRPr = removeFormattingFromRPr(rPr, formatTypes);
@@ -12918,7 +13267,7 @@ var HIGHLIGHT_COLOR_MAP = {
12918
13267
  "white": "white"
12919
13268
  };
12920
13269
  function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
12921
- const NS_W10 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
13270
+ const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
12922
13271
  const ooxmlColor = HIGHLIGHT_COLOR_MAP[color.toLowerCase()] || "yellow";
12923
13272
  const generateRedlines = options?.generateRedlines ?? false;
12924
13273
  const author = options?.author || getDefaultAuthor();
@@ -12937,10 +13286,10 @@ function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
12937
13286
  }
12938
13287
  });
12939
13288
  }
12940
- const existingHighlight = rPrElement.getElementsByTagNameNS(NS_W10, "highlight");
13289
+ const existingHighlight = rPrElement.getElementsByTagNameNS(NS_W11, "highlight");
12941
13290
  Array.from(existingHighlight).forEach(removeNode2);
12942
13291
  const highlightEl = createWordElement(doc, "w:highlight");
12943
- highlightEl.setAttributeNS(NS_W10, "w:val", ooxmlColor);
13292
+ highlightEl.setAttributeNS(NS_W11, "w:val", ooxmlColor);
12944
13293
  rPrElement.appendChild(highlightEl);
12945
13294
  if (generateRedlines && previousRPrState) {
12946
13295
  const rPrChange = createWordElement(doc, "w:rPrChange");
@@ -12949,7 +13298,7 @@ function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
12949
13298
  rPrChange.setAttribute("w:author", metadata.author);
12950
13299
  rPrChange.setAttribute("w:date", metadata.date);
12951
13300
  rPrChange.appendChild(previousRPrState);
12952
- const existingChange = rPrElement.getElementsByTagNameNS(NS_W10, "rPrChange");
13301
+ const existingChange = rPrElement.getElementsByTagNameNS(NS_W11, "rPrChange");
12953
13302
  Array.from(existingChange).forEach(removeNode2);
12954
13303
  rPrElement.appendChild(rPrChange);
12955
13304
  }
@@ -12966,12 +13315,12 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
12966
13315
  } else {
12967
13316
  revisionIdAllocator = createRevisionIdAllocator(doc);
12968
13317
  }
12969
- const NS_W10 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
13318
+ const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
12970
13319
  const getRunText = (run) => {
12971
- const textNodes = run.getElementsByTagNameNS(NS_W10, "t");
13320
+ const textNodes = run.getElementsByTagNameNS(NS_W11, "t");
12972
13321
  return Array.from(textNodes).map((t) => t.textContent).join("");
12973
13322
  };
12974
- const allRuns = Array.from(doc.getElementsByTagNameNS(NS_W10, "r"));
13323
+ const allRuns = Array.from(doc.getElementsByTagNameNS(NS_W11, "r"));
12975
13324
  const sourceRunsWithClaimedRevisionIds = /* @__PURE__ */ new WeakSet();
12976
13325
  const cloneRunWithText2 = (sourceRun, text2, shouldHighlight) => {
12977
13326
  const clonedRun = sourceRun.cloneNode(true);
@@ -12980,14 +13329,14 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
12980
13329
  } else {
12981
13330
  sourceRunsWithClaimedRevisionIds.add(sourceRun);
12982
13331
  }
12983
- const textNodes = clonedRun.getElementsByTagNameNS(NS_W10, "t");
13332
+ const textNodes = clonedRun.getElementsByTagNameNS(NS_W11, "t");
12984
13333
  Array.from(textNodes).forEach(removeNode2);
12985
13334
  const newText = createWordElement(doc, "w:t");
12986
13335
  newText.setAttribute("xml:space", "preserve");
12987
13336
  newText.textContent = text2;
12988
13337
  clonedRun.appendChild(newText);
12989
13338
  if (shouldHighlight) {
12990
- const rPrElements = clonedRun.getElementsByTagNameNS(NS_W10, "rPr");
13339
+ const rPrElements = clonedRun.getElementsByTagNameNS(NS_W11, "rPr");
12991
13340
  const existingRPr = rPrElements.length > 0 ? rPrElements[0] : null;
12992
13341
  const newRPr = injectHighlightIntoRPr(doc, existingRPr, color, options);
12993
13342
  if (existingRPr) {
@@ -13036,20 +13385,68 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
13036
13385
  return serializeOoxml(doc);
13037
13386
  }
13038
13387
 
13388
+ // services/package-parts.js
13389
+ var WORD_ML = "application/vnd.openxmlformats-officedocument.wordprocessingml";
13390
+ var REL_OFFICE_DOC = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
13391
+ var PART_SPECS = Object.freeze([
13392
+ { id: "document", path: "word/document.xml", pathPattern: null, contentType: `${WORD_ML}.document.main+xml`, legacyContentTypes: [], relType: `${REL_OFFICE_DOC}/officeDocument`, relTarget: null, verified: true },
13393
+ { id: "numbering", path: "word/numbering.xml", pathPattern: null, contentType: `${WORD_ML}.numbering+xml`, legacyContentTypes: [], relType: `${REL_OFFICE_DOC}/numbering`, relTarget: "numbering.xml", verified: false },
13394
+ { id: "comments", path: "word/comments.xml", pathPattern: null, contentType: `${WORD_ML}.comments+xml`, legacyContentTypes: [], relType: `${REL_OFFICE_DOC}/comments`, relTarget: "comments.xml", verified: true },
13395
+ {
13396
+ id: "commentsExtended",
13397
+ path: "word/commentsExtended.xml",
13398
+ pathPattern: null,
13399
+ contentType: `${WORD_ML}.commentsExtended+xml`,
13400
+ // Written by this package through 0.8.0; Word does not recognize it and offers to repair the file.
13401
+ legacyContentTypes: ["application/vnd.ms-word.commentsExtended+xml"],
13402
+ relType: "http://schemas.microsoft.com/office/2011/relationships/commentsExtended",
13403
+ relTarget: "commentsExtended.xml",
13404
+ verified: true
13405
+ },
13406
+ { 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 },
13407
+ { 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 },
13408
+ // Not yet produced by any Word fixture (Word only writes people.xml for signed-in authors): unverified.
13409
+ { 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 },
13410
+ { 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 },
13411
+ { 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 }
13412
+ ].map((spec) => Object.freeze(spec)));
13413
+ var BY_ID = new Map(PART_SPECS.map((spec) => [spec.id, spec]));
13414
+ function getPartSpec(id) {
13415
+ const spec = BY_ID.get(id);
13416
+ if (!spec) throw new Error(`Unknown package part: ${id}`);
13417
+ return spec;
13418
+ }
13419
+ function toEntryName(partName) {
13420
+ return String(partName || "").replace(/^\/+/, "");
13421
+ }
13422
+ function findPartSpecByPartName(partName) {
13423
+ const name = toEntryName(partName);
13424
+ const lower = name.toLowerCase();
13425
+ return PART_SPECS.find((spec) => spec.path ? spec.path.toLowerCase() === lower : spec.pathPattern?.test(name)) || null;
13426
+ }
13427
+ function correctedContentTypeFor(partName, actualContentType) {
13428
+ const spec = findPartSpecByPartName(partName);
13429
+ if (!spec || !spec.legacyContentTypes.includes(actualContentType)) return null;
13430
+ return spec.contentType;
13431
+ }
13432
+
13039
13433
  // services/standalone-docx-plumbing.js
13040
13434
  var NS_W7 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
13041
13435
  var NS_CT = "http://schemas.openxmlformats.org/package/2006/content-types";
13042
13436
  var NS_RELS = "http://schemas.openxmlformats.org/package/2006/relationships";
13043
- var NUMBERING_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering";
13044
- var NUMBERING_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml";
13045
- var COMMENTS_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
13046
- var COMMENTS_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml";
13047
- var COMMENTS_EXTENDED_REL_TYPE = "http://schemas.microsoft.com/office/2011/relationships/commentsExtended";
13048
- var COMMENTS_EXTENDED_CONTENT_TYPE = "application/vnd.ms-word.commentsExtended+xml";
13049
- var DOCUMENT_PATH = "word/document.xml";
13050
- var NUMBERING_PATH = "word/numbering.xml";
13051
- var COMMENTS_PATH = "word/comments.xml";
13052
- var COMMENTS_EXTENDED_PATH = "word/commentsExtended.xml";
13437
+ var NUMBERING = getPartSpec("numbering");
13438
+ var COMMENTS = getPartSpec("comments");
13439
+ var COMMENTS_EXTENDED = getPartSpec("commentsExtended");
13440
+ var NUMBERING_REL_TYPE = NUMBERING.relType;
13441
+ var NUMBERING_CONTENT_TYPE = NUMBERING.contentType;
13442
+ var COMMENTS_REL_TYPE = COMMENTS.relType;
13443
+ var COMMENTS_CONTENT_TYPE = COMMENTS.contentType;
13444
+ var COMMENTS_EXTENDED_REL_TYPE = COMMENTS_EXTENDED.relType;
13445
+ var COMMENTS_EXTENDED_CONTENT_TYPE = COMMENTS_EXTENDED.contentType;
13446
+ var DOCUMENT_PATH = getPartSpec("document").path;
13447
+ var NUMBERING_PATH = NUMBERING.path;
13448
+ var COMMENTS_PATH = COMMENTS.path;
13449
+ var COMMENTS_EXTENDED_PATH = COMMENTS_EXTENDED.path;
13053
13450
  var CONTENT_TYPES_PATH = "[Content_Types].xml";
13054
13451
  var DOCUMENT_RELS_PATH = "word/_rels/document.xml.rels";
13055
13452
  function parseXmlStrictStandalone(xmlText, label = "xml") {
@@ -13337,6 +13734,22 @@ async function ensureCommentsExtendedArtifactsInZip(zip, commentsExtendedXml, op
13337
13734
  }
13338
13735
  }
13339
13736
  }
13737
+ async function repairKnownContentTypes(zip) {
13738
+ const ctText = await readZipText(zip, CONTENT_TYPES_PATH);
13739
+ if (!ctText) return [];
13740
+ const ctDoc = parseXmlStrictStandalone(ctText, CONTENT_TYPES_PATH);
13741
+ const repairs = [];
13742
+ for (const override of Array.from(ctDoc.getElementsByTagNameNS("*", "Override"))) {
13743
+ const partName = override.getAttribute("PartName") || "";
13744
+ const actual = override.getAttribute("ContentType") || "";
13745
+ const expected = correctedContentTypeFor(partName, actual);
13746
+ if (!expected) continue;
13747
+ override.setAttribute("ContentType", expected);
13748
+ repairs.push({ partName, from: actual, to: expected });
13749
+ }
13750
+ if (repairs.length > 0) zip.file(CONTENT_TYPES_PATH, createSerializer().serializeToString(ctDoc));
13751
+ return repairs;
13752
+ }
13340
13753
  async function validateDocxPackage(zip) {
13341
13754
  const documentXml = await readZipText(zip, DOCUMENT_PATH);
13342
13755
  if (!documentXml) {
@@ -13418,9 +13831,8 @@ async function validateDocxPackage(zip) {
13418
13831
  const paraIdToCommentId = /* @__PURE__ */ new Map();
13419
13832
  for (const comment of Array.from(commentsDoc.getElementsByTagNameNS("*", "comment"))) {
13420
13833
  const id = comment.getAttribute("w:id") || comment.getAttribute("id");
13421
- const paragraph = Array.from(comment.getElementsByTagNameNS("*", "p"))[0];
13422
- const paraId = paragraph?.getAttribute("w14:paraId") || paragraph?.getAttribute("paraId");
13423
- if (paraId) paraIdToCommentId.set(paraId.toUpperCase(), id);
13834
+ const paraId = commentParaId(comment);
13835
+ if (paraId) paraIdToCommentId.set(paraId, id);
13424
13836
  }
13425
13837
  const knownParaIds = new Set(paraIdToCommentId.keys());
13426
13838
  const extendedParaIds = /* @__PURE__ */ new Set();
@@ -13481,10 +13893,39 @@ async function validateDocxPackage(zip) {
13481
13893
  throw new Error("Validation failed: comments rel missing");
13482
13894
  }
13483
13895
  }
13896
+ {
13897
+ const relationshipById = /* @__PURE__ */ new Map();
13898
+ for (const rel of Array.from(relsDoc.getElementsByTagNameNS("*", "Relationship"))) {
13899
+ relationshipById.set(rel.getAttribute("Id") || "", { type: rel.getAttribute("Type") || "", target: rel.getAttribute("Target") || "" });
13900
+ }
13901
+ const headerSpec = getPartSpec("header");
13902
+ const footerSpec = getPartSpec("footer");
13903
+ const overrideTypeByPart = new Map(Array.from(ctDoc.getElementsByTagNameNS("*", "Override")).map((override) => [(override.getAttribute("PartName") || "").toLowerCase(), override.getAttribute("ContentType") || ""]));
13904
+ for (const reference of [
13905
+ ...Array.from(documentDoc.getElementsByTagNameNS(NS_W7, "headerReference")),
13906
+ ...Array.from(documentDoc.getElementsByTagNameNS(NS_W7, "footerReference"))
13907
+ ]) {
13908
+ const relId = reference.getAttributeNS?.("http://schemas.openxmlformats.org/officeDocument/2006/relationships", "id") || reference.getAttribute("r:id") || "";
13909
+ const spec = reference.localName === "headerReference" ? headerSpec : footerSpec;
13910
+ const relationship = relationshipById.get(relId);
13911
+ if (!relationship) throw new Error(`Validation failed: ${reference.localName} ${relId || "(no r:id)"} has no document relationship`);
13912
+ if (relationship.type !== spec.relType) throw new Error(`Validation failed: ${reference.localName} ${relId} points at a relationship of the wrong type (${relationship.type})`);
13913
+ const partName = relationship.target.startsWith("/") ? relationship.target : `/word/${relationship.target}`;
13914
+ const partXml = await readZipText(zip, partName.slice(1));
13915
+ if (!partXml) throw new Error(`Validation failed: ${reference.localName} ${relId} targets a missing part: ${partName}`);
13916
+ parseXmlStrictStandalone(partXml, partName.slice(1));
13917
+ const contentType = overrideTypeByPart.get(partName.toLowerCase());
13918
+ if (contentType !== spec.contentType) throw new Error(`Validation failed: ${partName} content type is ${contentType ? `'${contentType}'` : "missing"}; Word expects '${spec.contentType}'`);
13919
+ }
13920
+ }
13484
13921
  if (commentsExtendedXml) {
13485
- const hasContentType = Array.from(ctDoc.getElementsByTagNameNS("*", "Override")).some(
13486
- (override) => (override.getAttribute("PartName") || "").toLowerCase() === "/word/commentsextended.xml" && (override.getAttribute("ContentType") || "") === COMMENTS_EXTENDED_CONTENT_TYPE
13922
+ const extendedOverride = Array.from(ctDoc.getElementsByTagNameNS("*", "Override")).find(
13923
+ (override) => (override.getAttribute("PartName") || "").toLowerCase() === "/word/commentsextended.xml"
13487
13924
  );
13925
+ if (extendedOverride && extendedOverride.getAttribute("ContentType") !== COMMENTS_EXTENDED_CONTENT_TYPE) {
13926
+ throw new Error(`Validation failed: commentsExtended CT override has wrong content type '${extendedOverride.getAttribute("ContentType")}' (Word expects '${COMMENTS_EXTENDED_CONTENT_TYPE}')`);
13927
+ }
13928
+ const hasContentType = !!extendedOverride;
13488
13929
  const hasRelationship = Array.from(relsDoc.getElementsByTagNameNS("*", "Relationship")).some(
13489
13930
  (rel) => (rel.getAttribute("Type") || "") === COMMENTS_EXTENDED_REL_TYPE
13490
13931
  );
@@ -13848,6 +14289,156 @@ function reconcileReceiptsAgainstOutput(parts, receipts) {
13848
14289
  return { valid: true };
13849
14290
  }
13850
14291
 
14292
+ // services/headers-footers.js
14293
+ var NS_W8 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
14294
+ var NS_R2 = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
14295
+ var REVISION_ELEMENTS = /* @__PURE__ */ new Set(["ins", "del", "moveFrom", "moveTo", "rPrChange", "pPrChange", "sectPrChange", "tblPrChange", "tblGridChange", "tcPrChange", "trPrChange", "numberingChange"]);
14296
+ var REVISION_CONTENT_ELEMENTS = /* @__PURE__ */ new Set(["ins", "del", "moveFrom", "moveTo"]);
14297
+ var attrOf2 = (node, qualified, local) => node?.getAttribute?.(qualified) || node?.getAttribute?.(local) || "";
14298
+ function resolveTarget(target) {
14299
+ const clean = String(target || "");
14300
+ if (clean.startsWith("/")) return clean.replace(/^\/+/, "");
14301
+ const parts = ["word"];
14302
+ for (const segment of clean.split("/")) {
14303
+ if (segment === "..") parts.pop();
14304
+ else if (segment && segment !== ".") parts.push(segment);
14305
+ }
14306
+ return parts.join("/");
14307
+ }
14308
+ function discoverHeaderFooterParts({ documentXml, relsXml = null, settingsXml = null }) {
14309
+ if (!documentXml || !relsXml) return [];
14310
+ const documentParsed = parseOoxmlSafe(documentXml, "application/xml");
14311
+ const relsParsed = parseOoxmlSafe(relsXml, "application/xml");
14312
+ if (!documentParsed.doc || documentParsed.error || !relsParsed.doc || relsParsed.error) return [];
14313
+ const targetByRelId = /* @__PURE__ */ new Map();
14314
+ for (const rel of Array.from(relsParsed.doc.getElementsByTagNameNS("*", "Relationship"))) {
14315
+ targetByRelId.set(rel.getAttribute("Id") || "", rel.getAttribute("Target") || "");
14316
+ }
14317
+ const evenAndOdd = !!settingsXml && /<w:evenAndOddHeaders\b(?![^>]*w:val="(?:0|false|off)")/.test(settingsXml);
14318
+ const byPath = /* @__PURE__ */ new Map();
14319
+ const inForce = /* @__PURE__ */ new Map();
14320
+ const sectPrs = Array.from(documentParsed.doc.getElementsByTagNameNS(NS_W8, "sectPr"));
14321
+ sectPrs.forEach((sectPr, sectionIndex) => {
14322
+ const titlePg = Array.from(sectPr.childNodes || []).some((child) => child.localName === "titlePg" && !/^(0|false|off)$/i.test(attrOf2(child, "w:val", "val")));
14323
+ for (const ref of Array.from(sectPr.childNodes || [])) {
14324
+ if (ref.nodeType !== 1 || ref.localName !== "headerReference" && ref.localName !== "footerReference") continue;
14325
+ const relId = ref.getAttributeNS?.(NS_R2, "id") || ref.getAttribute("r:id") || "";
14326
+ const target = targetByRelId.get(relId);
14327
+ if (!target) continue;
14328
+ const path = resolveTarget(target);
14329
+ const kind = ref.localName === "headerReference" ? "header" : "footer";
14330
+ const type = attrOf2(ref, "w:type", "type") || "default";
14331
+ const entry = byPath.get(path) || { path, kind, type, sections: [], applies: [], titlePgSections: 0 };
14332
+ if (!entry.sections.includes(sectionIndex)) entry.sections.push(sectionIndex);
14333
+ if (titlePg) entry.titlePgSections += 1;
14334
+ byPath.set(path, entry);
14335
+ inForce.set(`${kind}:${type}`, path);
14336
+ }
14337
+ for (const path of inForce.values()) {
14338
+ const entry = byPath.get(path);
14339
+ if (entry && !entry.applies.includes(sectionIndex)) entry.applies.push(sectionIndex);
14340
+ }
14341
+ });
14342
+ return Array.from(byPath.values()).map((entry) => ({
14343
+ path: entry.path,
14344
+ kind: entry.kind,
14345
+ type: entry.type,
14346
+ sections: entry.sections,
14347
+ appliesToSections: entry.applies,
14348
+ sharedBySections: entry.applies.length > 1,
14349
+ // first-page parts only show with w:titlePg; even parts only with w:evenAndOddHeaders in settings.xml.
14350
+ active: entry.type === "first" ? entry.titlePgSections > 0 : entry.type === "even" ? evenAndOdd : true
14351
+ }));
14352
+ }
14353
+ function resolvePartSelector(parts, selector) {
14354
+ const describe = (part) => ({ path: part.path, kind: part.kind, type: part.type, sections: part.sections, appliesToSections: part.appliesToSections });
14355
+ if (typeof selector === "string") {
14356
+ const wanted = selector.replace(/^\/+/, "").toLowerCase();
14357
+ const match = parts.find((part) => part.path.toLowerCase() === wanted || part.path.toLowerCase() === `word/${wanted}`);
14358
+ if (match) return { part: match };
14359
+ return { error: { code: "PART_NOT_FOUND", message: `No header/footer part '${selector}' is referenced by the document.`, candidates: parts.map(describe) } };
14360
+ }
14361
+ if (!selector || typeof selector !== "object" || selector.kind !== "header" && selector.kind !== "footer") {
14362
+ return { error: { code: "INVALID_OPERATION", message: 'part must be a part path or { kind: "header"|"footer", type?: "default"|"first"|"even", section?: number }.' } };
14363
+ }
14364
+ const type = selector.type || "default";
14365
+ if (!["default", "first", "even"].includes(type)) {
14366
+ return { error: { code: "INVALID_OPERATION", message: `part.type must be default, first or even (got '${type}').` } };
14367
+ }
14368
+ let matches = parts.filter((part) => part.kind === selector.kind && part.type === type);
14369
+ if (Number.isInteger(selector.section)) matches = matches.filter((part) => part.appliesToSections.includes(selector.section));
14370
+ if (matches.length === 1) return { part: matches[0] };
14371
+ if (matches.length === 0) {
14372
+ 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) } };
14373
+ }
14374
+ return { error: { code: "PART_AMBIGUOUS", message: `Several ${type} ${selector.kind} parts exist; add "section" or use the part path.`, candidates: matches.map(describe) } };
14375
+ }
14376
+ function isHeaderFooterPath(path) {
14377
+ const spec = findPartSpecByPartName(path);
14378
+ return spec?.id === "header" || spec?.id === "footer";
14379
+ }
14380
+ function countRevisionsInsideFields(xml) {
14381
+ const parsed = parseOoxmlSafe(xml, "application/xml");
14382
+ if (!parsed.doc || parsed.error) return 0;
14383
+ let depth = 0;
14384
+ let count = 0;
14385
+ const visit = (node) => {
14386
+ for (const child of Array.from(node.childNodes || [])) {
14387
+ if (child.nodeType !== 1) continue;
14388
+ const name = child.localName;
14389
+ if (name === "fldChar") {
14390
+ const type = attrOf2(child, "w:fldCharType", "fldCharType");
14391
+ if (type === "begin") depth += 1;
14392
+ else if (type === "end") depth = Math.max(0, depth - 1);
14393
+ continue;
14394
+ }
14395
+ if (REVISION_CONTENT_ELEMENTS.has(name) && depth > 0) count += 1;
14396
+ if (name === "fldSimple") {
14397
+ depth += 1;
14398
+ visit(child);
14399
+ depth -= 1;
14400
+ continue;
14401
+ }
14402
+ visit(child);
14403
+ }
14404
+ };
14405
+ visit(parsed.doc.documentElement);
14406
+ return count;
14407
+ }
14408
+ function collectRevisionIds(xml) {
14409
+ const ids = /* @__PURE__ */ new Set();
14410
+ 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]);
14411
+ return ids;
14412
+ }
14413
+ function renumberCollidingRevisionIds(xml, taken) {
14414
+ const parsed = parseOoxmlSafe(xml, "application/xml");
14415
+ if (!parsed.doc || parsed.error) return { xml, renumbered: 0, idMap: /* @__PURE__ */ new Map() };
14416
+ const idMap = /* @__PURE__ */ new Map();
14417
+ let next = 1 + Math.max(0, ...Array.from(taken, (id) => Number(id)).filter(Number.isFinite));
14418
+ const used = new Set(taken);
14419
+ let renumbered = 0;
14420
+ const visit = (node) => {
14421
+ for (const child of Array.from(node.childNodes || [])) {
14422
+ if (child.nodeType !== 1) continue;
14423
+ if (REVISION_ELEMENTS.has(child.localName)) {
14424
+ const id = attrOf2(child, "w:id", "id");
14425
+ if (id && used.has(id)) {
14426
+ while (used.has(String(next))) next += 1;
14427
+ child.setAttributeNS(NS_W8, "w:id", String(next));
14428
+ idMap.set(id, String(next));
14429
+ used.add(String(next));
14430
+ renumbered += 1;
14431
+ } else if (id) {
14432
+ used.add(id);
14433
+ }
14434
+ }
14435
+ visit(child);
14436
+ }
14437
+ };
14438
+ visit(parsed.doc.documentElement);
14439
+ return renumbered > 0 ? { xml: createSerializer().serializeToString(parsed.doc), renumbered, idMap } : { xml, renumbered: 0, idMap };
14440
+ }
14441
+
13851
14442
  // services/error-recovery.js
13852
14443
  var ERROR_RECOVERY_VERSION = 1;
13853
14444
  var RULES = Object.freeze({
@@ -13885,6 +14476,12 @@ var RULES = Object.freeze({
13885
14476
  STRUCTURED_CONTENT_INVALID: ["request", "request_fixable", "change_request"],
13886
14477
  UNSUPPORTED_INSERTION_AFFINITY: ["request", "request_fixable", "change_request"],
13887
14478
  EXISTING_REVISIONS: ["target-safety", "policy_choice_required", "set_option"],
14479
+ PART_NOT_FOUND: ["target-resolution", "target_refresh_required", "reinspect"],
14480
+ PART_AMBIGUOUS: ["target-resolution", "candidate_selection_required", "choose_candidate"],
14481
+ COMMENT_IN_HEADER_FOOTER: ["request", "request_fixable", "change_request"],
14482
+ FIELD_EDIT_REFUSED: ["target-safety", "request_fixable", "change_request"],
14483
+ COMMENT_NOT_FOUND: ["target-resolution", "target_refresh_required", "reinspect"],
14484
+ PARENT_ANCHOR_NOT_FOUND: ["target-resolution", "target_refresh_required", "reinspect"],
13888
14485
  COMMENTED_CONTENT_MERGE: ["target-safety", "user_authorization_required", "resolve_comments"],
13889
14486
  COMMENTED_CONTENT_DELETE: ["target-safety", "user_authorization_required", "resolve_comments"],
13890
14487
  OVERLAPPING_SOURCE_TARGETS: ["batch-compilation", "source_conflict", "consolidate_operations"],
@@ -14337,6 +14934,7 @@ var SUPPORTED_OPERATION_TYPES = /* @__PURE__ */ new Set([
14337
14934
  "delete",
14338
14935
  "comment",
14339
14936
  "comment_reply",
14937
+ "comment_resolve",
14340
14938
  "highlight"
14341
14939
  ]);
14342
14940
  var EXISTING_REVISIONS_POLICIES = Object.freeze([
@@ -14355,11 +14953,14 @@ function isRecord(value) {
14355
14953
  function nonEmptyString(value) {
14356
14954
  return typeof value === "string" && value.trim().length > 0;
14357
14955
  }
14956
+ function isCommentThreadOperationKind(kind) {
14957
+ return kind === "comment_reply" || kind === "comment_resolve";
14958
+ }
14358
14959
  function getCanonicalOperationType(operation) {
14359
14960
  const type = operation?.type;
14360
14961
  if (type === "insert" && operation?.target?.revisionView === "rejected") return "rejected-insert";
14361
14962
  if (type === "restore") return "restore";
14362
- if (type === "comment" || type === "comment_reply" || type === "highlight") return type;
14963
+ if (type === "comment" || type === "comment_reply" || type === "comment_resolve" || type === "highlight") return type;
14363
14964
  if (type === "paragraph-format") return "paragraph-format";
14364
14965
  if (type === "character-format" || type === "format" && (operation?.textToFormat != null || operation?.properties != null)) return "format";
14365
14966
  return "redline";
@@ -14479,7 +15080,7 @@ function validateDocumentOperation(operation) {
14479
15080
  };
14480
15081
  }
14481
15082
  const target = normalized.targetDescriptor;
14482
- if (normalized.operationKind !== "comment_reply" && !nonEmptyString(target.text) && target.index == null && !target.paragraphId && !target.fingerprint && !target.captureRef) {
15083
+ if (!isCommentThreadOperationKind(normalized.operationKind) && !nonEmptyString(target.text) && target.index == null && !target.paragraphId && !target.fingerprint && !target.captureRef) {
14483
15084
  return {
14484
15085
  valid: false,
14485
15086
  error: {
@@ -14706,6 +15307,14 @@ function validateDocumentOperation(operation) {
14706
15307
  return { valid: false, error: { code: "INVALID_OPERATION", message: 'Comment reply operations require a non-empty "commentContent" field.' } };
14707
15308
  }
14708
15309
  }
15310
+ if (normalized.operationKind === "comment_resolve") {
15311
+ if (!nonEmptyString(String(normalized.commentId ?? ""))) {
15312
+ return { valid: false, error: { code: "INVALID_OPERATION", message: 'Comment resolve operations require a "commentId".' } };
15313
+ }
15314
+ if (normalized.resolved != null && typeof normalized.resolved !== "boolean") {
15315
+ return { valid: false, error: { code: "INVALID_OPERATION", message: 'Comment resolve "resolved" must be a boolean when provided.' } };
15316
+ }
15317
+ }
14709
15318
  if (normalized.operationKind === "highlight") {
14710
15319
  if (!nonEmptyString(normalized.textToHighlight)) {
14711
15320
  return {
@@ -15185,7 +15794,7 @@ function compileOperationBatch(xmlDoc, operations = [], options = {}) {
15185
15794
  const dynamicOccurrence = operation.targetDescriptor?.occurrence != null && !operation.targetDescriptor?.paragraphId && !operation.targetDescriptor?.fingerprint;
15186
15795
  const compositeTextTarget = typeof operation.targetDescriptor?.text === "string" && /\r|\n/.test(operation.targetDescriptor.text) && !operation.targetEndDescriptor;
15187
15796
  if (dynamicOccurrence || compositeTextTarget) binding.dynamic = true;
15188
- if (validation.valid && operation.operationKind !== "comment_reply" && !operation.targetDescriptor?.captureRef && !binding.dynamic) {
15797
+ if (validation.valid && operation.operationKind !== "comment_reply" && operation.operationKind !== "comment_resolve" && !operation.targetDescriptor?.captureRef && !binding.dynamic) {
15189
15798
  try {
15190
15799
  const start = resolveDescriptor(
15191
15800
  xmlDoc,
@@ -15686,12 +16295,12 @@ function resolveTargetFromCapture(xmlDoc, session, targetDescriptor, _opType = "
15686
16295
  }
15687
16296
 
15688
16297
  // services/document-operation-mutations.js
15689
- var NS_W8 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
16298
+ var NS_W9 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
15690
16299
  var NS_W142 = "http://schemas.microsoft.com/office/word/2010/wordml";
15691
16300
  function getCommentIdsInElement(element) {
15692
16301
  const ids = /* @__PURE__ */ new Set();
15693
16302
  for (const localName2 of ["commentRangeStart", "commentRangeEnd", "commentReference"]) {
15694
- for (const node of Array.from(element?.getElementsByTagNameNS?.(NS_W8, localName2) || [])) {
16303
+ for (const node of Array.from(element?.getElementsByTagNameNS?.(NS_W9, localName2) || [])) {
15695
16304
  const id = node.getAttribute("w:id") || node.getAttribute("id");
15696
16305
  if (id !== "") ids.add(id);
15697
16306
  }
@@ -15905,7 +16514,7 @@ function normalizeBodySectionOrder(xmlDoc) {
15905
16514
  normalizeBodySectionOrderStandalone(xmlDoc);
15906
16515
  }
15907
16516
  function removeProofErrNodes(paragraph) {
15908
- for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W8, "proofErr") || [])) {
16517
+ for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W9, "proofErr") || [])) {
15909
16518
  node.parentNode?.removeChild(node);
15910
16519
  }
15911
16520
  }
@@ -15916,12 +16525,12 @@ function preprocessRedlineTargetParagraph(targetParagraph) {
15916
16525
  function getDirectWordChild4(element, localName2) {
15917
16526
  if (!element) return null;
15918
16527
  return Array.from(element.childNodes || []).find(
15919
- (node) => node && node.nodeType === 1 && node.namespaceURI === NS_W8 && node.localName === localName2
16528
+ (node) => node && node.nodeType === 1 && node.namespaceURI === NS_W9 && node.localName === localName2
15920
16529
  ) || null;
15921
16530
  }
15922
16531
  function computeTableIndexInDocument(xmlDoc, tableElement) {
15923
16532
  if (!xmlDoc || !tableElement) return null;
15924
- const tables = Array.from(xmlDoc.getElementsByTagNameNS(NS_W8, "tbl"));
16533
+ const tables = Array.from(xmlDoc.getElementsByTagNameNS(NS_W9, "tbl"));
15925
16534
  const idx = tables.indexOf(tableElement);
15926
16535
  return idx >= 0 ? idx + 1 : null;
15927
16536
  }
@@ -15971,7 +16580,7 @@ function buildInsertedListParagraph(xmlDoc, anchorParagraph, entry, revisionMeta
15971
16580
  markParagraphMarkInserted(xmlDoc, paragraph, author);
15972
16581
  }
15973
16582
  const run = createWordElement(xmlDoc, "w:r");
15974
- const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W8, "r"))[0] || null;
16583
+ const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W9, "r"))[0] || null;
15975
16584
  const anchorRunPr = anchorFirstRun ? getDirectWordChild4(anchorFirstRun, "rPr") : null;
15976
16585
  if (anchorRunPr) {
15977
16586
  run.appendChild(clonePropertiesWithoutRevisionHistory(anchorRunPr));
@@ -15996,7 +16605,7 @@ function buildInsertedListParagraph(xmlDoc, anchorParagraph, entry, revisionMeta
15996
16605
  }
15997
16606
  function serializeParagraphRangeAsDocument(paragraphs, serializer) {
15998
16607
  const paragraphXml = (paragraphs || []).map((paragraph) => serializer.serializeToString(paragraph)).join("");
15999
- return `<w:document xmlns:w="${NS_W8}"><w:body>${paragraphXml}</w:body></w:document>`;
16608
+ return `<w:document xmlns:w="${NS_W9}"><w:body>${paragraphXml}</w:body></w:document>`;
16000
16609
  }
16001
16610
  function applyExplicitRangeListInsertions({
16002
16611
  xmlDoc,
@@ -16065,7 +16674,7 @@ function buildEmptyParagraphTemplateFromAnchor(xmlDoc, anchorParagraph) {
16065
16674
  const anchorPPr = getDirectWordChild4(anchorParagraph, "pPr");
16066
16675
  if (anchorPPr) paragraph.appendChild(clonePropertiesWithoutRevisionHistory(anchorPPr));
16067
16676
  const run = createWordElement(xmlDoc, "w:r");
16068
- const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W8, "r"))[0] || null;
16677
+ const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W9, "r"))[0] || null;
16069
16678
  const anchorRunPr = anchorFirstRun ? getDirectWordChild4(anchorFirstRun, "rPr") : null;
16070
16679
  if (anchorRunPr) run.appendChild(clonePropertiesWithoutRevisionHistory(anchorRunPr));
16071
16680
  const textNode = createWordElement(xmlDoc, "w:t");
@@ -16088,7 +16697,7 @@ function wrapParagraphContentInInsertion(xmlDoc, paragraph, revisionMetadata, au
16088
16697
  ins.setAttribute("w:author", metadata.author);
16089
16698
  ins.setAttribute("w:date", metadata.date);
16090
16699
  for (const child of Array.from(paragraph.childNodes || [])) {
16091
- if (child?.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName === "pPr") continue;
16700
+ if (child?.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName === "pPr") continue;
16092
16701
  ins.appendChild(child.cloneNode(true));
16093
16702
  }
16094
16703
  wrappedParagraph.appendChild(ins);
@@ -16112,7 +16721,7 @@ async function buildInsertedPlainParagraph(xmlDoc, anchorParagraph, text2, revis
16112
16721
  if (typeof markdownResult?.oxml === "string") {
16113
16722
  const extracted = extractReplacementNodes(markdownResult.oxml);
16114
16723
  sourceParagraph = (extracted.replacementNodes || []).find(
16115
- (node) => node && node.nodeType === 1 && node.namespaceURI === NS_W8 && node.localName === "p"
16724
+ (node) => node && node.nodeType === 1 && node.namespaceURI === NS_W9 && node.localName === "p"
16116
16725
  ) || null;
16117
16726
  }
16118
16727
  if (!sourceParagraph) {
@@ -16144,7 +16753,7 @@ function collectNumberingIdsFromNodes(nodes) {
16144
16753
  }
16145
16754
  function directDeletionCarrierText(carrier) {
16146
16755
  return Array.from(carrier?.childNodes || []).reduce((text2, child) => {
16147
- return text2 + (child?.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName === "r" ? getRunContentPieces(child).map((piece) => piece.text).join("") : "");
16756
+ return text2 + (child?.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName === "r" ? getRunContentPieces(child).map((piece) => piece.text).join("") : "");
16148
16757
  }, "");
16149
16758
  }
16150
16759
  function findOccurrenceOffset(text2, needle, occurrence) {
@@ -16186,9 +16795,9 @@ function hasUnsupportedDeletionSplitMarkup(paragraph, carrier) {
16186
16795
  }
16187
16796
  for (const child of Array.from(carrier?.childNodes || [])) {
16188
16797
  if (child.nodeType !== 1) continue;
16189
- if (!(child.namespaceURI === NS_W8 && child.localName === "r")) return true;
16798
+ if (!(child.namespaceURI === NS_W9 && child.localName === "r")) return true;
16190
16799
  for (const runChild of Array.from(child.childNodes || [])) {
16191
- if (runChild.nodeType !== 1 || runChild.namespaceURI !== NS_W8) continue;
16800
+ if (runChild.nodeType !== 1 || runChild.namespaceURI !== NS_W9) continue;
16192
16801
  if (!["rPr", "delText", "t", "tab", "br", "cr", "noBreakHyphen", "softHyphen"].includes(runChild.localName)) return true;
16193
16802
  }
16194
16803
  }
@@ -16197,7 +16806,7 @@ function hasUnsupportedDeletionSplitMarkup(paragraph, carrier) {
16197
16806
  function insertedRunPropertiesAtDeletionOffset(carrier, localOffset) {
16198
16807
  let offset = 0;
16199
16808
  for (const child of Array.from(carrier?.childNodes || [])) {
16200
- if (!(child?.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName === "r")) continue;
16809
+ if (!(child?.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName === "r")) continue;
16201
16810
  const length = getRunTextLength(getRunContentPieces(child));
16202
16811
  if (localOffset <= offset + length) {
16203
16812
  return clonePropertiesWithoutRevisionHistory(getDirectWordChild4(child, "rPr"));
@@ -16227,7 +16836,7 @@ function normalizedAuthor2(author) {
16227
16836
  function directWordParagraphSibling(paragraph, direction = "nextSibling") {
16228
16837
  let cursor = paragraph?.[direction] || null;
16229
16838
  while (cursor) {
16230
- if (cursor.nodeType === 1 && cursor.namespaceURI === NS_W8 && cursor.localName === "p") return cursor;
16839
+ if (cursor.nodeType === 1 && cursor.namespaceURI === NS_W9 && cursor.localName === "p") return cursor;
16231
16840
  cursor = cursor[direction] || null;
16232
16841
  }
16233
16842
  return null;
@@ -16237,7 +16846,7 @@ function createSanitizedRestorationPPr(xmlDoc, sourceParagraph) {
16237
16846
  const pPr = createWordElement(xmlDoc, "w:pPr");
16238
16847
  if (!sourcePPr) return pPr;
16239
16848
  for (const child of Array.from(sourcePPr.childNodes || [])) {
16240
- if (child.nodeType !== 1 || child.namespaceURI !== NS_W8) continue;
16849
+ if (child.nodeType !== 1 || child.namespaceURI !== NS_W9) continue;
16241
16850
  if (!RESTORATION_PPR_ALLOWLIST.has(child.localName)) continue;
16242
16851
  pPr.appendChild(xmlDoc.importNode(child, true));
16243
16852
  }
@@ -16246,14 +16855,14 @@ function createSanitizedRestorationPPr(xmlDoc, sourceParagraph) {
16246
16855
  function collectRestorationAnchorWarnings(sourceParagraph) {
16247
16856
  const warnings = [];
16248
16857
  const bookmarkNames = /* @__PURE__ */ new Set();
16249
- for (const node of Array.from(sourceParagraph?.getElementsByTagNameNS?.(NS_W8, "bookmarkStart") || [])) {
16858
+ for (const node of Array.from(sourceParagraph?.getElementsByTagNameNS?.(NS_W9, "bookmarkStart") || [])) {
16250
16859
  const name = wordAttribute4(node, "name") || "(unnamed)";
16251
16860
  bookmarkNames.add(name);
16252
16861
  }
16253
16862
  for (const name of bookmarkNames) warnings.push(`RESTORATION_DROPPED_BOOKMARK:${name}`);
16254
16863
  const commentIds = /* @__PURE__ */ new Set();
16255
16864
  for (const localName2 of ["commentRangeStart", "commentRangeEnd", "commentReference"]) {
16256
- for (const node of Array.from(sourceParagraph?.getElementsByTagNameNS?.(NS_W8, localName2) || [])) {
16865
+ for (const node of Array.from(sourceParagraph?.getElementsByTagNameNS?.(NS_W9, localName2) || [])) {
16257
16866
  const id = wordAttribute4(node, "id");
16258
16867
  if (id !== "") commentIds.add(id);
16259
16868
  }
@@ -16270,24 +16879,24 @@ function removeRestorationAnchors(paragraph) {
16270
16879
  "commentReference"
16271
16880
  ];
16272
16881
  for (const localName2 of anchorNames) {
16273
- for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W8, localName2) || [])) {
16882
+ for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W9, localName2) || [])) {
16274
16883
  node.parentNode?.removeChild(node);
16275
16884
  }
16276
16885
  }
16277
- for (const run of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W8, "r") || [])) {
16278
- const meaningful = Array.from(run.childNodes || []).some((child) => child.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName !== "rPr");
16886
+ for (const run of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W9, "r") || [])) {
16887
+ const meaningful = Array.from(run.childNodes || []).some((child) => child.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName !== "rPr");
16279
16888
  if (!meaningful) run.parentNode?.removeChild(run);
16280
16889
  }
16281
16890
  }
16282
16891
  function buildRejectedRestorationTemplate(xmlDoc, sourceParagraph, serializer) {
16283
16892
  const rejected = rejectTrackedChangesInOoxml(serializer.serializeToString(sourceParagraph), { allAuthors: true });
16284
16893
  const parsed = parseOoxmlSafe(rejected.oxml, "application/xml");
16285
- const rejectedParagraph = parsed.doc?.getElementsByTagNameNS?.(NS_W8, "p")?.[0] || null;
16894
+ const rejectedParagraph = parsed.doc?.getElementsByTagNameNS?.(NS_W9, "p")?.[0] || null;
16286
16895
  if (!rejectedParagraph) return null;
16287
16896
  const paragraph = createWordElement(xmlDoc, "w:p");
16288
16897
  paragraph.appendChild(createSanitizedRestorationPPr(xmlDoc, sourceParagraph));
16289
16898
  for (const child of Array.from(rejectedParagraph.childNodes || [])) {
16290
- if (child.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName === "pPr") continue;
16899
+ if (child.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName === "pPr") continue;
16291
16900
  paragraph.appendChild(xmlDoc.importNode(child, true));
16292
16901
  }
16293
16902
  removeRestorationAnchors(paragraph);
@@ -16308,7 +16917,7 @@ async function editRestorationTemplate(xmlDoc, template, modifiedText, author, s
16308
16917
  );
16309
16918
  if (result?.status === "error" || typeof result?.oxml !== "string") return null;
16310
16919
  const extracted = extractReplacementNodes(result.oxml);
16311
- const paragraph = (extracted.replacementNodes || []).find((node) => node?.nodeType === 1 && node.namespaceURI === NS_W8 && node.localName === "p");
16920
+ const paragraph = (extracted.replacementNodes || []).find((node) => node?.nodeType === 1 && node.namespaceURI === NS_W9 && node.localName === "p");
16312
16921
  return paragraph ? xmlDoc.importNode(paragraph, true) : null;
16313
16922
  }
16314
16923
  function allocateFreshParagraphId(xmlDoc, operationSession) {
@@ -16479,7 +17088,7 @@ async function insertIntoRejectedDeletedText(documentXml, targetText, anchor, mo
16479
17088
  }
16480
17089
  const insertionOffset = anchorStart + anchor.offset;
16481
17090
  const carriers = Array.from(paragraph.childNodes || []).filter(
16482
- (node) => node?.nodeType === 1 && node.namespaceURI === NS_W8 && node.localName === "del"
17091
+ (node) => node?.nodeType === 1 && node.namespaceURI === NS_W9 && node.localName === "del"
16483
17092
  );
16484
17093
  if (carriers.some((carrier) => hasUnsupportedDeletionSplitMarkup(paragraph, carrier))) {
16485
17094
  return {
@@ -17136,7 +17745,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
17136
17745
  };
17137
17746
  }
17138
17747
  if (!allSame) {
17139
- const hasMove = targetParagraph.getElementsByTagNameNS(NS_W8, "moveFrom").length > 0 || targetParagraph.getElementsByTagNameNS(NS_W8, "moveTo").length > 0;
17748
+ const hasMove = targetParagraph.getElementsByTagNameNS(NS_W9, "moveFrom").length > 0 || targetParagraph.getElementsByTagNameNS(NS_W9, "moveTo").length > 0;
17140
17749
  if (hasMove) {
17141
17750
  return {
17142
17751
  documentXml,
@@ -17151,8 +17760,8 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
17151
17760
  }
17152
17761
  }
17153
17762
  } else {
17154
- const hasDel = targetParagraph.getElementsByTagNameNS(NS_W8, "del").length > 0;
17155
- const hasMove = targetParagraph.getElementsByTagNameNS(NS_W8, "moveFrom").length > 0 || targetParagraph.getElementsByTagNameNS(NS_W8, "moveTo").length > 0;
17763
+ const hasDel = targetParagraph.getElementsByTagNameNS(NS_W9, "del").length > 0;
17764
+ const hasMove = targetParagraph.getElementsByTagNameNS(NS_W9, "moveFrom").length > 0 || targetParagraph.getElementsByTagNameNS(NS_W9, "moveTo").length > 0;
17156
17765
  if (hasDel) {
17157
17766
  return {
17158
17767
  documentXml,
@@ -17671,7 +18280,7 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
17671
18280
  const targetParagraph = resolved.paragraph;
17672
18281
  const collectVisibleRuns = () => {
17673
18282
  const runEntries2 = [];
17674
- const allRuns = Array.from(targetParagraph.getElementsByTagNameNS(NS_W8, "r"));
18283
+ const allRuns = Array.from(targetParagraph.getElementsByTagNameNS(NS_W9, "r"));
17675
18284
  for (const run of allRuns) {
17676
18285
  let parent = run.parentNode;
17677
18286
  let isDel = false;
@@ -17763,7 +18372,7 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
17763
18372
  const targetRuns = finalRuns.filter((span) => span.start >= matchStart && span.end <= matchEnd);
17764
18373
  let anyChanged = false;
17765
18374
  for (const entry of targetRuns) {
17766
- const rPr = entry.run.getElementsByTagNameNS(NS_W8, "rPr")[0] || null;
18375
+ const rPr = entry.run.getElementsByTagNameNS(NS_W9, "rPr")[0] || null;
17767
18376
  if (checkRunPropertiesChanged(rPr, properties)) {
17768
18377
  anyChanged = true;
17769
18378
  break;
@@ -17774,7 +18383,7 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
17774
18383
  }
17775
18384
  for (const entry of targetRuns) {
17776
18385
  const run = entry.run;
17777
- let rPr = run.getElementsByTagNameNS(NS_W8, "rPr")[0];
18386
+ let rPr = run.getElementsByTagNameNS(NS_W9, "rPr")[0];
17778
18387
  if (!rPr) {
17779
18388
  rPr = createWordElement(xmlDoc, "w:rPr");
17780
18389
  run.insertBefore(rPr, run.firstChild);
@@ -17783,7 +18392,7 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
17783
18392
  let insideSameAuthorIns = false;
17784
18393
  while (insAncestor && insAncestor !== targetParagraph) {
17785
18394
  if (insAncestor.nodeType === 1 && (insAncestor.localName === "ins" || insAncestor.nodeName === "w:ins")) {
17786
- const insAuthor = insAncestor.getAttribute("w:author") || insAncestor.getAttributeNS(NS_W8, "author");
18395
+ const insAuthor = insAncestor.getAttribute("w:author") || insAncestor.getAttributeNS(NS_W9, "author");
17787
18396
  if (insAuthor === (author || getDefaultAuthor())) {
17788
18397
  insideSameAuthorIns = true;
17789
18398
  }
@@ -17831,7 +18440,7 @@ async function applyParagraphFormatToParagraphByExactText(documentXml, targetTex
17831
18440
  return { documentXml, hasChanges: false, status: "error", error: resolved?.error || { code: "TARGET_NOT_FOUND", message: "Target paragraph not found." } };
17832
18441
  }
17833
18442
  const targetParagraph = resolved.paragraph;
17834
- let pPr = targetParagraph.getElementsByTagNameNS(NS_W8, "pPr")[0];
18443
+ let pPr = targetParagraph.getElementsByTagNameNS(NS_W9, "pPr")[0];
17835
18444
  if (!pPr) {
17836
18445
  pPr = createWordElement(xmlDoc, "w:pPr");
17837
18446
  targetParagraph.insertBefore(pPr, targetParagraph.firstChild);
@@ -17879,7 +18488,7 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
17879
18488
  }
17880
18489
  let operation = validation.operation || normalizeDocumentOperation(op);
17881
18490
  const authorUsed = resolveDocumentOperationAuthor(operation, author, getDefaultAuthor());
17882
- if (operation.operationKind !== "comment_reply" && operation.operationKind !== "rejected-insert" && operation.operationKind !== "restore" && (operation.targetDescriptor?.revisionView === "rejected" || operation.targetEndDescriptor?.revisionView === "rejected")) {
18491
+ if (operation.operationKind !== "comment_reply" && operation.operationKind !== "comment_resolve" && operation.operationKind !== "rejected-insert" && operation.operationKind !== "restore" && (operation.targetDescriptor?.revisionView === "rejected" || operation.targetEndDescriptor?.revisionView === "rejected")) {
17883
18492
  return {
17884
18493
  documentXml,
17885
18494
  hasChanges: false,
@@ -18046,6 +18655,7 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
18046
18655
  result = applyCommentReplyToParts({
18047
18656
  commentsXml: existingCommentsXml,
18048
18657
  commentsExtendedXml: existingExtendedXml,
18658
+ documentXml,
18049
18659
  parentCommentId: operation.parentCommentId,
18050
18660
  commentId,
18051
18661
  commentContent: operation.commentContent,
@@ -18053,8 +18663,30 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
18053
18663
  date: operation.date || (/* @__PURE__ */ new Date()).toISOString()
18054
18664
  });
18055
18665
  result.documentXml = documentXml;
18056
- if (result.hasChanges && session.receiptCollector) session.receiptCollector.recordComment(commentId);
18666
+ if (result.status !== "error" && result.hasChanges) {
18667
+ if (!session.document || !anchorReplyInDocument(session.document, result.threadCommentIds, commentId)) {
18668
+ 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.` } };
18669
+ } else if (session.receiptCollector) {
18670
+ session.receiptCollector.recordComment(commentId);
18671
+ }
18672
+ }
18057
18673
  }
18674
+ } else if (operation.operationKind === "comment_resolve") {
18675
+ const resolution = applyThreadResolutionToParts({
18676
+ commentsXml: session.commentsXml || runtimeContext?.commentsXml || options.commentsXml,
18677
+ commentsExtendedXml: session.commentsExtendedXml || runtimeContext?.commentsExtendedXml || options.commentsExtendedXml,
18678
+ commentId: operation.commentId,
18679
+ resolved: operation.resolved !== false
18680
+ });
18681
+ result = resolution.status === "error" ? { documentXml, hasChanges: false, status: "error", error: resolution.error } : {
18682
+ documentXml,
18683
+ hasChanges: resolution.hasChanges,
18684
+ ...resolution.commentsXml ? { commentsXml: resolution.commentsXml, commentsXmlMode: "replace" } : {},
18685
+ ...resolution.commentsExtendedXml ? { commentsExtendedXml: resolution.commentsExtendedXml, commentsExtendedXmlMode: "replace" } : {},
18686
+ resolved: resolution.resolved,
18687
+ threadRootId: resolution.threadRootId,
18688
+ commentIds: resolution.commentIds
18689
+ };
18058
18690
  } else if (operation.operationKind === "highlight") {
18059
18691
  result = await applyHighlightToParagraphByExactText(
18060
18692
  documentXml,
@@ -18195,7 +18827,7 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
18195
18827
  ...resolutionCapture
18196
18828
  };
18197
18829
  }
18198
- session.markMutationCommitted(operation.operationKind !== "comment_reply");
18830
+ session.markMutationCommitted(true);
18199
18831
  if (operation._compiledSourceId && session.sourceTargetRegistry) {
18200
18832
  session.sourceTargetRegistry.commitMutation(
18201
18833
  operation._compiledSourceId,
@@ -18308,9 +18940,9 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
18308
18940
  }
18309
18941
 
18310
18942
  // services/batch-operation-orchestrator.js
18311
- var NS_W9 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
18943
+ var NS_W10 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
18312
18944
  function operationTargetPriority(op) {
18313
- return op?.type === "comment" || op?.type === "comment_reply" ? 0 : 1;
18945
+ return op?.type === "comment" || op?.type === "comment_reply" || op?.type === "comment_resolve" ? 0 : 1;
18314
18946
  }
18315
18947
  function buildOperationDependencyPlan(operations = []) {
18316
18948
  const list = Array.isArray(operations) ? operations : [];
@@ -18385,7 +19017,7 @@ function buildOperationDependencyPlan(operations = []) {
18385
19017
  inDegrees[i2]++;
18386
19018
  }
18387
19019
  const kind = normalizeDocumentOperation(op).operationKind;
18388
- if (kind !== "comment" && kind !== "comment_reply") {
19020
+ if (kind !== "comment" && kind !== "comment_reply" && kind !== "comment_resolve") {
18389
19021
  if (!mutatingCaptureConsumers.has(ref)) mutatingCaptureConsumers.set(ref, []);
18390
19022
  mutatingCaptureConsumers.get(ref).push({
18391
19023
  index: i2,
@@ -18453,9 +19085,9 @@ function mergeCommentsXml(existingXml, incomingXml) {
18453
19085
  const serializer = createSerializer();
18454
19086
  const existingRoot = existingDoc.documentElement;
18455
19087
  const existingIds = new Set(
18456
- Array.from(existingDoc.getElementsByTagNameNS(NS_W9, "comment")).map((node) => node.getAttribute("w:id") || node.getAttribute("id")).filter(Boolean)
19088
+ Array.from(existingDoc.getElementsByTagNameNS(NS_W10, "comment")).map((node) => node.getAttribute("w:id") || node.getAttribute("id")).filter(Boolean)
18457
19089
  );
18458
- for (const comment of Array.from(incomingDoc.getElementsByTagNameNS(NS_W9, "comment"))) {
19090
+ for (const comment of Array.from(incomingDoc.getElementsByTagNameNS(NS_W10, "comment"))) {
18459
19091
  const id = comment.getAttribute("w:id") || comment.getAttribute("id");
18460
19092
  if (id && existingIds.has(id)) continue;
18461
19093
  existingRoot.appendChild(existingDoc.importNode(comment, true));
@@ -18616,7 +19248,7 @@ async function applyOperationsToDocumentXml(documentXml, operations, author, run
18616
19248
  context.targetRefSnapshot = session.initialTargetReferenceSnapshot;
18617
19249
  }
18618
19250
  session.runtimeContext = context;
18619
- if (sourceOperations.some((op) => op?.type === "comment_reply")) {
19251
+ if (sourceOperations.some((op) => op?.type === "comment_reply" || op?.type === "comment_resolve")) {
18620
19252
  session.commentsXml = context.commentsXml || options.commentsXml || null;
18621
19253
  session.commentsExtendedXml = context.commentsExtendedXml || options.commentsExtendedXml || null;
18622
19254
  session.commentsXmlMode = "replace";
@@ -18689,6 +19321,7 @@ async function applyOperationsToDocumentXml(documentXml, operations, author, run
18689
19321
  ...result.resolvedTarget ? { resolvedTarget: result.resolvedTarget } : {},
18690
19322
  ...result.resolvedAnchor ? { resolvedAnchor: result.resolvedAnchor } : {},
18691
19323
  ...result.change ? { change: result.change } : {},
19324
+ ...result.operationType === "comment_resolve" && !isError ? { resolved: result.resolved, threadRootId: result.threadRootId, commentIds: result.commentIds } : {},
18692
19325
  ...Array.isArray(result.warnings) && result.warnings.length > 0 ? { warnings: result.warnings } : {},
18693
19326
  ...result.error ? { error: result.error } : {},
18694
19327
  ...result.receipt ? { receipt: result.receipt } : {}
@@ -18965,6 +19598,26 @@ function preflightOperations(documentXml, operations, author, options = {}) {
18965
19598
  }
18966
19599
  commentsRequired = commentsRequired || operation.operationKind === "comment" || operation.operationKind === "comment_reply";
18967
19600
  numberingRequired = numberingRequired || operationNeedsNumbering(operation);
19601
+ if (operation.operationKind === "comment_resolve") {
19602
+ const commentId = String(operation.commentId);
19603
+ results.push(options._existingCommentDetails?.[commentId] ? {
19604
+ index: index + 1,
19605
+ type: sourceOperation.type,
19606
+ operationType: operation.operationKind,
19607
+ status: "ready",
19608
+ authorUsed,
19609
+ resolvedBy: "comment_id",
19610
+ commentId
19611
+ } : {
19612
+ index: index + 1,
19613
+ type: sourceOperation.type,
19614
+ operationType: operation.operationKind,
19615
+ status: "error",
19616
+ authorUsed,
19617
+ error: { code: "COMMENT_NOT_FOUND", message: `Comment '${commentId}' was not found.` }
19618
+ });
19619
+ continue;
19620
+ }
18968
19621
  if (operation.operationKind === "comment_reply") {
18969
19622
  const parentId = String(operation.parentCommentId);
18970
19623
  const parent = options._existingCommentDetails?.[parentId];
@@ -20116,6 +20769,15 @@ var text = (entries, path) => {
20116
20769
  if (typeof entry === "string") return entry;
20117
20770
  return textDecoder2.decode(entry);
20118
20771
  };
20772
+ function reconcileCommentSiblings(working) {
20773
+ const updated = reconcileCommentSiblingParts({
20774
+ commentsXml: text(working, "word/comments.xml"),
20775
+ commentsIdsXml: text(working, "word/commentsIds.xml"),
20776
+ commentsExtensibleXml: text(working, "word/commentsExtensible.xml")
20777
+ });
20778
+ if (updated.commentsIdsXml) working.set("word/commentsIds.xml", textEncoder4.encode(updated.commentsIdsXml));
20779
+ if (updated.commentsExtensibleXml) working.set("word/commentsExtensible.xml", textEncoder4.encode(updated.commentsExtensibleXml));
20780
+ }
20119
20781
  var cloneEntries = (entries) => new Map([...entries].map(([name, data]) => [name, new Uint8Array(data)]));
20120
20782
  function toBufferCompatible(uint8Arr) {
20121
20783
  if (typeof Buffer !== "undefined" && typeof Buffer.from === "function") {
@@ -20183,6 +20845,7 @@ function computePackageRevisionToken(input) {
20183
20845
  entries
20184
20846
  });
20185
20847
  }
20848
+ var COMMENT_OPERATION_TYPES = /* @__PURE__ */ new Set(["comment", "comment_reply", "comment_resolve"]);
20186
20849
  function nextCommentId(entries) {
20187
20850
  const ids = `${text(entries, "word/document.xml") || ""} ${text(entries, "word/comments.xml") || ""}`.match(/(?:w:)?id=["'](\d+)["']/g) || [];
20188
20851
  let next = ids.reduce((max2, token) => Math.max(max2, Number(token.match(/\d+/)?.[0] || 0)), 0) + 1;
@@ -20201,7 +20864,7 @@ function existingCommentDetails(entries) {
20201
20864
  id,
20202
20865
  author: comment.getAttribute("w:author") || comment.getAttribute("author") || "",
20203
20866
  text: String(comment.textContent || "").trim(),
20204
- paraId: Array.from(comment.getElementsByTagNameNS("*", "p"))[0]?.getAttribute("w14:paraId") || null
20867
+ paraId: commentParaId(comment) || null
20205
20868
  };
20206
20869
  }
20207
20870
  return details;
@@ -20232,13 +20895,34 @@ var DocxDocument = class {
20232
20895
  }
20233
20896
  inspect(options = {}) {
20234
20897
  const digestFn = options.digestFn || ((bytes) => sha256(bytes));
20235
- return inspectDocumentParts({
20898
+ const inspection = inspectDocumentParts({
20236
20899
  documentXml: text(this.entries, "word/document.xml"),
20237
20900
  commentsXml: text(this.entries, "word/comments.xml"),
20238
20901
  commentsExtendedXml: text(this.entries, "word/commentsExtended.xml"),
20239
20902
  numberingXml: text(this.entries, "word/numbering.xml"),
20240
20903
  stylesXml: text(this.entries, "word/styles.xml")
20241
20904
  }, { ...options, digestFn });
20905
+ if (inspection.status === "error") return inspection;
20906
+ return { ...inspection, headersFooters: this.inspectHeadersFooters() };
20907
+ }
20908
+ /**
20909
+ * Header and footer parts the document uses, with their paragraph text, so operations can target them with
20910
+ * `part`. `hasFields` marks parts containing PAGE/NUMPAGES-style fields (edit around them, not inside).
20911
+ */
20912
+ inspectHeadersFooters() {
20913
+ return discoverHeaderFooterParts({
20914
+ documentXml: text(this.entries, "word/document.xml"),
20915
+ relsXml: text(this.entries, "word/_rels/document.xml.rels"),
20916
+ settingsXml: text(this.entries, "word/settings.xml")
20917
+ }).map((part) => {
20918
+ const xml = text(this.entries, part.path) || "";
20919
+ const partInspection = xml ? inspectDocumentParts({ documentXml: xml }) : { paragraphs: [] };
20920
+ return {
20921
+ ...part,
20922
+ hasFields: /<w:fldChar\b|<w:fldSimple\b/.test(xml),
20923
+ paragraphs: (partInspection.paragraphs || []).map((paragraph) => ({ index: paragraph.index, text: paragraph.text }))
20924
+ };
20925
+ });
20242
20926
  }
20243
20927
  getRevisionToken() {
20244
20928
  return computePackageRevisionToken(this.entries);
@@ -20247,12 +20931,51 @@ var DocxDocument = class {
20247
20931
  return this.getRevisionToken();
20248
20932
  }
20249
20933
  preflight(operations, author = getDefaultAuthor(), options = {}) {
20250
- return preflightOperations(
20934
+ const list = Array.isArray(operations) ? operations : [];
20935
+ const runBody = (ops) => preflightOperations(
20251
20936
  text(this.entries, "word/document.xml"),
20252
- operations,
20937
+ ops,
20253
20938
  author || getDefaultAuthor(),
20254
20939
  { ...options, _existingCommentDetails: existingCommentDetails(this.entries) }
20255
20940
  );
20941
+ if (!list.some((operation) => operation && typeof operation === "object" && operation.part != null)) return runBody(operations);
20942
+ const parts = this.inspectHeadersFooters();
20943
+ const bodyOps = [];
20944
+ const results = [];
20945
+ const conflicts = [];
20946
+ const authorsUsed = /* @__PURE__ */ new Set();
20947
+ let valid = true;
20948
+ list.forEach((operation, i2) => {
20949
+ if (!operation || typeof operation !== "object" || operation.part == null) {
20950
+ bodyOps.push({ operation, origIndex: i2 });
20951
+ return;
20952
+ }
20953
+ const resolved = resolvePartSelector(parts, operation.part);
20954
+ 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;
20955
+ if (refusal) {
20956
+ valid = false;
20957
+ results.push({ index: i2 + 1, type: operation.type || "redline", status: "error", error: normalizeErrorWithRecovery({ ...refusal, operationIndex: i2 + 1 }) });
20958
+ return;
20959
+ }
20960
+ const { part: _selector, ...rest } = operation;
20961
+ const partResult = preflightOperations(text(this.entries, resolved.part.path), [rest], author || getDefaultAuthor(), { ...options, _existingCommentDetails: {} });
20962
+ valid = valid && partResult.valid;
20963
+ for (const result of partResult.results || []) results.push({ ...result, index: i2 + 1, part: resolved.part.path });
20964
+ conflicts.push(...partResult.conflicts || []);
20965
+ for (const name of partResult.authorsUsed || []) authorsUsed.add(name);
20966
+ });
20967
+ let requiredArtifacts = { comments: false, numbering: false };
20968
+ if (bodyOps.length) {
20969
+ const body = runBody(bodyOps.map((item) => item.operation));
20970
+ valid = valid && body.valid;
20971
+ for (const result of body.results || []) results.push({ ...result, index: bodyOps[result.index - 1].origIndex + 1 });
20972
+ conflicts.push(...body.conflicts || []);
20973
+ for (const name of body.authorsUsed || []) authorsUsed.add(name);
20974
+ requiredArtifacts = body.requiredArtifacts || requiredArtifacts;
20975
+ }
20976
+ results.sort((a, b) => a.index - b.index);
20977
+ const ok = valid && conflicts.length === 0;
20978
+ return { valid: ok, status: ok ? "ok" : "error", results, conflicts, authorsUsed: [...authorsUsed], requiredArtifacts };
20256
20979
  }
20257
20980
  toUint8Array() {
20258
20981
  return zipDocx(this.entries);
@@ -20260,7 +20983,240 @@ var DocxDocument = class {
20260
20983
  toBuffer() {
20261
20984
  return toBufferCompatible(this.toUint8Array());
20262
20985
  }
20986
+ /**
20987
+ * Applies operations to the document. Operations without `part` target the body (word/document.xml).
20988
+ * An operation with `part` targets one header or footer part:
20989
+ * `{ part: { kind: 'footer', type: 'default', section: 0 } | 'word/footer2.xml', target: { exactText }, ... }`.
20990
+ * Discover parts with `inspect().headersFooters`.
20991
+ */
20263
20992
  async applyOperations(operations, options = {}) {
20993
+ const list = Array.isArray(operations) ? operations : [];
20994
+ if (!list.some((operation) => operation && typeof operation === "object" && operation.part != null)) {
20995
+ return this.applyBodyOperations(operations, options);
20996
+ }
20997
+ return this.applyOperationsWithParts(list, options);
20998
+ }
20999
+ async applyOperationsWithParts(operations, options = {}) {
21000
+ const author = options.author || getDefaultAuthor();
21001
+ const atomic = options.atomic === true;
21002
+ const snapshotEntries = this.entries;
21003
+ const snapshotBytes = this.originalBytes;
21004
+ const total = operations.length;
21005
+ const restore = () => {
21006
+ this.entries = snapshotEntries;
21007
+ this.originalBytes = snapshotBytes;
21008
+ };
21009
+ const original = () => ({
21010
+ uint8Array: snapshotBytes,
21011
+ toUint8Array: () => new Uint8Array(snapshotBytes),
21012
+ buffer: toBufferCompatible(snapshotBytes),
21013
+ toBuffer: () => toBufferCompatible(snapshotBytes)
21014
+ });
21015
+ const refuse = (error2, results2 = [], receipts2 = [], extra = {}) => ({
21016
+ status: "error",
21017
+ hasChanges: false,
21018
+ written: false,
21019
+ rolledBack: true,
21020
+ results: results2,
21021
+ receipts: receipts2,
21022
+ executionOrder: [],
21023
+ authorsUsed: [],
21024
+ artifactsChanged: [],
21025
+ error: normalizeErrorWithRecovery(error2),
21026
+ retryPlan: createRetryPlan({ atomic: true, rolledBack: true, results: results2, receipts: receipts2, operationCount: total }),
21027
+ validation: { originalIssues: [], generatedIssues: [] },
21028
+ ...extra,
21029
+ ...original()
21030
+ });
21031
+ if (options.expectedRevision) {
21032
+ const precondition = await this.applyBodyOperations([], { expectedRevision: options.expectedRevision });
21033
+ if (precondition.status === "error") return precondition;
21034
+ }
21035
+ const parts = discoverHeaderFooterParts({
21036
+ documentXml: text(this.entries, "word/document.xml"),
21037
+ relsXml: text(this.entries, "word/_rels/document.xml.rels"),
21038
+ settingsXml: text(this.entries, "word/settings.xml")
21039
+ });
21040
+ const bodyOps = [];
21041
+ const partOps = [];
21042
+ for (let i2 = 0; i2 < operations.length; i2++) {
21043
+ const operation = operations[i2];
21044
+ if (!operation || typeof operation !== "object" || operation.part == null) {
21045
+ bodyOps.push({ operation, origIndex: i2 });
21046
+ continue;
21047
+ }
21048
+ if (COMMENT_OPERATION_TYPES.has(operation.type)) {
21049
+ return refuse({ code: "COMMENT_IN_HEADER_FOOTER", message: `Operation ${i2 + 1}: Word does not support comments in headers or footers.`, operationIndex: i2 + 1 });
21050
+ }
21051
+ const resolved = resolvePartSelector(parts, operation.part);
21052
+ if (resolved.error) return refuse({ ...resolved.error, message: `Operation ${i2 + 1}: ${resolved.error.message}`, operationIndex: i2 + 1 });
21053
+ const { part: _selector, ...rest } = operation;
21054
+ partOps.push({ operation: rest, origIndex: i2, part: resolved.part });
21055
+ }
21056
+ const results = [];
21057
+ const receipts = [];
21058
+ const executionOrder = [];
21059
+ const authorsUsed = /* @__PURE__ */ new Set();
21060
+ const originalIssues = [];
21061
+ let anyChange = false;
21062
+ let anyFailure = false;
21063
+ let bodyResult = null;
21064
+ if (bodyOps.length) {
21065
+ bodyResult = await this.applyBodyOperations(bodyOps.map((item) => item.operation), { ...options, expectedRevision: void 0 });
21066
+ const toOriginal = (index) => bodyOps[index - 1]?.origIndex + 1;
21067
+ for (const result of bodyResult.results || []) {
21068
+ results.push({
21069
+ ...result,
21070
+ index: toOriginal(result.index),
21071
+ ...result.receipt ? { receipt: { ...result.receipt, operationIndex: toOriginal(result.receipt.operationIndex) } } : {}
21072
+ });
21073
+ }
21074
+ for (const receipt of bodyResult.receipts || []) receipts.push({ ...receipt, operationIndex: toOriginal(receipt.operationIndex) });
21075
+ for (const index of bodyResult.executionOrder || []) executionOrder.push(toOriginal(index));
21076
+ for (const name of bodyResult.authorsUsed || []) authorsUsed.add(name);
21077
+ originalIssues.push(...bodyResult.validation?.originalIssues || []);
21078
+ if (bodyResult.status === "error" || bodyResult.rolledBack) {
21079
+ anyFailure = true;
21080
+ if (atomic) {
21081
+ restore();
21082
+ return { ...bodyResult, results, receipts, executionOrder, written: false, artifactsChanged: [], rolledBack: true, ...original() };
21083
+ }
21084
+ }
21085
+ anyChange = bodyResult.hasChanges === true;
21086
+ }
21087
+ const working = cloneEntries(this.entries);
21088
+ const zip = new MemoryZip(working);
21089
+ const changedParts = /* @__PURE__ */ new Set();
21090
+ const { expectedRevision: _ignored, ...runnerOptions } = options;
21091
+ const failPart = (item, error2) => {
21092
+ anyFailure = true;
21093
+ const failure = normalizeErrorWithRecovery({ ...error2, operationIndex: item.origIndex + 1 });
21094
+ results.push({ index: item.origIndex + 1, type: item.operation.type || "redline", status: "error", authorUsed: author, part: item.part.path, error: failure });
21095
+ };
21096
+ for (const item of partOps) {
21097
+ const path = item.part.path;
21098
+ const xml = text(working, path);
21099
+ if (!xml) {
21100
+ failPart(item, { code: "PART_NOT_FOUND", message: `Part '${path}' is referenced but missing from the package.` });
21101
+ if (atomic) break;
21102
+ continue;
21103
+ }
21104
+ const numberingText = text(working, "word/numbering.xml");
21105
+ const outcome = await applyOperationsToDocumentXml(xml, [item.operation], author, { numberingIdState: createDynamicNumberingIdState(numberingText || void 0) }, {
21106
+ ...runnerOptions,
21107
+ atomic: true,
21108
+ strictTargets: options.strictTargets !== false,
21109
+ _existingCommentDetails: {}
21110
+ });
21111
+ const single = outcome.results?.[0];
21112
+ if (outcome.rolledBack || outcome.status === "error") {
21113
+ failPart(item, single?.error || outcome.error || { code: "OPERATION_FAILED", message: "Operation failed." });
21114
+ if (atomic) break;
21115
+ continue;
21116
+ }
21117
+ if (!outcome.hasChanges) {
21118
+ results.push({ ...single, index: item.origIndex + 1, part: path });
21119
+ continue;
21120
+ }
21121
+ if (countRevisionsInsideFields(outcome.documentXml) > countRevisionsInsideFields(xml)) {
21122
+ 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.` });
21123
+ if (atomic) break;
21124
+ continue;
21125
+ }
21126
+ const taken = /* @__PURE__ */ new Set();
21127
+ for (const [name, data] of working) {
21128
+ if (name === path || !(name === "word/document.xml" || isHeaderFooterPath(name))) continue;
21129
+ for (const id of collectRevisionIds(textDecoder2.decode(data))) taken.add(id);
21130
+ }
21131
+ const renumbered = renumberCollidingRevisionIds(outcome.documentXml, taken);
21132
+ working.set(path, textEncoder4.encode(renumbered.xml));
21133
+ const describeReceipt = (receipt) => receipt?.revisionItems ? { ...receipt, revisionItems: receipt.revisionItems.map((revision) => ({ ...revision, id: renumbered.idMap.get(String(revision.id)) ?? revision.id, partName: path })) } : receipt;
21134
+ if (outcome.numberingXmlParts?.length) {
21135
+ await ensureNumberingArtifactsInZip(zip, outcome.numberingXmlParts, { mergeNumberingXml: mergeNumberingXmlBySchemaOrder });
21136
+ }
21137
+ changedParts.add(path);
21138
+ anyChange = true;
21139
+ results.push({
21140
+ ...single,
21141
+ index: item.origIndex + 1,
21142
+ part: path,
21143
+ partSections: item.part.appliesToSections,
21144
+ ...single?.receipt ? { receipt: { ...describeReceipt(single.receipt), operationIndex: item.origIndex + 1 } } : {}
21145
+ });
21146
+ if (outcome.receipts?.[0]) receipts.push({ ...describeReceipt(outcome.receipts[0]), operationIndex: item.origIndex + 1 });
21147
+ executionOrder.push(item.origIndex + 1);
21148
+ for (const name of outcome.authorsUsed || []) authorsUsed.add(name);
21149
+ }
21150
+ const generatedIssues = [];
21151
+ if (!(atomic && anyFailure)) {
21152
+ for (const path of changedParts) {
21153
+ const before = validateRedlineOoxml(text(this.entries, path)).issues.map((issue) => ({ source: path, ...issue }));
21154
+ const after = validateRedlineOoxml(text(working, path)).issues.map((issue) => ({ source: path, ...issue }));
21155
+ generatedIssues.push(...validationErrors(subtractValidationIssueMultiset(after, before)));
21156
+ }
21157
+ if (options.validate !== false && changedParts.size) {
21158
+ try {
21159
+ await validateDocxPackage(zip);
21160
+ } catch (error2) {
21161
+ generatedIssues.push({ source: "package", code: "PACKAGE_VALIDATION", severity: "error", message: error2.message });
21162
+ }
21163
+ }
21164
+ }
21165
+ if (atomic && anyFailure || generatedIssues.length) {
21166
+ restore();
21167
+ 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." };
21168
+ const rolledBackResults = results.map((result) => result.receipt ? { ...result, receipt: { ...result.receipt, committed: false, finalDisposition: "rolled_back" } } : result).sort((a, b) => a.index - b.index);
21169
+ const rolledBackReceipts = receipts.map((receipt) => ({ ...receipt, committed: false, finalDisposition: "rolled_back" }));
21170
+ return refuse(error2, rolledBackResults, rolledBackReceipts, { validation: { originalIssues, generatedIssues }, issues: generatedIssues });
21171
+ }
21172
+ results.sort((a, b) => a.index - b.index);
21173
+ receipts.sort((a, b) => a.operationIndex - b.operationIndex);
21174
+ const status = !anyFailure ? "ok" : anyChange ? "partial" : "error";
21175
+ const failureError = anyFailure ? { error: normalizeErrorWithRecovery({ code: "BATCH_OPERATION_FAILED", message: "One or more operations failed; the others were applied." }) } : {};
21176
+ if (!changedParts.size) {
21177
+ const buf = toBufferCompatible(this.originalBytes);
21178
+ return {
21179
+ ...bodyResult || {},
21180
+ results,
21181
+ receipts,
21182
+ executionOrder,
21183
+ authorsUsed: [...authorsUsed],
21184
+ status,
21185
+ hasChanges: anyChange,
21186
+ written: bodyResult?.written === true,
21187
+ artifactsChanged: bodyResult?.artifactsChanged || [],
21188
+ validation: { originalIssues, generatedIssues: [] },
21189
+ ...failureError,
21190
+ uint8Array: this.originalBytes,
21191
+ toUint8Array: () => new Uint8Array(this.originalBytes),
21192
+ buffer: buf,
21193
+ toBuffer: () => toBufferCompatible(this.originalBytes)
21194
+ };
21195
+ }
21196
+ this.entries = working;
21197
+ const outputBytes = this.toUint8Array();
21198
+ this.originalBytes = outputBytes;
21199
+ const artifactsChanged = [...working].filter(([name, data]) => !snapshotEntries.has(name) || !areByteArraysEqual(data, snapshotEntries.get(name))).map(([name]) => name);
21200
+ return {
21201
+ ...bodyResult || {},
21202
+ status,
21203
+ hasChanges: true,
21204
+ written: true,
21205
+ results,
21206
+ receipts,
21207
+ executionOrder,
21208
+ authorsUsed: [...authorsUsed],
21209
+ artifactsChanged,
21210
+ validation: { originalIssues, generatedIssues: [] },
21211
+ ...failureError,
21212
+ uint8Array: outputBytes,
21213
+ toUint8Array: () => new Uint8Array(outputBytes),
21214
+ buffer: toBufferCompatible(outputBytes),
21215
+ inspection: this.inspect(),
21216
+ toBuffer: () => toBufferCompatible(outputBytes)
21217
+ };
21218
+ }
21219
+ async applyBodyOperations(operations, options = {}) {
20264
21220
  const failedApply = (error2) => {
20265
21221
  const results = [];
20266
21222
  const receipts = [];
@@ -20371,18 +21327,21 @@ var DocxDocument = class {
20371
21327
  toBuffer: () => toBufferCompatible(this.originalBytes)
20372
21328
  };
20373
21329
  }
20374
- working.set("word/document.xml", textEncoder4.encode(result.documentXml));
21330
+ const bodyChanged = (result.results || []).some((item) => item?.status !== "error" && item?.operationType !== "comment_resolve");
21331
+ if (bodyChanged) working.set("word/document.xml", textEncoder4.encode(result.documentXml));
20375
21332
  await ensureNumberingArtifactsInZip(zip, result.numberingXmlParts, { mergeNumberingXml: mergeNumberingXmlBySchemaOrder });
20376
21333
  const existingCommentsXml = text(working, "word/comments.xml");
20377
21334
  const commentsXmlForPackaging = result.commentsXml || existingCommentsXml;
20378
21335
  await ensureCommentsArtifactsInZip(zip, commentsXmlForPackaging, {
20379
21336
  replaceExisting: result.commentsXmlMode === "replace" || !result.commentsXml && !!existingCommentsXml
20380
21337
  });
20381
- const existingCommentsExtendedXml = text(working, "word/commentsExtended.xml");
20382
- const commentsExtendedXmlForPackaging = result.commentsExtendedXml || existingCommentsExtendedXml;
20383
- await ensureCommentsExtendedArtifactsInZip(zip, commentsExtendedXmlForPackaging, {
20384
- replaceExisting: result.commentsExtendedXmlMode === "replace" || !result.commentsExtendedXml && !!existingCommentsExtendedXml
20385
- });
21338
+ if (result.commentsExtendedXml) {
21339
+ await ensureCommentsExtendedArtifactsInZip(zip, result.commentsExtendedXml, {
21340
+ replaceExisting: result.commentsExtendedXmlMode === "replace"
21341
+ });
21342
+ }
21343
+ await repairKnownContentTypes(zip);
21344
+ reconcileCommentSiblings(working);
20386
21345
  if (options.validate !== false) {
20387
21346
  const generated = validateRedlineOoxml(result.documentXml);
20388
21347
  const outputIssues = generated.issues.map((issue) => ({ source: "word/document.xml", ...issue }));
@@ -20451,13 +21410,25 @@ var DocxDocument = class {
20451
21410
  const transform = action === "accept" ? acceptTrackedChangesInOoxml : action === "reject" ? rejectTrackedChangesInOoxml : null;
20452
21411
  if (!transform) return packageFailure(this.originalBytes, "INVALID_ACTION", `Unknown revision action: ${action}`);
20453
21412
  const sourceBytes = this.originalBytes;
21413
+ const originalEntries = this.entries;
20454
21414
  const working = cloneEntries(this.entries);
20455
21415
  const zip = new MemoryZip(working);
20456
21416
  const result = transform(text(working, "word/document.xml"), { author: options.author, allAuthors: options.allAuthors === true });
20457
21417
  if (result.status === "error" || result.error) {
20458
21418
  return packageFailure(sourceBytes, result.error?.code || "REVISION_OPERATION_FAILED", result.error?.message || "Revision operation failed.");
20459
21419
  }
20460
- if (!result.hasChanges) {
21420
+ const partUpdates = /* @__PURE__ */ new Map();
21421
+ for (const part of this.inspectHeadersFooters()) {
21422
+ const partXml = text(working, part.path);
21423
+ if (!partXml) continue;
21424
+ const partResult = transform(partXml, { author: options.author, allAuthors: options.allAuthors === true });
21425
+ if (partResult.status === "error" || partResult.error) {
21426
+ return packageFailure(sourceBytes, partResult.error?.code || "REVISION_OPERATION_FAILED", `${part.path}: ${partResult.error?.message || "Revision operation failed."}`);
21427
+ }
21428
+ if (partResult.hasChanges) partUpdates.set(part.path, partResult.oxml);
21429
+ }
21430
+ const hasChanges = result.hasChanges || partUpdates.size > 0;
21431
+ if (!hasChanges) {
20461
21432
  const buf = toBufferCompatible(sourceBytes);
20462
21433
  return {
20463
21434
  ...result,
@@ -20470,8 +21441,10 @@ var DocxDocument = class {
20470
21441
  toBuffer: () => toBufferCompatible(sourceBytes)
20471
21442
  };
20472
21443
  }
20473
- working.set("word/document.xml", textEncoder4.encode(result.oxml));
21444
+ if (result.hasChanges) working.set("word/document.xml", textEncoder4.encode(result.oxml));
21445
+ for (const [path, xml] of partUpdates) working.set(path, textEncoder4.encode(xml));
20474
21446
  try {
21447
+ await repairKnownContentTypes(zip);
20475
21448
  if (options.validate !== false) await validateDocxPackage(zip);
20476
21449
  } catch (error2) {
20477
21450
  return packageFailure(sourceBytes, "PACKAGE_VALIDATION", error2.message);
@@ -20484,13 +21457,71 @@ var DocxDocument = class {
20484
21457
  ...result,
20485
21458
  status: "ok",
20486
21459
  written: true,
20487
- artifactsChanged: ["word/document.xml"],
21460
+ hasChanges: true,
21461
+ artifactsChanged: [...working].filter(([name, data]) => !originalEntries.has(name) || !areByteArraysEqual(data, originalEntries.get(name))).map(([name]) => name),
20488
21462
  uint8Array: outputBytes,
20489
21463
  toUint8Array: () => new Uint8Array(outputBytes),
20490
21464
  buffer: outputBuf,
20491
21465
  toBuffer: () => toBufferCompatible(outputBytes)
20492
21466
  };
20493
21467
  }
21468
+ /**
21469
+ * Resolves (or reopens) the comment thread containing `commentId`.
21470
+ * Resolved is a thread-level state in Word, so the root and every reply are updated together.
21471
+ */
21472
+ async resolveComment(commentId, options = {}) {
21473
+ const sourceBytes = this.originalBytes;
21474
+ const working = cloneEntries(this.entries);
21475
+ const resolved = options.resolved !== false;
21476
+ const noChange = (extra) => {
21477
+ const buf = toBufferCompatible(sourceBytes);
21478
+ return {
21479
+ status: "ok",
21480
+ hasChanges: false,
21481
+ written: false,
21482
+ artifactsChanged: [],
21483
+ ...extra,
21484
+ uint8Array: sourceBytes,
21485
+ toUint8Array: () => new Uint8Array(sourceBytes),
21486
+ buffer: buf,
21487
+ toBuffer: () => toBufferCompatible(sourceBytes)
21488
+ };
21489
+ };
21490
+ const result = applyThreadResolutionToParts({
21491
+ commentsXml: text(working, "word/comments.xml"),
21492
+ commentsExtendedXml: text(working, "word/commentsExtended.xml"),
21493
+ commentId,
21494
+ resolved
21495
+ });
21496
+ if (result.status === "error") return packageFailure(sourceBytes, result.error.code, result.error.message);
21497
+ if (!result.hasChanges) return noChange({ resolved, threadRootId: result.threadRootId, commentIds: result.commentIds });
21498
+ const zip = new MemoryZip(working);
21499
+ try {
21500
+ if (result.commentsXml) working.set("word/comments.xml", textEncoder4.encode(result.commentsXml));
21501
+ await ensureCommentsExtendedArtifactsInZip(zip, result.commentsExtendedXml, { replaceExisting: true });
21502
+ await repairKnownContentTypes(zip);
21503
+ if (options.validate !== false) await validateDocxPackage(zip);
21504
+ } catch (error2) {
21505
+ return packageFailure(sourceBytes, "PACKAGE_VALIDATION", error2.message);
21506
+ }
21507
+ const artifactsChanged = [...working].filter(([name, data]) => !this.entries.has(name) || !areByteArraysEqual(data, this.entries.get(name))).map(([name]) => name);
21508
+ this.entries = working;
21509
+ const outputBytes = this.toUint8Array();
21510
+ this.originalBytes = outputBytes;
21511
+ return {
21512
+ status: "ok",
21513
+ hasChanges: true,
21514
+ written: true,
21515
+ resolved,
21516
+ threadRootId: result.threadRootId,
21517
+ commentIds: result.commentIds,
21518
+ artifactsChanged,
21519
+ uint8Array: outputBytes,
21520
+ toUint8Array: () => new Uint8Array(outputBytes),
21521
+ buffer: toBufferCompatible(outputBytes),
21522
+ toBuffer: () => toBufferCompatible(outputBytes)
21523
+ };
21524
+ }
20494
21525
  async deleteComments(options = {}) {
20495
21526
  const sourceBytes = this.originalBytes;
20496
21527
  const working = cloneEntries(this.entries);
@@ -20514,7 +21545,13 @@ var DocxDocument = class {
20514
21545
  if (!parsed.doc || parsed.error) {
20515
21546
  return packageFailure(sourceBytes, "PARSE_ERROR", parsed.error?.message || "Could not parse comments.xml.");
20516
21547
  }
20517
- const matches = (comment) => options.allAuthors === true || (comment.getAttribute("w:author") || comment.getAttribute("author")) === options.author;
21548
+ const requestedIds = Array.isArray(options.ids) ? new Set(options.ids.map(String)) : null;
21549
+ if (requestedIds) {
21550
+ const known = new Set(Array.from(parsed.doc.getElementsByTagNameNS("*", "comment")).map((node) => node.getAttribute("w:id") || node.getAttribute("id")));
21551
+ const missing = [...requestedIds].filter((id) => !known.has(id));
21552
+ if (missing.length) return packageFailure(sourceBytes, "COMMENT_NOT_FOUND", `Comment id(s) not found: ${missing.join(", ")}.`);
21553
+ }
21554
+ 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;
20518
21555
  const ids = new Set(
20519
21556
  Array.from(parsed.doc.getElementsByTagNameNS("*", "comment")).filter(matches).map((node) => node.getAttribute("w:id") || node.getAttribute("id")).filter(Boolean)
20520
21557
  );
@@ -20543,9 +21580,8 @@ var DocxDocument = class {
20543
21580
  const idByParaId = /* @__PURE__ */ new Map();
20544
21581
  for (const comment of Array.from(parsed.doc.getElementsByTagNameNS("*", "comment"))) {
20545
21582
  const id = comment.getAttribute("w:id") || comment.getAttribute("id");
20546
- const paragraph = Array.from(comment.getElementsByTagNameNS("*", "p"))[0];
20547
- const paraId = paragraph?.getAttribute("w14:paraId") || paragraph?.getAttribute("paraId");
20548
- if (paraId) idByParaId.set(paraId.toUpperCase(), id);
21583
+ const paraId = commentParaId(comment);
21584
+ if (paraId) idByParaId.set(paraId, id);
20549
21585
  }
20550
21586
  for (const [paraId, id] of idByParaId) {
20551
21587
  if (ids.has(id)) removedParaIds.add(paraId);
@@ -20564,8 +21600,8 @@ var DocxDocument = class {
20564
21600
  }
20565
21601
  }
20566
21602
  }
20567
- const commentsResult = deleteCommentsByAuthorInOoxml(commentsXml, { author: options.author, allAuthors: options.allAuthors === true });
20568
- const remainingParsed = parseOoxmlSafe(commentsResult.oxml, "application/xml");
21603
+ const remainingXml = requestedIds ? commentsXml : deleteCommentsByAuthorInOoxml(commentsXml, { author: options.author, allAuthors: options.allAuthors === true }).oxml;
21604
+ const remainingParsed = parseOoxmlSafe(remainingXml, "application/xml");
20569
21605
  if (!remainingParsed.doc || remainingParsed.error) {
20570
21606
  return packageFailure(sourceBytes, "PARSE_ERROR", remainingParsed.error?.message || "Could not parse updated comments.xml.");
20571
21607
  }
@@ -20601,10 +21637,14 @@ var DocxDocument = class {
20601
21637
  }
20602
21638
  working.set("word/document.xml", textEncoder4.encode(serializer.serializeToString(documentParsed.doc)));
20603
21639
  try {
20604
- if (options.validate !== false) await validateDocxPackage(new MemoryZip(working));
21640
+ reconcileCommentSiblings(working);
21641
+ const finalZip = new MemoryZip(working);
21642
+ await repairKnownContentTypes(finalZip);
21643
+ if (options.validate !== false) await validateDocxPackage(finalZip);
20605
21644
  } catch (error2) {
20606
21645
  return packageFailure(sourceBytes, "PACKAGE_VALIDATION", error2.message);
20607
21646
  }
21647
+ const artifactsChanged = [...working].filter(([name, data]) => !this.entries.has(name) || !areByteArraysEqual(data, this.entries.get(name))).map(([name]) => name);
20608
21648
  this.entries = working;
20609
21649
  const outputBytes = this.toUint8Array();
20610
21650
  this.originalBytes = outputBytes;
@@ -20615,7 +21655,7 @@ var DocxDocument = class {
20615
21655
  written: true,
20616
21656
  commentsRemoved: ids.size,
20617
21657
  referencesRemoved,
20618
- artifactsChanged: ["word/document.xml", "word/comments.xml", ...extendedParsed?.doc ? ["word/commentsExtended.xml"] : []],
21658
+ artifactsChanged,
20619
21659
  uint8Array: outputBytes,
20620
21660
  toUint8Array: () => new Uint8Array(outputBytes),
20621
21661
  buffer: outputBuf,
@@ -20871,6 +21911,7 @@ export {
20871
21911
  rejectTrackedChangesInOoxml,
20872
21912
  remapNumberingPayloadForDocument,
20873
21913
  removeFormattingFromRPr,
21914
+ repairKnownContentTypes,
20874
21915
  reserveNextNumberingId,
20875
21916
  reserveNextNumberingIdPair,
20876
21917
  resolveParagraphRangeByRefs,