@ansonlai/docx-redline-js 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +553 -486
  2. package/README.md +331 -309
  3. package/dist/docx-redline-js.esm.js +1209 -168
  4. package/dist/docx-redline-js.esm.js.map +4 -4
  5. package/dist/docx-redline-js.esm.min.js +107 -107
  6. package/dist/docx-redline-js.esm.min.js.map +4 -4
  7. package/dist/docx-redline.bundle.cjs +1209 -168
  8. package/dist/docx-redline.bundle.cjs.map +4 -4
  9. package/dist/docx-redline.bundle.js +1209 -168
  10. package/dist/docx-redline.bundle.js.map +4 -4
  11. package/docs/AGENT_KNOWLEDGE_BASE.md +69 -8
  12. package/docs/schemas/document-operations.schema.json +29 -0
  13. package/document/docx-document.js +416 -22
  14. package/engine/reconstruction-mode.js +183 -54
  15. package/engine/reconstruction-writer.js +18 -0
  16. package/index.d.ts +598 -575
  17. package/index.js +349 -348
  18. package/node/cli-help.js +5 -5
  19. package/node/cli.js +760 -754
  20. package/node/index.d.ts +1 -1
  21. package/package.json +2 -1
  22. package/pipeline/diff-engine.js +274 -258
  23. package/scripts/generate-word-comment-edge-case-fixture.ps1 +58 -0
  24. package/scripts/generate-word-comment-thread-fixture.ps1 +36 -0
  25. package/scripts/generate-word-header-footer-fixture.ps1 +32 -0
  26. package/scripts/generate-word-multi-section-fixture.ps1 +27 -0
  27. package/scripts/generate-word-package-integrity-cases.mjs +141 -0
  28. package/scripts/generate-word-thread-state-fixtures.ps1 +50 -0
  29. package/scripts/run-tests.mjs +14 -2
  30. package/scripts/word-com-package-integrity.ps1 +98 -0
  31. package/services/batch-operation-orchestrator.js +262 -259
  32. package/services/comment-replies.js +96 -95
  33. package/services/comment-thread-parts.js +280 -0
  34. package/services/document-inspection.js +366 -365
  35. package/services/document-operation-applier.js +230 -202
  36. package/services/document-operation-contract.js +17 -2
  37. package/services/error-recovery.js +6 -0
  38. package/services/headers-footers.js +206 -0
  39. package/services/operation-batch-compiler.js +1 -0
  40. package/services/operation-preflight.js +231 -218
  41. package/services/package-parts.js +80 -0
  42. package/services/standalone-docx-plumbing.js +631 -569
  43. package/services/standalone-operation-runner.d.ts +19 -3
@@ -1,4 +1,4 @@
1
- // @ansonlai/docx-redline-js standalone bundle v0.8.0 — https://github.com/AnsonLai/docx-redline-js
1
+ // @ansonlai/docx-redline-js standalone bundle v0.8.2 — https://github.com/AnsonLai/docx-redline-js
2
2
  var __create = Object.create;
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -9001,15 +9001,29 @@ function createDiffEngine(options = {}) {
9001
9001
  engine.Diff_Timeout = timeout;
9002
9002
  return engine;
9003
9003
  }
9004
- function tokenize(text2) {
9004
+ function tokenize(text2, atomicChars = null) {
9005
9005
  const tokens = [];
9006
+ const pushWord = (word) => {
9007
+ if (!atomicChars || atomicChars.size === 0) {
9008
+ tokens.push(word);
9009
+ return;
9010
+ }
9011
+ let start = 0;
9012
+ for (let index = 0; index < word.length; index++) {
9013
+ if (!atomicChars.has(word[index])) continue;
9014
+ if (index > start) tokens.push(word.slice(start, index));
9015
+ tokens.push(word[index]);
9016
+ start = index + 1;
9017
+ }
9018
+ if (start < word.length) tokens.push(word.slice(start));
9019
+ };
9006
9020
  const leading = text2.match(/^\s+/);
9007
9021
  if (leading) tokens.push(leading[0]);
9008
9022
  const regex = /(\S+)(\s*)/g;
9009
9023
  regex.lastIndex = leading?.[0].length || 0;
9010
9024
  let match;
9011
9025
  while ((match = regex.exec(text2)) !== null) {
9012
- if (match[1]) tokens.push(match[1]);
9026
+ if (match[1]) pushWord(match[1]);
9013
9027
  if (match[2]) tokens.push(match[2]);
9014
9028
  }
9015
9029
  return tokens;
@@ -9037,8 +9051,8 @@ function wordsToChars(text1, text2, options = {}) {
9037
9051
  }
9038
9052
  return { chars, tokenIds };
9039
9053
  }
9040
- const tokens1 = tokenize(text1);
9041
- const tokens2 = tokenize(text2);
9054
+ const tokens1 = tokenize(text1, options.atomicChars);
9055
+ const tokens2 = tokenize(text2, options.atomicChars);
9042
9056
  const encoded1 = mapTokensToChars(tokens1);
9043
9057
  const encoded2 = mapTokensToChars(tokens2);
9044
9058
  return {
@@ -14196,6 +14210,19 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
14196
14210
  pendingReplacementEvent = null;
14197
14211
  }
14198
14212
  }
14213
+ for (const offset of Array.from(sentinelMapByStart.keys()).sort((a, b) => a - b)) {
14214
+ for (const sentinel of sentinelMapByStart.get(offset)) {
14215
+ if (!sentinel.isCommentMarker || emittedCommentMarkers.has(sentinel.node)) continue;
14216
+ emittedCommentMarkers.add(sentinel.node);
14217
+ if (isWordElement(sentinel.node, "commentReference")) {
14218
+ const run = createWordElement(xmlDoc, "w:r");
14219
+ run.appendChild(sentinel.node.cloneNode(true));
14220
+ currentParagraph.appendChild(run);
14221
+ } else {
14222
+ currentParagraph.appendChild(sentinel.node.cloneNode(true));
14223
+ }
14224
+ }
14225
+ }
14199
14226
  if (generateRedlines) {
14200
14227
  containerFragments.forEach((fragment) => {
14201
14228
  Array.from(fragment.childNodes).forEach((node) => {
@@ -14430,7 +14457,11 @@ function applyReconstructionMode(xmlDoc, originalText, modifiedText, serializer,
14430
14457
  if (mapping.paragraphs.length === 0) {
14431
14458
  return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: false });
14432
14459
  }
14433
- const diffs = computeWordDiffs(mapping.originalFullText, mapping.processedModifiedText, diffOptions);
14460
+ const wordDiffs = computeWordDiffs(mapping.originalFullText, mapping.processedModifiedText, {
14461
+ ...diffOptions,
14462
+ atomicChars: new Set(mapping.referenceMap.keys())
14463
+ });
14464
+ const diffs = keepUnchangedTextOutsideWrappers(anchorSharedReferences(wordDiffs, mapping.referenceMap), mapping);
14434
14465
  return withOoxmlSourceType(applyReconstructionDiffs(
14435
14466
  xmlDoc,
14436
14467
  diffs,
@@ -14442,6 +14473,109 @@ function applyReconstructionMode(xmlDoc, originalText, modifiedText, serializer,
14442
14473
  options
14443
14474
  ));
14444
14475
  }
14476
+ function forEachChangeGroup(diffs, onEqual, onGroup) {
14477
+ let index = 0;
14478
+ while (index < diffs.length) {
14479
+ if (diffs[index][0] === 0) {
14480
+ onEqual(diffs[index]);
14481
+ index++;
14482
+ continue;
14483
+ }
14484
+ const group = [];
14485
+ while (index < diffs.length && diffs[index][0] !== 0) group.push(diffs[index++]);
14486
+ onGroup(
14487
+ group,
14488
+ group.filter(([op]) => op === -1).map(([, text2]) => text2).join(""),
14489
+ group.filter(([op]) => op === 1).map(([, text2]) => text2).join("")
14490
+ );
14491
+ }
14492
+ }
14493
+ function pushChange(result, deleted, inserted) {
14494
+ if (deleted === inserted) {
14495
+ if (deleted) result.push([0, deleted]);
14496
+ return;
14497
+ }
14498
+ if (deleted) result.push([-1, deleted]);
14499
+ if (inserted) result.push([1, inserted]);
14500
+ }
14501
+ function anchorSharedReferences(diffs, referenceMap) {
14502
+ if (referenceMap.size === 0) return diffs;
14503
+ const result = [];
14504
+ forEachChangeGroup(diffs, (part) => result.push(part), (group, deleted, inserted) => {
14505
+ const deletedRefs = [...deleted].filter((char) => referenceMap.has(char) && inserted.includes(char));
14506
+ const insertedRefs = [...inserted].filter((char) => deletedRefs.includes(char));
14507
+ if (deletedRefs.length === 0 || deletedRefs.join("") !== insertedRefs.join("")) {
14508
+ result.push(...group);
14509
+ return;
14510
+ }
14511
+ let deletedStart = 0;
14512
+ let insertedStart = 0;
14513
+ for (const ref of deletedRefs) {
14514
+ const deletedAt = deleted.indexOf(ref, deletedStart);
14515
+ const insertedAt = inserted.indexOf(ref, insertedStart);
14516
+ pushChange(result, deleted.slice(deletedStart, deletedAt), inserted.slice(insertedStart, insertedAt));
14517
+ result.push([0, ref]);
14518
+ deletedStart = deletedAt + 1;
14519
+ insertedStart = insertedAt + 1;
14520
+ }
14521
+ pushChange(result, deleted.slice(deletedStart), inserted.slice(insertedStart));
14522
+ });
14523
+ return result;
14524
+ }
14525
+ function commonPrefixLength(left, right) {
14526
+ const limit = Math.min(left.length, right.length);
14527
+ let length = 0;
14528
+ while (length < limit && left[length] === right[length]) length++;
14529
+ return length;
14530
+ }
14531
+ function commonSuffixLength(left, right, limit) {
14532
+ let length = 0;
14533
+ while (length < limit && left[left.length - 1 - length] === right[right.length - 1 - length]) length++;
14534
+ return length;
14535
+ }
14536
+ function keepUnchangedTextOutsideWrappers(diffs, mapping) {
14537
+ const wrapperAt = (index) => mapping.getRunProperties(index).wrapper || null;
14538
+ const result = [];
14539
+ let originalOffset = 0;
14540
+ forEachChangeGroup(diffs, (part) => {
14541
+ result.push(part);
14542
+ originalOffset += part[1].length;
14543
+ }, (group, deleted, inserted) => {
14544
+ const start = originalOffset;
14545
+ const end = start + deleted.length;
14546
+ originalOffset = end;
14547
+ const isWrapperBoundary = (index) => index > start && index < end && wrapperAt(index - 1) !== wrapperAt(index);
14548
+ let prefix = 0;
14549
+ let suffix = 0;
14550
+ if (deleted && inserted) {
14551
+ const sharedSuffix = commonSuffixLength(deleted, inserted, Math.min(deleted.length, inserted.length));
14552
+ for (let length = sharedSuffix; length > 0; length--) {
14553
+ if (isWrapperBoundary(end - length)) {
14554
+ suffix = length;
14555
+ break;
14556
+ }
14557
+ }
14558
+ const sharedPrefix = Math.min(
14559
+ commonPrefixLength(deleted, inserted),
14560
+ Math.min(deleted.length, inserted.length) - suffix
14561
+ );
14562
+ for (let length = sharedPrefix; length > 0; length--) {
14563
+ if (isWrapperBoundary(start + length)) {
14564
+ prefix = length;
14565
+ break;
14566
+ }
14567
+ }
14568
+ }
14569
+ if (prefix === 0 && suffix === 0) {
14570
+ result.push(...group);
14571
+ return;
14572
+ }
14573
+ if (prefix > 0) result.push([0, deleted.slice(0, prefix)]);
14574
+ pushChange(result, deleted.slice(prefix, deleted.length - suffix), inserted.slice(prefix, inserted.length - suffix));
14575
+ if (suffix > 0) result.push([0, deleted.slice(deleted.length - suffix)]);
14576
+ });
14577
+ return result;
14578
+ }
14445
14579
 
14446
14580
  // engine/table-mode.js
14447
14581
  function noChanges(serializer, xmlDoc) {
@@ -17990,6 +18124,260 @@ function ingestWordOoxmlToMarkdownResult(ooxml) {
17990
18124
  return { text: lines.join("\n\n").trim(), status: "ok", warnings: parsed.warnings };
17991
18125
  }
17992
18126
 
18127
+ // services/comment-builders.js
18128
+ var NS_W14 = "http://schemas.microsoft.com/office/word/2010/wordml";
18129
+ var NS_W15 = "http://schemas.microsoft.com/office/word/2012/wordml";
18130
+ function createCommentParaId(commentId) {
18131
+ const numeric = Number.parseInt(String(commentId), 10);
18132
+ const value = Number.isFinite(numeric) ? 1879048192 + (numeric >>> 0) >>> 0 : 1879048192;
18133
+ return value.toString(16).toUpperCase().padStart(8, "0").slice(-8);
18134
+ }
18135
+ function buildCommentElement(commentId, author, content, date, paraId = createCommentParaId(commentId)) {
18136
+ const initials = author.split(" ").map((word) => word[0]).join("").toUpperCase() || "AI";
18137
+ const escapedContent = escapeXml(content);
18138
+ const escapedAuthor = escapeXml(author);
18139
+ return `<w:comment w:id="${commentId}" w:author="${escapedAuthor}" w:date="${date}" w:initials="${initials}">
18140
+ <w:p w14:paraId="${escapeXml(paraId)}" xmlns:w14="${NS_W14}">
18141
+ <w:r><w:t>${escapedContent}</w:t></w:r>
18142
+ </w:p>
18143
+ </w:comment>`;
18144
+ }
18145
+ function buildCommentsPartXml(comments) {
18146
+ if (!comments || comments.length === 0) {
18147
+ return `<w:comments xmlns:w="${NS_W}"></w:comments>`;
18148
+ }
18149
+ const commentElements = comments.map(
18150
+ (comment) => buildCommentElement(comment.id, comment.author, comment.content, comment.date, comment.paraId)
18151
+ ).join("\n ");
18152
+ return `<w:comments xmlns:w="${NS_W}">
18153
+ ${commentElements}
18154
+ </w:comments>`;
18155
+ }
18156
+ function buildCommentsExtendedPartXml(entries) {
18157
+ const body = (entries || []).map((entry) => {
18158
+ const parent = entry.paraIdParent ? ` w15:paraIdParent="${escapeXml(entry.paraIdParent)}"` : "";
18159
+ return `<w15:commentEx w15:paraId="${escapeXml(entry.paraId)}"${parent} w15:done="${entry.done ? "1" : "0"}"/>`;
18160
+ }).join("");
18161
+ return `<w15:commentsEx xmlns:w15="${NS_W15}">${body}</w15:commentsEx>`;
18162
+ }
18163
+
18164
+ // services/comment-thread-parts.js
18165
+ var attrOf = (node, qualified, local) => node?.getAttribute?.(qualified) || node?.getAttribute?.(local) || "";
18166
+ function commentThreadParagraph(commentElement) {
18167
+ const paragraphs = Array.from(commentElement?.getElementsByTagNameNS?.("*", "p") || []);
18168
+ return paragraphs.length ? paragraphs[paragraphs.length - 1] : null;
18169
+ }
18170
+ function commentParaId(commentElement) {
18171
+ return attrOf(commentThreadParagraph(commentElement), "w14:paraId", "paraId").toUpperCase();
18172
+ }
18173
+ function usedParaIds(commentsDoc, extendedDoc) {
18174
+ const ids = /* @__PURE__ */ new Set();
18175
+ for (const p of Array.from(commentsDoc?.getElementsByTagNameNS("*", "p") || [])) {
18176
+ const id = attrOf(p, "w14:paraId", "paraId");
18177
+ if (id) ids.add(id.toUpperCase());
18178
+ }
18179
+ for (const ex of Array.from(extendedDoc?.getElementsByTagNameNS("*", "commentEx") || [])) {
18180
+ const id = attrOf(ex, "w15:paraId", "paraId");
18181
+ if (id) ids.add(id.toUpperCase());
18182
+ }
18183
+ return ids;
18184
+ }
18185
+ function allocateParaId(commentId, occupied) {
18186
+ let candidate = createCommentParaId(commentId);
18187
+ let value = Number.parseInt(candidate, 16) >>> 0;
18188
+ while (occupied.has(candidate)) {
18189
+ value = value + 1 >>> 0;
18190
+ candidate = value.toString(16).toUpperCase().padStart(8, "0");
18191
+ }
18192
+ occupied.add(candidate);
18193
+ return candidate;
18194
+ }
18195
+ var parseDoc = (xml, partName) => {
18196
+ const parsed = parseOoxmlSafe(xml, "application/xml");
18197
+ if (!parsed.doc || parsed.error) return { error: { code: "PARSE_ERROR", message: `Could not parse ${partName}: ${parsed.error?.message || "invalid XML"}` } };
18198
+ return { doc: parsed.doc };
18199
+ };
18200
+ function applyThreadResolutionToParts({ commentsXml, commentsExtendedXml = null, commentId, resolved = true }) {
18201
+ if (!commentsXml) return { status: "error", error: { code: "COMMENTS_PART_MISSING", message: "Resolving a comment requires an existing word/comments.xml part." } };
18202
+ const commentsParsed = parseDoc(commentsXml, "word/comments.xml");
18203
+ if (commentsParsed.error) return { status: "error", error: commentsParsed.error };
18204
+ const commentsDoc = commentsParsed.doc;
18205
+ const idOf = (node) => attrOf(node, "w:id", "id");
18206
+ const comments = Array.from(commentsDoc.getElementsByTagNameNS("*", "comment"));
18207
+ const target = comments.find((node) => idOf(node) === String(commentId));
18208
+ if (!target) return { status: "error", error: { code: "COMMENT_NOT_FOUND", message: `Comment '${commentId}' was not found.` } };
18209
+ let extendedDoc;
18210
+ if (commentsExtendedXml) {
18211
+ const parsed = parseDoc(commentsExtendedXml, "word/commentsExtended.xml");
18212
+ if (parsed.error) return { status: "error", error: parsed.error };
18213
+ extendedDoc = parsed.doc;
18214
+ } else {
18215
+ extendedDoc = parseDoc(`<w15:commentsEx xmlns:w15="${NS_W15}"/>`, "word/commentsExtended.xml").doc;
18216
+ }
18217
+ const root = extendedDoc.documentElement;
18218
+ const entryFor = (paraId) => Array.from(root.getElementsByTagNameNS("*", "commentEx")).find((node) => attrOf(node, "w15:paraId", "paraId").toUpperCase() === paraId) || null;
18219
+ const occupied = usedParaIds(commentsDoc, extendedDoc);
18220
+ let commentsChanged = false;
18221
+ const keyOf = (node) => {
18222
+ let paraId = commentParaId(node);
18223
+ if (!paraId) {
18224
+ paraId = allocateParaId(idOf(node), occupied);
18225
+ commentThreadParagraph(node)?.setAttributeNS(NS_W14, "w14:paraId", paraId);
18226
+ commentsChanged = true;
18227
+ }
18228
+ return paraId;
18229
+ };
18230
+ const targetKey = keyOf(target);
18231
+ const targetEntry = entryFor(targetKey);
18232
+ const rootKey = targetEntry && attrOf(targetEntry, "w15:paraIdParent", "paraIdParent").toUpperCase() || targetKey;
18233
+ const threadKeys = /* @__PURE__ */ new Set([rootKey, targetKey]);
18234
+ for (const entry of Array.from(root.getElementsByTagNameNS("*", "commentEx"))) {
18235
+ if (attrOf(entry, "w15:paraIdParent", "paraIdParent").toUpperCase() === rootKey) threadKeys.add(attrOf(entry, "w15:paraId", "paraId").toUpperCase());
18236
+ }
18237
+ const doneValue = resolved ? "1" : "0";
18238
+ let extendedChanged = !commentsExtendedXml;
18239
+ for (const key of threadKeys) {
18240
+ let entry = entryFor(key);
18241
+ if (!entry) {
18242
+ entry = extendedDoc.createElementNS(NS_W15, "w15:commentEx");
18243
+ entry.setAttributeNS(NS_W15, "w15:paraId", key);
18244
+ if (key !== rootKey) entry.setAttributeNS(NS_W15, "w15:paraIdParent", rootKey);
18245
+ entry.setAttributeNS(NS_W15, "w15:done", doneValue);
18246
+ root.appendChild(entry);
18247
+ extendedChanged = true;
18248
+ } else if (attrOf(entry, "w15:done", "done") !== doneValue) {
18249
+ entry.setAttributeNS(NS_W15, "w15:done", doneValue);
18250
+ extendedChanged = true;
18251
+ }
18252
+ }
18253
+ const byKey = new Map(comments.map((node) => [commentParaId(node), node]));
18254
+ const commentIds = [...threadKeys].map((key) => byKey.get(key)).filter(Boolean).map(idOf);
18255
+ const rootComment = byKey.get(rootKey);
18256
+ const serializer = createSerializer();
18257
+ const hasChanges = extendedChanged || commentsChanged;
18258
+ return {
18259
+ status: "ok",
18260
+ hasChanges,
18261
+ resolved,
18262
+ threadRootId: rootComment ? idOf(rootComment) : String(commentId),
18263
+ commentIds,
18264
+ ...hasChanges ? { commentsExtendedXml: serializer.serializeToString(extendedDoc) } : {},
18265
+ ...commentsChanged ? { commentsXml: serializer.serializeToString(commentsDoc) } : {}
18266
+ };
18267
+ }
18268
+ var NS_W_MAIN = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
18269
+ function anchorReplyInDocument(xmlDoc, memberIds, newId) {
18270
+ const wanted = new Set(memberIds.map(String));
18271
+ const idOf = (node) => attrOf(node, "w:id", "id");
18272
+ const all = (name) => Array.from(xmlDoc.getElementsByTagNameNS("*", name)).filter((node) => wanted.has(idOf(node)));
18273
+ const starts = all("commentRangeStart");
18274
+ const references = all("commentReference");
18275
+ if (!starts.length || !references.length) return false;
18276
+ const lastStart = starts[starts.length - 1];
18277
+ const lastReference = references[references.length - 1];
18278
+ const referenceRun = lastReference.parentNode?.localName === "r" ? lastReference.parentNode : lastReference;
18279
+ const make = (name, parent) => {
18280
+ const element = xmlDoc.createElementNS(NS_W_MAIN, `w:${name}`);
18281
+ element.setAttributeNS(NS_W_MAIN, "w:id", String(newId));
18282
+ if (parent) parent.appendChild(element);
18283
+ return element;
18284
+ };
18285
+ const insertAfter = (anchor, node) => anchor.parentNode.insertBefore(node, anchor.nextSibling);
18286
+ insertAfter(lastStart, make("commentRangeStart"));
18287
+ const end = make("commentRangeEnd");
18288
+ const run = xmlDoc.createElementNS(NS_W_MAIN, "w:r");
18289
+ run.appendChild(xmlDoc.createElementNS(NS_W_MAIN, "w:rPr"));
18290
+ make("commentReference", run);
18291
+ insertAfter(referenceRun, end);
18292
+ insertAfter(end, run);
18293
+ return true;
18294
+ }
18295
+ var NS_W16CID = "http://schemas.microsoft.com/office/word/2016/wordml/cid";
18296
+ var NS_W16CEX = "http://schemas.microsoft.com/office/word/2018/wordml/cex";
18297
+ var durableIdFor = (paraId, taken) => {
18298
+ let value = (Number.parseInt(paraId, 16) ^ 625341585) & 2147483646;
18299
+ let candidate = value.toString(16).toUpperCase().padStart(8, "0");
18300
+ while (taken.has(candidate)) {
18301
+ value = value + 2 & 2147483646;
18302
+ candidate = value.toString(16).toUpperCase().padStart(8, "0");
18303
+ }
18304
+ taken.add(candidate);
18305
+ return candidate;
18306
+ };
18307
+ function reconcileCommentSiblingParts({ commentsXml, commentsIdsXml = null, commentsExtensibleXml = null }) {
18308
+ const out = {};
18309
+ if (!commentsXml || !commentsIdsXml && !commentsExtensibleXml) return out;
18310
+ const commentsParsed = parseOoxmlSafe(commentsXml, "application/xml");
18311
+ if (!commentsParsed.doc || commentsParsed.error) return out;
18312
+ const serializer = createSerializer();
18313
+ const byKey = /* @__PURE__ */ new Map();
18314
+ for (const node of Array.from(commentsParsed.doc.getElementsByTagNameNS("*", "comment"))) {
18315
+ const key = commentParaId(node);
18316
+ if (key) byKey.set(key, node);
18317
+ }
18318
+ let idsDoc = null;
18319
+ const durableByKey = /* @__PURE__ */ new Map();
18320
+ const taken = /* @__PURE__ */ new Set();
18321
+ let idsChanged = false;
18322
+ if (commentsIdsXml) {
18323
+ const parsed = parseOoxmlSafe(commentsIdsXml, "application/xml");
18324
+ if (!parsed.doc || parsed.error) return out;
18325
+ idsDoc = parsed.doc;
18326
+ const root = idsDoc.documentElement;
18327
+ for (const entry of Array.from(root.getElementsByTagNameNS("*", "commentId"))) {
18328
+ const key = attrOf(entry, "w16cid:paraId", "paraId").toUpperCase();
18329
+ const durable = attrOf(entry, "w16cid:durableId", "durableId").toUpperCase();
18330
+ if (durable) taken.add(durable);
18331
+ if (!byKey.has(key)) {
18332
+ root.removeChild(entry);
18333
+ idsChanged = true;
18334
+ continue;
18335
+ }
18336
+ durableByKey.set(key, durable);
18337
+ }
18338
+ for (const key of byKey.keys()) {
18339
+ if (durableByKey.has(key)) continue;
18340
+ const durable = durableIdFor(key, taken);
18341
+ const entry = idsDoc.createElementNS(NS_W16CID, "w16cid:commentId");
18342
+ entry.setAttributeNS(NS_W16CID, "w16cid:paraId", key);
18343
+ entry.setAttributeNS(NS_W16CID, "w16cid:durableId", durable);
18344
+ root.appendChild(entry);
18345
+ durableByKey.set(key, durable);
18346
+ idsChanged = true;
18347
+ }
18348
+ }
18349
+ let extDoc = null;
18350
+ let extChanged = false;
18351
+ if (commentsExtensibleXml && idsDoc) {
18352
+ const parsed = parseOoxmlSafe(commentsExtensibleXml, "application/xml");
18353
+ if (parsed.doc && !parsed.error) {
18354
+ extDoc = parsed.doc;
18355
+ const root = extDoc.documentElement;
18356
+ const live = new Set(durableByKey.values());
18357
+ const present = /* @__PURE__ */ new Set();
18358
+ for (const entry of Array.from(root.getElementsByTagNameNS("*", "commentExtensible"))) {
18359
+ const durable = attrOf(entry, "w16cex:durableId", "durableId").toUpperCase();
18360
+ if (!live.has(durable)) {
18361
+ root.removeChild(entry);
18362
+ extChanged = true;
18363
+ } else present.add(durable);
18364
+ }
18365
+ for (const [key, durable] of durableByKey) {
18366
+ if (present.has(durable)) continue;
18367
+ const date = attrOf(byKey.get(key), "w:date", "date");
18368
+ const entry = extDoc.createElementNS(NS_W16CEX, "w16cex:commentExtensible");
18369
+ entry.setAttributeNS(NS_W16CEX, "w16cex:durableId", durable);
18370
+ if (date) entry.setAttributeNS(NS_W16CEX, "w16cex:dateUtc", date);
18371
+ root.appendChild(entry);
18372
+ extChanged = true;
18373
+ }
18374
+ }
18375
+ }
18376
+ if (idsChanged) out.commentsIdsXml = serializer.serializeToString(idsDoc);
18377
+ if (extChanged) out.commentsExtensibleXml = serializer.serializeToString(extDoc);
18378
+ return out;
18379
+ }
18380
+
17993
18381
  // core/sha256.js
17994
18382
  var K = new Uint32Array([
17995
18383
  1116352408,
@@ -18482,7 +18870,7 @@ function readCommentDefinitions(commentsDoc) {
18482
18870
  author: attr2(comment, "author") || null,
18483
18871
  date: attr2(comment, "date") || null,
18484
18872
  text: paragraphs.map((p) => extractCanonicalParagraphText(p)).join("\n"),
18485
- paraId: paragraphs[0]?.getAttribute?.("w14:paraId") || paragraphs[0]?.getAttribute?.("paraId") || null
18873
+ paraId: commentParaId(comment) || null
18486
18874
  });
18487
18875
  }
18488
18876
  return result;
@@ -18713,43 +19101,6 @@ function inspectDocumentParts(parts, options = {}) {
18713
19101
  };
18714
19102
  }
18715
19103
 
18716
- // services/comment-builders.js
18717
- var NS_W14 = "http://schemas.microsoft.com/office/word/2010/wordml";
18718
- var NS_W15 = "http://schemas.microsoft.com/office/word/2012/wordml";
18719
- function createCommentParaId(commentId) {
18720
- const numeric = Number.parseInt(String(commentId), 10);
18721
- const value = Number.isFinite(numeric) ? 1879048192 + (numeric >>> 0) >>> 0 : 1879048192;
18722
- return value.toString(16).toUpperCase().padStart(8, "0").slice(-8);
18723
- }
18724
- function buildCommentElement(commentId, author, content, date, paraId = createCommentParaId(commentId)) {
18725
- const initials = author.split(" ").map((word) => word[0]).join("").toUpperCase() || "AI";
18726
- const escapedContent = escapeXml(content);
18727
- const escapedAuthor = escapeXml(author);
18728
- return `<w:comment w:id="${commentId}" w:author="${escapedAuthor}" w:date="${date}" w:initials="${initials}">
18729
- <w:p w14:paraId="${escapeXml(paraId)}" xmlns:w14="${NS_W14}">
18730
- <w:r><w:t>${escapedContent}</w:t></w:r>
18731
- </w:p>
18732
- </w:comment>`;
18733
- }
18734
- function buildCommentsPartXml(comments) {
18735
- if (!comments || comments.length === 0) {
18736
- return `<w:comments xmlns:w="${NS_W}"></w:comments>`;
18737
- }
18738
- const commentElements = comments.map(
18739
- (comment) => buildCommentElement(comment.id, comment.author, comment.content, comment.date, comment.paraId)
18740
- ).join("\n ");
18741
- return `<w:comments xmlns:w="${NS_W}">
18742
- ${commentElements}
18743
- </w:comments>`;
18744
- }
18745
- function buildCommentsExtendedPartXml(entries) {
18746
- const body = (entries || []).map((entry) => {
18747
- const parent = entry.paraIdParent ? ` w15:paraIdParent="${escapeXml(entry.paraIdParent)}"` : "";
18748
- return `<w15:commentEx w15:paraId="${escapeXml(entry.paraId)}"${parent} w15:done="${entry.done ? "1" : "0"}"/>`;
18749
- }).join("");
18750
- return `<w15:commentsEx xmlns:w15="${NS_W15}">${body}</w15:commentsEx>`;
18751
- }
18752
-
18753
19104
  // services/comment-locator.js
18754
19105
  function createParagraphTextIndex(paragraph, options = {}) {
18755
19106
  const revisionView = options.revisionView === "current" ? "accepted" : options.revisionView || "accepted";
@@ -19210,29 +19561,7 @@ function parseRequired(xml, partName) {
19210
19561
  }
19211
19562
  return { doc: parsed.doc };
19212
19563
  }
19213
- function usedParaIds(commentsDoc, extendedDoc) {
19214
- const ids = /* @__PURE__ */ new Set();
19215
- for (const p of Array.from(commentsDoc?.getElementsByTagNameNS("*", "p") || [])) {
19216
- const id = attr3(p, "w14:paraId", "paraId");
19217
- if (id) ids.add(id.toUpperCase());
19218
- }
19219
- for (const ex of Array.from(extendedDoc?.getElementsByTagNameNS("*", "commentEx") || [])) {
19220
- const id = attr3(ex, "w15:paraId", "paraId");
19221
- if (id) ids.add(id.toUpperCase());
19222
- }
19223
- return ids;
19224
- }
19225
- function allocateParaId(commentId, occupied) {
19226
- let candidate = createCommentParaId(commentId);
19227
- let value = Number.parseInt(candidate, 16) >>> 0;
19228
- while (occupied.has(candidate)) {
19229
- value = value + 1 >>> 0;
19230
- candidate = value.toString(16).toUpperCase().padStart(8, "0");
19231
- }
19232
- occupied.add(candidate);
19233
- return candidate;
19234
- }
19235
- function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, parentCommentId, commentId, commentContent, author, date = (/* @__PURE__ */ new Date()).toISOString() }) {
19564
+ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, documentXml = null, parentCommentId, commentId, commentContent, author, date = (/* @__PURE__ */ new Date()).toISOString() }) {
19236
19565
  if (!commentsXml) return { status: "error", error: { code: "COMMENTS_PART_MISSING", message: "A comment reply requires an existing word/comments.xml part." } };
19237
19566
  const commentsParsed = parseRequired(commentsXml, "word/comments.xml");
19238
19567
  if (commentsParsed.error) return { status: "error", error: commentsParsed.error };
@@ -19246,15 +19575,31 @@ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, par
19246
19575
  extendedDoc = parsed.doc;
19247
19576
  }
19248
19577
  const occupied = usedParaIds(commentsDoc, extendedDoc);
19249
- const parentParagraph = Array.from(parent.getElementsByTagNameNS(NS_W6, "p"))[0] || Array.from(parent.getElementsByTagNameNS("*", "p"))[0];
19578
+ if (documentXml) for (const match of String(documentXml).matchAll(/w14:paraId="([0-9A-Fa-f]{8})"/g)) occupied.add(match[1].toUpperCase());
19579
+ const parentParagraph = commentThreadParagraph(parent);
19250
19580
  if (!parentParagraph) return { status: "error", error: { code: "PARENT_COMMENT_INVALID", message: `Parent comment '${parentCommentId}' has no paragraph.` } };
19251
- let parentParaId = attr3(parentParagraph, "w14:paraId", "paraId");
19581
+ let parentParaId = commentParaId(parent);
19252
19582
  if (!parentParaId) {
19253
19583
  parentParaId = allocateParaId(parentCommentId, occupied);
19254
19584
  parentParagraph.setAttributeNS(NS_W14, "w14:paraId", parentParaId);
19255
19585
  } else {
19256
19586
  parentParaId = parentParaId.toUpperCase();
19257
19587
  }
19588
+ for (const node of Array.from(extendedDoc?.getElementsByTagNameNS("*", "commentEx") || [])) {
19589
+ if (attr3(node, "w15:paraId", "paraId").toUpperCase() === parentParaId) {
19590
+ const root2 = attr3(node, "w15:paraIdParent", "paraIdParent").toUpperCase();
19591
+ if (root2) parentParaId = root2;
19592
+ break;
19593
+ }
19594
+ }
19595
+ const threadCommentIds = [];
19596
+ const commentNodes = Array.from(commentsDoc.getElementsByTagNameNS("*", "comment"));
19597
+ const parentOf = new Map(Array.from(extendedDoc?.getElementsByTagNameNS("*", "commentEx") || []).map((node) => [attr3(node, "w15:paraId", "paraId").toUpperCase(), attr3(node, "w15:paraIdParent", "paraIdParent").toUpperCase()]));
19598
+ for (const node of commentNodes) {
19599
+ const key = commentParaId(node);
19600
+ if (key === parentParaId || parentOf.get(key) === parentParaId) threadCommentIds.push(attr3(node, "w:id", "id"));
19601
+ }
19602
+ const threadRootCommentId = attr3(commentNodes.find((node) => commentParaId(node) === parentParaId), "w:id", "id") || String(parentCommentId);
19258
19603
  const replyParaId = allocateParaId(commentId, occupied);
19259
19604
  const replyParsed = parseRequired(`<w:comments xmlns:w="${NS_W6}" xmlns:w14="${NS_W14}">${buildCommentElement(commentId, author, commentContent, date, replyParaId)}</w:comments>`, "reply comment");
19260
19605
  commentsDoc.documentElement.appendChild(commentsDoc.importNode(replyParsed.doc.documentElement.firstChild, true));
@@ -19263,16 +19608,18 @@ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, par
19263
19608
  }
19264
19609
  const root = extendedDoc.documentElement;
19265
19610
  const entries = Array.from(root.getElementsByTagNameNS("*", "commentEx"));
19266
- if (!entries.some((node) => attr3(node, "w15:paraId", "paraId").toUpperCase() === parentParaId)) {
19611
+ const rootEntry = entries.find((node) => attr3(node, "w15:paraId", "paraId").toUpperCase() === parentParaId);
19612
+ if (!rootEntry) {
19267
19613
  const parentEx = extendedDoc.createElementNS(NS_W15, "w15:commentEx");
19268
19614
  parentEx.setAttributeNS(NS_W15, "w15:paraId", parentParaId);
19269
19615
  parentEx.setAttributeNS(NS_W15, "w15:done", "0");
19270
19616
  root.appendChild(parentEx);
19271
19617
  }
19618
+ const threadDone = attr3(rootEntry, "w15:done", "done") === "1" ? "1" : "0";
19272
19619
  const replyEx = extendedDoc.createElementNS(NS_W15, "w15:commentEx");
19273
19620
  replyEx.setAttributeNS(NS_W15, "w15:paraId", replyParaId);
19274
19621
  replyEx.setAttributeNS(NS_W15, "w15:paraIdParent", parentParaId);
19275
- replyEx.setAttributeNS(NS_W15, "w15:done", "0");
19622
+ replyEx.setAttributeNS(NS_W15, "w15:done", threadDone);
19276
19623
  root.appendChild(replyEx);
19277
19624
  const serializer = createSerializer();
19278
19625
  return {
@@ -19285,7 +19632,9 @@ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, par
19285
19632
  commentId,
19286
19633
  parentCommentId: String(parentCommentId),
19287
19634
  paraId: replyParaId,
19288
- parentParaId
19635
+ parentParaId,
19636
+ threadRootCommentId,
19637
+ threadCommentIds: threadCommentIds.length ? threadCommentIds : [threadRootCommentId]
19289
19638
  };
19290
19639
  }
19291
19640
 
@@ -19350,19 +19699,19 @@ function applyFormattingRemovalToOoxml(ooxmlString, targetText, formatTypes) {
19350
19699
  if (!targetText || !ooxmlString) return ooxmlString;
19351
19700
  const doc = parseOoxml(ooxmlString);
19352
19701
  if (!doc) return ooxmlString;
19353
- const NS_W10 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19354
- const runs = doc.getElementsByTagNameNS(NS_W10, "r");
19355
- const insertions = doc.getElementsByTagNameNS(NS_W10, "ins");
19702
+ const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19703
+ const runs = doc.getElementsByTagNameNS(NS_W11, "r");
19704
+ const insertions = doc.getElementsByTagNameNS(NS_W11, "ins");
19356
19705
  const allRuns = [...Array.from(runs)];
19357
19706
  for (const ins of insertions) {
19358
- const insideRuns = ins.getElementsByTagNameNS(NS_W10, "r");
19707
+ const insideRuns = ins.getElementsByTagNameNS(NS_W11, "r");
19359
19708
  allRuns.push(...Array.from(insideRuns));
19360
19709
  }
19361
19710
  for (const run of allRuns) {
19362
- const textNodes = run.getElementsByTagNameNS(NS_W10, "t");
19711
+ const textNodes = run.getElementsByTagNameNS(NS_W11, "t");
19363
19712
  const runText = Array.from(textNodes).map((t) => t.textContent).join("");
19364
19713
  if (runText.includes(targetText) || runText === targetText) {
19365
- const rPrElements = run.getElementsByTagNameNS(NS_W10, "rPr");
19714
+ const rPrElements = run.getElementsByTagNameNS(NS_W11, "rPr");
19366
19715
  if (rPrElements.length > 0) {
19367
19716
  const rPr = rPrElements[0];
19368
19717
  const newRPr = removeFormattingFromRPr(rPr, formatTypes);
@@ -19397,7 +19746,7 @@ var HIGHLIGHT_COLOR_MAP = {
19397
19746
  "white": "white"
19398
19747
  };
19399
19748
  function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
19400
- const NS_W10 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19749
+ const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19401
19750
  const ooxmlColor = HIGHLIGHT_COLOR_MAP[color.toLowerCase()] || "yellow";
19402
19751
  const generateRedlines = options?.generateRedlines ?? false;
19403
19752
  const author = options?.author || getDefaultAuthor();
@@ -19416,10 +19765,10 @@ function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
19416
19765
  }
19417
19766
  });
19418
19767
  }
19419
- const existingHighlight = rPrElement.getElementsByTagNameNS(NS_W10, "highlight");
19768
+ const existingHighlight = rPrElement.getElementsByTagNameNS(NS_W11, "highlight");
19420
19769
  Array.from(existingHighlight).forEach(removeNode2);
19421
19770
  const highlightEl = createWordElement(doc, "w:highlight");
19422
- highlightEl.setAttributeNS(NS_W10, "w:val", ooxmlColor);
19771
+ highlightEl.setAttributeNS(NS_W11, "w:val", ooxmlColor);
19423
19772
  rPrElement.appendChild(highlightEl);
19424
19773
  if (generateRedlines && previousRPrState) {
19425
19774
  const rPrChange = createWordElement(doc, "w:rPrChange");
@@ -19428,7 +19777,7 @@ function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
19428
19777
  rPrChange.setAttribute("w:author", metadata.author);
19429
19778
  rPrChange.setAttribute("w:date", metadata.date);
19430
19779
  rPrChange.appendChild(previousRPrState);
19431
- const existingChange = rPrElement.getElementsByTagNameNS(NS_W10, "rPrChange");
19780
+ const existingChange = rPrElement.getElementsByTagNameNS(NS_W11, "rPrChange");
19432
19781
  Array.from(existingChange).forEach(removeNode2);
19433
19782
  rPrElement.appendChild(rPrChange);
19434
19783
  }
@@ -19445,12 +19794,12 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
19445
19794
  } else {
19446
19795
  revisionIdAllocator = createRevisionIdAllocator(doc);
19447
19796
  }
19448
- const NS_W10 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19797
+ const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19449
19798
  const getRunText = (run) => {
19450
- const textNodes = run.getElementsByTagNameNS(NS_W10, "t");
19799
+ const textNodes = run.getElementsByTagNameNS(NS_W11, "t");
19451
19800
  return Array.from(textNodes).map((t) => t.textContent).join("");
19452
19801
  };
19453
- const allRuns = Array.from(doc.getElementsByTagNameNS(NS_W10, "r"));
19802
+ const allRuns = Array.from(doc.getElementsByTagNameNS(NS_W11, "r"));
19454
19803
  const sourceRunsWithClaimedRevisionIds = /* @__PURE__ */ new WeakSet();
19455
19804
  const cloneRunWithText2 = (sourceRun, text2, shouldHighlight) => {
19456
19805
  const clonedRun = sourceRun.cloneNode(true);
@@ -19459,14 +19808,14 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
19459
19808
  } else {
19460
19809
  sourceRunsWithClaimedRevisionIds.add(sourceRun);
19461
19810
  }
19462
- const textNodes = clonedRun.getElementsByTagNameNS(NS_W10, "t");
19811
+ const textNodes = clonedRun.getElementsByTagNameNS(NS_W11, "t");
19463
19812
  Array.from(textNodes).forEach(removeNode2);
19464
19813
  const newText = createWordElement(doc, "w:t");
19465
19814
  newText.setAttribute("xml:space", "preserve");
19466
19815
  newText.textContent = text2;
19467
19816
  clonedRun.appendChild(newText);
19468
19817
  if (shouldHighlight) {
19469
- const rPrElements = clonedRun.getElementsByTagNameNS(NS_W10, "rPr");
19818
+ const rPrElements = clonedRun.getElementsByTagNameNS(NS_W11, "rPr");
19470
19819
  const existingRPr = rPrElements.length > 0 ? rPrElements[0] : null;
19471
19820
  const newRPr = injectHighlightIntoRPr(doc, existingRPr, color, options);
19472
19821
  if (existingRPr) {
@@ -19515,20 +19864,68 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
19515
19864
  return serializeOoxml(doc);
19516
19865
  }
19517
19866
 
19867
+ // services/package-parts.js
19868
+ var WORD_ML = "application/vnd.openxmlformats-officedocument.wordprocessingml";
19869
+ var REL_OFFICE_DOC = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
19870
+ var PART_SPECS = Object.freeze([
19871
+ { id: "document", path: "word/document.xml", pathPattern: null, contentType: `${WORD_ML}.document.main+xml`, legacyContentTypes: [], relType: `${REL_OFFICE_DOC}/officeDocument`, relTarget: null, verified: true },
19872
+ { id: "numbering", path: "word/numbering.xml", pathPattern: null, contentType: `${WORD_ML}.numbering+xml`, legacyContentTypes: [], relType: `${REL_OFFICE_DOC}/numbering`, relTarget: "numbering.xml", verified: false },
19873
+ { id: "comments", path: "word/comments.xml", pathPattern: null, contentType: `${WORD_ML}.comments+xml`, legacyContentTypes: [], relType: `${REL_OFFICE_DOC}/comments`, relTarget: "comments.xml", verified: true },
19874
+ {
19875
+ id: "commentsExtended",
19876
+ path: "word/commentsExtended.xml",
19877
+ pathPattern: null,
19878
+ contentType: `${WORD_ML}.commentsExtended+xml`,
19879
+ // Written by this package through 0.8.0; Word does not recognize it and offers to repair the file.
19880
+ legacyContentTypes: ["application/vnd.ms-word.commentsExtended+xml"],
19881
+ relType: "http://schemas.microsoft.com/office/2011/relationships/commentsExtended",
19882
+ relTarget: "commentsExtended.xml",
19883
+ verified: true
19884
+ },
19885
+ { 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 },
19886
+ { 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 },
19887
+ // Not yet produced by any Word fixture (Word only writes people.xml for signed-in authors): unverified.
19888
+ { 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 },
19889
+ { 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 },
19890
+ { 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 }
19891
+ ].map((spec) => Object.freeze(spec)));
19892
+ var BY_ID = new Map(PART_SPECS.map((spec) => [spec.id, spec]));
19893
+ function getPartSpec(id) {
19894
+ const spec = BY_ID.get(id);
19895
+ if (!spec) throw new Error(`Unknown package part: ${id}`);
19896
+ return spec;
19897
+ }
19898
+ function toEntryName(partName) {
19899
+ return String(partName || "").replace(/^\/+/, "");
19900
+ }
19901
+ function findPartSpecByPartName(partName) {
19902
+ const name = toEntryName(partName);
19903
+ const lower = name.toLowerCase();
19904
+ return PART_SPECS.find((spec) => spec.path ? spec.path.toLowerCase() === lower : spec.pathPattern?.test(name)) || null;
19905
+ }
19906
+ function correctedContentTypeFor(partName, actualContentType) {
19907
+ const spec = findPartSpecByPartName(partName);
19908
+ if (!spec || !spec.legacyContentTypes.includes(actualContentType)) return null;
19909
+ return spec.contentType;
19910
+ }
19911
+
19518
19912
  // services/standalone-docx-plumbing.js
19519
19913
  var NS_W7 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
19520
19914
  var NS_CT = "http://schemas.openxmlformats.org/package/2006/content-types";
19521
19915
  var NS_RELS = "http://schemas.openxmlformats.org/package/2006/relationships";
19522
- var NUMBERING_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering";
19523
- var NUMBERING_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml";
19524
- var COMMENTS_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
19525
- var COMMENTS_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml";
19526
- var COMMENTS_EXTENDED_REL_TYPE = "http://schemas.microsoft.com/office/2011/relationships/commentsExtended";
19527
- var COMMENTS_EXTENDED_CONTENT_TYPE = "application/vnd.ms-word.commentsExtended+xml";
19528
- var DOCUMENT_PATH = "word/document.xml";
19529
- var NUMBERING_PATH = "word/numbering.xml";
19530
- var COMMENTS_PATH = "word/comments.xml";
19531
- var COMMENTS_EXTENDED_PATH = "word/commentsExtended.xml";
19916
+ var NUMBERING = getPartSpec("numbering");
19917
+ var COMMENTS = getPartSpec("comments");
19918
+ var COMMENTS_EXTENDED = getPartSpec("commentsExtended");
19919
+ var NUMBERING_REL_TYPE = NUMBERING.relType;
19920
+ var NUMBERING_CONTENT_TYPE = NUMBERING.contentType;
19921
+ var COMMENTS_REL_TYPE = COMMENTS.relType;
19922
+ var COMMENTS_CONTENT_TYPE = COMMENTS.contentType;
19923
+ var COMMENTS_EXTENDED_REL_TYPE = COMMENTS_EXTENDED.relType;
19924
+ var COMMENTS_EXTENDED_CONTENT_TYPE = COMMENTS_EXTENDED.contentType;
19925
+ var DOCUMENT_PATH = getPartSpec("document").path;
19926
+ var NUMBERING_PATH = NUMBERING.path;
19927
+ var COMMENTS_PATH = COMMENTS.path;
19928
+ var COMMENTS_EXTENDED_PATH = COMMENTS_EXTENDED.path;
19532
19929
  var CONTENT_TYPES_PATH = "[Content_Types].xml";
19533
19930
  var DOCUMENT_RELS_PATH = "word/_rels/document.xml.rels";
19534
19931
  function parseXmlStrictStandalone(xmlText, label = "xml") {
@@ -19816,6 +20213,22 @@ async function ensureCommentsExtendedArtifactsInZip(zip, commentsExtendedXml, op
19816
20213
  }
19817
20214
  }
19818
20215
  }
20216
+ async function repairKnownContentTypes(zip) {
20217
+ const ctText = await readZipText(zip, CONTENT_TYPES_PATH);
20218
+ if (!ctText) return [];
20219
+ const ctDoc = parseXmlStrictStandalone(ctText, CONTENT_TYPES_PATH);
20220
+ const repairs = [];
20221
+ for (const override of Array.from(ctDoc.getElementsByTagNameNS("*", "Override"))) {
20222
+ const partName = override.getAttribute("PartName") || "";
20223
+ const actual = override.getAttribute("ContentType") || "";
20224
+ const expected = correctedContentTypeFor(partName, actual);
20225
+ if (!expected) continue;
20226
+ override.setAttribute("ContentType", expected);
20227
+ repairs.push({ partName, from: actual, to: expected });
20228
+ }
20229
+ if (repairs.length > 0) zip.file(CONTENT_TYPES_PATH, createSerializer().serializeToString(ctDoc));
20230
+ return repairs;
20231
+ }
19819
20232
  async function validateDocxPackage(zip) {
19820
20233
  const documentXml = await readZipText(zip, DOCUMENT_PATH);
19821
20234
  if (!documentXml) {
@@ -19897,9 +20310,8 @@ async function validateDocxPackage(zip) {
19897
20310
  const paraIdToCommentId = /* @__PURE__ */ new Map();
19898
20311
  for (const comment of Array.from(commentsDoc.getElementsByTagNameNS("*", "comment"))) {
19899
20312
  const id = comment.getAttribute("w:id") || comment.getAttribute("id");
19900
- const paragraph = Array.from(comment.getElementsByTagNameNS("*", "p"))[0];
19901
- const paraId = paragraph?.getAttribute("w14:paraId") || paragraph?.getAttribute("paraId");
19902
- if (paraId) paraIdToCommentId.set(paraId.toUpperCase(), id);
20313
+ const paraId = commentParaId(comment);
20314
+ if (paraId) paraIdToCommentId.set(paraId, id);
19903
20315
  }
19904
20316
  const knownParaIds = new Set(paraIdToCommentId.keys());
19905
20317
  const extendedParaIds = /* @__PURE__ */ new Set();
@@ -19960,10 +20372,39 @@ async function validateDocxPackage(zip) {
19960
20372
  throw new Error("Validation failed: comments rel missing");
19961
20373
  }
19962
20374
  }
20375
+ {
20376
+ const relationshipById = /* @__PURE__ */ new Map();
20377
+ for (const rel of Array.from(relsDoc.getElementsByTagNameNS("*", "Relationship"))) {
20378
+ relationshipById.set(rel.getAttribute("Id") || "", { type: rel.getAttribute("Type") || "", target: rel.getAttribute("Target") || "" });
20379
+ }
20380
+ const headerSpec = getPartSpec("header");
20381
+ const footerSpec = getPartSpec("footer");
20382
+ const overrideTypeByPart = new Map(Array.from(ctDoc.getElementsByTagNameNS("*", "Override")).map((override) => [(override.getAttribute("PartName") || "").toLowerCase(), override.getAttribute("ContentType") || ""]));
20383
+ for (const reference of [
20384
+ ...Array.from(documentDoc.getElementsByTagNameNS(NS_W7, "headerReference")),
20385
+ ...Array.from(documentDoc.getElementsByTagNameNS(NS_W7, "footerReference"))
20386
+ ]) {
20387
+ const relId = reference.getAttributeNS?.("http://schemas.openxmlformats.org/officeDocument/2006/relationships", "id") || reference.getAttribute("r:id") || "";
20388
+ const spec = reference.localName === "headerReference" ? headerSpec : footerSpec;
20389
+ const relationship = relationshipById.get(relId);
20390
+ if (!relationship) throw new Error(`Validation failed: ${reference.localName} ${relId || "(no r:id)"} has no document relationship`);
20391
+ if (relationship.type !== spec.relType) throw new Error(`Validation failed: ${reference.localName} ${relId} points at a relationship of the wrong type (${relationship.type})`);
20392
+ const partName = relationship.target.startsWith("/") ? relationship.target : `/word/${relationship.target}`;
20393
+ const partXml = await readZipText(zip, partName.slice(1));
20394
+ if (!partXml) throw new Error(`Validation failed: ${reference.localName} ${relId} targets a missing part: ${partName}`);
20395
+ parseXmlStrictStandalone(partXml, partName.slice(1));
20396
+ const contentType = overrideTypeByPart.get(partName.toLowerCase());
20397
+ if (contentType !== spec.contentType) throw new Error(`Validation failed: ${partName} content type is ${contentType ? `'${contentType}'` : "missing"}; Word expects '${spec.contentType}'`);
20398
+ }
20399
+ }
19963
20400
  if (commentsExtendedXml) {
19964
- const hasContentType = Array.from(ctDoc.getElementsByTagNameNS("*", "Override")).some(
19965
- (override) => (override.getAttribute("PartName") || "").toLowerCase() === "/word/commentsextended.xml" && (override.getAttribute("ContentType") || "") === COMMENTS_EXTENDED_CONTENT_TYPE
20401
+ const extendedOverride = Array.from(ctDoc.getElementsByTagNameNS("*", "Override")).find(
20402
+ (override) => (override.getAttribute("PartName") || "").toLowerCase() === "/word/commentsextended.xml"
19966
20403
  );
20404
+ if (extendedOverride && extendedOverride.getAttribute("ContentType") !== COMMENTS_EXTENDED_CONTENT_TYPE) {
20405
+ throw new Error(`Validation failed: commentsExtended CT override has wrong content type '${extendedOverride.getAttribute("ContentType")}' (Word expects '${COMMENTS_EXTENDED_CONTENT_TYPE}')`);
20406
+ }
20407
+ const hasContentType = !!extendedOverride;
19967
20408
  const hasRelationship = Array.from(relsDoc.getElementsByTagNameNS("*", "Relationship")).some(
19968
20409
  (rel) => (rel.getAttribute("Type") || "") === COMMENTS_EXTENDED_REL_TYPE
19969
20410
  );
@@ -20327,6 +20768,156 @@ function reconcileReceiptsAgainstOutput(parts, receipts) {
20327
20768
  return { valid: true };
20328
20769
  }
20329
20770
 
20771
+ // services/headers-footers.js
20772
+ var NS_W8 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
20773
+ var NS_R2 = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
20774
+ var REVISION_ELEMENTS = /* @__PURE__ */ new Set(["ins", "del", "moveFrom", "moveTo", "rPrChange", "pPrChange", "sectPrChange", "tblPrChange", "tblGridChange", "tcPrChange", "trPrChange", "numberingChange"]);
20775
+ var REVISION_CONTENT_ELEMENTS = /* @__PURE__ */ new Set(["ins", "del", "moveFrom", "moveTo"]);
20776
+ var attrOf2 = (node, qualified, local) => node?.getAttribute?.(qualified) || node?.getAttribute?.(local) || "";
20777
+ function resolveTarget(target) {
20778
+ const clean = String(target || "");
20779
+ if (clean.startsWith("/")) return clean.replace(/^\/+/, "");
20780
+ const parts = ["word"];
20781
+ for (const segment of clean.split("/")) {
20782
+ if (segment === "..") parts.pop();
20783
+ else if (segment && segment !== ".") parts.push(segment);
20784
+ }
20785
+ return parts.join("/");
20786
+ }
20787
+ function discoverHeaderFooterParts({ documentXml, relsXml = null, settingsXml = null }) {
20788
+ if (!documentXml || !relsXml) return [];
20789
+ const documentParsed = parseOoxmlSafe(documentXml, "application/xml");
20790
+ const relsParsed = parseOoxmlSafe(relsXml, "application/xml");
20791
+ if (!documentParsed.doc || documentParsed.error || !relsParsed.doc || relsParsed.error) return [];
20792
+ const targetByRelId = /* @__PURE__ */ new Map();
20793
+ for (const rel of Array.from(relsParsed.doc.getElementsByTagNameNS("*", "Relationship"))) {
20794
+ targetByRelId.set(rel.getAttribute("Id") || "", rel.getAttribute("Target") || "");
20795
+ }
20796
+ const evenAndOdd = !!settingsXml && /<w:evenAndOddHeaders\b(?![^>]*w:val="(?:0|false|off)")/.test(settingsXml);
20797
+ const byPath = /* @__PURE__ */ new Map();
20798
+ const inForce = /* @__PURE__ */ new Map();
20799
+ const sectPrs = Array.from(documentParsed.doc.getElementsByTagNameNS(NS_W8, "sectPr"));
20800
+ sectPrs.forEach((sectPr, sectionIndex) => {
20801
+ const titlePg = Array.from(sectPr.childNodes || []).some((child) => child.localName === "titlePg" && !/^(0|false|off)$/i.test(attrOf2(child, "w:val", "val")));
20802
+ for (const ref of Array.from(sectPr.childNodes || [])) {
20803
+ if (ref.nodeType !== 1 || ref.localName !== "headerReference" && ref.localName !== "footerReference") continue;
20804
+ const relId = ref.getAttributeNS?.(NS_R2, "id") || ref.getAttribute("r:id") || "";
20805
+ const target = targetByRelId.get(relId);
20806
+ if (!target) continue;
20807
+ const path = resolveTarget(target);
20808
+ const kind = ref.localName === "headerReference" ? "header" : "footer";
20809
+ const type = attrOf2(ref, "w:type", "type") || "default";
20810
+ const entry = byPath.get(path) || { path, kind, type, sections: [], applies: [], titlePgSections: 0 };
20811
+ if (!entry.sections.includes(sectionIndex)) entry.sections.push(sectionIndex);
20812
+ if (titlePg) entry.titlePgSections += 1;
20813
+ byPath.set(path, entry);
20814
+ inForce.set(`${kind}:${type}`, path);
20815
+ }
20816
+ for (const path of inForce.values()) {
20817
+ const entry = byPath.get(path);
20818
+ if (entry && !entry.applies.includes(sectionIndex)) entry.applies.push(sectionIndex);
20819
+ }
20820
+ });
20821
+ return Array.from(byPath.values()).map((entry) => ({
20822
+ path: entry.path,
20823
+ kind: entry.kind,
20824
+ type: entry.type,
20825
+ sections: entry.sections,
20826
+ appliesToSections: entry.applies,
20827
+ sharedBySections: entry.applies.length > 1,
20828
+ // first-page parts only show with w:titlePg; even parts only with w:evenAndOddHeaders in settings.xml.
20829
+ active: entry.type === "first" ? entry.titlePgSections > 0 : entry.type === "even" ? evenAndOdd : true
20830
+ }));
20831
+ }
20832
+ function resolvePartSelector(parts, selector) {
20833
+ const describe = (part) => ({ path: part.path, kind: part.kind, type: part.type, sections: part.sections, appliesToSections: part.appliesToSections });
20834
+ if (typeof selector === "string") {
20835
+ const wanted = selector.replace(/^\/+/, "").toLowerCase();
20836
+ const match = parts.find((part) => part.path.toLowerCase() === wanted || part.path.toLowerCase() === `word/${wanted}`);
20837
+ if (match) return { part: match };
20838
+ return { error: { code: "PART_NOT_FOUND", message: `No header/footer part '${selector}' is referenced by the document.`, candidates: parts.map(describe) } };
20839
+ }
20840
+ if (!selector || typeof selector !== "object" || selector.kind !== "header" && selector.kind !== "footer") {
20841
+ return { error: { code: "INVALID_OPERATION", message: 'part must be a part path or { kind: "header"|"footer", type?: "default"|"first"|"even", section?: number }.' } };
20842
+ }
20843
+ const type = selector.type || "default";
20844
+ if (!["default", "first", "even"].includes(type)) {
20845
+ return { error: { code: "INVALID_OPERATION", message: `part.type must be default, first or even (got '${type}').` } };
20846
+ }
20847
+ let matches = parts.filter((part) => part.kind === selector.kind && part.type === type);
20848
+ if (Number.isInteger(selector.section)) matches = matches.filter((part) => part.appliesToSections.includes(selector.section));
20849
+ if (matches.length === 1) return { part: matches[0] };
20850
+ if (matches.length === 0) {
20851
+ 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) } };
20852
+ }
20853
+ return { error: { code: "PART_AMBIGUOUS", message: `Several ${type} ${selector.kind} parts exist; add "section" or use the part path.`, candidates: matches.map(describe) } };
20854
+ }
20855
+ function isHeaderFooterPath(path) {
20856
+ const spec = findPartSpecByPartName(path);
20857
+ return spec?.id === "header" || spec?.id === "footer";
20858
+ }
20859
+ function countRevisionsInsideFields(xml) {
20860
+ const parsed = parseOoxmlSafe(xml, "application/xml");
20861
+ if (!parsed.doc || parsed.error) return 0;
20862
+ let depth = 0;
20863
+ let count = 0;
20864
+ const visit = (node) => {
20865
+ for (const child of Array.from(node.childNodes || [])) {
20866
+ if (child.nodeType !== 1) continue;
20867
+ const name = child.localName;
20868
+ if (name === "fldChar") {
20869
+ const type = attrOf2(child, "w:fldCharType", "fldCharType");
20870
+ if (type === "begin") depth += 1;
20871
+ else if (type === "end") depth = Math.max(0, depth - 1);
20872
+ continue;
20873
+ }
20874
+ if (REVISION_CONTENT_ELEMENTS.has(name) && depth > 0) count += 1;
20875
+ if (name === "fldSimple") {
20876
+ depth += 1;
20877
+ visit(child);
20878
+ depth -= 1;
20879
+ continue;
20880
+ }
20881
+ visit(child);
20882
+ }
20883
+ };
20884
+ visit(parsed.doc.documentElement);
20885
+ return count;
20886
+ }
20887
+ function collectRevisionIds(xml) {
20888
+ const ids = /* @__PURE__ */ new Set();
20889
+ 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]);
20890
+ return ids;
20891
+ }
20892
+ function renumberCollidingRevisionIds(xml, taken) {
20893
+ const parsed = parseOoxmlSafe(xml, "application/xml");
20894
+ if (!parsed.doc || parsed.error) return { xml, renumbered: 0, idMap: /* @__PURE__ */ new Map() };
20895
+ const idMap = /* @__PURE__ */ new Map();
20896
+ let next = 1 + Math.max(0, ...Array.from(taken, (id) => Number(id)).filter(Number.isFinite));
20897
+ const used = new Set(taken);
20898
+ let renumbered = 0;
20899
+ const visit = (node) => {
20900
+ for (const child of Array.from(node.childNodes || [])) {
20901
+ if (child.nodeType !== 1) continue;
20902
+ if (REVISION_ELEMENTS.has(child.localName)) {
20903
+ const id = attrOf2(child, "w:id", "id");
20904
+ if (id && used.has(id)) {
20905
+ while (used.has(String(next))) next += 1;
20906
+ child.setAttributeNS(NS_W8, "w:id", String(next));
20907
+ idMap.set(id, String(next));
20908
+ used.add(String(next));
20909
+ renumbered += 1;
20910
+ } else if (id) {
20911
+ used.add(id);
20912
+ }
20913
+ }
20914
+ visit(child);
20915
+ }
20916
+ };
20917
+ visit(parsed.doc.documentElement);
20918
+ return renumbered > 0 ? { xml: createSerializer().serializeToString(parsed.doc), renumbered, idMap } : { xml, renumbered: 0, idMap };
20919
+ }
20920
+
20330
20921
  // services/error-recovery.js
20331
20922
  var ERROR_RECOVERY_VERSION = 1;
20332
20923
  var RULES = Object.freeze({
@@ -20364,6 +20955,12 @@ var RULES = Object.freeze({
20364
20955
  STRUCTURED_CONTENT_INVALID: ["request", "request_fixable", "change_request"],
20365
20956
  UNSUPPORTED_INSERTION_AFFINITY: ["request", "request_fixable", "change_request"],
20366
20957
  EXISTING_REVISIONS: ["target-safety", "policy_choice_required", "set_option"],
20958
+ PART_NOT_FOUND: ["target-resolution", "target_refresh_required", "reinspect"],
20959
+ PART_AMBIGUOUS: ["target-resolution", "candidate_selection_required", "choose_candidate"],
20960
+ COMMENT_IN_HEADER_FOOTER: ["request", "request_fixable", "change_request"],
20961
+ FIELD_EDIT_REFUSED: ["target-safety", "request_fixable", "change_request"],
20962
+ COMMENT_NOT_FOUND: ["target-resolution", "target_refresh_required", "reinspect"],
20963
+ PARENT_ANCHOR_NOT_FOUND: ["target-resolution", "target_refresh_required", "reinspect"],
20367
20964
  COMMENTED_CONTENT_MERGE: ["target-safety", "user_authorization_required", "resolve_comments"],
20368
20965
  COMMENTED_CONTENT_DELETE: ["target-safety", "user_authorization_required", "resolve_comments"],
20369
20966
  OVERLAPPING_SOURCE_TARGETS: ["batch-compilation", "source_conflict", "consolidate_operations"],
@@ -20816,6 +21413,7 @@ var SUPPORTED_OPERATION_TYPES = /* @__PURE__ */ new Set([
20816
21413
  "delete",
20817
21414
  "comment",
20818
21415
  "comment_reply",
21416
+ "comment_resolve",
20819
21417
  "highlight"
20820
21418
  ]);
20821
21419
  var EXISTING_REVISIONS_POLICIES = Object.freeze([
@@ -20834,11 +21432,14 @@ function isRecord(value) {
20834
21432
  function nonEmptyString(value) {
20835
21433
  return typeof value === "string" && value.trim().length > 0;
20836
21434
  }
21435
+ function isCommentThreadOperationKind(kind) {
21436
+ return kind === "comment_reply" || kind === "comment_resolve";
21437
+ }
20837
21438
  function getCanonicalOperationType(operation) {
20838
21439
  const type = operation?.type;
20839
21440
  if (type === "insert" && operation?.target?.revisionView === "rejected") return "rejected-insert";
20840
21441
  if (type === "restore") return "restore";
20841
- if (type === "comment" || type === "comment_reply" || type === "highlight") return type;
21442
+ if (type === "comment" || type === "comment_reply" || type === "comment_resolve" || type === "highlight") return type;
20842
21443
  if (type === "paragraph-format") return "paragraph-format";
20843
21444
  if (type === "character-format" || type === "format" && (operation?.textToFormat != null || operation?.properties != null)) return "format";
20844
21445
  return "redline";
@@ -20958,7 +21559,7 @@ function validateDocumentOperation(operation) {
20958
21559
  };
20959
21560
  }
20960
21561
  const target = normalized.targetDescriptor;
20961
- if (normalized.operationKind !== "comment_reply" && !nonEmptyString(target.text) && target.index == null && !target.paragraphId && !target.fingerprint && !target.captureRef) {
21562
+ if (!isCommentThreadOperationKind(normalized.operationKind) && !nonEmptyString(target.text) && target.index == null && !target.paragraphId && !target.fingerprint && !target.captureRef) {
20962
21563
  return {
20963
21564
  valid: false,
20964
21565
  error: {
@@ -21185,6 +21786,14 @@ function validateDocumentOperation(operation) {
21185
21786
  return { valid: false, error: { code: "INVALID_OPERATION", message: 'Comment reply operations require a non-empty "commentContent" field.' } };
21186
21787
  }
21187
21788
  }
21789
+ if (normalized.operationKind === "comment_resolve") {
21790
+ if (!nonEmptyString(String(normalized.commentId ?? ""))) {
21791
+ return { valid: false, error: { code: "INVALID_OPERATION", message: 'Comment resolve operations require a "commentId".' } };
21792
+ }
21793
+ if (normalized.resolved != null && typeof normalized.resolved !== "boolean") {
21794
+ return { valid: false, error: { code: "INVALID_OPERATION", message: 'Comment resolve "resolved" must be a boolean when provided.' } };
21795
+ }
21796
+ }
21188
21797
  if (normalized.operationKind === "highlight") {
21189
21798
  if (!nonEmptyString(normalized.textToHighlight)) {
21190
21799
  return {
@@ -21664,7 +22273,7 @@ function compileOperationBatch(xmlDoc, operations = [], options = {}) {
21664
22273
  const dynamicOccurrence = operation.targetDescriptor?.occurrence != null && !operation.targetDescriptor?.paragraphId && !operation.targetDescriptor?.fingerprint;
21665
22274
  const compositeTextTarget = typeof operation.targetDescriptor?.text === "string" && /\r|\n/.test(operation.targetDescriptor.text) && !operation.targetEndDescriptor;
21666
22275
  if (dynamicOccurrence || compositeTextTarget) binding.dynamic = true;
21667
- if (validation.valid && operation.operationKind !== "comment_reply" && !operation.targetDescriptor?.captureRef && !binding.dynamic) {
22276
+ if (validation.valid && operation.operationKind !== "comment_reply" && operation.operationKind !== "comment_resolve" && !operation.targetDescriptor?.captureRef && !binding.dynamic) {
21668
22277
  try {
21669
22278
  const start = resolveDescriptor(
21670
22279
  xmlDoc,
@@ -22165,12 +22774,12 @@ function resolveTargetFromCapture(xmlDoc, session, targetDescriptor, _opType = "
22165
22774
  }
22166
22775
 
22167
22776
  // services/document-operation-mutations.js
22168
- var NS_W8 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
22777
+ var NS_W9 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
22169
22778
  var NS_W142 = "http://schemas.microsoft.com/office/word/2010/wordml";
22170
22779
  function getCommentIdsInElement(element) {
22171
22780
  const ids = /* @__PURE__ */ new Set();
22172
22781
  for (const localName2 of ["commentRangeStart", "commentRangeEnd", "commentReference"]) {
22173
- for (const node of Array.from(element?.getElementsByTagNameNS?.(NS_W8, localName2) || [])) {
22782
+ for (const node of Array.from(element?.getElementsByTagNameNS?.(NS_W9, localName2) || [])) {
22174
22783
  const id = node.getAttribute("w:id") || node.getAttribute("id");
22175
22784
  if (id !== "") ids.add(id);
22176
22785
  }
@@ -22384,7 +22993,7 @@ function normalizeBodySectionOrder(xmlDoc) {
22384
22993
  normalizeBodySectionOrderStandalone(xmlDoc);
22385
22994
  }
22386
22995
  function removeProofErrNodes(paragraph) {
22387
- for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W8, "proofErr") || [])) {
22996
+ for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W9, "proofErr") || [])) {
22388
22997
  node.parentNode?.removeChild(node);
22389
22998
  }
22390
22999
  }
@@ -22395,12 +23004,12 @@ function preprocessRedlineTargetParagraph(targetParagraph) {
22395
23004
  function getDirectWordChild4(element, localName2) {
22396
23005
  if (!element) return null;
22397
23006
  return Array.from(element.childNodes || []).find(
22398
- (node) => node && node.nodeType === 1 && node.namespaceURI === NS_W8 && node.localName === localName2
23007
+ (node) => node && node.nodeType === 1 && node.namespaceURI === NS_W9 && node.localName === localName2
22399
23008
  ) || null;
22400
23009
  }
22401
23010
  function computeTableIndexInDocument(xmlDoc, tableElement) {
22402
23011
  if (!xmlDoc || !tableElement) return null;
22403
- const tables = Array.from(xmlDoc.getElementsByTagNameNS(NS_W8, "tbl"));
23012
+ const tables = Array.from(xmlDoc.getElementsByTagNameNS(NS_W9, "tbl"));
22404
23013
  const idx = tables.indexOf(tableElement);
22405
23014
  return idx >= 0 ? idx + 1 : null;
22406
23015
  }
@@ -22450,7 +23059,7 @@ function buildInsertedListParagraph(xmlDoc, anchorParagraph, entry, revisionMeta
22450
23059
  markParagraphMarkInserted(xmlDoc, paragraph, author);
22451
23060
  }
22452
23061
  const run = createWordElement(xmlDoc, "w:r");
22453
- const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W8, "r"))[0] || null;
23062
+ const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W9, "r"))[0] || null;
22454
23063
  const anchorRunPr = anchorFirstRun ? getDirectWordChild4(anchorFirstRun, "rPr") : null;
22455
23064
  if (anchorRunPr) {
22456
23065
  run.appendChild(clonePropertiesWithoutRevisionHistory(anchorRunPr));
@@ -22475,7 +23084,7 @@ function buildInsertedListParagraph(xmlDoc, anchorParagraph, entry, revisionMeta
22475
23084
  }
22476
23085
  function serializeParagraphRangeAsDocument(paragraphs, serializer) {
22477
23086
  const paragraphXml = (paragraphs || []).map((paragraph) => serializer.serializeToString(paragraph)).join("");
22478
- return `<w:document xmlns:w="${NS_W8}"><w:body>${paragraphXml}</w:body></w:document>`;
23087
+ return `<w:document xmlns:w="${NS_W9}"><w:body>${paragraphXml}</w:body></w:document>`;
22479
23088
  }
22480
23089
  function applyExplicitRangeListInsertions({
22481
23090
  xmlDoc,
@@ -22544,7 +23153,7 @@ function buildEmptyParagraphTemplateFromAnchor(xmlDoc, anchorParagraph) {
22544
23153
  const anchorPPr = getDirectWordChild4(anchorParagraph, "pPr");
22545
23154
  if (anchorPPr) paragraph.appendChild(clonePropertiesWithoutRevisionHistory(anchorPPr));
22546
23155
  const run = createWordElement(xmlDoc, "w:r");
22547
- const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W8, "r"))[0] || null;
23156
+ const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W9, "r"))[0] || null;
22548
23157
  const anchorRunPr = anchorFirstRun ? getDirectWordChild4(anchorFirstRun, "rPr") : null;
22549
23158
  if (anchorRunPr) run.appendChild(clonePropertiesWithoutRevisionHistory(anchorRunPr));
22550
23159
  const textNode = createWordElement(xmlDoc, "w:t");
@@ -22567,7 +23176,7 @@ function wrapParagraphContentInInsertion(xmlDoc, paragraph, revisionMetadata, au
22567
23176
  ins.setAttribute("w:author", metadata.author);
22568
23177
  ins.setAttribute("w:date", metadata.date);
22569
23178
  for (const child of Array.from(paragraph.childNodes || [])) {
22570
- if (child?.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName === "pPr") continue;
23179
+ if (child?.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName === "pPr") continue;
22571
23180
  ins.appendChild(child.cloneNode(true));
22572
23181
  }
22573
23182
  wrappedParagraph.appendChild(ins);
@@ -22591,7 +23200,7 @@ async function buildInsertedPlainParagraph(xmlDoc, anchorParagraph, text2, revis
22591
23200
  if (typeof markdownResult?.oxml === "string") {
22592
23201
  const extracted = extractReplacementNodes(markdownResult.oxml);
22593
23202
  sourceParagraph = (extracted.replacementNodes || []).find(
22594
- (node) => node && node.nodeType === 1 && node.namespaceURI === NS_W8 && node.localName === "p"
23203
+ (node) => node && node.nodeType === 1 && node.namespaceURI === NS_W9 && node.localName === "p"
22595
23204
  ) || null;
22596
23205
  }
22597
23206
  if (!sourceParagraph) {
@@ -22623,7 +23232,7 @@ function collectNumberingIdsFromNodes(nodes) {
22623
23232
  }
22624
23233
  function directDeletionCarrierText(carrier) {
22625
23234
  return Array.from(carrier?.childNodes || []).reduce((text2, child) => {
22626
- return text2 + (child?.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName === "r" ? getRunContentPieces(child).map((piece) => piece.text).join("") : "");
23235
+ return text2 + (child?.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName === "r" ? getRunContentPieces(child).map((piece) => piece.text).join("") : "");
22627
23236
  }, "");
22628
23237
  }
22629
23238
  function findOccurrenceOffset(text2, needle, occurrence) {
@@ -22665,9 +23274,9 @@ function hasUnsupportedDeletionSplitMarkup(paragraph, carrier) {
22665
23274
  }
22666
23275
  for (const child of Array.from(carrier?.childNodes || [])) {
22667
23276
  if (child.nodeType !== 1) continue;
22668
- if (!(child.namespaceURI === NS_W8 && child.localName === "r")) return true;
23277
+ if (!(child.namespaceURI === NS_W9 && child.localName === "r")) return true;
22669
23278
  for (const runChild of Array.from(child.childNodes || [])) {
22670
- if (runChild.nodeType !== 1 || runChild.namespaceURI !== NS_W8) continue;
23279
+ if (runChild.nodeType !== 1 || runChild.namespaceURI !== NS_W9) continue;
22671
23280
  if (!["rPr", "delText", "t", "tab", "br", "cr", "noBreakHyphen", "softHyphen"].includes(runChild.localName)) return true;
22672
23281
  }
22673
23282
  }
@@ -22676,7 +23285,7 @@ function hasUnsupportedDeletionSplitMarkup(paragraph, carrier) {
22676
23285
  function insertedRunPropertiesAtDeletionOffset(carrier, localOffset) {
22677
23286
  let offset = 0;
22678
23287
  for (const child of Array.from(carrier?.childNodes || [])) {
22679
- if (!(child?.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName === "r")) continue;
23288
+ if (!(child?.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName === "r")) continue;
22680
23289
  const length = getRunTextLength(getRunContentPieces(child));
22681
23290
  if (localOffset <= offset + length) {
22682
23291
  return clonePropertiesWithoutRevisionHistory(getDirectWordChild4(child, "rPr"));
@@ -22706,7 +23315,7 @@ function normalizedAuthor2(author) {
22706
23315
  function directWordParagraphSibling(paragraph, direction = "nextSibling") {
22707
23316
  let cursor = paragraph?.[direction] || null;
22708
23317
  while (cursor) {
22709
- if (cursor.nodeType === 1 && cursor.namespaceURI === NS_W8 && cursor.localName === "p") return cursor;
23318
+ if (cursor.nodeType === 1 && cursor.namespaceURI === NS_W9 && cursor.localName === "p") return cursor;
22710
23319
  cursor = cursor[direction] || null;
22711
23320
  }
22712
23321
  return null;
@@ -22716,7 +23325,7 @@ function createSanitizedRestorationPPr(xmlDoc, sourceParagraph) {
22716
23325
  const pPr = createWordElement(xmlDoc, "w:pPr");
22717
23326
  if (!sourcePPr) return pPr;
22718
23327
  for (const child of Array.from(sourcePPr.childNodes || [])) {
22719
- if (child.nodeType !== 1 || child.namespaceURI !== NS_W8) continue;
23328
+ if (child.nodeType !== 1 || child.namespaceURI !== NS_W9) continue;
22720
23329
  if (!RESTORATION_PPR_ALLOWLIST.has(child.localName)) continue;
22721
23330
  pPr.appendChild(xmlDoc.importNode(child, true));
22722
23331
  }
@@ -22725,14 +23334,14 @@ function createSanitizedRestorationPPr(xmlDoc, sourceParagraph) {
22725
23334
  function collectRestorationAnchorWarnings(sourceParagraph) {
22726
23335
  const warnings = [];
22727
23336
  const bookmarkNames = /* @__PURE__ */ new Set();
22728
- for (const node of Array.from(sourceParagraph?.getElementsByTagNameNS?.(NS_W8, "bookmarkStart") || [])) {
23337
+ for (const node of Array.from(sourceParagraph?.getElementsByTagNameNS?.(NS_W9, "bookmarkStart") || [])) {
22729
23338
  const name = wordAttribute4(node, "name") || "(unnamed)";
22730
23339
  bookmarkNames.add(name);
22731
23340
  }
22732
23341
  for (const name of bookmarkNames) warnings.push(`RESTORATION_DROPPED_BOOKMARK:${name}`);
22733
23342
  const commentIds = /* @__PURE__ */ new Set();
22734
23343
  for (const localName2 of ["commentRangeStart", "commentRangeEnd", "commentReference"]) {
22735
- for (const node of Array.from(sourceParagraph?.getElementsByTagNameNS?.(NS_W8, localName2) || [])) {
23344
+ for (const node of Array.from(sourceParagraph?.getElementsByTagNameNS?.(NS_W9, localName2) || [])) {
22736
23345
  const id = wordAttribute4(node, "id");
22737
23346
  if (id !== "") commentIds.add(id);
22738
23347
  }
@@ -22749,24 +23358,24 @@ function removeRestorationAnchors(paragraph) {
22749
23358
  "commentReference"
22750
23359
  ];
22751
23360
  for (const localName2 of anchorNames) {
22752
- for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W8, localName2) || [])) {
23361
+ for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W9, localName2) || [])) {
22753
23362
  node.parentNode?.removeChild(node);
22754
23363
  }
22755
23364
  }
22756
- for (const run of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W8, "r") || [])) {
22757
- const meaningful = Array.from(run.childNodes || []).some((child) => child.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName !== "rPr");
23365
+ for (const run of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W9, "r") || [])) {
23366
+ const meaningful = Array.from(run.childNodes || []).some((child) => child.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName !== "rPr");
22758
23367
  if (!meaningful) run.parentNode?.removeChild(run);
22759
23368
  }
22760
23369
  }
22761
23370
  function buildRejectedRestorationTemplate(xmlDoc, sourceParagraph, serializer) {
22762
23371
  const rejected = rejectTrackedChangesInOoxml(serializer.serializeToString(sourceParagraph), { allAuthors: true });
22763
23372
  const parsed = parseOoxmlSafe(rejected.oxml, "application/xml");
22764
- const rejectedParagraph = parsed.doc?.getElementsByTagNameNS?.(NS_W8, "p")?.[0] || null;
23373
+ const rejectedParagraph = parsed.doc?.getElementsByTagNameNS?.(NS_W9, "p")?.[0] || null;
22765
23374
  if (!rejectedParagraph) return null;
22766
23375
  const paragraph = createWordElement(xmlDoc, "w:p");
22767
23376
  paragraph.appendChild(createSanitizedRestorationPPr(xmlDoc, sourceParagraph));
22768
23377
  for (const child of Array.from(rejectedParagraph.childNodes || [])) {
22769
- if (child.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName === "pPr") continue;
23378
+ if (child.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName === "pPr") continue;
22770
23379
  paragraph.appendChild(xmlDoc.importNode(child, true));
22771
23380
  }
22772
23381
  removeRestorationAnchors(paragraph);
@@ -22787,7 +23396,7 @@ async function editRestorationTemplate(xmlDoc, template, modifiedText, author, s
22787
23396
  );
22788
23397
  if (result?.status === "error" || typeof result?.oxml !== "string") return null;
22789
23398
  const extracted = extractReplacementNodes(result.oxml);
22790
- const paragraph = (extracted.replacementNodes || []).find((node) => node?.nodeType === 1 && node.namespaceURI === NS_W8 && node.localName === "p");
23399
+ const paragraph = (extracted.replacementNodes || []).find((node) => node?.nodeType === 1 && node.namespaceURI === NS_W9 && node.localName === "p");
22791
23400
  return paragraph ? xmlDoc.importNode(paragraph, true) : null;
22792
23401
  }
22793
23402
  function allocateFreshParagraphId(xmlDoc, operationSession) {
@@ -22958,7 +23567,7 @@ async function insertIntoRejectedDeletedText(documentXml, targetText, anchor, mo
22958
23567
  }
22959
23568
  const insertionOffset = anchorStart + anchor.offset;
22960
23569
  const carriers = Array.from(paragraph.childNodes || []).filter(
22961
- (node) => node?.nodeType === 1 && node.namespaceURI === NS_W8 && node.localName === "del"
23570
+ (node) => node?.nodeType === 1 && node.namespaceURI === NS_W9 && node.localName === "del"
22962
23571
  );
22963
23572
  if (carriers.some((carrier) => hasUnsupportedDeletionSplitMarkup(paragraph, carrier))) {
22964
23573
  return {
@@ -23615,7 +24224,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
23615
24224
  };
23616
24225
  }
23617
24226
  if (!allSame) {
23618
- const hasMove = targetParagraph.getElementsByTagNameNS(NS_W8, "moveFrom").length > 0 || targetParagraph.getElementsByTagNameNS(NS_W8, "moveTo").length > 0;
24227
+ const hasMove = targetParagraph.getElementsByTagNameNS(NS_W9, "moveFrom").length > 0 || targetParagraph.getElementsByTagNameNS(NS_W9, "moveTo").length > 0;
23619
24228
  if (hasMove) {
23620
24229
  return {
23621
24230
  documentXml,
@@ -23630,8 +24239,8 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
23630
24239
  }
23631
24240
  }
23632
24241
  } else {
23633
- const hasDel = targetParagraph.getElementsByTagNameNS(NS_W8, "del").length > 0;
23634
- const hasMove = targetParagraph.getElementsByTagNameNS(NS_W8, "moveFrom").length > 0 || targetParagraph.getElementsByTagNameNS(NS_W8, "moveTo").length > 0;
24242
+ const hasDel = targetParagraph.getElementsByTagNameNS(NS_W9, "del").length > 0;
24243
+ const hasMove = targetParagraph.getElementsByTagNameNS(NS_W9, "moveFrom").length > 0 || targetParagraph.getElementsByTagNameNS(NS_W9, "moveTo").length > 0;
23635
24244
  if (hasDel) {
23636
24245
  return {
23637
24246
  documentXml,
@@ -24150,7 +24759,7 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
24150
24759
  const targetParagraph = resolved.paragraph;
24151
24760
  const collectVisibleRuns = () => {
24152
24761
  const runEntries2 = [];
24153
- const allRuns = Array.from(targetParagraph.getElementsByTagNameNS(NS_W8, "r"));
24762
+ const allRuns = Array.from(targetParagraph.getElementsByTagNameNS(NS_W9, "r"));
24154
24763
  for (const run of allRuns) {
24155
24764
  let parent = run.parentNode;
24156
24765
  let isDel = false;
@@ -24242,7 +24851,7 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
24242
24851
  const targetRuns = finalRuns.filter((span) => span.start >= matchStart && span.end <= matchEnd);
24243
24852
  let anyChanged = false;
24244
24853
  for (const entry of targetRuns) {
24245
- const rPr = entry.run.getElementsByTagNameNS(NS_W8, "rPr")[0] || null;
24854
+ const rPr = entry.run.getElementsByTagNameNS(NS_W9, "rPr")[0] || null;
24246
24855
  if (checkRunPropertiesChanged(rPr, properties)) {
24247
24856
  anyChanged = true;
24248
24857
  break;
@@ -24253,7 +24862,7 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
24253
24862
  }
24254
24863
  for (const entry of targetRuns) {
24255
24864
  const run = entry.run;
24256
- let rPr = run.getElementsByTagNameNS(NS_W8, "rPr")[0];
24865
+ let rPr = run.getElementsByTagNameNS(NS_W9, "rPr")[0];
24257
24866
  if (!rPr) {
24258
24867
  rPr = createWordElement(xmlDoc, "w:rPr");
24259
24868
  run.insertBefore(rPr, run.firstChild);
@@ -24262,7 +24871,7 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
24262
24871
  let insideSameAuthorIns = false;
24263
24872
  while (insAncestor && insAncestor !== targetParagraph) {
24264
24873
  if (insAncestor.nodeType === 1 && (insAncestor.localName === "ins" || insAncestor.nodeName === "w:ins")) {
24265
- const insAuthor = insAncestor.getAttribute("w:author") || insAncestor.getAttributeNS(NS_W8, "author");
24874
+ const insAuthor = insAncestor.getAttribute("w:author") || insAncestor.getAttributeNS(NS_W9, "author");
24266
24875
  if (insAuthor === (author || getDefaultAuthor())) {
24267
24876
  insideSameAuthorIns = true;
24268
24877
  }
@@ -24310,7 +24919,7 @@ async function applyParagraphFormatToParagraphByExactText(documentXml, targetTex
24310
24919
  return { documentXml, hasChanges: false, status: "error", error: resolved?.error || { code: "TARGET_NOT_FOUND", message: "Target paragraph not found." } };
24311
24920
  }
24312
24921
  const targetParagraph = resolved.paragraph;
24313
- let pPr = targetParagraph.getElementsByTagNameNS(NS_W8, "pPr")[0];
24922
+ let pPr = targetParagraph.getElementsByTagNameNS(NS_W9, "pPr")[0];
24314
24923
  if (!pPr) {
24315
24924
  pPr = createWordElement(xmlDoc, "w:pPr");
24316
24925
  targetParagraph.insertBefore(pPr, targetParagraph.firstChild);
@@ -24358,7 +24967,7 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
24358
24967
  }
24359
24968
  let operation = validation.operation || normalizeDocumentOperation(op);
24360
24969
  const authorUsed = resolveDocumentOperationAuthor(operation, author, getDefaultAuthor());
24361
- if (operation.operationKind !== "comment_reply" && operation.operationKind !== "rejected-insert" && operation.operationKind !== "restore" && (operation.targetDescriptor?.revisionView === "rejected" || operation.targetEndDescriptor?.revisionView === "rejected")) {
24970
+ if (operation.operationKind !== "comment_reply" && operation.operationKind !== "comment_resolve" && operation.operationKind !== "rejected-insert" && operation.operationKind !== "restore" && (operation.targetDescriptor?.revisionView === "rejected" || operation.targetEndDescriptor?.revisionView === "rejected")) {
24362
24971
  return {
24363
24972
  documentXml,
24364
24973
  hasChanges: false,
@@ -24525,6 +25134,7 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
24525
25134
  result = applyCommentReplyToParts({
24526
25135
  commentsXml: existingCommentsXml,
24527
25136
  commentsExtendedXml: existingExtendedXml,
25137
+ documentXml,
24528
25138
  parentCommentId: operation.parentCommentId,
24529
25139
  commentId,
24530
25140
  commentContent: operation.commentContent,
@@ -24532,8 +25142,30 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
24532
25142
  date: operation.date || (/* @__PURE__ */ new Date()).toISOString()
24533
25143
  });
24534
25144
  result.documentXml = documentXml;
24535
- if (result.hasChanges && session.receiptCollector) session.receiptCollector.recordComment(commentId);
25145
+ if (result.status !== "error" && result.hasChanges) {
25146
+ if (!session.document || !anchorReplyInDocument(session.document, result.threadCommentIds, commentId)) {
25147
+ 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.` } };
25148
+ } else if (session.receiptCollector) {
25149
+ session.receiptCollector.recordComment(commentId);
25150
+ }
25151
+ }
24536
25152
  }
25153
+ } else if (operation.operationKind === "comment_resolve") {
25154
+ const resolution = applyThreadResolutionToParts({
25155
+ commentsXml: session.commentsXml || runtimeContext?.commentsXml || options.commentsXml,
25156
+ commentsExtendedXml: session.commentsExtendedXml || runtimeContext?.commentsExtendedXml || options.commentsExtendedXml,
25157
+ commentId: operation.commentId,
25158
+ resolved: operation.resolved !== false
25159
+ });
25160
+ result = resolution.status === "error" ? { documentXml, hasChanges: false, status: "error", error: resolution.error } : {
25161
+ documentXml,
25162
+ hasChanges: resolution.hasChanges,
25163
+ ...resolution.commentsXml ? { commentsXml: resolution.commentsXml, commentsXmlMode: "replace" } : {},
25164
+ ...resolution.commentsExtendedXml ? { commentsExtendedXml: resolution.commentsExtendedXml, commentsExtendedXmlMode: "replace" } : {},
25165
+ resolved: resolution.resolved,
25166
+ threadRootId: resolution.threadRootId,
25167
+ commentIds: resolution.commentIds
25168
+ };
24537
25169
  } else if (operation.operationKind === "highlight") {
24538
25170
  result = await applyHighlightToParagraphByExactText(
24539
25171
  documentXml,
@@ -24674,7 +25306,7 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
24674
25306
  ...resolutionCapture
24675
25307
  };
24676
25308
  }
24677
- session.markMutationCommitted(operation.operationKind !== "comment_reply");
25309
+ session.markMutationCommitted(true);
24678
25310
  if (operation._compiledSourceId && session.sourceTargetRegistry) {
24679
25311
  session.sourceTargetRegistry.commitMutation(
24680
25312
  operation._compiledSourceId,
@@ -24787,9 +25419,9 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
24787
25419
  }
24788
25420
 
24789
25421
  // services/batch-operation-orchestrator.js
24790
- var NS_W9 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
25422
+ var NS_W10 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
24791
25423
  function operationTargetPriority(op) {
24792
- return op?.type === "comment" || op?.type === "comment_reply" ? 0 : 1;
25424
+ return op?.type === "comment" || op?.type === "comment_reply" || op?.type === "comment_resolve" ? 0 : 1;
24793
25425
  }
24794
25426
  function buildOperationDependencyPlan(operations = []) {
24795
25427
  const list = Array.isArray(operations) ? operations : [];
@@ -24864,7 +25496,7 @@ function buildOperationDependencyPlan(operations = []) {
24864
25496
  inDegrees[i2]++;
24865
25497
  }
24866
25498
  const kind = normalizeDocumentOperation(op).operationKind;
24867
- if (kind !== "comment" && kind !== "comment_reply") {
25499
+ if (kind !== "comment" && kind !== "comment_reply" && kind !== "comment_resolve") {
24868
25500
  if (!mutatingCaptureConsumers.has(ref)) mutatingCaptureConsumers.set(ref, []);
24869
25501
  mutatingCaptureConsumers.get(ref).push({
24870
25502
  index: i2,
@@ -24932,9 +25564,9 @@ function mergeCommentsXml(existingXml, incomingXml) {
24932
25564
  const serializer = createSerializer();
24933
25565
  const existingRoot = existingDoc.documentElement;
24934
25566
  const existingIds = new Set(
24935
- Array.from(existingDoc.getElementsByTagNameNS(NS_W9, "comment")).map((node) => node.getAttribute("w:id") || node.getAttribute("id")).filter(Boolean)
25567
+ Array.from(existingDoc.getElementsByTagNameNS(NS_W10, "comment")).map((node) => node.getAttribute("w:id") || node.getAttribute("id")).filter(Boolean)
24936
25568
  );
24937
- for (const comment of Array.from(incomingDoc.getElementsByTagNameNS(NS_W9, "comment"))) {
25569
+ for (const comment of Array.from(incomingDoc.getElementsByTagNameNS(NS_W10, "comment"))) {
24938
25570
  const id = comment.getAttribute("w:id") || comment.getAttribute("id");
24939
25571
  if (id && existingIds.has(id)) continue;
24940
25572
  existingRoot.appendChild(existingDoc.importNode(comment, true));
@@ -25095,7 +25727,7 @@ async function applyOperationsToDocumentXml(documentXml, operations, author, run
25095
25727
  context.targetRefSnapshot = session.initialTargetReferenceSnapshot;
25096
25728
  }
25097
25729
  session.runtimeContext = context;
25098
- if (sourceOperations.some((op) => op?.type === "comment_reply")) {
25730
+ if (sourceOperations.some((op) => op?.type === "comment_reply" || op?.type === "comment_resolve")) {
25099
25731
  session.commentsXml = context.commentsXml || options.commentsXml || null;
25100
25732
  session.commentsExtendedXml = context.commentsExtendedXml || options.commentsExtendedXml || null;
25101
25733
  session.commentsXmlMode = "replace";
@@ -25168,6 +25800,7 @@ async function applyOperationsToDocumentXml(documentXml, operations, author, run
25168
25800
  ...result.resolvedTarget ? { resolvedTarget: result.resolvedTarget } : {},
25169
25801
  ...result.resolvedAnchor ? { resolvedAnchor: result.resolvedAnchor } : {},
25170
25802
  ...result.change ? { change: result.change } : {},
25803
+ ...result.operationType === "comment_resolve" && !isError ? { resolved: result.resolved, threadRootId: result.threadRootId, commentIds: result.commentIds } : {},
25171
25804
  ...Array.isArray(result.warnings) && result.warnings.length > 0 ? { warnings: result.warnings } : {},
25172
25805
  ...result.error ? { error: result.error } : {},
25173
25806
  ...result.receipt ? { receipt: result.receipt } : {}
@@ -25444,6 +26077,26 @@ function preflightOperations(documentXml, operations, author, options = {}) {
25444
26077
  }
25445
26078
  commentsRequired = commentsRequired || operation.operationKind === "comment" || operation.operationKind === "comment_reply";
25446
26079
  numberingRequired = numberingRequired || operationNeedsNumbering(operation);
26080
+ if (operation.operationKind === "comment_resolve") {
26081
+ const commentId = String(operation.commentId);
26082
+ results.push(options._existingCommentDetails?.[commentId] ? {
26083
+ index: index + 1,
26084
+ type: sourceOperation.type,
26085
+ operationType: operation.operationKind,
26086
+ status: "ready",
26087
+ authorUsed,
26088
+ resolvedBy: "comment_id",
26089
+ commentId
26090
+ } : {
26091
+ index: index + 1,
26092
+ type: sourceOperation.type,
26093
+ operationType: operation.operationKind,
26094
+ status: "error",
26095
+ authorUsed,
26096
+ error: { code: "COMMENT_NOT_FOUND", message: `Comment '${commentId}' was not found.` }
26097
+ });
26098
+ continue;
26099
+ }
25447
26100
  if (operation.operationKind === "comment_reply") {
25448
26101
  const parentId = String(operation.parentCommentId);
25449
26102
  const parent = options._existingCommentDetails?.[parentId];
@@ -26595,6 +27248,15 @@ var text = (entries, path) => {
26595
27248
  if (typeof entry === "string") return entry;
26596
27249
  return textDecoder2.decode(entry);
26597
27250
  };
27251
+ function reconcileCommentSiblings(working) {
27252
+ const updated = reconcileCommentSiblingParts({
27253
+ commentsXml: text(working, "word/comments.xml"),
27254
+ commentsIdsXml: text(working, "word/commentsIds.xml"),
27255
+ commentsExtensibleXml: text(working, "word/commentsExtensible.xml")
27256
+ });
27257
+ if (updated.commentsIdsXml) working.set("word/commentsIds.xml", textEncoder4.encode(updated.commentsIdsXml));
27258
+ if (updated.commentsExtensibleXml) working.set("word/commentsExtensible.xml", textEncoder4.encode(updated.commentsExtensibleXml));
27259
+ }
26598
27260
  var cloneEntries = (entries) => new Map([...entries].map(([name, data]) => [name, new Uint8Array(data)]));
26599
27261
  function toBufferCompatible(uint8Arr) {
26600
27262
  if (typeof Buffer !== "undefined" && typeof Buffer.from === "function") {
@@ -26662,6 +27324,7 @@ function computePackageRevisionToken(input) {
26662
27324
  entries
26663
27325
  });
26664
27326
  }
27327
+ var COMMENT_OPERATION_TYPES = /* @__PURE__ */ new Set(["comment", "comment_reply", "comment_resolve"]);
26665
27328
  function nextCommentId(entries) {
26666
27329
  const ids = `${text(entries, "word/document.xml") || ""} ${text(entries, "word/comments.xml") || ""}`.match(/(?:w:)?id=["'](\d+)["']/g) || [];
26667
27330
  let next = ids.reduce((max2, token) => Math.max(max2, Number(token.match(/\d+/)?.[0] || 0)), 0) + 1;
@@ -26680,7 +27343,7 @@ function existingCommentDetails(entries) {
26680
27343
  id,
26681
27344
  author: comment.getAttribute("w:author") || comment.getAttribute("author") || "",
26682
27345
  text: String(comment.textContent || "").trim(),
26683
- paraId: Array.from(comment.getElementsByTagNameNS("*", "p"))[0]?.getAttribute("w14:paraId") || null
27346
+ paraId: commentParaId(comment) || null
26684
27347
  };
26685
27348
  }
26686
27349
  return details;
@@ -26711,13 +27374,34 @@ var DocxDocument = class {
26711
27374
  }
26712
27375
  inspect(options = {}) {
26713
27376
  const digestFn = options.digestFn || ((bytes) => sha256(bytes));
26714
- return inspectDocumentParts({
27377
+ const inspection = inspectDocumentParts({
26715
27378
  documentXml: text(this.entries, "word/document.xml"),
26716
27379
  commentsXml: text(this.entries, "word/comments.xml"),
26717
27380
  commentsExtendedXml: text(this.entries, "word/commentsExtended.xml"),
26718
27381
  numberingXml: text(this.entries, "word/numbering.xml"),
26719
27382
  stylesXml: text(this.entries, "word/styles.xml")
26720
27383
  }, { ...options, digestFn });
27384
+ if (inspection.status === "error") return inspection;
27385
+ return { ...inspection, headersFooters: this.inspectHeadersFooters() };
27386
+ }
27387
+ /**
27388
+ * Header and footer parts the document uses, with their paragraph text, so operations can target them with
27389
+ * `part`. `hasFields` marks parts containing PAGE/NUMPAGES-style fields (edit around them, not inside).
27390
+ */
27391
+ inspectHeadersFooters() {
27392
+ return discoverHeaderFooterParts({
27393
+ documentXml: text(this.entries, "word/document.xml"),
27394
+ relsXml: text(this.entries, "word/_rels/document.xml.rels"),
27395
+ settingsXml: text(this.entries, "word/settings.xml")
27396
+ }).map((part) => {
27397
+ const xml = text(this.entries, part.path) || "";
27398
+ const partInspection = xml ? inspectDocumentParts({ documentXml: xml }) : { paragraphs: [] };
27399
+ return {
27400
+ ...part,
27401
+ hasFields: /<w:fldChar\b|<w:fldSimple\b/.test(xml),
27402
+ paragraphs: (partInspection.paragraphs || []).map((paragraph) => ({ index: paragraph.index, text: paragraph.text }))
27403
+ };
27404
+ });
26721
27405
  }
26722
27406
  getRevisionToken() {
26723
27407
  return computePackageRevisionToken(this.entries);
@@ -26726,12 +27410,51 @@ var DocxDocument = class {
26726
27410
  return this.getRevisionToken();
26727
27411
  }
26728
27412
  preflight(operations, author = getDefaultAuthor(), options = {}) {
26729
- return preflightOperations(
27413
+ const list = Array.isArray(operations) ? operations : [];
27414
+ const runBody = (ops) => preflightOperations(
26730
27415
  text(this.entries, "word/document.xml"),
26731
- operations,
27416
+ ops,
26732
27417
  author || getDefaultAuthor(),
26733
27418
  { ...options, _existingCommentDetails: existingCommentDetails(this.entries) }
26734
27419
  );
27420
+ if (!list.some((operation) => operation && typeof operation === "object" && operation.part != null)) return runBody(operations);
27421
+ const parts = this.inspectHeadersFooters();
27422
+ const bodyOps = [];
27423
+ const results = [];
27424
+ const conflicts = [];
27425
+ const authorsUsed = /* @__PURE__ */ new Set();
27426
+ let valid = true;
27427
+ list.forEach((operation, i2) => {
27428
+ if (!operation || typeof operation !== "object" || operation.part == null) {
27429
+ bodyOps.push({ operation, origIndex: i2 });
27430
+ return;
27431
+ }
27432
+ const resolved = resolvePartSelector(parts, operation.part);
27433
+ 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;
27434
+ if (refusal) {
27435
+ valid = false;
27436
+ results.push({ index: i2 + 1, type: operation.type || "redline", status: "error", error: normalizeErrorWithRecovery({ ...refusal, operationIndex: i2 + 1 }) });
27437
+ return;
27438
+ }
27439
+ const { part: _selector, ...rest } = operation;
27440
+ const partResult = preflightOperations(text(this.entries, resolved.part.path), [rest], author || getDefaultAuthor(), { ...options, _existingCommentDetails: {} });
27441
+ valid = valid && partResult.valid;
27442
+ for (const result of partResult.results || []) results.push({ ...result, index: i2 + 1, part: resolved.part.path });
27443
+ conflicts.push(...partResult.conflicts || []);
27444
+ for (const name of partResult.authorsUsed || []) authorsUsed.add(name);
27445
+ });
27446
+ let requiredArtifacts = { comments: false, numbering: false };
27447
+ if (bodyOps.length) {
27448
+ const body = runBody(bodyOps.map((item) => item.operation));
27449
+ valid = valid && body.valid;
27450
+ for (const result of body.results || []) results.push({ ...result, index: bodyOps[result.index - 1].origIndex + 1 });
27451
+ conflicts.push(...body.conflicts || []);
27452
+ for (const name of body.authorsUsed || []) authorsUsed.add(name);
27453
+ requiredArtifacts = body.requiredArtifacts || requiredArtifacts;
27454
+ }
27455
+ results.sort((a, b) => a.index - b.index);
27456
+ const ok = valid && conflicts.length === 0;
27457
+ return { valid: ok, status: ok ? "ok" : "error", results, conflicts, authorsUsed: [...authorsUsed], requiredArtifacts };
26735
27458
  }
26736
27459
  toUint8Array() {
26737
27460
  return zipDocx(this.entries);
@@ -26739,7 +27462,240 @@ var DocxDocument = class {
26739
27462
  toBuffer() {
26740
27463
  return toBufferCompatible(this.toUint8Array());
26741
27464
  }
27465
+ /**
27466
+ * Applies operations to the document. Operations without `part` target the body (word/document.xml).
27467
+ * An operation with `part` targets one header or footer part:
27468
+ * `{ part: { kind: 'footer', type: 'default', section: 0 } | 'word/footer2.xml', target: { exactText }, ... }`.
27469
+ * Discover parts with `inspect().headersFooters`.
27470
+ */
26742
27471
  async applyOperations(operations, options = {}) {
27472
+ const list = Array.isArray(operations) ? operations : [];
27473
+ if (!list.some((operation) => operation && typeof operation === "object" && operation.part != null)) {
27474
+ return this.applyBodyOperations(operations, options);
27475
+ }
27476
+ return this.applyOperationsWithParts(list, options);
27477
+ }
27478
+ async applyOperationsWithParts(operations, options = {}) {
27479
+ const author = options.author || getDefaultAuthor();
27480
+ const atomic = options.atomic === true;
27481
+ const snapshotEntries = this.entries;
27482
+ const snapshotBytes = this.originalBytes;
27483
+ const total = operations.length;
27484
+ const restore = () => {
27485
+ this.entries = snapshotEntries;
27486
+ this.originalBytes = snapshotBytes;
27487
+ };
27488
+ const original = () => ({
27489
+ uint8Array: snapshotBytes,
27490
+ toUint8Array: () => new Uint8Array(snapshotBytes),
27491
+ buffer: toBufferCompatible(snapshotBytes),
27492
+ toBuffer: () => toBufferCompatible(snapshotBytes)
27493
+ });
27494
+ const refuse = (error2, results2 = [], receipts2 = [], extra = {}) => ({
27495
+ status: "error",
27496
+ hasChanges: false,
27497
+ written: false,
27498
+ rolledBack: true,
27499
+ results: results2,
27500
+ receipts: receipts2,
27501
+ executionOrder: [],
27502
+ authorsUsed: [],
27503
+ artifactsChanged: [],
27504
+ error: normalizeErrorWithRecovery(error2),
27505
+ retryPlan: createRetryPlan({ atomic: true, rolledBack: true, results: results2, receipts: receipts2, operationCount: total }),
27506
+ validation: { originalIssues: [], generatedIssues: [] },
27507
+ ...extra,
27508
+ ...original()
27509
+ });
27510
+ if (options.expectedRevision) {
27511
+ const precondition = await this.applyBodyOperations([], { expectedRevision: options.expectedRevision });
27512
+ if (precondition.status === "error") return precondition;
27513
+ }
27514
+ const parts = discoverHeaderFooterParts({
27515
+ documentXml: text(this.entries, "word/document.xml"),
27516
+ relsXml: text(this.entries, "word/_rels/document.xml.rels"),
27517
+ settingsXml: text(this.entries, "word/settings.xml")
27518
+ });
27519
+ const bodyOps = [];
27520
+ const partOps = [];
27521
+ for (let i2 = 0; i2 < operations.length; i2++) {
27522
+ const operation = operations[i2];
27523
+ if (!operation || typeof operation !== "object" || operation.part == null) {
27524
+ bodyOps.push({ operation, origIndex: i2 });
27525
+ continue;
27526
+ }
27527
+ if (COMMENT_OPERATION_TYPES.has(operation.type)) {
27528
+ return refuse({ code: "COMMENT_IN_HEADER_FOOTER", message: `Operation ${i2 + 1}: Word does not support comments in headers or footers.`, operationIndex: i2 + 1 });
27529
+ }
27530
+ const resolved = resolvePartSelector(parts, operation.part);
27531
+ if (resolved.error) return refuse({ ...resolved.error, message: `Operation ${i2 + 1}: ${resolved.error.message}`, operationIndex: i2 + 1 });
27532
+ const { part: _selector, ...rest } = operation;
27533
+ partOps.push({ operation: rest, origIndex: i2, part: resolved.part });
27534
+ }
27535
+ const results = [];
27536
+ const receipts = [];
27537
+ const executionOrder = [];
27538
+ const authorsUsed = /* @__PURE__ */ new Set();
27539
+ const originalIssues = [];
27540
+ let anyChange = false;
27541
+ let anyFailure = false;
27542
+ let bodyResult = null;
27543
+ if (bodyOps.length) {
27544
+ bodyResult = await this.applyBodyOperations(bodyOps.map((item) => item.operation), { ...options, expectedRevision: void 0 });
27545
+ const toOriginal = (index) => bodyOps[index - 1]?.origIndex + 1;
27546
+ for (const result of bodyResult.results || []) {
27547
+ results.push({
27548
+ ...result,
27549
+ index: toOriginal(result.index),
27550
+ ...result.receipt ? { receipt: { ...result.receipt, operationIndex: toOriginal(result.receipt.operationIndex) } } : {}
27551
+ });
27552
+ }
27553
+ for (const receipt of bodyResult.receipts || []) receipts.push({ ...receipt, operationIndex: toOriginal(receipt.operationIndex) });
27554
+ for (const index of bodyResult.executionOrder || []) executionOrder.push(toOriginal(index));
27555
+ for (const name of bodyResult.authorsUsed || []) authorsUsed.add(name);
27556
+ originalIssues.push(...bodyResult.validation?.originalIssues || []);
27557
+ if (bodyResult.status === "error" || bodyResult.rolledBack) {
27558
+ anyFailure = true;
27559
+ if (atomic) {
27560
+ restore();
27561
+ return { ...bodyResult, results, receipts, executionOrder, written: false, artifactsChanged: [], rolledBack: true, ...original() };
27562
+ }
27563
+ }
27564
+ anyChange = bodyResult.hasChanges === true;
27565
+ }
27566
+ const working = cloneEntries(this.entries);
27567
+ const zip = new MemoryZip(working);
27568
+ const changedParts = /* @__PURE__ */ new Set();
27569
+ const { expectedRevision: _ignored, ...runnerOptions } = options;
27570
+ const failPart = (item, error2) => {
27571
+ anyFailure = true;
27572
+ const failure = normalizeErrorWithRecovery({ ...error2, operationIndex: item.origIndex + 1 });
27573
+ results.push({ index: item.origIndex + 1, type: item.operation.type || "redline", status: "error", authorUsed: author, part: item.part.path, error: failure });
27574
+ };
27575
+ for (const item of partOps) {
27576
+ const path = item.part.path;
27577
+ const xml = text(working, path);
27578
+ if (!xml) {
27579
+ failPart(item, { code: "PART_NOT_FOUND", message: `Part '${path}' is referenced but missing from the package.` });
27580
+ if (atomic) break;
27581
+ continue;
27582
+ }
27583
+ const numberingText = text(working, "word/numbering.xml");
27584
+ const outcome = await applyOperationsToDocumentXml(xml, [item.operation], author, { numberingIdState: createDynamicNumberingIdState(numberingText || void 0) }, {
27585
+ ...runnerOptions,
27586
+ atomic: true,
27587
+ strictTargets: options.strictTargets !== false,
27588
+ _existingCommentDetails: {}
27589
+ });
27590
+ const single = outcome.results?.[0];
27591
+ if (outcome.rolledBack || outcome.status === "error") {
27592
+ failPart(item, single?.error || outcome.error || { code: "OPERATION_FAILED", message: "Operation failed." });
27593
+ if (atomic) break;
27594
+ continue;
27595
+ }
27596
+ if (!outcome.hasChanges) {
27597
+ results.push({ ...single, index: item.origIndex + 1, part: path });
27598
+ continue;
27599
+ }
27600
+ if (countRevisionsInsideFields(outcome.documentXml) > countRevisionsInsideFields(xml)) {
27601
+ 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.` });
27602
+ if (atomic) break;
27603
+ continue;
27604
+ }
27605
+ const taken = /* @__PURE__ */ new Set();
27606
+ for (const [name, data] of working) {
27607
+ if (name === path || !(name === "word/document.xml" || isHeaderFooterPath(name))) continue;
27608
+ for (const id of collectRevisionIds(textDecoder2.decode(data))) taken.add(id);
27609
+ }
27610
+ const renumbered = renumberCollidingRevisionIds(outcome.documentXml, taken);
27611
+ working.set(path, textEncoder4.encode(renumbered.xml));
27612
+ const describeReceipt = (receipt) => receipt?.revisionItems ? { ...receipt, revisionItems: receipt.revisionItems.map((revision) => ({ ...revision, id: renumbered.idMap.get(String(revision.id)) ?? revision.id, partName: path })) } : receipt;
27613
+ if (outcome.numberingXmlParts?.length) {
27614
+ await ensureNumberingArtifactsInZip(zip, outcome.numberingXmlParts, { mergeNumberingXml: mergeNumberingXmlBySchemaOrder });
27615
+ }
27616
+ changedParts.add(path);
27617
+ anyChange = true;
27618
+ results.push({
27619
+ ...single,
27620
+ index: item.origIndex + 1,
27621
+ part: path,
27622
+ partSections: item.part.appliesToSections,
27623
+ ...single?.receipt ? { receipt: { ...describeReceipt(single.receipt), operationIndex: item.origIndex + 1 } } : {}
27624
+ });
27625
+ if (outcome.receipts?.[0]) receipts.push({ ...describeReceipt(outcome.receipts[0]), operationIndex: item.origIndex + 1 });
27626
+ executionOrder.push(item.origIndex + 1);
27627
+ for (const name of outcome.authorsUsed || []) authorsUsed.add(name);
27628
+ }
27629
+ const generatedIssues = [];
27630
+ if (!(atomic && anyFailure)) {
27631
+ for (const path of changedParts) {
27632
+ const before = validateRedlineOoxml(text(this.entries, path)).issues.map((issue) => ({ source: path, ...issue }));
27633
+ const after = validateRedlineOoxml(text(working, path)).issues.map((issue) => ({ source: path, ...issue }));
27634
+ generatedIssues.push(...validationErrors(subtractValidationIssueMultiset(after, before)));
27635
+ }
27636
+ if (options.validate !== false && changedParts.size) {
27637
+ try {
27638
+ await validateDocxPackage(zip);
27639
+ } catch (error2) {
27640
+ generatedIssues.push({ source: "package", code: "PACKAGE_VALIDATION", severity: "error", message: error2.message });
27641
+ }
27642
+ }
27643
+ }
27644
+ if (atomic && anyFailure || generatedIssues.length) {
27645
+ restore();
27646
+ 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." };
27647
+ const rolledBackResults = results.map((result) => result.receipt ? { ...result, receipt: { ...result.receipt, committed: false, finalDisposition: "rolled_back" } } : result).sort((a, b) => a.index - b.index);
27648
+ const rolledBackReceipts = receipts.map((receipt) => ({ ...receipt, committed: false, finalDisposition: "rolled_back" }));
27649
+ return refuse(error2, rolledBackResults, rolledBackReceipts, { validation: { originalIssues, generatedIssues }, issues: generatedIssues });
27650
+ }
27651
+ results.sort((a, b) => a.index - b.index);
27652
+ receipts.sort((a, b) => a.operationIndex - b.operationIndex);
27653
+ const status = !anyFailure ? "ok" : anyChange ? "partial" : "error";
27654
+ const failureError = anyFailure ? { error: normalizeErrorWithRecovery({ code: "BATCH_OPERATION_FAILED", message: "One or more operations failed; the others were applied." }) } : {};
27655
+ if (!changedParts.size) {
27656
+ const buf = toBufferCompatible(this.originalBytes);
27657
+ return {
27658
+ ...bodyResult || {},
27659
+ results,
27660
+ receipts,
27661
+ executionOrder,
27662
+ authorsUsed: [...authorsUsed],
27663
+ status,
27664
+ hasChanges: anyChange,
27665
+ written: bodyResult?.written === true,
27666
+ artifactsChanged: bodyResult?.artifactsChanged || [],
27667
+ validation: { originalIssues, generatedIssues: [] },
27668
+ ...failureError,
27669
+ uint8Array: this.originalBytes,
27670
+ toUint8Array: () => new Uint8Array(this.originalBytes),
27671
+ buffer: buf,
27672
+ toBuffer: () => toBufferCompatible(this.originalBytes)
27673
+ };
27674
+ }
27675
+ this.entries = working;
27676
+ const outputBytes = this.toUint8Array();
27677
+ this.originalBytes = outputBytes;
27678
+ const artifactsChanged = [...working].filter(([name, data]) => !snapshotEntries.has(name) || !areByteArraysEqual(data, snapshotEntries.get(name))).map(([name]) => name);
27679
+ return {
27680
+ ...bodyResult || {},
27681
+ status,
27682
+ hasChanges: true,
27683
+ written: true,
27684
+ results,
27685
+ receipts,
27686
+ executionOrder,
27687
+ authorsUsed: [...authorsUsed],
27688
+ artifactsChanged,
27689
+ validation: { originalIssues, generatedIssues: [] },
27690
+ ...failureError,
27691
+ uint8Array: outputBytes,
27692
+ toUint8Array: () => new Uint8Array(outputBytes),
27693
+ buffer: toBufferCompatible(outputBytes),
27694
+ inspection: this.inspect(),
27695
+ toBuffer: () => toBufferCompatible(outputBytes)
27696
+ };
27697
+ }
27698
+ async applyBodyOperations(operations, options = {}) {
26743
27699
  const failedApply = (error2) => {
26744
27700
  const results = [];
26745
27701
  const receipts = [];
@@ -26850,18 +27806,21 @@ var DocxDocument = class {
26850
27806
  toBuffer: () => toBufferCompatible(this.originalBytes)
26851
27807
  };
26852
27808
  }
26853
- working.set("word/document.xml", textEncoder4.encode(result.documentXml));
27809
+ const bodyChanged = (result.results || []).some((item) => item?.status !== "error" && item?.operationType !== "comment_resolve");
27810
+ if (bodyChanged) working.set("word/document.xml", textEncoder4.encode(result.documentXml));
26854
27811
  await ensureNumberingArtifactsInZip(zip, result.numberingXmlParts, { mergeNumberingXml: mergeNumberingXmlBySchemaOrder });
26855
27812
  const existingCommentsXml = text(working, "word/comments.xml");
26856
27813
  const commentsXmlForPackaging = result.commentsXml || existingCommentsXml;
26857
27814
  await ensureCommentsArtifactsInZip(zip, commentsXmlForPackaging, {
26858
27815
  replaceExisting: result.commentsXmlMode === "replace" || !result.commentsXml && !!existingCommentsXml
26859
27816
  });
26860
- const existingCommentsExtendedXml = text(working, "word/commentsExtended.xml");
26861
- const commentsExtendedXmlForPackaging = result.commentsExtendedXml || existingCommentsExtendedXml;
26862
- await ensureCommentsExtendedArtifactsInZip(zip, commentsExtendedXmlForPackaging, {
26863
- replaceExisting: result.commentsExtendedXmlMode === "replace" || !result.commentsExtendedXml && !!existingCommentsExtendedXml
26864
- });
27817
+ if (result.commentsExtendedXml) {
27818
+ await ensureCommentsExtendedArtifactsInZip(zip, result.commentsExtendedXml, {
27819
+ replaceExisting: result.commentsExtendedXmlMode === "replace"
27820
+ });
27821
+ }
27822
+ await repairKnownContentTypes(zip);
27823
+ reconcileCommentSiblings(working);
26865
27824
  if (options.validate !== false) {
26866
27825
  const generated = validateRedlineOoxml(result.documentXml);
26867
27826
  const outputIssues = generated.issues.map((issue) => ({ source: "word/document.xml", ...issue }));
@@ -26930,13 +27889,25 @@ var DocxDocument = class {
26930
27889
  const transform = action === "accept" ? acceptTrackedChangesInOoxml : action === "reject" ? rejectTrackedChangesInOoxml : null;
26931
27890
  if (!transform) return packageFailure(this.originalBytes, "INVALID_ACTION", `Unknown revision action: ${action}`);
26932
27891
  const sourceBytes = this.originalBytes;
27892
+ const originalEntries = this.entries;
26933
27893
  const working = cloneEntries(this.entries);
26934
27894
  const zip = new MemoryZip(working);
26935
27895
  const result = transform(text(working, "word/document.xml"), { author: options.author, allAuthors: options.allAuthors === true });
26936
27896
  if (result.status === "error" || result.error) {
26937
27897
  return packageFailure(sourceBytes, result.error?.code || "REVISION_OPERATION_FAILED", result.error?.message || "Revision operation failed.");
26938
27898
  }
26939
- if (!result.hasChanges) {
27899
+ const partUpdates = /* @__PURE__ */ new Map();
27900
+ for (const part of this.inspectHeadersFooters()) {
27901
+ const partXml = text(working, part.path);
27902
+ if (!partXml) continue;
27903
+ const partResult = transform(partXml, { author: options.author, allAuthors: options.allAuthors === true });
27904
+ if (partResult.status === "error" || partResult.error) {
27905
+ return packageFailure(sourceBytes, partResult.error?.code || "REVISION_OPERATION_FAILED", `${part.path}: ${partResult.error?.message || "Revision operation failed."}`);
27906
+ }
27907
+ if (partResult.hasChanges) partUpdates.set(part.path, partResult.oxml);
27908
+ }
27909
+ const hasChanges = result.hasChanges || partUpdates.size > 0;
27910
+ if (!hasChanges) {
26940
27911
  const buf = toBufferCompatible(sourceBytes);
26941
27912
  return {
26942
27913
  ...result,
@@ -26949,8 +27920,10 @@ var DocxDocument = class {
26949
27920
  toBuffer: () => toBufferCompatible(sourceBytes)
26950
27921
  };
26951
27922
  }
26952
- working.set("word/document.xml", textEncoder4.encode(result.oxml));
27923
+ if (result.hasChanges) working.set("word/document.xml", textEncoder4.encode(result.oxml));
27924
+ for (const [path, xml] of partUpdates) working.set(path, textEncoder4.encode(xml));
26953
27925
  try {
27926
+ await repairKnownContentTypes(zip);
26954
27927
  if (options.validate !== false) await validateDocxPackage(zip);
26955
27928
  } catch (error2) {
26956
27929
  return packageFailure(sourceBytes, "PACKAGE_VALIDATION", error2.message);
@@ -26963,13 +27936,71 @@ var DocxDocument = class {
26963
27936
  ...result,
26964
27937
  status: "ok",
26965
27938
  written: true,
26966
- artifactsChanged: ["word/document.xml"],
27939
+ hasChanges: true,
27940
+ artifactsChanged: [...working].filter(([name, data]) => !originalEntries.has(name) || !areByteArraysEqual(data, originalEntries.get(name))).map(([name]) => name),
26967
27941
  uint8Array: outputBytes,
26968
27942
  toUint8Array: () => new Uint8Array(outputBytes),
26969
27943
  buffer: outputBuf,
26970
27944
  toBuffer: () => toBufferCompatible(outputBytes)
26971
27945
  };
26972
27946
  }
27947
+ /**
27948
+ * Resolves (or reopens) the comment thread containing `commentId`.
27949
+ * Resolved is a thread-level state in Word, so the root and every reply are updated together.
27950
+ */
27951
+ async resolveComment(commentId, options = {}) {
27952
+ const sourceBytes = this.originalBytes;
27953
+ const working = cloneEntries(this.entries);
27954
+ const resolved = options.resolved !== false;
27955
+ const noChange = (extra) => {
27956
+ const buf = toBufferCompatible(sourceBytes);
27957
+ return {
27958
+ status: "ok",
27959
+ hasChanges: false,
27960
+ written: false,
27961
+ artifactsChanged: [],
27962
+ ...extra,
27963
+ uint8Array: sourceBytes,
27964
+ toUint8Array: () => new Uint8Array(sourceBytes),
27965
+ buffer: buf,
27966
+ toBuffer: () => toBufferCompatible(sourceBytes)
27967
+ };
27968
+ };
27969
+ const result = applyThreadResolutionToParts({
27970
+ commentsXml: text(working, "word/comments.xml"),
27971
+ commentsExtendedXml: text(working, "word/commentsExtended.xml"),
27972
+ commentId,
27973
+ resolved
27974
+ });
27975
+ if (result.status === "error") return packageFailure(sourceBytes, result.error.code, result.error.message);
27976
+ if (!result.hasChanges) return noChange({ resolved, threadRootId: result.threadRootId, commentIds: result.commentIds });
27977
+ const zip = new MemoryZip(working);
27978
+ try {
27979
+ if (result.commentsXml) working.set("word/comments.xml", textEncoder4.encode(result.commentsXml));
27980
+ await ensureCommentsExtendedArtifactsInZip(zip, result.commentsExtendedXml, { replaceExisting: true });
27981
+ await repairKnownContentTypes(zip);
27982
+ if (options.validate !== false) await validateDocxPackage(zip);
27983
+ } catch (error2) {
27984
+ return packageFailure(sourceBytes, "PACKAGE_VALIDATION", error2.message);
27985
+ }
27986
+ const artifactsChanged = [...working].filter(([name, data]) => !this.entries.has(name) || !areByteArraysEqual(data, this.entries.get(name))).map(([name]) => name);
27987
+ this.entries = working;
27988
+ const outputBytes = this.toUint8Array();
27989
+ this.originalBytes = outputBytes;
27990
+ return {
27991
+ status: "ok",
27992
+ hasChanges: true,
27993
+ written: true,
27994
+ resolved,
27995
+ threadRootId: result.threadRootId,
27996
+ commentIds: result.commentIds,
27997
+ artifactsChanged,
27998
+ uint8Array: outputBytes,
27999
+ toUint8Array: () => new Uint8Array(outputBytes),
28000
+ buffer: toBufferCompatible(outputBytes),
28001
+ toBuffer: () => toBufferCompatible(outputBytes)
28002
+ };
28003
+ }
26973
28004
  async deleteComments(options = {}) {
26974
28005
  const sourceBytes = this.originalBytes;
26975
28006
  const working = cloneEntries(this.entries);
@@ -26993,7 +28024,13 @@ var DocxDocument = class {
26993
28024
  if (!parsed.doc || parsed.error) {
26994
28025
  return packageFailure(sourceBytes, "PARSE_ERROR", parsed.error?.message || "Could not parse comments.xml.");
26995
28026
  }
26996
- const matches = (comment) => options.allAuthors === true || (comment.getAttribute("w:author") || comment.getAttribute("author")) === options.author;
28027
+ const requestedIds = Array.isArray(options.ids) ? new Set(options.ids.map(String)) : null;
28028
+ if (requestedIds) {
28029
+ const known = new Set(Array.from(parsed.doc.getElementsByTagNameNS("*", "comment")).map((node) => node.getAttribute("w:id") || node.getAttribute("id")));
28030
+ const missing = [...requestedIds].filter((id) => !known.has(id));
28031
+ if (missing.length) return packageFailure(sourceBytes, "COMMENT_NOT_FOUND", `Comment id(s) not found: ${missing.join(", ")}.`);
28032
+ }
28033
+ const matches = (comment) => requestedIds ? requestedIds.has(comment.getAttribute("w:id") || comment.getAttribute("id")) : options.allAuthors === true || (comment.getAttribute("w:author") || comment.getAttribute("author")) === options.author;
26997
28034
  const ids = new Set(
26998
28035
  Array.from(parsed.doc.getElementsByTagNameNS("*", "comment")).filter(matches).map((node) => node.getAttribute("w:id") || node.getAttribute("id")).filter(Boolean)
26999
28036
  );
@@ -27022,9 +28059,8 @@ var DocxDocument = class {
27022
28059
  const idByParaId = /* @__PURE__ */ new Map();
27023
28060
  for (const comment of Array.from(parsed.doc.getElementsByTagNameNS("*", "comment"))) {
27024
28061
  const id = comment.getAttribute("w:id") || comment.getAttribute("id");
27025
- const paragraph = Array.from(comment.getElementsByTagNameNS("*", "p"))[0];
27026
- const paraId = paragraph?.getAttribute("w14:paraId") || paragraph?.getAttribute("paraId");
27027
- if (paraId) idByParaId.set(paraId.toUpperCase(), id);
28062
+ const paraId = commentParaId(comment);
28063
+ if (paraId) idByParaId.set(paraId, id);
27028
28064
  }
27029
28065
  for (const [paraId, id] of idByParaId) {
27030
28066
  if (ids.has(id)) removedParaIds.add(paraId);
@@ -27043,8 +28079,8 @@ var DocxDocument = class {
27043
28079
  }
27044
28080
  }
27045
28081
  }
27046
- const commentsResult = deleteCommentsByAuthorInOoxml(commentsXml, { author: options.author, allAuthors: options.allAuthors === true });
27047
- const remainingParsed = parseOoxmlSafe(commentsResult.oxml, "application/xml");
28082
+ const remainingXml = requestedIds ? commentsXml : deleteCommentsByAuthorInOoxml(commentsXml, { author: options.author, allAuthors: options.allAuthors === true }).oxml;
28083
+ const remainingParsed = parseOoxmlSafe(remainingXml, "application/xml");
27048
28084
  if (!remainingParsed.doc || remainingParsed.error) {
27049
28085
  return packageFailure(sourceBytes, "PARSE_ERROR", remainingParsed.error?.message || "Could not parse updated comments.xml.");
27050
28086
  }
@@ -27080,10 +28116,14 @@ var DocxDocument = class {
27080
28116
  }
27081
28117
  working.set("word/document.xml", textEncoder4.encode(serializer.serializeToString(documentParsed.doc)));
27082
28118
  try {
27083
- if (options.validate !== false) await validateDocxPackage(new MemoryZip(working));
28119
+ reconcileCommentSiblings(working);
28120
+ const finalZip = new MemoryZip(working);
28121
+ await repairKnownContentTypes(finalZip);
28122
+ if (options.validate !== false) await validateDocxPackage(finalZip);
27084
28123
  } catch (error2) {
27085
28124
  return packageFailure(sourceBytes, "PACKAGE_VALIDATION", error2.message);
27086
28125
  }
28126
+ const artifactsChanged = [...working].filter(([name, data]) => !this.entries.has(name) || !areByteArraysEqual(data, this.entries.get(name))).map(([name]) => name);
27087
28127
  this.entries = working;
27088
28128
  const outputBytes = this.toUint8Array();
27089
28129
  this.originalBytes = outputBytes;
@@ -27094,7 +28134,7 @@ var DocxDocument = class {
27094
28134
  written: true,
27095
28135
  commentsRemoved: ids.size,
27096
28136
  referencesRemoved,
27097
- artifactsChanged: ["word/document.xml", "word/comments.xml", ...extendedParsed?.doc ? ["word/commentsExtended.xml"] : []],
28137
+ artifactsChanged,
27098
28138
  uint8Array: outputBytes,
27099
28139
  toUint8Array: () => new Uint8Array(outputBytes),
27100
28140
  buffer: outputBuf,
@@ -27350,6 +28390,7 @@ export {
27350
28390
  rejectTrackedChangesInOoxml,
27351
28391
  remapNumberingPayloadForDocument,
27352
28392
  removeFormattingFromRPr,
28393
+ repairKnownContentTypes,
27353
28394
  reserveNextNumberingId,
27354
28395
  reserveNextNumberingIdPair,
27355
28396
  resolveParagraphRangeByRefs,