@ansonlai/docx-redline-js 0.8.0 → 0.8.1

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 (40) hide show
  1. package/CHANGELOG.md +539 -486
  2. package/README.md +331 -309
  3. package/dist/docx-redline-js.esm.js +1083 -163
  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 +1083 -163
  8. package/dist/docx-redline.bundle.cjs.map +4 -4
  9. package/dist/docx-redline.bundle.js +1083 -163
  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-writer.js +18 -0
  15. package/index.d.ts +598 -575
  16. package/index.js +349 -348
  17. package/node/cli-help.js +5 -5
  18. package/node/cli.js +760 -754
  19. package/node/index.d.ts +1 -1
  20. package/package.json +2 -1
  21. package/scripts/generate-word-comment-edge-case-fixture.ps1 +58 -0
  22. package/scripts/generate-word-comment-thread-fixture.ps1 +36 -0
  23. package/scripts/generate-word-header-footer-fixture.ps1 +32 -0
  24. package/scripts/generate-word-multi-section-fixture.ps1 +27 -0
  25. package/scripts/generate-word-package-integrity-cases.mjs +141 -0
  26. package/scripts/generate-word-thread-state-fixtures.ps1 +50 -0
  27. package/scripts/word-com-package-integrity.ps1 +98 -0
  28. package/services/batch-operation-orchestrator.js +262 -259
  29. package/services/comment-replies.js +96 -95
  30. package/services/comment-thread-parts.js +280 -0
  31. package/services/document-inspection.js +366 -365
  32. package/services/document-operation-applier.js +230 -202
  33. package/services/document-operation-contract.js +17 -2
  34. package/services/error-recovery.js +6 -0
  35. package/services/headers-footers.js +206 -0
  36. package/services/operation-batch-compiler.js +1 -0
  37. package/services/operation-preflight.js +231 -218
  38. package/services/package-parts.js +80 -0
  39. package/services/standalone-docx-plumbing.js +631 -569
  40. package/services/standalone-operation-runner.d.ts +19 -3
@@ -1,4 +1,4 @@
1
- // @ansonlai/docx-redline-js standalone bundle v0.8.0 — https://github.com/AnsonLai/docx-redline-js
1
+ // @ansonlai/docx-redline-js standalone bundle v0.8.1 — 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;
@@ -14196,6 +14196,19 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
14196
14196
  pendingReplacementEvent = null;
14197
14197
  }
14198
14198
  }
14199
+ for (const offset of Array.from(sentinelMapByStart.keys()).sort((a, b) => a - b)) {
14200
+ for (const sentinel of sentinelMapByStart.get(offset)) {
14201
+ if (!sentinel.isCommentMarker || emittedCommentMarkers.has(sentinel.node)) continue;
14202
+ emittedCommentMarkers.add(sentinel.node);
14203
+ if (isWordElement(sentinel.node, "commentReference")) {
14204
+ const run = createWordElement(xmlDoc, "w:r");
14205
+ run.appendChild(sentinel.node.cloneNode(true));
14206
+ currentParagraph.appendChild(run);
14207
+ } else {
14208
+ currentParagraph.appendChild(sentinel.node.cloneNode(true));
14209
+ }
14210
+ }
14211
+ }
14199
14212
  if (generateRedlines) {
14200
14213
  containerFragments.forEach((fragment) => {
14201
14214
  Array.from(fragment.childNodes).forEach((node) => {
@@ -17990,6 +18003,260 @@ function ingestWordOoxmlToMarkdownResult(ooxml) {
17990
18003
  return { text: lines.join("\n\n").trim(), status: "ok", warnings: parsed.warnings };
17991
18004
  }
17992
18005
 
18006
+ // services/comment-builders.js
18007
+ var NS_W14 = "http://schemas.microsoft.com/office/word/2010/wordml";
18008
+ var NS_W15 = "http://schemas.microsoft.com/office/word/2012/wordml";
18009
+ function createCommentParaId(commentId) {
18010
+ const numeric = Number.parseInt(String(commentId), 10);
18011
+ const value = Number.isFinite(numeric) ? 1879048192 + (numeric >>> 0) >>> 0 : 1879048192;
18012
+ return value.toString(16).toUpperCase().padStart(8, "0").slice(-8);
18013
+ }
18014
+ function buildCommentElement(commentId, author, content, date, paraId = createCommentParaId(commentId)) {
18015
+ const initials = author.split(" ").map((word) => word[0]).join("").toUpperCase() || "AI";
18016
+ const escapedContent = escapeXml(content);
18017
+ const escapedAuthor = escapeXml(author);
18018
+ return `<w:comment w:id="${commentId}" w:author="${escapedAuthor}" w:date="${date}" w:initials="${initials}">
18019
+ <w:p w14:paraId="${escapeXml(paraId)}" xmlns:w14="${NS_W14}">
18020
+ <w:r><w:t>${escapedContent}</w:t></w:r>
18021
+ </w:p>
18022
+ </w:comment>`;
18023
+ }
18024
+ function buildCommentsPartXml(comments) {
18025
+ if (!comments || comments.length === 0) {
18026
+ return `<w:comments xmlns:w="${NS_W}"></w:comments>`;
18027
+ }
18028
+ const commentElements = comments.map(
18029
+ (comment) => buildCommentElement(comment.id, comment.author, comment.content, comment.date, comment.paraId)
18030
+ ).join("\n ");
18031
+ return `<w:comments xmlns:w="${NS_W}">
18032
+ ${commentElements}
18033
+ </w:comments>`;
18034
+ }
18035
+ function buildCommentsExtendedPartXml(entries) {
18036
+ const body = (entries || []).map((entry) => {
18037
+ const parent = entry.paraIdParent ? ` w15:paraIdParent="${escapeXml(entry.paraIdParent)}"` : "";
18038
+ return `<w15:commentEx w15:paraId="${escapeXml(entry.paraId)}"${parent} w15:done="${entry.done ? "1" : "0"}"/>`;
18039
+ }).join("");
18040
+ return `<w15:commentsEx xmlns:w15="${NS_W15}">${body}</w15:commentsEx>`;
18041
+ }
18042
+
18043
+ // services/comment-thread-parts.js
18044
+ var attrOf = (node, qualified, local) => node?.getAttribute?.(qualified) || node?.getAttribute?.(local) || "";
18045
+ function commentThreadParagraph(commentElement) {
18046
+ const paragraphs = Array.from(commentElement?.getElementsByTagNameNS?.("*", "p") || []);
18047
+ return paragraphs.length ? paragraphs[paragraphs.length - 1] : null;
18048
+ }
18049
+ function commentParaId(commentElement) {
18050
+ return attrOf(commentThreadParagraph(commentElement), "w14:paraId", "paraId").toUpperCase();
18051
+ }
18052
+ function usedParaIds(commentsDoc, extendedDoc) {
18053
+ const ids = /* @__PURE__ */ new Set();
18054
+ for (const p of Array.from(commentsDoc?.getElementsByTagNameNS("*", "p") || [])) {
18055
+ const id = attrOf(p, "w14:paraId", "paraId");
18056
+ if (id) ids.add(id.toUpperCase());
18057
+ }
18058
+ for (const ex of Array.from(extendedDoc?.getElementsByTagNameNS("*", "commentEx") || [])) {
18059
+ const id = attrOf(ex, "w15:paraId", "paraId");
18060
+ if (id) ids.add(id.toUpperCase());
18061
+ }
18062
+ return ids;
18063
+ }
18064
+ function allocateParaId(commentId, occupied) {
18065
+ let candidate = createCommentParaId(commentId);
18066
+ let value = Number.parseInt(candidate, 16) >>> 0;
18067
+ while (occupied.has(candidate)) {
18068
+ value = value + 1 >>> 0;
18069
+ candidate = value.toString(16).toUpperCase().padStart(8, "0");
18070
+ }
18071
+ occupied.add(candidate);
18072
+ return candidate;
18073
+ }
18074
+ var parseDoc = (xml, partName) => {
18075
+ const parsed = parseOoxmlSafe(xml, "application/xml");
18076
+ if (!parsed.doc || parsed.error) return { error: { code: "PARSE_ERROR", message: `Could not parse ${partName}: ${parsed.error?.message || "invalid XML"}` } };
18077
+ return { doc: parsed.doc };
18078
+ };
18079
+ function applyThreadResolutionToParts({ commentsXml, commentsExtendedXml = null, commentId, resolved = true }) {
18080
+ if (!commentsXml) return { status: "error", error: { code: "COMMENTS_PART_MISSING", message: "Resolving a comment requires an existing word/comments.xml part." } };
18081
+ const commentsParsed = parseDoc(commentsXml, "word/comments.xml");
18082
+ if (commentsParsed.error) return { status: "error", error: commentsParsed.error };
18083
+ const commentsDoc = commentsParsed.doc;
18084
+ const idOf = (node) => attrOf(node, "w:id", "id");
18085
+ const comments = Array.from(commentsDoc.getElementsByTagNameNS("*", "comment"));
18086
+ const target = comments.find((node) => idOf(node) === String(commentId));
18087
+ if (!target) return { status: "error", error: { code: "COMMENT_NOT_FOUND", message: `Comment '${commentId}' was not found.` } };
18088
+ let extendedDoc;
18089
+ if (commentsExtendedXml) {
18090
+ const parsed = parseDoc(commentsExtendedXml, "word/commentsExtended.xml");
18091
+ if (parsed.error) return { status: "error", error: parsed.error };
18092
+ extendedDoc = parsed.doc;
18093
+ } else {
18094
+ extendedDoc = parseDoc(`<w15:commentsEx xmlns:w15="${NS_W15}"/>`, "word/commentsExtended.xml").doc;
18095
+ }
18096
+ const root = extendedDoc.documentElement;
18097
+ const entryFor = (paraId) => Array.from(root.getElementsByTagNameNS("*", "commentEx")).find((node) => attrOf(node, "w15:paraId", "paraId").toUpperCase() === paraId) || null;
18098
+ const occupied = usedParaIds(commentsDoc, extendedDoc);
18099
+ let commentsChanged = false;
18100
+ const keyOf = (node) => {
18101
+ let paraId = commentParaId(node);
18102
+ if (!paraId) {
18103
+ paraId = allocateParaId(idOf(node), occupied);
18104
+ commentThreadParagraph(node)?.setAttributeNS(NS_W14, "w14:paraId", paraId);
18105
+ commentsChanged = true;
18106
+ }
18107
+ return paraId;
18108
+ };
18109
+ const targetKey = keyOf(target);
18110
+ const targetEntry = entryFor(targetKey);
18111
+ const rootKey = targetEntry && attrOf(targetEntry, "w15:paraIdParent", "paraIdParent").toUpperCase() || targetKey;
18112
+ const threadKeys = /* @__PURE__ */ new Set([rootKey, targetKey]);
18113
+ for (const entry of Array.from(root.getElementsByTagNameNS("*", "commentEx"))) {
18114
+ if (attrOf(entry, "w15:paraIdParent", "paraIdParent").toUpperCase() === rootKey) threadKeys.add(attrOf(entry, "w15:paraId", "paraId").toUpperCase());
18115
+ }
18116
+ const doneValue = resolved ? "1" : "0";
18117
+ let extendedChanged = !commentsExtendedXml;
18118
+ for (const key of threadKeys) {
18119
+ let entry = entryFor(key);
18120
+ if (!entry) {
18121
+ entry = extendedDoc.createElementNS(NS_W15, "w15:commentEx");
18122
+ entry.setAttributeNS(NS_W15, "w15:paraId", key);
18123
+ if (key !== rootKey) entry.setAttributeNS(NS_W15, "w15:paraIdParent", rootKey);
18124
+ entry.setAttributeNS(NS_W15, "w15:done", doneValue);
18125
+ root.appendChild(entry);
18126
+ extendedChanged = true;
18127
+ } else if (attrOf(entry, "w15:done", "done") !== doneValue) {
18128
+ entry.setAttributeNS(NS_W15, "w15:done", doneValue);
18129
+ extendedChanged = true;
18130
+ }
18131
+ }
18132
+ const byKey = new Map(comments.map((node) => [commentParaId(node), node]));
18133
+ const commentIds = [...threadKeys].map((key) => byKey.get(key)).filter(Boolean).map(idOf);
18134
+ const rootComment = byKey.get(rootKey);
18135
+ const serializer = createSerializer();
18136
+ const hasChanges = extendedChanged || commentsChanged;
18137
+ return {
18138
+ status: "ok",
18139
+ hasChanges,
18140
+ resolved,
18141
+ threadRootId: rootComment ? idOf(rootComment) : String(commentId),
18142
+ commentIds,
18143
+ ...hasChanges ? { commentsExtendedXml: serializer.serializeToString(extendedDoc) } : {},
18144
+ ...commentsChanged ? { commentsXml: serializer.serializeToString(commentsDoc) } : {}
18145
+ };
18146
+ }
18147
+ var NS_W_MAIN = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
18148
+ function anchorReplyInDocument(xmlDoc, memberIds, newId) {
18149
+ const wanted = new Set(memberIds.map(String));
18150
+ const idOf = (node) => attrOf(node, "w:id", "id");
18151
+ const all = (name) => Array.from(xmlDoc.getElementsByTagNameNS("*", name)).filter((node) => wanted.has(idOf(node)));
18152
+ const starts = all("commentRangeStart");
18153
+ const references = all("commentReference");
18154
+ if (!starts.length || !references.length) return false;
18155
+ const lastStart = starts[starts.length - 1];
18156
+ const lastReference = references[references.length - 1];
18157
+ const referenceRun = lastReference.parentNode?.localName === "r" ? lastReference.parentNode : lastReference;
18158
+ const make = (name, parent) => {
18159
+ const element = xmlDoc.createElementNS(NS_W_MAIN, `w:${name}`);
18160
+ element.setAttributeNS(NS_W_MAIN, "w:id", String(newId));
18161
+ if (parent) parent.appendChild(element);
18162
+ return element;
18163
+ };
18164
+ const insertAfter = (anchor, node) => anchor.parentNode.insertBefore(node, anchor.nextSibling);
18165
+ insertAfter(lastStart, make("commentRangeStart"));
18166
+ const end = make("commentRangeEnd");
18167
+ const run = xmlDoc.createElementNS(NS_W_MAIN, "w:r");
18168
+ run.appendChild(xmlDoc.createElementNS(NS_W_MAIN, "w:rPr"));
18169
+ make("commentReference", run);
18170
+ insertAfter(referenceRun, end);
18171
+ insertAfter(end, run);
18172
+ return true;
18173
+ }
18174
+ var NS_W16CID = "http://schemas.microsoft.com/office/word/2016/wordml/cid";
18175
+ var NS_W16CEX = "http://schemas.microsoft.com/office/word/2018/wordml/cex";
18176
+ var durableIdFor = (paraId, taken) => {
18177
+ let value = (Number.parseInt(paraId, 16) ^ 625341585) & 2147483646;
18178
+ let candidate = value.toString(16).toUpperCase().padStart(8, "0");
18179
+ while (taken.has(candidate)) {
18180
+ value = value + 2 & 2147483646;
18181
+ candidate = value.toString(16).toUpperCase().padStart(8, "0");
18182
+ }
18183
+ taken.add(candidate);
18184
+ return candidate;
18185
+ };
18186
+ function reconcileCommentSiblingParts({ commentsXml, commentsIdsXml = null, commentsExtensibleXml = null }) {
18187
+ const out = {};
18188
+ if (!commentsXml || !commentsIdsXml && !commentsExtensibleXml) return out;
18189
+ const commentsParsed = parseOoxmlSafe(commentsXml, "application/xml");
18190
+ if (!commentsParsed.doc || commentsParsed.error) return out;
18191
+ const serializer = createSerializer();
18192
+ const byKey = /* @__PURE__ */ new Map();
18193
+ for (const node of Array.from(commentsParsed.doc.getElementsByTagNameNS("*", "comment"))) {
18194
+ const key = commentParaId(node);
18195
+ if (key) byKey.set(key, node);
18196
+ }
18197
+ let idsDoc = null;
18198
+ const durableByKey = /* @__PURE__ */ new Map();
18199
+ const taken = /* @__PURE__ */ new Set();
18200
+ let idsChanged = false;
18201
+ if (commentsIdsXml) {
18202
+ const parsed = parseOoxmlSafe(commentsIdsXml, "application/xml");
18203
+ if (!parsed.doc || parsed.error) return out;
18204
+ idsDoc = parsed.doc;
18205
+ const root = idsDoc.documentElement;
18206
+ for (const entry of Array.from(root.getElementsByTagNameNS("*", "commentId"))) {
18207
+ const key = attrOf(entry, "w16cid:paraId", "paraId").toUpperCase();
18208
+ const durable = attrOf(entry, "w16cid:durableId", "durableId").toUpperCase();
18209
+ if (durable) taken.add(durable);
18210
+ if (!byKey.has(key)) {
18211
+ root.removeChild(entry);
18212
+ idsChanged = true;
18213
+ continue;
18214
+ }
18215
+ durableByKey.set(key, durable);
18216
+ }
18217
+ for (const key of byKey.keys()) {
18218
+ if (durableByKey.has(key)) continue;
18219
+ const durable = durableIdFor(key, taken);
18220
+ const entry = idsDoc.createElementNS(NS_W16CID, "w16cid:commentId");
18221
+ entry.setAttributeNS(NS_W16CID, "w16cid:paraId", key);
18222
+ entry.setAttributeNS(NS_W16CID, "w16cid:durableId", durable);
18223
+ root.appendChild(entry);
18224
+ durableByKey.set(key, durable);
18225
+ idsChanged = true;
18226
+ }
18227
+ }
18228
+ let extDoc = null;
18229
+ let extChanged = false;
18230
+ if (commentsExtensibleXml && idsDoc) {
18231
+ const parsed = parseOoxmlSafe(commentsExtensibleXml, "application/xml");
18232
+ if (parsed.doc && !parsed.error) {
18233
+ extDoc = parsed.doc;
18234
+ const root = extDoc.documentElement;
18235
+ const live = new Set(durableByKey.values());
18236
+ const present = /* @__PURE__ */ new Set();
18237
+ for (const entry of Array.from(root.getElementsByTagNameNS("*", "commentExtensible"))) {
18238
+ const durable = attrOf(entry, "w16cex:durableId", "durableId").toUpperCase();
18239
+ if (!live.has(durable)) {
18240
+ root.removeChild(entry);
18241
+ extChanged = true;
18242
+ } else present.add(durable);
18243
+ }
18244
+ for (const [key, durable] of durableByKey) {
18245
+ if (present.has(durable)) continue;
18246
+ const date = attrOf(byKey.get(key), "w:date", "date");
18247
+ const entry = extDoc.createElementNS(NS_W16CEX, "w16cex:commentExtensible");
18248
+ entry.setAttributeNS(NS_W16CEX, "w16cex:durableId", durable);
18249
+ if (date) entry.setAttributeNS(NS_W16CEX, "w16cex:dateUtc", date);
18250
+ root.appendChild(entry);
18251
+ extChanged = true;
18252
+ }
18253
+ }
18254
+ }
18255
+ if (idsChanged) out.commentsIdsXml = serializer.serializeToString(idsDoc);
18256
+ if (extChanged) out.commentsExtensibleXml = serializer.serializeToString(extDoc);
18257
+ return out;
18258
+ }
18259
+
17993
18260
  // core/sha256.js
17994
18261
  var K = new Uint32Array([
17995
18262
  1116352408,
@@ -18482,7 +18749,7 @@ function readCommentDefinitions(commentsDoc) {
18482
18749
  author: attr2(comment, "author") || null,
18483
18750
  date: attr2(comment, "date") || null,
18484
18751
  text: paragraphs.map((p) => extractCanonicalParagraphText(p)).join("\n"),
18485
- paraId: paragraphs[0]?.getAttribute?.("w14:paraId") || paragraphs[0]?.getAttribute?.("paraId") || null
18752
+ paraId: commentParaId(comment) || null
18486
18753
  });
18487
18754
  }
18488
18755
  return result;
@@ -18713,43 +18980,6 @@ function inspectDocumentParts(parts, options = {}) {
18713
18980
  };
18714
18981
  }
18715
18982
 
18716
- // services/comment-builders.js
18717
- var NS_W14 = "http://schemas.microsoft.com/office/word/2010/wordml";
18718
- var NS_W15 = "http://schemas.microsoft.com/office/word/2012/wordml";
18719
- function createCommentParaId(commentId) {
18720
- const numeric = Number.parseInt(String(commentId), 10);
18721
- const value = Number.isFinite(numeric) ? 1879048192 + (numeric >>> 0) >>> 0 : 1879048192;
18722
- return value.toString(16).toUpperCase().padStart(8, "0").slice(-8);
18723
- }
18724
- function buildCommentElement(commentId, author, content, date, paraId = createCommentParaId(commentId)) {
18725
- const initials = author.split(" ").map((word) => word[0]).join("").toUpperCase() || "AI";
18726
- const escapedContent = escapeXml(content);
18727
- const escapedAuthor = escapeXml(author);
18728
- return `<w:comment w:id="${commentId}" w:author="${escapedAuthor}" w:date="${date}" w:initials="${initials}">
18729
- <w:p w14:paraId="${escapeXml(paraId)}" xmlns:w14="${NS_W14}">
18730
- <w:r><w:t>${escapedContent}</w:t></w:r>
18731
- </w:p>
18732
- </w:comment>`;
18733
- }
18734
- function buildCommentsPartXml(comments) {
18735
- if (!comments || comments.length === 0) {
18736
- return `<w:comments xmlns:w="${NS_W}"></w:comments>`;
18737
- }
18738
- const commentElements = comments.map(
18739
- (comment) => buildCommentElement(comment.id, comment.author, comment.content, comment.date, comment.paraId)
18740
- ).join("\n ");
18741
- return `<w:comments xmlns:w="${NS_W}">
18742
- ${commentElements}
18743
- </w:comments>`;
18744
- }
18745
- function buildCommentsExtendedPartXml(entries) {
18746
- const body = (entries || []).map((entry) => {
18747
- const parent = entry.paraIdParent ? ` w15:paraIdParent="${escapeXml(entry.paraIdParent)}"` : "";
18748
- return `<w15:commentEx w15:paraId="${escapeXml(entry.paraId)}"${parent} w15:done="${entry.done ? "1" : "0"}"/>`;
18749
- }).join("");
18750
- return `<w15:commentsEx xmlns:w15="${NS_W15}">${body}</w15:commentsEx>`;
18751
- }
18752
-
18753
18983
  // services/comment-locator.js
18754
18984
  function createParagraphTextIndex(paragraph, options = {}) {
18755
18985
  const revisionView = options.revisionView === "current" ? "accepted" : options.revisionView || "accepted";
@@ -19210,29 +19440,7 @@ function parseRequired(xml, partName) {
19210
19440
  }
19211
19441
  return { doc: parsed.doc };
19212
19442
  }
19213
- function usedParaIds(commentsDoc, extendedDoc) {
19214
- const ids = /* @__PURE__ */ new Set();
19215
- for (const p of Array.from(commentsDoc?.getElementsByTagNameNS("*", "p") || [])) {
19216
- const id = attr3(p, "w14:paraId", "paraId");
19217
- if (id) ids.add(id.toUpperCase());
19218
- }
19219
- for (const ex of Array.from(extendedDoc?.getElementsByTagNameNS("*", "commentEx") || [])) {
19220
- const id = attr3(ex, "w15:paraId", "paraId");
19221
- if (id) ids.add(id.toUpperCase());
19222
- }
19223
- return ids;
19224
- }
19225
- function allocateParaId(commentId, occupied) {
19226
- let candidate = createCommentParaId(commentId);
19227
- let value = Number.parseInt(candidate, 16) >>> 0;
19228
- while (occupied.has(candidate)) {
19229
- value = value + 1 >>> 0;
19230
- candidate = value.toString(16).toUpperCase().padStart(8, "0");
19231
- }
19232
- occupied.add(candidate);
19233
- return candidate;
19234
- }
19235
- function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, parentCommentId, commentId, commentContent, author, date = (/* @__PURE__ */ new Date()).toISOString() }) {
19443
+ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, documentXml = null, parentCommentId, commentId, commentContent, author, date = (/* @__PURE__ */ new Date()).toISOString() }) {
19236
19444
  if (!commentsXml) return { status: "error", error: { code: "COMMENTS_PART_MISSING", message: "A comment reply requires an existing word/comments.xml part." } };
19237
19445
  const commentsParsed = parseRequired(commentsXml, "word/comments.xml");
19238
19446
  if (commentsParsed.error) return { status: "error", error: commentsParsed.error };
@@ -19246,15 +19454,31 @@ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, par
19246
19454
  extendedDoc = parsed.doc;
19247
19455
  }
19248
19456
  const occupied = usedParaIds(commentsDoc, extendedDoc);
19249
- const parentParagraph = Array.from(parent.getElementsByTagNameNS(NS_W6, "p"))[0] || Array.from(parent.getElementsByTagNameNS("*", "p"))[0];
19457
+ if (documentXml) for (const match of String(documentXml).matchAll(/w14:paraId="([0-9A-Fa-f]{8})"/g)) occupied.add(match[1].toUpperCase());
19458
+ const parentParagraph = commentThreadParagraph(parent);
19250
19459
  if (!parentParagraph) return { status: "error", error: { code: "PARENT_COMMENT_INVALID", message: `Parent comment '${parentCommentId}' has no paragraph.` } };
19251
- let parentParaId = attr3(parentParagraph, "w14:paraId", "paraId");
19460
+ let parentParaId = commentParaId(parent);
19252
19461
  if (!parentParaId) {
19253
19462
  parentParaId = allocateParaId(parentCommentId, occupied);
19254
19463
  parentParagraph.setAttributeNS(NS_W14, "w14:paraId", parentParaId);
19255
19464
  } else {
19256
19465
  parentParaId = parentParaId.toUpperCase();
19257
19466
  }
19467
+ for (const node of Array.from(extendedDoc?.getElementsByTagNameNS("*", "commentEx") || [])) {
19468
+ if (attr3(node, "w15:paraId", "paraId").toUpperCase() === parentParaId) {
19469
+ const root2 = attr3(node, "w15:paraIdParent", "paraIdParent").toUpperCase();
19470
+ if (root2) parentParaId = root2;
19471
+ break;
19472
+ }
19473
+ }
19474
+ const threadCommentIds = [];
19475
+ const commentNodes = Array.from(commentsDoc.getElementsByTagNameNS("*", "comment"));
19476
+ const parentOf = new Map(Array.from(extendedDoc?.getElementsByTagNameNS("*", "commentEx") || []).map((node) => [attr3(node, "w15:paraId", "paraId").toUpperCase(), attr3(node, "w15:paraIdParent", "paraIdParent").toUpperCase()]));
19477
+ for (const node of commentNodes) {
19478
+ const key = commentParaId(node);
19479
+ if (key === parentParaId || parentOf.get(key) === parentParaId) threadCommentIds.push(attr3(node, "w:id", "id"));
19480
+ }
19481
+ const threadRootCommentId = attr3(commentNodes.find((node) => commentParaId(node) === parentParaId), "w:id", "id") || String(parentCommentId);
19258
19482
  const replyParaId = allocateParaId(commentId, occupied);
19259
19483
  const replyParsed = parseRequired(`<w:comments xmlns:w="${NS_W6}" xmlns:w14="${NS_W14}">${buildCommentElement(commentId, author, commentContent, date, replyParaId)}</w:comments>`, "reply comment");
19260
19484
  commentsDoc.documentElement.appendChild(commentsDoc.importNode(replyParsed.doc.documentElement.firstChild, true));
@@ -19263,16 +19487,18 @@ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, par
19263
19487
  }
19264
19488
  const root = extendedDoc.documentElement;
19265
19489
  const entries = Array.from(root.getElementsByTagNameNS("*", "commentEx"));
19266
- if (!entries.some((node) => attr3(node, "w15:paraId", "paraId").toUpperCase() === parentParaId)) {
19490
+ const rootEntry = entries.find((node) => attr3(node, "w15:paraId", "paraId").toUpperCase() === parentParaId);
19491
+ if (!rootEntry) {
19267
19492
  const parentEx = extendedDoc.createElementNS(NS_W15, "w15:commentEx");
19268
19493
  parentEx.setAttributeNS(NS_W15, "w15:paraId", parentParaId);
19269
19494
  parentEx.setAttributeNS(NS_W15, "w15:done", "0");
19270
19495
  root.appendChild(parentEx);
19271
19496
  }
19497
+ const threadDone = attr3(rootEntry, "w15:done", "done") === "1" ? "1" : "0";
19272
19498
  const replyEx = extendedDoc.createElementNS(NS_W15, "w15:commentEx");
19273
19499
  replyEx.setAttributeNS(NS_W15, "w15:paraId", replyParaId);
19274
19500
  replyEx.setAttributeNS(NS_W15, "w15:paraIdParent", parentParaId);
19275
- replyEx.setAttributeNS(NS_W15, "w15:done", "0");
19501
+ replyEx.setAttributeNS(NS_W15, "w15:done", threadDone);
19276
19502
  root.appendChild(replyEx);
19277
19503
  const serializer = createSerializer();
19278
19504
  return {
@@ -19285,7 +19511,9 @@ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, par
19285
19511
  commentId,
19286
19512
  parentCommentId: String(parentCommentId),
19287
19513
  paraId: replyParaId,
19288
- parentParaId
19514
+ parentParaId,
19515
+ threadRootCommentId,
19516
+ threadCommentIds: threadCommentIds.length ? threadCommentIds : [threadRootCommentId]
19289
19517
  };
19290
19518
  }
19291
19519
 
@@ -19350,19 +19578,19 @@ function applyFormattingRemovalToOoxml(ooxmlString, targetText, formatTypes) {
19350
19578
  if (!targetText || !ooxmlString) return ooxmlString;
19351
19579
  const doc = parseOoxml(ooxmlString);
19352
19580
  if (!doc) return ooxmlString;
19353
- const NS_W10 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19354
- const runs = doc.getElementsByTagNameNS(NS_W10, "r");
19355
- const insertions = doc.getElementsByTagNameNS(NS_W10, "ins");
19581
+ const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19582
+ const runs = doc.getElementsByTagNameNS(NS_W11, "r");
19583
+ const insertions = doc.getElementsByTagNameNS(NS_W11, "ins");
19356
19584
  const allRuns = [...Array.from(runs)];
19357
19585
  for (const ins of insertions) {
19358
- const insideRuns = ins.getElementsByTagNameNS(NS_W10, "r");
19586
+ const insideRuns = ins.getElementsByTagNameNS(NS_W11, "r");
19359
19587
  allRuns.push(...Array.from(insideRuns));
19360
19588
  }
19361
19589
  for (const run of allRuns) {
19362
- const textNodes = run.getElementsByTagNameNS(NS_W10, "t");
19590
+ const textNodes = run.getElementsByTagNameNS(NS_W11, "t");
19363
19591
  const runText = Array.from(textNodes).map((t) => t.textContent).join("");
19364
19592
  if (runText.includes(targetText) || runText === targetText) {
19365
- const rPrElements = run.getElementsByTagNameNS(NS_W10, "rPr");
19593
+ const rPrElements = run.getElementsByTagNameNS(NS_W11, "rPr");
19366
19594
  if (rPrElements.length > 0) {
19367
19595
  const rPr = rPrElements[0];
19368
19596
  const newRPr = removeFormattingFromRPr(rPr, formatTypes);
@@ -19397,7 +19625,7 @@ var HIGHLIGHT_COLOR_MAP = {
19397
19625
  "white": "white"
19398
19626
  };
19399
19627
  function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
19400
- const NS_W10 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19628
+ const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19401
19629
  const ooxmlColor = HIGHLIGHT_COLOR_MAP[color.toLowerCase()] || "yellow";
19402
19630
  const generateRedlines = options?.generateRedlines ?? false;
19403
19631
  const author = options?.author || getDefaultAuthor();
@@ -19416,10 +19644,10 @@ function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
19416
19644
  }
19417
19645
  });
19418
19646
  }
19419
- const existingHighlight = rPrElement.getElementsByTagNameNS(NS_W10, "highlight");
19647
+ const existingHighlight = rPrElement.getElementsByTagNameNS(NS_W11, "highlight");
19420
19648
  Array.from(existingHighlight).forEach(removeNode2);
19421
19649
  const highlightEl = createWordElement(doc, "w:highlight");
19422
- highlightEl.setAttributeNS(NS_W10, "w:val", ooxmlColor);
19650
+ highlightEl.setAttributeNS(NS_W11, "w:val", ooxmlColor);
19423
19651
  rPrElement.appendChild(highlightEl);
19424
19652
  if (generateRedlines && previousRPrState) {
19425
19653
  const rPrChange = createWordElement(doc, "w:rPrChange");
@@ -19428,7 +19656,7 @@ function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
19428
19656
  rPrChange.setAttribute("w:author", metadata.author);
19429
19657
  rPrChange.setAttribute("w:date", metadata.date);
19430
19658
  rPrChange.appendChild(previousRPrState);
19431
- const existingChange = rPrElement.getElementsByTagNameNS(NS_W10, "rPrChange");
19659
+ const existingChange = rPrElement.getElementsByTagNameNS(NS_W11, "rPrChange");
19432
19660
  Array.from(existingChange).forEach(removeNode2);
19433
19661
  rPrElement.appendChild(rPrChange);
19434
19662
  }
@@ -19445,12 +19673,12 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
19445
19673
  } else {
19446
19674
  revisionIdAllocator = createRevisionIdAllocator(doc);
19447
19675
  }
19448
- const NS_W10 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19676
+ const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19449
19677
  const getRunText = (run) => {
19450
- const textNodes = run.getElementsByTagNameNS(NS_W10, "t");
19678
+ const textNodes = run.getElementsByTagNameNS(NS_W11, "t");
19451
19679
  return Array.from(textNodes).map((t) => t.textContent).join("");
19452
19680
  };
19453
- const allRuns = Array.from(doc.getElementsByTagNameNS(NS_W10, "r"));
19681
+ const allRuns = Array.from(doc.getElementsByTagNameNS(NS_W11, "r"));
19454
19682
  const sourceRunsWithClaimedRevisionIds = /* @__PURE__ */ new WeakSet();
19455
19683
  const cloneRunWithText2 = (sourceRun, text2, shouldHighlight) => {
19456
19684
  const clonedRun = sourceRun.cloneNode(true);
@@ -19459,14 +19687,14 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
19459
19687
  } else {
19460
19688
  sourceRunsWithClaimedRevisionIds.add(sourceRun);
19461
19689
  }
19462
- const textNodes = clonedRun.getElementsByTagNameNS(NS_W10, "t");
19690
+ const textNodes = clonedRun.getElementsByTagNameNS(NS_W11, "t");
19463
19691
  Array.from(textNodes).forEach(removeNode2);
19464
19692
  const newText = createWordElement(doc, "w:t");
19465
19693
  newText.setAttribute("xml:space", "preserve");
19466
19694
  newText.textContent = text2;
19467
19695
  clonedRun.appendChild(newText);
19468
19696
  if (shouldHighlight) {
19469
- const rPrElements = clonedRun.getElementsByTagNameNS(NS_W10, "rPr");
19697
+ const rPrElements = clonedRun.getElementsByTagNameNS(NS_W11, "rPr");
19470
19698
  const existingRPr = rPrElements.length > 0 ? rPrElements[0] : null;
19471
19699
  const newRPr = injectHighlightIntoRPr(doc, existingRPr, color, options);
19472
19700
  if (existingRPr) {
@@ -19515,20 +19743,68 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
19515
19743
  return serializeOoxml(doc);
19516
19744
  }
19517
19745
 
19746
+ // services/package-parts.js
19747
+ var WORD_ML = "application/vnd.openxmlformats-officedocument.wordprocessingml";
19748
+ var REL_OFFICE_DOC = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
19749
+ var PART_SPECS = Object.freeze([
19750
+ { id: "document", path: "word/document.xml", pathPattern: null, contentType: `${WORD_ML}.document.main+xml`, legacyContentTypes: [], relType: `${REL_OFFICE_DOC}/officeDocument`, relTarget: null, verified: true },
19751
+ { id: "numbering", path: "word/numbering.xml", pathPattern: null, contentType: `${WORD_ML}.numbering+xml`, legacyContentTypes: [], relType: `${REL_OFFICE_DOC}/numbering`, relTarget: "numbering.xml", verified: false },
19752
+ { id: "comments", path: "word/comments.xml", pathPattern: null, contentType: `${WORD_ML}.comments+xml`, legacyContentTypes: [], relType: `${REL_OFFICE_DOC}/comments`, relTarget: "comments.xml", verified: true },
19753
+ {
19754
+ id: "commentsExtended",
19755
+ path: "word/commentsExtended.xml",
19756
+ pathPattern: null,
19757
+ contentType: `${WORD_ML}.commentsExtended+xml`,
19758
+ // Written by this package through 0.8.0; Word does not recognize it and offers to repair the file.
19759
+ legacyContentTypes: ["application/vnd.ms-word.commentsExtended+xml"],
19760
+ relType: "http://schemas.microsoft.com/office/2011/relationships/commentsExtended",
19761
+ relTarget: "commentsExtended.xml",
19762
+ verified: true
19763
+ },
19764
+ { 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 },
19765
+ { 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 },
19766
+ // Not yet produced by any Word fixture (Word only writes people.xml for signed-in authors): unverified.
19767
+ { 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 },
19768
+ { 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 },
19769
+ { 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 }
19770
+ ].map((spec) => Object.freeze(spec)));
19771
+ var BY_ID = new Map(PART_SPECS.map((spec) => [spec.id, spec]));
19772
+ function getPartSpec(id) {
19773
+ const spec = BY_ID.get(id);
19774
+ if (!spec) throw new Error(`Unknown package part: ${id}`);
19775
+ return spec;
19776
+ }
19777
+ function toEntryName(partName) {
19778
+ return String(partName || "").replace(/^\/+/, "");
19779
+ }
19780
+ function findPartSpecByPartName(partName) {
19781
+ const name = toEntryName(partName);
19782
+ const lower = name.toLowerCase();
19783
+ return PART_SPECS.find((spec) => spec.path ? spec.path.toLowerCase() === lower : spec.pathPattern?.test(name)) || null;
19784
+ }
19785
+ function correctedContentTypeFor(partName, actualContentType) {
19786
+ const spec = findPartSpecByPartName(partName);
19787
+ if (!spec || !spec.legacyContentTypes.includes(actualContentType)) return null;
19788
+ return spec.contentType;
19789
+ }
19790
+
19518
19791
  // services/standalone-docx-plumbing.js
19519
19792
  var NS_W7 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19520
19793
  var NS_CT = "http://schemas.openxmlformats.org/package/2006/content-types";
19521
19794
  var NS_RELS = "http://schemas.openxmlformats.org/package/2006/relationships";
19522
- var NUMBERING_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering";
19523
- var NUMBERING_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml";
19524
- var COMMENTS_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
19525
- var COMMENTS_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml";
19526
- var COMMENTS_EXTENDED_REL_TYPE = "http://schemas.microsoft.com/office/2011/relationships/commentsExtended";
19527
- var COMMENTS_EXTENDED_CONTENT_TYPE = "application/vnd.ms-word.commentsExtended+xml";
19528
- var DOCUMENT_PATH = "word/document.xml";
19529
- var NUMBERING_PATH = "word/numbering.xml";
19530
- var COMMENTS_PATH = "word/comments.xml";
19531
- var COMMENTS_EXTENDED_PATH = "word/commentsExtended.xml";
19795
+ var NUMBERING = getPartSpec("numbering");
19796
+ var COMMENTS = getPartSpec("comments");
19797
+ var COMMENTS_EXTENDED = getPartSpec("commentsExtended");
19798
+ var NUMBERING_REL_TYPE = NUMBERING.relType;
19799
+ var NUMBERING_CONTENT_TYPE = NUMBERING.contentType;
19800
+ var COMMENTS_REL_TYPE = COMMENTS.relType;
19801
+ var COMMENTS_CONTENT_TYPE = COMMENTS.contentType;
19802
+ var COMMENTS_EXTENDED_REL_TYPE = COMMENTS_EXTENDED.relType;
19803
+ var COMMENTS_EXTENDED_CONTENT_TYPE = COMMENTS_EXTENDED.contentType;
19804
+ var DOCUMENT_PATH = getPartSpec("document").path;
19805
+ var NUMBERING_PATH = NUMBERING.path;
19806
+ var COMMENTS_PATH = COMMENTS.path;
19807
+ var COMMENTS_EXTENDED_PATH = COMMENTS_EXTENDED.path;
19532
19808
  var CONTENT_TYPES_PATH = "[Content_Types].xml";
19533
19809
  var DOCUMENT_RELS_PATH = "word/_rels/document.xml.rels";
19534
19810
  function parseXmlStrictStandalone(xmlText, label = "xml") {
@@ -19816,6 +20092,22 @@ async function ensureCommentsExtendedArtifactsInZip(zip, commentsExtendedXml, op
19816
20092
  }
19817
20093
  }
19818
20094
  }
20095
+ async function repairKnownContentTypes(zip) {
20096
+ const ctText = await readZipText(zip, CONTENT_TYPES_PATH);
20097
+ if (!ctText) return [];
20098
+ const ctDoc = parseXmlStrictStandalone(ctText, CONTENT_TYPES_PATH);
20099
+ const repairs = [];
20100
+ for (const override of Array.from(ctDoc.getElementsByTagNameNS("*", "Override"))) {
20101
+ const partName = override.getAttribute("PartName") || "";
20102
+ const actual = override.getAttribute("ContentType") || "";
20103
+ const expected = correctedContentTypeFor(partName, actual);
20104
+ if (!expected) continue;
20105
+ override.setAttribute("ContentType", expected);
20106
+ repairs.push({ partName, from: actual, to: expected });
20107
+ }
20108
+ if (repairs.length > 0) zip.file(CONTENT_TYPES_PATH, createSerializer().serializeToString(ctDoc));
20109
+ return repairs;
20110
+ }
19819
20111
  async function validateDocxPackage(zip) {
19820
20112
  const documentXml = await readZipText(zip, DOCUMENT_PATH);
19821
20113
  if (!documentXml) {
@@ -19897,9 +20189,8 @@ async function validateDocxPackage(zip) {
19897
20189
  const paraIdToCommentId = /* @__PURE__ */ new Map();
19898
20190
  for (const comment of Array.from(commentsDoc.getElementsByTagNameNS("*", "comment"))) {
19899
20191
  const id = comment.getAttribute("w:id") || comment.getAttribute("id");
19900
- const paragraph = Array.from(comment.getElementsByTagNameNS("*", "p"))[0];
19901
- const paraId = paragraph?.getAttribute("w14:paraId") || paragraph?.getAttribute("paraId");
19902
- if (paraId) paraIdToCommentId.set(paraId.toUpperCase(), id);
20192
+ const paraId = commentParaId(comment);
20193
+ if (paraId) paraIdToCommentId.set(paraId, id);
19903
20194
  }
19904
20195
  const knownParaIds = new Set(paraIdToCommentId.keys());
19905
20196
  const extendedParaIds = /* @__PURE__ */ new Set();
@@ -19960,10 +20251,39 @@ async function validateDocxPackage(zip) {
19960
20251
  throw new Error("Validation failed: comments rel missing");
19961
20252
  }
19962
20253
  }
20254
+ {
20255
+ const relationshipById = /* @__PURE__ */ new Map();
20256
+ for (const rel of Array.from(relsDoc.getElementsByTagNameNS("*", "Relationship"))) {
20257
+ relationshipById.set(rel.getAttribute("Id") || "", { type: rel.getAttribute("Type") || "", target: rel.getAttribute("Target") || "" });
20258
+ }
20259
+ const headerSpec = getPartSpec("header");
20260
+ const footerSpec = getPartSpec("footer");
20261
+ const overrideTypeByPart = new Map(Array.from(ctDoc.getElementsByTagNameNS("*", "Override")).map((override) => [(override.getAttribute("PartName") || "").toLowerCase(), override.getAttribute("ContentType") || ""]));
20262
+ for (const reference of [
20263
+ ...Array.from(documentDoc.getElementsByTagNameNS(NS_W7, "headerReference")),
20264
+ ...Array.from(documentDoc.getElementsByTagNameNS(NS_W7, "footerReference"))
20265
+ ]) {
20266
+ const relId = reference.getAttributeNS?.("http://schemas.openxmlformats.org/officeDocument/2006/relationships", "id") || reference.getAttribute("r:id") || "";
20267
+ const spec = reference.localName === "headerReference" ? headerSpec : footerSpec;
20268
+ const relationship = relationshipById.get(relId);
20269
+ if (!relationship) throw new Error(`Validation failed: ${reference.localName} ${relId || "(no r:id)"} has no document relationship`);
20270
+ if (relationship.type !== spec.relType) throw new Error(`Validation failed: ${reference.localName} ${relId} points at a relationship of the wrong type (${relationship.type})`);
20271
+ const partName = relationship.target.startsWith("/") ? relationship.target : `/word/${relationship.target}`;
20272
+ const partXml = await readZipText(zip, partName.slice(1));
20273
+ if (!partXml) throw new Error(`Validation failed: ${reference.localName} ${relId} targets a missing part: ${partName}`);
20274
+ parseXmlStrictStandalone(partXml, partName.slice(1));
20275
+ const contentType = overrideTypeByPart.get(partName.toLowerCase());
20276
+ if (contentType !== spec.contentType) throw new Error(`Validation failed: ${partName} content type is ${contentType ? `'${contentType}'` : "missing"}; Word expects '${spec.contentType}'`);
20277
+ }
20278
+ }
19963
20279
  if (commentsExtendedXml) {
19964
- const hasContentType = Array.from(ctDoc.getElementsByTagNameNS("*", "Override")).some(
19965
- (override) => (override.getAttribute("PartName") || "").toLowerCase() === "/word/commentsextended.xml" && (override.getAttribute("ContentType") || "") === COMMENTS_EXTENDED_CONTENT_TYPE
20280
+ const extendedOverride = Array.from(ctDoc.getElementsByTagNameNS("*", "Override")).find(
20281
+ (override) => (override.getAttribute("PartName") || "").toLowerCase() === "/word/commentsextended.xml"
19966
20282
  );
20283
+ if (extendedOverride && extendedOverride.getAttribute("ContentType") !== COMMENTS_EXTENDED_CONTENT_TYPE) {
20284
+ throw new Error(`Validation failed: commentsExtended CT override has wrong content type '${extendedOverride.getAttribute("ContentType")}' (Word expects '${COMMENTS_EXTENDED_CONTENT_TYPE}')`);
20285
+ }
20286
+ const hasContentType = !!extendedOverride;
19967
20287
  const hasRelationship = Array.from(relsDoc.getElementsByTagNameNS("*", "Relationship")).some(
19968
20288
  (rel) => (rel.getAttribute("Type") || "") === COMMENTS_EXTENDED_REL_TYPE
19969
20289
  );
@@ -20327,6 +20647,156 @@ function reconcileReceiptsAgainstOutput(parts, receipts) {
20327
20647
  return { valid: true };
20328
20648
  }
20329
20649
 
20650
+ // services/headers-footers.js
20651
+ var NS_W8 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
20652
+ var NS_R2 = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
20653
+ var REVISION_ELEMENTS = /* @__PURE__ */ new Set(["ins", "del", "moveFrom", "moveTo", "rPrChange", "pPrChange", "sectPrChange", "tblPrChange", "tblGridChange", "tcPrChange", "trPrChange", "numberingChange"]);
20654
+ var REVISION_CONTENT_ELEMENTS = /* @__PURE__ */ new Set(["ins", "del", "moveFrom", "moveTo"]);
20655
+ var attrOf2 = (node, qualified, local) => node?.getAttribute?.(qualified) || node?.getAttribute?.(local) || "";
20656
+ function resolveTarget(target) {
20657
+ const clean = String(target || "");
20658
+ if (clean.startsWith("/")) return clean.replace(/^\/+/, "");
20659
+ const parts = ["word"];
20660
+ for (const segment of clean.split("/")) {
20661
+ if (segment === "..") parts.pop();
20662
+ else if (segment && segment !== ".") parts.push(segment);
20663
+ }
20664
+ return parts.join("/");
20665
+ }
20666
+ function discoverHeaderFooterParts({ documentXml, relsXml = null, settingsXml = null }) {
20667
+ if (!documentXml || !relsXml) return [];
20668
+ const documentParsed = parseOoxmlSafe(documentXml, "application/xml");
20669
+ const relsParsed = parseOoxmlSafe(relsXml, "application/xml");
20670
+ if (!documentParsed.doc || documentParsed.error || !relsParsed.doc || relsParsed.error) return [];
20671
+ const targetByRelId = /* @__PURE__ */ new Map();
20672
+ for (const rel of Array.from(relsParsed.doc.getElementsByTagNameNS("*", "Relationship"))) {
20673
+ targetByRelId.set(rel.getAttribute("Id") || "", rel.getAttribute("Target") || "");
20674
+ }
20675
+ const evenAndOdd = !!settingsXml && /<w:evenAndOddHeaders\b(?![^>]*w:val="(?:0|false|off)")/.test(settingsXml);
20676
+ const byPath = /* @__PURE__ */ new Map();
20677
+ const inForce = /* @__PURE__ */ new Map();
20678
+ const sectPrs = Array.from(documentParsed.doc.getElementsByTagNameNS(NS_W8, "sectPr"));
20679
+ sectPrs.forEach((sectPr, sectionIndex) => {
20680
+ const titlePg = Array.from(sectPr.childNodes || []).some((child) => child.localName === "titlePg" && !/^(0|false|off)$/i.test(attrOf2(child, "w:val", "val")));
20681
+ for (const ref of Array.from(sectPr.childNodes || [])) {
20682
+ if (ref.nodeType !== 1 || ref.localName !== "headerReference" && ref.localName !== "footerReference") continue;
20683
+ const relId = ref.getAttributeNS?.(NS_R2, "id") || ref.getAttribute("r:id") || "";
20684
+ const target = targetByRelId.get(relId);
20685
+ if (!target) continue;
20686
+ const path = resolveTarget(target);
20687
+ const kind = ref.localName === "headerReference" ? "header" : "footer";
20688
+ const type = attrOf2(ref, "w:type", "type") || "default";
20689
+ const entry = byPath.get(path) || { path, kind, type, sections: [], applies: [], titlePgSections: 0 };
20690
+ if (!entry.sections.includes(sectionIndex)) entry.sections.push(sectionIndex);
20691
+ if (titlePg) entry.titlePgSections += 1;
20692
+ byPath.set(path, entry);
20693
+ inForce.set(`${kind}:${type}`, path);
20694
+ }
20695
+ for (const path of inForce.values()) {
20696
+ const entry = byPath.get(path);
20697
+ if (entry && !entry.applies.includes(sectionIndex)) entry.applies.push(sectionIndex);
20698
+ }
20699
+ });
20700
+ return Array.from(byPath.values()).map((entry) => ({
20701
+ path: entry.path,
20702
+ kind: entry.kind,
20703
+ type: entry.type,
20704
+ sections: entry.sections,
20705
+ appliesToSections: entry.applies,
20706
+ sharedBySections: entry.applies.length > 1,
20707
+ // first-page parts only show with w:titlePg; even parts only with w:evenAndOddHeaders in settings.xml.
20708
+ active: entry.type === "first" ? entry.titlePgSections > 0 : entry.type === "even" ? evenAndOdd : true
20709
+ }));
20710
+ }
20711
+ function resolvePartSelector(parts, selector) {
20712
+ const describe = (part) => ({ path: part.path, kind: part.kind, type: part.type, sections: part.sections, appliesToSections: part.appliesToSections });
20713
+ if (typeof selector === "string") {
20714
+ const wanted = selector.replace(/^\/+/, "").toLowerCase();
20715
+ const match = parts.find((part) => part.path.toLowerCase() === wanted || part.path.toLowerCase() === `word/${wanted}`);
20716
+ if (match) return { part: match };
20717
+ return { error: { code: "PART_NOT_FOUND", message: `No header/footer part '${selector}' is referenced by the document.`, candidates: parts.map(describe) } };
20718
+ }
20719
+ if (!selector || typeof selector !== "object" || selector.kind !== "header" && selector.kind !== "footer") {
20720
+ return { error: { code: "INVALID_OPERATION", message: 'part must be a part path or { kind: "header"|"footer", type?: "default"|"first"|"even", section?: number }.' } };
20721
+ }
20722
+ const type = selector.type || "default";
20723
+ if (!["default", "first", "even"].includes(type)) {
20724
+ return { error: { code: "INVALID_OPERATION", message: `part.type must be default, first or even (got '${type}').` } };
20725
+ }
20726
+ let matches = parts.filter((part) => part.kind === selector.kind && part.type === type);
20727
+ if (Number.isInteger(selector.section)) matches = matches.filter((part) => part.appliesToSections.includes(selector.section));
20728
+ if (matches.length === 1) return { part: matches[0] };
20729
+ if (matches.length === 0) {
20730
+ 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) } };
20731
+ }
20732
+ return { error: { code: "PART_AMBIGUOUS", message: `Several ${type} ${selector.kind} parts exist; add "section" or use the part path.`, candidates: matches.map(describe) } };
20733
+ }
20734
+ function isHeaderFooterPath(path) {
20735
+ const spec = findPartSpecByPartName(path);
20736
+ return spec?.id === "header" || spec?.id === "footer";
20737
+ }
20738
+ function countRevisionsInsideFields(xml) {
20739
+ const parsed = parseOoxmlSafe(xml, "application/xml");
20740
+ if (!parsed.doc || parsed.error) return 0;
20741
+ let depth = 0;
20742
+ let count = 0;
20743
+ const visit = (node) => {
20744
+ for (const child of Array.from(node.childNodes || [])) {
20745
+ if (child.nodeType !== 1) continue;
20746
+ const name = child.localName;
20747
+ if (name === "fldChar") {
20748
+ const type = attrOf2(child, "w:fldCharType", "fldCharType");
20749
+ if (type === "begin") depth += 1;
20750
+ else if (type === "end") depth = Math.max(0, depth - 1);
20751
+ continue;
20752
+ }
20753
+ if (REVISION_CONTENT_ELEMENTS.has(name) && depth > 0) count += 1;
20754
+ if (name === "fldSimple") {
20755
+ depth += 1;
20756
+ visit(child);
20757
+ depth -= 1;
20758
+ continue;
20759
+ }
20760
+ visit(child);
20761
+ }
20762
+ };
20763
+ visit(parsed.doc.documentElement);
20764
+ return count;
20765
+ }
20766
+ function collectRevisionIds(xml) {
20767
+ const ids = /* @__PURE__ */ new Set();
20768
+ 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]);
20769
+ return ids;
20770
+ }
20771
+ function renumberCollidingRevisionIds(xml, taken) {
20772
+ const parsed = parseOoxmlSafe(xml, "application/xml");
20773
+ if (!parsed.doc || parsed.error) return { xml, renumbered: 0, idMap: /* @__PURE__ */ new Map() };
20774
+ const idMap = /* @__PURE__ */ new Map();
20775
+ let next = 1 + Math.max(0, ...Array.from(taken, (id) => Number(id)).filter(Number.isFinite));
20776
+ const used = new Set(taken);
20777
+ let renumbered = 0;
20778
+ const visit = (node) => {
20779
+ for (const child of Array.from(node.childNodes || [])) {
20780
+ if (child.nodeType !== 1) continue;
20781
+ if (REVISION_ELEMENTS.has(child.localName)) {
20782
+ const id = attrOf2(child, "w:id", "id");
20783
+ if (id && used.has(id)) {
20784
+ while (used.has(String(next))) next += 1;
20785
+ child.setAttributeNS(NS_W8, "w:id", String(next));
20786
+ idMap.set(id, String(next));
20787
+ used.add(String(next));
20788
+ renumbered += 1;
20789
+ } else if (id) {
20790
+ used.add(id);
20791
+ }
20792
+ }
20793
+ visit(child);
20794
+ }
20795
+ };
20796
+ visit(parsed.doc.documentElement);
20797
+ return renumbered > 0 ? { xml: createSerializer().serializeToString(parsed.doc), renumbered, idMap } : { xml, renumbered: 0, idMap };
20798
+ }
20799
+
20330
20800
  // services/error-recovery.js
20331
20801
  var ERROR_RECOVERY_VERSION = 1;
20332
20802
  var RULES = Object.freeze({
@@ -20364,6 +20834,12 @@ var RULES = Object.freeze({
20364
20834
  STRUCTURED_CONTENT_INVALID: ["request", "request_fixable", "change_request"],
20365
20835
  UNSUPPORTED_INSERTION_AFFINITY: ["request", "request_fixable", "change_request"],
20366
20836
  EXISTING_REVISIONS: ["target-safety", "policy_choice_required", "set_option"],
20837
+ PART_NOT_FOUND: ["target-resolution", "target_refresh_required", "reinspect"],
20838
+ PART_AMBIGUOUS: ["target-resolution", "candidate_selection_required", "choose_candidate"],
20839
+ COMMENT_IN_HEADER_FOOTER: ["request", "request_fixable", "change_request"],
20840
+ FIELD_EDIT_REFUSED: ["target-safety", "request_fixable", "change_request"],
20841
+ COMMENT_NOT_FOUND: ["target-resolution", "target_refresh_required", "reinspect"],
20842
+ PARENT_ANCHOR_NOT_FOUND: ["target-resolution", "target_refresh_required", "reinspect"],
20367
20843
  COMMENTED_CONTENT_MERGE: ["target-safety", "user_authorization_required", "resolve_comments"],
20368
20844
  COMMENTED_CONTENT_DELETE: ["target-safety", "user_authorization_required", "resolve_comments"],
20369
20845
  OVERLAPPING_SOURCE_TARGETS: ["batch-compilation", "source_conflict", "consolidate_operations"],
@@ -20816,6 +21292,7 @@ var SUPPORTED_OPERATION_TYPES = /* @__PURE__ */ new Set([
20816
21292
  "delete",
20817
21293
  "comment",
20818
21294
  "comment_reply",
21295
+ "comment_resolve",
20819
21296
  "highlight"
20820
21297
  ]);
20821
21298
  var EXISTING_REVISIONS_POLICIES = Object.freeze([
@@ -20834,11 +21311,14 @@ function isRecord(value) {
20834
21311
  function nonEmptyString(value) {
20835
21312
  return typeof value === "string" && value.trim().length > 0;
20836
21313
  }
21314
+ function isCommentThreadOperationKind(kind) {
21315
+ return kind === "comment_reply" || kind === "comment_resolve";
21316
+ }
20837
21317
  function getCanonicalOperationType(operation) {
20838
21318
  const type = operation?.type;
20839
21319
  if (type === "insert" && operation?.target?.revisionView === "rejected") return "rejected-insert";
20840
21320
  if (type === "restore") return "restore";
20841
- if (type === "comment" || type === "comment_reply" || type === "highlight") return type;
21321
+ if (type === "comment" || type === "comment_reply" || type === "comment_resolve" || type === "highlight") return type;
20842
21322
  if (type === "paragraph-format") return "paragraph-format";
20843
21323
  if (type === "character-format" || type === "format" && (operation?.textToFormat != null || operation?.properties != null)) return "format";
20844
21324
  return "redline";
@@ -20958,7 +21438,7 @@ function validateDocumentOperation(operation) {
20958
21438
  };
20959
21439
  }
20960
21440
  const target = normalized.targetDescriptor;
20961
- if (normalized.operationKind !== "comment_reply" && !nonEmptyString(target.text) && target.index == null && !target.paragraphId && !target.fingerprint && !target.captureRef) {
21441
+ if (!isCommentThreadOperationKind(normalized.operationKind) && !nonEmptyString(target.text) && target.index == null && !target.paragraphId && !target.fingerprint && !target.captureRef) {
20962
21442
  return {
20963
21443
  valid: false,
20964
21444
  error: {
@@ -21185,6 +21665,14 @@ function validateDocumentOperation(operation) {
21185
21665
  return { valid: false, error: { code: "INVALID_OPERATION", message: 'Comment reply operations require a non-empty "commentContent" field.' } };
21186
21666
  }
21187
21667
  }
21668
+ if (normalized.operationKind === "comment_resolve") {
21669
+ if (!nonEmptyString(String(normalized.commentId ?? ""))) {
21670
+ return { valid: false, error: { code: "INVALID_OPERATION", message: 'Comment resolve operations require a "commentId".' } };
21671
+ }
21672
+ if (normalized.resolved != null && typeof normalized.resolved !== "boolean") {
21673
+ return { valid: false, error: { code: "INVALID_OPERATION", message: 'Comment resolve "resolved" must be a boolean when provided.' } };
21674
+ }
21675
+ }
21188
21676
  if (normalized.operationKind === "highlight") {
21189
21677
  if (!nonEmptyString(normalized.textToHighlight)) {
21190
21678
  return {
@@ -21664,7 +22152,7 @@ function compileOperationBatch(xmlDoc, operations = [], options = {}) {
21664
22152
  const dynamicOccurrence = operation.targetDescriptor?.occurrence != null && !operation.targetDescriptor?.paragraphId && !operation.targetDescriptor?.fingerprint;
21665
22153
  const compositeTextTarget = typeof operation.targetDescriptor?.text === "string" && /\r|\n/.test(operation.targetDescriptor.text) && !operation.targetEndDescriptor;
21666
22154
  if (dynamicOccurrence || compositeTextTarget) binding.dynamic = true;
21667
- if (validation.valid && operation.operationKind !== "comment_reply" && !operation.targetDescriptor?.captureRef && !binding.dynamic) {
22155
+ if (validation.valid && operation.operationKind !== "comment_reply" && operation.operationKind !== "comment_resolve" && !operation.targetDescriptor?.captureRef && !binding.dynamic) {
21668
22156
  try {
21669
22157
  const start = resolveDescriptor(
21670
22158
  xmlDoc,
@@ -22165,12 +22653,12 @@ function resolveTargetFromCapture(xmlDoc, session, targetDescriptor, _opType = "
22165
22653
  }
22166
22654
 
22167
22655
  // services/document-operation-mutations.js
22168
- var NS_W8 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
22656
+ var NS_W9 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
22169
22657
  var NS_W142 = "http://schemas.microsoft.com/office/word/2010/wordml";
22170
22658
  function getCommentIdsInElement(element) {
22171
22659
  const ids = /* @__PURE__ */ new Set();
22172
22660
  for (const localName2 of ["commentRangeStart", "commentRangeEnd", "commentReference"]) {
22173
- for (const node of Array.from(element?.getElementsByTagNameNS?.(NS_W8, localName2) || [])) {
22661
+ for (const node of Array.from(element?.getElementsByTagNameNS?.(NS_W9, localName2) || [])) {
22174
22662
  const id = node.getAttribute("w:id") || node.getAttribute("id");
22175
22663
  if (id !== "") ids.add(id);
22176
22664
  }
@@ -22384,7 +22872,7 @@ function normalizeBodySectionOrder(xmlDoc) {
22384
22872
  normalizeBodySectionOrderStandalone(xmlDoc);
22385
22873
  }
22386
22874
  function removeProofErrNodes(paragraph) {
22387
- for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W8, "proofErr") || [])) {
22875
+ for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W9, "proofErr") || [])) {
22388
22876
  node.parentNode?.removeChild(node);
22389
22877
  }
22390
22878
  }
@@ -22395,12 +22883,12 @@ function preprocessRedlineTargetParagraph(targetParagraph) {
22395
22883
  function getDirectWordChild4(element, localName2) {
22396
22884
  if (!element) return null;
22397
22885
  return Array.from(element.childNodes || []).find(
22398
- (node) => node && node.nodeType === 1 && node.namespaceURI === NS_W8 && node.localName === localName2
22886
+ (node) => node && node.nodeType === 1 && node.namespaceURI === NS_W9 && node.localName === localName2
22399
22887
  ) || null;
22400
22888
  }
22401
22889
  function computeTableIndexInDocument(xmlDoc, tableElement) {
22402
22890
  if (!xmlDoc || !tableElement) return null;
22403
- const tables = Array.from(xmlDoc.getElementsByTagNameNS(NS_W8, "tbl"));
22891
+ const tables = Array.from(xmlDoc.getElementsByTagNameNS(NS_W9, "tbl"));
22404
22892
  const idx = tables.indexOf(tableElement);
22405
22893
  return idx >= 0 ? idx + 1 : null;
22406
22894
  }
@@ -22450,7 +22938,7 @@ function buildInsertedListParagraph(xmlDoc, anchorParagraph, entry, revisionMeta
22450
22938
  markParagraphMarkInserted(xmlDoc, paragraph, author);
22451
22939
  }
22452
22940
  const run = createWordElement(xmlDoc, "w:r");
22453
- const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W8, "r"))[0] || null;
22941
+ const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W9, "r"))[0] || null;
22454
22942
  const anchorRunPr = anchorFirstRun ? getDirectWordChild4(anchorFirstRun, "rPr") : null;
22455
22943
  if (anchorRunPr) {
22456
22944
  run.appendChild(clonePropertiesWithoutRevisionHistory(anchorRunPr));
@@ -22475,7 +22963,7 @@ function buildInsertedListParagraph(xmlDoc, anchorParagraph, entry, revisionMeta
22475
22963
  }
22476
22964
  function serializeParagraphRangeAsDocument(paragraphs, serializer) {
22477
22965
  const paragraphXml = (paragraphs || []).map((paragraph) => serializer.serializeToString(paragraph)).join("");
22478
- return `<w:document xmlns:w="${NS_W8}"><w:body>${paragraphXml}</w:body></w:document>`;
22966
+ return `<w:document xmlns:w="${NS_W9}"><w:body>${paragraphXml}</w:body></w:document>`;
22479
22967
  }
22480
22968
  function applyExplicitRangeListInsertions({
22481
22969
  xmlDoc,
@@ -22544,7 +23032,7 @@ function buildEmptyParagraphTemplateFromAnchor(xmlDoc, anchorParagraph) {
22544
23032
  const anchorPPr = getDirectWordChild4(anchorParagraph, "pPr");
22545
23033
  if (anchorPPr) paragraph.appendChild(clonePropertiesWithoutRevisionHistory(anchorPPr));
22546
23034
  const run = createWordElement(xmlDoc, "w:r");
22547
- const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W8, "r"))[0] || null;
23035
+ const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W9, "r"))[0] || null;
22548
23036
  const anchorRunPr = anchorFirstRun ? getDirectWordChild4(anchorFirstRun, "rPr") : null;
22549
23037
  if (anchorRunPr) run.appendChild(clonePropertiesWithoutRevisionHistory(anchorRunPr));
22550
23038
  const textNode = createWordElement(xmlDoc, "w:t");
@@ -22567,7 +23055,7 @@ function wrapParagraphContentInInsertion(xmlDoc, paragraph, revisionMetadata, au
22567
23055
  ins.setAttribute("w:author", metadata.author);
22568
23056
  ins.setAttribute("w:date", metadata.date);
22569
23057
  for (const child of Array.from(paragraph.childNodes || [])) {
22570
- if (child?.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName === "pPr") continue;
23058
+ if (child?.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName === "pPr") continue;
22571
23059
  ins.appendChild(child.cloneNode(true));
22572
23060
  }
22573
23061
  wrappedParagraph.appendChild(ins);
@@ -22591,7 +23079,7 @@ async function buildInsertedPlainParagraph(xmlDoc, anchorParagraph, text2, revis
22591
23079
  if (typeof markdownResult?.oxml === "string") {
22592
23080
  const extracted = extractReplacementNodes(markdownResult.oxml);
22593
23081
  sourceParagraph = (extracted.replacementNodes || []).find(
22594
- (node) => node && node.nodeType === 1 && node.namespaceURI === NS_W8 && node.localName === "p"
23082
+ (node) => node && node.nodeType === 1 && node.namespaceURI === NS_W9 && node.localName === "p"
22595
23083
  ) || null;
22596
23084
  }
22597
23085
  if (!sourceParagraph) {
@@ -22623,7 +23111,7 @@ function collectNumberingIdsFromNodes(nodes) {
22623
23111
  }
22624
23112
  function directDeletionCarrierText(carrier) {
22625
23113
  return Array.from(carrier?.childNodes || []).reduce((text2, child) => {
22626
- return text2 + (child?.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName === "r" ? getRunContentPieces(child).map((piece) => piece.text).join("") : "");
23114
+ return text2 + (child?.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName === "r" ? getRunContentPieces(child).map((piece) => piece.text).join("") : "");
22627
23115
  }, "");
22628
23116
  }
22629
23117
  function findOccurrenceOffset(text2, needle, occurrence) {
@@ -22665,9 +23153,9 @@ function hasUnsupportedDeletionSplitMarkup(paragraph, carrier) {
22665
23153
  }
22666
23154
  for (const child of Array.from(carrier?.childNodes || [])) {
22667
23155
  if (child.nodeType !== 1) continue;
22668
- if (!(child.namespaceURI === NS_W8 && child.localName === "r")) return true;
23156
+ if (!(child.namespaceURI === NS_W9 && child.localName === "r")) return true;
22669
23157
  for (const runChild of Array.from(child.childNodes || [])) {
22670
- if (runChild.nodeType !== 1 || runChild.namespaceURI !== NS_W8) continue;
23158
+ if (runChild.nodeType !== 1 || runChild.namespaceURI !== NS_W9) continue;
22671
23159
  if (!["rPr", "delText", "t", "tab", "br", "cr", "noBreakHyphen", "softHyphen"].includes(runChild.localName)) return true;
22672
23160
  }
22673
23161
  }
@@ -22676,7 +23164,7 @@ function hasUnsupportedDeletionSplitMarkup(paragraph, carrier) {
22676
23164
  function insertedRunPropertiesAtDeletionOffset(carrier, localOffset) {
22677
23165
  let offset = 0;
22678
23166
  for (const child of Array.from(carrier?.childNodes || [])) {
22679
- if (!(child?.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName === "r")) continue;
23167
+ if (!(child?.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName === "r")) continue;
22680
23168
  const length = getRunTextLength(getRunContentPieces(child));
22681
23169
  if (localOffset <= offset + length) {
22682
23170
  return clonePropertiesWithoutRevisionHistory(getDirectWordChild4(child, "rPr"));
@@ -22706,7 +23194,7 @@ function normalizedAuthor2(author) {
22706
23194
  function directWordParagraphSibling(paragraph, direction = "nextSibling") {
22707
23195
  let cursor = paragraph?.[direction] || null;
22708
23196
  while (cursor) {
22709
- if (cursor.nodeType === 1 && cursor.namespaceURI === NS_W8 && cursor.localName === "p") return cursor;
23197
+ if (cursor.nodeType === 1 && cursor.namespaceURI === NS_W9 && cursor.localName === "p") return cursor;
22710
23198
  cursor = cursor[direction] || null;
22711
23199
  }
22712
23200
  return null;
@@ -22716,7 +23204,7 @@ function createSanitizedRestorationPPr(xmlDoc, sourceParagraph) {
22716
23204
  const pPr = createWordElement(xmlDoc, "w:pPr");
22717
23205
  if (!sourcePPr) return pPr;
22718
23206
  for (const child of Array.from(sourcePPr.childNodes || [])) {
22719
- if (child.nodeType !== 1 || child.namespaceURI !== NS_W8) continue;
23207
+ if (child.nodeType !== 1 || child.namespaceURI !== NS_W9) continue;
22720
23208
  if (!RESTORATION_PPR_ALLOWLIST.has(child.localName)) continue;
22721
23209
  pPr.appendChild(xmlDoc.importNode(child, true));
22722
23210
  }
@@ -22725,14 +23213,14 @@ function createSanitizedRestorationPPr(xmlDoc, sourceParagraph) {
22725
23213
  function collectRestorationAnchorWarnings(sourceParagraph) {
22726
23214
  const warnings = [];
22727
23215
  const bookmarkNames = /* @__PURE__ */ new Set();
22728
- for (const node of Array.from(sourceParagraph?.getElementsByTagNameNS?.(NS_W8, "bookmarkStart") || [])) {
23216
+ for (const node of Array.from(sourceParagraph?.getElementsByTagNameNS?.(NS_W9, "bookmarkStart") || [])) {
22729
23217
  const name = wordAttribute4(node, "name") || "(unnamed)";
22730
23218
  bookmarkNames.add(name);
22731
23219
  }
22732
23220
  for (const name of bookmarkNames) warnings.push(`RESTORATION_DROPPED_BOOKMARK:${name}`);
22733
23221
  const commentIds = /* @__PURE__ */ new Set();
22734
23222
  for (const localName2 of ["commentRangeStart", "commentRangeEnd", "commentReference"]) {
22735
- for (const node of Array.from(sourceParagraph?.getElementsByTagNameNS?.(NS_W8, localName2) || [])) {
23223
+ for (const node of Array.from(sourceParagraph?.getElementsByTagNameNS?.(NS_W9, localName2) || [])) {
22736
23224
  const id = wordAttribute4(node, "id");
22737
23225
  if (id !== "") commentIds.add(id);
22738
23226
  }
@@ -22749,24 +23237,24 @@ function removeRestorationAnchors(paragraph) {
22749
23237
  "commentReference"
22750
23238
  ];
22751
23239
  for (const localName2 of anchorNames) {
22752
- for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W8, localName2) || [])) {
23240
+ for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W9, localName2) || [])) {
22753
23241
  node.parentNode?.removeChild(node);
22754
23242
  }
22755
23243
  }
22756
- for (const run of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W8, "r") || [])) {
22757
- const meaningful = Array.from(run.childNodes || []).some((child) => child.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName !== "rPr");
23244
+ for (const run of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W9, "r") || [])) {
23245
+ const meaningful = Array.from(run.childNodes || []).some((child) => child.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName !== "rPr");
22758
23246
  if (!meaningful) run.parentNode?.removeChild(run);
22759
23247
  }
22760
23248
  }
22761
23249
  function buildRejectedRestorationTemplate(xmlDoc, sourceParagraph, serializer) {
22762
23250
  const rejected = rejectTrackedChangesInOoxml(serializer.serializeToString(sourceParagraph), { allAuthors: true });
22763
23251
  const parsed = parseOoxmlSafe(rejected.oxml, "application/xml");
22764
- const rejectedParagraph = parsed.doc?.getElementsByTagNameNS?.(NS_W8, "p")?.[0] || null;
23252
+ const rejectedParagraph = parsed.doc?.getElementsByTagNameNS?.(NS_W9, "p")?.[0] || null;
22765
23253
  if (!rejectedParagraph) return null;
22766
23254
  const paragraph = createWordElement(xmlDoc, "w:p");
22767
23255
  paragraph.appendChild(createSanitizedRestorationPPr(xmlDoc, sourceParagraph));
22768
23256
  for (const child of Array.from(rejectedParagraph.childNodes || [])) {
22769
- if (child.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName === "pPr") continue;
23257
+ if (child.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName === "pPr") continue;
22770
23258
  paragraph.appendChild(xmlDoc.importNode(child, true));
22771
23259
  }
22772
23260
  removeRestorationAnchors(paragraph);
@@ -22787,7 +23275,7 @@ async function editRestorationTemplate(xmlDoc, template, modifiedText, author, s
22787
23275
  );
22788
23276
  if (result?.status === "error" || typeof result?.oxml !== "string") return null;
22789
23277
  const extracted = extractReplacementNodes(result.oxml);
22790
- const paragraph = (extracted.replacementNodes || []).find((node) => node?.nodeType === 1 && node.namespaceURI === NS_W8 && node.localName === "p");
23278
+ const paragraph = (extracted.replacementNodes || []).find((node) => node?.nodeType === 1 && node.namespaceURI === NS_W9 && node.localName === "p");
22791
23279
  return paragraph ? xmlDoc.importNode(paragraph, true) : null;
22792
23280
  }
22793
23281
  function allocateFreshParagraphId(xmlDoc, operationSession) {
@@ -22958,7 +23446,7 @@ async function insertIntoRejectedDeletedText(documentXml, targetText, anchor, mo
22958
23446
  }
22959
23447
  const insertionOffset = anchorStart + anchor.offset;
22960
23448
  const carriers = Array.from(paragraph.childNodes || []).filter(
22961
- (node) => node?.nodeType === 1 && node.namespaceURI === NS_W8 && node.localName === "del"
23449
+ (node) => node?.nodeType === 1 && node.namespaceURI === NS_W9 && node.localName === "del"
22962
23450
  );
22963
23451
  if (carriers.some((carrier) => hasUnsupportedDeletionSplitMarkup(paragraph, carrier))) {
22964
23452
  return {
@@ -23615,7 +24103,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
23615
24103
  };
23616
24104
  }
23617
24105
  if (!allSame) {
23618
- const hasMove = targetParagraph.getElementsByTagNameNS(NS_W8, "moveFrom").length > 0 || targetParagraph.getElementsByTagNameNS(NS_W8, "moveTo").length > 0;
24106
+ const hasMove = targetParagraph.getElementsByTagNameNS(NS_W9, "moveFrom").length > 0 || targetParagraph.getElementsByTagNameNS(NS_W9, "moveTo").length > 0;
23619
24107
  if (hasMove) {
23620
24108
  return {
23621
24109
  documentXml,
@@ -23630,8 +24118,8 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
23630
24118
  }
23631
24119
  }
23632
24120
  } else {
23633
- const hasDel = targetParagraph.getElementsByTagNameNS(NS_W8, "del").length > 0;
23634
- const hasMove = targetParagraph.getElementsByTagNameNS(NS_W8, "moveFrom").length > 0 || targetParagraph.getElementsByTagNameNS(NS_W8, "moveTo").length > 0;
24121
+ const hasDel = targetParagraph.getElementsByTagNameNS(NS_W9, "del").length > 0;
24122
+ const hasMove = targetParagraph.getElementsByTagNameNS(NS_W9, "moveFrom").length > 0 || targetParagraph.getElementsByTagNameNS(NS_W9, "moveTo").length > 0;
23635
24123
  if (hasDel) {
23636
24124
  return {
23637
24125
  documentXml,
@@ -24150,7 +24638,7 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
24150
24638
  const targetParagraph = resolved.paragraph;
24151
24639
  const collectVisibleRuns = () => {
24152
24640
  const runEntries2 = [];
24153
- const allRuns = Array.from(targetParagraph.getElementsByTagNameNS(NS_W8, "r"));
24641
+ const allRuns = Array.from(targetParagraph.getElementsByTagNameNS(NS_W9, "r"));
24154
24642
  for (const run of allRuns) {
24155
24643
  let parent = run.parentNode;
24156
24644
  let isDel = false;
@@ -24242,7 +24730,7 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
24242
24730
  const targetRuns = finalRuns.filter((span) => span.start >= matchStart && span.end <= matchEnd);
24243
24731
  let anyChanged = false;
24244
24732
  for (const entry of targetRuns) {
24245
- const rPr = entry.run.getElementsByTagNameNS(NS_W8, "rPr")[0] || null;
24733
+ const rPr = entry.run.getElementsByTagNameNS(NS_W9, "rPr")[0] || null;
24246
24734
  if (checkRunPropertiesChanged(rPr, properties)) {
24247
24735
  anyChanged = true;
24248
24736
  break;
@@ -24253,7 +24741,7 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
24253
24741
  }
24254
24742
  for (const entry of targetRuns) {
24255
24743
  const run = entry.run;
24256
- let rPr = run.getElementsByTagNameNS(NS_W8, "rPr")[0];
24744
+ let rPr = run.getElementsByTagNameNS(NS_W9, "rPr")[0];
24257
24745
  if (!rPr) {
24258
24746
  rPr = createWordElement(xmlDoc, "w:rPr");
24259
24747
  run.insertBefore(rPr, run.firstChild);
@@ -24262,7 +24750,7 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
24262
24750
  let insideSameAuthorIns = false;
24263
24751
  while (insAncestor && insAncestor !== targetParagraph) {
24264
24752
  if (insAncestor.nodeType === 1 && (insAncestor.localName === "ins" || insAncestor.nodeName === "w:ins")) {
24265
- const insAuthor = insAncestor.getAttribute("w:author") || insAncestor.getAttributeNS(NS_W8, "author");
24753
+ const insAuthor = insAncestor.getAttribute("w:author") || insAncestor.getAttributeNS(NS_W9, "author");
24266
24754
  if (insAuthor === (author || getDefaultAuthor())) {
24267
24755
  insideSameAuthorIns = true;
24268
24756
  }
@@ -24310,7 +24798,7 @@ async function applyParagraphFormatToParagraphByExactText(documentXml, targetTex
24310
24798
  return { documentXml, hasChanges: false, status: "error", error: resolved?.error || { code: "TARGET_NOT_FOUND", message: "Target paragraph not found." } };
24311
24799
  }
24312
24800
  const targetParagraph = resolved.paragraph;
24313
- let pPr = targetParagraph.getElementsByTagNameNS(NS_W8, "pPr")[0];
24801
+ let pPr = targetParagraph.getElementsByTagNameNS(NS_W9, "pPr")[0];
24314
24802
  if (!pPr) {
24315
24803
  pPr = createWordElement(xmlDoc, "w:pPr");
24316
24804
  targetParagraph.insertBefore(pPr, targetParagraph.firstChild);
@@ -24358,7 +24846,7 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
24358
24846
  }
24359
24847
  let operation = validation.operation || normalizeDocumentOperation(op);
24360
24848
  const authorUsed = resolveDocumentOperationAuthor(operation, author, getDefaultAuthor());
24361
- if (operation.operationKind !== "comment_reply" && operation.operationKind !== "rejected-insert" && operation.operationKind !== "restore" && (operation.targetDescriptor?.revisionView === "rejected" || operation.targetEndDescriptor?.revisionView === "rejected")) {
24849
+ if (operation.operationKind !== "comment_reply" && operation.operationKind !== "comment_resolve" && operation.operationKind !== "rejected-insert" && operation.operationKind !== "restore" && (operation.targetDescriptor?.revisionView === "rejected" || operation.targetEndDescriptor?.revisionView === "rejected")) {
24362
24850
  return {
24363
24851
  documentXml,
24364
24852
  hasChanges: false,
@@ -24525,6 +25013,7 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
24525
25013
  result = applyCommentReplyToParts({
24526
25014
  commentsXml: existingCommentsXml,
24527
25015
  commentsExtendedXml: existingExtendedXml,
25016
+ documentXml,
24528
25017
  parentCommentId: operation.parentCommentId,
24529
25018
  commentId,
24530
25019
  commentContent: operation.commentContent,
@@ -24532,8 +25021,30 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
24532
25021
  date: operation.date || (/* @__PURE__ */ new Date()).toISOString()
24533
25022
  });
24534
25023
  result.documentXml = documentXml;
24535
- if (result.hasChanges && session.receiptCollector) session.receiptCollector.recordComment(commentId);
25024
+ if (result.status !== "error" && result.hasChanges) {
25025
+ if (!session.document || !anchorReplyInDocument(session.document, result.threadCommentIds, commentId)) {
25026
+ 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.` } };
25027
+ } else if (session.receiptCollector) {
25028
+ session.receiptCollector.recordComment(commentId);
25029
+ }
25030
+ }
24536
25031
  }
25032
+ } else if (operation.operationKind === "comment_resolve") {
25033
+ const resolution = applyThreadResolutionToParts({
25034
+ commentsXml: session.commentsXml || runtimeContext?.commentsXml || options.commentsXml,
25035
+ commentsExtendedXml: session.commentsExtendedXml || runtimeContext?.commentsExtendedXml || options.commentsExtendedXml,
25036
+ commentId: operation.commentId,
25037
+ resolved: operation.resolved !== false
25038
+ });
25039
+ result = resolution.status === "error" ? { documentXml, hasChanges: false, status: "error", error: resolution.error } : {
25040
+ documentXml,
25041
+ hasChanges: resolution.hasChanges,
25042
+ ...resolution.commentsXml ? { commentsXml: resolution.commentsXml, commentsXmlMode: "replace" } : {},
25043
+ ...resolution.commentsExtendedXml ? { commentsExtendedXml: resolution.commentsExtendedXml, commentsExtendedXmlMode: "replace" } : {},
25044
+ resolved: resolution.resolved,
25045
+ threadRootId: resolution.threadRootId,
25046
+ commentIds: resolution.commentIds
25047
+ };
24537
25048
  } else if (operation.operationKind === "highlight") {
24538
25049
  result = await applyHighlightToParagraphByExactText(
24539
25050
  documentXml,
@@ -24674,7 +25185,7 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
24674
25185
  ...resolutionCapture
24675
25186
  };
24676
25187
  }
24677
- session.markMutationCommitted(operation.operationKind !== "comment_reply");
25188
+ session.markMutationCommitted(true);
24678
25189
  if (operation._compiledSourceId && session.sourceTargetRegistry) {
24679
25190
  session.sourceTargetRegistry.commitMutation(
24680
25191
  operation._compiledSourceId,
@@ -24787,9 +25298,9 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
24787
25298
  }
24788
25299
 
24789
25300
  // services/batch-operation-orchestrator.js
24790
- var NS_W9 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
25301
+ var NS_W10 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
24791
25302
  function operationTargetPriority(op) {
24792
- return op?.type === "comment" || op?.type === "comment_reply" ? 0 : 1;
25303
+ return op?.type === "comment" || op?.type === "comment_reply" || op?.type === "comment_resolve" ? 0 : 1;
24793
25304
  }
24794
25305
  function buildOperationDependencyPlan(operations = []) {
24795
25306
  const list = Array.isArray(operations) ? operations : [];
@@ -24864,7 +25375,7 @@ function buildOperationDependencyPlan(operations = []) {
24864
25375
  inDegrees[i2]++;
24865
25376
  }
24866
25377
  const kind = normalizeDocumentOperation(op).operationKind;
24867
- if (kind !== "comment" && kind !== "comment_reply") {
25378
+ if (kind !== "comment" && kind !== "comment_reply" && kind !== "comment_resolve") {
24868
25379
  if (!mutatingCaptureConsumers.has(ref)) mutatingCaptureConsumers.set(ref, []);
24869
25380
  mutatingCaptureConsumers.get(ref).push({
24870
25381
  index: i2,
@@ -24932,9 +25443,9 @@ function mergeCommentsXml(existingXml, incomingXml) {
24932
25443
  const serializer = createSerializer();
24933
25444
  const existingRoot = existingDoc.documentElement;
24934
25445
  const existingIds = new Set(
24935
- Array.from(existingDoc.getElementsByTagNameNS(NS_W9, "comment")).map((node) => node.getAttribute("w:id") || node.getAttribute("id")).filter(Boolean)
25446
+ Array.from(existingDoc.getElementsByTagNameNS(NS_W10, "comment")).map((node) => node.getAttribute("w:id") || node.getAttribute("id")).filter(Boolean)
24936
25447
  );
24937
- for (const comment of Array.from(incomingDoc.getElementsByTagNameNS(NS_W9, "comment"))) {
25448
+ for (const comment of Array.from(incomingDoc.getElementsByTagNameNS(NS_W10, "comment"))) {
24938
25449
  const id = comment.getAttribute("w:id") || comment.getAttribute("id");
24939
25450
  if (id && existingIds.has(id)) continue;
24940
25451
  existingRoot.appendChild(existingDoc.importNode(comment, true));
@@ -25095,7 +25606,7 @@ async function applyOperationsToDocumentXml(documentXml, operations, author, run
25095
25606
  context.targetRefSnapshot = session.initialTargetReferenceSnapshot;
25096
25607
  }
25097
25608
  session.runtimeContext = context;
25098
- if (sourceOperations.some((op) => op?.type === "comment_reply")) {
25609
+ if (sourceOperations.some((op) => op?.type === "comment_reply" || op?.type === "comment_resolve")) {
25099
25610
  session.commentsXml = context.commentsXml || options.commentsXml || null;
25100
25611
  session.commentsExtendedXml = context.commentsExtendedXml || options.commentsExtendedXml || null;
25101
25612
  session.commentsXmlMode = "replace";
@@ -25168,6 +25679,7 @@ async function applyOperationsToDocumentXml(documentXml, operations, author, run
25168
25679
  ...result.resolvedTarget ? { resolvedTarget: result.resolvedTarget } : {},
25169
25680
  ...result.resolvedAnchor ? { resolvedAnchor: result.resolvedAnchor } : {},
25170
25681
  ...result.change ? { change: result.change } : {},
25682
+ ...result.operationType === "comment_resolve" && !isError ? { resolved: result.resolved, threadRootId: result.threadRootId, commentIds: result.commentIds } : {},
25171
25683
  ...Array.isArray(result.warnings) && result.warnings.length > 0 ? { warnings: result.warnings } : {},
25172
25684
  ...result.error ? { error: result.error } : {},
25173
25685
  ...result.receipt ? { receipt: result.receipt } : {}
@@ -25444,6 +25956,26 @@ function preflightOperations(documentXml, operations, author, options = {}) {
25444
25956
  }
25445
25957
  commentsRequired = commentsRequired || operation.operationKind === "comment" || operation.operationKind === "comment_reply";
25446
25958
  numberingRequired = numberingRequired || operationNeedsNumbering(operation);
25959
+ if (operation.operationKind === "comment_resolve") {
25960
+ const commentId = String(operation.commentId);
25961
+ results.push(options._existingCommentDetails?.[commentId] ? {
25962
+ index: index + 1,
25963
+ type: sourceOperation.type,
25964
+ operationType: operation.operationKind,
25965
+ status: "ready",
25966
+ authorUsed,
25967
+ resolvedBy: "comment_id",
25968
+ commentId
25969
+ } : {
25970
+ index: index + 1,
25971
+ type: sourceOperation.type,
25972
+ operationType: operation.operationKind,
25973
+ status: "error",
25974
+ authorUsed,
25975
+ error: { code: "COMMENT_NOT_FOUND", message: `Comment '${commentId}' was not found.` }
25976
+ });
25977
+ continue;
25978
+ }
25447
25979
  if (operation.operationKind === "comment_reply") {
25448
25980
  const parentId = String(operation.parentCommentId);
25449
25981
  const parent = options._existingCommentDetails?.[parentId];
@@ -26595,6 +27127,15 @@ var text = (entries, path) => {
26595
27127
  if (typeof entry === "string") return entry;
26596
27128
  return textDecoder2.decode(entry);
26597
27129
  };
27130
+ function reconcileCommentSiblings(working) {
27131
+ const updated = reconcileCommentSiblingParts({
27132
+ commentsXml: text(working, "word/comments.xml"),
27133
+ commentsIdsXml: text(working, "word/commentsIds.xml"),
27134
+ commentsExtensibleXml: text(working, "word/commentsExtensible.xml")
27135
+ });
27136
+ if (updated.commentsIdsXml) working.set("word/commentsIds.xml", textEncoder4.encode(updated.commentsIdsXml));
27137
+ if (updated.commentsExtensibleXml) working.set("word/commentsExtensible.xml", textEncoder4.encode(updated.commentsExtensibleXml));
27138
+ }
26598
27139
  var cloneEntries = (entries) => new Map([...entries].map(([name, data]) => [name, new Uint8Array(data)]));
26599
27140
  function toBufferCompatible(uint8Arr) {
26600
27141
  if (typeof Buffer !== "undefined" && typeof Buffer.from === "function") {
@@ -26662,6 +27203,7 @@ function computePackageRevisionToken(input) {
26662
27203
  entries
26663
27204
  });
26664
27205
  }
27206
+ var COMMENT_OPERATION_TYPES = /* @__PURE__ */ new Set(["comment", "comment_reply", "comment_resolve"]);
26665
27207
  function nextCommentId(entries) {
26666
27208
  const ids = `${text(entries, "word/document.xml") || ""} ${text(entries, "word/comments.xml") || ""}`.match(/(?:w:)?id=["'](\d+)["']/g) || [];
26667
27209
  let next = ids.reduce((max2, token) => Math.max(max2, Number(token.match(/\d+/)?.[0] || 0)), 0) + 1;
@@ -26680,7 +27222,7 @@ function existingCommentDetails(entries) {
26680
27222
  id,
26681
27223
  author: comment.getAttribute("w:author") || comment.getAttribute("author") || "",
26682
27224
  text: String(comment.textContent || "").trim(),
26683
- paraId: Array.from(comment.getElementsByTagNameNS("*", "p"))[0]?.getAttribute("w14:paraId") || null
27225
+ paraId: commentParaId(comment) || null
26684
27226
  };
26685
27227
  }
26686
27228
  return details;
@@ -26711,13 +27253,34 @@ var DocxDocument = class {
26711
27253
  }
26712
27254
  inspect(options = {}) {
26713
27255
  const digestFn = options.digestFn || ((bytes) => sha256(bytes));
26714
- return inspectDocumentParts({
27256
+ const inspection = inspectDocumentParts({
26715
27257
  documentXml: text(this.entries, "word/document.xml"),
26716
27258
  commentsXml: text(this.entries, "word/comments.xml"),
26717
27259
  commentsExtendedXml: text(this.entries, "word/commentsExtended.xml"),
26718
27260
  numberingXml: text(this.entries, "word/numbering.xml"),
26719
27261
  stylesXml: text(this.entries, "word/styles.xml")
26720
27262
  }, { ...options, digestFn });
27263
+ if (inspection.status === "error") return inspection;
27264
+ return { ...inspection, headersFooters: this.inspectHeadersFooters() };
27265
+ }
27266
+ /**
27267
+ * Header and footer parts the document uses, with their paragraph text, so operations can target them with
27268
+ * `part`. `hasFields` marks parts containing PAGE/NUMPAGES-style fields (edit around them, not inside).
27269
+ */
27270
+ inspectHeadersFooters() {
27271
+ return discoverHeaderFooterParts({
27272
+ documentXml: text(this.entries, "word/document.xml"),
27273
+ relsXml: text(this.entries, "word/_rels/document.xml.rels"),
27274
+ settingsXml: text(this.entries, "word/settings.xml")
27275
+ }).map((part) => {
27276
+ const xml = text(this.entries, part.path) || "";
27277
+ const partInspection = xml ? inspectDocumentParts({ documentXml: xml }) : { paragraphs: [] };
27278
+ return {
27279
+ ...part,
27280
+ hasFields: /<w:fldChar\b|<w:fldSimple\b/.test(xml),
27281
+ paragraphs: (partInspection.paragraphs || []).map((paragraph) => ({ index: paragraph.index, text: paragraph.text }))
27282
+ };
27283
+ });
26721
27284
  }
26722
27285
  getRevisionToken() {
26723
27286
  return computePackageRevisionToken(this.entries);
@@ -26726,12 +27289,51 @@ var DocxDocument = class {
26726
27289
  return this.getRevisionToken();
26727
27290
  }
26728
27291
  preflight(operations, author = getDefaultAuthor(), options = {}) {
26729
- return preflightOperations(
27292
+ const list = Array.isArray(operations) ? operations : [];
27293
+ const runBody = (ops) => preflightOperations(
26730
27294
  text(this.entries, "word/document.xml"),
26731
- operations,
27295
+ ops,
26732
27296
  author || getDefaultAuthor(),
26733
27297
  { ...options, _existingCommentDetails: existingCommentDetails(this.entries) }
26734
27298
  );
27299
+ if (!list.some((operation) => operation && typeof operation === "object" && operation.part != null)) return runBody(operations);
27300
+ const parts = this.inspectHeadersFooters();
27301
+ const bodyOps = [];
27302
+ const results = [];
27303
+ const conflicts = [];
27304
+ const authorsUsed = /* @__PURE__ */ new Set();
27305
+ let valid = true;
27306
+ list.forEach((operation, i2) => {
27307
+ if (!operation || typeof operation !== "object" || operation.part == null) {
27308
+ bodyOps.push({ operation, origIndex: i2 });
27309
+ return;
27310
+ }
27311
+ const resolved = resolvePartSelector(parts, operation.part);
27312
+ 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;
27313
+ if (refusal) {
27314
+ valid = false;
27315
+ results.push({ index: i2 + 1, type: operation.type || "redline", status: "error", error: normalizeErrorWithRecovery({ ...refusal, operationIndex: i2 + 1 }) });
27316
+ return;
27317
+ }
27318
+ const { part: _selector, ...rest } = operation;
27319
+ const partResult = preflightOperations(text(this.entries, resolved.part.path), [rest], author || getDefaultAuthor(), { ...options, _existingCommentDetails: {} });
27320
+ valid = valid && partResult.valid;
27321
+ for (const result of partResult.results || []) results.push({ ...result, index: i2 + 1, part: resolved.part.path });
27322
+ conflicts.push(...partResult.conflicts || []);
27323
+ for (const name of partResult.authorsUsed || []) authorsUsed.add(name);
27324
+ });
27325
+ let requiredArtifacts = { comments: false, numbering: false };
27326
+ if (bodyOps.length) {
27327
+ const body = runBody(bodyOps.map((item) => item.operation));
27328
+ valid = valid && body.valid;
27329
+ for (const result of body.results || []) results.push({ ...result, index: bodyOps[result.index - 1].origIndex + 1 });
27330
+ conflicts.push(...body.conflicts || []);
27331
+ for (const name of body.authorsUsed || []) authorsUsed.add(name);
27332
+ requiredArtifacts = body.requiredArtifacts || requiredArtifacts;
27333
+ }
27334
+ results.sort((a, b) => a.index - b.index);
27335
+ const ok = valid && conflicts.length === 0;
27336
+ return { valid: ok, status: ok ? "ok" : "error", results, conflicts, authorsUsed: [...authorsUsed], requiredArtifacts };
26735
27337
  }
26736
27338
  toUint8Array() {
26737
27339
  return zipDocx(this.entries);
@@ -26739,7 +27341,240 @@ var DocxDocument = class {
26739
27341
  toBuffer() {
26740
27342
  return toBufferCompatible(this.toUint8Array());
26741
27343
  }
27344
+ /**
27345
+ * Applies operations to the document. Operations without `part` target the body (word/document.xml).
27346
+ * An operation with `part` targets one header or footer part:
27347
+ * `{ part: { kind: 'footer', type: 'default', section: 0 } | 'word/footer2.xml', target: { exactText }, ... }`.
27348
+ * Discover parts with `inspect().headersFooters`.
27349
+ */
26742
27350
  async applyOperations(operations, options = {}) {
27351
+ const list = Array.isArray(operations) ? operations : [];
27352
+ if (!list.some((operation) => operation && typeof operation === "object" && operation.part != null)) {
27353
+ return this.applyBodyOperations(operations, options);
27354
+ }
27355
+ return this.applyOperationsWithParts(list, options);
27356
+ }
27357
+ async applyOperationsWithParts(operations, options = {}) {
27358
+ const author = options.author || getDefaultAuthor();
27359
+ const atomic = options.atomic === true;
27360
+ const snapshotEntries = this.entries;
27361
+ const snapshotBytes = this.originalBytes;
27362
+ const total = operations.length;
27363
+ const restore = () => {
27364
+ this.entries = snapshotEntries;
27365
+ this.originalBytes = snapshotBytes;
27366
+ };
27367
+ const original = () => ({
27368
+ uint8Array: snapshotBytes,
27369
+ toUint8Array: () => new Uint8Array(snapshotBytes),
27370
+ buffer: toBufferCompatible(snapshotBytes),
27371
+ toBuffer: () => toBufferCompatible(snapshotBytes)
27372
+ });
27373
+ const refuse = (error2, results2 = [], receipts2 = [], extra = {}) => ({
27374
+ status: "error",
27375
+ hasChanges: false,
27376
+ written: false,
27377
+ rolledBack: true,
27378
+ results: results2,
27379
+ receipts: receipts2,
27380
+ executionOrder: [],
27381
+ authorsUsed: [],
27382
+ artifactsChanged: [],
27383
+ error: normalizeErrorWithRecovery(error2),
27384
+ retryPlan: createRetryPlan({ atomic: true, rolledBack: true, results: results2, receipts: receipts2, operationCount: total }),
27385
+ validation: { originalIssues: [], generatedIssues: [] },
27386
+ ...extra,
27387
+ ...original()
27388
+ });
27389
+ if (options.expectedRevision) {
27390
+ const precondition = await this.applyBodyOperations([], { expectedRevision: options.expectedRevision });
27391
+ if (precondition.status === "error") return precondition;
27392
+ }
27393
+ const parts = discoverHeaderFooterParts({
27394
+ documentXml: text(this.entries, "word/document.xml"),
27395
+ relsXml: text(this.entries, "word/_rels/document.xml.rels"),
27396
+ settingsXml: text(this.entries, "word/settings.xml")
27397
+ });
27398
+ const bodyOps = [];
27399
+ const partOps = [];
27400
+ for (let i2 = 0; i2 < operations.length; i2++) {
27401
+ const operation = operations[i2];
27402
+ if (!operation || typeof operation !== "object" || operation.part == null) {
27403
+ bodyOps.push({ operation, origIndex: i2 });
27404
+ continue;
27405
+ }
27406
+ if (COMMENT_OPERATION_TYPES.has(operation.type)) {
27407
+ return refuse({ code: "COMMENT_IN_HEADER_FOOTER", message: `Operation ${i2 + 1}: Word does not support comments in headers or footers.`, operationIndex: i2 + 1 });
27408
+ }
27409
+ const resolved = resolvePartSelector(parts, operation.part);
27410
+ if (resolved.error) return refuse({ ...resolved.error, message: `Operation ${i2 + 1}: ${resolved.error.message}`, operationIndex: i2 + 1 });
27411
+ const { part: _selector, ...rest } = operation;
27412
+ partOps.push({ operation: rest, origIndex: i2, part: resolved.part });
27413
+ }
27414
+ const results = [];
27415
+ const receipts = [];
27416
+ const executionOrder = [];
27417
+ const authorsUsed = /* @__PURE__ */ new Set();
27418
+ const originalIssues = [];
27419
+ let anyChange = false;
27420
+ let anyFailure = false;
27421
+ let bodyResult = null;
27422
+ if (bodyOps.length) {
27423
+ bodyResult = await this.applyBodyOperations(bodyOps.map((item) => item.operation), { ...options, expectedRevision: void 0 });
27424
+ const toOriginal = (index) => bodyOps[index - 1]?.origIndex + 1;
27425
+ for (const result of bodyResult.results || []) {
27426
+ results.push({
27427
+ ...result,
27428
+ index: toOriginal(result.index),
27429
+ ...result.receipt ? { receipt: { ...result.receipt, operationIndex: toOriginal(result.receipt.operationIndex) } } : {}
27430
+ });
27431
+ }
27432
+ for (const receipt of bodyResult.receipts || []) receipts.push({ ...receipt, operationIndex: toOriginal(receipt.operationIndex) });
27433
+ for (const index of bodyResult.executionOrder || []) executionOrder.push(toOriginal(index));
27434
+ for (const name of bodyResult.authorsUsed || []) authorsUsed.add(name);
27435
+ originalIssues.push(...bodyResult.validation?.originalIssues || []);
27436
+ if (bodyResult.status === "error" || bodyResult.rolledBack) {
27437
+ anyFailure = true;
27438
+ if (atomic) {
27439
+ restore();
27440
+ return { ...bodyResult, results, receipts, executionOrder, written: false, artifactsChanged: [], rolledBack: true, ...original() };
27441
+ }
27442
+ }
27443
+ anyChange = bodyResult.hasChanges === true;
27444
+ }
27445
+ const working = cloneEntries(this.entries);
27446
+ const zip = new MemoryZip(working);
27447
+ const changedParts = /* @__PURE__ */ new Set();
27448
+ const { expectedRevision: _ignored, ...runnerOptions } = options;
27449
+ const failPart = (item, error2) => {
27450
+ anyFailure = true;
27451
+ const failure = normalizeErrorWithRecovery({ ...error2, operationIndex: item.origIndex + 1 });
27452
+ results.push({ index: item.origIndex + 1, type: item.operation.type || "redline", status: "error", authorUsed: author, part: item.part.path, error: failure });
27453
+ };
27454
+ for (const item of partOps) {
27455
+ const path = item.part.path;
27456
+ const xml = text(working, path);
27457
+ if (!xml) {
27458
+ failPart(item, { code: "PART_NOT_FOUND", message: `Part '${path}' is referenced but missing from the package.` });
27459
+ if (atomic) break;
27460
+ continue;
27461
+ }
27462
+ const numberingText = text(working, "word/numbering.xml");
27463
+ const outcome = await applyOperationsToDocumentXml(xml, [item.operation], author, { numberingIdState: createDynamicNumberingIdState(numberingText || void 0) }, {
27464
+ ...runnerOptions,
27465
+ atomic: true,
27466
+ strictTargets: options.strictTargets !== false,
27467
+ _existingCommentDetails: {}
27468
+ });
27469
+ const single = outcome.results?.[0];
27470
+ if (outcome.rolledBack || outcome.status === "error") {
27471
+ failPart(item, single?.error || outcome.error || { code: "OPERATION_FAILED", message: "Operation failed." });
27472
+ if (atomic) break;
27473
+ continue;
27474
+ }
27475
+ if (!outcome.hasChanges) {
27476
+ results.push({ ...single, index: item.origIndex + 1, part: path });
27477
+ continue;
27478
+ }
27479
+ if (countRevisionsInsideFields(outcome.documentXml) > countRevisionsInsideFields(xml)) {
27480
+ 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.` });
27481
+ if (atomic) break;
27482
+ continue;
27483
+ }
27484
+ const taken = /* @__PURE__ */ new Set();
27485
+ for (const [name, data] of working) {
27486
+ if (name === path || !(name === "word/document.xml" || isHeaderFooterPath(name))) continue;
27487
+ for (const id of collectRevisionIds(textDecoder2.decode(data))) taken.add(id);
27488
+ }
27489
+ const renumbered = renumberCollidingRevisionIds(outcome.documentXml, taken);
27490
+ working.set(path, textEncoder4.encode(renumbered.xml));
27491
+ const describeReceipt = (receipt) => receipt?.revisionItems ? { ...receipt, revisionItems: receipt.revisionItems.map((revision) => ({ ...revision, id: renumbered.idMap.get(String(revision.id)) ?? revision.id, partName: path })) } : receipt;
27492
+ if (outcome.numberingXmlParts?.length) {
27493
+ await ensureNumberingArtifactsInZip(zip, outcome.numberingXmlParts, { mergeNumberingXml: mergeNumberingXmlBySchemaOrder });
27494
+ }
27495
+ changedParts.add(path);
27496
+ anyChange = true;
27497
+ results.push({
27498
+ ...single,
27499
+ index: item.origIndex + 1,
27500
+ part: path,
27501
+ partSections: item.part.appliesToSections,
27502
+ ...single?.receipt ? { receipt: { ...describeReceipt(single.receipt), operationIndex: item.origIndex + 1 } } : {}
27503
+ });
27504
+ if (outcome.receipts?.[0]) receipts.push({ ...describeReceipt(outcome.receipts[0]), operationIndex: item.origIndex + 1 });
27505
+ executionOrder.push(item.origIndex + 1);
27506
+ for (const name of outcome.authorsUsed || []) authorsUsed.add(name);
27507
+ }
27508
+ const generatedIssues = [];
27509
+ if (!(atomic && anyFailure)) {
27510
+ for (const path of changedParts) {
27511
+ const before = validateRedlineOoxml(text(this.entries, path)).issues.map((issue) => ({ source: path, ...issue }));
27512
+ const after = validateRedlineOoxml(text(working, path)).issues.map((issue) => ({ source: path, ...issue }));
27513
+ generatedIssues.push(...validationErrors(subtractValidationIssueMultiset(after, before)));
27514
+ }
27515
+ if (options.validate !== false && changedParts.size) {
27516
+ try {
27517
+ await validateDocxPackage(zip);
27518
+ } catch (error2) {
27519
+ generatedIssues.push({ source: "package", code: "PACKAGE_VALIDATION", severity: "error", message: error2.message });
27520
+ }
27521
+ }
27522
+ }
27523
+ if (atomic && anyFailure || generatedIssues.length) {
27524
+ restore();
27525
+ 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." };
27526
+ const rolledBackResults = results.map((result) => result.receipt ? { ...result, receipt: { ...result.receipt, committed: false, finalDisposition: "rolled_back" } } : result).sort((a, b) => a.index - b.index);
27527
+ const rolledBackReceipts = receipts.map((receipt) => ({ ...receipt, committed: false, finalDisposition: "rolled_back" }));
27528
+ return refuse(error2, rolledBackResults, rolledBackReceipts, { validation: { originalIssues, generatedIssues }, issues: generatedIssues });
27529
+ }
27530
+ results.sort((a, b) => a.index - b.index);
27531
+ receipts.sort((a, b) => a.operationIndex - b.operationIndex);
27532
+ const status = !anyFailure ? "ok" : anyChange ? "partial" : "error";
27533
+ const failureError = anyFailure ? { error: normalizeErrorWithRecovery({ code: "BATCH_OPERATION_FAILED", message: "One or more operations failed; the others were applied." }) } : {};
27534
+ if (!changedParts.size) {
27535
+ const buf = toBufferCompatible(this.originalBytes);
27536
+ return {
27537
+ ...bodyResult || {},
27538
+ results,
27539
+ receipts,
27540
+ executionOrder,
27541
+ authorsUsed: [...authorsUsed],
27542
+ status,
27543
+ hasChanges: anyChange,
27544
+ written: bodyResult?.written === true,
27545
+ artifactsChanged: bodyResult?.artifactsChanged || [],
27546
+ validation: { originalIssues, generatedIssues: [] },
27547
+ ...failureError,
27548
+ uint8Array: this.originalBytes,
27549
+ toUint8Array: () => new Uint8Array(this.originalBytes),
27550
+ buffer: buf,
27551
+ toBuffer: () => toBufferCompatible(this.originalBytes)
27552
+ };
27553
+ }
27554
+ this.entries = working;
27555
+ const outputBytes = this.toUint8Array();
27556
+ this.originalBytes = outputBytes;
27557
+ const artifactsChanged = [...working].filter(([name, data]) => !snapshotEntries.has(name) || !areByteArraysEqual(data, snapshotEntries.get(name))).map(([name]) => name);
27558
+ return {
27559
+ ...bodyResult || {},
27560
+ status,
27561
+ hasChanges: true,
27562
+ written: true,
27563
+ results,
27564
+ receipts,
27565
+ executionOrder,
27566
+ authorsUsed: [...authorsUsed],
27567
+ artifactsChanged,
27568
+ validation: { originalIssues, generatedIssues: [] },
27569
+ ...failureError,
27570
+ uint8Array: outputBytes,
27571
+ toUint8Array: () => new Uint8Array(outputBytes),
27572
+ buffer: toBufferCompatible(outputBytes),
27573
+ inspection: this.inspect(),
27574
+ toBuffer: () => toBufferCompatible(outputBytes)
27575
+ };
27576
+ }
27577
+ async applyBodyOperations(operations, options = {}) {
26743
27578
  const failedApply = (error2) => {
26744
27579
  const results = [];
26745
27580
  const receipts = [];
@@ -26850,18 +27685,21 @@ var DocxDocument = class {
26850
27685
  toBuffer: () => toBufferCompatible(this.originalBytes)
26851
27686
  };
26852
27687
  }
26853
- working.set("word/document.xml", textEncoder4.encode(result.documentXml));
27688
+ const bodyChanged = (result.results || []).some((item) => item?.status !== "error" && item?.operationType !== "comment_resolve");
27689
+ if (bodyChanged) working.set("word/document.xml", textEncoder4.encode(result.documentXml));
26854
27690
  await ensureNumberingArtifactsInZip(zip, result.numberingXmlParts, { mergeNumberingXml: mergeNumberingXmlBySchemaOrder });
26855
27691
  const existingCommentsXml = text(working, "word/comments.xml");
26856
27692
  const commentsXmlForPackaging = result.commentsXml || existingCommentsXml;
26857
27693
  await ensureCommentsArtifactsInZip(zip, commentsXmlForPackaging, {
26858
27694
  replaceExisting: result.commentsXmlMode === "replace" || !result.commentsXml && !!existingCommentsXml
26859
27695
  });
26860
- const existingCommentsExtendedXml = text(working, "word/commentsExtended.xml");
26861
- const commentsExtendedXmlForPackaging = result.commentsExtendedXml || existingCommentsExtendedXml;
26862
- await ensureCommentsExtendedArtifactsInZip(zip, commentsExtendedXmlForPackaging, {
26863
- replaceExisting: result.commentsExtendedXmlMode === "replace" || !result.commentsExtendedXml && !!existingCommentsExtendedXml
26864
- });
27696
+ if (result.commentsExtendedXml) {
27697
+ await ensureCommentsExtendedArtifactsInZip(zip, result.commentsExtendedXml, {
27698
+ replaceExisting: result.commentsExtendedXmlMode === "replace"
27699
+ });
27700
+ }
27701
+ await repairKnownContentTypes(zip);
27702
+ reconcileCommentSiblings(working);
26865
27703
  if (options.validate !== false) {
26866
27704
  const generated = validateRedlineOoxml(result.documentXml);
26867
27705
  const outputIssues = generated.issues.map((issue) => ({ source: "word/document.xml", ...issue }));
@@ -26930,13 +27768,25 @@ var DocxDocument = class {
26930
27768
  const transform = action === "accept" ? acceptTrackedChangesInOoxml : action === "reject" ? rejectTrackedChangesInOoxml : null;
26931
27769
  if (!transform) return packageFailure(this.originalBytes, "INVALID_ACTION", `Unknown revision action: ${action}`);
26932
27770
  const sourceBytes = this.originalBytes;
27771
+ const originalEntries = this.entries;
26933
27772
  const working = cloneEntries(this.entries);
26934
27773
  const zip = new MemoryZip(working);
26935
27774
  const result = transform(text(working, "word/document.xml"), { author: options.author, allAuthors: options.allAuthors === true });
26936
27775
  if (result.status === "error" || result.error) {
26937
27776
  return packageFailure(sourceBytes, result.error?.code || "REVISION_OPERATION_FAILED", result.error?.message || "Revision operation failed.");
26938
27777
  }
26939
- if (!result.hasChanges) {
27778
+ const partUpdates = /* @__PURE__ */ new Map();
27779
+ for (const part of this.inspectHeadersFooters()) {
27780
+ const partXml = text(working, part.path);
27781
+ if (!partXml) continue;
27782
+ const partResult = transform(partXml, { author: options.author, allAuthors: options.allAuthors === true });
27783
+ if (partResult.status === "error" || partResult.error) {
27784
+ return packageFailure(sourceBytes, partResult.error?.code || "REVISION_OPERATION_FAILED", `${part.path}: ${partResult.error?.message || "Revision operation failed."}`);
27785
+ }
27786
+ if (partResult.hasChanges) partUpdates.set(part.path, partResult.oxml);
27787
+ }
27788
+ const hasChanges = result.hasChanges || partUpdates.size > 0;
27789
+ if (!hasChanges) {
26940
27790
  const buf = toBufferCompatible(sourceBytes);
26941
27791
  return {
26942
27792
  ...result,
@@ -26949,8 +27799,10 @@ var DocxDocument = class {
26949
27799
  toBuffer: () => toBufferCompatible(sourceBytes)
26950
27800
  };
26951
27801
  }
26952
- working.set("word/document.xml", textEncoder4.encode(result.oxml));
27802
+ if (result.hasChanges) working.set("word/document.xml", textEncoder4.encode(result.oxml));
27803
+ for (const [path, xml] of partUpdates) working.set(path, textEncoder4.encode(xml));
26953
27804
  try {
27805
+ await repairKnownContentTypes(zip);
26954
27806
  if (options.validate !== false) await validateDocxPackage(zip);
26955
27807
  } catch (error2) {
26956
27808
  return packageFailure(sourceBytes, "PACKAGE_VALIDATION", error2.message);
@@ -26963,13 +27815,71 @@ var DocxDocument = class {
26963
27815
  ...result,
26964
27816
  status: "ok",
26965
27817
  written: true,
26966
- artifactsChanged: ["word/document.xml"],
27818
+ hasChanges: true,
27819
+ artifactsChanged: [...working].filter(([name, data]) => !originalEntries.has(name) || !areByteArraysEqual(data, originalEntries.get(name))).map(([name]) => name),
26967
27820
  uint8Array: outputBytes,
26968
27821
  toUint8Array: () => new Uint8Array(outputBytes),
26969
27822
  buffer: outputBuf,
26970
27823
  toBuffer: () => toBufferCompatible(outputBytes)
26971
27824
  };
26972
27825
  }
27826
+ /**
27827
+ * Resolves (or reopens) the comment thread containing `commentId`.
27828
+ * Resolved is a thread-level state in Word, so the root and every reply are updated together.
27829
+ */
27830
+ async resolveComment(commentId, options = {}) {
27831
+ const sourceBytes = this.originalBytes;
27832
+ const working = cloneEntries(this.entries);
27833
+ const resolved = options.resolved !== false;
27834
+ const noChange = (extra) => {
27835
+ const buf = toBufferCompatible(sourceBytes);
27836
+ return {
27837
+ status: "ok",
27838
+ hasChanges: false,
27839
+ written: false,
27840
+ artifactsChanged: [],
27841
+ ...extra,
27842
+ uint8Array: sourceBytes,
27843
+ toUint8Array: () => new Uint8Array(sourceBytes),
27844
+ buffer: buf,
27845
+ toBuffer: () => toBufferCompatible(sourceBytes)
27846
+ };
27847
+ };
27848
+ const result = applyThreadResolutionToParts({
27849
+ commentsXml: text(working, "word/comments.xml"),
27850
+ commentsExtendedXml: text(working, "word/commentsExtended.xml"),
27851
+ commentId,
27852
+ resolved
27853
+ });
27854
+ if (result.status === "error") return packageFailure(sourceBytes, result.error.code, result.error.message);
27855
+ if (!result.hasChanges) return noChange({ resolved, threadRootId: result.threadRootId, commentIds: result.commentIds });
27856
+ const zip = new MemoryZip(working);
27857
+ try {
27858
+ if (result.commentsXml) working.set("word/comments.xml", textEncoder4.encode(result.commentsXml));
27859
+ await ensureCommentsExtendedArtifactsInZip(zip, result.commentsExtendedXml, { replaceExisting: true });
27860
+ await repairKnownContentTypes(zip);
27861
+ if (options.validate !== false) await validateDocxPackage(zip);
27862
+ } catch (error2) {
27863
+ return packageFailure(sourceBytes, "PACKAGE_VALIDATION", error2.message);
27864
+ }
27865
+ const artifactsChanged = [...working].filter(([name, data]) => !this.entries.has(name) || !areByteArraysEqual(data, this.entries.get(name))).map(([name]) => name);
27866
+ this.entries = working;
27867
+ const outputBytes = this.toUint8Array();
27868
+ this.originalBytes = outputBytes;
27869
+ return {
27870
+ status: "ok",
27871
+ hasChanges: true,
27872
+ written: true,
27873
+ resolved,
27874
+ threadRootId: result.threadRootId,
27875
+ commentIds: result.commentIds,
27876
+ artifactsChanged,
27877
+ uint8Array: outputBytes,
27878
+ toUint8Array: () => new Uint8Array(outputBytes),
27879
+ buffer: toBufferCompatible(outputBytes),
27880
+ toBuffer: () => toBufferCompatible(outputBytes)
27881
+ };
27882
+ }
26973
27883
  async deleteComments(options = {}) {
26974
27884
  const sourceBytes = this.originalBytes;
26975
27885
  const working = cloneEntries(this.entries);
@@ -26993,7 +27903,13 @@ var DocxDocument = class {
26993
27903
  if (!parsed.doc || parsed.error) {
26994
27904
  return packageFailure(sourceBytes, "PARSE_ERROR", parsed.error?.message || "Could not parse comments.xml.");
26995
27905
  }
26996
- const matches = (comment) => options.allAuthors === true || (comment.getAttribute("w:author") || comment.getAttribute("author")) === options.author;
27906
+ const requestedIds = Array.isArray(options.ids) ? new Set(options.ids.map(String)) : null;
27907
+ if (requestedIds) {
27908
+ const known = new Set(Array.from(parsed.doc.getElementsByTagNameNS("*", "comment")).map((node) => node.getAttribute("w:id") || node.getAttribute("id")));
27909
+ const missing = [...requestedIds].filter((id) => !known.has(id));
27910
+ if (missing.length) return packageFailure(sourceBytes, "COMMENT_NOT_FOUND", `Comment id(s) not found: ${missing.join(", ")}.`);
27911
+ }
27912
+ 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;
26997
27913
  const ids = new Set(
26998
27914
  Array.from(parsed.doc.getElementsByTagNameNS("*", "comment")).filter(matches).map((node) => node.getAttribute("w:id") || node.getAttribute("id")).filter(Boolean)
26999
27915
  );
@@ -27022,9 +27938,8 @@ var DocxDocument = class {
27022
27938
  const idByParaId = /* @__PURE__ */ new Map();
27023
27939
  for (const comment of Array.from(parsed.doc.getElementsByTagNameNS("*", "comment"))) {
27024
27940
  const id = comment.getAttribute("w:id") || comment.getAttribute("id");
27025
- const paragraph = Array.from(comment.getElementsByTagNameNS("*", "p"))[0];
27026
- const paraId = paragraph?.getAttribute("w14:paraId") || paragraph?.getAttribute("paraId");
27027
- if (paraId) idByParaId.set(paraId.toUpperCase(), id);
27941
+ const paraId = commentParaId(comment);
27942
+ if (paraId) idByParaId.set(paraId, id);
27028
27943
  }
27029
27944
  for (const [paraId, id] of idByParaId) {
27030
27945
  if (ids.has(id)) removedParaIds.add(paraId);
@@ -27043,8 +27958,8 @@ var DocxDocument = class {
27043
27958
  }
27044
27959
  }
27045
27960
  }
27046
- const commentsResult = deleteCommentsByAuthorInOoxml(commentsXml, { author: options.author, allAuthors: options.allAuthors === true });
27047
- const remainingParsed = parseOoxmlSafe(commentsResult.oxml, "application/xml");
27961
+ const remainingXml = requestedIds ? commentsXml : deleteCommentsByAuthorInOoxml(commentsXml, { author: options.author, allAuthors: options.allAuthors === true }).oxml;
27962
+ const remainingParsed = parseOoxmlSafe(remainingXml, "application/xml");
27048
27963
  if (!remainingParsed.doc || remainingParsed.error) {
27049
27964
  return packageFailure(sourceBytes, "PARSE_ERROR", remainingParsed.error?.message || "Could not parse updated comments.xml.");
27050
27965
  }
@@ -27080,10 +27995,14 @@ var DocxDocument = class {
27080
27995
  }
27081
27996
  working.set("word/document.xml", textEncoder4.encode(serializer.serializeToString(documentParsed.doc)));
27082
27997
  try {
27083
- if (options.validate !== false) await validateDocxPackage(new MemoryZip(working));
27998
+ reconcileCommentSiblings(working);
27999
+ const finalZip = new MemoryZip(working);
28000
+ await repairKnownContentTypes(finalZip);
28001
+ if (options.validate !== false) await validateDocxPackage(finalZip);
27084
28002
  } catch (error2) {
27085
28003
  return packageFailure(sourceBytes, "PACKAGE_VALIDATION", error2.message);
27086
28004
  }
28005
+ const artifactsChanged = [...working].filter(([name, data]) => !this.entries.has(name) || !areByteArraysEqual(data, this.entries.get(name))).map(([name]) => name);
27087
28006
  this.entries = working;
27088
28007
  const outputBytes = this.toUint8Array();
27089
28008
  this.originalBytes = outputBytes;
@@ -27094,7 +28013,7 @@ var DocxDocument = class {
27094
28013
  written: true,
27095
28014
  commentsRemoved: ids.size,
27096
28015
  referencesRemoved,
27097
- artifactsChanged: ["word/document.xml", "word/comments.xml", ...extendedParsed?.doc ? ["word/commentsExtended.xml"] : []],
28016
+ artifactsChanged,
27098
28017
  uint8Array: outputBytes,
27099
28018
  toUint8Array: () => new Uint8Array(outputBytes),
27100
28019
  buffer: outputBuf,
@@ -27350,6 +28269,7 @@ export {
27350
28269
  rejectTrackedChangesInOoxml,
27351
28270
  remapNumberingPayloadForDocument,
27352
28271
  removeFormattingFromRPr,
28272
+ repairKnownContentTypes,
27353
28273
  reserveNextNumberingId,
27354
28274
  reserveNextNumberingIdPair,
27355
28275
  resolveParagraphRangeByRefs,