bun-docx 0.2.0 → 0.3.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 +863 -573
  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,20 +17708,20 @@ 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
 
17587
17715
  // src/cli/read/index.ts
17588
17716
  var exports_read = {};
17589
17717
  __export(exports_read, {
17590
- run: () => run20
17718
+ run: () => run19
17591
17719
  });
17592
- import { parseArgs as parseArgs17 } from "util";
17593
- async function run20(args) {
17720
+ import { parseArgs as parseArgs16 } from "util";
17721
+ async function run19(args) {
17594
17722
  let parsed;
17595
17723
  try {
17596
- parsed = parseArgs17({
17724
+ parsed = parseArgs16({
17597
17725
  args,
17598
17726
  allowPositionals: true,
17599
17727
  options: {
@@ -17602,15 +17730,15 @@ async function run20(args) {
17602
17730
  }
17603
17731
  });
17604
17732
  } catch (e) {
17605
- return fail("USAGE", e instanceof Error ? e.message : String(e), HELP20);
17733
+ return fail("USAGE", e instanceof Error ? e.message : String(e), HELP19);
17606
17734
  }
17607
17735
  if (parsed.values.help) {
17608
- await writeStdout(HELP20);
17736
+ await writeStdout(HELP19);
17609
17737
  return EXIT.OK;
17610
17738
  }
17611
17739
  const path = parsed.positionals[0];
17612
17740
  if (!path)
17613
- return fail("USAGE", "Missing FILE argument", HELP20);
17741
+ return fail("USAGE", "Missing FILE argument", HELP19);
17614
17742
  const view = await openOrFail(path);
17615
17743
  if (typeof view === "number")
17616
17744
  return view;
@@ -17618,7 +17746,7 @@ async function run20(args) {
17618
17746
  await respond(view.doc);
17619
17747
  return EXIT.OK;
17620
17748
  }
17621
- var HELP20 = `docx read \u2014 print AST as JSON
17749
+ var HELP19 = `docx read \u2014 print AST as JSON
17622
17750
 
17623
17751
  Usage:
17624
17752
  docx read FILE [options]
@@ -17637,7 +17765,7 @@ var init_read2 = __esm(() => {
17637
17765
  });
17638
17766
 
17639
17767
  // src/cli/replace/replace-span.tsx
17640
- function replaceSpanInParagraph(paragraph, span, replacement) {
17768
+ function replaceSpanInParagraph(paragraph, span, replacement, tracked) {
17641
17769
  if (span.start > span.end) {
17642
17770
  throw new Error(`replaceSpanInParagraph: invalid span ${span.start}-${span.end}`);
17643
17771
  }
@@ -17645,17 +17773,21 @@ function replaceSpanInParagraph(paragraph, span, replacement) {
17645
17773
  const overlapping = slots.filter((slot) => slot.offsetBefore + slot.length > span.start && slot.offsetBefore < span.end);
17646
17774
  const firstSlot = overlapping[0];
17647
17775
  if (!firstSlot) {
17648
- paragraph.children.push(replacementRun(null, replacement));
17776
+ paragraph.children.push(/* @__PURE__ */ jsxDEV(ReplacementRun, {
17777
+ runProperties: null,
17778
+ text: replacement
17779
+ }, undefined, false, undefined, this));
17649
17780
  return;
17650
17781
  }
17782
+ const inheritedProperties = firstSlot.run.findChild("w:rPr")?.clone() ?? null;
17651
17783
  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.");
17784
+ const allSameParent = overlapping.every((slot) => slot.parent === firstParent);
17785
+ if (allSameParent) {
17786
+ const containerStart = firstParent === paragraph ? 0 : firstSlot.offsetBefore;
17787
+ rebuildContainer(firstParent, containerStart, span, replacement, inheritedProperties, firstParent === paragraph, tracked ?? null);
17788
+ return;
17654
17789
  }
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);
17790
+ rebuildAcrossBoundaries(paragraph, span, replacement, inheritedProperties, tracked ?? null);
17659
17791
  }
17660
17792
  function collectRunSlots(paragraph) {
17661
17793
  const slots = [];
@@ -17689,10 +17821,23 @@ function collectRunSlots(paragraph) {
17689
17821
  }
17690
17822
  return slots;
17691
17823
  }
17692
- function rebuildContainer(container, baseOffset, span, replacement, runProperties, isParagraph) {
17824
+ function rebuildContainer(container, baseOffset, span, replacement, runProperties, isParagraph, tracked) {
17693
17825
  const newChildren = [];
17694
17826
  let offset = baseOffset;
17695
- let placedReplacement = false;
17827
+ let placed = false;
17828
+ const placeReplacement = () => {
17829
+ if (placed)
17830
+ return;
17831
+ placed = true;
17832
+ const run20 = /* @__PURE__ */ jsxDEV(ReplacementRun, {
17833
+ runProperties,
17834
+ text: replacement
17835
+ }, undefined, false, undefined, this);
17836
+ newChildren.push(tracked && isParagraph ? /* @__PURE__ */ jsxDEV(Ins, {
17837
+ meta: mintMeta(tracked),
17838
+ children: run20
17839
+ }, undefined, false, undefined, this) : run20);
17840
+ };
17696
17841
  for (const child of container.children) {
17697
17842
  if (child.tag === "w:r") {
17698
17843
  const length = runTextLength(child);
@@ -17704,10 +17849,7 @@ function rebuildContainer(container, baseOffset, span, replacement, runPropertie
17704
17849
  continue;
17705
17850
  }
17706
17851
  if (runStart >= span.end) {
17707
- if (!placedReplacement) {
17708
- newChildren.push(replacementRun(runProperties, replacement));
17709
- placedReplacement = true;
17710
- }
17852
+ placeReplacement();
17711
17853
  newChildren.push(child);
17712
17854
  continue;
17713
17855
  }
@@ -17716,10 +17858,15 @@ function rebuildContainer(container, baseOffset, span, replacement, runPropertie
17716
17858
  if (sliceStartInRun > 0) {
17717
17859
  newChildren.push(sliceRun(child, 0, sliceStartInRun));
17718
17860
  }
17719
- if (!placedReplacement) {
17720
- newChildren.push(replacementRun(runProperties, replacement));
17721
- placedReplacement = true;
17861
+ if (tracked) {
17862
+ const cutRun = sliceRun(child, sliceStartInRun, sliceEndInRun);
17863
+ convertRunTextToDelText(cutRun);
17864
+ newChildren.push(/* @__PURE__ */ jsxDEV(Del, {
17865
+ meta: mintMeta(tracked),
17866
+ children: cutRun
17867
+ }, undefined, false, undefined, this));
17722
17868
  }
17869
+ placeReplacement();
17723
17870
  if (sliceEndInRun < length) {
17724
17871
  newChildren.push(sliceRun(child, sliceEndInRun, length));
17725
17872
  }
@@ -17737,23 +17884,159 @@ function rebuildContainer(container, baseOffset, span, replacement, runPropertie
17737
17884
  }
17738
17885
  newChildren.push(child);
17739
17886
  }
17740
- if (!placedReplacement) {
17741
- newChildren.push(replacementRun(runProperties, replacement));
17742
- }
17887
+ if (!placed)
17888
+ placeReplacement();
17743
17889
  container.children = newChildren;
17744
17890
  }
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);
17891
+ function rebuildAcrossBoundaries(paragraph, span, replacement, runProperties, tracked) {
17892
+ const newChildren = [];
17893
+ let offset = 0;
17894
+ let placed = false;
17895
+ const placeReplacement = () => {
17896
+ if (placed)
17897
+ return;
17898
+ placed = true;
17899
+ const run20 = /* @__PURE__ */ jsxDEV(ReplacementRun, {
17900
+ runProperties,
17901
+ text: replacement
17902
+ }, undefined, false, undefined, this);
17903
+ newChildren.push(tracked ? /* @__PURE__ */ jsxDEV(Ins, {
17904
+ meta: mintMeta(tracked),
17905
+ children: run20
17906
+ }, undefined, false, undefined, this) : run20);
17907
+ };
17908
+ for (const child of paragraph.children) {
17909
+ if (child.tag === "w:r") {
17910
+ const length = runTextLength(child);
17911
+ const runStart = offset;
17912
+ const runEnd = offset + length;
17913
+ offset = runEnd;
17914
+ if (runEnd <= span.start) {
17915
+ newChildren.push(child);
17916
+ continue;
17917
+ }
17918
+ if (runStart >= span.end) {
17919
+ placeReplacement();
17920
+ newChildren.push(child);
17921
+ continue;
17922
+ }
17923
+ const sliceStartInRun = Math.max(0, span.start - runStart);
17924
+ const sliceEndInRun = Math.min(length, span.end - runStart);
17925
+ if (sliceStartInRun > 0) {
17926
+ newChildren.push(sliceRun(child, 0, sliceStartInRun));
17927
+ }
17928
+ if (tracked) {
17929
+ const cutRun = sliceRun(child, sliceStartInRun, sliceEndInRun);
17930
+ convertRunTextToDelText(cutRun);
17931
+ newChildren.push(/* @__PURE__ */ jsxDEV(Del, {
17932
+ meta: mintMeta(tracked),
17933
+ children: cutRun
17934
+ }, undefined, false, undefined, this));
17935
+ }
17936
+ placeReplacement();
17937
+ if (sliceEndInRun < length) {
17938
+ newChildren.push(sliceRun(child, sliceEndInRun, length));
17939
+ }
17940
+ continue;
17941
+ }
17942
+ if (child.tag === "w:ins" || child.tag === "w:del") {
17943
+ const innerLength = sumInnerRunLengths(child);
17944
+ const wrapperStart = offset;
17945
+ const wrapperEnd = offset + innerLength;
17946
+ offset = wrapperEnd;
17947
+ if (wrapperEnd <= span.start) {
17948
+ newChildren.push(child);
17949
+ continue;
17950
+ }
17951
+ if (wrapperStart >= span.end) {
17952
+ placeReplacement();
17953
+ newChildren.push(child);
17954
+ continue;
17955
+ }
17956
+ splitWrapperAcrossSpan(child, wrapperStart, span, tracked, newChildren, placeReplacement);
17957
+ continue;
17958
+ }
17959
+ newChildren.push(child);
17756
17960
  }
17961
+ if (!placed)
17962
+ placeReplacement();
17963
+ paragraph.children = newChildren;
17964
+ }
17965
+ function splitWrapperAcrossSpan(wrapper, wrapperStart, span, tracked, out, placeReplacement) {
17966
+ const isDel = wrapper.tag === "w:del";
17967
+ const preInner = [];
17968
+ const cutInner = [];
17969
+ const postInner = [];
17970
+ let innerOffset = wrapperStart;
17971
+ for (const inner of wrapper.children) {
17972
+ if (inner.tag !== "w:r") {
17973
+ preInner.push(inner);
17974
+ continue;
17975
+ }
17976
+ const length = runTextLength(inner);
17977
+ const runStart = innerOffset;
17978
+ const runEnd = innerOffset + length;
17979
+ innerOffset = runEnd;
17980
+ if (runEnd <= span.start) {
17981
+ preInner.push(inner);
17982
+ continue;
17983
+ }
17984
+ if (runStart >= span.end) {
17985
+ postInner.push(inner);
17986
+ continue;
17987
+ }
17988
+ const sliceStartInRun = Math.max(0, span.start - runStart);
17989
+ const sliceEndInRun = Math.min(length, span.end - runStart);
17990
+ if (sliceStartInRun > 0)
17991
+ preInner.push(sliceRun(inner, 0, sliceStartInRun));
17992
+ cutInner.push(sliceRun(inner, sliceStartInRun, sliceEndInRun));
17993
+ if (sliceEndInRun < length)
17994
+ postInner.push(sliceRun(inner, sliceEndInRun, length));
17995
+ }
17996
+ const preChildren = preInner.slice();
17997
+ if (isDel) {
17998
+ preChildren.push(...cutInner);
17999
+ } else if (tracked && cutInner.length > 0) {
18000
+ for (const cutRun of cutInner)
18001
+ convertRunTextToDelText(cutRun);
18002
+ preChildren.push(/* @__PURE__ */ jsxDEV(Del, {
18003
+ meta: mintMeta(tracked),
18004
+ children: cutInner
18005
+ }, undefined, false, undefined, this));
18006
+ }
18007
+ if (preChildren.length > 0) {
18008
+ const preWrapper = new XmlNode2(wrapper.tag, { ...wrapper.attributes });
18009
+ preWrapper.children = preChildren;
18010
+ out.push(preWrapper);
18011
+ }
18012
+ placeReplacement();
18013
+ if (postInner.length > 0) {
18014
+ const postWrapper = new XmlNode2(wrapper.tag, { ...wrapper.attributes });
18015
+ postWrapper.children = postInner;
18016
+ out.push(postWrapper);
18017
+ }
18018
+ }
18019
+ function sumInnerRunLengths(wrapper) {
18020
+ let total = 0;
18021
+ for (const inner of wrapper.children) {
18022
+ if (inner.tag === "w:r")
18023
+ total += runTextLength(inner);
18024
+ }
18025
+ return total;
18026
+ }
18027
+ function mintMeta(tracked) {
18028
+ return { ...tracked.meta, revisionId: tracked.allocator.next() };
18029
+ }
18030
+ function convertRunTextToDelText(run20) {
18031
+ for (const child of run20.children) {
18032
+ if (child.tag === "w:t")
18033
+ child.tag = "w:delText";
18034
+ }
18035
+ }
18036
+ function ReplacementRun({
18037
+ runProperties,
18038
+ text
18039
+ }) {
17757
18040
  return /* @__PURE__ */ jsxDEV(w.r, {
17758
18041
  children: [
17759
18042
  runProperties,
@@ -17764,29 +18047,23 @@ function replacementRun(runProperties, text) {
17764
18047
  ]
17765
18048
  }, undefined, true, undefined, this);
17766
18049
  }
17767
- var TrackedChangeBoundaryError;
17768
18050
  var init_replace_span = __esm(() => {
18051
+ init_core();
17769
18052
  init_jsx();
17770
18053
  init_parser();
17771
18054
  init_jsx_dev_runtime();
17772
- TrackedChangeBoundaryError = class TrackedChangeBoundaryError extends Error {
17773
- constructor(message) {
17774
- super(message);
17775
- this.name = "TrackedChangeBoundaryError";
17776
- }
17777
- };
17778
18055
  });
17779
18056
 
17780
18057
  // src/cli/replace/index.ts
17781
18058
  var exports_replace2 = {};
17782
18059
  __export(exports_replace2, {
17783
- run: () => run21
18060
+ run: () => run20
17784
18061
  });
17785
- import { parseArgs as parseArgs18 } from "util";
17786
- async function run21(args) {
18062
+ import { parseArgs as parseArgs17 } from "util";
18063
+ async function run20(args) {
17787
18064
  let parsed;
17788
18065
  try {
17789
- parsed = parseArgs18({
18066
+ parsed = parseArgs17({
17790
18067
  args,
17791
18068
  allowPositionals: true,
17792
18069
  options: {
@@ -17794,27 +18071,28 @@ async function run21(args) {
17794
18071
  "ignore-case": { type: "boolean" },
17795
18072
  all: { type: "boolean" },
17796
18073
  limit: { type: "string" },
18074
+ output: { type: "string", short: "o" },
17797
18075
  "dry-run": { type: "boolean" },
17798
18076
  help: { type: "boolean", short: "h" }
17799
18077
  }
17800
18078
  });
17801
18079
  } catch (parseError) {
17802
18080
  const message = parseError instanceof Error ? parseError.message : String(parseError);
17803
- return fail("USAGE", message, HELP21);
18081
+ return fail("USAGE", message, HELP20);
17804
18082
  }
17805
18083
  if (parsed.values.help) {
17806
- await writeStdout(HELP21);
18084
+ await writeStdout(HELP20);
17807
18085
  return EXIT.OK;
17808
18086
  }
17809
18087
  const path = parsed.positionals[0];
17810
18088
  const pattern = parsed.positionals[1];
17811
18089
  const replacement = parsed.positionals[2];
17812
18090
  if (!path)
17813
- return fail("USAGE", "Missing FILE argument", HELP21);
18091
+ return fail("USAGE", "Missing FILE argument", HELP20);
17814
18092
  if (pattern == null)
17815
- return fail("USAGE", "Missing PATTERN argument", HELP21);
18093
+ return fail("USAGE", "Missing PATTERN argument", HELP20);
17816
18094
  if (replacement == null) {
17817
- return fail("USAGE", "Missing REPLACEMENT argument", HELP21);
18095
+ return fail("USAGE", "Missing REPLACEMENT argument", HELP20);
17818
18096
  }
17819
18097
  const ignoreCase = Boolean(parsed.values["ignore-case"]);
17820
18098
  const useRegex = Boolean(parsed.values.regex);
@@ -17852,6 +18130,7 @@ async function run21(args) {
17852
18130
  end: match.end,
17853
18131
  text: match.text
17854
18132
  }));
18133
+ const outputPath = parsed.values.output;
17855
18134
  if (parsed.values["dry-run"]) {
17856
18135
  await respond({
17857
18136
  ok: true,
@@ -17864,7 +18143,8 @@ async function run21(args) {
17864
18143
  ignoreCase,
17865
18144
  totalMatches: allMatches.length,
17866
18145
  replaced: selected.length,
17867
- matches: matchesPayload
18146
+ matches: matchesPayload,
18147
+ ...outputPath ? { output: outputPath } : {}
17868
18148
  });
17869
18149
  return EXIT.OK;
17870
18150
  }
@@ -17889,24 +18169,21 @@ async function run21(args) {
17889
18169
  }
17890
18170
  return rightMatch.start - leftMatch.start;
17891
18171
  });
18172
+ const tracked = isTrackChangesEnabled(view) ? {
18173
+ meta: { author: resolveAuthor(), date: resolveDate() },
18174
+ allocator: createRevisionAllocator(view)
18175
+ } : undefined;
17892
18176
  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;
18177
+ for (const match of reversed) {
18178
+ const concreteReplacement = useRegex ? match.text.replace(new RegExp(pattern, regexFlags), replacement) : replacement;
18179
+ const blockRef = resolveBlock(view, match.blockId);
18180
+ replaceSpanInParagraph(blockRef.node, { start: match.start, end: match.end }, concreteReplacement, tracked);
17904
18181
  }
17905
- await saveDocView(view);
18182
+ await saveDocView(view, outputPath);
17906
18183
  await respond({
17907
18184
  ok: true,
17908
18185
  operation: "replace",
17909
- path,
18186
+ path: outputPath ?? path,
17910
18187
  pattern,
17911
18188
  replacement,
17912
18189
  regex: useRegex,
@@ -17917,7 +18194,7 @@ async function run21(args) {
17917
18194
  });
17918
18195
  return EXIT.OK;
17919
18196
  }
17920
- var HELP21 = `docx replace \u2014 substitute text spans (sed for docx)
18197
+ var HELP20 = `docx replace \u2014 substitute text spans (sed for docx)
17921
18198
 
17922
18199
  Usage:
17923
18200
  docx replace FILE PATTERN REPLACEMENT [options]
@@ -17927,6 +18204,7 @@ Options:
17927
18204
  --ignore-case case-insensitive match
17928
18205
  --all replace every match (default: just the first)
17929
18206
  --limit N replace at most N matches (in document order)
18207
+ -o, --output PATH write to PATH instead of overwriting FILE
17930
18208
  --dry-run report what would change without writing the file
17931
18209
  -h, --help show this help
17932
18210
 
@@ -17959,34 +18237,35 @@ var init_replace2 = __esm(() => {
17959
18237
  // src/cli/track-changes/index.tsx
17960
18238
  var exports_track_changes = {};
17961
18239
  __export(exports_track_changes, {
17962
- run: () => run22
18240
+ run: () => run21
17963
18241
  });
17964
- import { parseArgs as parseArgs19 } from "util";
17965
- async function run22(args) {
18242
+ import { parseArgs as parseArgs18 } from "util";
18243
+ async function run21(args) {
17966
18244
  let parsed;
17967
18245
  try {
17968
- parsed = parseArgs19({
18246
+ parsed = parseArgs18({
17969
18247
  args,
17970
18248
  allowPositionals: true,
17971
18249
  options: {
18250
+ output: { type: "string", short: "o" },
17972
18251
  "dry-run": { type: "boolean" },
17973
18252
  help: { type: "boolean", short: "h" }
17974
18253
  }
17975
18254
  });
17976
18255
  } catch (parseError) {
17977
18256
  const message = parseError instanceof Error ? parseError.message : String(parseError);
17978
- return fail("USAGE", message, HELP22);
18257
+ return fail("USAGE", message, HELP21);
17979
18258
  }
17980
18259
  if (parsed.values.help) {
17981
- await writeStdout(HELP22);
18260
+ await writeStdout(HELP21);
17982
18261
  return EXIT.OK;
17983
18262
  }
17984
18263
  const path = parsed.positionals[0];
17985
18264
  if (!path)
17986
- return fail("USAGE", "Missing FILE argument", HELP22);
18265
+ return fail("USAGE", "Missing FILE argument", HELP21);
17987
18266
  const mode = parsed.positionals[1];
17988
18267
  if (mode !== "on" && mode !== "off") {
17989
- return fail("USAGE", `Expected "on" or "off", got: ${mode ?? "<nothing>"}`, HELP22);
18268
+ return fail("USAGE", `Expected "on" or "off", got: ${mode ?? "<nothing>"}`, HELP21);
17990
18269
  }
17991
18270
  const view = await openOrFail(path);
17992
18271
  if (typeof view === "number")
@@ -18002,6 +18281,7 @@ async function run22(args) {
18002
18281
  view.settingsTree.push(settingsRoot);
18003
18282
  }
18004
18283
  const hasTrackChanges = settingsRoot.children.some((child) => child.tag === "w:trackChanges");
18284
+ const outputPath = parsed.values.output;
18005
18285
  if (parsed.values["dry-run"]) {
18006
18286
  await respond({
18007
18287
  ok: true,
@@ -18009,7 +18289,8 @@ async function run22(args) {
18009
18289
  dryRun: true,
18010
18290
  path,
18011
18291
  mode,
18012
- previouslyOn: hasTrackChanges
18292
+ previouslyOn: hasTrackChanges,
18293
+ ...outputPath ? { output: outputPath } : {}
18013
18294
  });
18014
18295
  return EXIT.OK;
18015
18296
  }
@@ -18026,25 +18307,28 @@ async function run22(args) {
18026
18307
  target: "settings.xml"
18027
18308
  });
18028
18309
  }
18029
- await saveDocView(view);
18310
+ await saveDocView(view, outputPath);
18030
18311
  await respond({
18031
18312
  ok: true,
18032
18313
  operation: "track-changes",
18033
- path,
18314
+ path: outputPath ?? path,
18034
18315
  mode,
18035
18316
  previouslyOn: hasTrackChanges
18036
18317
  });
18037
18318
  return EXIT.OK;
18038
18319
  }
18039
- var HELP22 = `docx track-changes \u2014 toggle the document's tracked-changes mode
18320
+ var HELP21 = `docx track-changes \u2014 toggle the document's tracked-changes mode
18040
18321
 
18041
18322
  Usage:
18042
18323
  docx track-changes FILE on|off [options]
18043
18324
 
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.
18325
+ Sets <w:trackChanges/> in word/settings.xml. When on, this CLI's
18326
+ insert/edit/delete/replace commands also emit <w:ins>/<w:del> markers
18327
+ (attributed to $DOCX_AUTHOR) so changes remain reviewable. Existing
18328
+ <w:ins>/<w:del> markers are unaffected by the toggle itself.
18046
18329
 
18047
18330
  Options:
18331
+ -o, --output PATH Write to PATH instead of overwriting FILE
18048
18332
  --dry-run Print what would change; do not write the file
18049
18333
  -h, --help Show this help
18050
18334
 
@@ -18052,7 +18336,7 @@ Examples:
18052
18336
  docx track-changes doc.docx on
18053
18337
  docx track-changes doc.docx off
18054
18338
  `, 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(() => {
18339
+ var init_track_changes2 = __esm(() => {
18056
18340
  init_core();
18057
18341
  init_jsx();
18058
18342
  init_package();
@@ -18063,7 +18347,7 @@ var init_track_changes = __esm(() => {
18063
18347
  // package.json
18064
18348
  var package_default = {
18065
18349
  name: "bun-docx",
18066
- version: "0.2.0",
18350
+ version: "0.3.0",
18067
18351
  description: "CLI for AI agents (Claude, Codex) to read, edit, and comment on .docx files with full format fidelity.",
18068
18352
  keywords: [
18069
18353
  "docx",
@@ -18138,7 +18422,7 @@ Commands:
18138
18422
  delete FILE Remove a block or run range
18139
18423
  find FILE QUERY Find text spans, return locators
18140
18424
  replace FILE PATTERN REPL Substitute text spans (sed for docx)
18141
- comments \u2026 Add, reply, resolve, delete, restore, list comments
18425
+ comments \u2026 Add, reply, resolve, delete, list comments
18142
18426
  images \u2026 Extract, replace, list images
18143
18427
  track-changes FILE on|off Toggle tracked-changes mode
18144
18428
  info \u2026 Reference material (schema, locators)
@@ -18146,7 +18430,13 @@ Commands:
18146
18430
  Run "docx <command> --help" for command-specific help.
18147
18431
 
18148
18432
  Environment:
18149
- DOCX_AUTHOR Default --author for "comments add" / "comments reply"
18433
+ DOCX_AUTHOR Default author for comments and tracked-change attribution
18434
+ DOCX_CLI_NOW Override the timestamp used for tracked changes (test only)
18435
+
18436
+ Tracked changes:
18437
+ When <w:trackChanges/> is set in the doc (toggle via "docx track-changes
18438
+ FILE on"), insert/edit/delete/replace automatically emit <w:ins>/<w:del>
18439
+ markers attributed to $DOCX_AUTHOR.
18150
18440
  `;
18151
18441
  async function printTopHelp() {
18152
18442
  await writeStdout(TOP_HELP);
@@ -18165,7 +18455,7 @@ var COMMANDS = {
18165
18455
  insert: () => Promise.resolve().then(() => (init_insert(), exports_insert)),
18166
18456
  read: () => Promise.resolve().then(() => (init_read2(), exports_read)),
18167
18457
  replace: () => Promise.resolve().then(() => (init_replace2(), exports_replace2)),
18168
- "track-changes": () => Promise.resolve().then(() => (init_track_changes(), exports_track_changes))
18458
+ "track-changes": () => Promise.resolve().then(() => (init_track_changes2(), exports_track_changes))
18169
18459
  };
18170
18460
  async function main(argv) {
18171
18461
  const args = argv.slice(2);