bun-docx 0.6.0 → 0.6.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 (3) hide show
  1. package/README.md +10 -1
  2. package/dist/index.js +230 -30
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -40,6 +40,9 @@ docx delete FILE --at p3
40
40
  docx find FILE QUERY [--regex] [--ignore-case] [--all] [--nth N]
41
41
  docx replace FILE PATTERN REPLACEMENT [--regex] [--ignore-case] [--all] [--limit N] [--dry-run]
42
42
 
43
+ docx wc FILE [LOCATOR]
44
+ docx outline FILE
45
+
43
46
  docx comments add FILE --range p3:5-20 --text "..." [--author NAME]
44
47
  docx comments reply FILE --to c0 --text "..."
45
48
  docx comments resolve FILE --id c0 [--unset]
@@ -62,7 +65,9 @@ docx info locators [--json]
62
65
 
63
66
  Every command has `--help`. Mutating commands accept `--dry-run` and `-o/--output PATH` (write to a parallel file instead of overwriting `FILE`). JSON output by default for `read` and `*.list`; structured `{ok, code, error, hint}` on failure.
64
67
 
65
- When `<w:trackChanges/>` is set in the doc (toggle via `docx track-changes FILE on`), `insert`/`edit`/`delete`/`replace` automatically emit `<w:ins>`/`<w:del>` markers attributed to `$DOCX_AUTHOR` (default `docx-cli`). To make a one-off untracked edit, flip the flag off, edit, then flip it back on. `find` results inside tracked-change wrappers carry a `trackedChanges` array so agents can decide what to do with hits in pending insertions/deletions.
68
+ When `<w:trackChanges/>` is set in the doc (toggle via `docx track-changes FILE on`), `insert`/`edit`/`delete`/`replace` automatically emit `<w:ins>`/`<w:del>` markers. Author resolution: per-call `--author NAME` overrides `$DOCX_AUTHOR`, which falls back to `docx-cli`. To make a one-off untracked edit, flip the flag off, edit, then flip it back on. `find` results inside tracked-change wrappers carry a `trackedChanges` array so agents can decide what to do with hits in pending insertions/deletions.
69
+
70
+ OOXML has no native tracked-change form for hyperlink edits or image swaps, so when track-changes is on, `hyperlinks add/replace/delete` and `images replace` auto-emit a `[docx-cli] …` comment anchored to the affected span/run instead. The comment carries the same `--author` attribution as the other tracked operations. Word itself silently bypasses tracking for these — we trade silence for an explicit audit trail.
66
71
 
67
72
  ### Markdown rendering
68
73
 
@@ -139,6 +144,10 @@ src/
139
144
  insert/ # insert FILE (uses ./emit Paragraph component)
140
145
  edit/ # edit FILE
141
146
  delete/ # delete FILE
147
+ find/ # find FILE QUERY
148
+ replace/ # replace FILE PATTERN REPLACEMENT
149
+ wc/ # wc FILE [LOCATOR]
150
+ outline/ # outline FILE
142
151
  comments/ # add | reply | resolve | delete | list
143
152
  images/ # list | extract | replace
144
153
  hyperlinks/ # add | list | replace | delete
package/dist/index.js CHANGED
@@ -14645,8 +14645,6 @@ function resolveAuthor(authorFlag) {
14645
14645
  return authorFlag;
14646
14646
  if (Bun.env.DOCX_AUTHOR)
14647
14647
  return Bun.env.DOCX_AUTHOR;
14648
- if (Bun.env.DOCX_CLI_AUTHOR)
14649
- return Bun.env.DOCX_CLI_AUTHOR;
14650
14648
  return "docx-cli";
14651
14649
  }
14652
14650
  function resolveDate() {
@@ -15067,15 +15065,15 @@ function ensureCommentsExtPart(view) {
15067
15065
  return root;
15068
15066
  }
15069
15067
  function paragraphTextLength(paragraph) {
15068
+ return sumTextLength(paragraph.children);
15069
+ }
15070
+ function sumTextLength(children) {
15070
15071
  let total = 0;
15071
- for (const child of paragraph.children) {
15072
+ for (const child of children) {
15072
15073
  if (child.tag === "w:r")
15073
15074
  total += runTextLength(child);
15074
- else if (child.tag === "w:ins" || child.tag === "w:del") {
15075
- for (const inner of child.children) {
15076
- if (inner.tag === "w:r")
15077
- total += runTextLength(inner);
15078
- }
15075
+ else if (child.tag === "w:ins" || child.tag === "w:del" || child.tag === "w:hyperlink") {
15076
+ total += sumTextLength(child.children);
15079
15077
  }
15080
15078
  }
15081
15079
  return total;
@@ -15114,6 +15112,97 @@ function addCommentRangeMarkers(startParagraph, startOffset, endParagraph, endOf
15114
15112
  }
15115
15113
  ]);
15116
15114
  }
15115
+ function addCommentMarkersAroundRun(paragraph, target, commentId) {
15116
+ function walk(parent) {
15117
+ const children = parent.children;
15118
+ for (let index = 0;index < children.length; index++) {
15119
+ const child = children[index];
15120
+ if (child === target) {
15121
+ children.splice(index, 1, commentRangeStartMarker(commentId), target, commentRangeEndMarker(commentId), commentReferenceRun(commentId));
15122
+ return true;
15123
+ }
15124
+ if (child && (child.tag === "w:ins" || child.tag === "w:del" || child.tag === "w:hyperlink")) {
15125
+ if (walk(child))
15126
+ return true;
15127
+ }
15128
+ }
15129
+ return false;
15130
+ }
15131
+ return walk(paragraph);
15132
+ }
15133
+ function findElementOffsetsInParagraph(paragraph, target) {
15134
+ let cursor = 0;
15135
+ let result = null;
15136
+ function walk(children) {
15137
+ for (const child of children) {
15138
+ if (child === target) {
15139
+ const start = cursor;
15140
+ cursor += sumTextLength([child]);
15141
+ result = { start, end: cursor };
15142
+ return true;
15143
+ }
15144
+ if (child.tag === "w:r") {
15145
+ cursor += runTextLength(child);
15146
+ continue;
15147
+ }
15148
+ if (child.tag === "w:ins" || child.tag === "w:del" || child.tag === "w:hyperlink") {
15149
+ if (walk(child.children))
15150
+ return true;
15151
+ }
15152
+ }
15153
+ return false;
15154
+ }
15155
+ walk(paragraph.children);
15156
+ return result;
15157
+ }
15158
+ function findContainingParagraph(documentTree, target) {
15159
+ function walk(node) {
15160
+ if (node.tag === "w:p" && containsNode(node, target))
15161
+ return node;
15162
+ for (const child of node.children) {
15163
+ const found = walk(child);
15164
+ if (found)
15165
+ return found;
15166
+ }
15167
+ return null;
15168
+ }
15169
+ for (const root of documentTree) {
15170
+ const found = walk(root);
15171
+ if (found)
15172
+ return found;
15173
+ }
15174
+ return null;
15175
+ }
15176
+ function containsNode(haystack, needle) {
15177
+ if (haystack === needle)
15178
+ return true;
15179
+ for (const child of haystack.children) {
15180
+ if (containsNode(child, needle))
15181
+ return true;
15182
+ }
15183
+ return false;
15184
+ }
15185
+ function emitAuditComment(view, anchor, options) {
15186
+ const numericId = nextCommentId(view);
15187
+ const paraId = generateParaId();
15188
+ if (anchor.kind === "span") {
15189
+ addCommentMarkersToParagraph(anchor.paragraph, numericId, anchor.span);
15190
+ } else {
15191
+ addCommentMarkersAroundRun(anchor.paragraph, anchor.run, numericId);
15192
+ }
15193
+ const commentsRoot = ensureCommentsPart(view);
15194
+ commentsRoot.children.push(/* @__PURE__ */ jsxDEV(CommentBody, {
15195
+ options: {
15196
+ id: numericId,
15197
+ author: options.author,
15198
+ date: options.date,
15199
+ initials: authorInitials(options.author),
15200
+ paraId,
15201
+ text: options.body
15202
+ }
15203
+ }, undefined, false, undefined, this));
15204
+ return numericId;
15205
+ }
15117
15206
  function commentRangeStartMarker(commentId) {
15118
15207
  return /* @__PURE__ */ jsxDEV(w.commentRangeStart, {
15119
15208
  "w-id": commentId
@@ -15149,13 +15238,13 @@ function placeMarkersInParagraph(paragraph, markers) {
15149
15238
  }
15150
15239
  const pending = markers.slice();
15151
15240
  const state = { offset: 0, placedCount: 0 };
15152
- paragraph.children = walkAndPlace(paragraph.children, pending, true, state);
15241
+ paragraph.children = walkAndPlace(paragraph.children, pending, state);
15153
15242
  flushAtCurrentOffset(paragraph.children, pending, state);
15154
15243
  if (state.placedCount !== markers.length) {
15155
15244
  throw new SpanOutOfRangeError(`Could not place comment markers (placed ${state.placedCount} of ${markers.length})`);
15156
15245
  }
15157
15246
  }
15158
- function walkAndPlace(children, pending, isParagraphLevel, state) {
15247
+ function walkAndPlace(children, pending, state) {
15159
15248
  const result = [];
15160
15249
  for (const child of children) {
15161
15250
  if (child.tag === "w:r") {
@@ -15197,9 +15286,9 @@ function walkAndPlace(children, pending, isParagraphLevel, state) {
15197
15286
  state.offset = runEnd;
15198
15287
  continue;
15199
15288
  }
15200
- if (isParagraphLevel && (child.tag === "w:ins" || child.tag === "w:del")) {
15289
+ if (child.tag === "w:ins" || child.tag === "w:del" || child.tag === "w:hyperlink") {
15201
15290
  flushAtCurrentOffset(result, pending, state);
15202
- const innerChildren = walkAndPlace(child.children, pending, false, state);
15291
+ const innerChildren = walkAndPlace(child.children, pending, state);
15203
15292
  const wrapper = new XmlNode2(child.tag, { ...child.attributes });
15204
15293
  wrapper.children = innerChildren;
15205
15294
  result.push(wrapper);
@@ -16174,6 +16263,7 @@ async function run8(args) {
16174
16263
  allowPositionals: true,
16175
16264
  options: {
16176
16265
  at: { type: "string" },
16266
+ author: { type: "string" },
16177
16267
  output: { type: "string", short: "o" },
16178
16268
  "dry-run": { type: "boolean" },
16179
16269
  help: { type: "boolean", short: "h" }
@@ -16219,7 +16309,7 @@ async function run8(args) {
16219
16309
  if (blockRef.node.tag !== "w:p") {
16220
16310
  return fail("TRACKED_CHANGE_CONFLICT", "Tracked deletion of non-paragraph blocks (e.g., tables) is not supported", "Use `docx track-changes off` first, or delete table contents row-by-row.");
16221
16311
  }
16222
- applyTrackedDeletion(view, blockRef.node);
16312
+ applyTrackedDeletion(view, blockRef.node, parsed.values.author);
16223
16313
  } else {
16224
16314
  blockRef.parent.splice(targetIndex, 1);
16225
16315
  }
@@ -16232,9 +16322,9 @@ async function run8(args) {
16232
16322
  });
16233
16323
  return EXIT.OK;
16234
16324
  }
16235
- function applyTrackedDeletion(view, paragraph) {
16325
+ function applyTrackedDeletion(view, paragraph, authorFlag) {
16236
16326
  const allocator = createRevisionAllocator(view);
16237
- const baseMeta = { author: resolveAuthor(), date: resolveDate() };
16327
+ const baseMeta = { author: resolveAuthor(authorFlag), date: resolveDate() };
16238
16328
  const mintMeta = () => ({
16239
16329
  ...baseMeta,
16240
16330
  revisionId: allocator.next()
@@ -16271,6 +16361,7 @@ Usage:
16271
16361
  Locator (required):
16272
16362
  --at LOCATOR Block to remove (e.g., p3, t0)
16273
16363
 
16364
+ --author NAME Author for tracked changes (default: $DOCX_AUTHOR)
16274
16365
  -o, --output PATH Write to PATH instead of overwriting FILE
16275
16366
  --dry-run Print what would be removed; do not write the file
16276
16367
  -h, --help Show this help
@@ -16423,6 +16514,7 @@ async function run9(args) {
16423
16514
  color: { type: "string" },
16424
16515
  bold: { type: "boolean" },
16425
16516
  italic: { type: "boolean" },
16517
+ author: { type: "string" },
16426
16518
  output: { type: "string", short: "o" },
16427
16519
  "dry-run": { type: "boolean" },
16428
16520
  help: { type: "boolean", short: "h" }
@@ -16510,7 +16602,7 @@ async function run9(args) {
16510
16602
  return EXIT.OK;
16511
16603
  }
16512
16604
  if (isTrackChangesEnabled(view)) {
16513
- applyTrackedEdit(view, blockRef.node, paragraphNode);
16605
+ applyTrackedEdit(view, blockRef.node, paragraphNode, parsed.values.author);
16514
16606
  } else {
16515
16607
  blockRef.parent.splice(targetIndex, 1, paragraphNode);
16516
16608
  }
@@ -16523,9 +16615,9 @@ async function run9(args) {
16523
16615
  });
16524
16616
  return EXIT.OK;
16525
16617
  }
16526
- function applyTrackedEdit(view, existingParagraph, newParagraph) {
16618
+ function applyTrackedEdit(view, existingParagraph, newParagraph, authorFlag) {
16527
16619
  const allocator = createRevisionAllocator(view);
16528
- const baseMeta = { author: resolveAuthor(), date: resolveDate() };
16620
+ const baseMeta = { author: resolveAuthor(authorFlag), date: resolveDate() };
16529
16621
  const mintMeta = () => ({
16530
16622
  ...baseMeta,
16531
16623
  revisionId: allocator.next()
@@ -16592,6 +16684,7 @@ Run options (only with --text):
16592
16684
  --bold Bold
16593
16685
  --italic Italic
16594
16686
 
16687
+ --author NAME Author for tracked changes (default: $DOCX_AUTHOR)
16595
16688
  -o, --output PATH Write to PATH instead of overwriting FILE
16596
16689
  --dry-run Print what would change; do not write the file
16597
16690
  -h, --help Show this help
@@ -16933,6 +17026,7 @@ async function run11(args) {
16933
17026
  options: {
16934
17027
  at: { type: "string" },
16935
17028
  url: { type: "string" },
17029
+ author: { type: "string" },
16936
17030
  output: { type: "string", short: "o" },
16937
17031
  "dry-run": { type: "boolean" },
16938
17032
  help: { type: "boolean", short: "h" }
@@ -17001,6 +17095,13 @@ async function run11(args) {
17001
17095
  throw error;
17002
17096
  }
17003
17097
  view.hyperlinksByRelationshipId.set(relationshipId, { url });
17098
+ if (isTrackChangesEnabled(view)) {
17099
+ emitAuditComment(view, { kind: "span", paragraph: paragraphRef.node, span: target.span }, {
17100
+ body: `[docx-cli] hyperlink added \u2192 ${url}`,
17101
+ author: resolveAuthor(parsed.values.author),
17102
+ date: resolveDate()
17103
+ });
17104
+ }
17004
17105
  await saveDocView(view, outputPath);
17005
17106
  await respond({
17006
17107
  ok: true,
@@ -17023,6 +17124,8 @@ Required:
17023
17124
  --url URL Target URL
17024
17125
 
17025
17126
  Optional:
17127
+ --author NAME Author for the audit comment when track-changes is on
17128
+ (default: $DOCX_AUTHOR)
17026
17129
  -o, --output PATH Write to PATH instead of overwriting FILE
17027
17130
  --dry-run Print what would change; do not write the file
17028
17131
  -h, --help Show this help
@@ -17030,12 +17133,16 @@ Optional:
17030
17133
  The span must lie inside a single paragraph and must not overlap an existing
17031
17134
  hyperlink or a tracked-change wrapper.
17032
17135
 
17136
+ When track-changes is on, an audit comment is anchored to the wrapped span
17137
+ since OOXML has no native tracked-change form for hyperlink edits.
17138
+
17033
17139
  Examples:
17034
17140
  docx hyperlinks add doc.docx --at p3:5-20 --url https://example.com
17035
17141
  `;
17036
17142
  var init_add2 = __esm(() => {
17037
17143
  init_core();
17038
17144
  init_parser();
17145
+ init_helpers();
17039
17146
  init_respond();
17040
17147
  init_wrap();
17041
17148
  });
@@ -17054,6 +17161,7 @@ async function run12(args) {
17054
17161
  allowPositionals: true,
17055
17162
  options: {
17056
17163
  at: { type: "string" },
17164
+ author: { type: "string" },
17057
17165
  output: { type: "string", short: "o" },
17058
17166
  "dry-run": { type: "boolean" },
17059
17167
  help: { type: "boolean", short: "h" }
@@ -17098,6 +17206,9 @@ async function run12(args) {
17098
17206
  if (index === -1) {
17099
17207
  return fail("HYPERLINK_NOT_FOUND", `Hyperlink reference is stale (parent does not contain it): ${targetId}`);
17100
17208
  }
17209
+ const trackingOn = isTrackChangesEnabled(view);
17210
+ const paragraph = trackingOn ? findContainingParagraph(view.documentTree, reference.node) : null;
17211
+ const offsets = trackingOn && paragraph ? findElementOffsetsInParagraph(paragraph, reference.node) : null;
17101
17212
  reference.parent.splice(index, 1, ...reference.node.children);
17102
17213
  view.hyperlinkById.delete(targetId);
17103
17214
  if (reference.relationshipId) {
@@ -17107,6 +17218,13 @@ async function run12(args) {
17107
17218
  view.hyperlinksByRelationshipId.delete(reference.relationshipId);
17108
17219
  }
17109
17220
  }
17221
+ if (trackingOn && paragraph && offsets) {
17222
+ emitAuditComment(view, { kind: "span", paragraph, span: offsets }, {
17223
+ body: `[docx-cli] hyperlink removed (was: ${oldUrl ?? "(none)"})`,
17224
+ author: resolveAuthor(parsed.values.author),
17225
+ date: resolveDate()
17226
+ });
17227
+ }
17110
17228
  await saveDocView(view, outputPath);
17111
17229
  await respond({
17112
17230
  ok: true,
@@ -17149,6 +17267,8 @@ Required:
17149
17267
  --at LINK_ID Existing hyperlink to remove (e.g., link0)
17150
17268
 
17151
17269
  Optional:
17270
+ --author NAME Author for the audit comment when track-changes is on
17271
+ (default: $DOCX_AUTHOR)
17152
17272
  -o, --output PATH Write to PATH instead of overwriting FILE
17153
17273
  --dry-run Print what would change; do not write the file
17154
17274
  -h, --help Show this help
@@ -17157,12 +17277,16 @@ The display text stays in place; only the <w:hyperlink> wrapper is removed.
17157
17277
  If the underlying relationship is no longer referenced, it is pruned from the
17158
17278
  rels file too.
17159
17279
 
17280
+ When track-changes is on, an audit comment is anchored to the surviving text
17281
+ since OOXML has no native tracked-change form for hyperlink removal.
17282
+
17160
17283
  Examples:
17161
17284
  docx hyperlinks delete doc.docx --at link0
17162
17285
  `;
17163
17286
  var init_delete3 = __esm(() => {
17164
17287
  init_core();
17165
17288
  init_parser();
17289
+ init_helpers();
17166
17290
  init_respond();
17167
17291
  });
17168
17292
 
@@ -17272,6 +17396,7 @@ async function run14(args) {
17272
17396
  options: {
17273
17397
  at: { type: "string" },
17274
17398
  with: { type: "string" },
17399
+ author: { type: "string" },
17275
17400
  output: { type: "string", short: "o" },
17276
17401
  "dry-run": { type: "boolean" },
17277
17402
  help: { type: "boolean", short: "h" }
@@ -17333,6 +17458,17 @@ async function run14(args) {
17333
17458
  updateRelationshipTarget(relationships, existingId, newUrl);
17334
17459
  view.hyperlinksByRelationshipId.set(existingId, { url: newUrl });
17335
17460
  }
17461
+ if (isTrackChangesEnabled(view)) {
17462
+ const paragraph = findContainingParagraph(view.documentTree, reference.node);
17463
+ const offsets = paragraph ? findElementOffsetsInParagraph(paragraph, reference.node) : null;
17464
+ if (paragraph && offsets) {
17465
+ emitAuditComment(view, { kind: "span", paragraph, span: offsets }, {
17466
+ body: `[docx-cli] hyperlink target changed: ${oldUrl ?? "(none)"} \u2192 ${newUrl}`,
17467
+ author: resolveAuthor(parsed.values.author),
17468
+ date: resolveDate()
17469
+ });
17470
+ }
17471
+ }
17336
17472
  await saveDocView(view, outputPath);
17337
17473
  await respond({
17338
17474
  ok: true,
@@ -17381,6 +17517,8 @@ Required:
17381
17517
  --with URL New target URL
17382
17518
 
17383
17519
  Optional:
17520
+ --author NAME Author for the audit comment when track-changes is on
17521
+ (default: $DOCX_AUTHOR)
17384
17522
  -o, --output PATH Write to PATH instead of overwriting FILE
17385
17523
  --dry-run Print what would change; do not write the file
17386
17524
  -h, --help Show this help
@@ -17389,12 +17527,17 @@ Replaces only the targeted hyperlink. If multiple hyperlinks shared the same
17389
17527
  underlying relationship, a new relationship is allocated so the others are
17390
17528
  unaffected.
17391
17529
 
17530
+ When track-changes is on, an audit comment is anchored to the affected
17531
+ hyperlink span since OOXML has no native tracked-change form for hyperlink
17532
+ target edits.
17533
+
17392
17534
  Examples:
17393
17535
  docx hyperlinks replace doc.docx --at link0 --with https://example.com
17394
17536
  `;
17395
17537
  var init_replace = __esm(() => {
17396
17538
  init_core();
17397
17539
  init_parser();
17540
+ init_helpers();
17398
17541
  init_respond();
17399
17542
  });
17400
17543
 
@@ -17654,6 +17797,7 @@ async function run18(args) {
17654
17797
  options: {
17655
17798
  at: { type: "string" },
17656
17799
  with: { type: "string" },
17800
+ author: { type: "string" },
17657
17801
  output: { type: "string", short: "o" },
17658
17802
  "dry-run": { type: "boolean" },
17659
17803
  help: { type: "boolean", short: "h" }
@@ -17709,6 +17853,7 @@ async function run18(args) {
17709
17853
  return EXIT.OK;
17710
17854
  }
17711
17855
  const bytes = new Uint8Array(await sourceFile.arrayBuffer());
17856
+ const originalMimeType = reference.contentType;
17712
17857
  if (newPartName === originalPartName) {
17713
17858
  view.pkg.writeBytes(originalPartName, bytes);
17714
17859
  } else {
@@ -17719,6 +17864,18 @@ async function run18(args) {
17719
17864
  reference.partName = newPartName;
17720
17865
  reference.contentType = newMimeType;
17721
17866
  }
17867
+ if (isTrackChangesEnabled(view)) {
17868
+ const author = resolveAuthor(parsed.values.author);
17869
+ const date = resolveDate();
17870
+ const body = `[docx-cli] image replaced: ${originalPartName} (${originalMimeType}) \u2192 ${newPartName} (${newMimeType}, ${bytes.length} bytes)`;
17871
+ const drawingRuns = findDrawingRunsForRelationship(view.documentTree, reference.relationshipId);
17872
+ for (const drawingRun of drawingRuns) {
17873
+ const paragraph = findContainingParagraph(view.documentTree, drawingRun);
17874
+ if (!paragraph)
17875
+ continue;
17876
+ emitAuditComment(view, { kind: "run", paragraph, run: drawingRun }, { body, author, date });
17877
+ }
17878
+ }
17722
17879
  await saveDocView(view, outputPath);
17723
17880
  await respond({
17724
17881
  ok: true,
@@ -17731,6 +17888,33 @@ async function run18(args) {
17731
17888
  });
17732
17889
  return EXIT.OK;
17733
17890
  }
17891
+ function findDrawingRunsForRelationship(documentTree, relationshipId) {
17892
+ const matches = [];
17893
+ function walk(node) {
17894
+ if (node.tag === "w:r" && runReferencesImage(node, relationshipId)) {
17895
+ matches.push(node);
17896
+ return;
17897
+ }
17898
+ for (const child of node.children)
17899
+ walk(child);
17900
+ }
17901
+ for (const root of documentTree)
17902
+ walk(root);
17903
+ return matches;
17904
+ }
17905
+ function runReferencesImage(run19, relationshipId) {
17906
+ for (const child of run19.children) {
17907
+ if (child.tag !== "w:drawing")
17908
+ continue;
17909
+ const blip = child.findDescendant("a:blip");
17910
+ if (!blip)
17911
+ continue;
17912
+ const embed = blip.getAttribute("r:embed") ?? blip.getAttribute("r:link");
17913
+ if (embed === relationshipId)
17914
+ return true;
17915
+ }
17916
+ return false;
17917
+ }
17734
17918
  function renameExtension(partName, newExtension) {
17735
17919
  const dotIndex = partName.lastIndexOf(".");
17736
17920
  const slashIndex = partName.lastIndexOf("/");
@@ -17777,6 +17961,8 @@ Required:
17777
17961
  --with PATH New image file (any image MIME type)
17778
17962
 
17779
17963
  Optional:
17964
+ --author NAME Author for the audit comment when track-changes is on
17965
+ (default: $DOCX_AUTHOR)
17780
17966
  -o, --output PATH Write to PATH instead of overwriting FILE
17781
17967
  --dry-run Print what would change; do not write the file
17782
17968
  -h, --help Show this help
@@ -17785,6 +17971,10 @@ If the replacement uses a different format from the original, the part is
17785
17971
  renamed (extension changes), the relationship Target is rewritten, and
17786
17972
  [Content_Types].xml gets a Default entry for the new extension if needed.
17787
17973
 
17974
+ When track-changes is on, an audit comment is anchored to each drawing that
17975
+ referenced the swapped image since OOXML has no native tracked-change form
17976
+ for image replacement.
17977
+
17788
17978
  Examples:
17789
17979
  docx images replace doc.docx --at img2 --with ./new-photo.png
17790
17980
  docx images replace doc.docx --at img0 --with ./diagram.svg
@@ -17792,6 +17982,7 @@ Examples:
17792
17982
  var init_replace2 = __esm(() => {
17793
17983
  init_core();
17794
17984
  init_parser();
17985
+ init_helpers();
17795
17986
  init_respond();
17796
17987
  EXTENSION_BY_MIME = {
17797
17988
  "image/png": "png",
@@ -18036,10 +18227,10 @@ async function run20(args) {
18036
18227
  await respond(JSON_SCHEMA);
18037
18228
  return EXIT.OK;
18038
18229
  }
18039
- var HELP20 = `docx schema \u2014 print the AST type definitions
18230
+ var HELP20 = `docx info schema \u2014 print the AST type definitions
18040
18231
 
18041
18232
  Usage:
18042
- docx schema [options]
18233
+ docx info schema [options]
18043
18234
 
18044
18235
  Options:
18045
18236
  --json Print as a JSON Schema document (default)
@@ -18047,8 +18238,8 @@ Options:
18047
18238
  -h, --help Show this help
18048
18239
 
18049
18240
  Examples:
18050
- docx schema | jq '.definitions.Run'
18051
- docx schema --ts > ast.d.ts
18241
+ docx info schema | jq '.$defs.Run'
18242
+ docx info schema --ts > ast.d.ts
18052
18243
  `, JSON_SCHEMA;
18053
18244
  var init_schema = __esm(() => {
18054
18245
  init_types();
@@ -18316,14 +18507,18 @@ async function run21(args) {
18316
18507
  await writeStdout(REFERENCE);
18317
18508
  return EXIT.OK;
18318
18509
  }
18319
- var HELP21 = `docx locators \u2014 print the locator grammar reference
18510
+ var HELP21 = `docx info locators \u2014 print the locator grammar reference
18320
18511
 
18321
18512
  Usage:
18322
- docx locators [options]
18513
+ docx info locators [options]
18323
18514
 
18324
18515
  Options:
18325
18516
  --json Print as JSON
18326
18517
  -h, --help Show this help
18518
+
18519
+ Examples:
18520
+ docx info locators
18521
+ docx info locators --json | jq '.entityLocators'
18327
18522
  `, REFERENCE = `LOCATOR GRAMMAR
18328
18523
 
18329
18524
  Block locators:
@@ -18456,6 +18651,7 @@ async function run23(args) {
18456
18651
  bold: { type: "boolean" },
18457
18652
  italic: { type: "boolean" },
18458
18653
  url: { type: "string" },
18654
+ author: { type: "string" },
18459
18655
  output: { type: "string", short: "o" },
18460
18656
  "dry-run": { type: "boolean" },
18461
18657
  help: { type: "boolean", short: "h" }
@@ -18544,7 +18740,7 @@ async function run23(args) {
18544
18740
  }
18545
18741
  const insertIndex = after !== undefined ? targetIndex + 1 : targetIndex;
18546
18742
  if (isTrackChangesEnabled(view)) {
18547
- applyTrackedInsertion(paragraphNode, view);
18743
+ applyTrackedInsertion(paragraphNode, view, parsed.values.author);
18548
18744
  }
18549
18745
  const outputPath = parsed.values.output;
18550
18746
  if (parsed.values["dry-run"]) {
@@ -18593,9 +18789,9 @@ function wrapFirstRunInHyperlink(view, paragraph, url) {
18593
18789
  }
18594
18790
  paragraph.children = newChildren;
18595
18791
  }
18596
- function applyTrackedInsertion(paragraph, view) {
18792
+ function applyTrackedInsertion(paragraph, view, authorFlag) {
18597
18793
  const allocator = createRevisionAllocator(view);
18598
- const baseMeta = { author: resolveAuthor(), date: resolveDate() };
18794
+ const baseMeta = { author: resolveAuthor(authorFlag), date: resolveDate() };
18599
18795
  const mintMeta = () => ({
18600
18796
  ...baseMeta,
18601
18797
  revisionId: allocator.next()
@@ -18646,6 +18842,7 @@ Run options (only with --text):
18646
18842
  --italic Italic
18647
18843
  --url URL Wrap the inserted text in a hyperlink to URL
18648
18844
 
18845
+ --author NAME Author for tracked changes (default: $DOCX_AUTHOR)
18649
18846
  -o, --output PATH Write to PATH instead of overwriting FILE
18650
18847
  --dry-run Print what would be inserted; do not write the file
18651
18848
  -h, --help Show this help
@@ -19694,6 +19891,7 @@ async function run26(args) {
19694
19891
  "ignore-case": { type: "boolean" },
19695
19892
  all: { type: "boolean" },
19696
19893
  limit: { type: "string" },
19894
+ author: { type: "string" },
19697
19895
  output: { type: "string", short: "o" },
19698
19896
  "dry-run": { type: "boolean" },
19699
19897
  help: { type: "boolean", short: "h" }
@@ -19792,8 +19990,9 @@ async function run26(args) {
19792
19990
  }
19793
19991
  return rightMatch.start - leftMatch.start;
19794
19992
  });
19993
+ const authorFlag = parsed.values.author;
19795
19994
  const tracked = isTrackChangesEnabled(view) ? {
19796
- meta: { author: resolveAuthor(), date: resolveDate() },
19995
+ meta: { author: resolveAuthor(authorFlag), date: resolveDate() },
19797
19996
  allocator: createRevisionAllocator(view)
19798
19997
  } : undefined;
19799
19998
  const regexFlags = ignoreCase ? "i" : "";
@@ -19827,6 +20026,7 @@ Options:
19827
20026
  --ignore-case case-insensitive match
19828
20027
  --all replace every match (default: just the first)
19829
20028
  --limit N replace at most N matches (in document order)
20029
+ --author NAME author for tracked changes (default: $DOCX_AUTHOR)
19830
20030
  -o, --output PATH write to PATH instead of overwriting FILE
19831
20031
  --dry-run report what would change without writing the file
19832
20032
  -h, --help show this help
@@ -20220,7 +20420,7 @@ var init_wc = __esm(() => {
20220
20420
  // package.json
20221
20421
  var package_default = {
20222
20422
  name: "bun-docx",
20223
- version: "0.6.0",
20423
+ version: "0.6.1",
20224
20424
  description: "CLI for AI agents (Claude, Codex) to read, edit, and comment on .docx files with full format fidelity.",
20225
20425
  keywords: [
20226
20426
  "docx",
@@ -20299,7 +20499,7 @@ Commands:
20299
20499
  outline FILE List headings as a hierarchical tree
20300
20500
  comments \u2026 Add, reply, resolve, delete, list comments
20301
20501
  images \u2026 Extract, replace, list images
20302
- hyperlinks \u2026 List, replace hyperlinks
20502
+ hyperlinks \u2026 Add, list, replace, delete hyperlinks
20303
20503
  track-changes FILE on|off Toggle tracked-changes mode
20304
20504
  info \u2026 Reference material (schema, locators)
20305
20505
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bun-docx",
3
- "version": "0.6.0",
3
+ "version": "0.6.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",