@ansonlai/docx-redline-js 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +539 -486
  2. package/README.md +331 -309
  3. package/dist/docx-redline-js.esm.js +1083 -163
  4. package/dist/docx-redline-js.esm.js.map +4 -4
  5. package/dist/docx-redline-js.esm.min.js +107 -107
  6. package/dist/docx-redline-js.esm.min.js.map +4 -4
  7. package/dist/docx-redline.bundle.cjs +1083 -163
  8. package/dist/docx-redline.bundle.cjs.map +4 -4
  9. package/dist/docx-redline.bundle.js +1083 -163
  10. package/dist/docx-redline.bundle.js.map +4 -4
  11. package/docs/AGENT_KNOWLEDGE_BASE.md +69 -8
  12. package/docs/schemas/document-operations.schema.json +29 -0
  13. package/document/docx-document.js +416 -22
  14. package/engine/reconstruction-writer.js +18 -0
  15. package/index.d.ts +598 -575
  16. package/index.js +349 -348
  17. package/node/cli-help.js +5 -5
  18. package/node/cli.js +760 -754
  19. package/node/index.d.ts +1 -1
  20. package/package.json +2 -1
  21. package/scripts/generate-word-comment-edge-case-fixture.ps1 +58 -0
  22. package/scripts/generate-word-comment-thread-fixture.ps1 +36 -0
  23. package/scripts/generate-word-header-footer-fixture.ps1 +32 -0
  24. package/scripts/generate-word-multi-section-fixture.ps1 +27 -0
  25. package/scripts/generate-word-package-integrity-cases.mjs +141 -0
  26. package/scripts/generate-word-thread-state-fixtures.ps1 +50 -0
  27. package/scripts/word-com-package-integrity.ps1 +98 -0
  28. package/services/batch-operation-orchestrator.js +262 -259
  29. package/services/comment-replies.js +96 -95
  30. package/services/comment-thread-parts.js +280 -0
  31. package/services/document-inspection.js +366 -365
  32. package/services/document-operation-applier.js +230 -202
  33. package/services/document-operation-contract.js +17 -2
  34. package/services/error-recovery.js +6 -0
  35. package/services/headers-footers.js +206 -0
  36. package/services/operation-batch-compiler.js +1 -0
  37. package/services/operation-preflight.js +231 -218
  38. package/services/package-parts.js +80 -0
  39. package/services/standalone-docx-plumbing.js +631 -569
  40. package/services/standalone-operation-runner.d.ts +19 -3
@@ -1,4 +1,4 @@
1
- // @ansonlai/docx-redline-js v0.8.0 — https://github.com/AnsonLai/docx-redline-js
1
+ // @ansonlai/docx-redline-js v0.8.1 — https://github.com/AnsonLai/docx-redline-js
2
2
  var __create = Object.create;
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -7717,6 +7717,19 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
7717
7717
  pendingReplacementEvent = null;
7718
7718
  }
7719
7719
  }
7720
+ for (const offset of Array.from(sentinelMapByStart.keys()).sort((a, b) => a - b)) {
7721
+ for (const sentinel of sentinelMapByStart.get(offset)) {
7722
+ if (!sentinel.isCommentMarker || emittedCommentMarkers.has(sentinel.node)) continue;
7723
+ emittedCommentMarkers.add(sentinel.node);
7724
+ if (isWordElement(sentinel.node, "commentReference")) {
7725
+ const run = createWordElement(xmlDoc, "w:r");
7726
+ run.appendChild(sentinel.node.cloneNode(true));
7727
+ currentParagraph.appendChild(run);
7728
+ } else {
7729
+ currentParagraph.appendChild(sentinel.node.cloneNode(true));
7730
+ }
7731
+ }
7732
+ }
7720
7733
  if (generateRedlines) {
7721
7734
  containerFragments.forEach((fragment) => {
7722
7735
  Array.from(fragment.childNodes).forEach((node) => {
@@ -11511,6 +11524,260 @@ function ingestWordOoxmlToMarkdownResult(ooxml) {
11511
11524
  return { text: lines.join("\n\n").trim(), status: "ok", warnings: parsed.warnings };
11512
11525
  }
11513
11526
 
11527
+ // services/comment-builders.js
11528
+ var NS_W14 = "http://schemas.microsoft.com/office/word/2010/wordml";
11529
+ var NS_W15 = "http://schemas.microsoft.com/office/word/2012/wordml";
11530
+ function createCommentParaId(commentId) {
11531
+ const numeric = Number.parseInt(String(commentId), 10);
11532
+ const value = Number.isFinite(numeric) ? 1879048192 + (numeric >>> 0) >>> 0 : 1879048192;
11533
+ return value.toString(16).toUpperCase().padStart(8, "0").slice(-8);
11534
+ }
11535
+ function buildCommentElement(commentId, author, content, date, paraId = createCommentParaId(commentId)) {
11536
+ const initials = author.split(" ").map((word) => word[0]).join("").toUpperCase() || "AI";
11537
+ const escapedContent = escapeXml(content);
11538
+ const escapedAuthor = escapeXml(author);
11539
+ return `<w:comment w:id="${commentId}" w:author="${escapedAuthor}" w:date="${date}" w:initials="${initials}">
11540
+ <w:p w14:paraId="${escapeXml(paraId)}" xmlns:w14="${NS_W14}">
11541
+ <w:r><w:t>${escapedContent}</w:t></w:r>
11542
+ </w:p>
11543
+ </w:comment>`;
11544
+ }
11545
+ function buildCommentsPartXml(comments) {
11546
+ if (!comments || comments.length === 0) {
11547
+ return `<w:comments xmlns:w="${NS_W}"></w:comments>`;
11548
+ }
11549
+ const commentElements = comments.map(
11550
+ (comment) => buildCommentElement(comment.id, comment.author, comment.content, comment.date, comment.paraId)
11551
+ ).join("\n ");
11552
+ return `<w:comments xmlns:w="${NS_W}">
11553
+ ${commentElements}
11554
+ </w:comments>`;
11555
+ }
11556
+ function buildCommentsExtendedPartXml(entries) {
11557
+ const body = (entries || []).map((entry) => {
11558
+ const parent = entry.paraIdParent ? ` w15:paraIdParent="${escapeXml(entry.paraIdParent)}"` : "";
11559
+ return `<w15:commentEx w15:paraId="${escapeXml(entry.paraId)}"${parent} w15:done="${entry.done ? "1" : "0"}"/>`;
11560
+ }).join("");
11561
+ return `<w15:commentsEx xmlns:w15="${NS_W15}">${body}</w15:commentsEx>`;
11562
+ }
11563
+
11564
+ // services/comment-thread-parts.js
11565
+ var attrOf = (node, qualified, local) => node?.getAttribute?.(qualified) || node?.getAttribute?.(local) || "";
11566
+ function commentThreadParagraph(commentElement) {
11567
+ const paragraphs = Array.from(commentElement?.getElementsByTagNameNS?.("*", "p") || []);
11568
+ return paragraphs.length ? paragraphs[paragraphs.length - 1] : null;
11569
+ }
11570
+ function commentParaId(commentElement) {
11571
+ return attrOf(commentThreadParagraph(commentElement), "w14:paraId", "paraId").toUpperCase();
11572
+ }
11573
+ function usedParaIds(commentsDoc, extendedDoc) {
11574
+ const ids = /* @__PURE__ */ new Set();
11575
+ for (const p of Array.from(commentsDoc?.getElementsByTagNameNS("*", "p") || [])) {
11576
+ const id = attrOf(p, "w14:paraId", "paraId");
11577
+ if (id) ids.add(id.toUpperCase());
11578
+ }
11579
+ for (const ex of Array.from(extendedDoc?.getElementsByTagNameNS("*", "commentEx") || [])) {
11580
+ const id = attrOf(ex, "w15:paraId", "paraId");
11581
+ if (id) ids.add(id.toUpperCase());
11582
+ }
11583
+ return ids;
11584
+ }
11585
+ function allocateParaId(commentId, occupied) {
11586
+ let candidate = createCommentParaId(commentId);
11587
+ let value = Number.parseInt(candidate, 16) >>> 0;
11588
+ while (occupied.has(candidate)) {
11589
+ value = value + 1 >>> 0;
11590
+ candidate = value.toString(16).toUpperCase().padStart(8, "0");
11591
+ }
11592
+ occupied.add(candidate);
11593
+ return candidate;
11594
+ }
11595
+ var parseDoc = (xml, partName) => {
11596
+ const parsed = parseOoxmlSafe(xml, "application/xml");
11597
+ if (!parsed.doc || parsed.error) return { error: { code: "PARSE_ERROR", message: `Could not parse ${partName}: ${parsed.error?.message || "invalid XML"}` } };
11598
+ return { doc: parsed.doc };
11599
+ };
11600
+ function applyThreadResolutionToParts({ commentsXml, commentsExtendedXml = null, commentId, resolved = true }) {
11601
+ if (!commentsXml) return { status: "error", error: { code: "COMMENTS_PART_MISSING", message: "Resolving a comment requires an existing word/comments.xml part." } };
11602
+ const commentsParsed = parseDoc(commentsXml, "word/comments.xml");
11603
+ if (commentsParsed.error) return { status: "error", error: commentsParsed.error };
11604
+ const commentsDoc = commentsParsed.doc;
11605
+ const idOf = (node) => attrOf(node, "w:id", "id");
11606
+ const comments = Array.from(commentsDoc.getElementsByTagNameNS("*", "comment"));
11607
+ const target = comments.find((node) => idOf(node) === String(commentId));
11608
+ if (!target) return { status: "error", error: { code: "COMMENT_NOT_FOUND", message: `Comment '${commentId}' was not found.` } };
11609
+ let extendedDoc;
11610
+ if (commentsExtendedXml) {
11611
+ const parsed = parseDoc(commentsExtendedXml, "word/commentsExtended.xml");
11612
+ if (parsed.error) return { status: "error", error: parsed.error };
11613
+ extendedDoc = parsed.doc;
11614
+ } else {
11615
+ extendedDoc = parseDoc(`<w15:commentsEx xmlns:w15="${NS_W15}"/>`, "word/commentsExtended.xml").doc;
11616
+ }
11617
+ const root = extendedDoc.documentElement;
11618
+ const entryFor = (paraId) => Array.from(root.getElementsByTagNameNS("*", "commentEx")).find((node) => attrOf(node, "w15:paraId", "paraId").toUpperCase() === paraId) || null;
11619
+ const occupied = usedParaIds(commentsDoc, extendedDoc);
11620
+ let commentsChanged = false;
11621
+ const keyOf = (node) => {
11622
+ let paraId = commentParaId(node);
11623
+ if (!paraId) {
11624
+ paraId = allocateParaId(idOf(node), occupied);
11625
+ commentThreadParagraph(node)?.setAttributeNS(NS_W14, "w14:paraId", paraId);
11626
+ commentsChanged = true;
11627
+ }
11628
+ return paraId;
11629
+ };
11630
+ const targetKey = keyOf(target);
11631
+ const targetEntry = entryFor(targetKey);
11632
+ const rootKey = targetEntry && attrOf(targetEntry, "w15:paraIdParent", "paraIdParent").toUpperCase() || targetKey;
11633
+ const threadKeys = /* @__PURE__ */ new Set([rootKey, targetKey]);
11634
+ for (const entry of Array.from(root.getElementsByTagNameNS("*", "commentEx"))) {
11635
+ if (attrOf(entry, "w15:paraIdParent", "paraIdParent").toUpperCase() === rootKey) threadKeys.add(attrOf(entry, "w15:paraId", "paraId").toUpperCase());
11636
+ }
11637
+ const doneValue = resolved ? "1" : "0";
11638
+ let extendedChanged = !commentsExtendedXml;
11639
+ for (const key of threadKeys) {
11640
+ let entry = entryFor(key);
11641
+ if (!entry) {
11642
+ entry = extendedDoc.createElementNS(NS_W15, "w15:commentEx");
11643
+ entry.setAttributeNS(NS_W15, "w15:paraId", key);
11644
+ if (key !== rootKey) entry.setAttributeNS(NS_W15, "w15:paraIdParent", rootKey);
11645
+ entry.setAttributeNS(NS_W15, "w15:done", doneValue);
11646
+ root.appendChild(entry);
11647
+ extendedChanged = true;
11648
+ } else if (attrOf(entry, "w15:done", "done") !== doneValue) {
11649
+ entry.setAttributeNS(NS_W15, "w15:done", doneValue);
11650
+ extendedChanged = true;
11651
+ }
11652
+ }
11653
+ const byKey = new Map(comments.map((node) => [commentParaId(node), node]));
11654
+ const commentIds = [...threadKeys].map((key) => byKey.get(key)).filter(Boolean).map(idOf);
11655
+ const rootComment = byKey.get(rootKey);
11656
+ const serializer = createSerializer();
11657
+ const hasChanges = extendedChanged || commentsChanged;
11658
+ return {
11659
+ status: "ok",
11660
+ hasChanges,
11661
+ resolved,
11662
+ threadRootId: rootComment ? idOf(rootComment) : String(commentId),
11663
+ commentIds,
11664
+ ...hasChanges ? { commentsExtendedXml: serializer.serializeToString(extendedDoc) } : {},
11665
+ ...commentsChanged ? { commentsXml: serializer.serializeToString(commentsDoc) } : {}
11666
+ };
11667
+ }
11668
+ var NS_W_MAIN = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
11669
+ function anchorReplyInDocument(xmlDoc, memberIds, newId) {
11670
+ const wanted = new Set(memberIds.map(String));
11671
+ const idOf = (node) => attrOf(node, "w:id", "id");
11672
+ const all = (name) => Array.from(xmlDoc.getElementsByTagNameNS("*", name)).filter((node) => wanted.has(idOf(node)));
11673
+ const starts = all("commentRangeStart");
11674
+ const references = all("commentReference");
11675
+ if (!starts.length || !references.length) return false;
11676
+ const lastStart = starts[starts.length - 1];
11677
+ const lastReference = references[references.length - 1];
11678
+ const referenceRun = lastReference.parentNode?.localName === "r" ? lastReference.parentNode : lastReference;
11679
+ const make = (name, parent) => {
11680
+ const element = xmlDoc.createElementNS(NS_W_MAIN, `w:${name}`);
11681
+ element.setAttributeNS(NS_W_MAIN, "w:id", String(newId));
11682
+ if (parent) parent.appendChild(element);
11683
+ return element;
11684
+ };
11685
+ const insertAfter = (anchor, node) => anchor.parentNode.insertBefore(node, anchor.nextSibling);
11686
+ insertAfter(lastStart, make("commentRangeStart"));
11687
+ const end = make("commentRangeEnd");
11688
+ const run = xmlDoc.createElementNS(NS_W_MAIN, "w:r");
11689
+ run.appendChild(xmlDoc.createElementNS(NS_W_MAIN, "w:rPr"));
11690
+ make("commentReference", run);
11691
+ insertAfter(referenceRun, end);
11692
+ insertAfter(end, run);
11693
+ return true;
11694
+ }
11695
+ var NS_W16CID = "http://schemas.microsoft.com/office/word/2016/wordml/cid";
11696
+ var NS_W16CEX = "http://schemas.microsoft.com/office/word/2018/wordml/cex";
11697
+ var durableIdFor = (paraId, taken) => {
11698
+ let value = (Number.parseInt(paraId, 16) ^ 625341585) & 2147483646;
11699
+ let candidate = value.toString(16).toUpperCase().padStart(8, "0");
11700
+ while (taken.has(candidate)) {
11701
+ value = value + 2 & 2147483646;
11702
+ candidate = value.toString(16).toUpperCase().padStart(8, "0");
11703
+ }
11704
+ taken.add(candidate);
11705
+ return candidate;
11706
+ };
11707
+ function reconcileCommentSiblingParts({ commentsXml, commentsIdsXml = null, commentsExtensibleXml = null }) {
11708
+ const out = {};
11709
+ if (!commentsXml || !commentsIdsXml && !commentsExtensibleXml) return out;
11710
+ const commentsParsed = parseOoxmlSafe(commentsXml, "application/xml");
11711
+ if (!commentsParsed.doc || commentsParsed.error) return out;
11712
+ const serializer = createSerializer();
11713
+ const byKey = /* @__PURE__ */ new Map();
11714
+ for (const node of Array.from(commentsParsed.doc.getElementsByTagNameNS("*", "comment"))) {
11715
+ const key = commentParaId(node);
11716
+ if (key) byKey.set(key, node);
11717
+ }
11718
+ let idsDoc = null;
11719
+ const durableByKey = /* @__PURE__ */ new Map();
11720
+ const taken = /* @__PURE__ */ new Set();
11721
+ let idsChanged = false;
11722
+ if (commentsIdsXml) {
11723
+ const parsed = parseOoxmlSafe(commentsIdsXml, "application/xml");
11724
+ if (!parsed.doc || parsed.error) return out;
11725
+ idsDoc = parsed.doc;
11726
+ const root = idsDoc.documentElement;
11727
+ for (const entry of Array.from(root.getElementsByTagNameNS("*", "commentId"))) {
11728
+ const key = attrOf(entry, "w16cid:paraId", "paraId").toUpperCase();
11729
+ const durable = attrOf(entry, "w16cid:durableId", "durableId").toUpperCase();
11730
+ if (durable) taken.add(durable);
11731
+ if (!byKey.has(key)) {
11732
+ root.removeChild(entry);
11733
+ idsChanged = true;
11734
+ continue;
11735
+ }
11736
+ durableByKey.set(key, durable);
11737
+ }
11738
+ for (const key of byKey.keys()) {
11739
+ if (durableByKey.has(key)) continue;
11740
+ const durable = durableIdFor(key, taken);
11741
+ const entry = idsDoc.createElementNS(NS_W16CID, "w16cid:commentId");
11742
+ entry.setAttributeNS(NS_W16CID, "w16cid:paraId", key);
11743
+ entry.setAttributeNS(NS_W16CID, "w16cid:durableId", durable);
11744
+ root.appendChild(entry);
11745
+ durableByKey.set(key, durable);
11746
+ idsChanged = true;
11747
+ }
11748
+ }
11749
+ let extDoc = null;
11750
+ let extChanged = false;
11751
+ if (commentsExtensibleXml && idsDoc) {
11752
+ const parsed = parseOoxmlSafe(commentsExtensibleXml, "application/xml");
11753
+ if (parsed.doc && !parsed.error) {
11754
+ extDoc = parsed.doc;
11755
+ const root = extDoc.documentElement;
11756
+ const live = new Set(durableByKey.values());
11757
+ const present = /* @__PURE__ */ new Set();
11758
+ for (const entry of Array.from(root.getElementsByTagNameNS("*", "commentExtensible"))) {
11759
+ const durable = attrOf(entry, "w16cex:durableId", "durableId").toUpperCase();
11760
+ if (!live.has(durable)) {
11761
+ root.removeChild(entry);
11762
+ extChanged = true;
11763
+ } else present.add(durable);
11764
+ }
11765
+ for (const [key, durable] of durableByKey) {
11766
+ if (present.has(durable)) continue;
11767
+ const date = attrOf(byKey.get(key), "w:date", "date");
11768
+ const entry = extDoc.createElementNS(NS_W16CEX, "w16cex:commentExtensible");
11769
+ entry.setAttributeNS(NS_W16CEX, "w16cex:durableId", durable);
11770
+ if (date) entry.setAttributeNS(NS_W16CEX, "w16cex:dateUtc", date);
11771
+ root.appendChild(entry);
11772
+ extChanged = true;
11773
+ }
11774
+ }
11775
+ }
11776
+ if (idsChanged) out.commentsIdsXml = serializer.serializeToString(idsDoc);
11777
+ if (extChanged) out.commentsExtensibleXml = serializer.serializeToString(extDoc);
11778
+ return out;
11779
+ }
11780
+
11514
11781
  // core/sha256.js
11515
11782
  var K = new Uint32Array([
11516
11783
  1116352408,
@@ -12003,7 +12270,7 @@ function readCommentDefinitions(commentsDoc) {
12003
12270
  author: attr2(comment, "author") || null,
12004
12271
  date: attr2(comment, "date") || null,
12005
12272
  text: paragraphs.map((p) => extractCanonicalParagraphText(p)).join("\n"),
12006
- paraId: paragraphs[0]?.getAttribute?.("w14:paraId") || paragraphs[0]?.getAttribute?.("paraId") || null
12273
+ paraId: commentParaId(comment) || null
12007
12274
  });
12008
12275
  }
12009
12276
  return result;
@@ -12234,43 +12501,6 @@ function inspectDocumentParts(parts, options = {}) {
12234
12501
  };
12235
12502
  }
12236
12503
 
12237
- // services/comment-builders.js
12238
- var NS_W14 = "http://schemas.microsoft.com/office/word/2010/wordml";
12239
- var NS_W15 = "http://schemas.microsoft.com/office/word/2012/wordml";
12240
- function createCommentParaId(commentId) {
12241
- const numeric = Number.parseInt(String(commentId), 10);
12242
- const value = Number.isFinite(numeric) ? 1879048192 + (numeric >>> 0) >>> 0 : 1879048192;
12243
- return value.toString(16).toUpperCase().padStart(8, "0").slice(-8);
12244
- }
12245
- function buildCommentElement(commentId, author, content, date, paraId = createCommentParaId(commentId)) {
12246
- const initials = author.split(" ").map((word) => word[0]).join("").toUpperCase() || "AI";
12247
- const escapedContent = escapeXml(content);
12248
- const escapedAuthor = escapeXml(author);
12249
- return `<w:comment w:id="${commentId}" w:author="${escapedAuthor}" w:date="${date}" w:initials="${initials}">
12250
- <w:p w14:paraId="${escapeXml(paraId)}" xmlns:w14="${NS_W14}">
12251
- <w:r><w:t>${escapedContent}</w:t></w:r>
12252
- </w:p>
12253
- </w:comment>`;
12254
- }
12255
- function buildCommentsPartXml(comments) {
12256
- if (!comments || comments.length === 0) {
12257
- return `<w:comments xmlns:w="${NS_W}"></w:comments>`;
12258
- }
12259
- const commentElements = comments.map(
12260
- (comment) => buildCommentElement(comment.id, comment.author, comment.content, comment.date, comment.paraId)
12261
- ).join("\n ");
12262
- return `<w:comments xmlns:w="${NS_W}">
12263
- ${commentElements}
12264
- </w:comments>`;
12265
- }
12266
- function buildCommentsExtendedPartXml(entries) {
12267
- const body = (entries || []).map((entry) => {
12268
- const parent = entry.paraIdParent ? ` w15:paraIdParent="${escapeXml(entry.paraIdParent)}"` : "";
12269
- return `<w15:commentEx w15:paraId="${escapeXml(entry.paraId)}"${parent} w15:done="${entry.done ? "1" : "0"}"/>`;
12270
- }).join("");
12271
- return `<w15:commentsEx xmlns:w15="${NS_W15}">${body}</w15:commentsEx>`;
12272
- }
12273
-
12274
12504
  // services/comment-locator.js
12275
12505
  function createParagraphTextIndex(paragraph, options = {}) {
12276
12506
  const revisionView = options.revisionView === "current" ? "accepted" : options.revisionView || "accepted";
@@ -12731,29 +12961,7 @@ function parseRequired(xml, partName) {
12731
12961
  }
12732
12962
  return { doc: parsed.doc };
12733
12963
  }
12734
- function usedParaIds(commentsDoc, extendedDoc) {
12735
- const ids = /* @__PURE__ */ new Set();
12736
- for (const p of Array.from(commentsDoc?.getElementsByTagNameNS("*", "p") || [])) {
12737
- const id = attr3(p, "w14:paraId", "paraId");
12738
- if (id) ids.add(id.toUpperCase());
12739
- }
12740
- for (const ex of Array.from(extendedDoc?.getElementsByTagNameNS("*", "commentEx") || [])) {
12741
- const id = attr3(ex, "w15:paraId", "paraId");
12742
- if (id) ids.add(id.toUpperCase());
12743
- }
12744
- return ids;
12745
- }
12746
- function allocateParaId(commentId, occupied) {
12747
- let candidate = createCommentParaId(commentId);
12748
- let value = Number.parseInt(candidate, 16) >>> 0;
12749
- while (occupied.has(candidate)) {
12750
- value = value + 1 >>> 0;
12751
- candidate = value.toString(16).toUpperCase().padStart(8, "0");
12752
- }
12753
- occupied.add(candidate);
12754
- return candidate;
12755
- }
12756
- function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, parentCommentId, commentId, commentContent, author, date = (/* @__PURE__ */ new Date()).toISOString() }) {
12964
+ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, documentXml = null, parentCommentId, commentId, commentContent, author, date = (/* @__PURE__ */ new Date()).toISOString() }) {
12757
12965
  if (!commentsXml) return { status: "error", error: { code: "COMMENTS_PART_MISSING", message: "A comment reply requires an existing word/comments.xml part." } };
12758
12966
  const commentsParsed = parseRequired(commentsXml, "word/comments.xml");
12759
12967
  if (commentsParsed.error) return { status: "error", error: commentsParsed.error };
@@ -12767,15 +12975,31 @@ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, par
12767
12975
  extendedDoc = parsed.doc;
12768
12976
  }
12769
12977
  const occupied = usedParaIds(commentsDoc, extendedDoc);
12770
- const parentParagraph = Array.from(parent.getElementsByTagNameNS(NS_W6, "p"))[0] || Array.from(parent.getElementsByTagNameNS("*", "p"))[0];
12978
+ if (documentXml) for (const match of String(documentXml).matchAll(/w14:paraId="([0-9A-Fa-f]{8})"/g)) occupied.add(match[1].toUpperCase());
12979
+ const parentParagraph = commentThreadParagraph(parent);
12771
12980
  if (!parentParagraph) return { status: "error", error: { code: "PARENT_COMMENT_INVALID", message: `Parent comment '${parentCommentId}' has no paragraph.` } };
12772
- let parentParaId = attr3(parentParagraph, "w14:paraId", "paraId");
12981
+ let parentParaId = commentParaId(parent);
12773
12982
  if (!parentParaId) {
12774
12983
  parentParaId = allocateParaId(parentCommentId, occupied);
12775
12984
  parentParagraph.setAttributeNS(NS_W14, "w14:paraId", parentParaId);
12776
12985
  } else {
12777
12986
  parentParaId = parentParaId.toUpperCase();
12778
12987
  }
12988
+ for (const node of Array.from(extendedDoc?.getElementsByTagNameNS("*", "commentEx") || [])) {
12989
+ if (attr3(node, "w15:paraId", "paraId").toUpperCase() === parentParaId) {
12990
+ const root2 = attr3(node, "w15:paraIdParent", "paraIdParent").toUpperCase();
12991
+ if (root2) parentParaId = root2;
12992
+ break;
12993
+ }
12994
+ }
12995
+ const threadCommentIds = [];
12996
+ const commentNodes = Array.from(commentsDoc.getElementsByTagNameNS("*", "comment"));
12997
+ const parentOf = new Map(Array.from(extendedDoc?.getElementsByTagNameNS("*", "commentEx") || []).map((node) => [attr3(node, "w15:paraId", "paraId").toUpperCase(), attr3(node, "w15:paraIdParent", "paraIdParent").toUpperCase()]));
12998
+ for (const node of commentNodes) {
12999
+ const key = commentParaId(node);
13000
+ if (key === parentParaId || parentOf.get(key) === parentParaId) threadCommentIds.push(attr3(node, "w:id", "id"));
13001
+ }
13002
+ const threadRootCommentId = attr3(commentNodes.find((node) => commentParaId(node) === parentParaId), "w:id", "id") || String(parentCommentId);
12779
13003
  const replyParaId = allocateParaId(commentId, occupied);
12780
13004
  const replyParsed = parseRequired(`<w:comments xmlns:w="${NS_W6}" xmlns:w14="${NS_W14}">${buildCommentElement(commentId, author, commentContent, date, replyParaId)}</w:comments>`, "reply comment");
12781
13005
  commentsDoc.documentElement.appendChild(commentsDoc.importNode(replyParsed.doc.documentElement.firstChild, true));
@@ -12784,16 +13008,18 @@ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, par
12784
13008
  }
12785
13009
  const root = extendedDoc.documentElement;
12786
13010
  const entries = Array.from(root.getElementsByTagNameNS("*", "commentEx"));
12787
- if (!entries.some((node) => attr3(node, "w15:paraId", "paraId").toUpperCase() === parentParaId)) {
13011
+ const rootEntry = entries.find((node) => attr3(node, "w15:paraId", "paraId").toUpperCase() === parentParaId);
13012
+ if (!rootEntry) {
12788
13013
  const parentEx = extendedDoc.createElementNS(NS_W15, "w15:commentEx");
12789
13014
  parentEx.setAttributeNS(NS_W15, "w15:paraId", parentParaId);
12790
13015
  parentEx.setAttributeNS(NS_W15, "w15:done", "0");
12791
13016
  root.appendChild(parentEx);
12792
13017
  }
13018
+ const threadDone = attr3(rootEntry, "w15:done", "done") === "1" ? "1" : "0";
12793
13019
  const replyEx = extendedDoc.createElementNS(NS_W15, "w15:commentEx");
12794
13020
  replyEx.setAttributeNS(NS_W15, "w15:paraId", replyParaId);
12795
13021
  replyEx.setAttributeNS(NS_W15, "w15:paraIdParent", parentParaId);
12796
- replyEx.setAttributeNS(NS_W15, "w15:done", "0");
13022
+ replyEx.setAttributeNS(NS_W15, "w15:done", threadDone);
12797
13023
  root.appendChild(replyEx);
12798
13024
  const serializer = createSerializer();
12799
13025
  return {
@@ -12806,7 +13032,9 @@ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, par
12806
13032
  commentId,
12807
13033
  parentCommentId: String(parentCommentId),
12808
13034
  paraId: replyParaId,
12809
- parentParaId
13035
+ parentParaId,
13036
+ threadRootCommentId,
13037
+ threadCommentIds: threadCommentIds.length ? threadCommentIds : [threadRootCommentId]
12810
13038
  };
12811
13039
  }
12812
13040
 
@@ -12871,19 +13099,19 @@ function applyFormattingRemovalToOoxml(ooxmlString, targetText, formatTypes) {
12871
13099
  if (!targetText || !ooxmlString) return ooxmlString;
12872
13100
  const doc = parseOoxml(ooxmlString);
12873
13101
  if (!doc) return ooxmlString;
12874
- const NS_W10 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
12875
- const runs = doc.getElementsByTagNameNS(NS_W10, "r");
12876
- const insertions = doc.getElementsByTagNameNS(NS_W10, "ins");
13102
+ const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
13103
+ const runs = doc.getElementsByTagNameNS(NS_W11, "r");
13104
+ const insertions = doc.getElementsByTagNameNS(NS_W11, "ins");
12877
13105
  const allRuns = [...Array.from(runs)];
12878
13106
  for (const ins of insertions) {
12879
- const insideRuns = ins.getElementsByTagNameNS(NS_W10, "r");
13107
+ const insideRuns = ins.getElementsByTagNameNS(NS_W11, "r");
12880
13108
  allRuns.push(...Array.from(insideRuns));
12881
13109
  }
12882
13110
  for (const run of allRuns) {
12883
- const textNodes = run.getElementsByTagNameNS(NS_W10, "t");
13111
+ const textNodes = run.getElementsByTagNameNS(NS_W11, "t");
12884
13112
  const runText = Array.from(textNodes).map((t) => t.textContent).join("");
12885
13113
  if (runText.includes(targetText) || runText === targetText) {
12886
- const rPrElements = run.getElementsByTagNameNS(NS_W10, "rPr");
13114
+ const rPrElements = run.getElementsByTagNameNS(NS_W11, "rPr");
12887
13115
  if (rPrElements.length > 0) {
12888
13116
  const rPr = rPrElements[0];
12889
13117
  const newRPr = removeFormattingFromRPr(rPr, formatTypes);
@@ -12918,7 +13146,7 @@ var HIGHLIGHT_COLOR_MAP = {
12918
13146
  "white": "white"
12919
13147
  };
12920
13148
  function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
12921
- const NS_W10 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
13149
+ const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
12922
13150
  const ooxmlColor = HIGHLIGHT_COLOR_MAP[color.toLowerCase()] || "yellow";
12923
13151
  const generateRedlines = options?.generateRedlines ?? false;
12924
13152
  const author = options?.author || getDefaultAuthor();
@@ -12937,10 +13165,10 @@ function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
12937
13165
  }
12938
13166
  });
12939
13167
  }
12940
- const existingHighlight = rPrElement.getElementsByTagNameNS(NS_W10, "highlight");
13168
+ const existingHighlight = rPrElement.getElementsByTagNameNS(NS_W11, "highlight");
12941
13169
  Array.from(existingHighlight).forEach(removeNode2);
12942
13170
  const highlightEl = createWordElement(doc, "w:highlight");
12943
- highlightEl.setAttributeNS(NS_W10, "w:val", ooxmlColor);
13171
+ highlightEl.setAttributeNS(NS_W11, "w:val", ooxmlColor);
12944
13172
  rPrElement.appendChild(highlightEl);
12945
13173
  if (generateRedlines && previousRPrState) {
12946
13174
  const rPrChange = createWordElement(doc, "w:rPrChange");
@@ -12949,7 +13177,7 @@ function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
12949
13177
  rPrChange.setAttribute("w:author", metadata.author);
12950
13178
  rPrChange.setAttribute("w:date", metadata.date);
12951
13179
  rPrChange.appendChild(previousRPrState);
12952
- const existingChange = rPrElement.getElementsByTagNameNS(NS_W10, "rPrChange");
13180
+ const existingChange = rPrElement.getElementsByTagNameNS(NS_W11, "rPrChange");
12953
13181
  Array.from(existingChange).forEach(removeNode2);
12954
13182
  rPrElement.appendChild(rPrChange);
12955
13183
  }
@@ -12966,12 +13194,12 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
12966
13194
  } else {
12967
13195
  revisionIdAllocator = createRevisionIdAllocator(doc);
12968
13196
  }
12969
- const NS_W10 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
13197
+ const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
12970
13198
  const getRunText = (run) => {
12971
- const textNodes = run.getElementsByTagNameNS(NS_W10, "t");
13199
+ const textNodes = run.getElementsByTagNameNS(NS_W11, "t");
12972
13200
  return Array.from(textNodes).map((t) => t.textContent).join("");
12973
13201
  };
12974
- const allRuns = Array.from(doc.getElementsByTagNameNS(NS_W10, "r"));
13202
+ const allRuns = Array.from(doc.getElementsByTagNameNS(NS_W11, "r"));
12975
13203
  const sourceRunsWithClaimedRevisionIds = /* @__PURE__ */ new WeakSet();
12976
13204
  const cloneRunWithText2 = (sourceRun, text2, shouldHighlight) => {
12977
13205
  const clonedRun = sourceRun.cloneNode(true);
@@ -12980,14 +13208,14 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
12980
13208
  } else {
12981
13209
  sourceRunsWithClaimedRevisionIds.add(sourceRun);
12982
13210
  }
12983
- const textNodes = clonedRun.getElementsByTagNameNS(NS_W10, "t");
13211
+ const textNodes = clonedRun.getElementsByTagNameNS(NS_W11, "t");
12984
13212
  Array.from(textNodes).forEach(removeNode2);
12985
13213
  const newText = createWordElement(doc, "w:t");
12986
13214
  newText.setAttribute("xml:space", "preserve");
12987
13215
  newText.textContent = text2;
12988
13216
  clonedRun.appendChild(newText);
12989
13217
  if (shouldHighlight) {
12990
- const rPrElements = clonedRun.getElementsByTagNameNS(NS_W10, "rPr");
13218
+ const rPrElements = clonedRun.getElementsByTagNameNS(NS_W11, "rPr");
12991
13219
  const existingRPr = rPrElements.length > 0 ? rPrElements[0] : null;
12992
13220
  const newRPr = injectHighlightIntoRPr(doc, existingRPr, color, options);
12993
13221
  if (existingRPr) {
@@ -13036,20 +13264,68 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
13036
13264
  return serializeOoxml(doc);
13037
13265
  }
13038
13266
 
13267
+ // services/package-parts.js
13268
+ var WORD_ML = "application/vnd.openxmlformats-officedocument.wordprocessingml";
13269
+ var REL_OFFICE_DOC = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
13270
+ var PART_SPECS = Object.freeze([
13271
+ { id: "document", path: "word/document.xml", pathPattern: null, contentType: `${WORD_ML}.document.main+xml`, legacyContentTypes: [], relType: `${REL_OFFICE_DOC}/officeDocument`, relTarget: null, verified: true },
13272
+ { id: "numbering", path: "word/numbering.xml", pathPattern: null, contentType: `${WORD_ML}.numbering+xml`, legacyContentTypes: [], relType: `${REL_OFFICE_DOC}/numbering`, relTarget: "numbering.xml", verified: false },
13273
+ { id: "comments", path: "word/comments.xml", pathPattern: null, contentType: `${WORD_ML}.comments+xml`, legacyContentTypes: [], relType: `${REL_OFFICE_DOC}/comments`, relTarget: "comments.xml", verified: true },
13274
+ {
13275
+ id: "commentsExtended",
13276
+ path: "word/commentsExtended.xml",
13277
+ pathPattern: null,
13278
+ contentType: `${WORD_ML}.commentsExtended+xml`,
13279
+ // Written by this package through 0.8.0; Word does not recognize it and offers to repair the file.
13280
+ legacyContentTypes: ["application/vnd.ms-word.commentsExtended+xml"],
13281
+ relType: "http://schemas.microsoft.com/office/2011/relationships/commentsExtended",
13282
+ relTarget: "commentsExtended.xml",
13283
+ verified: true
13284
+ },
13285
+ { 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 },
13286
+ { 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 },
13287
+ // Not yet produced by any Word fixture (Word only writes people.xml for signed-in authors): unverified.
13288
+ { 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 },
13289
+ { 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 },
13290
+ { 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 }
13291
+ ].map((spec) => Object.freeze(spec)));
13292
+ var BY_ID = new Map(PART_SPECS.map((spec) => [spec.id, spec]));
13293
+ function getPartSpec(id) {
13294
+ const spec = BY_ID.get(id);
13295
+ if (!spec) throw new Error(`Unknown package part: ${id}`);
13296
+ return spec;
13297
+ }
13298
+ function toEntryName(partName) {
13299
+ return String(partName || "").replace(/^\/+/, "");
13300
+ }
13301
+ function findPartSpecByPartName(partName) {
13302
+ const name = toEntryName(partName);
13303
+ const lower = name.toLowerCase();
13304
+ return PART_SPECS.find((spec) => spec.path ? spec.path.toLowerCase() === lower : spec.pathPattern?.test(name)) || null;
13305
+ }
13306
+ function correctedContentTypeFor(partName, actualContentType) {
13307
+ const spec = findPartSpecByPartName(partName);
13308
+ if (!spec || !spec.legacyContentTypes.includes(actualContentType)) return null;
13309
+ return spec.contentType;
13310
+ }
13311
+
13039
13312
  // services/standalone-docx-plumbing.js
13040
13313
  var NS_W7 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
13041
13314
  var NS_CT = "http://schemas.openxmlformats.org/package/2006/content-types";
13042
13315
  var NS_RELS = "http://schemas.openxmlformats.org/package/2006/relationships";
13043
- var NUMBERING_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering";
13044
- var NUMBERING_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml";
13045
- var COMMENTS_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
13046
- var COMMENTS_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml";
13047
- var COMMENTS_EXTENDED_REL_TYPE = "http://schemas.microsoft.com/office/2011/relationships/commentsExtended";
13048
- var COMMENTS_EXTENDED_CONTENT_TYPE = "application/vnd.ms-word.commentsExtended+xml";
13049
- var DOCUMENT_PATH = "word/document.xml";
13050
- var NUMBERING_PATH = "word/numbering.xml";
13051
- var COMMENTS_PATH = "word/comments.xml";
13052
- var COMMENTS_EXTENDED_PATH = "word/commentsExtended.xml";
13316
+ var NUMBERING = getPartSpec("numbering");
13317
+ var COMMENTS = getPartSpec("comments");
13318
+ var COMMENTS_EXTENDED = getPartSpec("commentsExtended");
13319
+ var NUMBERING_REL_TYPE = NUMBERING.relType;
13320
+ var NUMBERING_CONTENT_TYPE = NUMBERING.contentType;
13321
+ var COMMENTS_REL_TYPE = COMMENTS.relType;
13322
+ var COMMENTS_CONTENT_TYPE = COMMENTS.contentType;
13323
+ var COMMENTS_EXTENDED_REL_TYPE = COMMENTS_EXTENDED.relType;
13324
+ var COMMENTS_EXTENDED_CONTENT_TYPE = COMMENTS_EXTENDED.contentType;
13325
+ var DOCUMENT_PATH = getPartSpec("document").path;
13326
+ var NUMBERING_PATH = NUMBERING.path;
13327
+ var COMMENTS_PATH = COMMENTS.path;
13328
+ var COMMENTS_EXTENDED_PATH = COMMENTS_EXTENDED.path;
13053
13329
  var CONTENT_TYPES_PATH = "[Content_Types].xml";
13054
13330
  var DOCUMENT_RELS_PATH = "word/_rels/document.xml.rels";
13055
13331
  function parseXmlStrictStandalone(xmlText, label = "xml") {
@@ -13337,6 +13613,22 @@ async function ensureCommentsExtendedArtifactsInZip(zip, commentsExtendedXml, op
13337
13613
  }
13338
13614
  }
13339
13615
  }
13616
+ async function repairKnownContentTypes(zip) {
13617
+ const ctText = await readZipText(zip, CONTENT_TYPES_PATH);
13618
+ if (!ctText) return [];
13619
+ const ctDoc = parseXmlStrictStandalone(ctText, CONTENT_TYPES_PATH);
13620
+ const repairs = [];
13621
+ for (const override of Array.from(ctDoc.getElementsByTagNameNS("*", "Override"))) {
13622
+ const partName = override.getAttribute("PartName") || "";
13623
+ const actual = override.getAttribute("ContentType") || "";
13624
+ const expected = correctedContentTypeFor(partName, actual);
13625
+ if (!expected) continue;
13626
+ override.setAttribute("ContentType", expected);
13627
+ repairs.push({ partName, from: actual, to: expected });
13628
+ }
13629
+ if (repairs.length > 0) zip.file(CONTENT_TYPES_PATH, createSerializer().serializeToString(ctDoc));
13630
+ return repairs;
13631
+ }
13340
13632
  async function validateDocxPackage(zip) {
13341
13633
  const documentXml = await readZipText(zip, DOCUMENT_PATH);
13342
13634
  if (!documentXml) {
@@ -13418,9 +13710,8 @@ async function validateDocxPackage(zip) {
13418
13710
  const paraIdToCommentId = /* @__PURE__ */ new Map();
13419
13711
  for (const comment of Array.from(commentsDoc.getElementsByTagNameNS("*", "comment"))) {
13420
13712
  const id = comment.getAttribute("w:id") || comment.getAttribute("id");
13421
- const paragraph = Array.from(comment.getElementsByTagNameNS("*", "p"))[0];
13422
- const paraId = paragraph?.getAttribute("w14:paraId") || paragraph?.getAttribute("paraId");
13423
- if (paraId) paraIdToCommentId.set(paraId.toUpperCase(), id);
13713
+ const paraId = commentParaId(comment);
13714
+ if (paraId) paraIdToCommentId.set(paraId, id);
13424
13715
  }
13425
13716
  const knownParaIds = new Set(paraIdToCommentId.keys());
13426
13717
  const extendedParaIds = /* @__PURE__ */ new Set();
@@ -13481,10 +13772,39 @@ async function validateDocxPackage(zip) {
13481
13772
  throw new Error("Validation failed: comments rel missing");
13482
13773
  }
13483
13774
  }
13775
+ {
13776
+ const relationshipById = /* @__PURE__ */ new Map();
13777
+ for (const rel of Array.from(relsDoc.getElementsByTagNameNS("*", "Relationship"))) {
13778
+ relationshipById.set(rel.getAttribute("Id") || "", { type: rel.getAttribute("Type") || "", target: rel.getAttribute("Target") || "" });
13779
+ }
13780
+ const headerSpec = getPartSpec("header");
13781
+ const footerSpec = getPartSpec("footer");
13782
+ const overrideTypeByPart = new Map(Array.from(ctDoc.getElementsByTagNameNS("*", "Override")).map((override) => [(override.getAttribute("PartName") || "").toLowerCase(), override.getAttribute("ContentType") || ""]));
13783
+ for (const reference of [
13784
+ ...Array.from(documentDoc.getElementsByTagNameNS(NS_W7, "headerReference")),
13785
+ ...Array.from(documentDoc.getElementsByTagNameNS(NS_W7, "footerReference"))
13786
+ ]) {
13787
+ const relId = reference.getAttributeNS?.("http://schemas.openxmlformats.org/officeDocument/2006/relationships", "id") || reference.getAttribute("r:id") || "";
13788
+ const spec = reference.localName === "headerReference" ? headerSpec : footerSpec;
13789
+ const relationship = relationshipById.get(relId);
13790
+ if (!relationship) throw new Error(`Validation failed: ${reference.localName} ${relId || "(no r:id)"} has no document relationship`);
13791
+ if (relationship.type !== spec.relType) throw new Error(`Validation failed: ${reference.localName} ${relId} points at a relationship of the wrong type (${relationship.type})`);
13792
+ const partName = relationship.target.startsWith("/") ? relationship.target : `/word/${relationship.target}`;
13793
+ const partXml = await readZipText(zip, partName.slice(1));
13794
+ if (!partXml) throw new Error(`Validation failed: ${reference.localName} ${relId} targets a missing part: ${partName}`);
13795
+ parseXmlStrictStandalone(partXml, partName.slice(1));
13796
+ const contentType = overrideTypeByPart.get(partName.toLowerCase());
13797
+ if (contentType !== spec.contentType) throw new Error(`Validation failed: ${partName} content type is ${contentType ? `'${contentType}'` : "missing"}; Word expects '${spec.contentType}'`);
13798
+ }
13799
+ }
13484
13800
  if (commentsExtendedXml) {
13485
- const hasContentType = Array.from(ctDoc.getElementsByTagNameNS("*", "Override")).some(
13486
- (override) => (override.getAttribute("PartName") || "").toLowerCase() === "/word/commentsextended.xml" && (override.getAttribute("ContentType") || "") === COMMENTS_EXTENDED_CONTENT_TYPE
13801
+ const extendedOverride = Array.from(ctDoc.getElementsByTagNameNS("*", "Override")).find(
13802
+ (override) => (override.getAttribute("PartName") || "").toLowerCase() === "/word/commentsextended.xml"
13487
13803
  );
13804
+ if (extendedOverride && extendedOverride.getAttribute("ContentType") !== COMMENTS_EXTENDED_CONTENT_TYPE) {
13805
+ throw new Error(`Validation failed: commentsExtended CT override has wrong content type '${extendedOverride.getAttribute("ContentType")}' (Word expects '${COMMENTS_EXTENDED_CONTENT_TYPE}')`);
13806
+ }
13807
+ const hasContentType = !!extendedOverride;
13488
13808
  const hasRelationship = Array.from(relsDoc.getElementsByTagNameNS("*", "Relationship")).some(
13489
13809
  (rel) => (rel.getAttribute("Type") || "") === COMMENTS_EXTENDED_REL_TYPE
13490
13810
  );
@@ -13848,6 +14168,156 @@ function reconcileReceiptsAgainstOutput(parts, receipts) {
13848
14168
  return { valid: true };
13849
14169
  }
13850
14170
 
14171
+ // services/headers-footers.js
14172
+ var NS_W8 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
14173
+ var NS_R2 = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
14174
+ var REVISION_ELEMENTS = /* @__PURE__ */ new Set(["ins", "del", "moveFrom", "moveTo", "rPrChange", "pPrChange", "sectPrChange", "tblPrChange", "tblGridChange", "tcPrChange", "trPrChange", "numberingChange"]);
14175
+ var REVISION_CONTENT_ELEMENTS = /* @__PURE__ */ new Set(["ins", "del", "moveFrom", "moveTo"]);
14176
+ var attrOf2 = (node, qualified, local) => node?.getAttribute?.(qualified) || node?.getAttribute?.(local) || "";
14177
+ function resolveTarget(target) {
14178
+ const clean = String(target || "");
14179
+ if (clean.startsWith("/")) return clean.replace(/^\/+/, "");
14180
+ const parts = ["word"];
14181
+ for (const segment of clean.split("/")) {
14182
+ if (segment === "..") parts.pop();
14183
+ else if (segment && segment !== ".") parts.push(segment);
14184
+ }
14185
+ return parts.join("/");
14186
+ }
14187
+ function discoverHeaderFooterParts({ documentXml, relsXml = null, settingsXml = null }) {
14188
+ if (!documentXml || !relsXml) return [];
14189
+ const documentParsed = parseOoxmlSafe(documentXml, "application/xml");
14190
+ const relsParsed = parseOoxmlSafe(relsXml, "application/xml");
14191
+ if (!documentParsed.doc || documentParsed.error || !relsParsed.doc || relsParsed.error) return [];
14192
+ const targetByRelId = /* @__PURE__ */ new Map();
14193
+ for (const rel of Array.from(relsParsed.doc.getElementsByTagNameNS("*", "Relationship"))) {
14194
+ targetByRelId.set(rel.getAttribute("Id") || "", rel.getAttribute("Target") || "");
14195
+ }
14196
+ const evenAndOdd = !!settingsXml && /<w:evenAndOddHeaders\b(?![^>]*w:val="(?:0|false|off)")/.test(settingsXml);
14197
+ const byPath = /* @__PURE__ */ new Map();
14198
+ const inForce = /* @__PURE__ */ new Map();
14199
+ const sectPrs = Array.from(documentParsed.doc.getElementsByTagNameNS(NS_W8, "sectPr"));
14200
+ sectPrs.forEach((sectPr, sectionIndex) => {
14201
+ const titlePg = Array.from(sectPr.childNodes || []).some((child) => child.localName === "titlePg" && !/^(0|false|off)$/i.test(attrOf2(child, "w:val", "val")));
14202
+ for (const ref of Array.from(sectPr.childNodes || [])) {
14203
+ if (ref.nodeType !== 1 || ref.localName !== "headerReference" && ref.localName !== "footerReference") continue;
14204
+ const relId = ref.getAttributeNS?.(NS_R2, "id") || ref.getAttribute("r:id") || "";
14205
+ const target = targetByRelId.get(relId);
14206
+ if (!target) continue;
14207
+ const path = resolveTarget(target);
14208
+ const kind = ref.localName === "headerReference" ? "header" : "footer";
14209
+ const type = attrOf2(ref, "w:type", "type") || "default";
14210
+ const entry = byPath.get(path) || { path, kind, type, sections: [], applies: [], titlePgSections: 0 };
14211
+ if (!entry.sections.includes(sectionIndex)) entry.sections.push(sectionIndex);
14212
+ if (titlePg) entry.titlePgSections += 1;
14213
+ byPath.set(path, entry);
14214
+ inForce.set(`${kind}:${type}`, path);
14215
+ }
14216
+ for (const path of inForce.values()) {
14217
+ const entry = byPath.get(path);
14218
+ if (entry && !entry.applies.includes(sectionIndex)) entry.applies.push(sectionIndex);
14219
+ }
14220
+ });
14221
+ return Array.from(byPath.values()).map((entry) => ({
14222
+ path: entry.path,
14223
+ kind: entry.kind,
14224
+ type: entry.type,
14225
+ sections: entry.sections,
14226
+ appliesToSections: entry.applies,
14227
+ sharedBySections: entry.applies.length > 1,
14228
+ // first-page parts only show with w:titlePg; even parts only with w:evenAndOddHeaders in settings.xml.
14229
+ active: entry.type === "first" ? entry.titlePgSections > 0 : entry.type === "even" ? evenAndOdd : true
14230
+ }));
14231
+ }
14232
+ function resolvePartSelector(parts, selector) {
14233
+ const describe = (part) => ({ path: part.path, kind: part.kind, type: part.type, sections: part.sections, appliesToSections: part.appliesToSections });
14234
+ if (typeof selector === "string") {
14235
+ const wanted = selector.replace(/^\/+/, "").toLowerCase();
14236
+ const match = parts.find((part) => part.path.toLowerCase() === wanted || part.path.toLowerCase() === `word/${wanted}`);
14237
+ if (match) return { part: match };
14238
+ return { error: { code: "PART_NOT_FOUND", message: `No header/footer part '${selector}' is referenced by the document.`, candidates: parts.map(describe) } };
14239
+ }
14240
+ if (!selector || typeof selector !== "object" || selector.kind !== "header" && selector.kind !== "footer") {
14241
+ return { error: { code: "INVALID_OPERATION", message: 'part must be a part path or { kind: "header"|"footer", type?: "default"|"first"|"even", section?: number }.' } };
14242
+ }
14243
+ const type = selector.type || "default";
14244
+ if (!["default", "first", "even"].includes(type)) {
14245
+ return { error: { code: "INVALID_OPERATION", message: `part.type must be default, first or even (got '${type}').` } };
14246
+ }
14247
+ let matches = parts.filter((part) => part.kind === selector.kind && part.type === type);
14248
+ if (Number.isInteger(selector.section)) matches = matches.filter((part) => part.appliesToSections.includes(selector.section));
14249
+ if (matches.length === 1) return { part: matches[0] };
14250
+ if (matches.length === 0) {
14251
+ 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) } };
14252
+ }
14253
+ return { error: { code: "PART_AMBIGUOUS", message: `Several ${type} ${selector.kind} parts exist; add "section" or use the part path.`, candidates: matches.map(describe) } };
14254
+ }
14255
+ function isHeaderFooterPath(path) {
14256
+ const spec = findPartSpecByPartName(path);
14257
+ return spec?.id === "header" || spec?.id === "footer";
14258
+ }
14259
+ function countRevisionsInsideFields(xml) {
14260
+ const parsed = parseOoxmlSafe(xml, "application/xml");
14261
+ if (!parsed.doc || parsed.error) return 0;
14262
+ let depth = 0;
14263
+ let count = 0;
14264
+ const visit = (node) => {
14265
+ for (const child of Array.from(node.childNodes || [])) {
14266
+ if (child.nodeType !== 1) continue;
14267
+ const name = child.localName;
14268
+ if (name === "fldChar") {
14269
+ const type = attrOf2(child, "w:fldCharType", "fldCharType");
14270
+ if (type === "begin") depth += 1;
14271
+ else if (type === "end") depth = Math.max(0, depth - 1);
14272
+ continue;
14273
+ }
14274
+ if (REVISION_CONTENT_ELEMENTS.has(name) && depth > 0) count += 1;
14275
+ if (name === "fldSimple") {
14276
+ depth += 1;
14277
+ visit(child);
14278
+ depth -= 1;
14279
+ continue;
14280
+ }
14281
+ visit(child);
14282
+ }
14283
+ };
14284
+ visit(parsed.doc.documentElement);
14285
+ return count;
14286
+ }
14287
+ function collectRevisionIds(xml) {
14288
+ const ids = /* @__PURE__ */ new Set();
14289
+ 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]);
14290
+ return ids;
14291
+ }
14292
+ function renumberCollidingRevisionIds(xml, taken) {
14293
+ const parsed = parseOoxmlSafe(xml, "application/xml");
14294
+ if (!parsed.doc || parsed.error) return { xml, renumbered: 0, idMap: /* @__PURE__ */ new Map() };
14295
+ const idMap = /* @__PURE__ */ new Map();
14296
+ let next = 1 + Math.max(0, ...Array.from(taken, (id) => Number(id)).filter(Number.isFinite));
14297
+ const used = new Set(taken);
14298
+ let renumbered = 0;
14299
+ const visit = (node) => {
14300
+ for (const child of Array.from(node.childNodes || [])) {
14301
+ if (child.nodeType !== 1) continue;
14302
+ if (REVISION_ELEMENTS.has(child.localName)) {
14303
+ const id = attrOf2(child, "w:id", "id");
14304
+ if (id && used.has(id)) {
14305
+ while (used.has(String(next))) next += 1;
14306
+ child.setAttributeNS(NS_W8, "w:id", String(next));
14307
+ idMap.set(id, String(next));
14308
+ used.add(String(next));
14309
+ renumbered += 1;
14310
+ } else if (id) {
14311
+ used.add(id);
14312
+ }
14313
+ }
14314
+ visit(child);
14315
+ }
14316
+ };
14317
+ visit(parsed.doc.documentElement);
14318
+ return renumbered > 0 ? { xml: createSerializer().serializeToString(parsed.doc), renumbered, idMap } : { xml, renumbered: 0, idMap };
14319
+ }
14320
+
13851
14321
  // services/error-recovery.js
13852
14322
  var ERROR_RECOVERY_VERSION = 1;
13853
14323
  var RULES = Object.freeze({
@@ -13885,6 +14355,12 @@ var RULES = Object.freeze({
13885
14355
  STRUCTURED_CONTENT_INVALID: ["request", "request_fixable", "change_request"],
13886
14356
  UNSUPPORTED_INSERTION_AFFINITY: ["request", "request_fixable", "change_request"],
13887
14357
  EXISTING_REVISIONS: ["target-safety", "policy_choice_required", "set_option"],
14358
+ PART_NOT_FOUND: ["target-resolution", "target_refresh_required", "reinspect"],
14359
+ PART_AMBIGUOUS: ["target-resolution", "candidate_selection_required", "choose_candidate"],
14360
+ COMMENT_IN_HEADER_FOOTER: ["request", "request_fixable", "change_request"],
14361
+ FIELD_EDIT_REFUSED: ["target-safety", "request_fixable", "change_request"],
14362
+ COMMENT_NOT_FOUND: ["target-resolution", "target_refresh_required", "reinspect"],
14363
+ PARENT_ANCHOR_NOT_FOUND: ["target-resolution", "target_refresh_required", "reinspect"],
13888
14364
  COMMENTED_CONTENT_MERGE: ["target-safety", "user_authorization_required", "resolve_comments"],
13889
14365
  COMMENTED_CONTENT_DELETE: ["target-safety", "user_authorization_required", "resolve_comments"],
13890
14366
  OVERLAPPING_SOURCE_TARGETS: ["batch-compilation", "source_conflict", "consolidate_operations"],
@@ -14337,6 +14813,7 @@ var SUPPORTED_OPERATION_TYPES = /* @__PURE__ */ new Set([
14337
14813
  "delete",
14338
14814
  "comment",
14339
14815
  "comment_reply",
14816
+ "comment_resolve",
14340
14817
  "highlight"
14341
14818
  ]);
14342
14819
  var EXISTING_REVISIONS_POLICIES = Object.freeze([
@@ -14355,11 +14832,14 @@ function isRecord(value) {
14355
14832
  function nonEmptyString(value) {
14356
14833
  return typeof value === "string" && value.trim().length > 0;
14357
14834
  }
14835
+ function isCommentThreadOperationKind(kind) {
14836
+ return kind === "comment_reply" || kind === "comment_resolve";
14837
+ }
14358
14838
  function getCanonicalOperationType(operation) {
14359
14839
  const type = operation?.type;
14360
14840
  if (type === "insert" && operation?.target?.revisionView === "rejected") return "rejected-insert";
14361
14841
  if (type === "restore") return "restore";
14362
- if (type === "comment" || type === "comment_reply" || type === "highlight") return type;
14842
+ if (type === "comment" || type === "comment_reply" || type === "comment_resolve" || type === "highlight") return type;
14363
14843
  if (type === "paragraph-format") return "paragraph-format";
14364
14844
  if (type === "character-format" || type === "format" && (operation?.textToFormat != null || operation?.properties != null)) return "format";
14365
14845
  return "redline";
@@ -14479,7 +14959,7 @@ function validateDocumentOperation(operation) {
14479
14959
  };
14480
14960
  }
14481
14961
  const target = normalized.targetDescriptor;
14482
- if (normalized.operationKind !== "comment_reply" && !nonEmptyString(target.text) && target.index == null && !target.paragraphId && !target.fingerprint && !target.captureRef) {
14962
+ if (!isCommentThreadOperationKind(normalized.operationKind) && !nonEmptyString(target.text) && target.index == null && !target.paragraphId && !target.fingerprint && !target.captureRef) {
14483
14963
  return {
14484
14964
  valid: false,
14485
14965
  error: {
@@ -14706,6 +15186,14 @@ function validateDocumentOperation(operation) {
14706
15186
  return { valid: false, error: { code: "INVALID_OPERATION", message: 'Comment reply operations require a non-empty "commentContent" field.' } };
14707
15187
  }
14708
15188
  }
15189
+ if (normalized.operationKind === "comment_resolve") {
15190
+ if (!nonEmptyString(String(normalized.commentId ?? ""))) {
15191
+ return { valid: false, error: { code: "INVALID_OPERATION", message: 'Comment resolve operations require a "commentId".' } };
15192
+ }
15193
+ if (normalized.resolved != null && typeof normalized.resolved !== "boolean") {
15194
+ return { valid: false, error: { code: "INVALID_OPERATION", message: 'Comment resolve "resolved" must be a boolean when provided.' } };
15195
+ }
15196
+ }
14709
15197
  if (normalized.operationKind === "highlight") {
14710
15198
  if (!nonEmptyString(normalized.textToHighlight)) {
14711
15199
  return {
@@ -15185,7 +15673,7 @@ function compileOperationBatch(xmlDoc, operations = [], options = {}) {
15185
15673
  const dynamicOccurrence = operation.targetDescriptor?.occurrence != null && !operation.targetDescriptor?.paragraphId && !operation.targetDescriptor?.fingerprint;
15186
15674
  const compositeTextTarget = typeof operation.targetDescriptor?.text === "string" && /\r|\n/.test(operation.targetDescriptor.text) && !operation.targetEndDescriptor;
15187
15675
  if (dynamicOccurrence || compositeTextTarget) binding.dynamic = true;
15188
- if (validation.valid && operation.operationKind !== "comment_reply" && !operation.targetDescriptor?.captureRef && !binding.dynamic) {
15676
+ if (validation.valid && operation.operationKind !== "comment_reply" && operation.operationKind !== "comment_resolve" && !operation.targetDescriptor?.captureRef && !binding.dynamic) {
15189
15677
  try {
15190
15678
  const start = resolveDescriptor(
15191
15679
  xmlDoc,
@@ -15686,12 +16174,12 @@ function resolveTargetFromCapture(xmlDoc, session, targetDescriptor, _opType = "
15686
16174
  }
15687
16175
 
15688
16176
  // services/document-operation-mutations.js
15689
- var NS_W8 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
16177
+ var NS_W9 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
15690
16178
  var NS_W142 = "http://schemas.microsoft.com/office/word/2010/wordml";
15691
16179
  function getCommentIdsInElement(element) {
15692
16180
  const ids = /* @__PURE__ */ new Set();
15693
16181
  for (const localName2 of ["commentRangeStart", "commentRangeEnd", "commentReference"]) {
15694
- for (const node of Array.from(element?.getElementsByTagNameNS?.(NS_W8, localName2) || [])) {
16182
+ for (const node of Array.from(element?.getElementsByTagNameNS?.(NS_W9, localName2) || [])) {
15695
16183
  const id = node.getAttribute("w:id") || node.getAttribute("id");
15696
16184
  if (id !== "") ids.add(id);
15697
16185
  }
@@ -15905,7 +16393,7 @@ function normalizeBodySectionOrder(xmlDoc) {
15905
16393
  normalizeBodySectionOrderStandalone(xmlDoc);
15906
16394
  }
15907
16395
  function removeProofErrNodes(paragraph) {
15908
- for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W8, "proofErr") || [])) {
16396
+ for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W9, "proofErr") || [])) {
15909
16397
  node.parentNode?.removeChild(node);
15910
16398
  }
15911
16399
  }
@@ -15916,12 +16404,12 @@ function preprocessRedlineTargetParagraph(targetParagraph) {
15916
16404
  function getDirectWordChild4(element, localName2) {
15917
16405
  if (!element) return null;
15918
16406
  return Array.from(element.childNodes || []).find(
15919
- (node) => node && node.nodeType === 1 && node.namespaceURI === NS_W8 && node.localName === localName2
16407
+ (node) => node && node.nodeType === 1 && node.namespaceURI === NS_W9 && node.localName === localName2
15920
16408
  ) || null;
15921
16409
  }
15922
16410
  function computeTableIndexInDocument(xmlDoc, tableElement) {
15923
16411
  if (!xmlDoc || !tableElement) return null;
15924
- const tables = Array.from(xmlDoc.getElementsByTagNameNS(NS_W8, "tbl"));
16412
+ const tables = Array.from(xmlDoc.getElementsByTagNameNS(NS_W9, "tbl"));
15925
16413
  const idx = tables.indexOf(tableElement);
15926
16414
  return idx >= 0 ? idx + 1 : null;
15927
16415
  }
@@ -15971,7 +16459,7 @@ function buildInsertedListParagraph(xmlDoc, anchorParagraph, entry, revisionMeta
15971
16459
  markParagraphMarkInserted(xmlDoc, paragraph, author);
15972
16460
  }
15973
16461
  const run = createWordElement(xmlDoc, "w:r");
15974
- const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W8, "r"))[0] || null;
16462
+ const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W9, "r"))[0] || null;
15975
16463
  const anchorRunPr = anchorFirstRun ? getDirectWordChild4(anchorFirstRun, "rPr") : null;
15976
16464
  if (anchorRunPr) {
15977
16465
  run.appendChild(clonePropertiesWithoutRevisionHistory(anchorRunPr));
@@ -15996,7 +16484,7 @@ function buildInsertedListParagraph(xmlDoc, anchorParagraph, entry, revisionMeta
15996
16484
  }
15997
16485
  function serializeParagraphRangeAsDocument(paragraphs, serializer) {
15998
16486
  const paragraphXml = (paragraphs || []).map((paragraph) => serializer.serializeToString(paragraph)).join("");
15999
- return `<w:document xmlns:w="${NS_W8}"><w:body>${paragraphXml}</w:body></w:document>`;
16487
+ return `<w:document xmlns:w="${NS_W9}"><w:body>${paragraphXml}</w:body></w:document>`;
16000
16488
  }
16001
16489
  function applyExplicitRangeListInsertions({
16002
16490
  xmlDoc,
@@ -16065,7 +16553,7 @@ function buildEmptyParagraphTemplateFromAnchor(xmlDoc, anchorParagraph) {
16065
16553
  const anchorPPr = getDirectWordChild4(anchorParagraph, "pPr");
16066
16554
  if (anchorPPr) paragraph.appendChild(clonePropertiesWithoutRevisionHistory(anchorPPr));
16067
16555
  const run = createWordElement(xmlDoc, "w:r");
16068
- const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W8, "r"))[0] || null;
16556
+ const anchorFirstRun = Array.from(anchorParagraph.getElementsByTagNameNS(NS_W9, "r"))[0] || null;
16069
16557
  const anchorRunPr = anchorFirstRun ? getDirectWordChild4(anchorFirstRun, "rPr") : null;
16070
16558
  if (anchorRunPr) run.appendChild(clonePropertiesWithoutRevisionHistory(anchorRunPr));
16071
16559
  const textNode = createWordElement(xmlDoc, "w:t");
@@ -16088,7 +16576,7 @@ function wrapParagraphContentInInsertion(xmlDoc, paragraph, revisionMetadata, au
16088
16576
  ins.setAttribute("w:author", metadata.author);
16089
16577
  ins.setAttribute("w:date", metadata.date);
16090
16578
  for (const child of Array.from(paragraph.childNodes || [])) {
16091
- if (child?.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName === "pPr") continue;
16579
+ if (child?.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName === "pPr") continue;
16092
16580
  ins.appendChild(child.cloneNode(true));
16093
16581
  }
16094
16582
  wrappedParagraph.appendChild(ins);
@@ -16112,7 +16600,7 @@ async function buildInsertedPlainParagraph(xmlDoc, anchorParagraph, text2, revis
16112
16600
  if (typeof markdownResult?.oxml === "string") {
16113
16601
  const extracted = extractReplacementNodes(markdownResult.oxml);
16114
16602
  sourceParagraph = (extracted.replacementNodes || []).find(
16115
- (node) => node && node.nodeType === 1 && node.namespaceURI === NS_W8 && node.localName === "p"
16603
+ (node) => node && node.nodeType === 1 && node.namespaceURI === NS_W9 && node.localName === "p"
16116
16604
  ) || null;
16117
16605
  }
16118
16606
  if (!sourceParagraph) {
@@ -16144,7 +16632,7 @@ function collectNumberingIdsFromNodes(nodes) {
16144
16632
  }
16145
16633
  function directDeletionCarrierText(carrier) {
16146
16634
  return Array.from(carrier?.childNodes || []).reduce((text2, child) => {
16147
- return text2 + (child?.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName === "r" ? getRunContentPieces(child).map((piece) => piece.text).join("") : "");
16635
+ return text2 + (child?.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName === "r" ? getRunContentPieces(child).map((piece) => piece.text).join("") : "");
16148
16636
  }, "");
16149
16637
  }
16150
16638
  function findOccurrenceOffset(text2, needle, occurrence) {
@@ -16186,9 +16674,9 @@ function hasUnsupportedDeletionSplitMarkup(paragraph, carrier) {
16186
16674
  }
16187
16675
  for (const child of Array.from(carrier?.childNodes || [])) {
16188
16676
  if (child.nodeType !== 1) continue;
16189
- if (!(child.namespaceURI === NS_W8 && child.localName === "r")) return true;
16677
+ if (!(child.namespaceURI === NS_W9 && child.localName === "r")) return true;
16190
16678
  for (const runChild of Array.from(child.childNodes || [])) {
16191
- if (runChild.nodeType !== 1 || runChild.namespaceURI !== NS_W8) continue;
16679
+ if (runChild.nodeType !== 1 || runChild.namespaceURI !== NS_W9) continue;
16192
16680
  if (!["rPr", "delText", "t", "tab", "br", "cr", "noBreakHyphen", "softHyphen"].includes(runChild.localName)) return true;
16193
16681
  }
16194
16682
  }
@@ -16197,7 +16685,7 @@ function hasUnsupportedDeletionSplitMarkup(paragraph, carrier) {
16197
16685
  function insertedRunPropertiesAtDeletionOffset(carrier, localOffset) {
16198
16686
  let offset = 0;
16199
16687
  for (const child of Array.from(carrier?.childNodes || [])) {
16200
- if (!(child?.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName === "r")) continue;
16688
+ if (!(child?.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName === "r")) continue;
16201
16689
  const length = getRunTextLength(getRunContentPieces(child));
16202
16690
  if (localOffset <= offset + length) {
16203
16691
  return clonePropertiesWithoutRevisionHistory(getDirectWordChild4(child, "rPr"));
@@ -16227,7 +16715,7 @@ function normalizedAuthor2(author) {
16227
16715
  function directWordParagraphSibling(paragraph, direction = "nextSibling") {
16228
16716
  let cursor = paragraph?.[direction] || null;
16229
16717
  while (cursor) {
16230
- if (cursor.nodeType === 1 && cursor.namespaceURI === NS_W8 && cursor.localName === "p") return cursor;
16718
+ if (cursor.nodeType === 1 && cursor.namespaceURI === NS_W9 && cursor.localName === "p") return cursor;
16231
16719
  cursor = cursor[direction] || null;
16232
16720
  }
16233
16721
  return null;
@@ -16237,7 +16725,7 @@ function createSanitizedRestorationPPr(xmlDoc, sourceParagraph) {
16237
16725
  const pPr = createWordElement(xmlDoc, "w:pPr");
16238
16726
  if (!sourcePPr) return pPr;
16239
16727
  for (const child of Array.from(sourcePPr.childNodes || [])) {
16240
- if (child.nodeType !== 1 || child.namespaceURI !== NS_W8) continue;
16728
+ if (child.nodeType !== 1 || child.namespaceURI !== NS_W9) continue;
16241
16729
  if (!RESTORATION_PPR_ALLOWLIST.has(child.localName)) continue;
16242
16730
  pPr.appendChild(xmlDoc.importNode(child, true));
16243
16731
  }
@@ -16246,14 +16734,14 @@ function createSanitizedRestorationPPr(xmlDoc, sourceParagraph) {
16246
16734
  function collectRestorationAnchorWarnings(sourceParagraph) {
16247
16735
  const warnings = [];
16248
16736
  const bookmarkNames = /* @__PURE__ */ new Set();
16249
- for (const node of Array.from(sourceParagraph?.getElementsByTagNameNS?.(NS_W8, "bookmarkStart") || [])) {
16737
+ for (const node of Array.from(sourceParagraph?.getElementsByTagNameNS?.(NS_W9, "bookmarkStart") || [])) {
16250
16738
  const name = wordAttribute4(node, "name") || "(unnamed)";
16251
16739
  bookmarkNames.add(name);
16252
16740
  }
16253
16741
  for (const name of bookmarkNames) warnings.push(`RESTORATION_DROPPED_BOOKMARK:${name}`);
16254
16742
  const commentIds = /* @__PURE__ */ new Set();
16255
16743
  for (const localName2 of ["commentRangeStart", "commentRangeEnd", "commentReference"]) {
16256
- for (const node of Array.from(sourceParagraph?.getElementsByTagNameNS?.(NS_W8, localName2) || [])) {
16744
+ for (const node of Array.from(sourceParagraph?.getElementsByTagNameNS?.(NS_W9, localName2) || [])) {
16257
16745
  const id = wordAttribute4(node, "id");
16258
16746
  if (id !== "") commentIds.add(id);
16259
16747
  }
@@ -16270,24 +16758,24 @@ function removeRestorationAnchors(paragraph) {
16270
16758
  "commentReference"
16271
16759
  ];
16272
16760
  for (const localName2 of anchorNames) {
16273
- for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W8, localName2) || [])) {
16761
+ for (const node of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W9, localName2) || [])) {
16274
16762
  node.parentNode?.removeChild(node);
16275
16763
  }
16276
16764
  }
16277
- for (const run of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W8, "r") || [])) {
16278
- const meaningful = Array.from(run.childNodes || []).some((child) => child.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName !== "rPr");
16765
+ for (const run of Array.from(paragraph?.getElementsByTagNameNS?.(NS_W9, "r") || [])) {
16766
+ const meaningful = Array.from(run.childNodes || []).some((child) => child.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName !== "rPr");
16279
16767
  if (!meaningful) run.parentNode?.removeChild(run);
16280
16768
  }
16281
16769
  }
16282
16770
  function buildRejectedRestorationTemplate(xmlDoc, sourceParagraph, serializer) {
16283
16771
  const rejected = rejectTrackedChangesInOoxml(serializer.serializeToString(sourceParagraph), { allAuthors: true });
16284
16772
  const parsed = parseOoxmlSafe(rejected.oxml, "application/xml");
16285
- const rejectedParagraph = parsed.doc?.getElementsByTagNameNS?.(NS_W8, "p")?.[0] || null;
16773
+ const rejectedParagraph = parsed.doc?.getElementsByTagNameNS?.(NS_W9, "p")?.[0] || null;
16286
16774
  if (!rejectedParagraph) return null;
16287
16775
  const paragraph = createWordElement(xmlDoc, "w:p");
16288
16776
  paragraph.appendChild(createSanitizedRestorationPPr(xmlDoc, sourceParagraph));
16289
16777
  for (const child of Array.from(rejectedParagraph.childNodes || [])) {
16290
- if (child.nodeType === 1 && child.namespaceURI === NS_W8 && child.localName === "pPr") continue;
16778
+ if (child.nodeType === 1 && child.namespaceURI === NS_W9 && child.localName === "pPr") continue;
16291
16779
  paragraph.appendChild(xmlDoc.importNode(child, true));
16292
16780
  }
16293
16781
  removeRestorationAnchors(paragraph);
@@ -16308,7 +16796,7 @@ async function editRestorationTemplate(xmlDoc, template, modifiedText, author, s
16308
16796
  );
16309
16797
  if (result?.status === "error" || typeof result?.oxml !== "string") return null;
16310
16798
  const extracted = extractReplacementNodes(result.oxml);
16311
- const paragraph = (extracted.replacementNodes || []).find((node) => node?.nodeType === 1 && node.namespaceURI === NS_W8 && node.localName === "p");
16799
+ const paragraph = (extracted.replacementNodes || []).find((node) => node?.nodeType === 1 && node.namespaceURI === NS_W9 && node.localName === "p");
16312
16800
  return paragraph ? xmlDoc.importNode(paragraph, true) : null;
16313
16801
  }
16314
16802
  function allocateFreshParagraphId(xmlDoc, operationSession) {
@@ -16479,7 +16967,7 @@ async function insertIntoRejectedDeletedText(documentXml, targetText, anchor, mo
16479
16967
  }
16480
16968
  const insertionOffset = anchorStart + anchor.offset;
16481
16969
  const carriers = Array.from(paragraph.childNodes || []).filter(
16482
- (node) => node?.nodeType === 1 && node.namespaceURI === NS_W8 && node.localName === "del"
16970
+ (node) => node?.nodeType === 1 && node.namespaceURI === NS_W9 && node.localName === "del"
16483
16971
  );
16484
16972
  if (carriers.some((carrier) => hasUnsupportedDeletionSplitMarkup(paragraph, carrier))) {
16485
16973
  return {
@@ -17136,7 +17624,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
17136
17624
  };
17137
17625
  }
17138
17626
  if (!allSame) {
17139
- const hasMove = targetParagraph.getElementsByTagNameNS(NS_W8, "moveFrom").length > 0 || targetParagraph.getElementsByTagNameNS(NS_W8, "moveTo").length > 0;
17627
+ const hasMove = targetParagraph.getElementsByTagNameNS(NS_W9, "moveFrom").length > 0 || targetParagraph.getElementsByTagNameNS(NS_W9, "moveTo").length > 0;
17140
17628
  if (hasMove) {
17141
17629
  return {
17142
17630
  documentXml,
@@ -17151,8 +17639,8 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
17151
17639
  }
17152
17640
  }
17153
17641
  } else {
17154
- const hasDel = targetParagraph.getElementsByTagNameNS(NS_W8, "del").length > 0;
17155
- const hasMove = targetParagraph.getElementsByTagNameNS(NS_W8, "moveFrom").length > 0 || targetParagraph.getElementsByTagNameNS(NS_W8, "moveTo").length > 0;
17642
+ const hasDel = targetParagraph.getElementsByTagNameNS(NS_W9, "del").length > 0;
17643
+ const hasMove = targetParagraph.getElementsByTagNameNS(NS_W9, "moveFrom").length > 0 || targetParagraph.getElementsByTagNameNS(NS_W9, "moveTo").length > 0;
17156
17644
  if (hasDel) {
17157
17645
  return {
17158
17646
  documentXml,
@@ -17671,7 +18159,7 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
17671
18159
  const targetParagraph = resolved.paragraph;
17672
18160
  const collectVisibleRuns = () => {
17673
18161
  const runEntries2 = [];
17674
- const allRuns = Array.from(targetParagraph.getElementsByTagNameNS(NS_W8, "r"));
18162
+ const allRuns = Array.from(targetParagraph.getElementsByTagNameNS(NS_W9, "r"));
17675
18163
  for (const run of allRuns) {
17676
18164
  let parent = run.parentNode;
17677
18165
  let isDel = false;
@@ -17763,7 +18251,7 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
17763
18251
  const targetRuns = finalRuns.filter((span) => span.start >= matchStart && span.end <= matchEnd);
17764
18252
  let anyChanged = false;
17765
18253
  for (const entry of targetRuns) {
17766
- const rPr = entry.run.getElementsByTagNameNS(NS_W8, "rPr")[0] || null;
18254
+ const rPr = entry.run.getElementsByTagNameNS(NS_W9, "rPr")[0] || null;
17767
18255
  if (checkRunPropertiesChanged(rPr, properties)) {
17768
18256
  anyChanged = true;
17769
18257
  break;
@@ -17774,7 +18262,7 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
17774
18262
  }
17775
18263
  for (const entry of targetRuns) {
17776
18264
  const run = entry.run;
17777
- let rPr = run.getElementsByTagNameNS(NS_W8, "rPr")[0];
18265
+ let rPr = run.getElementsByTagNameNS(NS_W9, "rPr")[0];
17778
18266
  if (!rPr) {
17779
18267
  rPr = createWordElement(xmlDoc, "w:rPr");
17780
18268
  run.insertBefore(rPr, run.firstChild);
@@ -17783,7 +18271,7 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
17783
18271
  let insideSameAuthorIns = false;
17784
18272
  while (insAncestor && insAncestor !== targetParagraph) {
17785
18273
  if (insAncestor.nodeType === 1 && (insAncestor.localName === "ins" || insAncestor.nodeName === "w:ins")) {
17786
- const insAuthor = insAncestor.getAttribute("w:author") || insAncestor.getAttributeNS(NS_W8, "author");
18274
+ const insAuthor = insAncestor.getAttribute("w:author") || insAncestor.getAttributeNS(NS_W9, "author");
17787
18275
  if (insAuthor === (author || getDefaultAuthor())) {
17788
18276
  insideSameAuthorIns = true;
17789
18277
  }
@@ -17831,7 +18319,7 @@ async function applyParagraphFormatToParagraphByExactText(documentXml, targetTex
17831
18319
  return { documentXml, hasChanges: false, status: "error", error: resolved?.error || { code: "TARGET_NOT_FOUND", message: "Target paragraph not found." } };
17832
18320
  }
17833
18321
  const targetParagraph = resolved.paragraph;
17834
- let pPr = targetParagraph.getElementsByTagNameNS(NS_W8, "pPr")[0];
18322
+ let pPr = targetParagraph.getElementsByTagNameNS(NS_W9, "pPr")[0];
17835
18323
  if (!pPr) {
17836
18324
  pPr = createWordElement(xmlDoc, "w:pPr");
17837
18325
  targetParagraph.insertBefore(pPr, targetParagraph.firstChild);
@@ -17879,7 +18367,7 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
17879
18367
  }
17880
18368
  let operation = validation.operation || normalizeDocumentOperation(op);
17881
18369
  const authorUsed = resolveDocumentOperationAuthor(operation, author, getDefaultAuthor());
17882
- if (operation.operationKind !== "comment_reply" && operation.operationKind !== "rejected-insert" && operation.operationKind !== "restore" && (operation.targetDescriptor?.revisionView === "rejected" || operation.targetEndDescriptor?.revisionView === "rejected")) {
18370
+ if (operation.operationKind !== "comment_reply" && operation.operationKind !== "comment_resolve" && operation.operationKind !== "rejected-insert" && operation.operationKind !== "restore" && (operation.targetDescriptor?.revisionView === "rejected" || operation.targetEndDescriptor?.revisionView === "rejected")) {
17883
18371
  return {
17884
18372
  documentXml,
17885
18373
  hasChanges: false,
@@ -18046,6 +18534,7 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
18046
18534
  result = applyCommentReplyToParts({
18047
18535
  commentsXml: existingCommentsXml,
18048
18536
  commentsExtendedXml: existingExtendedXml,
18537
+ documentXml,
18049
18538
  parentCommentId: operation.parentCommentId,
18050
18539
  commentId,
18051
18540
  commentContent: operation.commentContent,
@@ -18053,8 +18542,30 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
18053
18542
  date: operation.date || (/* @__PURE__ */ new Date()).toISOString()
18054
18543
  });
18055
18544
  result.documentXml = documentXml;
18056
- if (result.hasChanges && session.receiptCollector) session.receiptCollector.recordComment(commentId);
18545
+ if (result.status !== "error" && result.hasChanges) {
18546
+ if (!session.document || !anchorReplyInDocument(session.document, result.threadCommentIds, commentId)) {
18547
+ 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.` } };
18548
+ } else if (session.receiptCollector) {
18549
+ session.receiptCollector.recordComment(commentId);
18550
+ }
18551
+ }
18057
18552
  }
18553
+ } else if (operation.operationKind === "comment_resolve") {
18554
+ const resolution = applyThreadResolutionToParts({
18555
+ commentsXml: session.commentsXml || runtimeContext?.commentsXml || options.commentsXml,
18556
+ commentsExtendedXml: session.commentsExtendedXml || runtimeContext?.commentsExtendedXml || options.commentsExtendedXml,
18557
+ commentId: operation.commentId,
18558
+ resolved: operation.resolved !== false
18559
+ });
18560
+ result = resolution.status === "error" ? { documentXml, hasChanges: false, status: "error", error: resolution.error } : {
18561
+ documentXml,
18562
+ hasChanges: resolution.hasChanges,
18563
+ ...resolution.commentsXml ? { commentsXml: resolution.commentsXml, commentsXmlMode: "replace" } : {},
18564
+ ...resolution.commentsExtendedXml ? { commentsExtendedXml: resolution.commentsExtendedXml, commentsExtendedXmlMode: "replace" } : {},
18565
+ resolved: resolution.resolved,
18566
+ threadRootId: resolution.threadRootId,
18567
+ commentIds: resolution.commentIds
18568
+ };
18058
18569
  } else if (operation.operationKind === "highlight") {
18059
18570
  result = await applyHighlightToParagraphByExactText(
18060
18571
  documentXml,
@@ -18195,7 +18706,7 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
18195
18706
  ...resolutionCapture
18196
18707
  };
18197
18708
  }
18198
- session.markMutationCommitted(operation.operationKind !== "comment_reply");
18709
+ session.markMutationCommitted(true);
18199
18710
  if (operation._compiledSourceId && session.sourceTargetRegistry) {
18200
18711
  session.sourceTargetRegistry.commitMutation(
18201
18712
  operation._compiledSourceId,
@@ -18308,9 +18819,9 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
18308
18819
  }
18309
18820
 
18310
18821
  // services/batch-operation-orchestrator.js
18311
- var NS_W9 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
18822
+ var NS_W10 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
18312
18823
  function operationTargetPriority(op) {
18313
- return op?.type === "comment" || op?.type === "comment_reply" ? 0 : 1;
18824
+ return op?.type === "comment" || op?.type === "comment_reply" || op?.type === "comment_resolve" ? 0 : 1;
18314
18825
  }
18315
18826
  function buildOperationDependencyPlan(operations = []) {
18316
18827
  const list = Array.isArray(operations) ? operations : [];
@@ -18385,7 +18896,7 @@ function buildOperationDependencyPlan(operations = []) {
18385
18896
  inDegrees[i2]++;
18386
18897
  }
18387
18898
  const kind = normalizeDocumentOperation(op).operationKind;
18388
- if (kind !== "comment" && kind !== "comment_reply") {
18899
+ if (kind !== "comment" && kind !== "comment_reply" && kind !== "comment_resolve") {
18389
18900
  if (!mutatingCaptureConsumers.has(ref)) mutatingCaptureConsumers.set(ref, []);
18390
18901
  mutatingCaptureConsumers.get(ref).push({
18391
18902
  index: i2,
@@ -18453,9 +18964,9 @@ function mergeCommentsXml(existingXml, incomingXml) {
18453
18964
  const serializer = createSerializer();
18454
18965
  const existingRoot = existingDoc.documentElement;
18455
18966
  const existingIds = new Set(
18456
- Array.from(existingDoc.getElementsByTagNameNS(NS_W9, "comment")).map((node) => node.getAttribute("w:id") || node.getAttribute("id")).filter(Boolean)
18967
+ Array.from(existingDoc.getElementsByTagNameNS(NS_W10, "comment")).map((node) => node.getAttribute("w:id") || node.getAttribute("id")).filter(Boolean)
18457
18968
  );
18458
- for (const comment of Array.from(incomingDoc.getElementsByTagNameNS(NS_W9, "comment"))) {
18969
+ for (const comment of Array.from(incomingDoc.getElementsByTagNameNS(NS_W10, "comment"))) {
18459
18970
  const id = comment.getAttribute("w:id") || comment.getAttribute("id");
18460
18971
  if (id && existingIds.has(id)) continue;
18461
18972
  existingRoot.appendChild(existingDoc.importNode(comment, true));
@@ -18616,7 +19127,7 @@ async function applyOperationsToDocumentXml(documentXml, operations, author, run
18616
19127
  context.targetRefSnapshot = session.initialTargetReferenceSnapshot;
18617
19128
  }
18618
19129
  session.runtimeContext = context;
18619
- if (sourceOperations.some((op) => op?.type === "comment_reply")) {
19130
+ if (sourceOperations.some((op) => op?.type === "comment_reply" || op?.type === "comment_resolve")) {
18620
19131
  session.commentsXml = context.commentsXml || options.commentsXml || null;
18621
19132
  session.commentsExtendedXml = context.commentsExtendedXml || options.commentsExtendedXml || null;
18622
19133
  session.commentsXmlMode = "replace";
@@ -18689,6 +19200,7 @@ async function applyOperationsToDocumentXml(documentXml, operations, author, run
18689
19200
  ...result.resolvedTarget ? { resolvedTarget: result.resolvedTarget } : {},
18690
19201
  ...result.resolvedAnchor ? { resolvedAnchor: result.resolvedAnchor } : {},
18691
19202
  ...result.change ? { change: result.change } : {},
19203
+ ...result.operationType === "comment_resolve" && !isError ? { resolved: result.resolved, threadRootId: result.threadRootId, commentIds: result.commentIds } : {},
18692
19204
  ...Array.isArray(result.warnings) && result.warnings.length > 0 ? { warnings: result.warnings } : {},
18693
19205
  ...result.error ? { error: result.error } : {},
18694
19206
  ...result.receipt ? { receipt: result.receipt } : {}
@@ -18965,6 +19477,26 @@ function preflightOperations(documentXml, operations, author, options = {}) {
18965
19477
  }
18966
19478
  commentsRequired = commentsRequired || operation.operationKind === "comment" || operation.operationKind === "comment_reply";
18967
19479
  numberingRequired = numberingRequired || operationNeedsNumbering(operation);
19480
+ if (operation.operationKind === "comment_resolve") {
19481
+ const commentId = String(operation.commentId);
19482
+ results.push(options._existingCommentDetails?.[commentId] ? {
19483
+ index: index + 1,
19484
+ type: sourceOperation.type,
19485
+ operationType: operation.operationKind,
19486
+ status: "ready",
19487
+ authorUsed,
19488
+ resolvedBy: "comment_id",
19489
+ commentId
19490
+ } : {
19491
+ index: index + 1,
19492
+ type: sourceOperation.type,
19493
+ operationType: operation.operationKind,
19494
+ status: "error",
19495
+ authorUsed,
19496
+ error: { code: "COMMENT_NOT_FOUND", message: `Comment '${commentId}' was not found.` }
19497
+ });
19498
+ continue;
19499
+ }
18968
19500
  if (operation.operationKind === "comment_reply") {
18969
19501
  const parentId = String(operation.parentCommentId);
18970
19502
  const parent = options._existingCommentDetails?.[parentId];
@@ -20116,6 +20648,15 @@ var text = (entries, path) => {
20116
20648
  if (typeof entry === "string") return entry;
20117
20649
  return textDecoder2.decode(entry);
20118
20650
  };
20651
+ function reconcileCommentSiblings(working) {
20652
+ const updated = reconcileCommentSiblingParts({
20653
+ commentsXml: text(working, "word/comments.xml"),
20654
+ commentsIdsXml: text(working, "word/commentsIds.xml"),
20655
+ commentsExtensibleXml: text(working, "word/commentsExtensible.xml")
20656
+ });
20657
+ if (updated.commentsIdsXml) working.set("word/commentsIds.xml", textEncoder4.encode(updated.commentsIdsXml));
20658
+ if (updated.commentsExtensibleXml) working.set("word/commentsExtensible.xml", textEncoder4.encode(updated.commentsExtensibleXml));
20659
+ }
20119
20660
  var cloneEntries = (entries) => new Map([...entries].map(([name, data]) => [name, new Uint8Array(data)]));
20120
20661
  function toBufferCompatible(uint8Arr) {
20121
20662
  if (typeof Buffer !== "undefined" && typeof Buffer.from === "function") {
@@ -20183,6 +20724,7 @@ function computePackageRevisionToken(input) {
20183
20724
  entries
20184
20725
  });
20185
20726
  }
20727
+ var COMMENT_OPERATION_TYPES = /* @__PURE__ */ new Set(["comment", "comment_reply", "comment_resolve"]);
20186
20728
  function nextCommentId(entries) {
20187
20729
  const ids = `${text(entries, "word/document.xml") || ""} ${text(entries, "word/comments.xml") || ""}`.match(/(?:w:)?id=["'](\d+)["']/g) || [];
20188
20730
  let next = ids.reduce((max2, token) => Math.max(max2, Number(token.match(/\d+/)?.[0] || 0)), 0) + 1;
@@ -20201,7 +20743,7 @@ function existingCommentDetails(entries) {
20201
20743
  id,
20202
20744
  author: comment.getAttribute("w:author") || comment.getAttribute("author") || "",
20203
20745
  text: String(comment.textContent || "").trim(),
20204
- paraId: Array.from(comment.getElementsByTagNameNS("*", "p"))[0]?.getAttribute("w14:paraId") || null
20746
+ paraId: commentParaId(comment) || null
20205
20747
  };
20206
20748
  }
20207
20749
  return details;
@@ -20232,13 +20774,34 @@ var DocxDocument = class {
20232
20774
  }
20233
20775
  inspect(options = {}) {
20234
20776
  const digestFn = options.digestFn || ((bytes) => sha256(bytes));
20235
- return inspectDocumentParts({
20777
+ const inspection = inspectDocumentParts({
20236
20778
  documentXml: text(this.entries, "word/document.xml"),
20237
20779
  commentsXml: text(this.entries, "word/comments.xml"),
20238
20780
  commentsExtendedXml: text(this.entries, "word/commentsExtended.xml"),
20239
20781
  numberingXml: text(this.entries, "word/numbering.xml"),
20240
20782
  stylesXml: text(this.entries, "word/styles.xml")
20241
20783
  }, { ...options, digestFn });
20784
+ if (inspection.status === "error") return inspection;
20785
+ return { ...inspection, headersFooters: this.inspectHeadersFooters() };
20786
+ }
20787
+ /**
20788
+ * Header and footer parts the document uses, with their paragraph text, so operations can target them with
20789
+ * `part`. `hasFields` marks parts containing PAGE/NUMPAGES-style fields (edit around them, not inside).
20790
+ */
20791
+ inspectHeadersFooters() {
20792
+ return discoverHeaderFooterParts({
20793
+ documentXml: text(this.entries, "word/document.xml"),
20794
+ relsXml: text(this.entries, "word/_rels/document.xml.rels"),
20795
+ settingsXml: text(this.entries, "word/settings.xml")
20796
+ }).map((part) => {
20797
+ const xml = text(this.entries, part.path) || "";
20798
+ const partInspection = xml ? inspectDocumentParts({ documentXml: xml }) : { paragraphs: [] };
20799
+ return {
20800
+ ...part,
20801
+ hasFields: /<w:fldChar\b|<w:fldSimple\b/.test(xml),
20802
+ paragraphs: (partInspection.paragraphs || []).map((paragraph) => ({ index: paragraph.index, text: paragraph.text }))
20803
+ };
20804
+ });
20242
20805
  }
20243
20806
  getRevisionToken() {
20244
20807
  return computePackageRevisionToken(this.entries);
@@ -20247,12 +20810,51 @@ var DocxDocument = class {
20247
20810
  return this.getRevisionToken();
20248
20811
  }
20249
20812
  preflight(operations, author = getDefaultAuthor(), options = {}) {
20250
- return preflightOperations(
20813
+ const list = Array.isArray(operations) ? operations : [];
20814
+ const runBody = (ops) => preflightOperations(
20251
20815
  text(this.entries, "word/document.xml"),
20252
- operations,
20816
+ ops,
20253
20817
  author || getDefaultAuthor(),
20254
20818
  { ...options, _existingCommentDetails: existingCommentDetails(this.entries) }
20255
20819
  );
20820
+ if (!list.some((operation) => operation && typeof operation === "object" && operation.part != null)) return runBody(operations);
20821
+ const parts = this.inspectHeadersFooters();
20822
+ const bodyOps = [];
20823
+ const results = [];
20824
+ const conflicts = [];
20825
+ const authorsUsed = /* @__PURE__ */ new Set();
20826
+ let valid = true;
20827
+ list.forEach((operation, i2) => {
20828
+ if (!operation || typeof operation !== "object" || operation.part == null) {
20829
+ bodyOps.push({ operation, origIndex: i2 });
20830
+ return;
20831
+ }
20832
+ const resolved = resolvePartSelector(parts, operation.part);
20833
+ 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;
20834
+ if (refusal) {
20835
+ valid = false;
20836
+ results.push({ index: i2 + 1, type: operation.type || "redline", status: "error", error: normalizeErrorWithRecovery({ ...refusal, operationIndex: i2 + 1 }) });
20837
+ return;
20838
+ }
20839
+ const { part: _selector, ...rest } = operation;
20840
+ const partResult = preflightOperations(text(this.entries, resolved.part.path), [rest], author || getDefaultAuthor(), { ...options, _existingCommentDetails: {} });
20841
+ valid = valid && partResult.valid;
20842
+ for (const result of partResult.results || []) results.push({ ...result, index: i2 + 1, part: resolved.part.path });
20843
+ conflicts.push(...partResult.conflicts || []);
20844
+ for (const name of partResult.authorsUsed || []) authorsUsed.add(name);
20845
+ });
20846
+ let requiredArtifacts = { comments: false, numbering: false };
20847
+ if (bodyOps.length) {
20848
+ const body = runBody(bodyOps.map((item) => item.operation));
20849
+ valid = valid && body.valid;
20850
+ for (const result of body.results || []) results.push({ ...result, index: bodyOps[result.index - 1].origIndex + 1 });
20851
+ conflicts.push(...body.conflicts || []);
20852
+ for (const name of body.authorsUsed || []) authorsUsed.add(name);
20853
+ requiredArtifacts = body.requiredArtifacts || requiredArtifacts;
20854
+ }
20855
+ results.sort((a, b) => a.index - b.index);
20856
+ const ok = valid && conflicts.length === 0;
20857
+ return { valid: ok, status: ok ? "ok" : "error", results, conflicts, authorsUsed: [...authorsUsed], requiredArtifacts };
20256
20858
  }
20257
20859
  toUint8Array() {
20258
20860
  return zipDocx(this.entries);
@@ -20260,7 +20862,240 @@ var DocxDocument = class {
20260
20862
  toBuffer() {
20261
20863
  return toBufferCompatible(this.toUint8Array());
20262
20864
  }
20865
+ /**
20866
+ * Applies operations to the document. Operations without `part` target the body (word/document.xml).
20867
+ * An operation with `part` targets one header or footer part:
20868
+ * `{ part: { kind: 'footer', type: 'default', section: 0 } | 'word/footer2.xml', target: { exactText }, ... }`.
20869
+ * Discover parts with `inspect().headersFooters`.
20870
+ */
20263
20871
  async applyOperations(operations, options = {}) {
20872
+ const list = Array.isArray(operations) ? operations : [];
20873
+ if (!list.some((operation) => operation && typeof operation === "object" && operation.part != null)) {
20874
+ return this.applyBodyOperations(operations, options);
20875
+ }
20876
+ return this.applyOperationsWithParts(list, options);
20877
+ }
20878
+ async applyOperationsWithParts(operations, options = {}) {
20879
+ const author = options.author || getDefaultAuthor();
20880
+ const atomic = options.atomic === true;
20881
+ const snapshotEntries = this.entries;
20882
+ const snapshotBytes = this.originalBytes;
20883
+ const total = operations.length;
20884
+ const restore = () => {
20885
+ this.entries = snapshotEntries;
20886
+ this.originalBytes = snapshotBytes;
20887
+ };
20888
+ const original = () => ({
20889
+ uint8Array: snapshotBytes,
20890
+ toUint8Array: () => new Uint8Array(snapshotBytes),
20891
+ buffer: toBufferCompatible(snapshotBytes),
20892
+ toBuffer: () => toBufferCompatible(snapshotBytes)
20893
+ });
20894
+ const refuse = (error2, results2 = [], receipts2 = [], extra = {}) => ({
20895
+ status: "error",
20896
+ hasChanges: false,
20897
+ written: false,
20898
+ rolledBack: true,
20899
+ results: results2,
20900
+ receipts: receipts2,
20901
+ executionOrder: [],
20902
+ authorsUsed: [],
20903
+ artifactsChanged: [],
20904
+ error: normalizeErrorWithRecovery(error2),
20905
+ retryPlan: createRetryPlan({ atomic: true, rolledBack: true, results: results2, receipts: receipts2, operationCount: total }),
20906
+ validation: { originalIssues: [], generatedIssues: [] },
20907
+ ...extra,
20908
+ ...original()
20909
+ });
20910
+ if (options.expectedRevision) {
20911
+ const precondition = await this.applyBodyOperations([], { expectedRevision: options.expectedRevision });
20912
+ if (precondition.status === "error") return precondition;
20913
+ }
20914
+ const parts = discoverHeaderFooterParts({
20915
+ documentXml: text(this.entries, "word/document.xml"),
20916
+ relsXml: text(this.entries, "word/_rels/document.xml.rels"),
20917
+ settingsXml: text(this.entries, "word/settings.xml")
20918
+ });
20919
+ const bodyOps = [];
20920
+ const partOps = [];
20921
+ for (let i2 = 0; i2 < operations.length; i2++) {
20922
+ const operation = operations[i2];
20923
+ if (!operation || typeof operation !== "object" || operation.part == null) {
20924
+ bodyOps.push({ operation, origIndex: i2 });
20925
+ continue;
20926
+ }
20927
+ if (COMMENT_OPERATION_TYPES.has(operation.type)) {
20928
+ return refuse({ code: "COMMENT_IN_HEADER_FOOTER", message: `Operation ${i2 + 1}: Word does not support comments in headers or footers.`, operationIndex: i2 + 1 });
20929
+ }
20930
+ const resolved = resolvePartSelector(parts, operation.part);
20931
+ if (resolved.error) return refuse({ ...resolved.error, message: `Operation ${i2 + 1}: ${resolved.error.message}`, operationIndex: i2 + 1 });
20932
+ const { part: _selector, ...rest } = operation;
20933
+ partOps.push({ operation: rest, origIndex: i2, part: resolved.part });
20934
+ }
20935
+ const results = [];
20936
+ const receipts = [];
20937
+ const executionOrder = [];
20938
+ const authorsUsed = /* @__PURE__ */ new Set();
20939
+ const originalIssues = [];
20940
+ let anyChange = false;
20941
+ let anyFailure = false;
20942
+ let bodyResult = null;
20943
+ if (bodyOps.length) {
20944
+ bodyResult = await this.applyBodyOperations(bodyOps.map((item) => item.operation), { ...options, expectedRevision: void 0 });
20945
+ const toOriginal = (index) => bodyOps[index - 1]?.origIndex + 1;
20946
+ for (const result of bodyResult.results || []) {
20947
+ results.push({
20948
+ ...result,
20949
+ index: toOriginal(result.index),
20950
+ ...result.receipt ? { receipt: { ...result.receipt, operationIndex: toOriginal(result.receipt.operationIndex) } } : {}
20951
+ });
20952
+ }
20953
+ for (const receipt of bodyResult.receipts || []) receipts.push({ ...receipt, operationIndex: toOriginal(receipt.operationIndex) });
20954
+ for (const index of bodyResult.executionOrder || []) executionOrder.push(toOriginal(index));
20955
+ for (const name of bodyResult.authorsUsed || []) authorsUsed.add(name);
20956
+ originalIssues.push(...bodyResult.validation?.originalIssues || []);
20957
+ if (bodyResult.status === "error" || bodyResult.rolledBack) {
20958
+ anyFailure = true;
20959
+ if (atomic) {
20960
+ restore();
20961
+ return { ...bodyResult, results, receipts, executionOrder, written: false, artifactsChanged: [], rolledBack: true, ...original() };
20962
+ }
20963
+ }
20964
+ anyChange = bodyResult.hasChanges === true;
20965
+ }
20966
+ const working = cloneEntries(this.entries);
20967
+ const zip = new MemoryZip(working);
20968
+ const changedParts = /* @__PURE__ */ new Set();
20969
+ const { expectedRevision: _ignored, ...runnerOptions } = options;
20970
+ const failPart = (item, error2) => {
20971
+ anyFailure = true;
20972
+ const failure = normalizeErrorWithRecovery({ ...error2, operationIndex: item.origIndex + 1 });
20973
+ results.push({ index: item.origIndex + 1, type: item.operation.type || "redline", status: "error", authorUsed: author, part: item.part.path, error: failure });
20974
+ };
20975
+ for (const item of partOps) {
20976
+ const path = item.part.path;
20977
+ const xml = text(working, path);
20978
+ if (!xml) {
20979
+ failPart(item, { code: "PART_NOT_FOUND", message: `Part '${path}' is referenced but missing from the package.` });
20980
+ if (atomic) break;
20981
+ continue;
20982
+ }
20983
+ const numberingText = text(working, "word/numbering.xml");
20984
+ const outcome = await applyOperationsToDocumentXml(xml, [item.operation], author, { numberingIdState: createDynamicNumberingIdState(numberingText || void 0) }, {
20985
+ ...runnerOptions,
20986
+ atomic: true,
20987
+ strictTargets: options.strictTargets !== false,
20988
+ _existingCommentDetails: {}
20989
+ });
20990
+ const single = outcome.results?.[0];
20991
+ if (outcome.rolledBack || outcome.status === "error") {
20992
+ failPart(item, single?.error || outcome.error || { code: "OPERATION_FAILED", message: "Operation failed." });
20993
+ if (atomic) break;
20994
+ continue;
20995
+ }
20996
+ if (!outcome.hasChanges) {
20997
+ results.push({ ...single, index: item.origIndex + 1, part: path });
20998
+ continue;
20999
+ }
21000
+ if (countRevisionsInsideFields(outcome.documentXml) > countRevisionsInsideFields(xml)) {
21001
+ 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.` });
21002
+ if (atomic) break;
21003
+ continue;
21004
+ }
21005
+ const taken = /* @__PURE__ */ new Set();
21006
+ for (const [name, data] of working) {
21007
+ if (name === path || !(name === "word/document.xml" || isHeaderFooterPath(name))) continue;
21008
+ for (const id of collectRevisionIds(textDecoder2.decode(data))) taken.add(id);
21009
+ }
21010
+ const renumbered = renumberCollidingRevisionIds(outcome.documentXml, taken);
21011
+ working.set(path, textEncoder4.encode(renumbered.xml));
21012
+ const describeReceipt = (receipt) => receipt?.revisionItems ? { ...receipt, revisionItems: receipt.revisionItems.map((revision) => ({ ...revision, id: renumbered.idMap.get(String(revision.id)) ?? revision.id, partName: path })) } : receipt;
21013
+ if (outcome.numberingXmlParts?.length) {
21014
+ await ensureNumberingArtifactsInZip(zip, outcome.numberingXmlParts, { mergeNumberingXml: mergeNumberingXmlBySchemaOrder });
21015
+ }
21016
+ changedParts.add(path);
21017
+ anyChange = true;
21018
+ results.push({
21019
+ ...single,
21020
+ index: item.origIndex + 1,
21021
+ part: path,
21022
+ partSections: item.part.appliesToSections,
21023
+ ...single?.receipt ? { receipt: { ...describeReceipt(single.receipt), operationIndex: item.origIndex + 1 } } : {}
21024
+ });
21025
+ if (outcome.receipts?.[0]) receipts.push({ ...describeReceipt(outcome.receipts[0]), operationIndex: item.origIndex + 1 });
21026
+ executionOrder.push(item.origIndex + 1);
21027
+ for (const name of outcome.authorsUsed || []) authorsUsed.add(name);
21028
+ }
21029
+ const generatedIssues = [];
21030
+ if (!(atomic && anyFailure)) {
21031
+ for (const path of changedParts) {
21032
+ const before = validateRedlineOoxml(text(this.entries, path)).issues.map((issue) => ({ source: path, ...issue }));
21033
+ const after = validateRedlineOoxml(text(working, path)).issues.map((issue) => ({ source: path, ...issue }));
21034
+ generatedIssues.push(...validationErrors(subtractValidationIssueMultiset(after, before)));
21035
+ }
21036
+ if (options.validate !== false && changedParts.size) {
21037
+ try {
21038
+ await validateDocxPackage(zip);
21039
+ } catch (error2) {
21040
+ generatedIssues.push({ source: "package", code: "PACKAGE_VALIDATION", severity: "error", message: error2.message });
21041
+ }
21042
+ }
21043
+ }
21044
+ if (atomic && anyFailure || generatedIssues.length) {
21045
+ restore();
21046
+ 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." };
21047
+ const rolledBackResults = results.map((result) => result.receipt ? { ...result, receipt: { ...result.receipt, committed: false, finalDisposition: "rolled_back" } } : result).sort((a, b) => a.index - b.index);
21048
+ const rolledBackReceipts = receipts.map((receipt) => ({ ...receipt, committed: false, finalDisposition: "rolled_back" }));
21049
+ return refuse(error2, rolledBackResults, rolledBackReceipts, { validation: { originalIssues, generatedIssues }, issues: generatedIssues });
21050
+ }
21051
+ results.sort((a, b) => a.index - b.index);
21052
+ receipts.sort((a, b) => a.operationIndex - b.operationIndex);
21053
+ const status = !anyFailure ? "ok" : anyChange ? "partial" : "error";
21054
+ const failureError = anyFailure ? { error: normalizeErrorWithRecovery({ code: "BATCH_OPERATION_FAILED", message: "One or more operations failed; the others were applied." }) } : {};
21055
+ if (!changedParts.size) {
21056
+ const buf = toBufferCompatible(this.originalBytes);
21057
+ return {
21058
+ ...bodyResult || {},
21059
+ results,
21060
+ receipts,
21061
+ executionOrder,
21062
+ authorsUsed: [...authorsUsed],
21063
+ status,
21064
+ hasChanges: anyChange,
21065
+ written: bodyResult?.written === true,
21066
+ artifactsChanged: bodyResult?.artifactsChanged || [],
21067
+ validation: { originalIssues, generatedIssues: [] },
21068
+ ...failureError,
21069
+ uint8Array: this.originalBytes,
21070
+ toUint8Array: () => new Uint8Array(this.originalBytes),
21071
+ buffer: buf,
21072
+ toBuffer: () => toBufferCompatible(this.originalBytes)
21073
+ };
21074
+ }
21075
+ this.entries = working;
21076
+ const outputBytes = this.toUint8Array();
21077
+ this.originalBytes = outputBytes;
21078
+ const artifactsChanged = [...working].filter(([name, data]) => !snapshotEntries.has(name) || !areByteArraysEqual(data, snapshotEntries.get(name))).map(([name]) => name);
21079
+ return {
21080
+ ...bodyResult || {},
21081
+ status,
21082
+ hasChanges: true,
21083
+ written: true,
21084
+ results,
21085
+ receipts,
21086
+ executionOrder,
21087
+ authorsUsed: [...authorsUsed],
21088
+ artifactsChanged,
21089
+ validation: { originalIssues, generatedIssues: [] },
21090
+ ...failureError,
21091
+ uint8Array: outputBytes,
21092
+ toUint8Array: () => new Uint8Array(outputBytes),
21093
+ buffer: toBufferCompatible(outputBytes),
21094
+ inspection: this.inspect(),
21095
+ toBuffer: () => toBufferCompatible(outputBytes)
21096
+ };
21097
+ }
21098
+ async applyBodyOperations(operations, options = {}) {
20264
21099
  const failedApply = (error2) => {
20265
21100
  const results = [];
20266
21101
  const receipts = [];
@@ -20371,18 +21206,21 @@ var DocxDocument = class {
20371
21206
  toBuffer: () => toBufferCompatible(this.originalBytes)
20372
21207
  };
20373
21208
  }
20374
- working.set("word/document.xml", textEncoder4.encode(result.documentXml));
21209
+ const bodyChanged = (result.results || []).some((item) => item?.status !== "error" && item?.operationType !== "comment_resolve");
21210
+ if (bodyChanged) working.set("word/document.xml", textEncoder4.encode(result.documentXml));
20375
21211
  await ensureNumberingArtifactsInZip(zip, result.numberingXmlParts, { mergeNumberingXml: mergeNumberingXmlBySchemaOrder });
20376
21212
  const existingCommentsXml = text(working, "word/comments.xml");
20377
21213
  const commentsXmlForPackaging = result.commentsXml || existingCommentsXml;
20378
21214
  await ensureCommentsArtifactsInZip(zip, commentsXmlForPackaging, {
20379
21215
  replaceExisting: result.commentsXmlMode === "replace" || !result.commentsXml && !!existingCommentsXml
20380
21216
  });
20381
- const existingCommentsExtendedXml = text(working, "word/commentsExtended.xml");
20382
- const commentsExtendedXmlForPackaging = result.commentsExtendedXml || existingCommentsExtendedXml;
20383
- await ensureCommentsExtendedArtifactsInZip(zip, commentsExtendedXmlForPackaging, {
20384
- replaceExisting: result.commentsExtendedXmlMode === "replace" || !result.commentsExtendedXml && !!existingCommentsExtendedXml
20385
- });
21217
+ if (result.commentsExtendedXml) {
21218
+ await ensureCommentsExtendedArtifactsInZip(zip, result.commentsExtendedXml, {
21219
+ replaceExisting: result.commentsExtendedXmlMode === "replace"
21220
+ });
21221
+ }
21222
+ await repairKnownContentTypes(zip);
21223
+ reconcileCommentSiblings(working);
20386
21224
  if (options.validate !== false) {
20387
21225
  const generated = validateRedlineOoxml(result.documentXml);
20388
21226
  const outputIssues = generated.issues.map((issue) => ({ source: "word/document.xml", ...issue }));
@@ -20451,13 +21289,25 @@ var DocxDocument = class {
20451
21289
  const transform = action === "accept" ? acceptTrackedChangesInOoxml : action === "reject" ? rejectTrackedChangesInOoxml : null;
20452
21290
  if (!transform) return packageFailure(this.originalBytes, "INVALID_ACTION", `Unknown revision action: ${action}`);
20453
21291
  const sourceBytes = this.originalBytes;
21292
+ const originalEntries = this.entries;
20454
21293
  const working = cloneEntries(this.entries);
20455
21294
  const zip = new MemoryZip(working);
20456
21295
  const result = transform(text(working, "word/document.xml"), { author: options.author, allAuthors: options.allAuthors === true });
20457
21296
  if (result.status === "error" || result.error) {
20458
21297
  return packageFailure(sourceBytes, result.error?.code || "REVISION_OPERATION_FAILED", result.error?.message || "Revision operation failed.");
20459
21298
  }
20460
- if (!result.hasChanges) {
21299
+ const partUpdates = /* @__PURE__ */ new Map();
21300
+ for (const part of this.inspectHeadersFooters()) {
21301
+ const partXml = text(working, part.path);
21302
+ if (!partXml) continue;
21303
+ const partResult = transform(partXml, { author: options.author, allAuthors: options.allAuthors === true });
21304
+ if (partResult.status === "error" || partResult.error) {
21305
+ return packageFailure(sourceBytes, partResult.error?.code || "REVISION_OPERATION_FAILED", `${part.path}: ${partResult.error?.message || "Revision operation failed."}`);
21306
+ }
21307
+ if (partResult.hasChanges) partUpdates.set(part.path, partResult.oxml);
21308
+ }
21309
+ const hasChanges = result.hasChanges || partUpdates.size > 0;
21310
+ if (!hasChanges) {
20461
21311
  const buf = toBufferCompatible(sourceBytes);
20462
21312
  return {
20463
21313
  ...result,
@@ -20470,8 +21320,10 @@ var DocxDocument = class {
20470
21320
  toBuffer: () => toBufferCompatible(sourceBytes)
20471
21321
  };
20472
21322
  }
20473
- working.set("word/document.xml", textEncoder4.encode(result.oxml));
21323
+ if (result.hasChanges) working.set("word/document.xml", textEncoder4.encode(result.oxml));
21324
+ for (const [path, xml] of partUpdates) working.set(path, textEncoder4.encode(xml));
20474
21325
  try {
21326
+ await repairKnownContentTypes(zip);
20475
21327
  if (options.validate !== false) await validateDocxPackage(zip);
20476
21328
  } catch (error2) {
20477
21329
  return packageFailure(sourceBytes, "PACKAGE_VALIDATION", error2.message);
@@ -20484,13 +21336,71 @@ var DocxDocument = class {
20484
21336
  ...result,
20485
21337
  status: "ok",
20486
21338
  written: true,
20487
- artifactsChanged: ["word/document.xml"],
21339
+ hasChanges: true,
21340
+ artifactsChanged: [...working].filter(([name, data]) => !originalEntries.has(name) || !areByteArraysEqual(data, originalEntries.get(name))).map(([name]) => name),
20488
21341
  uint8Array: outputBytes,
20489
21342
  toUint8Array: () => new Uint8Array(outputBytes),
20490
21343
  buffer: outputBuf,
20491
21344
  toBuffer: () => toBufferCompatible(outputBytes)
20492
21345
  };
20493
21346
  }
21347
+ /**
21348
+ * Resolves (or reopens) the comment thread containing `commentId`.
21349
+ * Resolved is a thread-level state in Word, so the root and every reply are updated together.
21350
+ */
21351
+ async resolveComment(commentId, options = {}) {
21352
+ const sourceBytes = this.originalBytes;
21353
+ const working = cloneEntries(this.entries);
21354
+ const resolved = options.resolved !== false;
21355
+ const noChange = (extra) => {
21356
+ const buf = toBufferCompatible(sourceBytes);
21357
+ return {
21358
+ status: "ok",
21359
+ hasChanges: false,
21360
+ written: false,
21361
+ artifactsChanged: [],
21362
+ ...extra,
21363
+ uint8Array: sourceBytes,
21364
+ toUint8Array: () => new Uint8Array(sourceBytes),
21365
+ buffer: buf,
21366
+ toBuffer: () => toBufferCompatible(sourceBytes)
21367
+ };
21368
+ };
21369
+ const result = applyThreadResolutionToParts({
21370
+ commentsXml: text(working, "word/comments.xml"),
21371
+ commentsExtendedXml: text(working, "word/commentsExtended.xml"),
21372
+ commentId,
21373
+ resolved
21374
+ });
21375
+ if (result.status === "error") return packageFailure(sourceBytes, result.error.code, result.error.message);
21376
+ if (!result.hasChanges) return noChange({ resolved, threadRootId: result.threadRootId, commentIds: result.commentIds });
21377
+ const zip = new MemoryZip(working);
21378
+ try {
21379
+ if (result.commentsXml) working.set("word/comments.xml", textEncoder4.encode(result.commentsXml));
21380
+ await ensureCommentsExtendedArtifactsInZip(zip, result.commentsExtendedXml, { replaceExisting: true });
21381
+ await repairKnownContentTypes(zip);
21382
+ if (options.validate !== false) await validateDocxPackage(zip);
21383
+ } catch (error2) {
21384
+ return packageFailure(sourceBytes, "PACKAGE_VALIDATION", error2.message);
21385
+ }
21386
+ const artifactsChanged = [...working].filter(([name, data]) => !this.entries.has(name) || !areByteArraysEqual(data, this.entries.get(name))).map(([name]) => name);
21387
+ this.entries = working;
21388
+ const outputBytes = this.toUint8Array();
21389
+ this.originalBytes = outputBytes;
21390
+ return {
21391
+ status: "ok",
21392
+ hasChanges: true,
21393
+ written: true,
21394
+ resolved,
21395
+ threadRootId: result.threadRootId,
21396
+ commentIds: result.commentIds,
21397
+ artifactsChanged,
21398
+ uint8Array: outputBytes,
21399
+ toUint8Array: () => new Uint8Array(outputBytes),
21400
+ buffer: toBufferCompatible(outputBytes),
21401
+ toBuffer: () => toBufferCompatible(outputBytes)
21402
+ };
21403
+ }
20494
21404
  async deleteComments(options = {}) {
20495
21405
  const sourceBytes = this.originalBytes;
20496
21406
  const working = cloneEntries(this.entries);
@@ -20514,7 +21424,13 @@ var DocxDocument = class {
20514
21424
  if (!parsed.doc || parsed.error) {
20515
21425
  return packageFailure(sourceBytes, "PARSE_ERROR", parsed.error?.message || "Could not parse comments.xml.");
20516
21426
  }
20517
- const matches = (comment) => options.allAuthors === true || (comment.getAttribute("w:author") || comment.getAttribute("author")) === options.author;
21427
+ const requestedIds = Array.isArray(options.ids) ? new Set(options.ids.map(String)) : null;
21428
+ if (requestedIds) {
21429
+ const known = new Set(Array.from(parsed.doc.getElementsByTagNameNS("*", "comment")).map((node) => node.getAttribute("w:id") || node.getAttribute("id")));
21430
+ const missing = [...requestedIds].filter((id) => !known.has(id));
21431
+ if (missing.length) return packageFailure(sourceBytes, "COMMENT_NOT_FOUND", `Comment id(s) not found: ${missing.join(", ")}.`);
21432
+ }
21433
+ const matches = (comment) => requestedIds ? requestedIds.has(comment.getAttribute("w:id") || comment.getAttribute("id")) : options.allAuthors === true || (comment.getAttribute("w:author") || comment.getAttribute("author")) === options.author;
20518
21434
  const ids = new Set(
20519
21435
  Array.from(parsed.doc.getElementsByTagNameNS("*", "comment")).filter(matches).map((node) => node.getAttribute("w:id") || node.getAttribute("id")).filter(Boolean)
20520
21436
  );
@@ -20543,9 +21459,8 @@ var DocxDocument = class {
20543
21459
  const idByParaId = /* @__PURE__ */ new Map();
20544
21460
  for (const comment of Array.from(parsed.doc.getElementsByTagNameNS("*", "comment"))) {
20545
21461
  const id = comment.getAttribute("w:id") || comment.getAttribute("id");
20546
- const paragraph = Array.from(comment.getElementsByTagNameNS("*", "p"))[0];
20547
- const paraId = paragraph?.getAttribute("w14:paraId") || paragraph?.getAttribute("paraId");
20548
- if (paraId) idByParaId.set(paraId.toUpperCase(), id);
21462
+ const paraId = commentParaId(comment);
21463
+ if (paraId) idByParaId.set(paraId, id);
20549
21464
  }
20550
21465
  for (const [paraId, id] of idByParaId) {
20551
21466
  if (ids.has(id)) removedParaIds.add(paraId);
@@ -20564,8 +21479,8 @@ var DocxDocument = class {
20564
21479
  }
20565
21480
  }
20566
21481
  }
20567
- const commentsResult = deleteCommentsByAuthorInOoxml(commentsXml, { author: options.author, allAuthors: options.allAuthors === true });
20568
- const remainingParsed = parseOoxmlSafe(commentsResult.oxml, "application/xml");
21482
+ const remainingXml = requestedIds ? commentsXml : deleteCommentsByAuthorInOoxml(commentsXml, { author: options.author, allAuthors: options.allAuthors === true }).oxml;
21483
+ const remainingParsed = parseOoxmlSafe(remainingXml, "application/xml");
20569
21484
  if (!remainingParsed.doc || remainingParsed.error) {
20570
21485
  return packageFailure(sourceBytes, "PARSE_ERROR", remainingParsed.error?.message || "Could not parse updated comments.xml.");
20571
21486
  }
@@ -20601,10 +21516,14 @@ var DocxDocument = class {
20601
21516
  }
20602
21517
  working.set("word/document.xml", textEncoder4.encode(serializer.serializeToString(documentParsed.doc)));
20603
21518
  try {
20604
- if (options.validate !== false) await validateDocxPackage(new MemoryZip(working));
21519
+ reconcileCommentSiblings(working);
21520
+ const finalZip = new MemoryZip(working);
21521
+ await repairKnownContentTypes(finalZip);
21522
+ if (options.validate !== false) await validateDocxPackage(finalZip);
20605
21523
  } catch (error2) {
20606
21524
  return packageFailure(sourceBytes, "PACKAGE_VALIDATION", error2.message);
20607
21525
  }
21526
+ const artifactsChanged = [...working].filter(([name, data]) => !this.entries.has(name) || !areByteArraysEqual(data, this.entries.get(name))).map(([name]) => name);
20608
21527
  this.entries = working;
20609
21528
  const outputBytes = this.toUint8Array();
20610
21529
  this.originalBytes = outputBytes;
@@ -20615,7 +21534,7 @@ var DocxDocument = class {
20615
21534
  written: true,
20616
21535
  commentsRemoved: ids.size,
20617
21536
  referencesRemoved,
20618
- artifactsChanged: ["word/document.xml", "word/comments.xml", ...extendedParsed?.doc ? ["word/commentsExtended.xml"] : []],
21537
+ artifactsChanged,
20619
21538
  uint8Array: outputBytes,
20620
21539
  toUint8Array: () => new Uint8Array(outputBytes),
20621
21540
  buffer: outputBuf,
@@ -20871,6 +21790,7 @@ export {
20871
21790
  rejectTrackedChangesInOoxml,
20872
21791
  remapNumberingPayloadForDocument,
20873
21792
  removeFormattingFromRPr,
21793
+ repairKnownContentTypes,
20874
21794
  reserveNextNumberingId,
20875
21795
  reserveNextNumberingIdPair,
20876
21796
  resolveParagraphRangeByRefs,