@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 CommonJS bundle v0.8.0 — https://github.com/AnsonLai/docx-redline-js
1
+ // @ansonlai/docx-redline-js standalone CommonJS 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;
@@ -8080,6 +8080,7 @@ __export(index_exports, {
8080
8080
  rejectTrackedChangesInOoxml: () => rejectTrackedChangesInOoxml,
8081
8081
  remapNumberingPayloadForDocument: () => remapNumberingPayloadForDocument,
8082
8082
  removeFormattingFromRPr: () => removeFormattingFromRPr,
8083
+ repairKnownContentTypes: () => repairKnownContentTypes,
8083
8084
  reserveNextNumberingId: () => reserveNextNumberingId,
8084
8085
  reserveNextNumberingIdPair: () => reserveNextNumberingIdPair,
8085
8086
  resolveParagraphRangeByRefs: () => resolveParagraphRangeByRefs,
@@ -14333,6 +14334,19 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
14333
14334
  pendingReplacementEvent = null;
14334
14335
  }
14335
14336
  }
14337
+ for (const offset of Array.from(sentinelMapByStart.keys()).sort((a, b) => a - b)) {
14338
+ for (const sentinel of sentinelMapByStart.get(offset)) {
14339
+ if (!sentinel.isCommentMarker || emittedCommentMarkers.has(sentinel.node)) continue;
14340
+ emittedCommentMarkers.add(sentinel.node);
14341
+ if (isWordElement(sentinel.node, "commentReference")) {
14342
+ const run = createWordElement(xmlDoc, "w:r");
14343
+ run.appendChild(sentinel.node.cloneNode(true));
14344
+ currentParagraph.appendChild(run);
14345
+ } else {
14346
+ currentParagraph.appendChild(sentinel.node.cloneNode(true));
14347
+ }
14348
+ }
14349
+ }
14336
14350
  if (generateRedlines) {
14337
14351
  containerFragments.forEach((fragment) => {
14338
14352
  Array.from(fragment.childNodes).forEach((node) => {
@@ -18127,6 +18141,260 @@ function ingestWordOoxmlToMarkdownResult(ooxml) {
18127
18141
  return { text: lines.join("\n\n").trim(), status: "ok", warnings: parsed.warnings };
18128
18142
  }
18129
18143
 
18144
+ // services/comment-builders.js
18145
+ var NS_W14 = "http://schemas.microsoft.com/office/word/2010/wordml";
18146
+ var NS_W15 = "http://schemas.microsoft.com/office/word/2012/wordml";
18147
+ function createCommentParaId(commentId) {
18148
+ const numeric = Number.parseInt(String(commentId), 10);
18149
+ const value = Number.isFinite(numeric) ? 1879048192 + (numeric >>> 0) >>> 0 : 1879048192;
18150
+ return value.toString(16).toUpperCase().padStart(8, "0").slice(-8);
18151
+ }
18152
+ function buildCommentElement(commentId, author, content, date, paraId = createCommentParaId(commentId)) {
18153
+ const initials = author.split(" ").map((word) => word[0]).join("").toUpperCase() || "AI";
18154
+ const escapedContent = escapeXml(content);
18155
+ const escapedAuthor = escapeXml(author);
18156
+ return `<w:comment w:id="${commentId}" w:author="${escapedAuthor}" w:date="${date}" w:initials="${initials}">
18157
+ <w:p w14:paraId="${escapeXml(paraId)}" xmlns:w14="${NS_W14}">
18158
+ <w:r><w:t>${escapedContent}</w:t></w:r>
18159
+ </w:p>
18160
+ </w:comment>`;
18161
+ }
18162
+ function buildCommentsPartXml(comments) {
18163
+ if (!comments || comments.length === 0) {
18164
+ return `<w:comments xmlns:w="${NS_W}"></w:comments>`;
18165
+ }
18166
+ const commentElements = comments.map(
18167
+ (comment) => buildCommentElement(comment.id, comment.author, comment.content, comment.date, comment.paraId)
18168
+ ).join("\n ");
18169
+ return `<w:comments xmlns:w="${NS_W}">
18170
+ ${commentElements}
18171
+ </w:comments>`;
18172
+ }
18173
+ function buildCommentsExtendedPartXml(entries) {
18174
+ const body = (entries || []).map((entry) => {
18175
+ const parent = entry.paraIdParent ? ` w15:paraIdParent="${escapeXml(entry.paraIdParent)}"` : "";
18176
+ return `<w15:commentEx w15:paraId="${escapeXml(entry.paraId)}"${parent} w15:done="${entry.done ? "1" : "0"}"/>`;
18177
+ }).join("");
18178
+ return `<w15:commentsEx xmlns:w15="${NS_W15}">${body}</w15:commentsEx>`;
18179
+ }
18180
+
18181
+ // services/comment-thread-parts.js
18182
+ var attrOf = (node, qualified, local) => node?.getAttribute?.(qualified) || node?.getAttribute?.(local) || "";
18183
+ function commentThreadParagraph(commentElement) {
18184
+ const paragraphs = Array.from(commentElement?.getElementsByTagNameNS?.("*", "p") || []);
18185
+ return paragraphs.length ? paragraphs[paragraphs.length - 1] : null;
18186
+ }
18187
+ function commentParaId(commentElement) {
18188
+ return attrOf(commentThreadParagraph(commentElement), "w14:paraId", "paraId").toUpperCase();
18189
+ }
18190
+ function usedParaIds(commentsDoc, extendedDoc) {
18191
+ const ids = /* @__PURE__ */ new Set();
18192
+ for (const p of Array.from(commentsDoc?.getElementsByTagNameNS("*", "p") || [])) {
18193
+ const id = attrOf(p, "w14:paraId", "paraId");
18194
+ if (id) ids.add(id.toUpperCase());
18195
+ }
18196
+ for (const ex of Array.from(extendedDoc?.getElementsByTagNameNS("*", "commentEx") || [])) {
18197
+ const id = attrOf(ex, "w15:paraId", "paraId");
18198
+ if (id) ids.add(id.toUpperCase());
18199
+ }
18200
+ return ids;
18201
+ }
18202
+ function allocateParaId(commentId, occupied) {
18203
+ let candidate = createCommentParaId(commentId);
18204
+ let value = Number.parseInt(candidate, 16) >>> 0;
18205
+ while (occupied.has(candidate)) {
18206
+ value = value + 1 >>> 0;
18207
+ candidate = value.toString(16).toUpperCase().padStart(8, "0");
18208
+ }
18209
+ occupied.add(candidate);
18210
+ return candidate;
18211
+ }
18212
+ var parseDoc = (xml, partName) => {
18213
+ const parsed = parseOoxmlSafe(xml, "application/xml");
18214
+ if (!parsed.doc || parsed.error) return { error: { code: "PARSE_ERROR", message: `Could not parse ${partName}: ${parsed.error?.message || "invalid XML"}` } };
18215
+ return { doc: parsed.doc };
18216
+ };
18217
+ function applyThreadResolutionToParts({ commentsXml, commentsExtendedXml = null, commentId, resolved = true }) {
18218
+ if (!commentsXml) return { status: "error", error: { code: "COMMENTS_PART_MISSING", message: "Resolving a comment requires an existing word/comments.xml part." } };
18219
+ const commentsParsed = parseDoc(commentsXml, "word/comments.xml");
18220
+ if (commentsParsed.error) return { status: "error", error: commentsParsed.error };
18221
+ const commentsDoc = commentsParsed.doc;
18222
+ const idOf = (node) => attrOf(node, "w:id", "id");
18223
+ const comments = Array.from(commentsDoc.getElementsByTagNameNS("*", "comment"));
18224
+ const target = comments.find((node) => idOf(node) === String(commentId));
18225
+ if (!target) return { status: "error", error: { code: "COMMENT_NOT_FOUND", message: `Comment '${commentId}' was not found.` } };
18226
+ let extendedDoc;
18227
+ if (commentsExtendedXml) {
18228
+ const parsed = parseDoc(commentsExtendedXml, "word/commentsExtended.xml");
18229
+ if (parsed.error) return { status: "error", error: parsed.error };
18230
+ extendedDoc = parsed.doc;
18231
+ } else {
18232
+ extendedDoc = parseDoc(`<w15:commentsEx xmlns:w15="${NS_W15}"/>`, "word/commentsExtended.xml").doc;
18233
+ }
18234
+ const root = extendedDoc.documentElement;
18235
+ const entryFor = (paraId) => Array.from(root.getElementsByTagNameNS("*", "commentEx")).find((node) => attrOf(node, "w15:paraId", "paraId").toUpperCase() === paraId) || null;
18236
+ const occupied = usedParaIds(commentsDoc, extendedDoc);
18237
+ let commentsChanged = false;
18238
+ const keyOf = (node) => {
18239
+ let paraId = commentParaId(node);
18240
+ if (!paraId) {
18241
+ paraId = allocateParaId(idOf(node), occupied);
18242
+ commentThreadParagraph(node)?.setAttributeNS(NS_W14, "w14:paraId", paraId);
18243
+ commentsChanged = true;
18244
+ }
18245
+ return paraId;
18246
+ };
18247
+ const targetKey = keyOf(target);
18248
+ const targetEntry = entryFor(targetKey);
18249
+ const rootKey = targetEntry && attrOf(targetEntry, "w15:paraIdParent", "paraIdParent").toUpperCase() || targetKey;
18250
+ const threadKeys = /* @__PURE__ */ new Set([rootKey, targetKey]);
18251
+ for (const entry of Array.from(root.getElementsByTagNameNS("*", "commentEx"))) {
18252
+ if (attrOf(entry, "w15:paraIdParent", "paraIdParent").toUpperCase() === rootKey) threadKeys.add(attrOf(entry, "w15:paraId", "paraId").toUpperCase());
18253
+ }
18254
+ const doneValue = resolved ? "1" : "0";
18255
+ let extendedChanged = !commentsExtendedXml;
18256
+ for (const key of threadKeys) {
18257
+ let entry = entryFor(key);
18258
+ if (!entry) {
18259
+ entry = extendedDoc.createElementNS(NS_W15, "w15:commentEx");
18260
+ entry.setAttributeNS(NS_W15, "w15:paraId", key);
18261
+ if (key !== rootKey) entry.setAttributeNS(NS_W15, "w15:paraIdParent", rootKey);
18262
+ entry.setAttributeNS(NS_W15, "w15:done", doneValue);
18263
+ root.appendChild(entry);
18264
+ extendedChanged = true;
18265
+ } else if (attrOf(entry, "w15:done", "done") !== doneValue) {
18266
+ entry.setAttributeNS(NS_W15, "w15:done", doneValue);
18267
+ extendedChanged = true;
18268
+ }
18269
+ }
18270
+ const byKey = new Map(comments.map((node) => [commentParaId(node), node]));
18271
+ const commentIds = [...threadKeys].map((key) => byKey.get(key)).filter(Boolean).map(idOf);
18272
+ const rootComment = byKey.get(rootKey);
18273
+ const serializer = createSerializer();
18274
+ const hasChanges = extendedChanged || commentsChanged;
18275
+ return {
18276
+ status: "ok",
18277
+ hasChanges,
18278
+ resolved,
18279
+ threadRootId: rootComment ? idOf(rootComment) : String(commentId),
18280
+ commentIds,
18281
+ ...hasChanges ? { commentsExtendedXml: serializer.serializeToString(extendedDoc) } : {},
18282
+ ...commentsChanged ? { commentsXml: serializer.serializeToString(commentsDoc) } : {}
18283
+ };
18284
+ }
18285
+ var NS_W_MAIN = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
18286
+ function anchorReplyInDocument(xmlDoc, memberIds, newId) {
18287
+ const wanted = new Set(memberIds.map(String));
18288
+ const idOf = (node) => attrOf(node, "w:id", "id");
18289
+ const all = (name) => Array.from(xmlDoc.getElementsByTagNameNS("*", name)).filter((node) => wanted.has(idOf(node)));
18290
+ const starts = all("commentRangeStart");
18291
+ const references = all("commentReference");
18292
+ if (!starts.length || !references.length) return false;
18293
+ const lastStart = starts[starts.length - 1];
18294
+ const lastReference = references[references.length - 1];
18295
+ const referenceRun = lastReference.parentNode?.localName === "r" ? lastReference.parentNode : lastReference;
18296
+ const make = (name, parent) => {
18297
+ const element = xmlDoc.createElementNS(NS_W_MAIN, `w:${name}`);
18298
+ element.setAttributeNS(NS_W_MAIN, "w:id", String(newId));
18299
+ if (parent) parent.appendChild(element);
18300
+ return element;
18301
+ };
18302
+ const insertAfter = (anchor, node) => anchor.parentNode.insertBefore(node, anchor.nextSibling);
18303
+ insertAfter(lastStart, make("commentRangeStart"));
18304
+ const end = make("commentRangeEnd");
18305
+ const run = xmlDoc.createElementNS(NS_W_MAIN, "w:r");
18306
+ run.appendChild(xmlDoc.createElementNS(NS_W_MAIN, "w:rPr"));
18307
+ make("commentReference", run);
18308
+ insertAfter(referenceRun, end);
18309
+ insertAfter(end, run);
18310
+ return true;
18311
+ }
18312
+ var NS_W16CID = "http://schemas.microsoft.com/office/word/2016/wordml/cid";
18313
+ var NS_W16CEX = "http://schemas.microsoft.com/office/word/2018/wordml/cex";
18314
+ var durableIdFor = (paraId, taken) => {
18315
+ let value = (Number.parseInt(paraId, 16) ^ 625341585) & 2147483646;
18316
+ let candidate = value.toString(16).toUpperCase().padStart(8, "0");
18317
+ while (taken.has(candidate)) {
18318
+ value = value + 2 & 2147483646;
18319
+ candidate = value.toString(16).toUpperCase().padStart(8, "0");
18320
+ }
18321
+ taken.add(candidate);
18322
+ return candidate;
18323
+ };
18324
+ function reconcileCommentSiblingParts({ commentsXml, commentsIdsXml = null, commentsExtensibleXml = null }) {
18325
+ const out = {};
18326
+ if (!commentsXml || !commentsIdsXml && !commentsExtensibleXml) return out;
18327
+ const commentsParsed = parseOoxmlSafe(commentsXml, "application/xml");
18328
+ if (!commentsParsed.doc || commentsParsed.error) return out;
18329
+ const serializer = createSerializer();
18330
+ const byKey = /* @__PURE__ */ new Map();
18331
+ for (const node of Array.from(commentsParsed.doc.getElementsByTagNameNS("*", "comment"))) {
18332
+ const key = commentParaId(node);
18333
+ if (key) byKey.set(key, node);
18334
+ }
18335
+ let idsDoc = null;
18336
+ const durableByKey = /* @__PURE__ */ new Map();
18337
+ const taken = /* @__PURE__ */ new Set();
18338
+ let idsChanged = false;
18339
+ if (commentsIdsXml) {
18340
+ const parsed = parseOoxmlSafe(commentsIdsXml, "application/xml");
18341
+ if (!parsed.doc || parsed.error) return out;
18342
+ idsDoc = parsed.doc;
18343
+ const root = idsDoc.documentElement;
18344
+ for (const entry of Array.from(root.getElementsByTagNameNS("*", "commentId"))) {
18345
+ const key = attrOf(entry, "w16cid:paraId", "paraId").toUpperCase();
18346
+ const durable = attrOf(entry, "w16cid:durableId", "durableId").toUpperCase();
18347
+ if (durable) taken.add(durable);
18348
+ if (!byKey.has(key)) {
18349
+ root.removeChild(entry);
18350
+ idsChanged = true;
18351
+ continue;
18352
+ }
18353
+ durableByKey.set(key, durable);
18354
+ }
18355
+ for (const key of byKey.keys()) {
18356
+ if (durableByKey.has(key)) continue;
18357
+ const durable = durableIdFor(key, taken);
18358
+ const entry = idsDoc.createElementNS(NS_W16CID, "w16cid:commentId");
18359
+ entry.setAttributeNS(NS_W16CID, "w16cid:paraId", key);
18360
+ entry.setAttributeNS(NS_W16CID, "w16cid:durableId", durable);
18361
+ root.appendChild(entry);
18362
+ durableByKey.set(key, durable);
18363
+ idsChanged = true;
18364
+ }
18365
+ }
18366
+ let extDoc = null;
18367
+ let extChanged = false;
18368
+ if (commentsExtensibleXml && idsDoc) {
18369
+ const parsed = parseOoxmlSafe(commentsExtensibleXml, "application/xml");
18370
+ if (parsed.doc && !parsed.error) {
18371
+ extDoc = parsed.doc;
18372
+ const root = extDoc.documentElement;
18373
+ const live = new Set(durableByKey.values());
18374
+ const present = /* @__PURE__ */ new Set();
18375
+ for (const entry of Array.from(root.getElementsByTagNameNS("*", "commentExtensible"))) {
18376
+ const durable = attrOf(entry, "w16cex:durableId", "durableId").toUpperCase();
18377
+ if (!live.has(durable)) {
18378
+ root.removeChild(entry);
18379
+ extChanged = true;
18380
+ } else present.add(durable);
18381
+ }
18382
+ for (const [key, durable] of durableByKey) {
18383
+ if (present.has(durable)) continue;
18384
+ const date = attrOf(byKey.get(key), "w:date", "date");
18385
+ const entry = extDoc.createElementNS(NS_W16CEX, "w16cex:commentExtensible");
18386
+ entry.setAttributeNS(NS_W16CEX, "w16cex:durableId", durable);
18387
+ if (date) entry.setAttributeNS(NS_W16CEX, "w16cex:dateUtc", date);
18388
+ root.appendChild(entry);
18389
+ extChanged = true;
18390
+ }
18391
+ }
18392
+ }
18393
+ if (idsChanged) out.commentsIdsXml = serializer.serializeToString(idsDoc);
18394
+ if (extChanged) out.commentsExtensibleXml = serializer.serializeToString(extDoc);
18395
+ return out;
18396
+ }
18397
+
18130
18398
  // core/sha256.js
18131
18399
  var K = new Uint32Array([
18132
18400
  1116352408,
@@ -18619,7 +18887,7 @@ function readCommentDefinitions(commentsDoc) {
18619
18887
  author: attr2(comment, "author") || null,
18620
18888
  date: attr2(comment, "date") || null,
18621
18889
  text: paragraphs.map((p) => extractCanonicalParagraphText(p)).join("\n"),
18622
- paraId: paragraphs[0]?.getAttribute?.("w14:paraId") || paragraphs[0]?.getAttribute?.("paraId") || null
18890
+ paraId: commentParaId(comment) || null
18623
18891
  });
18624
18892
  }
18625
18893
  return result;
@@ -18850,43 +19118,6 @@ function inspectDocumentParts(parts, options = {}) {
18850
19118
  };
18851
19119
  }
18852
19120
 
18853
- // services/comment-builders.js
18854
- var NS_W14 = "http://schemas.microsoft.com/office/word/2010/wordml";
18855
- var NS_W15 = "http://schemas.microsoft.com/office/word/2012/wordml";
18856
- function createCommentParaId(commentId) {
18857
- const numeric = Number.parseInt(String(commentId), 10);
18858
- const value = Number.isFinite(numeric) ? 1879048192 + (numeric >>> 0) >>> 0 : 1879048192;
18859
- return value.toString(16).toUpperCase().padStart(8, "0").slice(-8);
18860
- }
18861
- function buildCommentElement(commentId, author, content, date, paraId = createCommentParaId(commentId)) {
18862
- const initials = author.split(" ").map((word) => word[0]).join("").toUpperCase() || "AI";
18863
- const escapedContent = escapeXml(content);
18864
- const escapedAuthor = escapeXml(author);
18865
- return `<w:comment w:id="${commentId}" w:author="${escapedAuthor}" w:date="${date}" w:initials="${initials}">
18866
- <w:p w14:paraId="${escapeXml(paraId)}" xmlns:w14="${NS_W14}">
18867
- <w:r><w:t>${escapedContent}</w:t></w:r>
18868
- </w:p>
18869
- </w:comment>`;
18870
- }
18871
- function buildCommentsPartXml(comments) {
18872
- if (!comments || comments.length === 0) {
18873
- return `<w:comments xmlns:w="${NS_W}"></w:comments>`;
18874
- }
18875
- const commentElements = comments.map(
18876
- (comment) => buildCommentElement(comment.id, comment.author, comment.content, comment.date, comment.paraId)
18877
- ).join("\n ");
18878
- return `<w:comments xmlns:w="${NS_W}">
18879
- ${commentElements}
18880
- </w:comments>`;
18881
- }
18882
- function buildCommentsExtendedPartXml(entries) {
18883
- const body = (entries || []).map((entry) => {
18884
- const parent = entry.paraIdParent ? ` w15:paraIdParent="${escapeXml(entry.paraIdParent)}"` : "";
18885
- return `<w15:commentEx w15:paraId="${escapeXml(entry.paraId)}"${parent} w15:done="${entry.done ? "1" : "0"}"/>`;
18886
- }).join("");
18887
- return `<w15:commentsEx xmlns:w15="${NS_W15}">${body}</w15:commentsEx>`;
18888
- }
18889
-
18890
19121
  // services/comment-locator.js
18891
19122
  function createParagraphTextIndex(paragraph, options = {}) {
18892
19123
  const revisionView = options.revisionView === "current" ? "accepted" : options.revisionView || "accepted";
@@ -19347,29 +19578,7 @@ function parseRequired(xml, partName) {
19347
19578
  }
19348
19579
  return { doc: parsed.doc };
19349
19580
  }
19350
- function usedParaIds(commentsDoc, extendedDoc) {
19351
- const ids = /* @__PURE__ */ new Set();
19352
- for (const p of Array.from(commentsDoc?.getElementsByTagNameNS("*", "p") || [])) {
19353
- const id = attr3(p, "w14:paraId", "paraId");
19354
- if (id) ids.add(id.toUpperCase());
19355
- }
19356
- for (const ex of Array.from(extendedDoc?.getElementsByTagNameNS("*", "commentEx") || [])) {
19357
- const id = attr3(ex, "w15:paraId", "paraId");
19358
- if (id) ids.add(id.toUpperCase());
19359
- }
19360
- return ids;
19361
- }
19362
- function allocateParaId(commentId, occupied) {
19363
- let candidate = createCommentParaId(commentId);
19364
- let value = Number.parseInt(candidate, 16) >>> 0;
19365
- while (occupied.has(candidate)) {
19366
- value = value + 1 >>> 0;
19367
- candidate = value.toString(16).toUpperCase().padStart(8, "0");
19368
- }
19369
- occupied.add(candidate);
19370
- return candidate;
19371
- }
19372
- function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, parentCommentId, commentId, commentContent, author, date = (/* @__PURE__ */ new Date()).toISOString() }) {
19581
+ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, documentXml = null, parentCommentId, commentId, commentContent, author, date = (/* @__PURE__ */ new Date()).toISOString() }) {
19373
19582
  if (!commentsXml) return { status: "error", error: { code: "COMMENTS_PART_MISSING", message: "A comment reply requires an existing word/comments.xml part." } };
19374
19583
  const commentsParsed = parseRequired(commentsXml, "word/comments.xml");
19375
19584
  if (commentsParsed.error) return { status: "error", error: commentsParsed.error };
@@ -19383,15 +19592,31 @@ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, par
19383
19592
  extendedDoc = parsed.doc;
19384
19593
  }
19385
19594
  const occupied = usedParaIds(commentsDoc, extendedDoc);
19386
- const parentParagraph = Array.from(parent.getElementsByTagNameNS(NS_W6, "p"))[0] || Array.from(parent.getElementsByTagNameNS("*", "p"))[0];
19595
+ if (documentXml) for (const match of String(documentXml).matchAll(/w14:paraId="([0-9A-Fa-f]{8})"/g)) occupied.add(match[1].toUpperCase());
19596
+ const parentParagraph = commentThreadParagraph(parent);
19387
19597
  if (!parentParagraph) return { status: "error", error: { code: "PARENT_COMMENT_INVALID", message: `Parent comment '${parentCommentId}' has no paragraph.` } };
19388
- let parentParaId = attr3(parentParagraph, "w14:paraId", "paraId");
19598
+ let parentParaId = commentParaId(parent);
19389
19599
  if (!parentParaId) {
19390
19600
  parentParaId = allocateParaId(parentCommentId, occupied);
19391
19601
  parentParagraph.setAttributeNS(NS_W14, "w14:paraId", parentParaId);
19392
19602
  } else {
19393
19603
  parentParaId = parentParaId.toUpperCase();
19394
19604
  }
19605
+ for (const node of Array.from(extendedDoc?.getElementsByTagNameNS("*", "commentEx") || [])) {
19606
+ if (attr3(node, "w15:paraId", "paraId").toUpperCase() === parentParaId) {
19607
+ const root2 = attr3(node, "w15:paraIdParent", "paraIdParent").toUpperCase();
19608
+ if (root2) parentParaId = root2;
19609
+ break;
19610
+ }
19611
+ }
19612
+ const threadCommentIds = [];
19613
+ const commentNodes = Array.from(commentsDoc.getElementsByTagNameNS("*", "comment"));
19614
+ const parentOf = new Map(Array.from(extendedDoc?.getElementsByTagNameNS("*", "commentEx") || []).map((node) => [attr3(node, "w15:paraId", "paraId").toUpperCase(), attr3(node, "w15:paraIdParent", "paraIdParent").toUpperCase()]));
19615
+ for (const node of commentNodes) {
19616
+ const key = commentParaId(node);
19617
+ if (key === parentParaId || parentOf.get(key) === parentParaId) threadCommentIds.push(attr3(node, "w:id", "id"));
19618
+ }
19619
+ const threadRootCommentId = attr3(commentNodes.find((node) => commentParaId(node) === parentParaId), "w:id", "id") || String(parentCommentId);
19395
19620
  const replyParaId = allocateParaId(commentId, occupied);
19396
19621
  const replyParsed = parseRequired(`<w:comments xmlns:w="${NS_W6}" xmlns:w14="${NS_W14}">${buildCommentElement(commentId, author, commentContent, date, replyParaId)}</w:comments>`, "reply comment");
19397
19622
  commentsDoc.documentElement.appendChild(commentsDoc.importNode(replyParsed.doc.documentElement.firstChild, true));
@@ -19400,16 +19625,18 @@ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, par
19400
19625
  }
19401
19626
  const root = extendedDoc.documentElement;
19402
19627
  const entries = Array.from(root.getElementsByTagNameNS("*", "commentEx"));
19403
- if (!entries.some((node) => attr3(node, "w15:paraId", "paraId").toUpperCase() === parentParaId)) {
19628
+ const rootEntry = entries.find((node) => attr3(node, "w15:paraId", "paraId").toUpperCase() === parentParaId);
19629
+ if (!rootEntry) {
19404
19630
  const parentEx = extendedDoc.createElementNS(NS_W15, "w15:commentEx");
19405
19631
  parentEx.setAttributeNS(NS_W15, "w15:paraId", parentParaId);
19406
19632
  parentEx.setAttributeNS(NS_W15, "w15:done", "0");
19407
19633
  root.appendChild(parentEx);
19408
19634
  }
19635
+ const threadDone = attr3(rootEntry, "w15:done", "done") === "1" ? "1" : "0";
19409
19636
  const replyEx = extendedDoc.createElementNS(NS_W15, "w15:commentEx");
19410
19637
  replyEx.setAttributeNS(NS_W15, "w15:paraId", replyParaId);
19411
19638
  replyEx.setAttributeNS(NS_W15, "w15:paraIdParent", parentParaId);
19412
- replyEx.setAttributeNS(NS_W15, "w15:done", "0");
19639
+ replyEx.setAttributeNS(NS_W15, "w15:done", threadDone);
19413
19640
  root.appendChild(replyEx);
19414
19641
  const serializer = createSerializer();
19415
19642
  return {
@@ -19422,7 +19649,9 @@ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, par
19422
19649
  commentId,
19423
19650
  parentCommentId: String(parentCommentId),
19424
19651
  paraId: replyParaId,
19425
- parentParaId
19652
+ parentParaId,
19653
+ threadRootCommentId,
19654
+ threadCommentIds: threadCommentIds.length ? threadCommentIds : [threadRootCommentId]
19426
19655
  };
19427
19656
  }
19428
19657
 
@@ -19487,19 +19716,19 @@ function applyFormattingRemovalToOoxml(ooxmlString, targetText, formatTypes) {
19487
19716
  if (!targetText || !ooxmlString) return ooxmlString;
19488
19717
  const doc = parseOoxml(ooxmlString);
19489
19718
  if (!doc) return ooxmlString;
19490
- const NS_W10 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19491
- const runs = doc.getElementsByTagNameNS(NS_W10, "r");
19492
- const insertions = doc.getElementsByTagNameNS(NS_W10, "ins");
19719
+ const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19720
+ const runs = doc.getElementsByTagNameNS(NS_W11, "r");
19721
+ const insertions = doc.getElementsByTagNameNS(NS_W11, "ins");
19493
19722
  const allRuns = [...Array.from(runs)];
19494
19723
  for (const ins of insertions) {
19495
- const insideRuns = ins.getElementsByTagNameNS(NS_W10, "r");
19724
+ const insideRuns = ins.getElementsByTagNameNS(NS_W11, "r");
19496
19725
  allRuns.push(...Array.from(insideRuns));
19497
19726
  }
19498
19727
  for (const run of allRuns) {
19499
- const textNodes = run.getElementsByTagNameNS(NS_W10, "t");
19728
+ const textNodes = run.getElementsByTagNameNS(NS_W11, "t");
19500
19729
  const runText = Array.from(textNodes).map((t) => t.textContent).join("");
19501
19730
  if (runText.includes(targetText) || runText === targetText) {
19502
- const rPrElements = run.getElementsByTagNameNS(NS_W10, "rPr");
19731
+ const rPrElements = run.getElementsByTagNameNS(NS_W11, "rPr");
19503
19732
  if (rPrElements.length > 0) {
19504
19733
  const rPr = rPrElements[0];
19505
19734
  const newRPr = removeFormattingFromRPr(rPr, formatTypes);
@@ -19534,7 +19763,7 @@ var HIGHLIGHT_COLOR_MAP = {
19534
19763
  "white": "white"
19535
19764
  };
19536
19765
  function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
19537
- const NS_W10 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19766
+ const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19538
19767
  const ooxmlColor = HIGHLIGHT_COLOR_MAP[color.toLowerCase()] || "yellow";
19539
19768
  const generateRedlines = options?.generateRedlines ?? false;
19540
19769
  const author = options?.author || getDefaultAuthor();
@@ -19553,10 +19782,10 @@ function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
19553
19782
  }
19554
19783
  });
19555
19784
  }
19556
- const existingHighlight = rPrElement.getElementsByTagNameNS(NS_W10, "highlight");
19785
+ const existingHighlight = rPrElement.getElementsByTagNameNS(NS_W11, "highlight");
19557
19786
  Array.from(existingHighlight).forEach(removeNode2);
19558
19787
  const highlightEl = createWordElement(doc, "w:highlight");
19559
- highlightEl.setAttributeNS(NS_W10, "w:val", ooxmlColor);
19788
+ highlightEl.setAttributeNS(NS_W11, "w:val", ooxmlColor);
19560
19789
  rPrElement.appendChild(highlightEl);
19561
19790
  if (generateRedlines && previousRPrState) {
19562
19791
  const rPrChange = createWordElement(doc, "w:rPrChange");
@@ -19565,7 +19794,7 @@ function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
19565
19794
  rPrChange.setAttribute("w:author", metadata.author);
19566
19795
  rPrChange.setAttribute("w:date", metadata.date);
19567
19796
  rPrChange.appendChild(previousRPrState);
19568
- const existingChange = rPrElement.getElementsByTagNameNS(NS_W10, "rPrChange");
19797
+ const existingChange = rPrElement.getElementsByTagNameNS(NS_W11, "rPrChange");
19569
19798
  Array.from(existingChange).forEach(removeNode2);
19570
19799
  rPrElement.appendChild(rPrChange);
19571
19800
  }
@@ -19582,12 +19811,12 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
19582
19811
  } else {
19583
19812
  revisionIdAllocator = createRevisionIdAllocator(doc);
19584
19813
  }
19585
- const NS_W10 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19814
+ const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19586
19815
  const getRunText = (run) => {
19587
- const textNodes = run.getElementsByTagNameNS(NS_W10, "t");
19816
+ const textNodes = run.getElementsByTagNameNS(NS_W11, "t");
19588
19817
  return Array.from(textNodes).map((t) => t.textContent).join("");
19589
19818
  };
19590
- const allRuns = Array.from(doc.getElementsByTagNameNS(NS_W10, "r"));
19819
+ const allRuns = Array.from(doc.getElementsByTagNameNS(NS_W11, "r"));
19591
19820
  const sourceRunsWithClaimedRevisionIds = /* @__PURE__ */ new WeakSet();
19592
19821
  const cloneRunWithText2 = (sourceRun, text2, shouldHighlight) => {
19593
19822
  const clonedRun = sourceRun.cloneNode(true);
@@ -19596,14 +19825,14 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
19596
19825
  } else {
19597
19826
  sourceRunsWithClaimedRevisionIds.add(sourceRun);
19598
19827
  }
19599
- const textNodes = clonedRun.getElementsByTagNameNS(NS_W10, "t");
19828
+ const textNodes = clonedRun.getElementsByTagNameNS(NS_W11, "t");
19600
19829
  Array.from(textNodes).forEach(removeNode2);
19601
19830
  const newText = createWordElement(doc, "w:t");
19602
19831
  newText.setAttribute("xml:space", "preserve");
19603
19832
  newText.textContent = text2;
19604
19833
  clonedRun.appendChild(newText);
19605
19834
  if (shouldHighlight) {
19606
- const rPrElements = clonedRun.getElementsByTagNameNS(NS_W10, "rPr");
19835
+ const rPrElements = clonedRun.getElementsByTagNameNS(NS_W11, "rPr");
19607
19836
  const existingRPr = rPrElements.length > 0 ? rPrElements[0] : null;
19608
19837
  const newRPr = injectHighlightIntoRPr(doc, existingRPr, color, options);
19609
19838
  if (existingRPr) {
@@ -19652,20 +19881,68 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
19652
19881
  return serializeOoxml(doc);
19653
19882
  }
19654
19883
 
19884
+ // services/package-parts.js
19885
+ var WORD_ML = "application/vnd.openxmlformats-officedocument.wordprocessingml";
19886
+ var REL_OFFICE_DOC = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
19887
+ var PART_SPECS = Object.freeze([
19888
+ { id: "document", path: "word/document.xml", pathPattern: null, contentType: `${WORD_ML}.document.main+xml`, legacyContentTypes: [], relType: `${REL_OFFICE_DOC}/officeDocument`, relTarget: null, verified: true },
19889
+ { id: "numbering", path: "word/numbering.xml", pathPattern: null, contentType: `${WORD_ML}.numbering+xml`, legacyContentTypes: [], relType: `${REL_OFFICE_DOC}/numbering`, relTarget: "numbering.xml", verified: false },
19890
+ { id: "comments", path: "word/comments.xml", pathPattern: null, contentType: `${WORD_ML}.comments+xml`, legacyContentTypes: [], relType: `${REL_OFFICE_DOC}/comments`, relTarget: "comments.xml", verified: true },
19891
+ {
19892
+ id: "commentsExtended",
19893
+ path: "word/commentsExtended.xml",
19894
+ pathPattern: null,
19895
+ contentType: `${WORD_ML}.commentsExtended+xml`,
19896
+ // Written by this package through 0.8.0; Word does not recognize it and offers to repair the file.
19897
+ legacyContentTypes: ["application/vnd.ms-word.commentsExtended+xml"],
19898
+ relType: "http://schemas.microsoft.com/office/2011/relationships/commentsExtended",
19899
+ relTarget: "commentsExtended.xml",
19900
+ verified: true
19901
+ },
19902
+ { 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 },
19903
+ { 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 },
19904
+ // Not yet produced by any Word fixture (Word only writes people.xml for signed-in authors): unverified.
19905
+ { 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 },
19906
+ { 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 },
19907
+ { 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 }
19908
+ ].map((spec) => Object.freeze(spec)));
19909
+ var BY_ID = new Map(PART_SPECS.map((spec) => [spec.id, spec]));
19910
+ function getPartSpec(id) {
19911
+ const spec = BY_ID.get(id);
19912
+ if (!spec) throw new Error(`Unknown package part: ${id}`);
19913
+ return spec;
19914
+ }
19915
+ function toEntryName(partName) {
19916
+ return String(partName || "").replace(/^\/+/, "");
19917
+ }
19918
+ function findPartSpecByPartName(partName) {
19919
+ const name = toEntryName(partName);
19920
+ const lower = name.toLowerCase();
19921
+ return PART_SPECS.find((spec) => spec.path ? spec.path.toLowerCase() === lower : spec.pathPattern?.test(name)) || null;
19922
+ }
19923
+ function correctedContentTypeFor(partName, actualContentType) {
19924
+ const spec = findPartSpecByPartName(partName);
19925
+ if (!spec || !spec.legacyContentTypes.includes(actualContentType)) return null;
19926
+ return spec.contentType;
19927
+ }
19928
+
19655
19929
  // services/standalone-docx-plumbing.js
19656
19930
  var NS_W7 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19657
19931
  var NS_CT = "http://schemas.openxmlformats.org/package/2006/content-types";
19658
19932
  var NS_RELS = "http://schemas.openxmlformats.org/package/2006/relationships";
19659
- var NUMBERING_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering";
19660
- var NUMBERING_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml";
19661
- var COMMENTS_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
19662
- var COMMENTS_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml";
19663
- var COMMENTS_EXTENDED_REL_TYPE = "http://schemas.microsoft.com/office/2011/relationships/commentsExtended";
19664
- var COMMENTS_EXTENDED_CONTENT_TYPE = "application/vnd.ms-word.commentsExtended+xml";
19665
- var DOCUMENT_PATH = "word/document.xml";
19666
- var NUMBERING_PATH = "word/numbering.xml";
19667
- var COMMENTS_PATH = "word/comments.xml";
19668
- var COMMENTS_EXTENDED_PATH = "word/commentsExtended.xml";
19933
+ var NUMBERING = getPartSpec("numbering");
19934
+ var COMMENTS = getPartSpec("comments");
19935
+ var COMMENTS_EXTENDED = getPartSpec("commentsExtended");
19936
+ var NUMBERING_REL_TYPE = NUMBERING.relType;
19937
+ var NUMBERING_CONTENT_TYPE = NUMBERING.contentType;
19938
+ var COMMENTS_REL_TYPE = COMMENTS.relType;
19939
+ var COMMENTS_CONTENT_TYPE = COMMENTS.contentType;
19940
+ var COMMENTS_EXTENDED_REL_TYPE = COMMENTS_EXTENDED.relType;
19941
+ var COMMENTS_EXTENDED_CONTENT_TYPE = COMMENTS_EXTENDED.contentType;
19942
+ var DOCUMENT_PATH = getPartSpec("document").path;
19943
+ var NUMBERING_PATH = NUMBERING.path;
19944
+ var COMMENTS_PATH = COMMENTS.path;
19945
+ var COMMENTS_EXTENDED_PATH = COMMENTS_EXTENDED.path;
19669
19946
  var CONTENT_TYPES_PATH = "[Content_Types].xml";
19670
19947
  var DOCUMENT_RELS_PATH = "word/_rels/document.xml.rels";
19671
19948
  function parseXmlStrictStandalone(xmlText, label = "xml") {
@@ -19953,6 +20230,22 @@ async function ensureCommentsExtendedArtifactsInZip(zip, commentsExtendedXml, op
19953
20230
  }
19954
20231
  }
19955
20232
  }
20233
+ async function repairKnownContentTypes(zip) {
20234
+ const ctText = await readZipText(zip, CONTENT_TYPES_PATH);
20235
+ if (!ctText) return [];
20236
+ const ctDoc = parseXmlStrictStandalone(ctText, CONTENT_TYPES_PATH);
20237
+ const repairs = [];
20238
+ for (const override of Array.from(ctDoc.getElementsByTagNameNS("*", "Override"))) {
20239
+ const partName = override.getAttribute("PartName") || "";
20240
+ const actual = override.getAttribute("ContentType") || "";
20241
+ const expected = correctedContentTypeFor(partName, actual);
20242
+ if (!expected) continue;
20243
+ override.setAttribute("ContentType", expected);
20244
+ repairs.push({ partName, from: actual, to: expected });
20245
+ }
20246
+ if (repairs.length > 0) zip.file(CONTENT_TYPES_PATH, createSerializer().serializeToString(ctDoc));
20247
+ return repairs;
20248
+ }
19956
20249
  async function validateDocxPackage(zip) {
19957
20250
  const documentXml = await readZipText(zip, DOCUMENT_PATH);
19958
20251
  if (!documentXml) {
@@ -20034,9 +20327,8 @@ async function validateDocxPackage(zip) {
20034
20327
  const paraIdToCommentId = /* @__PURE__ */ new Map();
20035
20328
  for (const comment of Array.from(commentsDoc.getElementsByTagNameNS("*", "comment"))) {
20036
20329
  const id = comment.getAttribute("w:id") || comment.getAttribute("id");
20037
- const paragraph = Array.from(comment.getElementsByTagNameNS("*", "p"))[0];
20038
- const paraId = paragraph?.getAttribute("w14:paraId") || paragraph?.getAttribute("paraId");
20039
- if (paraId) paraIdToCommentId.set(paraId.toUpperCase(), id);
20330
+ const paraId = commentParaId(comment);
20331
+ if (paraId) paraIdToCommentId.set(paraId, id);
20040
20332
  }
20041
20333
  const knownParaIds = new Set(paraIdToCommentId.keys());
20042
20334
  const extendedParaIds = /* @__PURE__ */ new Set();
@@ -20097,10 +20389,39 @@ async function validateDocxPackage(zip) {
20097
20389
  throw new Error("Validation failed: comments rel missing");
20098
20390
  }
20099
20391
  }
20392
+ {
20393
+ const relationshipById = /* @__PURE__ */ new Map();
20394
+ for (const rel of Array.from(relsDoc.getElementsByTagNameNS("*", "Relationship"))) {
20395
+ relationshipById.set(rel.getAttribute("Id") || "", { type: rel.getAttribute("Type") || "", target: rel.getAttribute("Target") || "" });
20396
+ }
20397
+ const headerSpec = getPartSpec("header");
20398
+ const footerSpec = getPartSpec("footer");
20399
+ const overrideTypeByPart = new Map(Array.from(ctDoc.getElementsByTagNameNS("*", "Override")).map((override) => [(override.getAttribute("PartName") || "").toLowerCase(), override.getAttribute("ContentType") || ""]));
20400
+ for (const reference of [
20401
+ ...Array.from(documentDoc.getElementsByTagNameNS(NS_W7, "headerReference")),
20402
+ ...Array.from(documentDoc.getElementsByTagNameNS(NS_W7, "footerReference"))
20403
+ ]) {
20404
+ const relId = reference.getAttributeNS?.("http://schemas.openxmlformats.org/officeDocument/2006/relationships", "id") || reference.getAttribute("r:id") || "";
20405
+ const spec = reference.localName === "headerReference" ? headerSpec : footerSpec;
20406
+ const relationship = relationshipById.get(relId);
20407
+ if (!relationship) throw new Error(`Validation failed: ${reference.localName} ${relId || "(no r:id)"} has no document relationship`);
20408
+ if (relationship.type !== spec.relType) throw new Error(`Validation failed: ${reference.localName} ${relId} points at a relationship of the wrong type (${relationship.type})`);
20409
+ const partName = relationship.target.startsWith("/") ? relationship.target : `/word/${relationship.target}`;
20410
+ const partXml = await readZipText(zip, partName.slice(1));
20411
+ if (!partXml) throw new Error(`Validation failed: ${reference.localName} ${relId} targets a missing part: ${partName}`);
20412
+ parseXmlStrictStandalone(partXml, partName.slice(1));
20413
+ const contentType = overrideTypeByPart.get(partName.toLowerCase());
20414
+ if (contentType !== spec.contentType) throw new Error(`Validation failed: ${partName} content type is ${contentType ? `'${contentType}'` : "missing"}; Word expects '${spec.contentType}'`);
20415
+ }
20416
+ }
20100
20417
  if (commentsExtendedXml) {
20101
- const hasContentType = Array.from(ctDoc.getElementsByTagNameNS("*", "Override")).some(
20102
- (override) => (override.getAttribute("PartName") || "").toLowerCase() === "/word/commentsextended.xml" && (override.getAttribute("ContentType") || "") === COMMENTS_EXTENDED_CONTENT_TYPE
20418
+ const extendedOverride = Array.from(ctDoc.getElementsByTagNameNS("*", "Override")).find(
20419
+ (override) => (override.getAttribute("PartName") || "").toLowerCase() === "/word/commentsextended.xml"
20103
20420
  );
20421
+ if (extendedOverride && extendedOverride.getAttribute("ContentType") !== COMMENTS_EXTENDED_CONTENT_TYPE) {
20422
+ throw new Error(`Validation failed: commentsExtended CT override has wrong content type '${extendedOverride.getAttribute("ContentType")}' (Word expects '${COMMENTS_EXTENDED_CONTENT_TYPE}')`);
20423
+ }
20424
+ const hasContentType = !!extendedOverride;
20104
20425
  const hasRelationship = Array.from(relsDoc.getElementsByTagNameNS("*", "Relationship")).some(
20105
20426
  (rel) => (rel.getAttribute("Type") || "") === COMMENTS_EXTENDED_REL_TYPE
20106
20427
  );
@@ -20464,6 +20785,156 @@ function reconcileReceiptsAgainstOutput(parts, receipts) {
20464
20785
  return { valid: true };
20465
20786
  }
20466
20787
 
20788
+ // services/headers-footers.js
20789
+ var NS_W8 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
20790
+ var NS_R2 = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
20791
+ var REVISION_ELEMENTS = /* @__PURE__ */ new Set(["ins", "del", "moveFrom", "moveTo", "rPrChange", "pPrChange", "sectPrChange", "tblPrChange", "tblGridChange", "tcPrChange", "trPrChange", "numberingChange"]);
20792
+ var REVISION_CONTENT_ELEMENTS = /* @__PURE__ */ new Set(["ins", "del", "moveFrom", "moveTo"]);
20793
+ var attrOf2 = (node, qualified, local) => node?.getAttribute?.(qualified) || node?.getAttribute?.(local) || "";
20794
+ function resolveTarget(target) {
20795
+ const clean = String(target || "");
20796
+ if (clean.startsWith("/")) return clean.replace(/^\/+/, "");
20797
+ const parts = ["word"];
20798
+ for (const segment of clean.split("/")) {
20799
+ if (segment === "..") parts.pop();
20800
+ else if (segment && segment !== ".") parts.push(segment);
20801
+ }
20802
+ return parts.join("/");
20803
+ }
20804
+ function discoverHeaderFooterParts({ documentXml, relsXml = null, settingsXml = null }) {
20805
+ if (!documentXml || !relsXml) return [];
20806
+ const documentParsed = parseOoxmlSafe(documentXml, "application/xml");
20807
+ const relsParsed = parseOoxmlSafe(relsXml, "application/xml");
20808
+ if (!documentParsed.doc || documentParsed.error || !relsParsed.doc || relsParsed.error) return [];
20809
+ const targetByRelId = /* @__PURE__ */ new Map();
20810
+ for (const rel of Array.from(relsParsed.doc.getElementsByTagNameNS("*", "Relationship"))) {
20811
+ targetByRelId.set(rel.getAttribute("Id") || "", rel.getAttribute("Target") || "");
20812
+ }
20813
+ const evenAndOdd = !!settingsXml && /<w:evenAndOddHeaders\b(?![^>]*w:val="(?:0|false|off)")/.test(settingsXml);
20814
+ const byPath = /* @__PURE__ */ new Map();
20815
+ const inForce = /* @__PURE__ */ new Map();
20816
+ const sectPrs = Array.from(documentParsed.doc.getElementsByTagNameNS(NS_W8, "sectPr"));
20817
+ sectPrs.forEach((sectPr, sectionIndex) => {
20818
+ const titlePg = Array.from(sectPr.childNodes || []).some((child) => child.localName === "titlePg" && !/^(0|false|off)$/i.test(attrOf2(child, "w:val", "val")));
20819
+ for (const ref of Array.from(sectPr.childNodes || [])) {
20820
+ if (ref.nodeType !== 1 || ref.localName !== "headerReference" && ref.localName !== "footerReference") continue;
20821
+ const relId = ref.getAttributeNS?.(NS_R2, "id") || ref.getAttribute("r:id") || "";
20822
+ const target = targetByRelId.get(relId);
20823
+ if (!target) continue;
20824
+ const path = resolveTarget(target);
20825
+ const kind = ref.localName === "headerReference" ? "header" : "footer";
20826
+ const type = attrOf2(ref, "w:type", "type") || "default";
20827
+ const entry = byPath.get(path) || { path, kind, type, sections: [], applies: [], titlePgSections: 0 };
20828
+ if (!entry.sections.includes(sectionIndex)) entry.sections.push(sectionIndex);
20829
+ if (titlePg) entry.titlePgSections += 1;
20830
+ byPath.set(path, entry);
20831
+ inForce.set(`${kind}:${type}`, path);
20832
+ }
20833
+ for (const path of inForce.values()) {
20834
+ const entry = byPath.get(path);
20835
+ if (entry && !entry.applies.includes(sectionIndex)) entry.applies.push(sectionIndex);
20836
+ }
20837
+ });
20838
+ return Array.from(byPath.values()).map((entry) => ({
20839
+ path: entry.path,
20840
+ kind: entry.kind,
20841
+ type: entry.type,
20842
+ sections: entry.sections,
20843
+ appliesToSections: entry.applies,
20844
+ sharedBySections: entry.applies.length > 1,
20845
+ // first-page parts only show with w:titlePg; even parts only with w:evenAndOddHeaders in settings.xml.
20846
+ active: entry.type === "first" ? entry.titlePgSections > 0 : entry.type === "even" ? evenAndOdd : true
20847
+ }));
20848
+ }
20849
+ function resolvePartSelector(parts, selector) {
20850
+ const describe = (part) => ({ path: part.path, kind: part.kind, type: part.type, sections: part.sections, appliesToSections: part.appliesToSections });
20851
+ if (typeof selector === "string") {
20852
+ const wanted = selector.replace(/^\/+/, "").toLowerCase();
20853
+ const match = parts.find((part) => part.path.toLowerCase() === wanted || part.path.toLowerCase() === `word/${wanted}`);
20854
+ if (match) return { part: match };
20855
+ return { error: { code: "PART_NOT_FOUND", message: `No header/footer part '${selector}' is referenced by the document.`, candidates: parts.map(describe) } };
20856
+ }
20857
+ if (!selector || typeof selector !== "object" || selector.kind !== "header" && selector.kind !== "footer") {
20858
+ return { error: { code: "INVALID_OPERATION", message: 'part must be a part path or { kind: "header"|"footer", type?: "default"|"first"|"even", section?: number }.' } };
20859
+ }
20860
+ const type = selector.type || "default";
20861
+ if (!["default", "first", "even"].includes(type)) {
20862
+ return { error: { code: "INVALID_OPERATION", message: `part.type must be default, first or even (got '${type}').` } };
20863
+ }
20864
+ let matches = parts.filter((part) => part.kind === selector.kind && part.type === type);
20865
+ if (Number.isInteger(selector.section)) matches = matches.filter((part) => part.appliesToSections.includes(selector.section));
20866
+ if (matches.length === 1) return { part: matches[0] };
20867
+ if (matches.length === 0) {
20868
+ 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) } };
20869
+ }
20870
+ return { error: { code: "PART_AMBIGUOUS", message: `Several ${type} ${selector.kind} parts exist; add "section" or use the part path.`, candidates: matches.map(describe) } };
20871
+ }
20872
+ function isHeaderFooterPath(path) {
20873
+ const spec = findPartSpecByPartName(path);
20874
+ return spec?.id === "header" || spec?.id === "footer";
20875
+ }
20876
+ function countRevisionsInsideFields(xml) {
20877
+ const parsed = parseOoxmlSafe(xml, "application/xml");
20878
+ if (!parsed.doc || parsed.error) return 0;
20879
+ let depth = 0;
20880
+ let count = 0;
20881
+ const visit = (node) => {
20882
+ for (const child of Array.from(node.childNodes || [])) {
20883
+ if (child.nodeType !== 1) continue;
20884
+ const name = child.localName;
20885
+ if (name === "fldChar") {
20886
+ const type = attrOf2(child, "w:fldCharType", "fldCharType");
20887
+ if (type === "begin") depth += 1;
20888
+ else if (type === "end") depth = Math.max(0, depth - 1);
20889
+ continue;
20890
+ }
20891
+ if (REVISION_CONTENT_ELEMENTS.has(name) && depth > 0) count += 1;
20892
+ if (name === "fldSimple") {
20893
+ depth += 1;
20894
+ visit(child);
20895
+ depth -= 1;
20896
+ continue;
20897
+ }
20898
+ visit(child);
20899
+ }
20900
+ };
20901
+ visit(parsed.doc.documentElement);
20902
+ return count;
20903
+ }
20904
+ function collectRevisionIds(xml) {
20905
+ const ids = /* @__PURE__ */ new Set();
20906
+ 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]);
20907
+ return ids;
20908
+ }
20909
+ function renumberCollidingRevisionIds(xml, taken) {
20910
+ const parsed = parseOoxmlSafe(xml, "application/xml");
20911
+ if (!parsed.doc || parsed.error) return { xml, renumbered: 0, idMap: /* @__PURE__ */ new Map() };
20912
+ const idMap = /* @__PURE__ */ new Map();
20913
+ let next = 1 + Math.max(0, ...Array.from(taken, (id) => Number(id)).filter(Number.isFinite));
20914
+ const used = new Set(taken);
20915
+ let renumbered = 0;
20916
+ const visit = (node) => {
20917
+ for (const child of Array.from(node.childNodes || [])) {
20918
+ if (child.nodeType !== 1) continue;
20919
+ if (REVISION_ELEMENTS.has(child.localName)) {
20920
+ const id = attrOf2(child, "w:id", "id");
20921
+ if (id && used.has(id)) {
20922
+ while (used.has(String(next))) next += 1;
20923
+ child.setAttributeNS(NS_W8, "w:id", String(next));
20924
+ idMap.set(id, String(next));
20925
+ used.add(String(next));
20926
+ renumbered += 1;
20927
+ } else if (id) {
20928
+ used.add(id);
20929
+ }
20930
+ }
20931
+ visit(child);
20932
+ }
20933
+ };
20934
+ visit(parsed.doc.documentElement);
20935
+ return renumbered > 0 ? { xml: createSerializer().serializeToString(parsed.doc), renumbered, idMap } : { xml, renumbered: 0, idMap };
20936
+ }
20937
+
20467
20938
  // services/error-recovery.js
20468
20939
  var ERROR_RECOVERY_VERSION = 1;
20469
20940
  var RULES = Object.freeze({
@@ -20501,6 +20972,12 @@ var RULES = Object.freeze({
20501
20972
  STRUCTURED_CONTENT_INVALID: ["request", "request_fixable", "change_request"],
20502
20973
  UNSUPPORTED_INSERTION_AFFINITY: ["request", "request_fixable", "change_request"],
20503
20974
  EXISTING_REVISIONS: ["target-safety", "policy_choice_required", "set_option"],
20975
+ PART_NOT_FOUND: ["target-resolution", "target_refresh_required", "reinspect"],
20976
+ PART_AMBIGUOUS: ["target-resolution", "candidate_selection_required", "choose_candidate"],
20977
+ COMMENT_IN_HEADER_FOOTER: ["request", "request_fixable", "change_request"],
20978
+ FIELD_EDIT_REFUSED: ["target-safety", "request_fixable", "change_request"],
20979
+ COMMENT_NOT_FOUND: ["target-resolution", "target_refresh_required", "reinspect"],
20980
+ PARENT_ANCHOR_NOT_FOUND: ["target-resolution", "target_refresh_required", "reinspect"],
20504
20981
  COMMENTED_CONTENT_MERGE: ["target-safety", "user_authorization_required", "resolve_comments"],
20505
20982
  COMMENTED_CONTENT_DELETE: ["target-safety", "user_authorization_required", "resolve_comments"],
20506
20983
  OVERLAPPING_SOURCE_TARGETS: ["batch-compilation", "source_conflict", "consolidate_operations"],
@@ -20953,6 +21430,7 @@ var SUPPORTED_OPERATION_TYPES = /* @__PURE__ */ new Set([
20953
21430
  "delete",
20954
21431
  "comment",
20955
21432
  "comment_reply",
21433
+ "comment_resolve",
20956
21434
  "highlight"
20957
21435
  ]);
20958
21436
  var EXISTING_REVISIONS_POLICIES = Object.freeze([
@@ -20971,11 +21449,14 @@ function isRecord(value) {
20971
21449
  function nonEmptyString(value) {
20972
21450
  return typeof value === "string" && value.trim().length > 0;
20973
21451
  }
21452
+ function isCommentThreadOperationKind(kind) {
21453
+ return kind === "comment_reply" || kind === "comment_resolve";
21454
+ }
20974
21455
  function getCanonicalOperationType(operation) {
20975
21456
  const type = operation?.type;
20976
21457
  if (type === "insert" && operation?.target?.revisionView === "rejected") return "rejected-insert";
20977
21458
  if (type === "restore") return "restore";
20978
- if (type === "comment" || type === "comment_reply" || type === "highlight") return type;
21459
+ if (type === "comment" || type === "comment_reply" || type === "comment_resolve" || type === "highlight") return type;
20979
21460
  if (type === "paragraph-format") return "paragraph-format";
20980
21461
  if (type === "character-format" || type === "format" && (operation?.textToFormat != null || operation?.properties != null)) return "format";
20981
21462
  return "redline";
@@ -21095,7 +21576,7 @@ function validateDocumentOperation(operation) {
21095
21576
  };
21096
21577
  }
21097
21578
  const target = normalized.targetDescriptor;
21098
- if (normalized.operationKind !== "comment_reply" && !nonEmptyString(target.text) && target.index == null && !target.paragraphId && !target.fingerprint && !target.captureRef) {
21579
+ if (!isCommentThreadOperationKind(normalized.operationKind) && !nonEmptyString(target.text) && target.index == null && !target.paragraphId && !target.fingerprint && !target.captureRef) {
21099
21580
  return {
21100
21581
  valid: false,
21101
21582
  error: {
@@ -21322,6 +21803,14 @@ function validateDocumentOperation(operation) {
21322
21803
  return { valid: false, error: { code: "INVALID_OPERATION", message: 'Comment reply operations require a non-empty "commentContent" field.' } };
21323
21804
  }
21324
21805
  }
21806
+ if (normalized.operationKind === "comment_resolve") {
21807
+ if (!nonEmptyString(String(normalized.commentId ?? ""))) {
21808
+ return { valid: false, error: { code: "INVALID_OPERATION", message: 'Comment resolve operations require a "commentId".' } };
21809
+ }
21810
+ if (normalized.resolved != null && typeof normalized.resolved !== "boolean") {
21811
+ return { valid: false, error: { code: "INVALID_OPERATION", message: 'Comment resolve "resolved" must be a boolean when provided.' } };
21812
+ }
21813
+ }
21325
21814
  if (normalized.operationKind === "highlight") {
21326
21815
  if (!nonEmptyString(normalized.textToHighlight)) {
21327
21816
  return {
@@ -21801,7 +22290,7 @@ function compileOperationBatch(xmlDoc, operations = [], options = {}) {
21801
22290
  const dynamicOccurrence = operation.targetDescriptor?.occurrence != null && !operation.targetDescriptor?.paragraphId && !operation.targetDescriptor?.fingerprint;
21802
22291
  const compositeTextTarget = typeof operation.targetDescriptor?.text === "string" && /\r|\n/.test(operation.targetDescriptor.text) && !operation.targetEndDescriptor;
21803
22292
  if (dynamicOccurrence || compositeTextTarget) binding.dynamic = true;
21804
- if (validation.valid && operation.operationKind !== "comment_reply" && !operation.targetDescriptor?.captureRef && !binding.dynamic) {
22293
+ if (validation.valid && operation.operationKind !== "comment_reply" && operation.operationKind !== "comment_resolve" && !operation.targetDescriptor?.captureRef && !binding.dynamic) {
21805
22294
  try {
21806
22295
  const start = resolveDescriptor(
21807
22296
  xmlDoc,
@@ -22302,12 +22791,12 @@ function resolveTargetFromCapture(xmlDoc, session, targetDescriptor, _opType = "
22302
22791
  }
22303
22792
 
22304
22793
  // services/document-operation-mutations.js
22305
- var NS_W8 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
22794
+ var NS_W9 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
22306
22795
  var NS_W142 = "http://schemas.microsoft.com/office/word/2010/wordml";
22307
22796
  function getCommentIdsInElement(element) {
22308
22797
  const ids = /* @__PURE__ */ new Set();
22309
22798
  for (const localName2 of ["commentRangeStart", "commentRangeEnd", "commentReference"]) {
22310
- for (const node of Array.from(element?.getElementsByTagNameNS?.(NS_W8, localName2) || [])) {
22799
+ for (const node of Array.from(element?.getElementsByTagNameNS?.(NS_W9, localName2) || [])) {
22311
22800
  const id = node.getAttribute("w:id") || node.getAttribute("id");
22312
22801
  if (id !== "") ids.add(id);
22313
22802
  }
@@ -22521,7 +23010,7 @@ function normalizeBodySectionOrder(xmlDoc) {
22521
23010
  normalizeBodySectionOrderStandalone(xmlDoc);
22522
23011
  }
22523
23012
  function removeProofErrNodes(paragraph) {
22524
- for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W8, "proofErr") || [])) {
23013
+ for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W9, "proofErr") || [])) {
22525
23014
  node.parentNode?.removeChild(node);
22526
23015
  }
22527
23016
  }
@@ -22532,12 +23021,12 @@ function preprocessRedlineTargetParagraph(targetParagraph) {
22532
23021
  function getDirectWordChild4(element, localName2) {
22533
23022
  if (!element) return null;
22534
23023
  return Array.from(element.childNodes || []).find(
22535
- (node) => node && node.nodeType === 1 && node.namespaceURI === NS_W8 && node.localName === localName2
23024
+ (node) => node && node.nodeType === 1 && node.namespaceURI === NS_W9 && node.localName === localName2
22536
23025
  ) || null;
22537
23026
  }
22538
23027
  function computeTableIndexInDocument(xmlDoc, tableElement) {
22539
23028
  if (!xmlDoc || !tableElement) return null;
22540
- const tables = Array.from(xmlDoc.getElementsByTagNameNS(NS_W8, "tbl"));
23029
+ const tables = Array.from(xmlDoc.getElementsByTagNameNS(NS_W9, "tbl"));
22541
23030
  const idx = tables.indexOf(tableElement);
22542
23031
  return idx >= 0 ? idx + 1 : null;
22543
23032
  }
@@ -22587,7 +23076,7 @@ function buildInsertedListParagraph(xmlDoc, anchorParagraph, entry, revisionMeta
22587
23076
  markParagraphMarkInserted(xmlDoc, paragraph, author);
22588
23077
  }
22589
23078
  const run = createWordElement(xmlDoc, "w:r");
22590
- const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W8, "r"))[0] || null;
23079
+ const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W9, "r"))[0] || null;
22591
23080
  const anchorRunPr = anchorFirstRun ? getDirectWordChild4(anchorFirstRun, "rPr") : null;
22592
23081
  if (anchorRunPr) {
22593
23082
  run.appendChild(clonePropertiesWithoutRevisionHistory(anchorRunPr));
@@ -22612,7 +23101,7 @@ function buildInsertedListParagraph(xmlDoc, anchorParagraph, entry, revisionMeta
22612
23101
  }
22613
23102
  function serializeParagraphRangeAsDocument(paragraphs, serializer) {
22614
23103
  const paragraphXml = (paragraphs || []).map((paragraph) => serializer.serializeToString(paragraph)).join("");
22615
- return `<w:document xmlns:w="${NS_W8}"><w:body>${paragraphXml}</w:body></w:document>`;
23104
+ return `<w:document xmlns:w="${NS_W9}"><w:body>${paragraphXml}</w:body></w:document>`;
22616
23105
  }
22617
23106
  function applyExplicitRangeListInsertions({
22618
23107
  xmlDoc,
@@ -22681,7 +23170,7 @@ function buildEmptyParagraphTemplateFromAnchor(xmlDoc, anchorParagraph) {
22681
23170
  const anchorPPr = getDirectWordChild4(anchorParagraph, "pPr");
22682
23171
  if (anchorPPr) paragraph.appendChild(clonePropertiesWithoutRevisionHistory(anchorPPr));
22683
23172
  const run = createWordElement(xmlDoc, "w:r");
22684
- const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W8, "r"))[0] || null;
23173
+ const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W9, "r"))[0] || null;
22685
23174
  const anchorRunPr = anchorFirstRun ? getDirectWordChild4(anchorFirstRun, "rPr") : null;
22686
23175
  if (anchorRunPr) run.appendChild(clonePropertiesWithoutRevisionHistory(anchorRunPr));
22687
23176
  const textNode = createWordElement(xmlDoc, "w:t");
@@ -22704,7 +23193,7 @@ function wrapParagraphContentInInsertion(xmlDoc, paragraph, revisionMetadata, au
22704
23193
  ins.setAttribute("w:author", metadata.author);
22705
23194
  ins.setAttribute("w:date", metadata.date);
22706
23195
  for (const child of Array.from(paragraph.childNodes || [])) {
22707
- if (child?.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName === "pPr") continue;
23196
+ if (child?.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName === "pPr") continue;
22708
23197
  ins.appendChild(child.cloneNode(true));
22709
23198
  }
22710
23199
  wrappedParagraph.appendChild(ins);
@@ -22728,7 +23217,7 @@ async function buildInsertedPlainParagraph(xmlDoc, anchorParagraph, text2, revis
22728
23217
  if (typeof markdownResult?.oxml === "string") {
22729
23218
  const extracted = extractReplacementNodes(markdownResult.oxml);
22730
23219
  sourceParagraph = (extracted.replacementNodes || []).find(
22731
- (node) => node && node.nodeType === 1 && node.namespaceURI === NS_W8 && node.localName === "p"
23220
+ (node) => node && node.nodeType === 1 && node.namespaceURI === NS_W9 && node.localName === "p"
22732
23221
  ) || null;
22733
23222
  }
22734
23223
  if (!sourceParagraph) {
@@ -22760,7 +23249,7 @@ function collectNumberingIdsFromNodes(nodes) {
22760
23249
  }
22761
23250
  function directDeletionCarrierText(carrier) {
22762
23251
  return Array.from(carrier?.childNodes || []).reduce((text2, child) => {
22763
- return text2 + (child?.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName === "r" ? getRunContentPieces(child).map((piece) => piece.text).join("") : "");
23252
+ return text2 + (child?.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName === "r" ? getRunContentPieces(child).map((piece) => piece.text).join("") : "");
22764
23253
  }, "");
22765
23254
  }
22766
23255
  function findOccurrenceOffset(text2, needle, occurrence) {
@@ -22802,9 +23291,9 @@ function hasUnsupportedDeletionSplitMarkup(paragraph, carrier) {
22802
23291
  }
22803
23292
  for (const child of Array.from(carrier?.childNodes || [])) {
22804
23293
  if (child.nodeType !== 1) continue;
22805
- if (!(child.namespaceURI === NS_W8 && child.localName === "r")) return true;
23294
+ if (!(child.namespaceURI === NS_W9 && child.localName === "r")) return true;
22806
23295
  for (const runChild of Array.from(child.childNodes || [])) {
22807
- if (runChild.nodeType !== 1 || runChild.namespaceURI !== NS_W8) continue;
23296
+ if (runChild.nodeType !== 1 || runChild.namespaceURI !== NS_W9) continue;
22808
23297
  if (!["rPr", "delText", "t", "tab", "br", "cr", "noBreakHyphen", "softHyphen"].includes(runChild.localName)) return true;
22809
23298
  }
22810
23299
  }
@@ -22813,7 +23302,7 @@ function hasUnsupportedDeletionSplitMarkup(paragraph, carrier) {
22813
23302
  function insertedRunPropertiesAtDeletionOffset(carrier, localOffset) {
22814
23303
  let offset = 0;
22815
23304
  for (const child of Array.from(carrier?.childNodes || [])) {
22816
- if (!(child?.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName === "r")) continue;
23305
+ if (!(child?.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName === "r")) continue;
22817
23306
  const length = getRunTextLength(getRunContentPieces(child));
22818
23307
  if (localOffset <= offset + length) {
22819
23308
  return clonePropertiesWithoutRevisionHistory(getDirectWordChild4(child, "rPr"));
@@ -22843,7 +23332,7 @@ function normalizedAuthor2(author) {
22843
23332
  function directWordParagraphSibling(paragraph, direction = "nextSibling") {
22844
23333
  let cursor = paragraph?.[direction] || null;
22845
23334
  while (cursor) {
22846
- if (cursor.nodeType === 1 && cursor.namespaceURI === NS_W8 && cursor.localName === "p") return cursor;
23335
+ if (cursor.nodeType === 1 && cursor.namespaceURI === NS_W9 && cursor.localName === "p") return cursor;
22847
23336
  cursor = cursor[direction] || null;
22848
23337
  }
22849
23338
  return null;
@@ -22853,7 +23342,7 @@ function createSanitizedRestorationPPr(xmlDoc, sourceParagraph) {
22853
23342
  const pPr = createWordElement(xmlDoc, "w:pPr");
22854
23343
  if (!sourcePPr) return pPr;
22855
23344
  for (const child of Array.from(sourcePPr.childNodes || [])) {
22856
- if (child.nodeType !== 1 || child.namespaceURI !== NS_W8) continue;
23345
+ if (child.nodeType !== 1 || child.namespaceURI !== NS_W9) continue;
22857
23346
  if (!RESTORATION_PPR_ALLOWLIST.has(child.localName)) continue;
22858
23347
  pPr.appendChild(xmlDoc.importNode(child, true));
22859
23348
  }
@@ -22862,14 +23351,14 @@ function createSanitizedRestorationPPr(xmlDoc, sourceParagraph) {
22862
23351
  function collectRestorationAnchorWarnings(sourceParagraph) {
22863
23352
  const warnings = [];
22864
23353
  const bookmarkNames = /* @__PURE__ */ new Set();
22865
- for (const node of Array.from(sourceParagraph?.getElementsByTagNameNS?.(NS_W8, "bookmarkStart") || [])) {
23354
+ for (const node of Array.from(sourceParagraph?.getElementsByTagNameNS?.(NS_W9, "bookmarkStart") || [])) {
22866
23355
  const name = wordAttribute4(node, "name") || "(unnamed)";
22867
23356
  bookmarkNames.add(name);
22868
23357
  }
22869
23358
  for (const name of bookmarkNames) warnings.push(`RESTORATION_DROPPED_BOOKMARK:${name}`);
22870
23359
  const commentIds = /* @__PURE__ */ new Set();
22871
23360
  for (const localName2 of ["commentRangeStart", "commentRangeEnd", "commentReference"]) {
22872
- for (const node of Array.from(sourceParagraph?.getElementsByTagNameNS?.(NS_W8, localName2) || [])) {
23361
+ for (const node of Array.from(sourceParagraph?.getElementsByTagNameNS?.(NS_W9, localName2) || [])) {
22873
23362
  const id = wordAttribute4(node, "id");
22874
23363
  if (id !== "") commentIds.add(id);
22875
23364
  }
@@ -22886,24 +23375,24 @@ function removeRestorationAnchors(paragraph) {
22886
23375
  "commentReference"
22887
23376
  ];
22888
23377
  for (const localName2 of anchorNames) {
22889
- for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W8, localName2) || [])) {
23378
+ for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W9, localName2) || [])) {
22890
23379
  node.parentNode?.removeChild(node);
22891
23380
  }
22892
23381
  }
22893
- for (const run of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W8, "r") || [])) {
22894
- const meaningful = Array.from(run.childNodes || []).some((child) => child.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName !== "rPr");
23382
+ for (const run of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W9, "r") || [])) {
23383
+ const meaningful = Array.from(run.childNodes || []).some((child) => child.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName !== "rPr");
22895
23384
  if (!meaningful) run.parentNode?.removeChild(run);
22896
23385
  }
22897
23386
  }
22898
23387
  function buildRejectedRestorationTemplate(xmlDoc, sourceParagraph, serializer) {
22899
23388
  const rejected = rejectTrackedChangesInOoxml(serializer.serializeToString(sourceParagraph), { allAuthors: true });
22900
23389
  const parsed = parseOoxmlSafe(rejected.oxml, "application/xml");
22901
- const rejectedParagraph = parsed.doc?.getElementsByTagNameNS?.(NS_W8, "p")?.[0] || null;
23390
+ const rejectedParagraph = parsed.doc?.getElementsByTagNameNS?.(NS_W9, "p")?.[0] || null;
22902
23391
  if (!rejectedParagraph) return null;
22903
23392
  const paragraph = createWordElement(xmlDoc, "w:p");
22904
23393
  paragraph.appendChild(createSanitizedRestorationPPr(xmlDoc, sourceParagraph));
22905
23394
  for (const child of Array.from(rejectedParagraph.childNodes || [])) {
22906
- if (child.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName === "pPr") continue;
23395
+ if (child.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName === "pPr") continue;
22907
23396
  paragraph.appendChild(xmlDoc.importNode(child, true));
22908
23397
  }
22909
23398
  removeRestorationAnchors(paragraph);
@@ -22924,7 +23413,7 @@ async function editRestorationTemplate(xmlDoc, template, modifiedText, author, s
22924
23413
  );
22925
23414
  if (result?.status === "error" || typeof result?.oxml !== "string") return null;
22926
23415
  const extracted = extractReplacementNodes(result.oxml);
22927
- const paragraph = (extracted.replacementNodes || []).find((node) => node?.nodeType === 1 && node.namespaceURI === NS_W8 && node.localName === "p");
23416
+ const paragraph = (extracted.replacementNodes || []).find((node) => node?.nodeType === 1 && node.namespaceURI === NS_W9 && node.localName === "p");
22928
23417
  return paragraph ? xmlDoc.importNode(paragraph, true) : null;
22929
23418
  }
22930
23419
  function allocateFreshParagraphId(xmlDoc, operationSession) {
@@ -23095,7 +23584,7 @@ async function insertIntoRejectedDeletedText(documentXml, targetText, anchor, mo
23095
23584
  }
23096
23585
  const insertionOffset = anchorStart + anchor.offset;
23097
23586
  const carriers = Array.from(paragraph.childNodes || []).filter(
23098
- (node) => node?.nodeType === 1 && node.namespaceURI === NS_W8 && node.localName === "del"
23587
+ (node) => node?.nodeType === 1 && node.namespaceURI === NS_W9 && node.localName === "del"
23099
23588
  );
23100
23589
  if (carriers.some((carrier) => hasUnsupportedDeletionSplitMarkup(paragraph, carrier))) {
23101
23590
  return {
@@ -23752,7 +24241,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
23752
24241
  };
23753
24242
  }
23754
24243
  if (!allSame) {
23755
- const hasMove = targetParagraph.getElementsByTagNameNS(NS_W8, "moveFrom").length > 0 || targetParagraph.getElementsByTagNameNS(NS_W8, "moveTo").length > 0;
24244
+ const hasMove = targetParagraph.getElementsByTagNameNS(NS_W9, "moveFrom").length > 0 || targetParagraph.getElementsByTagNameNS(NS_W9, "moveTo").length > 0;
23756
24245
  if (hasMove) {
23757
24246
  return {
23758
24247
  documentXml,
@@ -23767,8 +24256,8 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
23767
24256
  }
23768
24257
  }
23769
24258
  } else {
23770
- const hasDel = targetParagraph.getElementsByTagNameNS(NS_W8, "del").length > 0;
23771
- const hasMove = targetParagraph.getElementsByTagNameNS(NS_W8, "moveFrom").length > 0 || targetParagraph.getElementsByTagNameNS(NS_W8, "moveTo").length > 0;
24259
+ const hasDel = targetParagraph.getElementsByTagNameNS(NS_W9, "del").length > 0;
24260
+ const hasMove = targetParagraph.getElementsByTagNameNS(NS_W9, "moveFrom").length > 0 || targetParagraph.getElementsByTagNameNS(NS_W9, "moveTo").length > 0;
23772
24261
  if (hasDel) {
23773
24262
  return {
23774
24263
  documentXml,
@@ -24287,7 +24776,7 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
24287
24776
  const targetParagraph = resolved.paragraph;
24288
24777
  const collectVisibleRuns = () => {
24289
24778
  const runEntries2 = [];
24290
- const allRuns = Array.from(targetParagraph.getElementsByTagNameNS(NS_W8, "r"));
24779
+ const allRuns = Array.from(targetParagraph.getElementsByTagNameNS(NS_W9, "r"));
24291
24780
  for (const run of allRuns) {
24292
24781
  let parent = run.parentNode;
24293
24782
  let isDel = false;
@@ -24379,7 +24868,7 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
24379
24868
  const targetRuns = finalRuns.filter((span) => span.start >= matchStart && span.end <= matchEnd);
24380
24869
  let anyChanged = false;
24381
24870
  for (const entry of targetRuns) {
24382
- const rPr = entry.run.getElementsByTagNameNS(NS_W8, "rPr")[0] || null;
24871
+ const rPr = entry.run.getElementsByTagNameNS(NS_W9, "rPr")[0] || null;
24383
24872
  if (checkRunPropertiesChanged(rPr, properties)) {
24384
24873
  anyChanged = true;
24385
24874
  break;
@@ -24390,7 +24879,7 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
24390
24879
  }
24391
24880
  for (const entry of targetRuns) {
24392
24881
  const run = entry.run;
24393
- let rPr = run.getElementsByTagNameNS(NS_W8, "rPr")[0];
24882
+ let rPr = run.getElementsByTagNameNS(NS_W9, "rPr")[0];
24394
24883
  if (!rPr) {
24395
24884
  rPr = createWordElement(xmlDoc, "w:rPr");
24396
24885
  run.insertBefore(rPr, run.firstChild);
@@ -24399,7 +24888,7 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
24399
24888
  let insideSameAuthorIns = false;
24400
24889
  while (insAncestor && insAncestor !== targetParagraph) {
24401
24890
  if (insAncestor.nodeType === 1 && (insAncestor.localName === "ins" || insAncestor.nodeName === "w:ins")) {
24402
- const insAuthor = insAncestor.getAttribute("w:author") || insAncestor.getAttributeNS(NS_W8, "author");
24891
+ const insAuthor = insAncestor.getAttribute("w:author") || insAncestor.getAttributeNS(NS_W9, "author");
24403
24892
  if (insAuthor === (author || getDefaultAuthor())) {
24404
24893
  insideSameAuthorIns = true;
24405
24894
  }
@@ -24447,7 +24936,7 @@ async function applyParagraphFormatToParagraphByExactText(documentXml, targetTex
24447
24936
  return { documentXml, hasChanges: false, status: "error", error: resolved?.error || { code: "TARGET_NOT_FOUND", message: "Target paragraph not found." } };
24448
24937
  }
24449
24938
  const targetParagraph = resolved.paragraph;
24450
- let pPr = targetParagraph.getElementsByTagNameNS(NS_W8, "pPr")[0];
24939
+ let pPr = targetParagraph.getElementsByTagNameNS(NS_W9, "pPr")[0];
24451
24940
  if (!pPr) {
24452
24941
  pPr = createWordElement(xmlDoc, "w:pPr");
24453
24942
  targetParagraph.insertBefore(pPr, targetParagraph.firstChild);
@@ -24495,7 +24984,7 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
24495
24984
  }
24496
24985
  let operation = validation.operation || normalizeDocumentOperation(op);
24497
24986
  const authorUsed = resolveDocumentOperationAuthor(operation, author, getDefaultAuthor());
24498
- if (operation.operationKind !== "comment_reply" && operation.operationKind !== "rejected-insert" && operation.operationKind !== "restore" && (operation.targetDescriptor?.revisionView === "rejected" || operation.targetEndDescriptor?.revisionView === "rejected")) {
24987
+ if (operation.operationKind !== "comment_reply" && operation.operationKind !== "comment_resolve" && operation.operationKind !== "rejected-insert" && operation.operationKind !== "restore" && (operation.targetDescriptor?.revisionView === "rejected" || operation.targetEndDescriptor?.revisionView === "rejected")) {
24499
24988
  return {
24500
24989
  documentXml,
24501
24990
  hasChanges: false,
@@ -24662,6 +25151,7 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
24662
25151
  result = applyCommentReplyToParts({
24663
25152
  commentsXml: existingCommentsXml,
24664
25153
  commentsExtendedXml: existingExtendedXml,
25154
+ documentXml,
24665
25155
  parentCommentId: operation.parentCommentId,
24666
25156
  commentId,
24667
25157
  commentContent: operation.commentContent,
@@ -24669,8 +25159,30 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
24669
25159
  date: operation.date || (/* @__PURE__ */ new Date()).toISOString()
24670
25160
  });
24671
25161
  result.documentXml = documentXml;
24672
- if (result.hasChanges && session.receiptCollector) session.receiptCollector.recordComment(commentId);
25162
+ if (result.status !== "error" && result.hasChanges) {
25163
+ if (!session.document || !anchorReplyInDocument(session.document, result.threadCommentIds, commentId)) {
25164
+ 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.` } };
25165
+ } else if (session.receiptCollector) {
25166
+ session.receiptCollector.recordComment(commentId);
25167
+ }
25168
+ }
24673
25169
  }
25170
+ } else if (operation.operationKind === "comment_resolve") {
25171
+ const resolution = applyThreadResolutionToParts({
25172
+ commentsXml: session.commentsXml || runtimeContext?.commentsXml || options.commentsXml,
25173
+ commentsExtendedXml: session.commentsExtendedXml || runtimeContext?.commentsExtendedXml || options.commentsExtendedXml,
25174
+ commentId: operation.commentId,
25175
+ resolved: operation.resolved !== false
25176
+ });
25177
+ result = resolution.status === "error" ? { documentXml, hasChanges: false, status: "error", error: resolution.error } : {
25178
+ documentXml,
25179
+ hasChanges: resolution.hasChanges,
25180
+ ...resolution.commentsXml ? { commentsXml: resolution.commentsXml, commentsXmlMode: "replace" } : {},
25181
+ ...resolution.commentsExtendedXml ? { commentsExtendedXml: resolution.commentsExtendedXml, commentsExtendedXmlMode: "replace" } : {},
25182
+ resolved: resolution.resolved,
25183
+ threadRootId: resolution.threadRootId,
25184
+ commentIds: resolution.commentIds
25185
+ };
24674
25186
  } else if (operation.operationKind === "highlight") {
24675
25187
  result = await applyHighlightToParagraphByExactText(
24676
25188
  documentXml,
@@ -24811,7 +25323,7 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
24811
25323
  ...resolutionCapture
24812
25324
  };
24813
25325
  }
24814
- session.markMutationCommitted(operation.operationKind !== "comment_reply");
25326
+ session.markMutationCommitted(true);
24815
25327
  if (operation._compiledSourceId && session.sourceTargetRegistry) {
24816
25328
  session.sourceTargetRegistry.commitMutation(
24817
25329
  operation._compiledSourceId,
@@ -24924,9 +25436,9 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
24924
25436
  }
24925
25437
 
24926
25438
  // services/batch-operation-orchestrator.js
24927
- var NS_W9 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
25439
+ var NS_W10 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
24928
25440
  function operationTargetPriority(op) {
24929
- return op?.type === "comment" || op?.type === "comment_reply" ? 0 : 1;
25441
+ return op?.type === "comment" || op?.type === "comment_reply" || op?.type === "comment_resolve" ? 0 : 1;
24930
25442
  }
24931
25443
  function buildOperationDependencyPlan(operations = []) {
24932
25444
  const list = Array.isArray(operations) ? operations : [];
@@ -25001,7 +25513,7 @@ function buildOperationDependencyPlan(operations = []) {
25001
25513
  inDegrees[i2]++;
25002
25514
  }
25003
25515
  const kind = normalizeDocumentOperation(op).operationKind;
25004
- if (kind !== "comment" && kind !== "comment_reply") {
25516
+ if (kind !== "comment" && kind !== "comment_reply" && kind !== "comment_resolve") {
25005
25517
  if (!mutatingCaptureConsumers.has(ref)) mutatingCaptureConsumers.set(ref, []);
25006
25518
  mutatingCaptureConsumers.get(ref).push({
25007
25519
  index: i2,
@@ -25069,9 +25581,9 @@ function mergeCommentsXml(existingXml, incomingXml) {
25069
25581
  const serializer = createSerializer();
25070
25582
  const existingRoot = existingDoc.documentElement;
25071
25583
  const existingIds = new Set(
25072
- Array.from(existingDoc.getElementsByTagNameNS(NS_W9, "comment")).map((node) => node.getAttribute("w:id") || node.getAttribute("id")).filter(Boolean)
25584
+ Array.from(existingDoc.getElementsByTagNameNS(NS_W10, "comment")).map((node) => node.getAttribute("w:id") || node.getAttribute("id")).filter(Boolean)
25073
25585
  );
25074
- for (const comment of Array.from(incomingDoc.getElementsByTagNameNS(NS_W9, "comment"))) {
25586
+ for (const comment of Array.from(incomingDoc.getElementsByTagNameNS(NS_W10, "comment"))) {
25075
25587
  const id = comment.getAttribute("w:id") || comment.getAttribute("id");
25076
25588
  if (id && existingIds.has(id)) continue;
25077
25589
  existingRoot.appendChild(existingDoc.importNode(comment, true));
@@ -25232,7 +25744,7 @@ async function applyOperationsToDocumentXml(documentXml, operations, author, run
25232
25744
  context.targetRefSnapshot = session.initialTargetReferenceSnapshot;
25233
25745
  }
25234
25746
  session.runtimeContext = context;
25235
- if (sourceOperations.some((op) => op?.type === "comment_reply")) {
25747
+ if (sourceOperations.some((op) => op?.type === "comment_reply" || op?.type === "comment_resolve")) {
25236
25748
  session.commentsXml = context.commentsXml || options.commentsXml || null;
25237
25749
  session.commentsExtendedXml = context.commentsExtendedXml || options.commentsExtendedXml || null;
25238
25750
  session.commentsXmlMode = "replace";
@@ -25305,6 +25817,7 @@ async function applyOperationsToDocumentXml(documentXml, operations, author, run
25305
25817
  ...result.resolvedTarget ? { resolvedTarget: result.resolvedTarget } : {},
25306
25818
  ...result.resolvedAnchor ? { resolvedAnchor: result.resolvedAnchor } : {},
25307
25819
  ...result.change ? { change: result.change } : {},
25820
+ ...result.operationType === "comment_resolve" && !isError ? { resolved: result.resolved, threadRootId: result.threadRootId, commentIds: result.commentIds } : {},
25308
25821
  ...Array.isArray(result.warnings) && result.warnings.length > 0 ? { warnings: result.warnings } : {},
25309
25822
  ...result.error ? { error: result.error } : {},
25310
25823
  ...result.receipt ? { receipt: result.receipt } : {}
@@ -25581,6 +26094,26 @@ function preflightOperations(documentXml, operations, author, options = {}) {
25581
26094
  }
25582
26095
  commentsRequired = commentsRequired || operation.operationKind === "comment" || operation.operationKind === "comment_reply";
25583
26096
  numberingRequired = numberingRequired || operationNeedsNumbering(operation);
26097
+ if (operation.operationKind === "comment_resolve") {
26098
+ const commentId = String(operation.commentId);
26099
+ results.push(options._existingCommentDetails?.[commentId] ? {
26100
+ index: index + 1,
26101
+ type: sourceOperation.type,
26102
+ operationType: operation.operationKind,
26103
+ status: "ready",
26104
+ authorUsed,
26105
+ resolvedBy: "comment_id",
26106
+ commentId
26107
+ } : {
26108
+ index: index + 1,
26109
+ type: sourceOperation.type,
26110
+ operationType: operation.operationKind,
26111
+ status: "error",
26112
+ authorUsed,
26113
+ error: { code: "COMMENT_NOT_FOUND", message: `Comment '${commentId}' was not found.` }
26114
+ });
26115
+ continue;
26116
+ }
25584
26117
  if (operation.operationKind === "comment_reply") {
25585
26118
  const parentId = String(operation.parentCommentId);
25586
26119
  const parent = options._existingCommentDetails?.[parentId];
@@ -26732,6 +27265,15 @@ var text = (entries, path) => {
26732
27265
  if (typeof entry === "string") return entry;
26733
27266
  return textDecoder2.decode(entry);
26734
27267
  };
27268
+ function reconcileCommentSiblings(working) {
27269
+ const updated = reconcileCommentSiblingParts({
27270
+ commentsXml: text(working, "word/comments.xml"),
27271
+ commentsIdsXml: text(working, "word/commentsIds.xml"),
27272
+ commentsExtensibleXml: text(working, "word/commentsExtensible.xml")
27273
+ });
27274
+ if (updated.commentsIdsXml) working.set("word/commentsIds.xml", textEncoder4.encode(updated.commentsIdsXml));
27275
+ if (updated.commentsExtensibleXml) working.set("word/commentsExtensible.xml", textEncoder4.encode(updated.commentsExtensibleXml));
27276
+ }
26735
27277
  var cloneEntries = (entries) => new Map([...entries].map(([name, data]) => [name, new Uint8Array(data)]));
26736
27278
  function toBufferCompatible(uint8Arr) {
26737
27279
  if (typeof Buffer !== "undefined" && typeof Buffer.from === "function") {
@@ -26799,6 +27341,7 @@ function computePackageRevisionToken(input) {
26799
27341
  entries
26800
27342
  });
26801
27343
  }
27344
+ var COMMENT_OPERATION_TYPES = /* @__PURE__ */ new Set(["comment", "comment_reply", "comment_resolve"]);
26802
27345
  function nextCommentId(entries) {
26803
27346
  const ids = `${text(entries, "word/document.xml") || ""} ${text(entries, "word/comments.xml") || ""}`.match(/(?:w:)?id=["'](\d+)["']/g) || [];
26804
27347
  let next = ids.reduce((max2, token) => Math.max(max2, Number(token.match(/\d+/)?.[0] || 0)), 0) + 1;
@@ -26817,7 +27360,7 @@ function existingCommentDetails(entries) {
26817
27360
  id,
26818
27361
  author: comment.getAttribute("w:author") || comment.getAttribute("author") || "",
26819
27362
  text: String(comment.textContent || "").trim(),
26820
- paraId: Array.from(comment.getElementsByTagNameNS("*", "p"))[0]?.getAttribute("w14:paraId") || null
27363
+ paraId: commentParaId(comment) || null
26821
27364
  };
26822
27365
  }
26823
27366
  return details;
@@ -26848,13 +27391,34 @@ var DocxDocument = class {
26848
27391
  }
26849
27392
  inspect(options = {}) {
26850
27393
  const digestFn = options.digestFn || ((bytes) => sha256(bytes));
26851
- return inspectDocumentParts({
27394
+ const inspection = inspectDocumentParts({
26852
27395
  documentXml: text(this.entries, "word/document.xml"),
26853
27396
  commentsXml: text(this.entries, "word/comments.xml"),
26854
27397
  commentsExtendedXml: text(this.entries, "word/commentsExtended.xml"),
26855
27398
  numberingXml: text(this.entries, "word/numbering.xml"),
26856
27399
  stylesXml: text(this.entries, "word/styles.xml")
26857
27400
  }, { ...options, digestFn });
27401
+ if (inspection.status === "error") return inspection;
27402
+ return { ...inspection, headersFooters: this.inspectHeadersFooters() };
27403
+ }
27404
+ /**
27405
+ * Header and footer parts the document uses, with their paragraph text, so operations can target them with
27406
+ * `part`. `hasFields` marks parts containing PAGE/NUMPAGES-style fields (edit around them, not inside).
27407
+ */
27408
+ inspectHeadersFooters() {
27409
+ return discoverHeaderFooterParts({
27410
+ documentXml: text(this.entries, "word/document.xml"),
27411
+ relsXml: text(this.entries, "word/_rels/document.xml.rels"),
27412
+ settingsXml: text(this.entries, "word/settings.xml")
27413
+ }).map((part) => {
27414
+ const xml = text(this.entries, part.path) || "";
27415
+ const partInspection = xml ? inspectDocumentParts({ documentXml: xml }) : { paragraphs: [] };
27416
+ return {
27417
+ ...part,
27418
+ hasFields: /<w:fldChar\b|<w:fldSimple\b/.test(xml),
27419
+ paragraphs: (partInspection.paragraphs || []).map((paragraph) => ({ index: paragraph.index, text: paragraph.text }))
27420
+ };
27421
+ });
26858
27422
  }
26859
27423
  getRevisionToken() {
26860
27424
  return computePackageRevisionToken(this.entries);
@@ -26863,12 +27427,51 @@ var DocxDocument = class {
26863
27427
  return this.getRevisionToken();
26864
27428
  }
26865
27429
  preflight(operations, author = getDefaultAuthor(), options = {}) {
26866
- return preflightOperations(
27430
+ const list = Array.isArray(operations) ? operations : [];
27431
+ const runBody = (ops) => preflightOperations(
26867
27432
  text(this.entries, "word/document.xml"),
26868
- operations,
27433
+ ops,
26869
27434
  author || getDefaultAuthor(),
26870
27435
  { ...options, _existingCommentDetails: existingCommentDetails(this.entries) }
26871
27436
  );
27437
+ if (!list.some((operation) => operation && typeof operation === "object" && operation.part != null)) return runBody(operations);
27438
+ const parts = this.inspectHeadersFooters();
27439
+ const bodyOps = [];
27440
+ const results = [];
27441
+ const conflicts = [];
27442
+ const authorsUsed = /* @__PURE__ */ new Set();
27443
+ let valid = true;
27444
+ list.forEach((operation, i2) => {
27445
+ if (!operation || typeof operation !== "object" || operation.part == null) {
27446
+ bodyOps.push({ operation, origIndex: i2 });
27447
+ return;
27448
+ }
27449
+ const resolved = resolvePartSelector(parts, operation.part);
27450
+ 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;
27451
+ if (refusal) {
27452
+ valid = false;
27453
+ results.push({ index: i2 + 1, type: operation.type || "redline", status: "error", error: normalizeErrorWithRecovery({ ...refusal, operationIndex: i2 + 1 }) });
27454
+ return;
27455
+ }
27456
+ const { part: _selector, ...rest } = operation;
27457
+ const partResult = preflightOperations(text(this.entries, resolved.part.path), [rest], author || getDefaultAuthor(), { ...options, _existingCommentDetails: {} });
27458
+ valid = valid && partResult.valid;
27459
+ for (const result of partResult.results || []) results.push({ ...result, index: i2 + 1, part: resolved.part.path });
27460
+ conflicts.push(...partResult.conflicts || []);
27461
+ for (const name of partResult.authorsUsed || []) authorsUsed.add(name);
27462
+ });
27463
+ let requiredArtifacts = { comments: false, numbering: false };
27464
+ if (bodyOps.length) {
27465
+ const body = runBody(bodyOps.map((item) => item.operation));
27466
+ valid = valid && body.valid;
27467
+ for (const result of body.results || []) results.push({ ...result, index: bodyOps[result.index - 1].origIndex + 1 });
27468
+ conflicts.push(...body.conflicts || []);
27469
+ for (const name of body.authorsUsed || []) authorsUsed.add(name);
27470
+ requiredArtifacts = body.requiredArtifacts || requiredArtifacts;
27471
+ }
27472
+ results.sort((a, b) => a.index - b.index);
27473
+ const ok = valid && conflicts.length === 0;
27474
+ return { valid: ok, status: ok ? "ok" : "error", results, conflicts, authorsUsed: [...authorsUsed], requiredArtifacts };
26872
27475
  }
26873
27476
  toUint8Array() {
26874
27477
  return zipDocx(this.entries);
@@ -26876,7 +27479,240 @@ var DocxDocument = class {
26876
27479
  toBuffer() {
26877
27480
  return toBufferCompatible(this.toUint8Array());
26878
27481
  }
27482
+ /**
27483
+ * Applies operations to the document. Operations without `part` target the body (word/document.xml).
27484
+ * An operation with `part` targets one header or footer part:
27485
+ * `{ part: { kind: 'footer', type: 'default', section: 0 } | 'word/footer2.xml', target: { exactText }, ... }`.
27486
+ * Discover parts with `inspect().headersFooters`.
27487
+ */
26879
27488
  async applyOperations(operations, options = {}) {
27489
+ const list = Array.isArray(operations) ? operations : [];
27490
+ if (!list.some((operation) => operation && typeof operation === "object" && operation.part != null)) {
27491
+ return this.applyBodyOperations(operations, options);
27492
+ }
27493
+ return this.applyOperationsWithParts(list, options);
27494
+ }
27495
+ async applyOperationsWithParts(operations, options = {}) {
27496
+ const author = options.author || getDefaultAuthor();
27497
+ const atomic = options.atomic === true;
27498
+ const snapshotEntries = this.entries;
27499
+ const snapshotBytes = this.originalBytes;
27500
+ const total = operations.length;
27501
+ const restore = () => {
27502
+ this.entries = snapshotEntries;
27503
+ this.originalBytes = snapshotBytes;
27504
+ };
27505
+ const original = () => ({
27506
+ uint8Array: snapshotBytes,
27507
+ toUint8Array: () => new Uint8Array(snapshotBytes),
27508
+ buffer: toBufferCompatible(snapshotBytes),
27509
+ toBuffer: () => toBufferCompatible(snapshotBytes)
27510
+ });
27511
+ const refuse = (error2, results2 = [], receipts2 = [], extra = {}) => ({
27512
+ status: "error",
27513
+ hasChanges: false,
27514
+ written: false,
27515
+ rolledBack: true,
27516
+ results: results2,
27517
+ receipts: receipts2,
27518
+ executionOrder: [],
27519
+ authorsUsed: [],
27520
+ artifactsChanged: [],
27521
+ error: normalizeErrorWithRecovery(error2),
27522
+ retryPlan: createRetryPlan({ atomic: true, rolledBack: true, results: results2, receipts: receipts2, operationCount: total }),
27523
+ validation: { originalIssues: [], generatedIssues: [] },
27524
+ ...extra,
27525
+ ...original()
27526
+ });
27527
+ if (options.expectedRevision) {
27528
+ const precondition = await this.applyBodyOperations([], { expectedRevision: options.expectedRevision });
27529
+ if (precondition.status === "error") return precondition;
27530
+ }
27531
+ const parts = discoverHeaderFooterParts({
27532
+ documentXml: text(this.entries, "word/document.xml"),
27533
+ relsXml: text(this.entries, "word/_rels/document.xml.rels"),
27534
+ settingsXml: text(this.entries, "word/settings.xml")
27535
+ });
27536
+ const bodyOps = [];
27537
+ const partOps = [];
27538
+ for (let i2 = 0; i2 < operations.length; i2++) {
27539
+ const operation = operations[i2];
27540
+ if (!operation || typeof operation !== "object" || operation.part == null) {
27541
+ bodyOps.push({ operation, origIndex: i2 });
27542
+ continue;
27543
+ }
27544
+ if (COMMENT_OPERATION_TYPES.has(operation.type)) {
27545
+ return refuse({ code: "COMMENT_IN_HEADER_FOOTER", message: `Operation ${i2 + 1}: Word does not support comments in headers or footers.`, operationIndex: i2 + 1 });
27546
+ }
27547
+ const resolved = resolvePartSelector(parts, operation.part);
27548
+ if (resolved.error) return refuse({ ...resolved.error, message: `Operation ${i2 + 1}: ${resolved.error.message}`, operationIndex: i2 + 1 });
27549
+ const { part: _selector, ...rest } = operation;
27550
+ partOps.push({ operation: rest, origIndex: i2, part: resolved.part });
27551
+ }
27552
+ const results = [];
27553
+ const receipts = [];
27554
+ const executionOrder = [];
27555
+ const authorsUsed = /* @__PURE__ */ new Set();
27556
+ const originalIssues = [];
27557
+ let anyChange = false;
27558
+ let anyFailure = false;
27559
+ let bodyResult = null;
27560
+ if (bodyOps.length) {
27561
+ bodyResult = await this.applyBodyOperations(bodyOps.map((item) => item.operation), { ...options, expectedRevision: void 0 });
27562
+ const toOriginal = (index) => bodyOps[index - 1]?.origIndex + 1;
27563
+ for (const result of bodyResult.results || []) {
27564
+ results.push({
27565
+ ...result,
27566
+ index: toOriginal(result.index),
27567
+ ...result.receipt ? { receipt: { ...result.receipt, operationIndex: toOriginal(result.receipt.operationIndex) } } : {}
27568
+ });
27569
+ }
27570
+ for (const receipt of bodyResult.receipts || []) receipts.push({ ...receipt, operationIndex: toOriginal(receipt.operationIndex) });
27571
+ for (const index of bodyResult.executionOrder || []) executionOrder.push(toOriginal(index));
27572
+ for (const name of bodyResult.authorsUsed || []) authorsUsed.add(name);
27573
+ originalIssues.push(...bodyResult.validation?.originalIssues || []);
27574
+ if (bodyResult.status === "error" || bodyResult.rolledBack) {
27575
+ anyFailure = true;
27576
+ if (atomic) {
27577
+ restore();
27578
+ return { ...bodyResult, results, receipts, executionOrder, written: false, artifactsChanged: [], rolledBack: true, ...original() };
27579
+ }
27580
+ }
27581
+ anyChange = bodyResult.hasChanges === true;
27582
+ }
27583
+ const working = cloneEntries(this.entries);
27584
+ const zip = new MemoryZip(working);
27585
+ const changedParts = /* @__PURE__ */ new Set();
27586
+ const { expectedRevision: _ignored, ...runnerOptions } = options;
27587
+ const failPart = (item, error2) => {
27588
+ anyFailure = true;
27589
+ const failure = normalizeErrorWithRecovery({ ...error2, operationIndex: item.origIndex + 1 });
27590
+ results.push({ index: item.origIndex + 1, type: item.operation.type || "redline", status: "error", authorUsed: author, part: item.part.path, error: failure });
27591
+ };
27592
+ for (const item of partOps) {
27593
+ const path = item.part.path;
27594
+ const xml = text(working, path);
27595
+ if (!xml) {
27596
+ failPart(item, { code: "PART_NOT_FOUND", message: `Part '${path}' is referenced but missing from the package.` });
27597
+ if (atomic) break;
27598
+ continue;
27599
+ }
27600
+ const numberingText = text(working, "word/numbering.xml");
27601
+ const outcome = await applyOperationsToDocumentXml(xml, [item.operation], author, { numberingIdState: createDynamicNumberingIdState(numberingText || void 0) }, {
27602
+ ...runnerOptions,
27603
+ atomic: true,
27604
+ strictTargets: options.strictTargets !== false,
27605
+ _existingCommentDetails: {}
27606
+ });
27607
+ const single = outcome.results?.[0];
27608
+ if (outcome.rolledBack || outcome.status === "error") {
27609
+ failPart(item, single?.error || outcome.error || { code: "OPERATION_FAILED", message: "Operation failed." });
27610
+ if (atomic) break;
27611
+ continue;
27612
+ }
27613
+ if (!outcome.hasChanges) {
27614
+ results.push({ ...single, index: item.origIndex + 1, part: path });
27615
+ continue;
27616
+ }
27617
+ if (countRevisionsInsideFields(outcome.documentXml) > countRevisionsInsideFields(xml)) {
27618
+ 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.` });
27619
+ if (atomic) break;
27620
+ continue;
27621
+ }
27622
+ const taken = /* @__PURE__ */ new Set();
27623
+ for (const [name, data] of working) {
27624
+ if (name === path || !(name === "word/document.xml" || isHeaderFooterPath(name))) continue;
27625
+ for (const id of collectRevisionIds(textDecoder2.decode(data))) taken.add(id);
27626
+ }
27627
+ const renumbered = renumberCollidingRevisionIds(outcome.documentXml, taken);
27628
+ working.set(path, textEncoder4.encode(renumbered.xml));
27629
+ const describeReceipt = (receipt) => receipt?.revisionItems ? { ...receipt, revisionItems: receipt.revisionItems.map((revision) => ({ ...revision, id: renumbered.idMap.get(String(revision.id)) ?? revision.id, partName: path })) } : receipt;
27630
+ if (outcome.numberingXmlParts?.length) {
27631
+ await ensureNumberingArtifactsInZip(zip, outcome.numberingXmlParts, { mergeNumberingXml: mergeNumberingXmlBySchemaOrder });
27632
+ }
27633
+ changedParts.add(path);
27634
+ anyChange = true;
27635
+ results.push({
27636
+ ...single,
27637
+ index: item.origIndex + 1,
27638
+ part: path,
27639
+ partSections: item.part.appliesToSections,
27640
+ ...single?.receipt ? { receipt: { ...describeReceipt(single.receipt), operationIndex: item.origIndex + 1 } } : {}
27641
+ });
27642
+ if (outcome.receipts?.[0]) receipts.push({ ...describeReceipt(outcome.receipts[0]), operationIndex: item.origIndex + 1 });
27643
+ executionOrder.push(item.origIndex + 1);
27644
+ for (const name of outcome.authorsUsed || []) authorsUsed.add(name);
27645
+ }
27646
+ const generatedIssues = [];
27647
+ if (!(atomic && anyFailure)) {
27648
+ for (const path of changedParts) {
27649
+ const before = validateRedlineOoxml(text(this.entries, path)).issues.map((issue) => ({ source: path, ...issue }));
27650
+ const after = validateRedlineOoxml(text(working, path)).issues.map((issue) => ({ source: path, ...issue }));
27651
+ generatedIssues.push(...validationErrors(subtractValidationIssueMultiset(after, before)));
27652
+ }
27653
+ if (options.validate !== false && changedParts.size) {
27654
+ try {
27655
+ await validateDocxPackage(zip);
27656
+ } catch (error2) {
27657
+ generatedIssues.push({ source: "package", code: "PACKAGE_VALIDATION", severity: "error", message: error2.message });
27658
+ }
27659
+ }
27660
+ }
27661
+ if (atomic && anyFailure || generatedIssues.length) {
27662
+ restore();
27663
+ 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." };
27664
+ const rolledBackResults = results.map((result) => result.receipt ? { ...result, receipt: { ...result.receipt, committed: false, finalDisposition: "rolled_back" } } : result).sort((a, b) => a.index - b.index);
27665
+ const rolledBackReceipts = receipts.map((receipt) => ({ ...receipt, committed: false, finalDisposition: "rolled_back" }));
27666
+ return refuse(error2, rolledBackResults, rolledBackReceipts, { validation: { originalIssues, generatedIssues }, issues: generatedIssues });
27667
+ }
27668
+ results.sort((a, b) => a.index - b.index);
27669
+ receipts.sort((a, b) => a.operationIndex - b.operationIndex);
27670
+ const status = !anyFailure ? "ok" : anyChange ? "partial" : "error";
27671
+ const failureError = anyFailure ? { error: normalizeErrorWithRecovery({ code: "BATCH_OPERATION_FAILED", message: "One or more operations failed; the others were applied." }) } : {};
27672
+ if (!changedParts.size) {
27673
+ const buf = toBufferCompatible(this.originalBytes);
27674
+ return {
27675
+ ...bodyResult || {},
27676
+ results,
27677
+ receipts,
27678
+ executionOrder,
27679
+ authorsUsed: [...authorsUsed],
27680
+ status,
27681
+ hasChanges: anyChange,
27682
+ written: bodyResult?.written === true,
27683
+ artifactsChanged: bodyResult?.artifactsChanged || [],
27684
+ validation: { originalIssues, generatedIssues: [] },
27685
+ ...failureError,
27686
+ uint8Array: this.originalBytes,
27687
+ toUint8Array: () => new Uint8Array(this.originalBytes),
27688
+ buffer: buf,
27689
+ toBuffer: () => toBufferCompatible(this.originalBytes)
27690
+ };
27691
+ }
27692
+ this.entries = working;
27693
+ const outputBytes = this.toUint8Array();
27694
+ this.originalBytes = outputBytes;
27695
+ const artifactsChanged = [...working].filter(([name, data]) => !snapshotEntries.has(name) || !areByteArraysEqual(data, snapshotEntries.get(name))).map(([name]) => name);
27696
+ return {
27697
+ ...bodyResult || {},
27698
+ status,
27699
+ hasChanges: true,
27700
+ written: true,
27701
+ results,
27702
+ receipts,
27703
+ executionOrder,
27704
+ authorsUsed: [...authorsUsed],
27705
+ artifactsChanged,
27706
+ validation: { originalIssues, generatedIssues: [] },
27707
+ ...failureError,
27708
+ uint8Array: outputBytes,
27709
+ toUint8Array: () => new Uint8Array(outputBytes),
27710
+ buffer: toBufferCompatible(outputBytes),
27711
+ inspection: this.inspect(),
27712
+ toBuffer: () => toBufferCompatible(outputBytes)
27713
+ };
27714
+ }
27715
+ async applyBodyOperations(operations, options = {}) {
26880
27716
  const failedApply = (error2) => {
26881
27717
  const results = [];
26882
27718
  const receipts = [];
@@ -26987,18 +27823,21 @@ var DocxDocument = class {
26987
27823
  toBuffer: () => toBufferCompatible(this.originalBytes)
26988
27824
  };
26989
27825
  }
26990
- working.set("word/document.xml", textEncoder4.encode(result.documentXml));
27826
+ const bodyChanged = (result.results || []).some((item) => item?.status !== "error" && item?.operationType !== "comment_resolve");
27827
+ if (bodyChanged) working.set("word/document.xml", textEncoder4.encode(result.documentXml));
26991
27828
  await ensureNumberingArtifactsInZip(zip, result.numberingXmlParts, { mergeNumberingXml: mergeNumberingXmlBySchemaOrder });
26992
27829
  const existingCommentsXml = text(working, "word/comments.xml");
26993
27830
  const commentsXmlForPackaging = result.commentsXml || existingCommentsXml;
26994
27831
  await ensureCommentsArtifactsInZip(zip, commentsXmlForPackaging, {
26995
27832
  replaceExisting: result.commentsXmlMode === "replace" || !result.commentsXml && !!existingCommentsXml
26996
27833
  });
26997
- const existingCommentsExtendedXml = text(working, "word/commentsExtended.xml");
26998
- const commentsExtendedXmlForPackaging = result.commentsExtendedXml || existingCommentsExtendedXml;
26999
- await ensureCommentsExtendedArtifactsInZip(zip, commentsExtendedXmlForPackaging, {
27000
- replaceExisting: result.commentsExtendedXmlMode === "replace" || !result.commentsExtendedXml && !!existingCommentsExtendedXml
27001
- });
27834
+ if (result.commentsExtendedXml) {
27835
+ await ensureCommentsExtendedArtifactsInZip(zip, result.commentsExtendedXml, {
27836
+ replaceExisting: result.commentsExtendedXmlMode === "replace"
27837
+ });
27838
+ }
27839
+ await repairKnownContentTypes(zip);
27840
+ reconcileCommentSiblings(working);
27002
27841
  if (options.validate !== false) {
27003
27842
  const generated = validateRedlineOoxml(result.documentXml);
27004
27843
  const outputIssues = generated.issues.map((issue) => ({ source: "word/document.xml", ...issue }));
@@ -27067,13 +27906,25 @@ var DocxDocument = class {
27067
27906
  const transform = action === "accept" ? acceptTrackedChangesInOoxml : action === "reject" ? rejectTrackedChangesInOoxml : null;
27068
27907
  if (!transform) return packageFailure(this.originalBytes, "INVALID_ACTION", `Unknown revision action: ${action}`);
27069
27908
  const sourceBytes = this.originalBytes;
27909
+ const originalEntries = this.entries;
27070
27910
  const working = cloneEntries(this.entries);
27071
27911
  const zip = new MemoryZip(working);
27072
27912
  const result = transform(text(working, "word/document.xml"), { author: options.author, allAuthors: options.allAuthors === true });
27073
27913
  if (result.status === "error" || result.error) {
27074
27914
  return packageFailure(sourceBytes, result.error?.code || "REVISION_OPERATION_FAILED", result.error?.message || "Revision operation failed.");
27075
27915
  }
27076
- if (!result.hasChanges) {
27916
+ const partUpdates = /* @__PURE__ */ new Map();
27917
+ for (const part of this.inspectHeadersFooters()) {
27918
+ const partXml = text(working, part.path);
27919
+ if (!partXml) continue;
27920
+ const partResult = transform(partXml, { author: options.author, allAuthors: options.allAuthors === true });
27921
+ if (partResult.status === "error" || partResult.error) {
27922
+ return packageFailure(sourceBytes, partResult.error?.code || "REVISION_OPERATION_FAILED", `${part.path}: ${partResult.error?.message || "Revision operation failed."}`);
27923
+ }
27924
+ if (partResult.hasChanges) partUpdates.set(part.path, partResult.oxml);
27925
+ }
27926
+ const hasChanges = result.hasChanges || partUpdates.size > 0;
27927
+ if (!hasChanges) {
27077
27928
  const buf = toBufferCompatible(sourceBytes);
27078
27929
  return {
27079
27930
  ...result,
@@ -27086,8 +27937,10 @@ var DocxDocument = class {
27086
27937
  toBuffer: () => toBufferCompatible(sourceBytes)
27087
27938
  };
27088
27939
  }
27089
- working.set("word/document.xml", textEncoder4.encode(result.oxml));
27940
+ if (result.hasChanges) working.set("word/document.xml", textEncoder4.encode(result.oxml));
27941
+ for (const [path, xml] of partUpdates) working.set(path, textEncoder4.encode(xml));
27090
27942
  try {
27943
+ await repairKnownContentTypes(zip);
27091
27944
  if (options.validate !== false) await validateDocxPackage(zip);
27092
27945
  } catch (error2) {
27093
27946
  return packageFailure(sourceBytes, "PACKAGE_VALIDATION", error2.message);
@@ -27100,13 +27953,71 @@ var DocxDocument = class {
27100
27953
  ...result,
27101
27954
  status: "ok",
27102
27955
  written: true,
27103
- artifactsChanged: ["word/document.xml"],
27956
+ hasChanges: true,
27957
+ artifactsChanged: [...working].filter(([name, data]) => !originalEntries.has(name) || !areByteArraysEqual(data, originalEntries.get(name))).map(([name]) => name),
27104
27958
  uint8Array: outputBytes,
27105
27959
  toUint8Array: () => new Uint8Array(outputBytes),
27106
27960
  buffer: outputBuf,
27107
27961
  toBuffer: () => toBufferCompatible(outputBytes)
27108
27962
  };
27109
27963
  }
27964
+ /**
27965
+ * Resolves (or reopens) the comment thread containing `commentId`.
27966
+ * Resolved is a thread-level state in Word, so the root and every reply are updated together.
27967
+ */
27968
+ async resolveComment(commentId, options = {}) {
27969
+ const sourceBytes = this.originalBytes;
27970
+ const working = cloneEntries(this.entries);
27971
+ const resolved = options.resolved !== false;
27972
+ const noChange = (extra) => {
27973
+ const buf = toBufferCompatible(sourceBytes);
27974
+ return {
27975
+ status: "ok",
27976
+ hasChanges: false,
27977
+ written: false,
27978
+ artifactsChanged: [],
27979
+ ...extra,
27980
+ uint8Array: sourceBytes,
27981
+ toUint8Array: () => new Uint8Array(sourceBytes),
27982
+ buffer: buf,
27983
+ toBuffer: () => toBufferCompatible(sourceBytes)
27984
+ };
27985
+ };
27986
+ const result = applyThreadResolutionToParts({
27987
+ commentsXml: text(working, "word/comments.xml"),
27988
+ commentsExtendedXml: text(working, "word/commentsExtended.xml"),
27989
+ commentId,
27990
+ resolved
27991
+ });
27992
+ if (result.status === "error") return packageFailure(sourceBytes, result.error.code, result.error.message);
27993
+ if (!result.hasChanges) return noChange({ resolved, threadRootId: result.threadRootId, commentIds: result.commentIds });
27994
+ const zip = new MemoryZip(working);
27995
+ try {
27996
+ if (result.commentsXml) working.set("word/comments.xml", textEncoder4.encode(result.commentsXml));
27997
+ await ensureCommentsExtendedArtifactsInZip(zip, result.commentsExtendedXml, { replaceExisting: true });
27998
+ await repairKnownContentTypes(zip);
27999
+ if (options.validate !== false) await validateDocxPackage(zip);
28000
+ } catch (error2) {
28001
+ return packageFailure(sourceBytes, "PACKAGE_VALIDATION", error2.message);
28002
+ }
28003
+ const artifactsChanged = [...working].filter(([name, data]) => !this.entries.has(name) || !areByteArraysEqual(data, this.entries.get(name))).map(([name]) => name);
28004
+ this.entries = working;
28005
+ const outputBytes = this.toUint8Array();
28006
+ this.originalBytes = outputBytes;
28007
+ return {
28008
+ status: "ok",
28009
+ hasChanges: true,
28010
+ written: true,
28011
+ resolved,
28012
+ threadRootId: result.threadRootId,
28013
+ commentIds: result.commentIds,
28014
+ artifactsChanged,
28015
+ uint8Array: outputBytes,
28016
+ toUint8Array: () => new Uint8Array(outputBytes),
28017
+ buffer: toBufferCompatible(outputBytes),
28018
+ toBuffer: () => toBufferCompatible(outputBytes)
28019
+ };
28020
+ }
27110
28021
  async deleteComments(options = {}) {
27111
28022
  const sourceBytes = this.originalBytes;
27112
28023
  const working = cloneEntries(this.entries);
@@ -27130,7 +28041,13 @@ var DocxDocument = class {
27130
28041
  if (!parsed.doc || parsed.error) {
27131
28042
  return packageFailure(sourceBytes, "PARSE_ERROR", parsed.error?.message || "Could not parse comments.xml.");
27132
28043
  }
27133
- const matches = (comment) => options.allAuthors === true || (comment.getAttribute("w:author") || comment.getAttribute("author")) === options.author;
28044
+ const requestedIds = Array.isArray(options.ids) ? new Set(options.ids.map(String)) : null;
28045
+ if (requestedIds) {
28046
+ const known = new Set(Array.from(parsed.doc.getElementsByTagNameNS("*", "comment")).map((node) => node.getAttribute("w:id") || node.getAttribute("id")));
28047
+ const missing = [...requestedIds].filter((id) => !known.has(id));
28048
+ if (missing.length) return packageFailure(sourceBytes, "COMMENT_NOT_FOUND", `Comment id(s) not found: ${missing.join(", ")}.`);
28049
+ }
28050
+ const matches = (comment) => requestedIds ? requestedIds.has(comment.getAttribute("w:id") || comment.getAttribute("id")) : options.allAuthors === true || (comment.getAttribute("w:author") || comment.getAttribute("author")) === options.author;
27134
28051
  const ids = new Set(
27135
28052
  Array.from(parsed.doc.getElementsByTagNameNS("*", "comment")).filter(matches).map((node) => node.getAttribute("w:id") || node.getAttribute("id")).filter(Boolean)
27136
28053
  );
@@ -27159,9 +28076,8 @@ var DocxDocument = class {
27159
28076
  const idByParaId = /* @__PURE__ */ new Map();
27160
28077
  for (const comment of Array.from(parsed.doc.getElementsByTagNameNS("*", "comment"))) {
27161
28078
  const id = comment.getAttribute("w:id") || comment.getAttribute("id");
27162
- const paragraph = Array.from(comment.getElementsByTagNameNS("*", "p"))[0];
27163
- const paraId = paragraph?.getAttribute("w14:paraId") || paragraph?.getAttribute("paraId");
27164
- if (paraId) idByParaId.set(paraId.toUpperCase(), id);
28079
+ const paraId = commentParaId(comment);
28080
+ if (paraId) idByParaId.set(paraId, id);
27165
28081
  }
27166
28082
  for (const [paraId, id] of idByParaId) {
27167
28083
  if (ids.has(id)) removedParaIds.add(paraId);
@@ -27180,8 +28096,8 @@ var DocxDocument = class {
27180
28096
  }
27181
28097
  }
27182
28098
  }
27183
- const commentsResult = deleteCommentsByAuthorInOoxml(commentsXml, { author: options.author, allAuthors: options.allAuthors === true });
27184
- const remainingParsed = parseOoxmlSafe(commentsResult.oxml, "application/xml");
28099
+ const remainingXml = requestedIds ? commentsXml : deleteCommentsByAuthorInOoxml(commentsXml, { author: options.author, allAuthors: options.allAuthors === true }).oxml;
28100
+ const remainingParsed = parseOoxmlSafe(remainingXml, "application/xml");
27185
28101
  if (!remainingParsed.doc || remainingParsed.error) {
27186
28102
  return packageFailure(sourceBytes, "PARSE_ERROR", remainingParsed.error?.message || "Could not parse updated comments.xml.");
27187
28103
  }
@@ -27217,10 +28133,14 @@ var DocxDocument = class {
27217
28133
  }
27218
28134
  working.set("word/document.xml", textEncoder4.encode(serializer.serializeToString(documentParsed.doc)));
27219
28135
  try {
27220
- if (options.validate !== false) await validateDocxPackage(new MemoryZip(working));
28136
+ reconcileCommentSiblings(working);
28137
+ const finalZip = new MemoryZip(working);
28138
+ await repairKnownContentTypes(finalZip);
28139
+ if (options.validate !== false) await validateDocxPackage(finalZip);
27221
28140
  } catch (error2) {
27222
28141
  return packageFailure(sourceBytes, "PACKAGE_VALIDATION", error2.message);
27223
28142
  }
28143
+ const artifactsChanged = [...working].filter(([name, data]) => !this.entries.has(name) || !areByteArraysEqual(data, this.entries.get(name))).map(([name]) => name);
27224
28144
  this.entries = working;
27225
28145
  const outputBytes = this.toUint8Array();
27226
28146
  this.originalBytes = outputBytes;
@@ -27231,7 +28151,7 @@ var DocxDocument = class {
27231
28151
  written: true,
27232
28152
  commentsRemoved: ids.size,
27233
28153
  referencesRemoved,
27234
- artifactsChanged: ["word/document.xml", "word/comments.xml", ...extendedParsed?.doc ? ["word/commentsExtended.xml"] : []],
28154
+ artifactsChanged,
27235
28155
  uint8Array: outputBytes,
27236
28156
  toUint8Array: () => new Uint8Array(outputBytes),
27237
28157
  buffer: outputBuf,