bun-docx 0.14.0 → 0.14.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 (2) hide show
  1. package/dist/index.js +196 -29
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -22198,15 +22198,12 @@ class CommentsView {
22198
22198
  }
22199
22199
  paraIdFor(commentId) {
22200
22200
  const numericId = commentId.startsWith("c") ? commentId.slice(1) : commentId;
22201
- const comment = this.findById(numericId);
22202
- const paragraph = comment?.findChild("w:p");
22203
- return paragraph?.getAttribute("w14:paraId");
22201
+ return lastParagraph(this.findById(numericId))?.getAttribute("w14:paraId");
22204
22202
  }
22205
22203
  ensureParaId(commentId) {
22206
22204
  const numericId = commentId.startsWith("c") ? commentId.slice(1) : commentId;
22207
22205
  const root = XmlNode2.findRoot(this.tree, "w:comments");
22208
- const comment = this.findById(numericId);
22209
- const paragraph = comment?.findChild("w:p");
22206
+ const paragraph = lastParagraph(this.findById(numericId));
22210
22207
  if (!root || !paragraph)
22211
22208
  return;
22212
22209
  const existing = paragraph.getAttribute("w14:paraId");
@@ -22236,6 +22233,96 @@ class CommentsView {
22236
22233
  }
22237
22234
  return String(highest + 1);
22238
22235
  }
22236
+ threadRootId(commentId) {
22237
+ const root = XmlNode2.findRoot(this.tree, "w:comments");
22238
+ const numericId = commentId.startsWith("c") ? commentId.slice(1) : commentId;
22239
+ if (!root)
22240
+ return numericId;
22241
+ const extended = this.#readExtended();
22242
+ const idByParaId = new Map;
22243
+ for (const child2 of root.children) {
22244
+ if (child2.tag !== "w:comment")
22245
+ continue;
22246
+ const childId = child2.getAttribute("w:id");
22247
+ if (childId == null)
22248
+ continue;
22249
+ const paraId = lastParagraph(child2)?.getAttribute("w14:paraId");
22250
+ if (paraId)
22251
+ idByParaId.set(paraId, childId);
22252
+ }
22253
+ let currentId = numericId;
22254
+ const seen = new Set([currentId]);
22255
+ for (;; ) {
22256
+ const paraId = lastParagraph(this.findById(currentId))?.getAttribute("w14:paraId");
22257
+ const parentParaId = paraId ? extended.get(paraId)?.parentParaId : undefined;
22258
+ const parentId = parentParaId ? idByParaId.get(parentParaId) : undefined;
22259
+ if (!parentId || seen.has(parentId))
22260
+ return currentId;
22261
+ seen.add(parentId);
22262
+ currentId = parentId;
22263
+ }
22264
+ }
22265
+ descendantReplyIds(commentId) {
22266
+ const root = XmlNode2.findRoot(this.tree, "w:comments");
22267
+ if (!root)
22268
+ return [];
22269
+ const numericId = commentId.startsWith("c") ? commentId.slice(1) : commentId;
22270
+ const startParaId = lastParagraph(this.findById(numericId))?.getAttribute("w14:paraId");
22271
+ if (!startParaId)
22272
+ return [];
22273
+ const extended = this.#readExtended();
22274
+ const idByParaId = new Map;
22275
+ const childParaIdsByParent = new Map;
22276
+ for (const child2 of root.children) {
22277
+ if (child2.tag !== "w:comment")
22278
+ continue;
22279
+ const childId = child2.getAttribute("w:id");
22280
+ if (childId == null)
22281
+ continue;
22282
+ const paraId = lastParagraph(child2)?.getAttribute("w14:paraId");
22283
+ if (!paraId)
22284
+ continue;
22285
+ idByParaId.set(paraId, childId);
22286
+ const parentParaId = extended.get(paraId)?.parentParaId;
22287
+ if (!parentParaId)
22288
+ continue;
22289
+ const siblings = childParaIdsByParent.get(parentParaId) ?? [];
22290
+ siblings.push(paraId);
22291
+ childParaIdsByParent.set(parentParaId, siblings);
22292
+ }
22293
+ const descendants = [];
22294
+ const queue = [startParaId];
22295
+ while (queue.length > 0) {
22296
+ const paraId = queue.pop();
22297
+ if (!paraId)
22298
+ break;
22299
+ for (const childParaId of childParaIdsByParent.get(paraId) ?? []) {
22300
+ const childId = idByParaId.get(childParaId);
22301
+ if (childId)
22302
+ descendants.push(childId);
22303
+ queue.push(childParaId);
22304
+ }
22305
+ }
22306
+ return descendants;
22307
+ }
22308
+ insertReplyAfter(parentNumericId, comment) {
22309
+ const root = XmlNode2.findRoot(this.tree, "w:comments");
22310
+ if (!root)
22311
+ throw new Error("expected <w:comments> root");
22312
+ const threadIds = new Set([
22313
+ parentNumericId,
22314
+ ...this.descendantReplyIds(parentNumericId)
22315
+ ]);
22316
+ let insertIndex = root.children.length;
22317
+ for (const [index, child2] of root.children.entries()) {
22318
+ if (child2.tag !== "w:comment")
22319
+ continue;
22320
+ const id = child2.getAttribute("w:id");
22321
+ if (id != null && threadIds.has(id))
22322
+ insertIndex = index + 1;
22323
+ }
22324
+ root.children.splice(insertIndex, 0, comment);
22325
+ }
22239
22326
  toComments(anchors) {
22240
22327
  const root = XmlNode2.findRoot(this.tree, "w:comments");
22241
22328
  if (!root)
@@ -22247,8 +22334,7 @@ class CommentsView {
22247
22334
  const numericId = child2.getAttribute("w:id");
22248
22335
  if (numericId == null)
22249
22336
  continue;
22250
- const paragraph = child2.findChild("w:p");
22251
- const paraId = paragraph?.getAttribute("w14:paraId");
22337
+ const paraId = lastParagraph(child2)?.getAttribute("w14:paraId");
22252
22338
  if (paraId)
22253
22339
  commentIdByParaId.set(paraId, `c${numericId}`);
22254
22340
  }
@@ -22271,8 +22357,7 @@ class CommentsView {
22271
22357
  endBlockId: "",
22272
22358
  endOffset: 0
22273
22359
  };
22274
- const paragraph = child2.findChild("w:p");
22275
- const paraId = paragraph?.getAttribute("w14:paraId");
22360
+ const paraId = lastParagraph(child2)?.getAttribute("w14:paraId");
22276
22361
  const meta = paraId ? extendedByParaId.get(paraId) ?? {} : {};
22277
22362
  const parentCommentId = meta.parentParaId ? commentIdByParaId.get(meta.parentParaId) : undefined;
22278
22363
  comments.push({
@@ -22331,12 +22416,21 @@ function CommentsRoot() {
22331
22416
  "xmlns:w14": NS_W14
22332
22417
  }, undefined, false, undefined, this);
22333
22418
  }
22419
+ function lastParagraph(comment) {
22420
+ return comment?.findChildren("w:p").at(-1);
22421
+ }
22334
22422
  function generateParaId() {
22423
+ const seed = Bun.env.DOCX_CLI_PARA_ID_SEED;
22424
+ if (seed) {
22425
+ const minted = (Number.parseInt(seed, 16) + seededParaIds++) % 2147483648;
22426
+ return (minted || 1).toString(16).padStart(8, "0").toUpperCase();
22427
+ }
22335
22428
  const bytes = new Uint8Array(4);
22336
22429
  crypto.getRandomValues(bytes);
22337
- let hex = "";
22338
- for (const byte of bytes)
22339
- hex += byte.toString(16).padStart(2, "0");
22430
+ bytes[0] = (bytes[0] ?? 0) & 127;
22431
+ const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
22432
+ if (hex === "00000000")
22433
+ return "00000001";
22340
22434
  return hex.toUpperCase();
22341
22435
  }
22342
22436
  function CommentsExRoot() {
@@ -22346,7 +22440,7 @@ function CommentsExRoot() {
22346
22440
  "mc:Ignorable": "w15"
22347
22441
  }, undefined, false, undefined, this);
22348
22442
  }
22349
- var COMMENTS_PART_NAME = "word/comments.xml", COMMENTS_EXT_PART_NAME = "word/commentsExtended.xml", COMMENTS_RELATIONSHIP_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments", COMMENTS_EXT_RELATIONSHIP_TYPE = "http://schemas.microsoft.com/office/2011/relationships/commentsExtended", COMMENTS_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml", COMMENTS_EXT_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml", NS_W2 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main", NS_W14 = "http://schemas.microsoft.com/office/word/2010/wordml", NS_W15 = "http://schemas.microsoft.com/office/word/2012/wordml";
22443
+ var COMMENTS_PART_NAME = "word/comments.xml", COMMENTS_EXT_PART_NAME = "word/commentsExtended.xml", COMMENTS_RELATIONSHIP_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments", COMMENTS_EXT_RELATIONSHIP_TYPE = "http://schemas.microsoft.com/office/2011/relationships/commentsExtended", COMMENTS_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml", COMMENTS_EXT_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml", NS_W2 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main", NS_W14 = "http://schemas.microsoft.com/office/word/2010/wordml", NS_W15 = "http://schemas.microsoft.com/office/word/2012/wordml", seededParaIds = 0;
22350
22444
  var init_comments = __esm(() => {
22351
22445
  init_jsx();
22352
22446
  init_parser();
@@ -33826,6 +33920,38 @@ function CommentBody({
33826
33920
  }, undefined, false, undefined, this)
33827
33921
  }, undefined, false, undefined, this);
33828
33922
  }
33923
+ function addReplyCommentMarkers(documentTree, threadNumericIds, replyNumericId) {
33924
+ const document2 = XmlNode2.findRoot(documentTree, "w:document");
33925
+ if (!document2)
33926
+ return false;
33927
+ const threadIds = new Set(threadNumericIds);
33928
+ const referenceRun = findLastDescendant(document2, (node) => node.tag === "w:r" && node.children.some((child2) => child2.tag === "w:commentReference" && threadIds.has(child2.getAttribute("w:id") ?? "")));
33929
+ if (!referenceRun)
33930
+ return false;
33931
+ const rangeStart = findLastDescendant(document2, (node) => node.tag === "w:commentRangeStart" && threadIds.has(node.getAttribute("w:id") ?? ""));
33932
+ if (!rangeStart) {
33933
+ spliceAfter(referenceRun, commentReferenceRun(replyNumericId));
33934
+ return true;
33935
+ }
33936
+ spliceAfter(rangeStart, commentRangeStartMarker(replyNumericId));
33937
+ spliceAfter(referenceRun, commentRangeEndMarker(replyNumericId), commentReferenceRun(replyNumericId));
33938
+ return true;
33939
+ }
33940
+ function findLastDescendant(root, matches) {
33941
+ let last;
33942
+ for (const child2 of root.children) {
33943
+ if (matches(child2))
33944
+ last = { node: child2, siblings: root.children };
33945
+ const found = findLastDescendant(child2, matches);
33946
+ if (found)
33947
+ last = found;
33948
+ }
33949
+ return last;
33950
+ }
33951
+ function spliceAfter(target, ...nodes) {
33952
+ const index = target.siblings.indexOf(target.node);
33953
+ target.siblings.splice(index + 1, 0, ...nodes);
33954
+ }
33829
33955
  function removeCommentMarkers(documentTree, numericId) {
33830
33956
  const document2 = XmlNode2.findRoot(documentTree, "w:document");
33831
33957
  if (!document2)
@@ -33886,7 +34012,7 @@ class Comments {
33886
34012
  return comments;
33887
34013
  }
33888
34014
  add(anchor, options) {
33889
- const date = options.date ?? new Date().toISOString();
34015
+ const date = options.date ?? resolveDate();
33890
34016
  const numericId = this.document.comments?.nextId() ?? "0";
33891
34017
  const paraId = generateParaId();
33892
34018
  try {
@@ -33913,21 +34039,47 @@ class Comments {
33913
34039
  if (!view?.findById(parentNumericId)) {
33914
34040
  throw new CommentsError("COMMENT_NOT_FOUND", `Parent comment not found: c${parentNumericId}`);
33915
34041
  }
33916
- const parentParaId = view.ensureParaId(parentNumericId);
33917
- if (!parentParaId) {
33918
- throw new CommentsError("COMMENT_NOT_FOUND", `Parent comment c${parentNumericId} could not be assigned a w14:paraId.`);
34042
+ const rootNumericId = view.threadRootId(parentNumericId);
34043
+ const rootParaId = view.ensureParaId(rootNumericId);
34044
+ if (!rootParaId) {
34045
+ throw new CommentsError("COMMENT_NOT_FOUND", `Parent comment c${rootNumericId} could not be assigned a w14:paraId.`);
33919
34046
  }
33920
- const date = options.date ?? new Date().toISOString();
34047
+ const date = options.date ?? resolveDate();
33921
34048
  const numericId = view.nextId();
33922
34049
  const replyParaId = generateParaId();
33923
- this.#appendCommentBody(numericId, replyParaId, body, options.author, date);
34050
+ const anchored = addReplyCommentMarkers(this.document.documentTree, [rootNumericId, ...view.descendantReplyIds(rootNumericId)], numericId);
34051
+ if (!anchored) {
34052
+ throw new CommentsError("COMMENT_NOT_FOUND", `Parent comment c${parentNumericId} has no anchor in the document body; cannot anchor a reply to it.`);
34053
+ }
34054
+ this.#appendCommentBody(numericId, replyParaId, body, options.author, date, {
34055
+ threadParentNumericId: rootNumericId
34056
+ });
33924
34057
  const extView = this.document.ensureCommentsExtended();
33925
34058
  const extRoot = extView.extendedTree ? XmlNode2.findRoot(extView.extendedTree, "w15:commentsEx") : undefined;
33926
34059
  if (!extRoot)
33927
34060
  throw new Error("expected <w15:commentsEx> root");
33928
- extRoot.children.push(new XmlNode2("w15:commentEx", {
34061
+ const threadParaIds = new Set([
34062
+ rootParaId,
34063
+ ...view.descendantReplyIds(rootNumericId).map((replyId) => view.paraIdFor(replyId))
34064
+ ]);
34065
+ const hasRootEntry = extRoot.children.some((child2) => child2.tag === "w15:commentEx" && child2.getAttribute("w15:paraId") === rootParaId);
34066
+ if (!hasRootEntry) {
34067
+ extRoot.children.push(new XmlNode2("w15:commentEx", {
34068
+ "w15:paraId": rootParaId,
34069
+ "w15:done": "0"
34070
+ }));
34071
+ }
34072
+ let insertIndex = extRoot.children.length;
34073
+ for (const [index, child2] of extRoot.children.entries()) {
34074
+ if (child2.tag !== "w15:commentEx")
34075
+ continue;
34076
+ const paraId = child2.getAttribute("w15:paraId");
34077
+ if (paraId && threadParaIds.has(paraId))
34078
+ insertIndex = index + 1;
34079
+ }
34080
+ extRoot.children.splice(insertIndex, 0, new XmlNode2("w15:commentEx", {
33929
34081
  "w15:paraId": replyParaId,
33930
- "w15:paraIdParent": parentParaId,
34082
+ "w15:paraIdParent": rootParaId,
33931
34083
  "w15:done": "0"
33932
34084
  }));
33933
34085
  return numericId;
@@ -33977,10 +34129,16 @@ class Comments {
33977
34129
  }
33978
34130
  if (!view)
33979
34131
  return;
34132
+ const cascaded = new Set(normalized);
34133
+ for (const commentId of normalized) {
34134
+ for (const replyId of view.descendantReplyIds(commentId)) {
34135
+ cascaded.add(`c${replyId}`);
34136
+ }
34137
+ }
33980
34138
  const root = XmlNode2.findRoot(view.tree, "w:comments");
33981
34139
  if (!root)
33982
34140
  return;
33983
- for (const commentId of normalized) {
34141
+ for (const commentId of cascaded) {
33984
34142
  const numericId = commentId.slice(1);
33985
34143
  const node = view.findById(numericId);
33986
34144
  if (!node)
@@ -34009,12 +34167,12 @@ class Comments {
34009
34167
  this.#appendCommentBody(numericId, paraId, options.body, options.author, options.date);
34010
34168
  return numericId;
34011
34169
  }
34012
- #appendCommentBody(numericId, paraId, text2, author, date) {
34170
+ #appendCommentBody(numericId, paraId, text2, author, date, options) {
34013
34171
  const commentsView = this.document.ensureComments();
34014
34172
  const commentsRoot = XmlNode2.findRoot(commentsView.tree, "w:comments");
34015
34173
  if (!commentsRoot)
34016
34174
  throw new Error("expected <w:comments> root");
34017
- commentsRoot.children.push(/* @__PURE__ */ jsxDEV(CommentBody, {
34175
+ const comment = /* @__PURE__ */ jsxDEV(CommentBody, {
34018
34176
  options: {
34019
34177
  id: numericId,
34020
34178
  author,
@@ -34023,7 +34181,12 @@ class Comments {
34023
34181
  paraId,
34024
34182
  text: text2
34025
34183
  }
34026
- }, undefined, false, undefined, this));
34184
+ }, undefined, false, undefined, this);
34185
+ if (options?.threadParentNumericId) {
34186
+ commentsView.insertReplyAfter(options.threadParentNumericId, comment);
34187
+ return;
34188
+ }
34189
+ commentsRoot.children.push(comment);
34027
34190
  }
34028
34191
  #resolveBlock(blockId) {
34029
34192
  try {
@@ -34058,6 +34221,7 @@ var init_comments2 = __esm(() => {
34058
34221
  init_parser();
34059
34222
  init_markers();
34060
34223
  init_markers();
34224
+ init_track_changes();
34061
34225
  init_jsx_dev_runtime();
34062
34226
  CommentsError = class CommentsError extends Error {
34063
34227
  code;
@@ -81196,7 +81360,8 @@ Target (one required, mutually exclusive):
81196
81360
  --at cN Comment id (e.g., c0). Repeat for multiple ids:
81197
81361
  --at c1 --at c3 --at c5. All ids are validated against
81198
81362
  the pre-mutation tree, so the batch is atomic. The "c"
81199
- prefix is optional.
81363
+ prefix is optional. Deleting a thread parent also
81364
+ deletes its replies.
81200
81365
  --batch PATH JSONL with one {"id": "cN"} per line. Use - for stdin.
81201
81366
 
81202
81367
  Optional:
@@ -81320,7 +81485,7 @@ async function run4(args) {
81320
81485
  dryRun: true,
81321
81486
  path: path2,
81322
81487
  commentId: `c${nextId}`,
81323
- parentId: `c${parentNumericId}`,
81488
+ parentId: `c${document4.comments.threadRootId(parentNumericId)}`,
81324
81489
  ...outputPath ? { output: outputPath } : {}
81325
81490
  });
81326
81491
  return EXIT2.OK;
@@ -81340,7 +81505,7 @@ async function run4(args) {
81340
81505
  operation: "comments.reply",
81341
81506
  path: outputPath ?? path2,
81342
81507
  commentId: `c${numericId}`,
81343
- parentId: `c${parentNumericId}`
81508
+ parentId: `c${document4.comments?.threadRootId(parentNumericId) ?? parentNumericId}`
81344
81509
  });
81345
81510
  return EXIT2.OK;
81346
81511
  }
@@ -81351,6 +81516,8 @@ Usage:
81351
81516
 
81352
81517
  Required:
81353
81518
  --at cN Parent comment id (e.g., c0). The "c" prefix is optional.
81519
+ Replying to a reply attaches to the thread root (Word
81520
+ threads are single-level); the ack's parentId reports it.
81354
81521
  --text TEXT Reply body
81355
81522
 
81356
81523
  Optional:
@@ -92085,7 +92252,7 @@ Examples:
92085
92252
  // package.json
92086
92253
  var package_default = {
92087
92254
  name: "bun-docx",
92088
- version: "0.14.0",
92255
+ version: "0.14.1",
92089
92256
  description: "CLI for AI agents (Claude, Codex) to read, edit, and comment on .docx files with full format fidelity.",
92090
92257
  keywords: [
92091
92258
  "docx",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bun-docx",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "description": "CLI for AI agents (Claude, Codex) to read, edit, and comment on .docx files with full format fidelity.",
5
5
  "keywords": [
6
6
  "docx",