bun-docx 0.2.0 → 0.4.0

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 +11 -12
  2. package/dist/index.js +1262 -546
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -13422,10 +13422,8 @@ class XmlNode2 {
13422
13422
  }
13423
13423
  static serialize(tree) {
13424
13424
  const builder = new Builder(BUILD_OPTIONS);
13425
- const flat = [];
13426
- flattenFragments(tree, flat);
13427
13425
  const pojo = [];
13428
- for (const node of flat) {
13426
+ for (const node of tree) {
13429
13427
  pojo.push(node.toObject());
13430
13428
  }
13431
13429
  return builder.build(pojo);
@@ -13516,6 +13514,15 @@ class XmlNode2 {
13516
13514
  }
13517
13515
  return out;
13518
13516
  }
13517
+ clone() {
13518
+ const cloned = new XmlNode2(this.tag, { ...this.attributes });
13519
+ if (this.text !== undefined)
13520
+ cloned.text = this.text;
13521
+ for (const child of this.children) {
13522
+ cloned.children.push(child.clone());
13523
+ }
13524
+ return cloned;
13525
+ }
13519
13526
  toObject() {
13520
13527
  if (this.isText) {
13521
13528
  return { "#text": this.text ?? "" };
@@ -13539,16 +13546,7 @@ class XmlNode2 {
13539
13546
  return result;
13540
13547
  }
13541
13548
  }
13542
- function flattenFragments(tree, out) {
13543
- for (const node of tree) {
13544
- if (node.tag === FRAGMENT_TAG) {
13545
- flattenFragments(node.children, out);
13546
- continue;
13547
- }
13548
- out.push(node);
13549
- }
13550
- }
13551
- var PARSE_OPTIONS, BUILD_OPTIONS, FRAGMENT_TAG = "#fragment";
13549
+ var PARSE_OPTIONS, BUILD_OPTIONS;
13552
13550
  var init_xml_node = __esm(() => {
13553
13551
  init_fxb();
13554
13552
  init_fxp();
@@ -13596,19 +13594,10 @@ function sliceRun(run, start, end) {
13596
13594
  consumed += text.length;
13597
13595
  continue;
13598
13596
  }
13599
- sliced.children.push(deepCloneNode(child));
13597
+ sliced.children.push(child.clone());
13600
13598
  }
13601
13599
  return sliced;
13602
13600
  }
13603
- function deepCloneNode(node) {
13604
- const clone = new XmlNode2(node.tag, { ...node.attributes });
13605
- if (node.text !== undefined)
13606
- clone.text = node.text;
13607
- for (const child of node.children) {
13608
- clone.children.push(deepCloneNode(child));
13609
- }
13610
- return clone;
13611
- }
13612
13601
  var init_run_ops = __esm(() => {
13613
13602
  init_xml_node();
13614
13603
  });
@@ -13665,6 +13654,26 @@ var init_parts = __esm(() => {
13665
13654
  });
13666
13655
 
13667
13656
  // src/core/package/index.ts
13657
+ import { lstat, rename, unlink } from "fs/promises";
13658
+ async function writeAtomic(target, buf) {
13659
+ let isSymlink = false;
13660
+ try {
13661
+ isSymlink = (await lstat(target)).isSymbolicLink();
13662
+ } catch {}
13663
+ if (isSymlink) {
13664
+ await Bun.write(target, buf);
13665
+ return;
13666
+ }
13667
+ const tmp = `${target}.docx-cli-tmp-${process.pid}-${Date.now()}`;
13668
+ try {
13669
+ await Bun.write(tmp, buf);
13670
+ await rename(tmp, target);
13671
+ } catch (err) {
13672
+ await unlink(tmp).catch(() => {});
13673
+ throw err;
13674
+ }
13675
+ }
13676
+
13668
13677
  class Pkg {
13669
13678
  zip;
13670
13679
  path;
@@ -13727,7 +13736,7 @@ class Pkg {
13727
13736
  compression: "DEFLATE",
13728
13737
  compressionOptions: { level: 6 }
13729
13738
  });
13730
- await Bun.write(target, buf);
13739
+ await writeAtomic(target, buf);
13731
13740
  }
13732
13741
  async toBytes() {
13733
13742
  return this.zip.generateAsync({
@@ -13934,8 +13943,9 @@ function readRun(view, node, activeComments, trackedChange, state) {
13934
13943
  }
13935
13944
  let combinedText = "";
13936
13945
  for (const child of node.children) {
13937
- if (child.tag === "w:t")
13946
+ if (child.tag === "w:t" || child.tag === "w:delText") {
13938
13947
  combinedText += child.collectText();
13948
+ }
13939
13949
  }
13940
13950
  if (combinedText.length === 0)
13941
13951
  return null;
@@ -14421,87 +14431,82 @@ var init_locators = __esm(() => {
14421
14431
  init_resolve();
14422
14432
  });
14423
14433
 
14424
- // src/core/index.ts
14425
- var init_core = __esm(() => {
14426
- init_ast();
14427
- init_locators();
14428
- init_package();
14429
- init_parser();
14430
- });
14431
-
14432
- // src/cli/respond.ts
14433
- async function respond(payload) {
14434
- await Bun.write(Bun.stdout, `${JSON.stringify(payload)}
14435
- `);
14436
- }
14437
- async function writeStdout(text) {
14438
- await Bun.write(Bun.stdout, text);
14434
+ // src/core/track-changes/index.ts
14435
+ function isTrackChangesEnabled(view) {
14436
+ if (!view.settingsTree)
14437
+ return false;
14438
+ const settingsRoot = XmlNode2.findRoot(view.settingsTree, "w:settings");
14439
+ if (!settingsRoot)
14440
+ return false;
14441
+ return settingsRoot.children.some((child) => child.tag === "w:trackChanges");
14439
14442
  }
14440
- async function fail(code, message, hint) {
14441
- const payload = {
14442
- ok: false,
14443
- code,
14444
- error: message
14443
+ function createRevisionAllocator(view) {
14444
+ let nextId = computeMaxRevisionId(view) + 1;
14445
+ return {
14446
+ next() {
14447
+ const id = nextId;
14448
+ nextId += 1;
14449
+ return id;
14450
+ }
14445
14451
  };
14446
- if (hint)
14447
- payload.hint = hint;
14448
- await respond(payload);
14449
- return exitCodeFor(code);
14450
14452
  }
14451
- function exitCodeFor(code) {
14452
- switch (code) {
14453
- case "USAGE":
14454
- case "INVALID_LOCATOR":
14455
- return EXIT.USAGE_ERROR;
14456
- case "FILE_NOT_FOUND":
14457
- case "PART_NOT_FOUND":
14458
- case "BLOCK_NOT_FOUND":
14459
- case "COMMENT_NOT_FOUND":
14460
- case "IMAGE_NOT_FOUND":
14461
- case "MATCH_NOT_FOUND":
14462
- return EXIT.NOT_FOUND;
14463
- case "NOT_A_ZIP":
14464
- case "TRACKED_CHANGE_CONFLICT":
14465
- case "UNHANDLED":
14466
- return EXIT.GENERAL_ERROR;
14467
- }
14453
+ function resolveAuthor(authorFlag) {
14454
+ if (authorFlag)
14455
+ return authorFlag;
14456
+ if (Bun.env.DOCX_AUTHOR)
14457
+ return Bun.env.DOCX_AUTHOR;
14458
+ if (Bun.env.DOCX_CLI_AUTHOR)
14459
+ return Bun.env.DOCX_CLI_AUTHOR;
14460
+ return "docx-cli";
14468
14461
  }
14469
- async function openOrFail(path) {
14470
- try {
14471
- return await openDocView(path);
14472
- } catch (err) {
14473
- if (err instanceof PkgError) {
14474
- if (err.code === "FILE_NOT_FOUND") {
14475
- return await fail("FILE_NOT_FOUND", err.message);
14476
- }
14477
- if (err.code === "NOT_A_ZIP")
14478
- return await fail("NOT_A_ZIP", err.message);
14479
- }
14480
- throw err;
14462
+ function resolveDate() {
14463
+ return Bun.env.DOCX_CLI_NOW ?? new Date().toISOString();
14464
+ }
14465
+ function convertTextToDelText(node) {
14466
+ const cloned = node.clone();
14467
+ mutateTextToDelText([cloned]);
14468
+ return cloned;
14469
+ }
14470
+ function computeMaxRevisionId(view) {
14471
+ let max = -1;
14472
+ walkXml(view.documentTree, (node) => {
14473
+ if (node.tag !== "w:ins" && node.tag !== "w:del")
14474
+ return;
14475
+ const idAttr = node.getAttribute("w:id");
14476
+ if (!idAttr)
14477
+ return;
14478
+ const value = Number(idAttr);
14479
+ if (Number.isFinite(value) && value > max)
14480
+ max = value;
14481
+ });
14482
+ return max;
14483
+ }
14484
+ function walkXml(nodes, visit) {
14485
+ for (const node of nodes) {
14486
+ visit(node);
14487
+ if (node.children.length > 0)
14488
+ walkXml(node.children, visit);
14481
14489
  }
14482
14490
  }
14483
- async function resolveBlockOrFail(view, locator) {
14484
- try {
14485
- return resolveBlock(view, locator);
14486
- } catch (err) {
14487
- if (err instanceof LocatorResolveError) {
14488
- return await fail("BLOCK_NOT_FOUND", err.message);
14489
- }
14490
- throw err;
14491
+ function mutateTextToDelText(nodes) {
14492
+ for (const node of nodes) {
14493
+ if (node.tag === "w:t")
14494
+ node.tag = "w:delText";
14495
+ mutateTextToDelText(node.children);
14491
14496
  }
14492
14497
  }
14493
- var EXIT;
14494
- var init_respond = __esm(() => {
14495
- init_core();
14496
- EXIT = {
14497
- OK: 0,
14498
- GENERAL_ERROR: 1,
14499
- USAGE_ERROR: 2,
14500
- NOT_FOUND: 3
14501
- };
14498
+ var init_track_changes = __esm(() => {
14499
+ init_parser();
14502
14500
  });
14503
14501
 
14504
14502
  // src/core/jsx/index.ts
14503
+ function normalizeChildren(children) {
14504
+ if (children === undefined || children === null)
14505
+ return [];
14506
+ if (Array.isArray(children))
14507
+ return children;
14508
+ return [children];
14509
+ }
14505
14510
  function namespace(prefix, tags) {
14506
14511
  const result = {};
14507
14512
  for (const tagName of tags) {
@@ -14510,19 +14515,22 @@ function namespace(prefix, tags) {
14510
14515
  return result;
14511
14516
  }
14512
14517
  function makeTag(qualifiedName) {
14513
- return (props, ...children) => {
14518
+ return (props) => {
14514
14519
  const attributes = {};
14520
+ let childrenProp;
14515
14521
  if (props) {
14516
14522
  for (const [key, value] of Object.entries(props)) {
14517
- if (key === "children")
14523
+ if (key === "children") {
14524
+ childrenProp = value;
14518
14525
  continue;
14526
+ }
14519
14527
  if (value === false || value == null)
14520
14528
  continue;
14521
14529
  attributes[mapAttributeName(key)] = String(value);
14522
14530
  }
14523
14531
  }
14524
14532
  const childNodes = [];
14525
- flatten(children, childNodes);
14533
+ flatten(normalizeChildren(childrenProp), childNodes);
14526
14534
  return new XmlNode2(qualifiedName, attributes, childNodes);
14527
14535
  };
14528
14536
  }
@@ -14535,7 +14543,7 @@ function flatten(items, out) {
14535
14543
  continue;
14536
14544
  }
14537
14545
  if (item instanceof XmlNode2) {
14538
- if (item.tag === FRAGMENT_TAG2) {
14546
+ if (item.tag === FRAGMENT_TAG) {
14539
14547
  for (const child of item.children)
14540
14548
  out.push(child);
14541
14549
  continue;
@@ -14554,7 +14562,7 @@ function mapAttributeName(name) {
14554
14562
  return name;
14555
14563
  return `${name.slice(0, dashIndex)}:${name.slice(dashIndex + 1)}`;
14556
14564
  }
14557
- var FRAGMENT_TAG2 = "#fragment", W_TAGS, R_TAGS, A_TAGS, WP_TAGS, PIC_TAGS, CP_TAGS, DC_TAGS, DCTERMS_TAGS, W14_TAGS, W15_TAGS, w, r, a, wp, pic, cp, dc, dcterms, w14, w15;
14565
+ var FRAGMENT_TAG = "#fragment", W_TAGS, R_TAGS, A_TAGS, WP_TAGS, PIC_TAGS, CP_TAGS, DC_TAGS, DCTERMS_TAGS, W14_TAGS, W15_TAGS, w, r, a, wp, pic, cp, dc, dcterms, w14, w15;
14558
14566
  var init_jsx = __esm(() => {
14559
14567
  init_parser();
14560
14568
  W_TAGS = [
@@ -14592,6 +14600,7 @@ var init_jsx = __esm(() => {
14592
14600
  "tcPr",
14593
14601
  "ins",
14594
14602
  "del",
14603
+ "delText",
14595
14604
  "commentRangeStart",
14596
14605
  "commentRangeEnd",
14597
14606
  "commentReference",
@@ -14646,18 +14655,9 @@ var init_jsx = __esm(() => {
14646
14655
 
14647
14656
  // src/core/jsx/jsx-runtime.ts
14648
14657
  function jsx(type, props, _key) {
14649
- const { children, ...rest } = props ?? {};
14650
- const childArgs = normalizeChildren(children);
14651
- const result = type(rest, ...childArgs);
14658
+ const result = type(props);
14652
14659
  return result ?? new XmlNode2("#fragment");
14653
14660
  }
14654
- function normalizeChildren(children) {
14655
- if (children === undefined)
14656
- return [];
14657
- if (Array.isArray(children))
14658
- return children;
14659
- return [children];
14660
- }
14661
14661
  var jsxDEV;
14662
14662
  var init_jsx_runtime = __esm(() => {
14663
14663
  init_parser();
@@ -14670,6 +14670,134 @@ var init_jsx_dev_runtime = __esm(() => {
14670
14670
  init_jsx_runtime();
14671
14671
  });
14672
14672
 
14673
+ // src/core/track-changes/emit.tsx
14674
+ function Ins({
14675
+ meta,
14676
+ children
14677
+ }) {
14678
+ return /* @__PURE__ */ jsxDEV(w.ins, {
14679
+ "w-id": String(meta.revisionId),
14680
+ "w-author": meta.author,
14681
+ "w-date": meta.date,
14682
+ children
14683
+ }, undefined, false, undefined, this);
14684
+ }
14685
+ function Del({
14686
+ meta,
14687
+ children
14688
+ }) {
14689
+ return /* @__PURE__ */ jsxDEV(w.del, {
14690
+ "w-id": String(meta.revisionId),
14691
+ "w-author": meta.author,
14692
+ "w-date": meta.date,
14693
+ children
14694
+ }, undefined, false, undefined, this);
14695
+ }
14696
+ function markParagraphMarkAs(paragraph, kind, meta) {
14697
+ let pPr = paragraph.findChild("w:pPr");
14698
+ if (!pPr) {
14699
+ pPr = /* @__PURE__ */ jsxDEV(w.pPr, {}, undefined, false, undefined, this);
14700
+ paragraph.children.unshift(pPr);
14701
+ }
14702
+ let rPr = pPr.findChild("w:rPr");
14703
+ if (!rPr) {
14704
+ rPr = /* @__PURE__ */ jsxDEV(w.rPr, {}, undefined, false, undefined, this);
14705
+ pPr.children.push(rPr);
14706
+ }
14707
+ const marker = kind === "ins" ? /* @__PURE__ */ jsxDEV(Ins, {
14708
+ meta
14709
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV(Del, {
14710
+ meta
14711
+ }, undefined, false, undefined, this);
14712
+ rPr.children.push(marker);
14713
+ }
14714
+ var init_emit = __esm(() => {
14715
+ init_jsx();
14716
+ init_jsx_dev_runtime();
14717
+ });
14718
+
14719
+ // src/core/index.ts
14720
+ var init_core = __esm(() => {
14721
+ init_ast();
14722
+ init_locators();
14723
+ init_package();
14724
+ init_parser();
14725
+ init_track_changes();
14726
+ init_emit();
14727
+ });
14728
+
14729
+ // src/cli/respond.ts
14730
+ async function respond(payload) {
14731
+ await Bun.write(Bun.stdout, `${JSON.stringify(payload)}
14732
+ `);
14733
+ }
14734
+ async function writeStdout(text) {
14735
+ await Bun.write(Bun.stdout, text);
14736
+ }
14737
+ async function fail(code, message, hint) {
14738
+ const payload = {
14739
+ ok: false,
14740
+ code,
14741
+ error: message
14742
+ };
14743
+ if (hint)
14744
+ payload.hint = hint;
14745
+ await respond(payload);
14746
+ return exitCodeFor(code);
14747
+ }
14748
+ function exitCodeFor(code) {
14749
+ switch (code) {
14750
+ case "USAGE":
14751
+ case "INVALID_LOCATOR":
14752
+ return EXIT.USAGE_ERROR;
14753
+ case "FILE_NOT_FOUND":
14754
+ case "PART_NOT_FOUND":
14755
+ case "BLOCK_NOT_FOUND":
14756
+ case "COMMENT_NOT_FOUND":
14757
+ case "IMAGE_NOT_FOUND":
14758
+ case "MATCH_NOT_FOUND":
14759
+ return EXIT.NOT_FOUND;
14760
+ case "NOT_A_ZIP":
14761
+ case "TRACKED_CHANGE_CONFLICT":
14762
+ case "UNHANDLED":
14763
+ return EXIT.GENERAL_ERROR;
14764
+ }
14765
+ }
14766
+ async function openOrFail(path) {
14767
+ try {
14768
+ return await openDocView(path);
14769
+ } catch (err) {
14770
+ if (err instanceof PkgError) {
14771
+ if (err.code === "FILE_NOT_FOUND") {
14772
+ return await fail("FILE_NOT_FOUND", err.message);
14773
+ }
14774
+ if (err.code === "NOT_A_ZIP")
14775
+ return await fail("NOT_A_ZIP", err.message);
14776
+ }
14777
+ throw err;
14778
+ }
14779
+ }
14780
+ async function resolveBlockOrFail(view, locator) {
14781
+ try {
14782
+ return resolveBlock(view, locator);
14783
+ } catch (err) {
14784
+ if (err instanceof LocatorResolveError) {
14785
+ return await fail("BLOCK_NOT_FOUND", err.message);
14786
+ }
14787
+ throw err;
14788
+ }
14789
+ }
14790
+ var EXIT;
14791
+ var init_respond = __esm(() => {
14792
+ init_core();
14793
+ EXIT = {
14794
+ OK: 0,
14795
+ GENERAL_ERROR: 1,
14796
+ USAGE_ERROR: 2,
14797
+ NOT_FOUND: 3
14798
+ };
14799
+ });
14800
+
14673
14801
  // src/cli/comments/helpers.tsx
14674
14802
  function nextCommentId(view) {
14675
14803
  if (!view.commentsTree)
@@ -15045,6 +15173,7 @@ async function run(args) {
15045
15173
  range: { type: "string" },
15046
15174
  text: { type: "string" },
15047
15175
  author: { type: "string" },
15176
+ output: { type: "string", short: "o" },
15048
15177
  "dry-run": { type: "boolean" },
15049
15178
  help: { type: "boolean", short: "h" }
15050
15179
  }
@@ -15094,6 +15223,7 @@ async function run(args) {
15094
15223
  const date = new Date().toISOString();
15095
15224
  const numericId = nextCommentId(view);
15096
15225
  const paraId = generateParaId();
15226
+ const outputPath = parsed.values.output;
15097
15227
  if (parsed.values["dry-run"]) {
15098
15228
  await respond({
15099
15229
  ok: true,
@@ -15101,7 +15231,8 @@ async function run(args) {
15101
15231
  dryRun: true,
15102
15232
  path,
15103
15233
  commentId: `c${numericId}`,
15104
- locator: rangeInput
15234
+ locator: rangeInput,
15235
+ ...outputPath ? { output: outputPath } : {}
15105
15236
  });
15106
15237
  return EXIT.OK;
15107
15238
  }
@@ -15124,11 +15255,11 @@ async function run(args) {
15124
15255
  text
15125
15256
  }
15126
15257
  }, undefined, false, undefined, this));
15127
- await saveDocView(view);
15258
+ await saveDocView(view, outputPath);
15128
15259
  await respond({
15129
15260
  ok: true,
15130
15261
  operation: "comments.add",
15131
- path,
15262
+ path: outputPath ?? path,
15132
15263
  commentId: `c${numericId}`,
15133
15264
  locator: rangeInput
15134
15265
  });
@@ -15148,6 +15279,7 @@ async function runCrossBlock(parsed, path, rangeInput, locator, text) {
15148
15279
  const date = new Date().toISOString();
15149
15280
  const numericId = nextCommentId(view);
15150
15281
  const paraId = generateParaId();
15282
+ const outputPath = parsed.values.output;
15151
15283
  if (parsed.values["dry-run"]) {
15152
15284
  await respond({
15153
15285
  ok: true,
@@ -15155,7 +15287,8 @@ async function runCrossBlock(parsed, path, rangeInput, locator, text) {
15155
15287
  dryRun: true,
15156
15288
  path,
15157
15289
  commentId: `c${numericId}`,
15158
- locator: rangeInput
15290
+ locator: rangeInput,
15291
+ ...outputPath ? { output: outputPath } : {}
15159
15292
  });
15160
15293
  return EXIT.OK;
15161
15294
  }
@@ -15178,11 +15311,11 @@ async function runCrossBlock(parsed, path, rangeInput, locator, text) {
15178
15311
  text
15179
15312
  }
15180
15313
  }, undefined, false, undefined, this));
15181
- await saveDocView(view);
15314
+ await saveDocView(view, outputPath);
15182
15315
  await respond({
15183
15316
  ok: true,
15184
15317
  operation: "comments.add",
15185
- path,
15318
+ path: outputPath ?? path,
15186
15319
  commentId: `c${numericId}`,
15187
15320
  locator: rangeInput
15188
15321
  });
@@ -15204,6 +15337,7 @@ Required:
15204
15337
 
15205
15338
  Optional:
15206
15339
  --author NAME Author name (default: $DOCX_AUTHOR)
15340
+ -o, --output PATH Write to PATH instead of overwriting FILE
15207
15341
  --dry-run Print what would be added; do not write the file
15208
15342
  -h, --help Show this help
15209
15343
 
@@ -15219,53 +15353,6 @@ var init_add = __esm(() => {
15219
15353
  init_jsx_dev_runtime();
15220
15354
  });
15221
15355
 
15222
- // src/cli/comments/trash.ts
15223
- import { dirname, join } from "path";
15224
- function trashPathFor(docPath) {
15225
- return join(dirname(docPath), TRASH_DIR, TRASH_FILE);
15226
- }
15227
- async function readTrash(docPath) {
15228
- const path = trashPathFor(docPath);
15229
- const file = Bun.file(path);
15230
- if (!await file.exists()) {
15231
- return { version: TRASH_VERSION, entries: [] };
15232
- }
15233
- const parsed = await file.json();
15234
- if (parsed.version !== TRASH_VERSION) {
15235
- return { version: TRASH_VERSION, entries: [] };
15236
- }
15237
- return parsed;
15238
- }
15239
- async function writeTrash(docPath, trash) {
15240
- const path = trashPathFor(docPath);
15241
- await Bun.write(path, `${JSON.stringify(trash, null, 2)}
15242
- `);
15243
- }
15244
- async function pushTrashEntry(docPath, entry) {
15245
- const trash = await readTrash(docPath);
15246
- trash.entries.push(entry);
15247
- await writeTrash(docPath, trash);
15248
- }
15249
- async function popTrashEntry(docPath, commentId) {
15250
- const trash = await readTrash(docPath);
15251
- const fileName = docPath.split("/").pop() ?? docPath;
15252
- for (let index = trash.entries.length - 1;index >= 0; index--) {
15253
- const entry = trash.entries[index];
15254
- if (!entry)
15255
- continue;
15256
- if (entry.file !== fileName)
15257
- continue;
15258
- if (entry.commentId !== commentId)
15259
- continue;
15260
- trash.entries.splice(index, 1);
15261
- await writeTrash(docPath, trash);
15262
- return entry;
15263
- }
15264
- return;
15265
- }
15266
- var TRASH_VERSION = 1, TRASH_DIR = ".docx-cli", TRASH_FILE = "trash.json";
15267
- var init_trash = () => {};
15268
-
15269
15356
  // src/cli/comments/delete.ts
15270
15357
  var exports_delete = {};
15271
15358
  __export(exports_delete, {
@@ -15280,6 +15367,7 @@ async function run2(args) {
15280
15367
  allowPositionals: true,
15281
15368
  options: {
15282
15369
  id: { type: "string" },
15370
+ output: { type: "string", short: "o" },
15283
15371
  "dry-run": { type: "boolean" },
15284
15372
  help: { type: "boolean", short: "h" }
15285
15373
  }
@@ -15307,31 +15395,19 @@ async function run2(args) {
15307
15395
  if (!commentReference) {
15308
15396
  return fail("COMMENT_NOT_FOUND", `Comment not found: ${commentId}`);
15309
15397
  }
15310
- const anchor = view.doc.comments.find((c) => c.id === commentId)?.anchor;
15311
- if (!anchor) {
15312
- return fail("COMMENT_NOT_FOUND", `Anchor not found for ${commentId}`);
15313
- }
15398
+ const outputPath = parsed.values.output;
15314
15399
  if (parsed.values["dry-run"]) {
15315
15400
  await respond({
15316
15401
  ok: true,
15317
15402
  operation: "comments.delete",
15318
15403
  dryRun: true,
15319
15404
  path,
15320
- commentId
15405
+ commentId,
15406
+ ...outputPath ? { output: outputPath } : {}
15321
15407
  });
15322
15408
  return EXIT.OK;
15323
15409
  }
15324
- const commentXml = XmlNode2.serialize([commentReference.node]);
15325
15410
  const paraId = findCommentParaId(view, commentId);
15326
- const extXml = paraId ? extractExtEntryXml(view, paraId) : null;
15327
- await pushTrashEntry(path, {
15328
- file: path.split("/").pop() ?? path,
15329
- deletedAt: new Date().toISOString(),
15330
- commentId,
15331
- anchor,
15332
- commentXml,
15333
- extXml
15334
- });
15335
15411
  const commentIndex = commentReference.parent.indexOf(commentReference.node);
15336
15412
  if (commentIndex !== -1)
15337
15413
  commentReference.parent.splice(commentIndex, 1);
@@ -15342,28 +15418,15 @@ async function run2(args) {
15342
15418
  }
15343
15419
  }
15344
15420
  removeCommentMarkers(view.documentTree, numericId);
15345
- await saveDocView(view);
15421
+ await saveDocView(view, outputPath);
15346
15422
  await respond({
15347
15423
  ok: true,
15348
15424
  operation: "comments.delete",
15349
- path,
15425
+ path: outputPath ?? path,
15350
15426
  commentId
15351
15427
  });
15352
15428
  return EXIT.OK;
15353
15429
  }
15354
- function extractExtEntryXml(view, paraId) {
15355
- if (!view.commentsExtTree)
15356
- return null;
15357
- const root = XmlNode2.findRoot(view.commentsExtTree, "w15:commentsEx");
15358
- if (!root)
15359
- return null;
15360
- for (const child of root.children) {
15361
- if (child.tag === "w15:commentEx" && child.getAttribute("w15:paraId") === paraId) {
15362
- return XmlNode2.serialize([child]);
15363
- }
15364
- }
15365
- return null;
15366
- }
15367
15430
  var HELP2 = `docx comments delete \u2014 remove a comment
15368
15431
 
15369
15432
  Usage:
@@ -15373,12 +15436,10 @@ Required:
15373
15436
  --id ID Comment id (e.g., c0)
15374
15437
 
15375
15438
  Optional:
15439
+ -o, --output PATH Write to PATH instead of overwriting FILE
15376
15440
  --dry-run Print what would be removed; do not write the file
15377
15441
  -h, --help Show this help
15378
15442
 
15379
- The deleted comment is journaled to <dir>/.docx-cli/trash.json so it can
15380
- be brought back via "docx comments restore".
15381
-
15382
15443
  Examples:
15383
15444
  docx comments delete doc.docx --id c2
15384
15445
  `;
@@ -15387,7 +15448,6 @@ var init_delete = __esm(() => {
15387
15448
  init_parser();
15388
15449
  init_respond();
15389
15450
  init_helpers();
15390
- init_trash();
15391
15451
  });
15392
15452
 
15393
15453
  // src/cli/comments/list.ts
@@ -15484,6 +15544,7 @@ async function run4(args) {
15484
15544
  to: { type: "string" },
15485
15545
  text: { type: "string" },
15486
15546
  author: { type: "string" },
15547
+ output: { type: "string", short: "o" },
15487
15548
  "dry-run": { type: "boolean" },
15488
15549
  help: { type: "boolean", short: "h" }
15489
15550
  }
@@ -15521,6 +15582,7 @@ async function run4(args) {
15521
15582
  const date = new Date().toISOString();
15522
15583
  const numericId = nextCommentId(view);
15523
15584
  const replyParaId = generateParaId();
15585
+ const outputPath = parsed.values.output;
15524
15586
  if (parsed.values["dry-run"]) {
15525
15587
  await respond({
15526
15588
  ok: true,
@@ -15528,7 +15590,8 @@ async function run4(args) {
15528
15590
  dryRun: true,
15529
15591
  path,
15530
15592
  commentId: `c${numericId}`,
15531
- parentId: `c${parentNumericId}`
15593
+ parentId: `c${parentNumericId}`,
15594
+ ...outputPath ? { output: outputPath } : {}
15532
15595
  });
15533
15596
  return EXIT.OK;
15534
15597
  }
@@ -15549,11 +15612,11 @@ async function run4(args) {
15549
15612
  "w15:paraIdParent": parentParaId,
15550
15613
  "w15:done": "0"
15551
15614
  }));
15552
- await saveDocView(view);
15615
+ await saveDocView(view, outputPath);
15553
15616
  await respond({
15554
15617
  ok: true,
15555
15618
  operation: "comments.reply",
15556
- path,
15619
+ path: outputPath ?? path,
15557
15620
  commentId: `c${numericId}`,
15558
15621
  parentId: `c${parentNumericId}`
15559
15622
  });
@@ -15570,6 +15633,7 @@ Required:
15570
15633
 
15571
15634
  Optional:
15572
15635
  --author NAME Author name (default: $DOCX_AUTHOR)
15636
+ -o, --output PATH Write to PATH instead of overwriting FILE
15573
15637
  --dry-run Print what would be added; do not write the file
15574
15638
  -h, --help Show this help
15575
15639
 
@@ -15599,6 +15663,7 @@ async function run5(args) {
15599
15663
  options: {
15600
15664
  id: { type: "string" },
15601
15665
  unset: { type: "boolean" },
15666
+ output: { type: "string", short: "o" },
15602
15667
  "dry-run": { type: "boolean" },
15603
15668
  help: { type: "boolean", short: "h" }
15604
15669
  }
@@ -15630,6 +15695,7 @@ async function run5(args) {
15630
15695
  if (!paraId) {
15631
15696
  return fail("COMMENT_NOT_FOUND", `Comment c${numericId} could not be assigned a w14:paraId.`);
15632
15697
  }
15698
+ const outputPath = parsed.values.output;
15633
15699
  if (parsed.values["dry-run"]) {
15634
15700
  await respond({
15635
15701
  ok: true,
@@ -15637,7 +15703,8 @@ async function run5(args) {
15637
15703
  dryRun: true,
15638
15704
  path,
15639
15705
  commentId: `c${numericId}`,
15640
- resolved
15706
+ resolved,
15707
+ ...outputPath ? { output: outputPath } : {}
15641
15708
  });
15642
15709
  return EXIT.OK;
15643
15710
  }
@@ -15651,11 +15718,11 @@ async function run5(args) {
15651
15718
  entry.setAttribute("w15:done", "1");
15652
15719
  else
15653
15720
  delete entry.attributes["w15:done"];
15654
- await saveDocView(view);
15721
+ await saveDocView(view, outputPath);
15655
15722
  await respond({
15656
15723
  ok: true,
15657
15724
  operation: "comments.resolve",
15658
- path,
15725
+ path: outputPath ?? path,
15659
15726
  commentId: `c${numericId}`,
15660
15727
  resolved
15661
15728
  });
@@ -15671,6 +15738,7 @@ Required:
15671
15738
 
15672
15739
  Optional:
15673
15740
  --unset Mark unresolved instead of resolved
15741
+ -o, --output PATH Write to PATH instead of overwriting FILE
15674
15742
  --dry-run Print what would change; do not write the file
15675
15743
  -h, --help Show this help
15676
15744
 
@@ -15685,132 +15753,15 @@ var init_resolve2 = __esm(() => {
15685
15753
  init_helpers();
15686
15754
  });
15687
15755
 
15688
- // src/cli/comments/restore.ts
15689
- var exports_restore = {};
15690
- __export(exports_restore, {
15756
+ // src/cli/comments/index.ts
15757
+ var exports_comments = {};
15758
+ __export(exports_comments, {
15691
15759
  run: () => run6
15692
15760
  });
15693
- import { parseArgs as parseArgs6 } from "util";
15694
15761
  async function run6(args) {
15695
- let parsed;
15696
- try {
15697
- parsed = parseArgs6({
15698
- args,
15699
- allowPositionals: true,
15700
- options: {
15701
- id: { type: "string" },
15702
- "dry-run": { type: "boolean" },
15703
- help: { type: "boolean", short: "h" }
15704
- }
15705
- });
15706
- } catch (parseError) {
15707
- const message = parseError instanceof Error ? parseError.message : String(parseError);
15708
- return fail("USAGE", message, HELP6);
15709
- }
15710
- if (parsed.values.help) {
15711
- await writeStdout(HELP6);
15712
- return EXIT.OK;
15713
- }
15714
- const path = parsed.positionals[0];
15715
- if (!path)
15716
- return fail("USAGE", "Missing FILE argument", HELP6);
15717
- const idInput = parsed.values.id;
15718
- if (!idInput)
15719
- return fail("USAGE", "Missing --id COMMENT_ID", HELP6);
15720
- const commentId = idInput.startsWith("c") ? idInput : `c${idInput}`;
15721
- const numericId = commentId.slice(1);
15722
- if (parsed.values["dry-run"]) {
15723
- await respond({
15724
- ok: true,
15725
- operation: "comments.restore",
15726
- dryRun: true,
15727
- path,
15728
- commentId
15729
- });
15730
- return EXIT.OK;
15731
- }
15732
- const entry = await popTrashEntry(path, commentId);
15733
- if (!entry) {
15734
- return fail("COMMENT_NOT_FOUND", `No trashed entry for ${commentId} in ${path}`, "Trash lives at <dir>/.docx-cli/trash.json \u2014 make sure it's the same directory.");
15735
- }
15736
- const view = await openOrFail(path);
15737
- if (typeof view === "number")
15738
- return view;
15739
- const startBlock = view.blockReferences.get(entry.anchor.startBlockId);
15740
- if (!startBlock) {
15741
- return fail("BLOCK_NOT_FOUND", `Original anchor block ${entry.anchor.startBlockId} no longer exists`);
15742
- }
15743
- const endBlock = view.blockReferences.get(entry.anchor.endBlockId);
15744
- if (!endBlock) {
15745
- return fail("BLOCK_NOT_FOUND", `Original anchor block ${entry.anchor.endBlockId} no longer exists`);
15746
- }
15747
- try {
15748
- addCommentRangeMarkers(startBlock.node, entry.anchor.startOffset, endBlock.node, entry.anchor.endOffset, numericId);
15749
- } catch (error) {
15750
- if (error instanceof SpanOutOfRangeError) {
15751
- return fail("INVALID_LOCATOR", `Saved span no longer fits the block: ${error.message}`);
15752
- }
15753
- throw error;
15754
- }
15755
- const commentNodes = XmlNode2.parse(entry.commentXml);
15756
- const commentNode = commentNodes[0];
15757
- if (!commentNode) {
15758
- return fail("USAGE", "Trashed commentXml is empty");
15759
- }
15760
- const commentsRoot = ensureCommentsPart(view);
15761
- commentsRoot.children.push(commentNode);
15762
- if (entry.extXml) {
15763
- const extNodes = XmlNode2.parse(entry.extXml);
15764
- const extNode = extNodes[0];
15765
- if (extNode) {
15766
- const extRoot = ensureCommentsExtPart(view);
15767
- extRoot.children.push(extNode);
15768
- }
15769
- }
15770
- await saveDocView(view);
15771
- await respond({
15772
- ok: true,
15773
- operation: "comments.restore",
15774
- path,
15775
- commentId
15776
- });
15777
- return EXIT.OK;
15778
- }
15779
- var HELP6 = `docx comments restore \u2014 undo a recent delete
15780
-
15781
- Usage:
15782
- docx comments restore FILE --id cN [options]
15783
-
15784
- Required:
15785
- --id ID Comment id to restore (e.g., c0)
15786
-
15787
- Optional:
15788
- --dry-run Print what would be restored; do not write the file
15789
- -h, --help Show this help
15790
-
15791
- Pulls the most recent matching entry from <dir>/.docx-cli/trash.json
15792
- and re-anchors the comment at its original location.
15793
-
15794
- Examples:
15795
- docx comments restore doc.docx --id c2
15796
- `;
15797
- var init_restore = __esm(() => {
15798
- init_core();
15799
- init_parser();
15800
- init_respond();
15801
- init_helpers();
15802
- init_trash();
15803
- });
15804
-
15805
- // src/cli/comments/index.ts
15806
- var exports_comments = {};
15807
- __export(exports_comments, {
15808
- run: () => run7
15809
- });
15810
- async function run7(args) {
15811
15762
  const verb = args[0];
15812
15763
  if (!verb || verb === "--help" || verb === "-h" || verb === "help") {
15813
- await writeStdout(HELP7);
15764
+ await writeStdout(HELP6);
15814
15765
  return verb ? 0 : 2;
15815
15766
  }
15816
15767
  const loader = SUBCOMMANDS[verb];
@@ -15820,7 +15771,7 @@ async function run7(args) {
15820
15771
  const module_ = await loader();
15821
15772
  return module_.run(args.slice(1));
15822
15773
  }
15823
- var SUBCOMMANDS, HELP7 = `docx comments \u2014 manage Word comments
15774
+ var SUBCOMMANDS, HELP6 = `docx comments \u2014 manage Word comments
15824
15775
 
15825
15776
  Usage:
15826
15777
  docx comments <verb> FILE [options]
@@ -15831,7 +15782,6 @@ Verbs:
15831
15782
  list Print existing comments as JSON
15832
15783
  resolve Mark a comment resolved
15833
15784
  delete Remove a comment
15834
- restore Restore a recently deleted comment
15835
15785
 
15836
15786
  Run "docx comments <verb> --help" for verb-specific help.
15837
15787
  `;
@@ -15842,8 +15792,7 @@ var init_comments = __esm(() => {
15842
15792
  delete: () => Promise.resolve().then(() => (init_delete(), exports_delete)),
15843
15793
  list: () => Promise.resolve().then(() => (init_list(), exports_list)),
15844
15794
  reply: () => Promise.resolve().then(() => (init_reply(), exports_reply)),
15845
- resolve: () => Promise.resolve().then(() => (init_resolve2(), exports_resolve)),
15846
- restore: () => Promise.resolve().then(() => (init_restore(), exports_restore))
15795
+ resolve: () => Promise.resolve().then(() => (init_resolve2(), exports_resolve))
15847
15796
  };
15848
15797
  });
15849
15798
 
@@ -15939,13 +15888,13 @@ var init_template = __esm(() => {
15939
15888
  // src/cli/create/index.tsx
15940
15889
  var exports_create = {};
15941
15890
  __export(exports_create, {
15942
- run: () => run8
15891
+ run: () => run7
15943
15892
  });
15944
- import { parseArgs as parseArgs7 } from "util";
15945
- async function run8(args) {
15893
+ import { parseArgs as parseArgs6 } from "util";
15894
+ async function run7(args) {
15946
15895
  let parsed;
15947
15896
  try {
15948
- parsed = parseArgs7({
15897
+ parsed = parseArgs6({
15949
15898
  args,
15950
15899
  allowPositionals: true,
15951
15900
  options: {
@@ -15957,15 +15906,15 @@ async function run8(args) {
15957
15906
  }
15958
15907
  });
15959
15908
  } catch (e) {
15960
- return fail("USAGE", e instanceof Error ? e.message : String(e), HELP8);
15909
+ return fail("USAGE", e instanceof Error ? e.message : String(e), HELP7);
15961
15910
  }
15962
15911
  if (parsed.values.help) {
15963
- await writeStdout(HELP8);
15912
+ await writeStdout(HELP7);
15964
15913
  return EXIT.OK;
15965
15914
  }
15966
15915
  const path = parsed.positionals[0];
15967
15916
  if (!path) {
15968
- return fail("USAGE", "Missing FILE argument", HELP8);
15917
+ return fail("USAGE", "Missing FILE argument", HELP7);
15969
15918
  }
15970
15919
  if (await Bun.file(path).exists() && !parsed.values.force) {
15971
15920
  return fail("USAGE", `File already exists: ${path}`, "Pass --force to overwrite.");
@@ -15985,7 +15934,7 @@ async function run8(args) {
15985
15934
  compression: "DEFLATE",
15986
15935
  compressionOptions: { level: 6 }
15987
15936
  });
15988
- await Bun.write(path, buf);
15937
+ await writeAtomic(path, buf);
15989
15938
  await respond({
15990
15939
  ok: true,
15991
15940
  operation: "create",
@@ -15995,7 +15944,7 @@ async function run8(args) {
15995
15944
  });
15996
15945
  return EXIT.OK;
15997
15946
  }
15998
- var import_jszip2, HELP8 = `docx create \u2014 create a new minimal .docx
15947
+ var import_jszip2, HELP7 = `docx create \u2014 create a new minimal .docx
15999
15948
 
16000
15949
  Usage:
16001
15950
  docx create FILE [options]
@@ -16012,43 +15961,45 @@ Examples:
16012
15961
  docx create out.docx --title "Spec" --author "Claude" --text "First paragraph."
16013
15962
  `;
16014
15963
  var init_create = __esm(() => {
15964
+ init_package();
16015
15965
  init_respond();
16016
15966
  init_template();
16017
15967
  import_jszip2 = __toESM(require_lib3(), 1);
16018
15968
  });
16019
15969
 
16020
- // src/cli/delete/index.ts
15970
+ // src/cli/delete/index.tsx
16021
15971
  var exports_delete2 = {};
16022
15972
  __export(exports_delete2, {
16023
- run: () => run9
15973
+ run: () => run8
16024
15974
  });
16025
- import { parseArgs as parseArgs8 } from "util";
16026
- async function run9(args) {
15975
+ import { parseArgs as parseArgs7 } from "util";
15976
+ async function run8(args) {
16027
15977
  let parsed;
16028
15978
  try {
16029
- parsed = parseArgs8({
15979
+ parsed = parseArgs7({
16030
15980
  args,
16031
15981
  allowPositionals: true,
16032
15982
  options: {
16033
15983
  at: { type: "string" },
15984
+ output: { type: "string", short: "o" },
16034
15985
  "dry-run": { type: "boolean" },
16035
15986
  help: { type: "boolean", short: "h" }
16036
15987
  }
16037
15988
  });
16038
15989
  } catch (parseError) {
16039
15990
  const message = parseError instanceof Error ? parseError.message : String(parseError);
16040
- return fail("USAGE", message, HELP9);
15991
+ return fail("USAGE", message, HELP8);
16041
15992
  }
16042
15993
  if (parsed.values.help) {
16043
- await writeStdout(HELP9);
15994
+ await writeStdout(HELP8);
16044
15995
  return EXIT.OK;
16045
15996
  }
16046
15997
  const path = parsed.positionals[0];
16047
15998
  if (!path)
16048
- return fail("USAGE", "Missing FILE argument", HELP9);
15999
+ return fail("USAGE", "Missing FILE argument", HELP8);
16049
16000
  const at = parsed.values.at;
16050
16001
  if (!at)
16051
- return fail("USAGE", "Missing --at LOCATOR", HELP9);
16002
+ return fail("USAGE", "Missing --at LOCATOR", HELP8);
16052
16003
  const view = await openOrFail(path);
16053
16004
  if (typeof view === "number")
16054
16005
  return view;
@@ -16059,22 +16010,67 @@ async function run9(args) {
16059
16010
  if (targetIndex === -1) {
16060
16011
  return fail("BLOCK_NOT_FOUND", "Block reference is stale (parent does not contain it)");
16061
16012
  }
16013
+ const outputPath = parsed.values.output;
16062
16014
  if (parsed.values["dry-run"]) {
16063
16015
  await respond({
16064
16016
  ok: true,
16065
16017
  operation: "delete",
16066
16018
  dryRun: true,
16067
16019
  path,
16068
- locator: at
16020
+ locator: at,
16021
+ ...outputPath ? { output: outputPath } : {}
16069
16022
  });
16070
16023
  return EXIT.OK;
16071
16024
  }
16072
- blockRef.parent.splice(targetIndex, 1);
16073
- await saveDocView(view);
16074
- await respond({ ok: true, operation: "delete", path, locator: at });
16025
+ if (isTrackChangesEnabled(view)) {
16026
+ if (blockRef.node.tag !== "w:p") {
16027
+ 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.");
16028
+ }
16029
+ applyTrackedDeletion(view, blockRef.node);
16030
+ } else {
16031
+ blockRef.parent.splice(targetIndex, 1);
16032
+ }
16033
+ await saveDocView(view, outputPath);
16034
+ await respond({
16035
+ ok: true,
16036
+ operation: "delete",
16037
+ path: outputPath ?? path,
16038
+ locator: at
16039
+ });
16075
16040
  return EXIT.OK;
16076
16041
  }
16077
- var HELP9 = `docx delete \u2014 remove a block at a locator
16042
+ function applyTrackedDeletion(view, paragraph) {
16043
+ const allocator = createRevisionAllocator(view);
16044
+ const baseMeta = { author: resolveAuthor(), date: resolveDate() };
16045
+ const mintMeta = () => ({
16046
+ ...baseMeta,
16047
+ revisionId: allocator.next()
16048
+ });
16049
+ const newChildren = [];
16050
+ let runBuffer = [];
16051
+ const flush = () => {
16052
+ if (runBuffer.length === 0)
16053
+ return;
16054
+ const converted = runBuffer.map((run9) => convertTextToDelText(run9));
16055
+ newChildren.push(/* @__PURE__ */ jsxDEV(Del, {
16056
+ meta: mintMeta(),
16057
+ children: converted
16058
+ }, undefined, false, undefined, this));
16059
+ runBuffer = [];
16060
+ };
16061
+ for (const child of paragraph.children) {
16062
+ if (child.tag === "w:r") {
16063
+ runBuffer.push(child);
16064
+ continue;
16065
+ }
16066
+ flush();
16067
+ newChildren.push(child);
16068
+ }
16069
+ flush();
16070
+ paragraph.children = newChildren;
16071
+ markParagraphMarkAs(paragraph, "del", mintMeta());
16072
+ }
16073
+ var HELP8 = `docx delete \u2014 remove a block at a locator
16078
16074
 
16079
16075
  Usage:
16080
16076
  docx delete FILE [options]
@@ -16082,6 +16078,7 @@ Usage:
16082
16078
  Locator (required):
16083
16079
  --at LOCATOR Block to remove (e.g., p3, t0)
16084
16080
 
16081
+ -o, --output PATH Write to PATH instead of overwriting FILE
16085
16082
  --dry-run Print what would be removed; do not write the file
16086
16083
  -h, --help Show this help
16087
16084
 
@@ -16092,6 +16089,7 @@ Examples:
16092
16089
  var init_delete2 = __esm(() => {
16093
16090
  init_core();
16094
16091
  init_respond();
16092
+ init_jsx_dev_runtime();
16095
16093
  });
16096
16094
 
16097
16095
  // src/cli/insert/emit.tsx
@@ -16103,68 +16101,68 @@ function Paragraph(props) {
16103
16101
  /* @__PURE__ */ jsxDEV(ParagraphProperties, {
16104
16102
  options: { style, alignment }
16105
16103
  }, undefined, false, undefined, this),
16106
- runs.map((run10) => /* @__PURE__ */ jsxDEV(RunElement, {
16107
- run: run10
16104
+ runs.map((run9) => /* @__PURE__ */ jsxDEV(RunElement, {
16105
+ run: run9
16108
16106
  }, undefined, false, undefined, this))
16109
16107
  ]
16110
16108
  }, undefined, true, undefined, this);
16111
16109
  }
16112
- function RunElement({ run: run10 }) {
16113
- if (run10.type === "text")
16110
+ function RunElement({ run: run9 }) {
16111
+ if (run9.type === "text")
16114
16112
  return /* @__PURE__ */ jsxDEV(TextRunElement, {
16115
- run: run10
16113
+ run: run9
16116
16114
  }, undefined, false, undefined, this);
16117
- if (run10.type === "break") {
16115
+ if (run9.type === "break") {
16118
16116
  return /* @__PURE__ */ jsxDEV(w.r, {
16119
16117
  children: /* @__PURE__ */ jsxDEV(w.br, {
16120
- "w-type": run10.kind === "line" ? undefined : run10.kind
16118
+ "w-type": run9.kind === "line" ? undefined : run9.kind
16121
16119
  }, undefined, false, undefined, this)
16122
16120
  }, undefined, false, undefined, this);
16123
16121
  }
16124
- if (run10.type === "tab") {
16122
+ if (run9.type === "tab") {
16125
16123
  return /* @__PURE__ */ jsxDEV(w.r, {
16126
16124
  children: /* @__PURE__ */ jsxDEV(w.tab, {}, undefined, false, undefined, this)
16127
16125
  }, undefined, false, undefined, this);
16128
16126
  }
16129
- throw new Error(`Cannot emit run of type "${run10.type}" \u2014 image emission lives in the images command.`);
16127
+ throw new Error(`Cannot emit run of type "${run9.type}" \u2014 image emission lives in the images command.`);
16130
16128
  }
16131
- function TextRunElement({ run: run10 }) {
16129
+ function TextRunElement({ run: run9 }) {
16132
16130
  return /* @__PURE__ */ jsxDEV(w.r, {
16133
16131
  children: [
16134
16132
  /* @__PURE__ */ jsxDEV(RunProperties, {
16135
- run: run10
16133
+ run: run9
16136
16134
  }, undefined, false, undefined, this),
16137
16135
  /* @__PURE__ */ jsxDEV(w.t, {
16138
16136
  "xml:space": "preserve",
16139
- children: run10.text
16137
+ children: run9.text
16140
16138
  }, undefined, false, undefined, this)
16141
16139
  ]
16142
16140
  }, undefined, true, undefined, this);
16143
16141
  }
16144
- function RunProperties({ run: run10 }) {
16145
- const isEmpty = FORMATTING_KEYS.every((key) => run10[key] == null);
16142
+ function RunProperties({ run: run9 }) {
16143
+ const isEmpty = FORMATTING_KEYS.every((key) => run9[key] == null);
16146
16144
  if (isEmpty)
16147
16145
  return null;
16148
16146
  return /* @__PURE__ */ jsxDEV(w.rPr, {
16149
16147
  children: [
16150
- run10.color && /* @__PURE__ */ jsxDEV(w.color, {
16151
- "w-val": run10.color
16148
+ run9.color && /* @__PURE__ */ jsxDEV(w.color, {
16149
+ "w-val": run9.color
16152
16150
  }, undefined, false, undefined, this),
16153
- run10.highlight && /* @__PURE__ */ jsxDEV(w.highlight, {
16154
- "w-val": run10.highlight
16151
+ run9.highlight && /* @__PURE__ */ jsxDEV(w.highlight, {
16152
+ "w-val": run9.highlight
16155
16153
  }, undefined, false, undefined, this),
16156
- run10.bold && /* @__PURE__ */ jsxDEV(w.b, {}, undefined, false, undefined, this),
16157
- run10.italic && /* @__PURE__ */ jsxDEV(w.i, {}, undefined, false, undefined, this),
16158
- run10.underline && /* @__PURE__ */ jsxDEV(w.u, {
16159
- "w-val": run10.underline
16154
+ run9.bold && /* @__PURE__ */ jsxDEV(w.b, {}, undefined, false, undefined, this),
16155
+ run9.italic && /* @__PURE__ */ jsxDEV(w.i, {}, undefined, false, undefined, this),
16156
+ run9.underline && /* @__PURE__ */ jsxDEV(w.u, {
16157
+ "w-val": run9.underline
16160
16158
  }, undefined, false, undefined, this),
16161
- run10.strike && /* @__PURE__ */ jsxDEV(w.strike, {}, undefined, false, undefined, this),
16162
- run10.font && /* @__PURE__ */ jsxDEV(w.rFonts, {
16163
- "w-ascii": run10.font,
16164
- "w-hAnsi": run10.font
16159
+ run9.strike && /* @__PURE__ */ jsxDEV(w.strike, {}, undefined, false, undefined, this),
16160
+ run9.font && /* @__PURE__ */ jsxDEV(w.rFonts, {
16161
+ "w-ascii": run9.font,
16162
+ "w-hAnsi": run9.font
16165
16163
  }, undefined, false, undefined, this),
16166
- run10.sizeHalfPoints !== undefined && /* @__PURE__ */ jsxDEV(w.sz, {
16167
- "w-val": String(run10.sizeHalfPoints)
16164
+ run9.sizeHalfPoints !== undefined && /* @__PURE__ */ jsxDEV(w.sz, {
16165
+ "w-val": String(run9.sizeHalfPoints)
16168
16166
  }, undefined, false, undefined, this)
16169
16167
  ]
16170
16168
  }, undefined, true, undefined, this);
@@ -16196,7 +16194,7 @@ function textRunFromProps(props) {
16196
16194
  return { type: "text", text, ...formatting };
16197
16195
  }
16198
16196
  var FORMATTING_KEYS;
16199
- var init_emit = __esm(() => {
16197
+ var init_emit2 = __esm(() => {
16200
16198
  init_jsx();
16201
16199
  init_jsx_dev_runtime();
16202
16200
  FORMATTING_KEYS = [
@@ -16214,13 +16212,13 @@ var init_emit = __esm(() => {
16214
16212
  // src/cli/edit/index.tsx
16215
16213
  var exports_edit = {};
16216
16214
  __export(exports_edit, {
16217
- run: () => run10
16215
+ run: () => run9
16218
16216
  });
16219
- import { parseArgs as parseArgs9 } from "util";
16220
- async function run10(args) {
16217
+ import { parseArgs as parseArgs8 } from "util";
16218
+ async function run9(args) {
16221
16219
  let parsed;
16222
16220
  try {
16223
- parsed = parseArgs9({
16221
+ parsed = parseArgs8({
16224
16222
  args,
16225
16223
  allowPositionals: true,
16226
16224
  options: {
@@ -16232,31 +16230,32 @@ async function run10(args) {
16232
16230
  color: { type: "string" },
16233
16231
  bold: { type: "boolean" },
16234
16232
  italic: { type: "boolean" },
16233
+ output: { type: "string", short: "o" },
16235
16234
  "dry-run": { type: "boolean" },
16236
16235
  help: { type: "boolean", short: "h" }
16237
16236
  }
16238
16237
  });
16239
16238
  } catch (parseError) {
16240
16239
  const message = parseError instanceof Error ? parseError.message : String(parseError);
16241
- return fail("USAGE", message, HELP10);
16240
+ return fail("USAGE", message, HELP9);
16242
16241
  }
16243
16242
  if (parsed.values.help) {
16244
- await writeStdout(HELP10);
16243
+ await writeStdout(HELP9);
16245
16244
  return EXIT.OK;
16246
16245
  }
16247
16246
  const path = parsed.positionals[0];
16248
16247
  if (!path)
16249
- return fail("USAGE", "Missing FILE argument", HELP10);
16248
+ return fail("USAGE", "Missing FILE argument", HELP9);
16250
16249
  const at = parsed.values.at;
16251
16250
  if (!at)
16252
- return fail("USAGE", "Missing --at LOCATOR", HELP10);
16251
+ return fail("USAGE", "Missing --at LOCATOR", HELP9);
16253
16252
  const text = parsed.values.text;
16254
16253
  const runsJson = parsed.values.runs;
16255
16254
  if (!text && !runsJson) {
16256
- return fail("USAGE", "Missing content: pass --text or --runs", HELP10);
16255
+ return fail("USAGE", "Missing content: pass --text or --runs", HELP9);
16257
16256
  }
16258
16257
  if (text && runsJson) {
16259
- return fail("USAGE", "Pass either --text or --runs, not both", HELP10);
16258
+ return fail("USAGE", "Pass either --text or --runs, not both", HELP9);
16260
16259
  }
16261
16260
  const paragraphOptions = {};
16262
16261
  const styleValue = parsed.values.style;
@@ -16305,22 +16304,81 @@ async function run10(args) {
16305
16304
  if (targetIndex === -1) {
16306
16305
  return fail("BLOCK_NOT_FOUND", "Block reference is stale (parent does not contain it)");
16307
16306
  }
16307
+ const outputPath = parsed.values.output;
16308
16308
  if (parsed.values["dry-run"]) {
16309
16309
  await respond({
16310
16310
  ok: true,
16311
16311
  operation: "edit",
16312
16312
  dryRun: true,
16313
16313
  path,
16314
- locator: at
16314
+ locator: at,
16315
+ ...outputPath ? { output: outputPath } : {}
16315
16316
  });
16316
16317
  return EXIT.OK;
16317
16318
  }
16318
- blockRef.parent.splice(targetIndex, 1, paragraphNode);
16319
- await saveDocView(view);
16320
- await respond({ ok: true, operation: "edit", path, locator: at });
16319
+ if (isTrackChangesEnabled(view)) {
16320
+ applyTrackedEdit(view, blockRef.node, paragraphNode);
16321
+ } else {
16322
+ blockRef.parent.splice(targetIndex, 1, paragraphNode);
16323
+ }
16324
+ await saveDocView(view, outputPath);
16325
+ await respond({
16326
+ ok: true,
16327
+ operation: "edit",
16328
+ path: outputPath ?? path,
16329
+ locator: at
16330
+ });
16321
16331
  return EXIT.OK;
16322
16332
  }
16323
- var HELP10 = `docx edit \u2014 replace a paragraph at a locator
16333
+ function applyTrackedEdit(view, existingParagraph, newParagraph) {
16334
+ const allocator = createRevisionAllocator(view);
16335
+ const baseMeta = { author: resolveAuthor(), date: resolveDate() };
16336
+ const mintMeta = () => ({
16337
+ ...baseMeta,
16338
+ revisionId: allocator.next()
16339
+ });
16340
+ const oldRuns = [];
16341
+ const oldNonRuns = [];
16342
+ for (const child of existingParagraph.children) {
16343
+ if (child.tag === "w:r")
16344
+ oldRuns.push(child);
16345
+ else
16346
+ oldNonRuns.push(child);
16347
+ }
16348
+ let newPPr = null;
16349
+ const newRuns = [];
16350
+ for (const child of newParagraph.children) {
16351
+ if (child.tag === "w:pPr")
16352
+ newPPr = child;
16353
+ else if (child.tag === "w:r")
16354
+ newRuns.push(child);
16355
+ }
16356
+ const rebuilt = [];
16357
+ if (newPPr) {
16358
+ rebuilt.push(newPPr);
16359
+ for (const child of oldNonRuns) {
16360
+ if (child.tag !== "w:pPr")
16361
+ rebuilt.push(child);
16362
+ }
16363
+ } else {
16364
+ rebuilt.push(...oldNonRuns);
16365
+ }
16366
+ if (oldRuns.length > 0) {
16367
+ const deletedRuns = oldRuns.map((run10) => convertTextToDelText(run10));
16368
+ rebuilt.push(/* @__PURE__ */ jsxDEV(Del, {
16369
+ meta: mintMeta(),
16370
+ children: deletedRuns
16371
+ }, undefined, false, undefined, this));
16372
+ }
16373
+ if (newRuns.length > 0) {
16374
+ rebuilt.push(/* @__PURE__ */ jsxDEV(Ins, {
16375
+ meta: mintMeta(),
16376
+ children: newRuns
16377
+ }, undefined, false, undefined, this));
16378
+ }
16379
+ existingParagraph.children = rebuilt;
16380
+ }
16381
+ var HELP9 = `docx edit \u2014 replace a paragraph at a locator
16324
16382
 
16325
16383
  Usage:
16326
16384
  docx edit FILE [options]
@@ -16341,6 +16399,7 @@ Run options (only with --text):
16341
16399
  --bold Bold
16342
16400
  --italic Italic
16343
16401
 
16402
+ -o, --output PATH Write to PATH instead of overwriting FILE
16344
16403
  --dry-run Print what would change; do not write the file
16345
16404
  -h, --help Show this help
16346
16405
 
@@ -16350,7 +16409,7 @@ Examples:
16350
16409
  `;
16351
16410
  var init_edit = __esm(() => {
16352
16411
  init_core();
16353
- init_emit();
16412
+ init_emit2();
16354
16413
  init_respond();
16355
16414
  init_jsx_dev_runtime();
16356
16415
  });
@@ -16409,14 +16468,18 @@ function regexMatcher(pattern, ignoreCase) {
16409
16468
  function collectMatches(blocks, matcher, out) {
16410
16469
  for (const block of blocks) {
16411
16470
  if (block.type === "paragraph") {
16412
- const paragraphText = block.runs.map((run11) => run11.type === "text" ? run11.text : "").join("");
16471
+ const paragraphText = block.runs.map((run10) => run10.type === "text" ? run10.text : "").join("");
16413
16472
  for (const span of matcher(paragraphText)) {
16414
- out.push({
16473
+ const match = {
16415
16474
  blockId: block.id,
16416
16475
  start: span.start,
16417
16476
  end: span.end,
16418
16477
  text: span.text
16419
- });
16478
+ };
16479
+ const overlaps = trackedChangesOverlapping(block, span.start, span.end);
16480
+ if (overlaps.length > 0)
16481
+ match.trackedChanges = overlaps;
16482
+ out.push(match);
16420
16483
  }
16421
16484
  continue;
16422
16485
  }
@@ -16429,17 +16492,41 @@ function collectMatches(blocks, matcher, out) {
16429
16492
  }
16430
16493
  }
16431
16494
  }
16495
+ function trackedChangesOverlapping(paragraph, start, end) {
16496
+ const seen = new Set;
16497
+ const out = [];
16498
+ let offset = 0;
16499
+ for (const run10 of paragraph.runs) {
16500
+ const length = run10.type === "text" ? run10.text.length : 0;
16501
+ const runStart = offset;
16502
+ const runEnd = offset + length;
16503
+ offset = runEnd;
16504
+ if (run10.type !== "text")
16505
+ continue;
16506
+ if (runEnd <= start || runStart >= end)
16507
+ continue;
16508
+ const change = run10.trackedChange;
16509
+ if (!change)
16510
+ continue;
16511
+ const key = `${change.kind}:${change.revisionId}:${change.author}`;
16512
+ if (seen.has(key))
16513
+ continue;
16514
+ seen.add(key);
16515
+ out.push(change);
16516
+ }
16517
+ return out;
16518
+ }
16432
16519
 
16433
16520
  // src/cli/find/index.ts
16434
16521
  var exports_find = {};
16435
16522
  __export(exports_find, {
16436
- run: () => run11
16523
+ run: () => run10
16437
16524
  });
16438
- import { parseArgs as parseArgs10 } from "util";
16439
- async function run11(args) {
16525
+ import { parseArgs as parseArgs9 } from "util";
16526
+ async function run10(args) {
16440
16527
  let parsed;
16441
16528
  try {
16442
- parsed = parseArgs10({
16529
+ parsed = parseArgs9({
16443
16530
  args,
16444
16531
  allowPositionals: true,
16445
16532
  options: {
@@ -16452,18 +16539,18 @@ async function run11(args) {
16452
16539
  });
16453
16540
  } catch (parseError) {
16454
16541
  const message = parseError instanceof Error ? parseError.message : String(parseError);
16455
- return fail("USAGE", message, HELP11);
16542
+ return fail("USAGE", message, HELP10);
16456
16543
  }
16457
16544
  if (parsed.values.help) {
16458
- await writeStdout(HELP11);
16545
+ await writeStdout(HELP10);
16459
16546
  return EXIT.OK;
16460
16547
  }
16461
16548
  const path = parsed.positionals[0];
16462
16549
  const query = parsed.positionals[1];
16463
16550
  if (!path)
16464
- return fail("USAGE", "Missing FILE argument", HELP11);
16551
+ return fail("USAGE", "Missing FILE argument", HELP10);
16465
16552
  if (query == null)
16466
- return fail("USAGE", "Missing QUERY argument", HELP11);
16553
+ return fail("USAGE", "Missing QUERY argument", HELP10);
16467
16554
  const ignoreCase = Boolean(parsed.values["ignore-case"]);
16468
16555
  const useRegex = Boolean(parsed.values.regex);
16469
16556
  const wantAll = Boolean(parsed.values.all);
@@ -16512,7 +16599,7 @@ async function run11(args) {
16512
16599
  });
16513
16600
  return EXIT.OK;
16514
16601
  }
16515
- var HELP11 = `docx find \u2014 locate text spans and return their locators
16602
+ var HELP10 = `docx find \u2014 locate text spans and return their locators
16516
16603
 
16517
16604
  Usage:
16518
16605
  docx find FILE QUERY [options]
@@ -16545,14 +16632,14 @@ var init_find = __esm(() => {
16545
16632
  // src/cli/images/extract.ts
16546
16633
  var exports_extract = {};
16547
16634
  __export(exports_extract, {
16548
- run: () => run12
16635
+ run: () => run11
16549
16636
  });
16550
- import { join as join2 } from "path";
16551
- import { parseArgs as parseArgs11 } from "util";
16552
- async function run12(args) {
16637
+ import { join } from "path";
16638
+ import { parseArgs as parseArgs10 } from "util";
16639
+ async function run11(args) {
16553
16640
  let parsed;
16554
16641
  try {
16555
- parsed = parseArgs11({
16642
+ parsed = parseArgs10({
16556
16643
  args,
16557
16644
  allowPositionals: true,
16558
16645
  options: {
@@ -16563,18 +16650,18 @@ async function run12(args) {
16563
16650
  });
16564
16651
  } catch (parseError) {
16565
16652
  const message = parseError instanceof Error ? parseError.message : String(parseError);
16566
- return fail("USAGE", message, HELP12);
16653
+ return fail("USAGE", message, HELP11);
16567
16654
  }
16568
16655
  if (parsed.values.help) {
16569
- await writeStdout(HELP12);
16656
+ await writeStdout(HELP11);
16570
16657
  return EXIT.OK;
16571
16658
  }
16572
16659
  const path = parsed.positionals[0];
16573
16660
  if (!path)
16574
- return fail("USAGE", "Missing FILE argument", HELP12);
16661
+ return fail("USAGE", "Missing FILE argument", HELP11);
16575
16662
  const outputDir = parsed.values.to;
16576
16663
  if (!outputDir)
16577
- return fail("USAGE", "Missing --to DIR", HELP12);
16664
+ return fail("USAGE", "Missing --to DIR", HELP11);
16578
16665
  const targetId = parsed.values.id;
16579
16666
  const view = await openOrFail(path);
16580
16667
  if (typeof view === "number")
@@ -16594,7 +16681,7 @@ async function run12(args) {
16594
16681
  continue;
16595
16682
  const extension = extensionFor(image.contentType, reference.partName);
16596
16683
  const fileName = `${image.hash}.${extension}`;
16597
- const outputPath = join2(outputDir, fileName);
16684
+ const outputPath = join(outputDir, fileName);
16598
16685
  if (!seenHashes.has(image.hash)) {
16599
16686
  const bytes = await view.pkg.readBytes(reference.partName);
16600
16687
  await Bun.write(outputPath, bytes);
@@ -16612,9 +16699,9 @@ async function run12(args) {
16612
16699
  function collectImages(blocks, out) {
16613
16700
  for (const block of blocks) {
16614
16701
  if (block.type === "paragraph") {
16615
- for (const run13 of block.runs) {
16616
- if (run13.type === "image")
16617
- out.push(run13);
16702
+ for (const run12 of block.runs) {
16703
+ if (run12.type === "image")
16704
+ out.push(run12);
16618
16705
  }
16619
16706
  continue;
16620
16707
  }
@@ -16634,7 +16721,7 @@ function extensionFor(contentType, partName) {
16634
16721
  const fromName = partName.split(".").pop()?.toLowerCase();
16635
16722
  return fromName ?? "bin";
16636
16723
  }
16637
- var HELP12 = `docx images extract \u2014 dump image bytes to a directory
16724
+ var HELP11 = `docx images extract \u2014 dump image bytes to a directory
16638
16725
 
16639
16726
  Usage:
16640
16727
  docx images extract FILE --to DIR [options]
@@ -16675,13 +16762,13 @@ var init_extract = __esm(() => {
16675
16762
  // src/cli/images/list.ts
16676
16763
  var exports_list2 = {};
16677
16764
  __export(exports_list2, {
16678
- run: () => run13
16765
+ run: () => run12
16679
16766
  });
16680
- import { parseArgs as parseArgs12 } from "util";
16681
- async function run13(args) {
16767
+ import { parseArgs as parseArgs11 } from "util";
16768
+ async function run12(args) {
16682
16769
  let parsed;
16683
16770
  try {
16684
- parsed = parseArgs12({
16771
+ parsed = parseArgs11({
16685
16772
  args,
16686
16773
  allowPositionals: true,
16687
16774
  options: {
@@ -16690,15 +16777,15 @@ async function run13(args) {
16690
16777
  });
16691
16778
  } catch (parseError) {
16692
16779
  const message = parseError instanceof Error ? parseError.message : String(parseError);
16693
- return fail("USAGE", message, HELP13);
16780
+ return fail("USAGE", message, HELP12);
16694
16781
  }
16695
16782
  if (parsed.values.help) {
16696
- await writeStdout(HELP13);
16783
+ await writeStdout(HELP12);
16697
16784
  return EXIT.OK;
16698
16785
  }
16699
16786
  const path = parsed.positionals[0];
16700
16787
  if (!path)
16701
- return fail("USAGE", "Missing FILE argument", HELP13);
16788
+ return fail("USAGE", "Missing FILE argument", HELP12);
16702
16789
  const view = await openOrFail(path);
16703
16790
  if (typeof view === "number")
16704
16791
  return view;
@@ -16711,9 +16798,9 @@ async function run13(args) {
16711
16798
  function collectImages2(blocks, out) {
16712
16799
  for (const block of blocks) {
16713
16800
  if (block.type === "paragraph") {
16714
- for (const run14 of block.runs) {
16715
- if (run14.type === "image")
16716
- out.push(run14);
16801
+ for (const run13 of block.runs) {
16802
+ if (run13.type === "image")
16803
+ out.push(run13);
16717
16804
  }
16718
16805
  continue;
16719
16806
  }
@@ -16726,7 +16813,7 @@ function collectImages2(blocks, out) {
16726
16813
  }
16727
16814
  }
16728
16815
  }
16729
- var HELP13 = `docx images list \u2014 print image manifest as JSON
16816
+ var HELP12 = `docx images list \u2014 print image manifest as JSON
16730
16817
 
16731
16818
  Usage:
16732
16819
  docx images list FILE [options]
@@ -16745,39 +16832,40 @@ var init_list2 = __esm(() => {
16745
16832
  // src/cli/images/replace.ts
16746
16833
  var exports_replace = {};
16747
16834
  __export(exports_replace, {
16748
- run: () => run14
16835
+ run: () => run13
16749
16836
  });
16750
- import { parseArgs as parseArgs13 } from "util";
16751
- async function run14(args) {
16837
+ import { parseArgs as parseArgs12 } from "util";
16838
+ async function run13(args) {
16752
16839
  let parsed;
16753
16840
  try {
16754
- parsed = parseArgs13({
16841
+ parsed = parseArgs12({
16755
16842
  args,
16756
16843
  allowPositionals: true,
16757
16844
  options: {
16758
16845
  at: { type: "string" },
16759
16846
  with: { type: "string" },
16847
+ output: { type: "string", short: "o" },
16760
16848
  "dry-run": { type: "boolean" },
16761
16849
  help: { type: "boolean", short: "h" }
16762
16850
  }
16763
16851
  });
16764
16852
  } catch (parseError) {
16765
16853
  const message = parseError instanceof Error ? parseError.message : String(parseError);
16766
- return fail("USAGE", message, HELP14);
16854
+ return fail("USAGE", message, HELP13);
16767
16855
  }
16768
16856
  if (parsed.values.help) {
16769
- await writeStdout(HELP14);
16857
+ await writeStdout(HELP13);
16770
16858
  return EXIT.OK;
16771
16859
  }
16772
16860
  const path = parsed.positionals[0];
16773
16861
  if (!path)
16774
- return fail("USAGE", "Missing FILE argument", HELP14);
16862
+ return fail("USAGE", "Missing FILE argument", HELP13);
16775
16863
  const targetId = parsed.values.at;
16776
16864
  if (!targetId)
16777
- return fail("USAGE", "Missing --at IMG_ID", HELP14);
16865
+ return fail("USAGE", "Missing --at IMG_ID", HELP13);
16778
16866
  const sourcePath = parsed.values.with;
16779
16867
  if (!sourcePath)
16780
- return fail("USAGE", "Missing --with PATH", HELP14);
16868
+ return fail("USAGE", "Missing --with PATH", HELP13);
16781
16869
  const sourceFile = Bun.file(sourcePath);
16782
16870
  if (!await sourceFile.exists()) {
16783
16871
  return fail("FILE_NOT_FOUND", `Replacement file not found: ${sourcePath}`);
@@ -16796,6 +16884,7 @@ async function run14(args) {
16796
16884
  }
16797
16885
  const originalPartName = reference.partName;
16798
16886
  const newPartName = renameExtension(originalPartName, newExtension);
16887
+ const outputPath = parsed.values.output;
16799
16888
  if (parsed.values["dry-run"]) {
16800
16889
  await respond({
16801
16890
  ok: true,
@@ -16804,7 +16893,8 @@ async function run14(args) {
16804
16893
  path,
16805
16894
  imageId: targetId,
16806
16895
  from: { partName: originalPartName, mimeType: reference.contentType },
16807
- to: { partName: newPartName, mimeType: newMimeType }
16896
+ to: { partName: newPartName, mimeType: newMimeType },
16897
+ ...outputPath ? { output: outputPath } : {}
16808
16898
  });
16809
16899
  return EXIT.OK;
16810
16900
  }
@@ -16819,11 +16909,11 @@ async function run14(args) {
16819
16909
  reference.partName = newPartName;
16820
16910
  reference.contentType = newMimeType;
16821
16911
  }
16822
- await saveDocView(view);
16912
+ await saveDocView(view, outputPath);
16823
16913
  await respond({
16824
16914
  ok: true,
16825
16915
  operation: "images.replace",
16826
- path,
16916
+ path: outputPath ?? path,
16827
16917
  imageId: targetId,
16828
16918
  partName: newPartName,
16829
16919
  mimeType: newMimeType,
@@ -16867,7 +16957,7 @@ function ensureContentTypeDefault(contentTypesTree, extension, mimeType) {
16867
16957
  }
16868
16958
  types.children.push(new XmlNode2("Default", { Extension: extension, ContentType: mimeType }));
16869
16959
  }
16870
- var HELP14 = `docx images replace \u2014 swap an image's bytes
16960
+ var HELP13 = `docx images replace \u2014 swap an image's bytes
16871
16961
 
16872
16962
  Usage:
16873
16963
  docx images replace FILE --at IMG_ID --with PATH [options]
@@ -16877,6 +16967,7 @@ Required:
16877
16967
  --with PATH New image file (any image MIME type)
16878
16968
 
16879
16969
  Optional:
16970
+ -o, --output PATH Write to PATH instead of overwriting FILE
16880
16971
  --dry-run Print what would change; do not write the file
16881
16972
  -h, --help Show this help
16882
16973
 
@@ -16909,12 +17000,12 @@ var init_replace = __esm(() => {
16909
17000
  // src/cli/images/index.ts
16910
17001
  var exports_images = {};
16911
17002
  __export(exports_images, {
16912
- run: () => run15
17003
+ run: () => run14
16913
17004
  });
16914
- async function run15(args) {
17005
+ async function run14(args) {
16915
17006
  const verb = args[0];
16916
17007
  if (!verb || verb === "--help" || verb === "-h" || verb === "help") {
16917
- await writeStdout(HELP15);
17008
+ await writeStdout(HELP14);
16918
17009
  return verb ? 0 : 2;
16919
17010
  }
16920
17011
  const loader = SUBCOMMANDS2[verb];
@@ -16924,7 +17015,7 @@ async function run15(args) {
16924
17015
  const module_ = await loader();
16925
17016
  return module_.run(args.slice(1));
16926
17017
  }
16927
- var SUBCOMMANDS2, HELP15 = `docx images \u2014 manage embedded images
17018
+ var SUBCOMMANDS2, HELP14 = `docx images \u2014 manage embedded images
16928
17019
 
16929
17020
  Usage:
16930
17021
  docx images <verb> FILE [options]
@@ -17057,13 +17148,13 @@ var init_types = () => {};
17057
17148
  // src/cli/info/schema.ts
17058
17149
  var exports_schema = {};
17059
17150
  __export(exports_schema, {
17060
- run: () => run16
17151
+ run: () => run15
17061
17152
  });
17062
- import { parseArgs as parseArgs14 } from "util";
17063
- async function run16(args) {
17153
+ import { parseArgs as parseArgs13 } from "util";
17154
+ async function run15(args) {
17064
17155
  let parsed;
17065
17156
  try {
17066
- parsed = parseArgs14({
17157
+ parsed = parseArgs13({
17067
17158
  args,
17068
17159
  allowPositionals: true,
17069
17160
  options: {
@@ -17074,10 +17165,10 @@ async function run16(args) {
17074
17165
  });
17075
17166
  } catch (parseError) {
17076
17167
  const message = parseError instanceof Error ? parseError.message : String(parseError);
17077
- return fail("USAGE", message, HELP16);
17168
+ return fail("USAGE", message, HELP15);
17078
17169
  }
17079
17170
  if (parsed.values.help) {
17080
- await writeStdout(HELP16);
17171
+ await writeStdout(HELP15);
17081
17172
  return EXIT.OK;
17082
17173
  }
17083
17174
  if (parsed.values.ts) {
@@ -17087,7 +17178,7 @@ async function run16(args) {
17087
17178
  await respond(JSON_SCHEMA);
17088
17179
  return EXIT.OK;
17089
17180
  }
17090
- var HELP16 = `docx schema \u2014 print the AST type definitions
17181
+ var HELP15 = `docx schema \u2014 print the AST type definitions
17091
17182
 
17092
17183
  Usage:
17093
17184
  docx schema [options]
@@ -17279,13 +17370,13 @@ var init_schema = __esm(() => {
17279
17370
  // src/cli/info/locators.ts
17280
17371
  var exports_locators = {};
17281
17372
  __export(exports_locators, {
17282
- run: () => run17
17373
+ run: () => run16
17283
17374
  });
17284
- import { parseArgs as parseArgs15 } from "util";
17285
- async function run17(args) {
17375
+ import { parseArgs as parseArgs14 } from "util";
17376
+ async function run16(args) {
17286
17377
  let parsed;
17287
17378
  try {
17288
- parsed = parseArgs15({
17379
+ parsed = parseArgs14({
17289
17380
  args,
17290
17381
  allowPositionals: true,
17291
17382
  options: {
@@ -17295,10 +17386,10 @@ async function run17(args) {
17295
17386
  });
17296
17387
  } catch (parseError) {
17297
17388
  const message = parseError instanceof Error ? parseError.message : String(parseError);
17298
- return fail("USAGE", message, HELP17);
17389
+ return fail("USAGE", message, HELP16);
17299
17390
  }
17300
17391
  if (parsed.values.help) {
17301
- await writeStdout(HELP17);
17392
+ await writeStdout(HELP16);
17302
17393
  return EXIT.OK;
17303
17394
  }
17304
17395
  if (parsed.values.json) {
@@ -17309,7 +17400,7 @@ async function run17(args) {
17309
17400
  await writeStdout(REFERENCE);
17310
17401
  return EXIT.OK;
17311
17402
  }
17312
- var HELP17 = `docx locators \u2014 print the locator grammar reference
17403
+ var HELP16 = `docx locators \u2014 print the locator grammar reference
17313
17404
 
17314
17405
  Usage:
17315
17406
  docx locators [options]
@@ -17389,12 +17480,12 @@ var init_locators2 = __esm(() => {
17389
17480
  // src/cli/info/index.ts
17390
17481
  var exports_info = {};
17391
17482
  __export(exports_info, {
17392
- run: () => run18
17483
+ run: () => run17
17393
17484
  });
17394
- async function run18(args) {
17485
+ async function run17(args) {
17395
17486
  const topic = args[0];
17396
17487
  if (!topic || topic === "--help" || topic === "-h" || topic === "help") {
17397
- await writeStdout(HELP18);
17488
+ await writeStdout(HELP17);
17398
17489
  return topic ? 0 : 2;
17399
17490
  }
17400
17491
  const loader = SUBCOMMANDS3[topic];
@@ -17404,7 +17495,7 @@ async function run18(args) {
17404
17495
  const module_ = await loader();
17405
17496
  return module_.run(args.slice(1));
17406
17497
  }
17407
- var SUBCOMMANDS3, HELP18 = `docx info \u2014 print reference material about the CLI
17498
+ var SUBCOMMANDS3, HELP17 = `docx info \u2014 print reference material about the CLI
17408
17499
 
17409
17500
  Usage:
17410
17501
  docx info <topic> [options]
@@ -17426,13 +17517,13 @@ var init_info = __esm(() => {
17426
17517
  // src/cli/insert/index.tsx
17427
17518
  var exports_insert = {};
17428
17519
  __export(exports_insert, {
17429
- run: () => run19
17520
+ run: () => run18
17430
17521
  });
17431
- import { parseArgs as parseArgs16 } from "util";
17432
- async function run19(args) {
17522
+ import { parseArgs as parseArgs15 } from "util";
17523
+ async function run18(args) {
17433
17524
  let parsed;
17434
17525
  try {
17435
- parsed = parseArgs16({
17526
+ parsed = parseArgs15({
17436
17527
  args,
17437
17528
  allowPositionals: true,
17438
17529
  options: {
@@ -17445,36 +17536,37 @@ async function run19(args) {
17445
17536
  color: { type: "string" },
17446
17537
  bold: { type: "boolean" },
17447
17538
  italic: { type: "boolean" },
17539
+ output: { type: "string", short: "o" },
17448
17540
  "dry-run": { type: "boolean" },
17449
17541
  help: { type: "boolean", short: "h" }
17450
17542
  }
17451
17543
  });
17452
17544
  } catch (parseError) {
17453
17545
  const message = parseError instanceof Error ? parseError.message : String(parseError);
17454
- return fail("USAGE", message, HELP19);
17546
+ return fail("USAGE", message, HELP18);
17455
17547
  }
17456
17548
  if (parsed.values.help) {
17457
- await writeStdout(HELP19);
17549
+ await writeStdout(HELP18);
17458
17550
  return EXIT.OK;
17459
17551
  }
17460
17552
  const path = parsed.positionals[0];
17461
17553
  if (!path)
17462
- return fail("USAGE", "Missing FILE argument", HELP19);
17554
+ return fail("USAGE", "Missing FILE argument", HELP18);
17463
17555
  const after = parsed.values.after;
17464
17556
  const before = parsed.values.before;
17465
17557
  if (!after && !before) {
17466
- return fail("USAGE", "Missing locator: pass --after or --before", HELP19);
17558
+ return fail("USAGE", "Missing locator: pass --after or --before", HELP18);
17467
17559
  }
17468
17560
  if (after && before) {
17469
- return fail("USAGE", "Pass either --after or --before, not both", HELP19);
17561
+ return fail("USAGE", "Pass either --after or --before, not both", HELP18);
17470
17562
  }
17471
17563
  const text = parsed.values.text;
17472
17564
  const runsJson = parsed.values.runs;
17473
17565
  if (!text && !runsJson) {
17474
- return fail("USAGE", "Missing content: pass --text or --runs", HELP19);
17566
+ return fail("USAGE", "Missing content: pass --text or --runs", HELP18);
17475
17567
  }
17476
17568
  if (text && runsJson) {
17477
- return fail("USAGE", "Pass either --text or --runs, not both", HELP19);
17569
+ return fail("USAGE", "Pass either --text or --runs, not both", HELP18);
17478
17570
  }
17479
17571
  const paragraphOptions = {};
17480
17572
  const styleValue = parsed.values.style;
@@ -17525,6 +17617,10 @@ async function run19(args) {
17525
17617
  return fail("BLOCK_NOT_FOUND", "Block reference is stale (parent does not contain it)");
17526
17618
  }
17527
17619
  const insertIndex = after !== undefined ? targetIndex + 1 : targetIndex;
17620
+ if (isTrackChangesEnabled(view)) {
17621
+ applyTrackedInsertion(paragraphNode, view);
17622
+ }
17623
+ const outputPath = parsed.values.output;
17528
17624
  if (parsed.values["dry-run"]) {
17529
17625
  await respond({
17530
17626
  ok: true,
@@ -17532,22 +17628,53 @@ async function run19(args) {
17532
17628
  dryRun: true,
17533
17629
  path,
17534
17630
  locator: targetLocator,
17535
- placement: after !== undefined ? "after" : "before"
17631
+ placement: after !== undefined ? "after" : "before",
17632
+ ...outputPath ? { output: outputPath } : {}
17536
17633
  });
17537
17634
  return EXIT.OK;
17538
17635
  }
17539
17636
  blockRef.parent.splice(insertIndex, 0, paragraphNode);
17540
- await saveDocView(view);
17637
+ await saveDocView(view, outputPath);
17541
17638
  await respond({
17542
17639
  ok: true,
17543
17640
  operation: "insert",
17544
- path,
17641
+ path: outputPath ?? path,
17545
17642
  locator: targetLocator,
17546
17643
  placement: after !== undefined ? "after" : "before"
17547
17644
  });
17548
17645
  return EXIT.OK;
17549
17646
  }
17550
- var HELP19 = `docx insert \u2014 insert a paragraph at a locator
17647
+ function applyTrackedInsertion(paragraph, view) {
17648
+ const allocator = createRevisionAllocator(view);
17649
+ const baseMeta = { author: resolveAuthor(), date: resolveDate() };
17650
+ const mintMeta = () => ({
17651
+ ...baseMeta,
17652
+ revisionId: allocator.next()
17653
+ });
17654
+ const newChildren = [];
17655
+ let runBuffer = [];
17656
+ const flush = () => {
17657
+ if (runBuffer.length === 0)
17658
+ return;
17659
+ newChildren.push(/* @__PURE__ */ jsxDEV(Ins, {
17660
+ meta: mintMeta(),
17661
+ children: runBuffer
17662
+ }, undefined, false, undefined, this));
17663
+ runBuffer = [];
17664
+ };
17665
+ for (const child of paragraph.children) {
17666
+ if (child.tag === "w:r") {
17667
+ runBuffer.push(child);
17668
+ continue;
17669
+ }
17670
+ flush();
17671
+ newChildren.push(child);
17672
+ }
17673
+ flush();
17674
+ paragraph.children = newChildren;
17675
+ markParagraphMarkAs(paragraph, "ins", mintMeta());
17676
+ }
17677
+ var HELP18 = `docx insert \u2014 insert a paragraph at a locator
17551
17678
 
17552
17679
  Usage:
17553
17680
  docx insert FILE [options]
@@ -17569,6 +17696,7 @@ Run options (only with --text):
17569
17696
  --bold Bold
17570
17697
  --italic Italic
17571
17698
 
17699
+ -o, --output PATH Write to PATH instead of overwriting FILE
17572
17700
  --dry-run Print what would be inserted; do not write the file
17573
17701
  -h, --help Show this help
17574
17702
 
@@ -17580,10 +17708,145 @@ Examples:
17580
17708
  var init_insert = __esm(() => {
17581
17709
  init_core();
17582
17710
  init_respond();
17583
- init_emit();
17711
+ init_emit2();
17584
17712
  init_jsx_dev_runtime();
17585
17713
  });
17586
17714
 
17715
+ // src/cli/outline/build.ts
17716
+ function buildOutline(doc, options = {}) {
17717
+ const stylePrefix = options.stylePrefix ?? "Heading";
17718
+ const root = {
17719
+ id: "",
17720
+ locator: "",
17721
+ level: 0,
17722
+ style: "",
17723
+ text: "",
17724
+ children: []
17725
+ };
17726
+ const stack = [root];
17727
+ for (const paragraph of headingParagraphs(doc.blocks, stylePrefix)) {
17728
+ const level = headingLevel(paragraph.style, stylePrefix);
17729
+ if (level === null)
17730
+ continue;
17731
+ while ((stack[stack.length - 1]?.level ?? 0) >= level)
17732
+ stack.pop();
17733
+ const parent = stack[stack.length - 1] ?? root;
17734
+ const entry = {
17735
+ id: paragraph.id,
17736
+ locator: paragraph.id,
17737
+ level,
17738
+ style: paragraph.style ?? "",
17739
+ text: paragraphText(paragraph),
17740
+ children: []
17741
+ };
17742
+ parent.children.push(entry);
17743
+ stack.push(entry);
17744
+ }
17745
+ return root.children;
17746
+ }
17747
+ function headingLevel(style, stylePrefix) {
17748
+ if (!style)
17749
+ return null;
17750
+ if (!style.startsWith(stylePrefix))
17751
+ return null;
17752
+ const remainder = style.slice(stylePrefix.length).trim();
17753
+ if (remainder === "")
17754
+ return 1;
17755
+ const parsed = Number(remainder);
17756
+ if (!Number.isInteger(parsed) || parsed < 1)
17757
+ return null;
17758
+ return parsed;
17759
+ }
17760
+ function* headingParagraphs(blocks, stylePrefix) {
17761
+ for (const block of blocks) {
17762
+ if (block.type !== "paragraph")
17763
+ continue;
17764
+ if (headingLevel(block.style, stylePrefix) === null)
17765
+ continue;
17766
+ yield block;
17767
+ }
17768
+ }
17769
+ function paragraphText(paragraph) {
17770
+ let out = "";
17771
+ for (const run19 of paragraph.runs) {
17772
+ if (run19.type === "text")
17773
+ out += run19.text;
17774
+ }
17775
+ return out;
17776
+ }
17777
+
17778
+ // src/cli/outline/index.ts
17779
+ var exports_outline = {};
17780
+ __export(exports_outline, {
17781
+ run: () => run19
17782
+ });
17783
+ import { parseArgs as parseArgs16 } from "util";
17784
+ async function run19(args) {
17785
+ let parsed;
17786
+ try {
17787
+ parsed = parseArgs16({
17788
+ args,
17789
+ allowPositionals: true,
17790
+ options: {
17791
+ "style-prefix": { type: "string" },
17792
+ help: { type: "boolean", short: "h" }
17793
+ }
17794
+ });
17795
+ } catch (parseError) {
17796
+ const message = parseError instanceof Error ? parseError.message : String(parseError);
17797
+ return fail("USAGE", message, HELP19);
17798
+ }
17799
+ if (parsed.values.help) {
17800
+ await writeStdout(HELP19);
17801
+ return EXIT.OK;
17802
+ }
17803
+ const path = parsed.positionals[0];
17804
+ if (!path)
17805
+ return fail("USAGE", "Missing FILE argument", HELP19);
17806
+ const stylePrefix = parsed.values["style-prefix"] ?? "Heading";
17807
+ if (stylePrefix.length === 0) {
17808
+ return fail("USAGE", "--style-prefix cannot be empty");
17809
+ }
17810
+ const view = await openOrFail(path);
17811
+ if (typeof view === "number")
17812
+ return view;
17813
+ const outline = buildOutline(view.doc, { stylePrefix });
17814
+ await respond({
17815
+ ok: true,
17816
+ operation: "outline",
17817
+ path,
17818
+ stylePrefix,
17819
+ outline
17820
+ });
17821
+ return EXIT.OK;
17822
+ }
17823
+ var HELP19 = `docx outline \u2014 list headings as a hierarchical tree
17824
+
17825
+ Usage:
17826
+ docx outline FILE [options]
17827
+
17828
+ Options:
17829
+ --style-prefix S paragraph-style prefix that marks a heading (default: "Heading")
17830
+ -h, --help show this help
17831
+
17832
+ Walks top-level paragraphs whose style starts with the prefix and parses the
17833
+ trailing number as the heading level (e.g. "Heading1" \u2192 1, "Heading 2" \u2192 2).
17834
+ Paragraphs nested inside table cells are skipped \u2014 outlines reflect the
17835
+ document's structural skeleton, not embedded labels. Lower levels nest under
17836
+ higher ones; missing intermediate levels nest directly (H1 \u2192 H3 is fine).
17837
+
17838
+ Output is JSON: an array of entries, each shaped like
17839
+ { id, locator, level, style, text, children }.
17840
+
17841
+ Examples:
17842
+ docx outline doc.docx
17843
+ docx outline doc.docx --style-prefix "Section"
17844
+ docx outline doc.docx | jq '.outline[].text'
17845
+ `;
17846
+ var init_outline = __esm(() => {
17847
+ init_respond();
17848
+ });
17849
+
17587
17850
  // src/cli/read/index.ts
17588
17851
  var exports_read = {};
17589
17852
  __export(exports_read, {
@@ -17637,7 +17900,7 @@ var init_read2 = __esm(() => {
17637
17900
  });
17638
17901
 
17639
17902
  // src/cli/replace/replace-span.tsx
17640
- function replaceSpanInParagraph(paragraph, span, replacement) {
17903
+ function replaceSpanInParagraph(paragraph, span, replacement, tracked) {
17641
17904
  if (span.start > span.end) {
17642
17905
  throw new Error(`replaceSpanInParagraph: invalid span ${span.start}-${span.end}`);
17643
17906
  }
@@ -17645,17 +17908,21 @@ function replaceSpanInParagraph(paragraph, span, replacement) {
17645
17908
  const overlapping = slots.filter((slot) => slot.offsetBefore + slot.length > span.start && slot.offsetBefore < span.end);
17646
17909
  const firstSlot = overlapping[0];
17647
17910
  if (!firstSlot) {
17648
- paragraph.children.push(replacementRun(null, replacement));
17911
+ paragraph.children.push(/* @__PURE__ */ jsxDEV(ReplacementRun, {
17912
+ runProperties: null,
17913
+ text: replacement
17914
+ }, undefined, false, undefined, this));
17649
17915
  return;
17650
17916
  }
17917
+ const inheritedProperties = firstSlot.run.findChild("w:rPr")?.clone() ?? null;
17651
17918
  const firstParent = firstSlot.parent;
17652
- if (overlapping.some((slot) => slot.parent !== firstParent)) {
17653
- throw new TrackedChangeBoundaryError("Span crosses a tracked-change (<w:ins>/<w:del>) boundary; accept or reject the tracked change first.");
17919
+ const allSameParent = overlapping.every((slot) => slot.parent === firstParent);
17920
+ if (allSameParent) {
17921
+ const containerStart = firstParent === paragraph ? 0 : firstSlot.offsetBefore;
17922
+ rebuildContainer(firstParent, containerStart, span, replacement, inheritedProperties, firstParent === paragraph, tracked ?? null);
17923
+ return;
17654
17924
  }
17655
- const firstRunProperties = firstSlot.run.findChild("w:rPr");
17656
- const inheritedProperties = firstRunProperties ? deepCloneNode(firstRunProperties) : null;
17657
- const containerStart = firstParent === paragraph ? 0 : firstSlot.offsetBefore;
17658
- rebuildContainer(firstParent, containerStart, span, replacement, inheritedProperties, firstParent === paragraph);
17925
+ rebuildAcrossBoundaries(paragraph, span, replacement, inheritedProperties, tracked ?? null);
17659
17926
  }
17660
17927
  function collectRunSlots(paragraph) {
17661
17928
  const slots = [];
@@ -17689,10 +17956,23 @@ function collectRunSlots(paragraph) {
17689
17956
  }
17690
17957
  return slots;
17691
17958
  }
17692
- function rebuildContainer(container, baseOffset, span, replacement, runProperties, isParagraph) {
17959
+ function rebuildContainer(container, baseOffset, span, replacement, runProperties, isParagraph, tracked) {
17693
17960
  const newChildren = [];
17694
17961
  let offset = baseOffset;
17695
- let placedReplacement = false;
17962
+ let placed = false;
17963
+ const placeReplacement = () => {
17964
+ if (placed)
17965
+ return;
17966
+ placed = true;
17967
+ const run21 = /* @__PURE__ */ jsxDEV(ReplacementRun, {
17968
+ runProperties,
17969
+ text: replacement
17970
+ }, undefined, false, undefined, this);
17971
+ newChildren.push(tracked && isParagraph ? /* @__PURE__ */ jsxDEV(Ins, {
17972
+ meta: mintMeta(tracked),
17973
+ children: run21
17974
+ }, undefined, false, undefined, this) : run21);
17975
+ };
17696
17976
  for (const child of container.children) {
17697
17977
  if (child.tag === "w:r") {
17698
17978
  const length = runTextLength(child);
@@ -17704,10 +17984,7 @@ function rebuildContainer(container, baseOffset, span, replacement, runPropertie
17704
17984
  continue;
17705
17985
  }
17706
17986
  if (runStart >= span.end) {
17707
- if (!placedReplacement) {
17708
- newChildren.push(replacementRun(runProperties, replacement));
17709
- placedReplacement = true;
17710
- }
17987
+ placeReplacement();
17711
17988
  newChildren.push(child);
17712
17989
  continue;
17713
17990
  }
@@ -17716,10 +17993,15 @@ function rebuildContainer(container, baseOffset, span, replacement, runPropertie
17716
17993
  if (sliceStartInRun > 0) {
17717
17994
  newChildren.push(sliceRun(child, 0, sliceStartInRun));
17718
17995
  }
17719
- if (!placedReplacement) {
17720
- newChildren.push(replacementRun(runProperties, replacement));
17721
- placedReplacement = true;
17996
+ if (tracked) {
17997
+ const cutRun = sliceRun(child, sliceStartInRun, sliceEndInRun);
17998
+ convertRunTextToDelText(cutRun);
17999
+ newChildren.push(/* @__PURE__ */ jsxDEV(Del, {
18000
+ meta: mintMeta(tracked),
18001
+ children: cutRun
18002
+ }, undefined, false, undefined, this));
17722
18003
  }
18004
+ placeReplacement();
17723
18005
  if (sliceEndInRun < length) {
17724
18006
  newChildren.push(sliceRun(child, sliceEndInRun, length));
17725
18007
  }
@@ -17737,23 +18019,159 @@ function rebuildContainer(container, baseOffset, span, replacement, runPropertie
17737
18019
  }
17738
18020
  newChildren.push(child);
17739
18021
  }
17740
- if (!placedReplacement) {
17741
- newChildren.push(replacementRun(runProperties, replacement));
17742
- }
18022
+ if (!placed)
18023
+ placeReplacement();
17743
18024
  container.children = newChildren;
17744
18025
  }
17745
- function replacementRun(runProperties, text) {
17746
- if (text.length === 0) {
17747
- return /* @__PURE__ */ jsxDEV(w.r, {
17748
- children: [
17749
- runProperties,
17750
- /* @__PURE__ */ jsxDEV(w.t, {
17751
- "xml:space": "preserve",
17752
- children: ""
17753
- }, undefined, false, undefined, this)
17754
- ]
17755
- }, undefined, true, undefined, this);
18026
+ function rebuildAcrossBoundaries(paragraph, span, replacement, runProperties, tracked) {
18027
+ const newChildren = [];
18028
+ let offset = 0;
18029
+ let placed = false;
18030
+ const placeReplacement = () => {
18031
+ if (placed)
18032
+ return;
18033
+ placed = true;
18034
+ const run21 = /* @__PURE__ */ jsxDEV(ReplacementRun, {
18035
+ runProperties,
18036
+ text: replacement
18037
+ }, undefined, false, undefined, this);
18038
+ newChildren.push(tracked ? /* @__PURE__ */ jsxDEV(Ins, {
18039
+ meta: mintMeta(tracked),
18040
+ children: run21
18041
+ }, undefined, false, undefined, this) : run21);
18042
+ };
18043
+ for (const child of paragraph.children) {
18044
+ if (child.tag === "w:r") {
18045
+ const length = runTextLength(child);
18046
+ const runStart = offset;
18047
+ const runEnd = offset + length;
18048
+ offset = runEnd;
18049
+ if (runEnd <= span.start) {
18050
+ newChildren.push(child);
18051
+ continue;
18052
+ }
18053
+ if (runStart >= span.end) {
18054
+ placeReplacement();
18055
+ newChildren.push(child);
18056
+ continue;
18057
+ }
18058
+ const sliceStartInRun = Math.max(0, span.start - runStart);
18059
+ const sliceEndInRun = Math.min(length, span.end - runStart);
18060
+ if (sliceStartInRun > 0) {
18061
+ newChildren.push(sliceRun(child, 0, sliceStartInRun));
18062
+ }
18063
+ if (tracked) {
18064
+ const cutRun = sliceRun(child, sliceStartInRun, sliceEndInRun);
18065
+ convertRunTextToDelText(cutRun);
18066
+ newChildren.push(/* @__PURE__ */ jsxDEV(Del, {
18067
+ meta: mintMeta(tracked),
18068
+ children: cutRun
18069
+ }, undefined, false, undefined, this));
18070
+ }
18071
+ placeReplacement();
18072
+ if (sliceEndInRun < length) {
18073
+ newChildren.push(sliceRun(child, sliceEndInRun, length));
18074
+ }
18075
+ continue;
18076
+ }
18077
+ if (child.tag === "w:ins" || child.tag === "w:del") {
18078
+ const innerLength = sumInnerRunLengths(child);
18079
+ const wrapperStart = offset;
18080
+ const wrapperEnd = offset + innerLength;
18081
+ offset = wrapperEnd;
18082
+ if (wrapperEnd <= span.start) {
18083
+ newChildren.push(child);
18084
+ continue;
18085
+ }
18086
+ if (wrapperStart >= span.end) {
18087
+ placeReplacement();
18088
+ newChildren.push(child);
18089
+ continue;
18090
+ }
18091
+ splitWrapperAcrossSpan(child, wrapperStart, span, tracked, newChildren, placeReplacement);
18092
+ continue;
18093
+ }
18094
+ newChildren.push(child);
18095
+ }
18096
+ if (!placed)
18097
+ placeReplacement();
18098
+ paragraph.children = newChildren;
18099
+ }
18100
+ function splitWrapperAcrossSpan(wrapper, wrapperStart, span, tracked, out, placeReplacement) {
18101
+ const isDel = wrapper.tag === "w:del";
18102
+ const preInner = [];
18103
+ const cutInner = [];
18104
+ const postInner = [];
18105
+ let innerOffset = wrapperStart;
18106
+ for (const inner of wrapper.children) {
18107
+ if (inner.tag !== "w:r") {
18108
+ preInner.push(inner);
18109
+ continue;
18110
+ }
18111
+ const length = runTextLength(inner);
18112
+ const runStart = innerOffset;
18113
+ const runEnd = innerOffset + length;
18114
+ innerOffset = runEnd;
18115
+ if (runEnd <= span.start) {
18116
+ preInner.push(inner);
18117
+ continue;
18118
+ }
18119
+ if (runStart >= span.end) {
18120
+ postInner.push(inner);
18121
+ continue;
18122
+ }
18123
+ const sliceStartInRun = Math.max(0, span.start - runStart);
18124
+ const sliceEndInRun = Math.min(length, span.end - runStart);
18125
+ if (sliceStartInRun > 0)
18126
+ preInner.push(sliceRun(inner, 0, sliceStartInRun));
18127
+ cutInner.push(sliceRun(inner, sliceStartInRun, sliceEndInRun));
18128
+ if (sliceEndInRun < length)
18129
+ postInner.push(sliceRun(inner, sliceEndInRun, length));
18130
+ }
18131
+ const preChildren = preInner.slice();
18132
+ if (isDel) {
18133
+ preChildren.push(...cutInner);
18134
+ } else if (tracked && cutInner.length > 0) {
18135
+ for (const cutRun of cutInner)
18136
+ convertRunTextToDelText(cutRun);
18137
+ preChildren.push(/* @__PURE__ */ jsxDEV(Del, {
18138
+ meta: mintMeta(tracked),
18139
+ children: cutInner
18140
+ }, undefined, false, undefined, this));
18141
+ }
18142
+ if (preChildren.length > 0) {
18143
+ const preWrapper = new XmlNode2(wrapper.tag, { ...wrapper.attributes });
18144
+ preWrapper.children = preChildren;
18145
+ out.push(preWrapper);
18146
+ }
18147
+ placeReplacement();
18148
+ if (postInner.length > 0) {
18149
+ const postWrapper = new XmlNode2(wrapper.tag, { ...wrapper.attributes });
18150
+ postWrapper.children = postInner;
18151
+ out.push(postWrapper);
18152
+ }
18153
+ }
18154
+ function sumInnerRunLengths(wrapper) {
18155
+ let total = 0;
18156
+ for (const inner of wrapper.children) {
18157
+ if (inner.tag === "w:r")
18158
+ total += runTextLength(inner);
18159
+ }
18160
+ return total;
18161
+ }
18162
+ function mintMeta(tracked) {
18163
+ return { ...tracked.meta, revisionId: tracked.allocator.next() };
18164
+ }
18165
+ function convertRunTextToDelText(run21) {
18166
+ for (const child of run21.children) {
18167
+ if (child.tag === "w:t")
18168
+ child.tag = "w:delText";
17756
18169
  }
18170
+ }
18171
+ function ReplacementRun({
18172
+ runProperties,
18173
+ text
18174
+ }) {
17757
18175
  return /* @__PURE__ */ jsxDEV(w.r, {
17758
18176
  children: [
17759
18177
  runProperties,
@@ -17764,17 +18182,11 @@ function replacementRun(runProperties, text) {
17764
18182
  ]
17765
18183
  }, undefined, true, undefined, this);
17766
18184
  }
17767
- var TrackedChangeBoundaryError;
17768
18185
  var init_replace_span = __esm(() => {
18186
+ init_core();
17769
18187
  init_jsx();
17770
18188
  init_parser();
17771
18189
  init_jsx_dev_runtime();
17772
- TrackedChangeBoundaryError = class TrackedChangeBoundaryError extends Error {
17773
- constructor(message) {
17774
- super(message);
17775
- this.name = "TrackedChangeBoundaryError";
17776
- }
17777
- };
17778
18190
  });
17779
18191
 
17780
18192
  // src/cli/replace/index.ts
@@ -17794,6 +18206,7 @@ async function run21(args) {
17794
18206
  "ignore-case": { type: "boolean" },
17795
18207
  all: { type: "boolean" },
17796
18208
  limit: { type: "string" },
18209
+ output: { type: "string", short: "o" },
17797
18210
  "dry-run": { type: "boolean" },
17798
18211
  help: { type: "boolean", short: "h" }
17799
18212
  }
@@ -17852,6 +18265,7 @@ async function run21(args) {
17852
18265
  end: match.end,
17853
18266
  text: match.text
17854
18267
  }));
18268
+ const outputPath = parsed.values.output;
17855
18269
  if (parsed.values["dry-run"]) {
17856
18270
  await respond({
17857
18271
  ok: true,
@@ -17864,7 +18278,8 @@ async function run21(args) {
17864
18278
  ignoreCase,
17865
18279
  totalMatches: allMatches.length,
17866
18280
  replaced: selected.length,
17867
- matches: matchesPayload
18281
+ matches: matchesPayload,
18282
+ ...outputPath ? { output: outputPath } : {}
17868
18283
  });
17869
18284
  return EXIT.OK;
17870
18285
  }
@@ -17889,24 +18304,21 @@ async function run21(args) {
17889
18304
  }
17890
18305
  return rightMatch.start - leftMatch.start;
17891
18306
  });
18307
+ const tracked = isTrackChangesEnabled(view) ? {
18308
+ meta: { author: resolveAuthor(), date: resolveDate() },
18309
+ allocator: createRevisionAllocator(view)
18310
+ } : undefined;
17892
18311
  const regexFlags = ignoreCase ? "i" : "";
17893
- try {
17894
- for (const match of reversed) {
17895
- const concreteReplacement = useRegex ? match.text.replace(new RegExp(pattern, regexFlags), replacement) : replacement;
17896
- const blockRef = resolveBlock(view, match.blockId);
17897
- replaceSpanInParagraph(blockRef.node, { start: match.start, end: match.end }, concreteReplacement);
17898
- }
17899
- } catch (error) {
17900
- if (error instanceof TrackedChangeBoundaryError) {
17901
- return fail("TRACKED_CHANGE_CONFLICT", error.message, "Use `docx track-changes off` (or accept/reject the change in Word) before replacing.");
17902
- }
17903
- throw error;
18312
+ for (const match of reversed) {
18313
+ const concreteReplacement = useRegex ? match.text.replace(new RegExp(pattern, regexFlags), replacement) : replacement;
18314
+ const blockRef = resolveBlock(view, match.blockId);
18315
+ replaceSpanInParagraph(blockRef.node, { start: match.start, end: match.end }, concreteReplacement, tracked);
17904
18316
  }
17905
- await saveDocView(view);
18317
+ await saveDocView(view, outputPath);
17906
18318
  await respond({
17907
18319
  ok: true,
17908
18320
  operation: "replace",
17909
- path,
18321
+ path: outputPath ?? path,
17910
18322
  pattern,
17911
18323
  replacement,
17912
18324
  regex: useRegex,
@@ -17927,6 +18339,7 @@ Options:
17927
18339
  --ignore-case case-insensitive match
17928
18340
  --all replace every match (default: just the first)
17929
18341
  --limit N replace at most N matches (in document order)
18342
+ -o, --output PATH write to PATH instead of overwriting FILE
17930
18343
  --dry-run report what would change without writing the file
17931
18344
  -h, --help show this help
17932
18345
 
@@ -17969,6 +18382,7 @@ async function run22(args) {
17969
18382
  args,
17970
18383
  allowPositionals: true,
17971
18384
  options: {
18385
+ output: { type: "string", short: "o" },
17972
18386
  "dry-run": { type: "boolean" },
17973
18387
  help: { type: "boolean", short: "h" }
17974
18388
  }
@@ -18002,6 +18416,7 @@ async function run22(args) {
18002
18416
  view.settingsTree.push(settingsRoot);
18003
18417
  }
18004
18418
  const hasTrackChanges = settingsRoot.children.some((child) => child.tag === "w:trackChanges");
18419
+ const outputPath = parsed.values.output;
18005
18420
  if (parsed.values["dry-run"]) {
18006
18421
  await respond({
18007
18422
  ok: true,
@@ -18009,7 +18424,8 @@ async function run22(args) {
18009
18424
  dryRun: true,
18010
18425
  path,
18011
18426
  mode,
18012
- previouslyOn: hasTrackChanges
18427
+ previouslyOn: hasTrackChanges,
18428
+ ...outputPath ? { output: outputPath } : {}
18013
18429
  });
18014
18430
  return EXIT.OK;
18015
18431
  }
@@ -18026,11 +18442,11 @@ async function run22(args) {
18026
18442
  target: "settings.xml"
18027
18443
  });
18028
18444
  }
18029
- await saveDocView(view);
18445
+ await saveDocView(view, outputPath);
18030
18446
  await respond({
18031
18447
  ok: true,
18032
18448
  operation: "track-changes",
18033
- path,
18449
+ path: outputPath ?? path,
18034
18450
  mode,
18035
18451
  previouslyOn: hasTrackChanges
18036
18452
  });
@@ -18041,10 +18457,13 @@ var HELP22 = `docx track-changes \u2014 toggle the document's tracked-changes mo
18041
18457
  Usage:
18042
18458
  docx track-changes FILE on|off [options]
18043
18459
 
18044
- Sets <w:trackChanges/> in word/settings.xml. When on, Word records new
18045
- edits as tracked changes. Existing <w:ins>/<w:del> markers are unaffected.
18460
+ Sets <w:trackChanges/> in word/settings.xml. When on, this CLI's
18461
+ insert/edit/delete/replace commands also emit <w:ins>/<w:del> markers
18462
+ (attributed to $DOCX_AUTHOR) so changes remain reviewable. Existing
18463
+ <w:ins>/<w:del> markers are unaffected by the toggle itself.
18046
18464
 
18047
18465
  Options:
18466
+ -o, --output PATH Write to PATH instead of overwriting FILE
18048
18467
  --dry-run Print what would change; do not write the file
18049
18468
  -h, --help Show this help
18050
18469
 
@@ -18052,7 +18471,7 @@ Examples:
18052
18471
  docx track-changes doc.docx on
18053
18472
  docx track-changes doc.docx off
18054
18473
  `, SETTINGS_PART = "word/settings.xml", SETTINGS_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings", SETTINGS_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml", NS_W2 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
18055
- var init_track_changes = __esm(() => {
18474
+ var init_track_changes2 = __esm(() => {
18056
18475
  init_core();
18057
18476
  init_jsx();
18058
18477
  init_package();
@@ -18060,10 +18479,297 @@ var init_track_changes = __esm(() => {
18060
18479
  init_respond();
18061
18480
  init_jsx_dev_runtime();
18062
18481
  });
18482
+
18483
+ // src/cli/wc/count.ts
18484
+ function countWords(text) {
18485
+ const matches = text.match(/\S+/g);
18486
+ return matches?.length ?? 0;
18487
+ }
18488
+ function paragraphText2(paragraph) {
18489
+ let out = "";
18490
+ for (const run23 of paragraph.runs) {
18491
+ if (run23.type === "text")
18492
+ out += run23.text;
18493
+ }
18494
+ return out;
18495
+ }
18496
+ function countWordsInBlocks(blocks) {
18497
+ let total = 0;
18498
+ for (const block of blocks) {
18499
+ if (block.type === "paragraph") {
18500
+ total += countWords(paragraphText2(block));
18501
+ } else if (block.type === "table") {
18502
+ for (const row of block.rows) {
18503
+ for (const cell of row.cells) {
18504
+ total += countWordsInBlocks(cell.blocks);
18505
+ }
18506
+ }
18507
+ }
18508
+ }
18509
+ return total;
18510
+ }
18511
+ function findBlockById(blocks, blockId) {
18512
+ for (const block of blocks) {
18513
+ if (block.id === blockId)
18514
+ return block;
18515
+ if (block.type === "table") {
18516
+ for (const row of block.rows) {
18517
+ for (const cell of row.cells) {
18518
+ const inner = findBlockById(cell.blocks, blockId);
18519
+ if (inner)
18520
+ return inner;
18521
+ }
18522
+ }
18523
+ }
18524
+ }
18525
+ return null;
18526
+ }
18527
+ function countWordsInParagraphSpan(paragraph, start, end) {
18528
+ const text = paragraphText2(paragraph);
18529
+ const clampedEnd = Math.min(Math.max(end, 0), text.length);
18530
+ const clampedStart = Math.min(Math.max(start, 0), clampedEnd);
18531
+ return countWords(text.slice(clampedStart, clampedEnd));
18532
+ }
18533
+ function countWordsInRange(paragraphsInOrder, startBlockId, startOffset, endBlockId, endOffset) {
18534
+ const startIndex = paragraphsInOrder.findIndex((p) => p.id === startBlockId);
18535
+ const endIndex = paragraphsInOrder.findIndex((p) => p.id === endBlockId);
18536
+ if (startIndex === -1 || endIndex === -1)
18537
+ return 0;
18538
+ if (endIndex < startIndex)
18539
+ return 0;
18540
+ if (startIndex === endIndex) {
18541
+ const paragraph = paragraphsInOrder[startIndex];
18542
+ if (!paragraph)
18543
+ return 0;
18544
+ return countWordsInParagraphSpan(paragraph, startOffset, endOffset);
18545
+ }
18546
+ let total = 0;
18547
+ const first = paragraphsInOrder[startIndex];
18548
+ if (first) {
18549
+ total += countWordsInParagraphSpan(first, startOffset, paragraphText2(first).length);
18550
+ }
18551
+ for (let index = startIndex + 1;index < endIndex; index++) {
18552
+ const middle = paragraphsInOrder[index];
18553
+ if (middle)
18554
+ total += countWords(paragraphText2(middle));
18555
+ }
18556
+ const last = paragraphsInOrder[endIndex];
18557
+ if (last)
18558
+ total += countWordsInParagraphSpan(last, 0, endOffset);
18559
+ return total;
18560
+ }
18561
+ function flattenParagraphs(blocks) {
18562
+ const out = [];
18563
+ for (const block of blocks) {
18564
+ if (block.type === "paragraph") {
18565
+ out.push(block);
18566
+ continue;
18567
+ }
18568
+ if (block.type === "table") {
18569
+ for (const row of block.rows) {
18570
+ for (const cell of row.cells) {
18571
+ out.push(...flattenParagraphs(cell.blocks));
18572
+ }
18573
+ }
18574
+ }
18575
+ }
18576
+ return out;
18577
+ }
18578
+
18579
+ // src/cli/wc/index.ts
18580
+ var exports_wc = {};
18581
+ __export(exports_wc, {
18582
+ run: () => run23
18583
+ });
18584
+ import { parseArgs as parseArgs20 } from "util";
18585
+ async function run23(args) {
18586
+ let parsed;
18587
+ try {
18588
+ parsed = parseArgs20({
18589
+ args,
18590
+ allowPositionals: true,
18591
+ options: {
18592
+ help: { type: "boolean", short: "h" }
18593
+ }
18594
+ });
18595
+ } catch (parseError) {
18596
+ const message = parseError instanceof Error ? parseError.message : String(parseError);
18597
+ return fail("USAGE", message, HELP23);
18598
+ }
18599
+ if (parsed.values.help) {
18600
+ await writeStdout(HELP23);
18601
+ return EXIT.OK;
18602
+ }
18603
+ const path = parsed.positionals[0];
18604
+ const locatorInput = parsed.positionals[1];
18605
+ if (!path)
18606
+ return fail("USAGE", "Missing FILE argument", HELP23);
18607
+ const view = await openOrFail(path);
18608
+ if (typeof view === "number")
18609
+ return view;
18610
+ if (!locatorInput) {
18611
+ await respond({
18612
+ ok: true,
18613
+ operation: "wc",
18614
+ path,
18615
+ scope: "document",
18616
+ words: countWordsInBlocks(view.doc.blocks)
18617
+ });
18618
+ return EXIT.OK;
18619
+ }
18620
+ let locator;
18621
+ try {
18622
+ locator = parseLocator(locatorInput);
18623
+ } catch (error) {
18624
+ if (error instanceof LocatorParseError) {
18625
+ return fail("INVALID_LOCATOR", error.message);
18626
+ }
18627
+ throw error;
18628
+ }
18629
+ if (locator.kind === "comment" || locator.kind === "image") {
18630
+ return fail("USAGE", `Locator ${locatorInput} addresses a ${locator.kind}, not text`, "wc accepts paragraph, span, range, table, and cell locators.");
18631
+ }
18632
+ const blocks = view.doc.blocks;
18633
+ if (locator.kind === "block") {
18634
+ const block = findBlockById(blocks, locator.blockId);
18635
+ if (!block) {
18636
+ return fail("BLOCK_NOT_FOUND", `Block not found: ${locator.blockId}`);
18637
+ }
18638
+ const words = block.type === "paragraph" ? countWords(paragraphText2(block)) : block.type === "table" ? countWordsInBlocks([block]) : 0;
18639
+ await respond({
18640
+ ok: true,
18641
+ operation: "wc",
18642
+ path,
18643
+ locator: locatorInput,
18644
+ scope: block.type,
18645
+ words
18646
+ });
18647
+ return EXIT.OK;
18648
+ }
18649
+ if (locator.kind === "blockSpan") {
18650
+ const block = findBlockById(blocks, locator.blockId);
18651
+ if (!block || block.type !== "paragraph") {
18652
+ return fail("BLOCK_NOT_FOUND", `Paragraph not found: ${locator.blockId}`);
18653
+ }
18654
+ await respond({
18655
+ ok: true,
18656
+ operation: "wc",
18657
+ path,
18658
+ locator: locatorInput,
18659
+ scope: "paragraphSpan",
18660
+ words: countWordsInParagraphSpan(block, locator.start, locator.end)
18661
+ });
18662
+ return EXIT.OK;
18663
+ }
18664
+ if (locator.kind === "range") {
18665
+ const paragraphs = flattenParagraphs(blocks);
18666
+ const startExists = paragraphs.some((p) => p.id === locator.start.blockId);
18667
+ const endExists = paragraphs.some((p) => p.id === locator.end.blockId);
18668
+ if (!startExists) {
18669
+ return fail("BLOCK_NOT_FOUND", `Paragraph not found: ${locator.start.blockId}`);
18670
+ }
18671
+ if (!endExists) {
18672
+ return fail("BLOCK_NOT_FOUND", `Paragraph not found: ${locator.end.blockId}`);
18673
+ }
18674
+ await respond({
18675
+ ok: true,
18676
+ operation: "wc",
18677
+ path,
18678
+ locator: locatorInput,
18679
+ scope: "range",
18680
+ words: countWordsInRange(paragraphs, locator.start.blockId, locator.start.offset, locator.end.blockId, locator.end.offset)
18681
+ });
18682
+ return EXIT.OK;
18683
+ }
18684
+ if (locator.kind === "cell") {
18685
+ const cellPath = `${locator.tableId}:r${locator.row}c${locator.col}`;
18686
+ if (!locator.inner) {
18687
+ const cellBlocks = findCellBlocks(blocks, locator);
18688
+ if (!cellBlocks) {
18689
+ return fail("BLOCK_NOT_FOUND", `Cell not found: ${cellPath}`);
18690
+ }
18691
+ await respond({
18692
+ ok: true,
18693
+ operation: "wc",
18694
+ path,
18695
+ locator: locatorInput,
18696
+ scope: "cell",
18697
+ words: countWordsInBlocks(cellBlocks)
18698
+ });
18699
+ return EXIT.OK;
18700
+ }
18701
+ const innerBlockId = locator.inner.kind === "block" ? locator.inner.blockId : locator.inner.kind === "blockSpan" ? locator.inner.blockId : null;
18702
+ if (!innerBlockId) {
18703
+ return fail("USAGE", `Unsupported inner locator for cell: ${locatorInput}`, "Inside a cell, wc accepts pK or pK:S-E only.");
18704
+ }
18705
+ const composedId = `${cellPath}:${innerBlockId}`;
18706
+ const block = findBlockById(blocks, composedId);
18707
+ if (!block || block.type !== "paragraph") {
18708
+ return fail("BLOCK_NOT_FOUND", `Paragraph not found: ${composedId}`);
18709
+ }
18710
+ const words = locator.inner.kind === "blockSpan" ? countWordsInParagraphSpan(block, locator.inner.start, locator.inner.end) : countWords(paragraphText2(block));
18711
+ await respond({
18712
+ ok: true,
18713
+ operation: "wc",
18714
+ path,
18715
+ locator: locatorInput,
18716
+ scope: locator.inner.kind === "blockSpan" ? "paragraphSpan" : "paragraph",
18717
+ words
18718
+ });
18719
+ return EXIT.OK;
18720
+ }
18721
+ return fail("USAGE", `Unsupported locator: ${locatorInput}`);
18722
+ }
18723
+ function findCellBlocks(blocks, locator) {
18724
+ const block = findBlockById(blocks, locator.tableId);
18725
+ if (!block || block.type !== "table")
18726
+ return null;
18727
+ const row = block.rows[locator.row];
18728
+ if (!row)
18729
+ return null;
18730
+ const cell = row.cells[locator.col];
18731
+ if (!cell)
18732
+ return null;
18733
+ return cell.blocks;
18734
+ }
18735
+ var HELP23 = `docx wc \u2014 count words in a document or a locator-addressed slice
18736
+
18737
+ Usage:
18738
+ docx wc FILE [LOCATOR] [options]
18739
+
18740
+ Locators (optional; default: whole document):
18741
+ pN whole paragraph N
18742
+ pN:S-E chars S..E within paragraph N
18743
+ pN:S-pM:E cross-paragraph range
18744
+ tN whole table
18745
+ tN:rRcC whole cell
18746
+ tN:rRcC:pK paragraph K inside that cell
18747
+ tN:rRcC:pK:S-E span within a cell paragraph
18748
+
18749
+ Options:
18750
+ -h, --help show this help
18751
+
18752
+ Counting is whitespace-segmented (\\S+) over the joined paragraph text. Hidden
18753
+ content like images/breaks/tabs contributes no words. Tracked-change deletions
18754
+ are counted alongside insertions because their characters are still part of the
18755
+ on-disk text; accept or reject changes first if you need a delta on the
18756
+ "accepted" view.
18757
+
18758
+ Examples:
18759
+ docx wc doc.docx
18760
+ docx wc doc.docx p3
18761
+ docx wc doc.docx p3:0-120
18762
+ docx wc doc.docx p5:10-p9:42
18763
+ docx wc doc.docx t0:r1c0
18764
+ `;
18765
+ var init_wc = __esm(() => {
18766
+ init_core();
18767
+ init_respond();
18768
+ });
18063
18769
  // package.json
18064
18770
  var package_default = {
18065
18771
  name: "bun-docx",
18066
- version: "0.2.0",
18772
+ version: "0.4.0",
18067
18773
  description: "CLI for AI agents (Claude, Codex) to read, edit, and comment on .docx files with full format fidelity.",
18068
18774
  keywords: [
18069
18775
  "docx",
@@ -18138,7 +18844,9 @@ Commands:
18138
18844
  delete FILE Remove a block or run range
18139
18845
  find FILE QUERY Find text spans, return locators
18140
18846
  replace FILE PATTERN REPL Substitute text spans (sed for docx)
18141
- comments \u2026 Add, reply, resolve, delete, restore, list comments
18847
+ wc FILE [LOCATOR] Count words in the doc or a slice
18848
+ outline FILE List headings as a hierarchical tree
18849
+ comments \u2026 Add, reply, resolve, delete, list comments
18142
18850
  images \u2026 Extract, replace, list images
18143
18851
  track-changes FILE on|off Toggle tracked-changes mode
18144
18852
  info \u2026 Reference material (schema, locators)
@@ -18146,7 +18854,13 @@ Commands:
18146
18854
  Run "docx <command> --help" for command-specific help.
18147
18855
 
18148
18856
  Environment:
18149
- DOCX_AUTHOR Default --author for "comments add" / "comments reply"
18857
+ DOCX_AUTHOR Default author for comments and tracked-change attribution
18858
+ DOCX_CLI_NOW Override the timestamp used for tracked changes (test only)
18859
+
18860
+ Tracked changes:
18861
+ When <w:trackChanges/> is set in the doc (toggle via "docx track-changes
18862
+ FILE on"), insert/edit/delete/replace automatically emit <w:ins>/<w:del>
18863
+ markers attributed to $DOCX_AUTHOR.
18150
18864
  `;
18151
18865
  async function printTopHelp() {
18152
18866
  await writeStdout(TOP_HELP);
@@ -18163,9 +18877,11 @@ var COMMANDS = {
18163
18877
  images: () => Promise.resolve().then(() => (init_images(), exports_images)),
18164
18878
  info: () => Promise.resolve().then(() => (init_info(), exports_info)),
18165
18879
  insert: () => Promise.resolve().then(() => (init_insert(), exports_insert)),
18880
+ outline: () => Promise.resolve().then(() => (init_outline(), exports_outline)),
18166
18881
  read: () => Promise.resolve().then(() => (init_read2(), exports_read)),
18167
18882
  replace: () => Promise.resolve().then(() => (init_replace2(), exports_replace2)),
18168
- "track-changes": () => Promise.resolve().then(() => (init_track_changes(), exports_track_changes))
18883
+ "track-changes": () => Promise.resolve().then(() => (init_track_changes2(), exports_track_changes)),
18884
+ wc: () => Promise.resolve().then(() => (init_wc(), exports_wc))
18169
18885
  };
18170
18886
  async function main(argv) {
18171
18887
  const args = argv.slice(2);