bun-docx 0.1.1 → 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 +27 -26
  2. package/dist/index.js +2370 -1477
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -47,57 +47,6 @@ var __export = (target, all) => {
47
47
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
48
48
  var __require = import.meta.require;
49
49
 
50
- // src/cli/respond.ts
51
- async function respond(payload) {
52
- await Bun.write(Bun.stdout, `${JSON.stringify(payload)}
53
- `);
54
- }
55
- async function writeStdout(text) {
56
- await Bun.write(Bun.stdout, text);
57
- }
58
- async function fail(code, message, hint) {
59
- const payload = {
60
- ok: false,
61
- code,
62
- error: message
63
- };
64
- if (hint)
65
- payload.hint = hint;
66
- await respond(payload);
67
- return exitCodeFor(code);
68
- }
69
- function exitCodeFor(code) {
70
- switch (code) {
71
- case "USAGE":
72
- case "INVALID_LOCATOR":
73
- return EXIT.USAGE_ERROR;
74
- case "FILE_NOT_FOUND":
75
- case "PART_NOT_FOUND":
76
- case "BLOCK_NOT_FOUND":
77
- case "COMMENT_NOT_FOUND":
78
- case "IMAGE_NOT_FOUND":
79
- return EXIT.NOT_FOUND;
80
- case "PERMISSION_DENIED":
81
- return EXIT.PERMISSION_DENIED;
82
- case "ALREADY_APPLIED":
83
- return EXIT.ALREADY_APPLIED;
84
- case "NOT_A_ZIP":
85
- case "UNHANDLED":
86
- return EXIT.GENERAL_ERROR;
87
- }
88
- }
89
- var EXIT;
90
- var init_respond = __esm(() => {
91
- EXIT = {
92
- OK: 0,
93
- GENERAL_ERROR: 1,
94
- USAGE_ERROR: 2,
95
- NOT_FOUND: 3,
96
- PERMISSION_DENIED: 4,
97
- ALREADY_APPLIED: 5
98
- };
99
- });
100
-
101
50
  // node_modules/process-nextick-args/index.js
102
51
  var require_process_nextick_args = __commonJS((exports, module) => {
103
52
  if (typeof process === "undefined" || !process.version || process.version.indexOf("v0.") === 0 || process.version.indexOf("v1.") === 0 && process.version.indexOf("v1.8.") !== 0) {
@@ -9177,92 +9126,6 @@ var require_lib3 = __commonJS((exports, module) => {
9177
9126
  module.exports = JSZip;
9178
9127
  });
9179
9128
 
9180
- // src/core/package/index.ts
9181
- class Pkg {
9182
- zip;
9183
- path;
9184
- constructor(zip, path) {
9185
- this.zip = zip;
9186
- this.path = path;
9187
- }
9188
- static async open(path) {
9189
- const file = Bun.file(path);
9190
- if (!await file.exists()) {
9191
- throw new PkgError("FILE_NOT_FOUND", `File not found: ${path}`);
9192
- }
9193
- const buf = await file.arrayBuffer();
9194
- let zip;
9195
- try {
9196
- zip = await import_jszip.default.loadAsync(buf);
9197
- } catch (err) {
9198
- const reason = err instanceof Error ? err.message : String(err);
9199
- throw new PkgError("NOT_A_ZIP", `Not a valid .docx (zip): ${reason}`);
9200
- }
9201
- return new Pkg(zip, path);
9202
- }
9203
- hasPart(name) {
9204
- return this.zip.file(name) !== null;
9205
- }
9206
- listParts() {
9207
- const names = [];
9208
- this.zip.forEach((p) => {
9209
- names.push(p);
9210
- });
9211
- return names;
9212
- }
9213
- async readText(name) {
9214
- const entry = this.zip.file(name);
9215
- if (!entry) {
9216
- throw new PkgError("PART_NOT_FOUND", `Part not found: ${name}`);
9217
- }
9218
- return entry.async("string");
9219
- }
9220
- async readBytes(name) {
9221
- const entry = this.zip.file(name);
9222
- if (!entry) {
9223
- throw new PkgError("PART_NOT_FOUND", `Part not found: ${name}`);
9224
- }
9225
- return entry.async("uint8array");
9226
- }
9227
- writeText(name, content) {
9228
- this.zip.file(name, content);
9229
- }
9230
- writeBytes(name, content) {
9231
- this.zip.file(name, content);
9232
- }
9233
- deletePart(name) {
9234
- this.zip.remove(name);
9235
- }
9236
- async save(path) {
9237
- const target = path ?? this.path;
9238
- const buf = await this.zip.generateAsync({
9239
- type: "uint8array",
9240
- compression: "DEFLATE",
9241
- compressionOptions: { level: 6 }
9242
- });
9243
- await Bun.write(target, buf);
9244
- }
9245
- async toBytes() {
9246
- return this.zip.generateAsync({
9247
- type: "uint8array",
9248
- compression: "DEFLATE",
9249
- compressionOptions: { level: 6 }
9250
- });
9251
- }
9252
- }
9253
- var import_jszip, PkgError;
9254
- var init_package = __esm(() => {
9255
- import_jszip = __toESM(require_lib3(), 1);
9256
- PkgError = class PkgError extends Error {
9257
- code;
9258
- constructor(code, message) {
9259
- super(message);
9260
- this.code = code;
9261
- this.name = "PkgError";
9262
- }
9263
- };
9264
- });
9265
-
9266
9129
  // node_modules/path-expression-matcher/src/Expression.js
9267
9130
  class Expression {
9268
9131
  constructor(pattern, options = {}, data) {
@@ -13559,10 +13422,8 @@ class XmlNode2 {
13559
13422
  }
13560
13423
  static serialize(tree) {
13561
13424
  const builder = new Builder(BUILD_OPTIONS);
13562
- const flat = [];
13563
- flattenFragments(tree, flat);
13564
13425
  const pojo = [];
13565
- for (const node of flat) {
13426
+ for (const node of tree) {
13566
13427
  pojo.push(node.toObject());
13567
13428
  }
13568
13429
  return builder.build(pojo);
@@ -13653,6 +13514,15 @@ class XmlNode2 {
13653
13514
  }
13654
13515
  return out;
13655
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
+ }
13656
13526
  toObject() {
13657
13527
  if (this.isText) {
13658
13528
  return { "#text": this.text ?? "" };
@@ -13676,16 +13546,7 @@ class XmlNode2 {
13676
13546
  return result;
13677
13547
  }
13678
13548
  }
13679
- function flattenFragments(tree, out) {
13680
- for (const node of tree) {
13681
- if (node.tag === FRAGMENT_TAG) {
13682
- flattenFragments(node.children, out);
13683
- continue;
13684
- }
13685
- out.push(node);
13686
- }
13687
- }
13688
- var PARSE_OPTIONS, BUILD_OPTIONS, FRAGMENT_TAG = "#fragment";
13549
+ var PARSE_OPTIONS, BUILD_OPTIONS;
13689
13550
  var init_xml_node = __esm(() => {
13690
13551
  init_fxb();
13691
13552
  init_fxp();
@@ -13708,66 +13569,252 @@ var init_xml_node = __esm(() => {
13708
13569
  };
13709
13570
  });
13710
13571
 
13572
+ // src/core/parser/run-ops.ts
13573
+ function runTextLength(run) {
13574
+ let total = 0;
13575
+ for (const child of run.children) {
13576
+ if (child.tag === "w:t")
13577
+ total += child.collectText().length;
13578
+ }
13579
+ return total;
13580
+ }
13581
+ function sliceRun(run, start, end) {
13582
+ const sliced = new XmlNode2("w:r", { ...run.attributes });
13583
+ let consumed = 0;
13584
+ for (const child of run.children) {
13585
+ if (child.tag === "w:t") {
13586
+ const text = child.collectText();
13587
+ const localStart = Math.max(0, start - consumed);
13588
+ const localEnd = Math.min(text.length, end - consumed);
13589
+ if (localStart < localEnd) {
13590
+ const slicedText = new XmlNode2("w:t", { "xml:space": "preserve" });
13591
+ slicedText.children.push(XmlNode2.textNode(text.slice(localStart, localEnd)));
13592
+ sliced.children.push(slicedText);
13593
+ }
13594
+ consumed += text.length;
13595
+ continue;
13596
+ }
13597
+ sliced.children.push(child.clone());
13598
+ }
13599
+ return sliced;
13600
+ }
13601
+ var init_run_ops = __esm(() => {
13602
+ init_xml_node();
13603
+ });
13604
+
13711
13605
  // src/core/parser/index.ts
13712
13606
  var init_parser = __esm(() => {
13607
+ init_run_ops();
13713
13608
  init_xml_node();
13714
13609
  });
13715
13610
 
13716
- // src/core/ast/read.ts
13717
- function buildDoc(view, path) {
13718
- readImageRelationships(view);
13719
- const properties = readDocProperties(view);
13720
- const documentRoot = XmlNode2.findRoot(view.documentTree, "w:document");
13721
- if (!documentRoot) {
13722
- throw new Error("Invalid .docx: missing <w:document>");
13723
- }
13724
- const body = documentRoot.findChild("w:body");
13725
- if (!body) {
13726
- throw new Error("Invalid .docx: missing <w:body>");
13727
- }
13728
- const state = {
13729
- imageIndex: 0,
13730
- commentAnchors: new Map,
13731
- openComments: new Map
13732
- };
13733
- const blocks = readBlocks(view, body, state);
13734
- const comments = readComments(view, state.commentAnchors);
13735
- return { schemaVersion: 1, path, properties, blocks, comments };
13736
- }
13737
- function readBlocks(view, body, state) {
13738
- const blocks = [];
13739
- let paragraphIndex = 0;
13740
- let tableIndex = 0;
13741
- let sectionIndex = 0;
13742
- for (const child of body.children) {
13743
- if (child.tag === "w:p") {
13744
- const id = `p${paragraphIndex++}`;
13745
- blocks.push(readParagraph(view, child, id, state));
13746
- view.blockReferences.set(id, { node: child, parent: body.children });
13611
+ // src/core/package/parts.ts
13612
+ function nextRelationshipId(relationships) {
13613
+ let highest = 0;
13614
+ for (const child of relationships.children) {
13615
+ if (child.tag !== "Relationship")
13747
13616
  continue;
13748
- }
13749
- if (child.tag === "w:tbl") {
13750
- const id = `t${tableIndex++}`;
13751
- blocks.push(readTable(view, child, id, state));
13752
- view.blockReferences.set(id, { node: child, parent: body.children });
13617
+ const id = child.getAttribute("Id");
13618
+ if (!id)
13619
+ continue;
13620
+ const match = id.match(/^rId(\d+)$/);
13621
+ if (!match)
13753
13622
  continue;
13623
+ const numeric = Number(match[1]);
13624
+ if (Number.isFinite(numeric) && numeric > highest)
13625
+ highest = numeric;
13626
+ }
13627
+ return `rId${highest + 1}`;
13628
+ }
13629
+ function registerPart(relationshipsTree, contentTypesTree, part) {
13630
+ const relationships = XmlNode2.findRoot(relationshipsTree, "Relationships");
13631
+ if (relationships) {
13632
+ const alreadyLinked = relationships.children.some((child) => child.tag === "Relationship" && child.getAttribute("Type") === part.relationshipType);
13633
+ if (!alreadyLinked) {
13634
+ relationships.children.push(new XmlNode2("Relationship", {
13635
+ Id: nextRelationshipId(relationships),
13636
+ Type: part.relationshipType,
13637
+ Target: part.target
13638
+ }));
13754
13639
  }
13755
- if (child.tag === "w:sectPr") {
13756
- const id = `s${sectionIndex++}`;
13757
- blocks.push({ id, type: "sectionBreak" });
13758
- view.blockReferences.set(id, { node: child, parent: body.children });
13640
+ }
13641
+ const types = XmlNode2.findRoot(contentTypesTree, "Types");
13642
+ if (types) {
13643
+ const overrideExists = types.children.some((child) => child.tag === "Override" && child.getAttribute("PartName") === `/${part.partName}`);
13644
+ if (!overrideExists) {
13645
+ types.children.push(new XmlNode2("Override", {
13646
+ PartName: `/${part.partName}`,
13647
+ ContentType: part.contentType
13648
+ }));
13759
13649
  }
13760
13650
  }
13761
- return blocks;
13762
13651
  }
13763
- function readParagraph(view, node, id, state) {
13764
- const paragraph = { id, type: "paragraph", runs: [] };
13765
- const paragraphProperties = node.findChild("w:pPr");
13766
- if (paragraphProperties) {
13767
- applyParagraphProperties(paragraph, paragraphProperties);
13652
+ var init_parts = __esm(() => {
13653
+ init_parser();
13654
+ });
13655
+
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;
13768
13666
  }
13769
- const activeComments = new Set;
13770
- let offset = 0;
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
+
13677
+ class Pkg {
13678
+ zip;
13679
+ path;
13680
+ constructor(zip, path) {
13681
+ this.zip = zip;
13682
+ this.path = path;
13683
+ }
13684
+ static async open(path) {
13685
+ const file = Bun.file(path);
13686
+ if (!await file.exists()) {
13687
+ throw new PkgError("FILE_NOT_FOUND", `File not found: ${path}`);
13688
+ }
13689
+ const buf = await file.arrayBuffer();
13690
+ let zip;
13691
+ try {
13692
+ zip = await import_jszip.default.loadAsync(buf);
13693
+ } catch (err) {
13694
+ const reason = err instanceof Error ? err.message : String(err);
13695
+ throw new PkgError("NOT_A_ZIP", `Not a valid .docx (zip): ${reason}`);
13696
+ }
13697
+ return new Pkg(zip, path);
13698
+ }
13699
+ hasPart(name) {
13700
+ return this.zip.file(name) !== null;
13701
+ }
13702
+ listParts() {
13703
+ const names = [];
13704
+ this.zip.forEach((p) => {
13705
+ names.push(p);
13706
+ });
13707
+ return names;
13708
+ }
13709
+ async readText(name) {
13710
+ const entry = this.zip.file(name);
13711
+ if (!entry) {
13712
+ throw new PkgError("PART_NOT_FOUND", `Part not found: ${name}`);
13713
+ }
13714
+ return entry.async("string");
13715
+ }
13716
+ async readBytes(name) {
13717
+ const entry = this.zip.file(name);
13718
+ if (!entry) {
13719
+ throw new PkgError("PART_NOT_FOUND", `Part not found: ${name}`);
13720
+ }
13721
+ return entry.async("uint8array");
13722
+ }
13723
+ writeText(name, content) {
13724
+ this.zip.file(name, content);
13725
+ }
13726
+ writeBytes(name, content) {
13727
+ this.zip.file(name, content);
13728
+ }
13729
+ deletePart(name) {
13730
+ this.zip.remove(name);
13731
+ }
13732
+ async save(path) {
13733
+ const target = path ?? this.path;
13734
+ const buf = await this.zip.generateAsync({
13735
+ type: "uint8array",
13736
+ compression: "DEFLATE",
13737
+ compressionOptions: { level: 6 }
13738
+ });
13739
+ await writeAtomic(target, buf);
13740
+ }
13741
+ async toBytes() {
13742
+ return this.zip.generateAsync({
13743
+ type: "uint8array",
13744
+ compression: "DEFLATE",
13745
+ compressionOptions: { level: 6 }
13746
+ });
13747
+ }
13748
+ }
13749
+ var import_jszip, PkgError;
13750
+ var init_package = __esm(() => {
13751
+ init_parts();
13752
+ import_jszip = __toESM(require_lib3(), 1);
13753
+ PkgError = class PkgError extends Error {
13754
+ code;
13755
+ constructor(code, message) {
13756
+ super(message);
13757
+ this.code = code;
13758
+ this.name = "PkgError";
13759
+ }
13760
+ };
13761
+ });
13762
+
13763
+ // src/core/ast/read.ts
13764
+ function buildDoc(view, path) {
13765
+ readImageRelationships(view);
13766
+ const properties = readDocProperties(view);
13767
+ const documentRoot = XmlNode2.findRoot(view.documentTree, "w:document");
13768
+ if (!documentRoot) {
13769
+ throw new Error("Invalid .docx: missing <w:document>");
13770
+ }
13771
+ const body = documentRoot.findChild("w:body");
13772
+ if (!body) {
13773
+ throw new Error("Invalid .docx: missing <w:body>");
13774
+ }
13775
+ const state = {
13776
+ imageIndex: 0,
13777
+ commentAnchors: new Map,
13778
+ openComments: new Map
13779
+ };
13780
+ const blocks = readBlocks(view, body, state);
13781
+ const comments = readComments(view, state.commentAnchors);
13782
+ return { schemaVersion: 1, path, properties, blocks, comments };
13783
+ }
13784
+ function readBlocks(view, body, state) {
13785
+ const blocks = [];
13786
+ let paragraphIndex = 0;
13787
+ let tableIndex = 0;
13788
+ let sectionIndex = 0;
13789
+ for (const child of body.children) {
13790
+ if (child.tag === "w:p") {
13791
+ const id = `p${paragraphIndex++}`;
13792
+ blocks.push(readParagraph(view, child, id, state));
13793
+ view.blockReferences.set(id, { node: child, parent: body.children });
13794
+ continue;
13795
+ }
13796
+ if (child.tag === "w:tbl") {
13797
+ const id = `t${tableIndex++}`;
13798
+ blocks.push(readTable(view, child, id, state));
13799
+ view.blockReferences.set(id, { node: child, parent: body.children });
13800
+ continue;
13801
+ }
13802
+ if (child.tag === "w:sectPr") {
13803
+ const id = `s${sectionIndex++}`;
13804
+ blocks.push({ id, type: "sectionBreak" });
13805
+ view.blockReferences.set(id, { node: child, parent: body.children });
13806
+ }
13807
+ }
13808
+ return blocks;
13809
+ }
13810
+ function readParagraph(view, node, id, state) {
13811
+ const paragraph = { id, type: "paragraph", runs: [] };
13812
+ const paragraphProperties = node.findChild("w:pPr");
13813
+ if (paragraphProperties) {
13814
+ applyParagraphProperties(paragraph, paragraphProperties);
13815
+ }
13816
+ const activeComments = new Set;
13817
+ let offset = 0;
13771
13818
  for (const child of node.children) {
13772
13819
  if (child.tag === "w:pPr")
13773
13820
  continue;
@@ -13815,6 +13862,33 @@ function readParagraph(view, node, id, state) {
13815
13862
  revisionId: child.getAttribute("w:id") ?? ""
13816
13863
  };
13817
13864
  for (const inner of child.children) {
13865
+ if (inner.tag === "w:commentRangeStart") {
13866
+ const commentId = inner.getAttribute("w:id");
13867
+ if (commentId) {
13868
+ const key = `c${commentId}`;
13869
+ activeComments.add(key);
13870
+ state.openComments.set(key, { blockId: id, offset });
13871
+ }
13872
+ continue;
13873
+ }
13874
+ if (inner.tag === "w:commentRangeEnd") {
13875
+ const commentId = inner.getAttribute("w:id");
13876
+ if (commentId) {
13877
+ const key = `c${commentId}`;
13878
+ activeComments.delete(key);
13879
+ const opened = state.openComments.get(key);
13880
+ if (opened) {
13881
+ state.commentAnchors.set(key, {
13882
+ startBlockId: opened.blockId,
13883
+ startOffset: opened.offset,
13884
+ endBlockId: id,
13885
+ endOffset: offset
13886
+ });
13887
+ state.openComments.delete(key);
13888
+ }
13889
+ }
13890
+ continue;
13891
+ }
13818
13892
  if (inner.tag !== "w:r")
13819
13893
  continue;
13820
13894
  const run = readRun(view, inner, activeComments, change, state);
@@ -13869,8 +13943,9 @@ function readRun(view, node, activeComments, trackedChange, state) {
13869
13943
  }
13870
13944
  let combinedText = "";
13871
13945
  for (const child of node.children) {
13872
- if (child.tag === "w:t")
13946
+ if (child.tag === "w:t" || child.tag === "w:delText") {
13873
13947
  combinedText += child.collectText();
13948
+ }
13874
13949
  }
13875
13950
  if (combinedText.length === 0)
13876
13951
  return null;
@@ -13954,26 +14029,33 @@ function readImageFromDrawing(view, drawing, state) {
13954
14029
  }
13955
14030
  function readTable(view, node, id, state) {
13956
14031
  const rows = [];
14032
+ let rowIndex = 0;
13957
14033
  for (const child of node.children) {
13958
14034
  if (child.tag !== "w:tr")
13959
14035
  continue;
13960
14036
  const cells = [];
14037
+ let columnIndex = 0;
13961
14038
  for (const cellNode of child.children) {
13962
14039
  if (cellNode.tag !== "w:tc")
13963
14040
  continue;
13964
- cells.push({ blocks: readCellBlocks(view, cellNode, state) });
14041
+ cells.push({
14042
+ blocks: readCellBlocks(view, cellNode, id, rowIndex, columnIndex, state)
14043
+ });
14044
+ columnIndex++;
13965
14045
  }
13966
14046
  rows.push({ cells });
14047
+ rowIndex++;
13967
14048
  }
13968
14049
  return { id, type: "table", rows };
13969
14050
  }
13970
- function readCellBlocks(view, cell, state) {
14051
+ function readCellBlocks(view, cell, tableId, rowIndex, columnIndex, state) {
13971
14052
  const blocks = [];
13972
14053
  let paragraphIndex = 0;
13973
14054
  for (const child of cell.children) {
13974
14055
  if (child.tag === "w:p") {
13975
- const id = `cellp${paragraphIndex++}`;
14056
+ const id = `${tableId}:r${rowIndex}c${columnIndex}:p${paragraphIndex++}`;
13976
14057
  blocks.push(readParagraph(view, child, id, state));
14058
+ view.blockReferences.set(id, { node: child, parent: cell.children });
13977
14059
  }
13978
14060
  }
13979
14061
  return blocks;
@@ -14136,6 +14218,7 @@ async function openDocView(path) {
14136
14218
  const commentsTree = pkg.hasPart("word/comments.xml") ? XmlNode2.parse(await pkg.readText("word/comments.xml")) : undefined;
14137
14219
  const commentsExtTree = pkg.hasPart("word/commentsExtended.xml") ? XmlNode2.parse(await pkg.readText("word/commentsExtended.xml")) : undefined;
14138
14220
  const corePropertiesTree = pkg.hasPart("docProps/core.xml") ? XmlNode2.parse(await pkg.readText("docProps/core.xml")) : undefined;
14221
+ const settingsTree = pkg.hasPart("word/settings.xml") ? XmlNode2.parse(await pkg.readText("word/settings.xml")) : undefined;
14139
14222
  const view = {
14140
14223
  pkg,
14141
14224
  documentTree,
@@ -14144,6 +14227,7 @@ async function openDocView(path) {
14144
14227
  relationshipsTree,
14145
14228
  contentTypesTree,
14146
14229
  corePropertiesTree,
14230
+ settingsTree,
14147
14231
  doc: undefined,
14148
14232
  blockReferences: new Map,
14149
14233
  commentReferences: new Map,
@@ -14163,6 +14247,9 @@ async function saveDocView(view, path) {
14163
14247
  if (view.commentsExtTree) {
14164
14248
  view.pkg.writeText("word/commentsExtended.xml", XmlNode2.serialize(view.commentsExtTree));
14165
14249
  }
14250
+ if (view.settingsTree) {
14251
+ view.pkg.writeText("word/settings.xml", XmlNode2.serialize(view.settingsTree));
14252
+ }
14166
14253
  await view.pkg.save(path);
14167
14254
  }
14168
14255
  async function enrichImageHashes(view) {
@@ -14290,7 +14377,7 @@ var init_parse = __esm(() => {
14290
14377
  this.name = "LocatorParseError";
14291
14378
  }
14292
14379
  };
14293
- BLOCK_RE = /^(p|t|s|cellp)(\d+)$/;
14380
+ BLOCK_RE = /^(p|t|s)(\d+)$/;
14294
14381
  SPAN_RE = /^p(\d+):(\d+)-(\d+)$/;
14295
14382
  RANGE_RE = /^p(\d+):(\d+)-p(\d+):(\d+)$/;
14296
14383
  COMMENT_RE = /^c(\d+)$/;
@@ -14299,6 +14386,26 @@ var init_parse = __esm(() => {
14299
14386
  });
14300
14387
 
14301
14388
  // src/core/locators/resolve.ts
14389
+ function locatorToBlockTarget(locator) {
14390
+ if (locator.kind === "block")
14391
+ return { blockId: locator.blockId };
14392
+ if (locator.kind === "blockSpan") {
14393
+ return {
14394
+ blockId: locator.blockId,
14395
+ span: { start: locator.start, end: locator.end }
14396
+ };
14397
+ }
14398
+ if (locator.kind === "cell" && locator.inner) {
14399
+ const inner = locatorToBlockTarget(locator.inner);
14400
+ if (!inner)
14401
+ return null;
14402
+ return {
14403
+ blockId: `${locator.tableId}:r${locator.row}c${locator.col}:${inner.blockId}`,
14404
+ span: inner.span
14405
+ };
14406
+ }
14407
+ return null;
14408
+ }
14302
14409
  function resolveBlock(view, blockId) {
14303
14410
  const reference = view.blockReferences.get(blockId);
14304
14411
  if (!reference) {
@@ -14324,15 +14431,82 @@ var init_locators = __esm(() => {
14324
14431
  init_resolve();
14325
14432
  });
14326
14433
 
14327
- // src/core/index.ts
14328
- var init_core = __esm(() => {
14329
- init_ast();
14330
- init_locators();
14331
- init_package();
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");
14442
+ }
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
+ }
14451
+ };
14452
+ }
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";
14461
+ }
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);
14489
+ }
14490
+ }
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);
14496
+ }
14497
+ }
14498
+ var init_track_changes = __esm(() => {
14332
14499
  init_parser();
14333
14500
  });
14334
14501
 
14335
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
+ }
14336
14510
  function namespace(prefix, tags) {
14337
14511
  const result = {};
14338
14512
  for (const tagName of tags) {
@@ -14341,19 +14515,22 @@ function namespace(prefix, tags) {
14341
14515
  return result;
14342
14516
  }
14343
14517
  function makeTag(qualifiedName) {
14344
- return (props, ...children) => {
14518
+ return (props) => {
14345
14519
  const attributes = {};
14520
+ let childrenProp;
14346
14521
  if (props) {
14347
14522
  for (const [key, value] of Object.entries(props)) {
14348
- if (key === "children")
14523
+ if (key === "children") {
14524
+ childrenProp = value;
14349
14525
  continue;
14526
+ }
14350
14527
  if (value === false || value == null)
14351
14528
  continue;
14352
14529
  attributes[mapAttributeName(key)] = String(value);
14353
14530
  }
14354
14531
  }
14355
14532
  const childNodes = [];
14356
- flatten(children, childNodes);
14533
+ flatten(normalizeChildren(childrenProp), childNodes);
14357
14534
  return new XmlNode2(qualifiedName, attributes, childNodes);
14358
14535
  };
14359
14536
  }
@@ -14366,7 +14543,7 @@ function flatten(items, out) {
14366
14543
  continue;
14367
14544
  }
14368
14545
  if (item instanceof XmlNode2) {
14369
- if (item.tag === FRAGMENT_TAG2) {
14546
+ if (item.tag === FRAGMENT_TAG) {
14370
14547
  for (const child of item.children)
14371
14548
  out.push(child);
14372
14549
  continue;
@@ -14385,7 +14562,7 @@ function mapAttributeName(name) {
14385
14562
  return name;
14386
14563
  return `${name.slice(0, dashIndex)}:${name.slice(dashIndex + 1)}`;
14387
14564
  }
14388
- 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;
14389
14566
  var init_jsx = __esm(() => {
14390
14567
  init_parser();
14391
14568
  W_TAGS = [
@@ -14423,6 +14600,7 @@ var init_jsx = __esm(() => {
14423
14600
  "tcPr",
14424
14601
  "ins",
14425
14602
  "del",
14603
+ "delText",
14426
14604
  "commentRangeStart",
14427
14605
  "commentRangeEnd",
14428
14606
  "commentReference",
@@ -14477,18 +14655,9 @@ var init_jsx = __esm(() => {
14477
14655
 
14478
14656
  // src/core/jsx/jsx-runtime.ts
14479
14657
  function jsx(type, props, _key) {
14480
- const { children, ...rest } = props ?? {};
14481
- const childArgs = normalizeChildren(children);
14482
- const result = type(rest, ...childArgs);
14658
+ const result = type(props);
14483
14659
  return result ?? new XmlNode2("#fragment");
14484
14660
  }
14485
- function normalizeChildren(children) {
14486
- if (children === undefined)
14487
- return [];
14488
- if (Array.isArray(children))
14489
- return children;
14490
- return [children];
14491
- }
14492
14661
  var jsxDEV;
14493
14662
  var init_jsx_runtime = __esm(() => {
14494
14663
  init_parser();
@@ -14501,63 +14670,191 @@ var init_jsx_dev_runtime = __esm(() => {
14501
14670
  init_jsx_runtime();
14502
14671
  });
14503
14672
 
14504
- // src/cli/comments/helpers.tsx
14505
- function nextCommentId(view) {
14506
- if (!view.commentsTree)
14507
- return "0";
14508
- const root = XmlNode2.findRoot(view.commentsTree, "w:comments");
14509
- if (!root)
14510
- return "0";
14511
- let highest = -1;
14512
- for (const child of root.children) {
14513
- if (child.tag !== "w:comment")
14514
- continue;
14515
- const idAttribute = child.getAttribute("w:id");
14516
- if (idAttribute == null)
14517
- continue;
14518
- const numeric = Number(idAttribute);
14519
- if (Number.isFinite(numeric) && numeric > highest)
14520
- highest = numeric;
14521
- }
14522
- return String(highest + 1);
14523
- }
14524
- function generateParaId() {
14525
- const bytes = new Uint8Array(4);
14526
- crypto.getRandomValues(bytes);
14527
- let hex = "";
14528
- for (const byte of bytes)
14529
- hex += byte.toString(16).padStart(2, "0");
14530
- return hex.toUpperCase();
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);
14531
14684
  }
14532
- function authorInitials(author) {
14533
- const parts = author.trim().split(/\s+/).filter(Boolean);
14534
- if (parts.length === 0)
14535
- return "?";
14536
- const first = parts[0]?.charAt(0) ?? "";
14537
- const last = parts.length > 1 ? parts[parts.length - 1]?.charAt(0) ?? "" : "";
14538
- return (first + last).toUpperCase() || "?";
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);
14539
14695
  }
14540
- function ensureCommentsPart(view) {
14541
- if (view.commentsTree) {
14542
- const existing = XmlNode2.findRoot(view.commentsTree, "w:comments");
14543
- if (existing)
14544
- return existing;
14545
- }
14546
- const root = /* @__PURE__ */ jsxDEV(w.comments, {
14547
- "xmlns:w": NS_W,
14548
- "xmlns:w14": NS_W14
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
14549
14711
  }, undefined, false, undefined, this);
14550
- view.commentsTree = [root];
14551
- registerPart(view, {
14552
- partName: "word/comments.xml",
14553
- contentType: COMMENTS_CONTENT_TYPE,
14554
- relationshipType: COMMENTS_REL_TYPE,
14555
- target: "comments.xml"
14556
- });
14557
- return root;
14712
+ rPr.children.push(marker);
14558
14713
  }
14559
- function ensureCommentsExtPart(view) {
14560
- if (view.commentsExtTree) {
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
+
14801
+ // src/cli/comments/helpers.tsx
14802
+ function nextCommentId(view) {
14803
+ if (!view.commentsTree)
14804
+ return "0";
14805
+ const root = XmlNode2.findRoot(view.commentsTree, "w:comments");
14806
+ if (!root)
14807
+ return "0";
14808
+ let highest = -1;
14809
+ for (const child of root.children) {
14810
+ if (child.tag !== "w:comment")
14811
+ continue;
14812
+ const idAttribute = child.getAttribute("w:id");
14813
+ if (idAttribute == null)
14814
+ continue;
14815
+ const numeric = Number(idAttribute);
14816
+ if (Number.isFinite(numeric) && numeric > highest)
14817
+ highest = numeric;
14818
+ }
14819
+ return String(highest + 1);
14820
+ }
14821
+ function generateParaId() {
14822
+ const bytes = new Uint8Array(4);
14823
+ crypto.getRandomValues(bytes);
14824
+ let hex = "";
14825
+ for (const byte of bytes)
14826
+ hex += byte.toString(16).padStart(2, "0");
14827
+ return hex.toUpperCase();
14828
+ }
14829
+ function authorInitials(author) {
14830
+ const parts = author.trim().split(/\s+/).filter(Boolean);
14831
+ if (parts.length === 0)
14832
+ return "?";
14833
+ const first = parts[0]?.charAt(0) ?? "";
14834
+ const last = parts.length > 1 ? parts[parts.length - 1]?.charAt(0) ?? "" : "";
14835
+ return (first + last).toUpperCase() || "?";
14836
+ }
14837
+ function ensureCommentsPart(view) {
14838
+ if (view.commentsTree) {
14839
+ const existing = XmlNode2.findRoot(view.commentsTree, "w:comments");
14840
+ if (existing)
14841
+ return existing;
14842
+ }
14843
+ const root = /* @__PURE__ */ jsxDEV(w.comments, {
14844
+ "xmlns:w": NS_W,
14845
+ "xmlns:w14": NS_W14
14846
+ }, undefined, false, undefined, this);
14847
+ view.commentsTree = [root];
14848
+ registerPart(view.relationshipsTree, view.contentTypesTree, {
14849
+ partName: "word/comments.xml",
14850
+ contentType: COMMENTS_CONTENT_TYPE,
14851
+ relationshipType: COMMENTS_REL_TYPE,
14852
+ target: "comments.xml"
14853
+ });
14854
+ return root;
14855
+ }
14856
+ function ensureCommentsExtPart(view) {
14857
+ if (view.commentsExtTree) {
14561
14858
  const existing = XmlNode2.findRoot(view.commentsExtTree, "w15:commentsEx");
14562
14859
  if (existing)
14563
14860
  return existing;
@@ -14568,7 +14865,7 @@ function ensureCommentsExtPart(view) {
14568
14865
  "mc:Ignorable": "w15"
14569
14866
  }, undefined, false, undefined, this);
14570
14867
  view.commentsExtTree = [root];
14571
- registerPart(view, {
14868
+ registerPart(view.relationshipsTree, view.contentTypesTree, {
14572
14869
  partName: "word/commentsExtended.xml",
14573
14870
  contentType: COMMENTS_EXT_CONTENT_TYPE,
14574
14871
  relationshipType: COMMENTS_EXT_REL_TYPE,
@@ -14576,46 +14873,6 @@ function ensureCommentsExtPart(view) {
14576
14873
  });
14577
14874
  return root;
14578
14875
  }
14579
- function registerPart(view, part) {
14580
- const relationships = XmlNode2.findRoot(view.relationshipsTree, "Relationships");
14581
- if (relationships) {
14582
- const alreadyLinked = relationships.children.some((child) => child.tag === "Relationship" && child.getAttribute("Type") === part.relationshipType);
14583
- if (!alreadyLinked) {
14584
- relationships.children.push(new XmlNode2("Relationship", {
14585
- Id: nextRelationshipId(relationships),
14586
- Type: part.relationshipType,
14587
- Target: part.target
14588
- }));
14589
- }
14590
- }
14591
- const types = XmlNode2.findRoot(view.contentTypesTree, "Types");
14592
- if (types) {
14593
- const overrideExists = types.children.some((child) => child.tag === "Override" && child.getAttribute("PartName") === `/${part.partName}`);
14594
- if (!overrideExists) {
14595
- types.children.push(new XmlNode2("Override", {
14596
- PartName: `/${part.partName}`,
14597
- ContentType: part.contentType
14598
- }));
14599
- }
14600
- }
14601
- }
14602
- function nextRelationshipId(relationships) {
14603
- let highest = 0;
14604
- for (const child of relationships.children) {
14605
- if (child.tag !== "Relationship")
14606
- continue;
14607
- const id = child.getAttribute("Id");
14608
- if (!id)
14609
- continue;
14610
- const match = id.match(/^rId(\d+)$/);
14611
- if (!match)
14612
- continue;
14613
- const numeric = Number(match[1]);
14614
- if (Number.isFinite(numeric) && numeric > highest)
14615
- highest = numeric;
14616
- }
14617
- return `rId${highest + 1}`;
14618
- }
14619
14876
  function paragraphTextLength(paragraph) {
14620
14877
  let total = 0;
14621
14878
  for (const child of paragraph.children) {
@@ -14636,17 +14893,46 @@ function addCommentMarkersToParagraph(paragraph, commentId, span) {
14636
14893
  if (range.start < 0 || range.end > total || range.start > range.end) {
14637
14894
  throw new SpanOutOfRangeError(`Span ${range.start}-${range.end} out of paragraph length ${total}`);
14638
14895
  }
14639
- const newChildren = [];
14640
- let offset = 0;
14641
- let placedStart = false;
14642
- let placedEnd = false;
14643
- const startMarker = /* @__PURE__ */ jsxDEV(w.commentRangeStart, {
14896
+ placeMarkersInParagraph(paragraph, [
14897
+ { offset: range.start, node: commentRangeStartMarker(commentId) },
14898
+ {
14899
+ offset: range.end,
14900
+ node: commentRangeEndMarker(commentId),
14901
+ follower: commentReferenceRun(commentId)
14902
+ }
14903
+ ]);
14904
+ }
14905
+ function addCommentRangeMarkers(startParagraph, startOffset, endParagraph, endOffset, commentId) {
14906
+ if (startParagraph === endParagraph) {
14907
+ addCommentMarkersToParagraph(startParagraph, commentId, {
14908
+ start: startOffset,
14909
+ end: endOffset
14910
+ });
14911
+ return;
14912
+ }
14913
+ placeMarkersInParagraph(startParagraph, [
14914
+ { offset: startOffset, node: commentRangeStartMarker(commentId) }
14915
+ ]);
14916
+ placeMarkersInParagraph(endParagraph, [
14917
+ {
14918
+ offset: endOffset,
14919
+ node: commentRangeEndMarker(commentId),
14920
+ follower: commentReferenceRun(commentId)
14921
+ }
14922
+ ]);
14923
+ }
14924
+ function commentRangeStartMarker(commentId) {
14925
+ return /* @__PURE__ */ jsxDEV(w.commentRangeStart, {
14644
14926
  "w-id": commentId
14645
14927
  }, undefined, false, undefined, this);
14646
- const endMarker = /* @__PURE__ */ jsxDEV(w.commentRangeEnd, {
14928
+ }
14929
+ function commentRangeEndMarker(commentId) {
14930
+ return /* @__PURE__ */ jsxDEV(w.commentRangeEnd, {
14647
14931
  "w-id": commentId
14648
14932
  }, undefined, false, undefined, this);
14649
- const referenceRun = /* @__PURE__ */ jsxDEV(w.r, {
14933
+ }
14934
+ function commentReferenceRun(commentId) {
14935
+ return /* @__PURE__ */ jsxDEV(w.r, {
14650
14936
  children: [
14651
14937
  /* @__PURE__ */ jsxDEV(w.rPr, {
14652
14938
  children: /* @__PURE__ */ jsxDEV(w.rStyle, {
@@ -14658,108 +14944,97 @@ function addCommentMarkersToParagraph(paragraph, commentId, span) {
14658
14944
  }, undefined, false, undefined, this)
14659
14945
  ]
14660
14946
  }, undefined, true, undefined, this);
14661
- for (const child of paragraph.children) {
14662
- if (child.tag !== "w:r") {
14663
- if (!placedStart && offset === range.start) {
14664
- placedStart = true;
14665
- if (child.tag === "w:pPr") {
14666
- newChildren.push(child);
14667
- newChildren.push(startMarker);
14947
+ }
14948
+ function placeMarkersInParagraph(paragraph, markers) {
14949
+ if (markers.length === 0)
14950
+ return;
14951
+ const total = paragraphTextLength(paragraph);
14952
+ for (const marker of markers) {
14953
+ if (marker.offset < 0 || marker.offset > total) {
14954
+ throw new SpanOutOfRangeError(`Marker offset ${marker.offset} out of paragraph length ${total}`);
14955
+ }
14956
+ }
14957
+ const pending = markers.slice();
14958
+ const state = { offset: 0, placedCount: 0 };
14959
+ paragraph.children = walkAndPlace(paragraph.children, pending, true, state);
14960
+ flushAtCurrentOffset(paragraph.children, pending, state);
14961
+ if (state.placedCount !== markers.length) {
14962
+ throw new SpanOutOfRangeError(`Could not place comment markers (placed ${state.placedCount} of ${markers.length})`);
14963
+ }
14964
+ }
14965
+ function walkAndPlace(children, pending, isParagraphLevel, state) {
14966
+ const result = [];
14967
+ for (const child of children) {
14968
+ if (child.tag === "w:r") {
14969
+ const length = runTextLength(child);
14970
+ const runStart = state.offset;
14971
+ const runEnd = state.offset + length;
14972
+ const splits = [];
14973
+ for (let i = 0;i < pending.length; i++) {
14974
+ const marker = pending[i];
14975
+ if (!marker)
14668
14976
  continue;
14977
+ if (runStart <= marker.offset && marker.offset <= runEnd) {
14978
+ splits.push({ at: marker.offset - runStart, index: i });
14669
14979
  }
14670
- newChildren.push(startMarker);
14671
14980
  }
14672
- if (!placedEnd && offset === range.end && placedStart) {
14673
- placedEnd = true;
14674
- newChildren.push(endMarker);
14675
- newChildren.push(referenceRun);
14981
+ splits.sort((left, right) => left.at - right.at || left.index - right.index);
14982
+ if (splits.length === 0) {
14983
+ result.push(child);
14984
+ } else {
14985
+ let cursor = 0;
14986
+ for (const split of splits) {
14987
+ if (split.at > cursor) {
14988
+ result.push(sliceRun(child, cursor, split.at));
14989
+ }
14990
+ const marker = pending[split.index];
14991
+ if (!marker)
14992
+ continue;
14993
+ result.push(marker.node);
14994
+ if (marker.follower)
14995
+ result.push(marker.follower);
14996
+ pending[split.index] = null;
14997
+ state.placedCount++;
14998
+ cursor = split.at;
14999
+ }
15000
+ if (cursor < length) {
15001
+ result.push(sliceRun(child, cursor, length));
15002
+ }
14676
15003
  }
14677
- newChildren.push(child);
15004
+ state.offset = runEnd;
14678
15005
  continue;
14679
15006
  }
14680
- const length = runTextLength(child);
14681
- const runStart = offset;
14682
- const runEnd = offset + length;
14683
- const splits = [];
14684
- if (!placedStart && runStart <= range.start && range.start <= runEnd) {
14685
- splits.push({ at: range.start - runStart, node: startMarker });
14686
- placedStart = true;
14687
- }
14688
- if (!placedEnd && runStart <= range.end && range.end <= runEnd) {
14689
- splits.push({ at: range.end - runStart, node: endMarker });
14690
- placedEnd = true;
14691
- }
14692
- splits.sort((leftSplit, rightSplit) => leftSplit.at - rightSplit.at);
14693
- if (splits.length === 0) {
14694
- newChildren.push(child);
14695
- } else {
14696
- let cursor = 0;
14697
- for (const split of splits) {
14698
- if (split.at > cursor) {
14699
- newChildren.push(sliceRun(child, cursor, split.at));
14700
- }
14701
- newChildren.push(split.node);
14702
- cursor = split.at;
14703
- }
14704
- if (cursor < length) {
14705
- newChildren.push(sliceRun(child, cursor, length));
14706
- }
14707
- if (splits.some((split) => split.node === endMarker)) {
14708
- newChildren.push(referenceRun);
14709
- }
15007
+ if (isParagraphLevel && (child.tag === "w:ins" || child.tag === "w:del")) {
15008
+ flushAtCurrentOffset(result, pending, state);
15009
+ const innerChildren = walkAndPlace(child.children, pending, false, state);
15010
+ const wrapper = new XmlNode2(child.tag, { ...child.attributes });
15011
+ wrapper.children = innerChildren;
15012
+ result.push(wrapper);
15013
+ continue;
14710
15014
  }
14711
- offset = runEnd;
14712
- }
14713
- if (!placedStart && offset === range.start) {
14714
- newChildren.push(startMarker);
14715
- placedStart = true;
14716
- }
14717
- if (!placedEnd && offset === range.end) {
14718
- newChildren.push(endMarker);
14719
- newChildren.push(referenceRun);
14720
- placedEnd = true;
14721
- }
14722
- if (!placedStart || !placedEnd) {
14723
- throw new SpanOutOfRangeError(`Could not place comment markers (start placed: ${placedStart}, end placed: ${placedEnd})`);
14724
- }
14725
- paragraph.children = newChildren;
14726
- }
14727
- function runTextLength(run) {
14728
- let total = 0;
14729
- for (const child of run.children) {
14730
- if (child.tag === "w:t")
14731
- total += child.collectText().length;
14732
- }
14733
- return total;
14734
- }
14735
- function sliceRun(run, start, end) {
14736
- const sliced = new XmlNode2("w:r", { ...run.attributes });
14737
- let consumed = 0;
14738
- for (const child of run.children) {
14739
- if (child.tag === "w:t") {
14740
- const text = child.collectText();
14741
- const localStart = Math.max(0, start - consumed);
14742
- const localEnd = Math.min(text.length, end - consumed);
14743
- if (localStart < localEnd) {
14744
- const slicedText = new XmlNode2("w:t", { "xml:space": "preserve" });
14745
- slicedText.children.push(XmlNode2.textNode(text.slice(localStart, localEnd)));
14746
- sliced.children.push(slicedText);
14747
- }
14748
- consumed += text.length;
15015
+ if (child.tag === "w:pPr") {
15016
+ result.push(child);
15017
+ flushAtCurrentOffset(result, pending, state);
14749
15018
  continue;
14750
15019
  }
14751
- sliced.children.push(deepCloneNode(child));
15020
+ flushAtCurrentOffset(result, pending, state);
15021
+ result.push(child);
14752
15022
  }
14753
- return sliced;
15023
+ return result;
14754
15024
  }
14755
- function deepCloneNode(node) {
14756
- const clone = new XmlNode2(node.tag, { ...node.attributes });
14757
- if (node.text !== undefined)
14758
- clone.text = node.text;
14759
- for (const child of node.children) {
14760
- clone.children.push(deepCloneNode(child));
15025
+ function flushAtCurrentOffset(out, pending, state) {
15026
+ for (let i = 0;i < pending.length; i++) {
15027
+ const marker = pending[i];
15028
+ if (!marker)
15029
+ continue;
15030
+ if (marker.offset !== state.offset)
15031
+ continue;
15032
+ out.push(marker.node);
15033
+ if (marker.follower)
15034
+ out.push(marker.follower);
15035
+ pending[i] = null;
15036
+ state.placedCount++;
14761
15037
  }
14762
- return clone;
14763
15038
  }
14764
15039
  function CommentBody({
14765
15040
  options
@@ -14871,6 +15146,7 @@ function containsCommentReference(run, numericId) {
14871
15146
  var COMMENTS_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments", COMMENTS_EXT_REL_TYPE = "http://schemas.microsoft.com/office/2011/relationships/commentsExtended", COMMENTS_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml", COMMENTS_EXT_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml", NS_W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main", NS_W14 = "http://schemas.microsoft.com/office/word/2010/wordml", NS_W15 = "http://schemas.microsoft.com/office/word/2012/wordml", SpanOutOfRangeError;
14872
15147
  var init_helpers = __esm(() => {
14873
15148
  init_jsx();
15149
+ init_package();
14874
15150
  init_parser();
14875
15151
  init_jsx_dev_runtime();
14876
15152
  SpanOutOfRangeError = class SpanOutOfRangeError extends Error {
@@ -14897,6 +15173,7 @@ async function run(args) {
14897
15173
  range: { type: "string" },
14898
15174
  text: { type: "string" },
14899
15175
  author: { type: "string" },
15176
+ output: { type: "string", short: "o" },
14900
15177
  "dry-run": { type: "boolean" },
14901
15178
  help: { type: "boolean", short: "h" }
14902
15179
  }
@@ -14927,38 +15204,26 @@ async function run(args) {
14927
15204
  }
14928
15205
  throw error;
14929
15206
  }
14930
- const blockId = locator.kind === "block" ? locator.blockId : locator.kind === "blockSpan" ? locator.blockId : null;
14931
- if (!blockId?.startsWith("p")) {
14932
- return fail("INVALID_LOCATOR", "comments add supports paragraph locators only (pN or pN:start-end)", "Cross-block ranges and table-cell anchors are not yet supported.");
14933
- }
14934
- let view;
14935
- try {
14936
- view = await openDocView(path);
14937
- } catch (openError) {
14938
- if (openError instanceof PkgError) {
14939
- if (openError.code === "FILE_NOT_FOUND") {
14940
- return fail("FILE_NOT_FOUND", openError.message);
14941
- }
14942
- if (openError.code === "NOT_A_ZIP") {
14943
- return fail("NOT_A_ZIP", openError.message);
14944
- }
14945
- }
14946
- throw openError;
14947
- }
14948
- let paragraphRef;
14949
- try {
14950
- paragraphRef = resolveBlock(view, blockId);
14951
- } catch (error) {
14952
- if (error instanceof LocatorResolveError) {
14953
- return fail("BLOCK_NOT_FOUND", error.message);
14954
- }
14955
- throw error;
14956
- }
14957
- const span = locator.kind === "blockSpan" ? { start: locator.start, end: locator.end } : undefined;
15207
+ if (locator.kind === "range") {
15208
+ return runCrossBlock(parsed, path, rangeInput, locator, text);
15209
+ }
15210
+ const target = locatorToBlockTarget(locator);
15211
+ if (!target) {
15212
+ return fail("INVALID_LOCATOR", "comments add supports paragraph locators only (pN, pN:start-end, pN:S-pM:E, or tT:rRcC:pK[:start-end])", "Comments and images are not valid anchors.");
15213
+ }
15214
+ const blockId = target.blockId;
15215
+ const view = await openOrFail(path);
15216
+ if (typeof view === "number")
15217
+ return view;
15218
+ const paragraphRef = await resolveBlockOrFail(view, blockId);
15219
+ if (typeof paragraphRef === "number")
15220
+ return paragraphRef;
15221
+ const span = target.span;
14958
15222
  const author = parsed.values.author ?? Bun.env.DOCX_AUTHOR ?? "";
14959
15223
  const date = new Date().toISOString();
14960
15224
  const numericId = nextCommentId(view);
14961
15225
  const paraId = generateParaId();
15226
+ const outputPath = parsed.values.output;
14962
15227
  if (parsed.values["dry-run"]) {
14963
15228
  await respond({
14964
15229
  ok: true,
@@ -14966,7 +15231,8 @@ async function run(args) {
14966
15231
  dryRun: true,
14967
15232
  path,
14968
15233
  commentId: `c${numericId}`,
14969
- locator: rangeInput
15234
+ locator: rangeInput,
15235
+ ...outputPath ? { output: outputPath } : {}
14970
15236
  });
14971
15237
  return EXIT.OK;
14972
15238
  }
@@ -14989,34 +15255,96 @@ async function run(args) {
14989
15255
  text
14990
15256
  }
14991
15257
  }, undefined, false, undefined, this));
14992
- await saveDocView(view);
15258
+ await saveDocView(view, outputPath);
14993
15259
  await respond({
14994
15260
  ok: true,
14995
15261
  operation: "comments.add",
14996
- path,
15262
+ path: outputPath ?? path,
14997
15263
  commentId: `c${numericId}`,
14998
15264
  locator: rangeInput
14999
15265
  });
15000
15266
  return EXIT.OK;
15001
15267
  }
15002
- var HELP = `docx comments add \u2014 anchor a new comment to a locator
15003
-
15004
- Usage:
15005
- docx comments add FILE [options]
15006
-
15007
- Required:
15008
- --range LOCATOR Where to anchor (e.g., p3 for the whole paragraph,
15009
- p3:5-20 to comment on chars 5..20 of p3)
15010
- --text TEXT Comment body
15011
-
15268
+ async function runCrossBlock(parsed, path, rangeInput, locator, text) {
15269
+ const view = await openOrFail(path);
15270
+ if (typeof view === "number")
15271
+ return view;
15272
+ const startRef = await resolveBlockOrFail(view, locator.start.blockId);
15273
+ if (typeof startRef === "number")
15274
+ return startRef;
15275
+ const endRef = await resolveBlockOrFail(view, locator.end.blockId);
15276
+ if (typeof endRef === "number")
15277
+ return endRef;
15278
+ const author = parsed.values.author ?? Bun.env.DOCX_AUTHOR ?? "";
15279
+ const date = new Date().toISOString();
15280
+ const numericId = nextCommentId(view);
15281
+ const paraId = generateParaId();
15282
+ const outputPath = parsed.values.output;
15283
+ if (parsed.values["dry-run"]) {
15284
+ await respond({
15285
+ ok: true,
15286
+ operation: "comments.add",
15287
+ dryRun: true,
15288
+ path,
15289
+ commentId: `c${numericId}`,
15290
+ locator: rangeInput,
15291
+ ...outputPath ? { output: outputPath } : {}
15292
+ });
15293
+ return EXIT.OK;
15294
+ }
15295
+ try {
15296
+ addCommentRangeMarkers(startRef.node, locator.start.offset, endRef.node, locator.end.offset, numericId);
15297
+ } catch (error) {
15298
+ if (error instanceof SpanOutOfRangeError) {
15299
+ return fail("INVALID_LOCATOR", error.message);
15300
+ }
15301
+ throw error;
15302
+ }
15303
+ const commentsRoot = ensureCommentsPart(view);
15304
+ commentsRoot.children.push(/* @__PURE__ */ jsxDEV(CommentBody, {
15305
+ options: {
15306
+ id: numericId,
15307
+ author,
15308
+ date,
15309
+ initials: authorInitials(author),
15310
+ paraId,
15311
+ text
15312
+ }
15313
+ }, undefined, false, undefined, this));
15314
+ await saveDocView(view, outputPath);
15315
+ await respond({
15316
+ ok: true,
15317
+ operation: "comments.add",
15318
+ path: outputPath ?? path,
15319
+ commentId: `c${numericId}`,
15320
+ locator: rangeInput
15321
+ });
15322
+ return EXIT.OK;
15323
+ }
15324
+ var HELP = `docx comments add \u2014 anchor a new comment to a locator
15325
+
15326
+ Usage:
15327
+ docx comments add FILE [options]
15328
+
15329
+ Required:
15330
+ --range LOCATOR Where to anchor. Supports:
15331
+ pN whole paragraph
15332
+ pN:S-E chars S..E of pN
15333
+ pN:S-pM:E chars S of pN through char E of pM (cross-paragraph)
15334
+ tT:rRcC:pK whole cell paragraph
15335
+ tT:rRcC:pK:S-E chars S..E of cell paragraph
15336
+ --text TEXT Comment body
15337
+
15012
15338
  Optional:
15013
15339
  --author NAME Author name (default: $DOCX_AUTHOR)
15340
+ -o, --output PATH Write to PATH instead of overwriting FILE
15014
15341
  --dry-run Print what would be added; do not write the file
15015
15342
  -h, --help Show this help
15016
15343
 
15017
15344
  Examples:
15018
15345
  docx comments add doc.docx --range p3 --text "Reconsider this paragraph"
15019
15346
  docx comments add doc.docx --range p3:5-20 --text "Sharper wording?" --author "Jane"
15347
+ docx comments add doc.docx --range p3:5-p5:10 --text "Whole section?" --author "Reviewer"
15020
15348
  `;
15021
15349
  var init_add = __esm(() => {
15022
15350
  init_core();
@@ -15025,53 +15353,6 @@ var init_add = __esm(() => {
15025
15353
  init_jsx_dev_runtime();
15026
15354
  });
15027
15355
 
15028
- // src/cli/comments/trash.ts
15029
- import { dirname, join } from "path";
15030
- function trashPathFor(docPath) {
15031
- return join(dirname(docPath), TRASH_DIR, TRASH_FILE);
15032
- }
15033
- async function readTrash(docPath) {
15034
- const path = trashPathFor(docPath);
15035
- const file = Bun.file(path);
15036
- if (!await file.exists()) {
15037
- return { version: TRASH_VERSION, entries: [] };
15038
- }
15039
- const parsed = await file.json();
15040
- if (parsed.version !== TRASH_VERSION) {
15041
- return { version: TRASH_VERSION, entries: [] };
15042
- }
15043
- return parsed;
15044
- }
15045
- async function writeTrash(docPath, trash) {
15046
- const path = trashPathFor(docPath);
15047
- await Bun.write(path, `${JSON.stringify(trash, null, 2)}
15048
- `);
15049
- }
15050
- async function pushTrashEntry(docPath, entry) {
15051
- const trash = await readTrash(docPath);
15052
- trash.entries.push(entry);
15053
- await writeTrash(docPath, trash);
15054
- }
15055
- async function popTrashEntry(docPath, commentId) {
15056
- const trash = await readTrash(docPath);
15057
- const fileName = docPath.split("/").pop() ?? docPath;
15058
- for (let index = trash.entries.length - 1;index >= 0; index--) {
15059
- const entry = trash.entries[index];
15060
- if (!entry)
15061
- continue;
15062
- if (entry.file !== fileName)
15063
- continue;
15064
- if (entry.commentId !== commentId)
15065
- continue;
15066
- trash.entries.splice(index, 1);
15067
- await writeTrash(docPath, trash);
15068
- return entry;
15069
- }
15070
- return;
15071
- }
15072
- var TRASH_VERSION = 1, TRASH_DIR = ".docx-cli", TRASH_FILE = "trash.json";
15073
- var init_trash = () => {};
15074
-
15075
15356
  // src/cli/comments/delete.ts
15076
15357
  var exports_delete = {};
15077
15358
  __export(exports_delete, {
@@ -15086,6 +15367,7 @@ async function run2(args) {
15086
15367
  allowPositionals: true,
15087
15368
  options: {
15088
15369
  id: { type: "string" },
15370
+ output: { type: "string", short: "o" },
15089
15371
  "dry-run": { type: "boolean" },
15090
15372
  help: { type: "boolean", short: "h" }
15091
15373
  }
@@ -15106,49 +15388,26 @@ async function run2(args) {
15106
15388
  return fail("USAGE", "Missing --id COMMENT_ID", HELP2);
15107
15389
  const numericId = idInput.startsWith("c") ? idInput.slice(1) : idInput;
15108
15390
  const commentId = `c${numericId}`;
15109
- let view;
15110
- try {
15111
- view = await openDocView(path);
15112
- } catch (openError) {
15113
- if (openError instanceof PkgError) {
15114
- if (openError.code === "FILE_NOT_FOUND") {
15115
- return fail("FILE_NOT_FOUND", openError.message);
15116
- }
15117
- if (openError.code === "NOT_A_ZIP") {
15118
- return fail("NOT_A_ZIP", openError.message);
15119
- }
15120
- }
15121
- throw openError;
15122
- }
15391
+ const view = await openOrFail(path);
15392
+ if (typeof view === "number")
15393
+ return view;
15123
15394
  const commentReference = findCommentByNumericId(view, numericId);
15124
15395
  if (!commentReference) {
15125
15396
  return fail("COMMENT_NOT_FOUND", `Comment not found: ${commentId}`);
15126
15397
  }
15127
- const anchor = view.doc.comments.find((c) => c.id === commentId)?.anchor;
15128
- if (!anchor) {
15129
- return fail("COMMENT_NOT_FOUND", `Anchor not found for ${commentId}`);
15130
- }
15398
+ const outputPath = parsed.values.output;
15131
15399
  if (parsed.values["dry-run"]) {
15132
15400
  await respond({
15133
15401
  ok: true,
15134
15402
  operation: "comments.delete",
15135
15403
  dryRun: true,
15136
15404
  path,
15137
- commentId
15405
+ commentId,
15406
+ ...outputPath ? { output: outputPath } : {}
15138
15407
  });
15139
15408
  return EXIT.OK;
15140
15409
  }
15141
- const commentXml = XmlNode2.serialize([commentReference.node]);
15142
15410
  const paraId = findCommentParaId(view, commentId);
15143
- const extXml = paraId ? extractExtEntryXml(view, paraId) : null;
15144
- await pushTrashEntry(path, {
15145
- file: path.split("/").pop() ?? path,
15146
- deletedAt: new Date().toISOString(),
15147
- commentId,
15148
- anchor,
15149
- commentXml,
15150
- extXml
15151
- });
15152
15411
  const commentIndex = commentReference.parent.indexOf(commentReference.node);
15153
15412
  if (commentIndex !== -1)
15154
15413
  commentReference.parent.splice(commentIndex, 1);
@@ -15159,28 +15418,15 @@ async function run2(args) {
15159
15418
  }
15160
15419
  }
15161
15420
  removeCommentMarkers(view.documentTree, numericId);
15162
- await saveDocView(view);
15421
+ await saveDocView(view, outputPath);
15163
15422
  await respond({
15164
15423
  ok: true,
15165
15424
  operation: "comments.delete",
15166
- path,
15425
+ path: outputPath ?? path,
15167
15426
  commentId
15168
15427
  });
15169
15428
  return EXIT.OK;
15170
15429
  }
15171
- function extractExtEntryXml(view, paraId) {
15172
- if (!view.commentsExtTree)
15173
- return null;
15174
- const root = XmlNode2.findRoot(view.commentsExtTree, "w15:commentsEx");
15175
- if (!root)
15176
- return null;
15177
- for (const child of root.children) {
15178
- if (child.tag === "w15:commentEx" && child.getAttribute("w15:paraId") === paraId) {
15179
- return XmlNode2.serialize([child]);
15180
- }
15181
- }
15182
- return null;
15183
- }
15184
15430
  var HELP2 = `docx comments delete \u2014 remove a comment
15185
15431
 
15186
15432
  Usage:
@@ -15190,12 +15436,10 @@ Required:
15190
15436
  --id ID Comment id (e.g., c0)
15191
15437
 
15192
15438
  Optional:
15439
+ -o, --output PATH Write to PATH instead of overwriting FILE
15193
15440
  --dry-run Print what would be removed; do not write the file
15194
15441
  -h, --help Show this help
15195
15442
 
15196
- The deleted comment is journaled to <dir>/.docx-cli/trash.json so it can
15197
- be brought back via "docx comments restore".
15198
-
15199
15443
  Examples:
15200
15444
  docx comments delete doc.docx --id c2
15201
15445
  `;
@@ -15204,7 +15448,6 @@ var init_delete = __esm(() => {
15204
15448
  init_parser();
15205
15449
  init_respond();
15206
15450
  init_helpers();
15207
- init_trash();
15208
15451
  });
15209
15452
 
15210
15453
  // src/cli/comments/list.ts
@@ -15236,20 +15479,9 @@ async function run3(args) {
15236
15479
  const path = parsed.positionals[0];
15237
15480
  if (!path)
15238
15481
  return fail("USAGE", "Missing FILE argument", HELP3);
15239
- let view;
15240
- try {
15241
- view = await openDocView(path);
15242
- } catch (openError) {
15243
- if (openError instanceof PkgError) {
15244
- if (openError.code === "FILE_NOT_FOUND") {
15245
- return fail("FILE_NOT_FOUND", openError.message);
15246
- }
15247
- if (openError.code === "NOT_A_ZIP") {
15248
- return fail("NOT_A_ZIP", openError.message);
15249
- }
15250
- }
15251
- throw openError;
15252
- }
15482
+ const view = await openOrFail(path);
15483
+ if (typeof view === "number")
15484
+ return view;
15253
15485
  let comments = view.doc.comments;
15254
15486
  if (!parsed.values["include-resolved"]) {
15255
15487
  comments = comments.filter((comment) => !comment.resolved);
@@ -15293,7 +15525,6 @@ Examples:
15293
15525
  docx comments list doc.docx --include-resolved | jq '.[] | select(.author == "Jane")'
15294
15526
  `;
15295
15527
  var init_list = __esm(() => {
15296
- init_core();
15297
15528
  init_respond();
15298
15529
  });
15299
15530
 
@@ -15313,6 +15544,7 @@ async function run4(args) {
15313
15544
  to: { type: "string" },
15314
15545
  text: { type: "string" },
15315
15546
  author: { type: "string" },
15547
+ output: { type: "string", short: "o" },
15316
15548
  "dry-run": { type: "boolean" },
15317
15549
  help: { type: "boolean", short: "h" }
15318
15550
  }
@@ -15335,20 +15567,9 @@ async function run4(args) {
15335
15567
  if (!text)
15336
15568
  return fail("USAGE", "Missing --text TEXT", HELP4);
15337
15569
  const parentNumericId = parentInput.startsWith("c") ? parentInput.slice(1) : parentInput;
15338
- let view;
15339
- try {
15340
- view = await openDocView(path);
15341
- } catch (openError) {
15342
- if (openError instanceof PkgError) {
15343
- if (openError.code === "FILE_NOT_FOUND") {
15344
- return fail("FILE_NOT_FOUND", openError.message);
15345
- }
15346
- if (openError.code === "NOT_A_ZIP") {
15347
- return fail("NOT_A_ZIP", openError.message);
15348
- }
15349
- }
15350
- throw openError;
15351
- }
15570
+ const view = await openOrFail(path);
15571
+ if (typeof view === "number")
15572
+ return view;
15352
15573
  const parentReference = findCommentByNumericId(view, parentNumericId);
15353
15574
  if (!parentReference) {
15354
15575
  return fail("COMMENT_NOT_FOUND", `Parent comment not found: c${parentNumericId}`);
@@ -15361,6 +15582,7 @@ async function run4(args) {
15361
15582
  const date = new Date().toISOString();
15362
15583
  const numericId = nextCommentId(view);
15363
15584
  const replyParaId = generateParaId();
15585
+ const outputPath = parsed.values.output;
15364
15586
  if (parsed.values["dry-run"]) {
15365
15587
  await respond({
15366
15588
  ok: true,
@@ -15368,7 +15590,8 @@ async function run4(args) {
15368
15590
  dryRun: true,
15369
15591
  path,
15370
15592
  commentId: `c${numericId}`,
15371
- parentId: `c${parentNumericId}`
15593
+ parentId: `c${parentNumericId}`,
15594
+ ...outputPath ? { output: outputPath } : {}
15372
15595
  });
15373
15596
  return EXIT.OK;
15374
15597
  }
@@ -15389,11 +15612,11 @@ async function run4(args) {
15389
15612
  "w15:paraIdParent": parentParaId,
15390
15613
  "w15:done": "0"
15391
15614
  }));
15392
- await saveDocView(view);
15615
+ await saveDocView(view, outputPath);
15393
15616
  await respond({
15394
15617
  ok: true,
15395
15618
  operation: "comments.reply",
15396
- path,
15619
+ path: outputPath ?? path,
15397
15620
  commentId: `c${numericId}`,
15398
15621
  parentId: `c${parentNumericId}`
15399
15622
  });
@@ -15410,6 +15633,7 @@ Required:
15410
15633
 
15411
15634
  Optional:
15412
15635
  --author NAME Author name (default: $DOCX_AUTHOR)
15636
+ -o, --output PATH Write to PATH instead of overwriting FILE
15413
15637
  --dry-run Print what would be added; do not write the file
15414
15638
  -h, --help Show this help
15415
15639
 
@@ -15439,6 +15663,7 @@ async function run5(args) {
15439
15663
  options: {
15440
15664
  id: { type: "string" },
15441
15665
  unset: { type: "boolean" },
15666
+ output: { type: "string", short: "o" },
15442
15667
  "dry-run": { type: "boolean" },
15443
15668
  help: { type: "boolean", short: "h" }
15444
15669
  }
@@ -15459,20 +15684,9 @@ async function run5(args) {
15459
15684
  return fail("USAGE", "Missing --id COMMENT_ID", HELP5);
15460
15685
  const numericId = idInput.startsWith("c") ? idInput.slice(1) : idInput;
15461
15686
  const resolved = !parsed.values.unset;
15462
- let view;
15463
- try {
15464
- view = await openDocView(path);
15465
- } catch (openError) {
15466
- if (openError instanceof PkgError) {
15467
- if (openError.code === "FILE_NOT_FOUND") {
15468
- return fail("FILE_NOT_FOUND", openError.message);
15469
- }
15470
- if (openError.code === "NOT_A_ZIP") {
15471
- return fail("NOT_A_ZIP", openError.message);
15472
- }
15473
- }
15474
- throw openError;
15475
- }
15687
+ const view = await openOrFail(path);
15688
+ if (typeof view === "number")
15689
+ return view;
15476
15690
  const commentReference = findCommentByNumericId(view, numericId);
15477
15691
  if (!commentReference) {
15478
15692
  return fail("COMMENT_NOT_FOUND", `Comment not found: c${numericId}`);
@@ -15481,6 +15695,7 @@ async function run5(args) {
15481
15695
  if (!paraId) {
15482
15696
  return fail("COMMENT_NOT_FOUND", `Comment c${numericId} could not be assigned a w14:paraId.`);
15483
15697
  }
15698
+ const outputPath = parsed.values.output;
15484
15699
  if (parsed.values["dry-run"]) {
15485
15700
  await respond({
15486
15701
  ok: true,
@@ -15488,7 +15703,8 @@ async function run5(args) {
15488
15703
  dryRun: true,
15489
15704
  path,
15490
15705
  commentId: `c${numericId}`,
15491
- resolved
15706
+ resolved,
15707
+ ...outputPath ? { output: outputPath } : {}
15492
15708
  });
15493
15709
  return EXIT.OK;
15494
15710
  }
@@ -15502,11 +15718,11 @@ async function run5(args) {
15502
15718
  entry.setAttribute("w15:done", "1");
15503
15719
  else
15504
15720
  delete entry.attributes["w15:done"];
15505
- await saveDocView(view);
15721
+ await saveDocView(view, outputPath);
15506
15722
  await respond({
15507
15723
  ok: true,
15508
15724
  operation: "comments.resolve",
15509
- path,
15725
+ path: outputPath ?? path,
15510
15726
  commentId: `c${numericId}`,
15511
15727
  resolved
15512
15728
  });
@@ -15522,6 +15738,7 @@ Required:
15522
15738
 
15523
15739
  Optional:
15524
15740
  --unset Mark unresolved instead of resolved
15741
+ -o, --output PATH Write to PATH instead of overwriting FILE
15525
15742
  --dry-run Print what would change; do not write the file
15526
15743
  -h, --help Show this help
15527
15744
 
@@ -15536,147 +15753,15 @@ var init_resolve2 = __esm(() => {
15536
15753
  init_helpers();
15537
15754
  });
15538
15755
 
15539
- // src/cli/comments/restore.ts
15540
- var exports_restore = {};
15541
- __export(exports_restore, {
15542
- run: () => run6
15543
- });
15544
- import { parseArgs as parseArgs6 } from "util";
15545
- async function run6(args) {
15546
- let parsed;
15547
- try {
15548
- parsed = parseArgs6({
15549
- args,
15550
- allowPositionals: true,
15551
- options: {
15552
- id: { type: "string" },
15553
- "dry-run": { type: "boolean" },
15554
- help: { type: "boolean", short: "h" }
15555
- }
15556
- });
15557
- } catch (parseError) {
15558
- const message = parseError instanceof Error ? parseError.message : String(parseError);
15559
- return fail("USAGE", message, HELP6);
15560
- }
15561
- if (parsed.values.help) {
15562
- await writeStdout(HELP6);
15563
- return EXIT.OK;
15564
- }
15565
- const path = parsed.positionals[0];
15566
- if (!path)
15567
- return fail("USAGE", "Missing FILE argument", HELP6);
15568
- const idInput = parsed.values.id;
15569
- if (!idInput)
15570
- return fail("USAGE", "Missing --id COMMENT_ID", HELP6);
15571
- const commentId = idInput.startsWith("c") ? idInput : `c${idInput}`;
15572
- const numericId = commentId.slice(1);
15573
- if (parsed.values["dry-run"]) {
15574
- await respond({
15575
- ok: true,
15576
- operation: "comments.restore",
15577
- dryRun: true,
15578
- path,
15579
- commentId
15580
- });
15581
- return EXIT.OK;
15582
- }
15583
- const entry = await popTrashEntry(path, commentId);
15584
- if (!entry) {
15585
- 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.");
15586
- }
15587
- if (entry.anchor.startBlockId !== entry.anchor.endBlockId) {
15588
- return fail("USAGE", "Cross-block comment restore is not yet supported", "v1 supports restoring single-paragraph comment anchors only.");
15589
- }
15590
- let view;
15591
- try {
15592
- view = await openDocView(path);
15593
- } catch (openError) {
15594
- if (openError instanceof PkgError) {
15595
- if (openError.code === "FILE_NOT_FOUND") {
15596
- return fail("FILE_NOT_FOUND", openError.message);
15597
- }
15598
- if (openError.code === "NOT_A_ZIP") {
15599
- return fail("NOT_A_ZIP", openError.message);
15600
- }
15601
- }
15602
- throw openError;
15603
- }
15604
- const blockId = entry.anchor.startBlockId;
15605
- const block = view.blockReferences.get(blockId);
15606
- if (!block) {
15607
- return fail("BLOCK_NOT_FOUND", `Original anchor block ${blockId} no longer exists`);
15608
- }
15609
- const span = {
15610
- start: entry.anchor.startOffset,
15611
- end: entry.anchor.endOffset
15612
- };
15613
- try {
15614
- addCommentMarkersToParagraph(block.node, numericId, span);
15615
- } catch (error) {
15616
- if (error instanceof SpanOutOfRangeError) {
15617
- return fail("INVALID_LOCATOR", `Saved span no longer fits the block: ${error.message}`);
15618
- }
15619
- throw error;
15620
- }
15621
- const commentNodes = XmlNode2.parse(entry.commentXml);
15622
- const commentNode = commentNodes[0];
15623
- if (!commentNode) {
15624
- return fail("USAGE", "Trashed commentXml is empty");
15625
- }
15626
- const commentsRoot = ensureCommentsPart(view);
15627
- commentsRoot.children.push(commentNode);
15628
- if (entry.extXml) {
15629
- const extNodes = XmlNode2.parse(entry.extXml);
15630
- const extNode = extNodes[0];
15631
- if (extNode) {
15632
- const extRoot = ensureCommentsExtPart(view);
15633
- extRoot.children.push(extNode);
15634
- }
15635
- }
15636
- await saveDocView(view);
15637
- await respond({
15638
- ok: true,
15639
- operation: "comments.restore",
15640
- path,
15641
- commentId
15642
- });
15643
- return EXIT.OK;
15644
- }
15645
- var HELP6 = `docx comments restore \u2014 undo a recent delete
15646
-
15647
- Usage:
15648
- docx comments restore FILE --id cN [options]
15649
-
15650
- Required:
15651
- --id ID Comment id to restore (e.g., c0)
15652
-
15653
- Optional:
15654
- --dry-run Print what would be restored; do not write the file
15655
- -h, --help Show this help
15656
-
15657
- Pulls the most recent matching entry from <dir>/.docx-cli/trash.json
15658
- and re-anchors the comment at its original location.
15659
-
15660
- Examples:
15661
- docx comments restore doc.docx --id c2
15662
- `;
15663
- var init_restore = __esm(() => {
15664
- init_core();
15665
- init_parser();
15666
- init_respond();
15667
- init_helpers();
15668
- init_trash();
15669
- });
15670
-
15671
15756
  // src/cli/comments/index.ts
15672
15757
  var exports_comments = {};
15673
15758
  __export(exports_comments, {
15674
- run: () => run7
15759
+ run: () => run6
15675
15760
  });
15676
- async function run7(args) {
15761
+ async function run6(args) {
15677
15762
  const verb = args[0];
15678
15763
  if (!verb || verb === "--help" || verb === "-h" || verb === "help") {
15679
- await writeStdout(HELP7);
15764
+ await writeStdout(HELP6);
15680
15765
  return verb ? 0 : 2;
15681
15766
  }
15682
15767
  const loader = SUBCOMMANDS[verb];
@@ -15686,7 +15771,7 @@ async function run7(args) {
15686
15771
  const module_ = await loader();
15687
15772
  return module_.run(args.slice(1));
15688
15773
  }
15689
- var SUBCOMMANDS, HELP7 = `docx comments \u2014 manage Word comments
15774
+ var SUBCOMMANDS, HELP6 = `docx comments \u2014 manage Word comments
15690
15775
 
15691
15776
  Usage:
15692
15777
  docx comments <verb> FILE [options]
@@ -15697,7 +15782,6 @@ Verbs:
15697
15782
  list Print existing comments as JSON
15698
15783
  resolve Mark a comment resolved
15699
15784
  delete Remove a comment
15700
- restore Restore a recently deleted comment
15701
15785
 
15702
15786
  Run "docx comments <verb> --help" for verb-specific help.
15703
15787
  `;
@@ -15708,8 +15792,7 @@ var init_comments = __esm(() => {
15708
15792
  delete: () => Promise.resolve().then(() => (init_delete(), exports_delete)),
15709
15793
  list: () => Promise.resolve().then(() => (init_list(), exports_list)),
15710
15794
  reply: () => Promise.resolve().then(() => (init_reply(), exports_reply)),
15711
- resolve: () => Promise.resolve().then(() => (init_resolve2(), exports_resolve)),
15712
- restore: () => Promise.resolve().then(() => (init_restore(), exports_restore))
15795
+ resolve: () => Promise.resolve().then(() => (init_resolve2(), exports_resolve))
15713
15796
  };
15714
15797
  });
15715
15798
 
@@ -15805,13 +15888,13 @@ var init_template = __esm(() => {
15805
15888
  // src/cli/create/index.tsx
15806
15889
  var exports_create = {};
15807
15890
  __export(exports_create, {
15808
- run: () => run8
15891
+ run: () => run7
15809
15892
  });
15810
- import { parseArgs as parseArgs7 } from "util";
15811
- async function run8(args) {
15893
+ import { parseArgs as parseArgs6 } from "util";
15894
+ async function run7(args) {
15812
15895
  let parsed;
15813
15896
  try {
15814
- parsed = parseArgs7({
15897
+ parsed = parseArgs6({
15815
15898
  args,
15816
15899
  allowPositionals: true,
15817
15900
  options: {
@@ -15823,20 +15906,20 @@ async function run8(args) {
15823
15906
  }
15824
15907
  });
15825
15908
  } catch (e) {
15826
- return fail("USAGE", e instanceof Error ? e.message : String(e), HELP8);
15909
+ return fail("USAGE", e instanceof Error ? e.message : String(e), HELP7);
15827
15910
  }
15828
15911
  if (parsed.values.help) {
15829
- await writeStdout(HELP8);
15912
+ await writeStdout(HELP7);
15830
15913
  return EXIT.OK;
15831
15914
  }
15832
15915
  const path = parsed.positionals[0];
15833
15916
  if (!path) {
15834
- return fail("USAGE", "Missing FILE argument", HELP8);
15917
+ return fail("USAGE", "Missing FILE argument", HELP7);
15835
15918
  }
15836
15919
  if (await Bun.file(path).exists() && !parsed.values.force) {
15837
15920
  return fail("USAGE", `File already exists: ${path}`, "Pass --force to overwrite.");
15838
15921
  }
15839
- const author = parsed.values.author ?? process.env.DOCX_AUTHOR ?? "";
15922
+ const author = parsed.values.author ?? Bun.env.DOCX_AUTHOR ?? "";
15840
15923
  const title = parsed.values.title ?? "";
15841
15924
  const text = parsed.values.text;
15842
15925
  const now = new Date().toISOString();
@@ -15851,7 +15934,7 @@ async function run8(args) {
15851
15934
  compression: "DEFLATE",
15852
15935
  compressionOptions: { level: 6 }
15853
15936
  });
15854
- await Bun.write(path, buf);
15937
+ await writeAtomic(path, buf);
15855
15938
  await respond({
15856
15939
  ok: true,
15857
15940
  operation: "create",
@@ -15861,7 +15944,7 @@ async function run8(args) {
15861
15944
  });
15862
15945
  return EXIT.OK;
15863
15946
  }
15864
- 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
15865
15948
 
15866
15949
  Usage:
15867
15950
  docx create FILE [options]
@@ -15878,86 +15961,116 @@ Examples:
15878
15961
  docx create out.docx --title "Spec" --author "Claude" --text "First paragraph."
15879
15962
  `;
15880
15963
  var init_create = __esm(() => {
15964
+ init_package();
15881
15965
  init_respond();
15882
15966
  init_template();
15883
15967
  import_jszip2 = __toESM(require_lib3(), 1);
15884
15968
  });
15885
15969
 
15886
- // src/cli/delete/index.ts
15970
+ // src/cli/delete/index.tsx
15887
15971
  var exports_delete2 = {};
15888
15972
  __export(exports_delete2, {
15889
- run: () => run9
15973
+ run: () => run8
15890
15974
  });
15891
- import { parseArgs as parseArgs8 } from "util";
15892
- async function run9(args) {
15975
+ import { parseArgs as parseArgs7 } from "util";
15976
+ async function run8(args) {
15893
15977
  let parsed;
15894
15978
  try {
15895
- parsed = parseArgs8({
15979
+ parsed = parseArgs7({
15896
15980
  args,
15897
15981
  allowPositionals: true,
15898
15982
  options: {
15899
15983
  at: { type: "string" },
15984
+ output: { type: "string", short: "o" },
15900
15985
  "dry-run": { type: "boolean" },
15901
15986
  help: { type: "boolean", short: "h" }
15902
15987
  }
15903
15988
  });
15904
15989
  } catch (parseError) {
15905
15990
  const message = parseError instanceof Error ? parseError.message : String(parseError);
15906
- return fail("USAGE", message, HELP9);
15991
+ return fail("USAGE", message, HELP8);
15907
15992
  }
15908
15993
  if (parsed.values.help) {
15909
- await writeStdout(HELP9);
15994
+ await writeStdout(HELP8);
15910
15995
  return EXIT.OK;
15911
15996
  }
15912
15997
  const path = parsed.positionals[0];
15913
15998
  if (!path)
15914
- return fail("USAGE", "Missing FILE argument", HELP9);
15999
+ return fail("USAGE", "Missing FILE argument", HELP8);
15915
16000
  const at = parsed.values.at;
15916
16001
  if (!at)
15917
- return fail("USAGE", "Missing --at LOCATOR", HELP9);
15918
- let view;
15919
- try {
15920
- view = await openDocView(path);
15921
- } catch (openError) {
15922
- if (openError instanceof PkgError) {
15923
- if (openError.code === "FILE_NOT_FOUND") {
15924
- return fail("FILE_NOT_FOUND", openError.message);
15925
- }
15926
- if (openError.code === "NOT_A_ZIP") {
15927
- return fail("NOT_A_ZIP", openError.message);
15928
- }
15929
- }
15930
- throw openError;
15931
- }
15932
- let blockRef;
15933
- try {
15934
- blockRef = resolveBlock(view, at);
15935
- } catch (resolveError) {
15936
- if (resolveError instanceof LocatorResolveError) {
15937
- return fail("BLOCK_NOT_FOUND", resolveError.message);
15938
- }
15939
- throw resolveError;
15940
- }
16002
+ return fail("USAGE", "Missing --at LOCATOR", HELP8);
16003
+ const view = await openOrFail(path);
16004
+ if (typeof view === "number")
16005
+ return view;
16006
+ const blockRef = await resolveBlockOrFail(view, at);
16007
+ if (typeof blockRef === "number")
16008
+ return blockRef;
15941
16009
  const targetIndex = blockRef.parent.indexOf(blockRef.node);
15942
16010
  if (targetIndex === -1) {
15943
16011
  return fail("BLOCK_NOT_FOUND", "Block reference is stale (parent does not contain it)");
15944
16012
  }
16013
+ const outputPath = parsed.values.output;
15945
16014
  if (parsed.values["dry-run"]) {
15946
16015
  await respond({
15947
16016
  ok: true,
15948
16017
  operation: "delete",
15949
16018
  dryRun: true,
15950
16019
  path,
15951
- locator: at
16020
+ locator: at,
16021
+ ...outputPath ? { output: outputPath } : {}
15952
16022
  });
15953
16023
  return EXIT.OK;
15954
16024
  }
15955
- blockRef.parent.splice(targetIndex, 1);
15956
- await saveDocView(view);
15957
- 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
+ });
15958
16040
  return EXIT.OK;
15959
16041
  }
15960
- 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
15961
16074
 
15962
16075
  Usage:
15963
16076
  docx delete FILE [options]
@@ -15965,6 +16078,7 @@ Usage:
15965
16078
  Locator (required):
15966
16079
  --at LOCATOR Block to remove (e.g., p3, t0)
15967
16080
 
16081
+ -o, --output PATH Write to PATH instead of overwriting FILE
15968
16082
  --dry-run Print what would be removed; do not write the file
15969
16083
  -h, --help Show this help
15970
16084
 
@@ -15975,6 +16089,7 @@ Examples:
15975
16089
  var init_delete2 = __esm(() => {
15976
16090
  init_core();
15977
16091
  init_respond();
16092
+ init_jsx_dev_runtime();
15978
16093
  });
15979
16094
 
15980
16095
  // src/cli/insert/emit.tsx
@@ -15986,68 +16101,68 @@ function Paragraph(props) {
15986
16101
  /* @__PURE__ */ jsxDEV(ParagraphProperties, {
15987
16102
  options: { style, alignment }
15988
16103
  }, undefined, false, undefined, this),
15989
- runs.map((run10) => /* @__PURE__ */ jsxDEV(RunElement, {
15990
- run: run10
16104
+ runs.map((run9) => /* @__PURE__ */ jsxDEV(RunElement, {
16105
+ run: run9
15991
16106
  }, undefined, false, undefined, this))
15992
16107
  ]
15993
16108
  }, undefined, true, undefined, this);
15994
16109
  }
15995
- function RunElement({ run: run10 }) {
15996
- if (run10.type === "text")
16110
+ function RunElement({ run: run9 }) {
16111
+ if (run9.type === "text")
15997
16112
  return /* @__PURE__ */ jsxDEV(TextRunElement, {
15998
- run: run10
16113
+ run: run9
15999
16114
  }, undefined, false, undefined, this);
16000
- if (run10.type === "break") {
16115
+ if (run9.type === "break") {
16001
16116
  return /* @__PURE__ */ jsxDEV(w.r, {
16002
16117
  children: /* @__PURE__ */ jsxDEV(w.br, {
16003
- "w-type": run10.kind === "line" ? undefined : run10.kind
16118
+ "w-type": run9.kind === "line" ? undefined : run9.kind
16004
16119
  }, undefined, false, undefined, this)
16005
16120
  }, undefined, false, undefined, this);
16006
16121
  }
16007
- if (run10.type === "tab") {
16122
+ if (run9.type === "tab") {
16008
16123
  return /* @__PURE__ */ jsxDEV(w.r, {
16009
16124
  children: /* @__PURE__ */ jsxDEV(w.tab, {}, undefined, false, undefined, this)
16010
16125
  }, undefined, false, undefined, this);
16011
16126
  }
16012
- 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.`);
16013
16128
  }
16014
- function TextRunElement({ run: run10 }) {
16129
+ function TextRunElement({ run: run9 }) {
16015
16130
  return /* @__PURE__ */ jsxDEV(w.r, {
16016
16131
  children: [
16017
16132
  /* @__PURE__ */ jsxDEV(RunProperties, {
16018
- run: run10
16133
+ run: run9
16019
16134
  }, undefined, false, undefined, this),
16020
16135
  /* @__PURE__ */ jsxDEV(w.t, {
16021
16136
  "xml:space": "preserve",
16022
- children: run10.text
16137
+ children: run9.text
16023
16138
  }, undefined, false, undefined, this)
16024
16139
  ]
16025
16140
  }, undefined, true, undefined, this);
16026
16141
  }
16027
- function RunProperties({ run: run10 }) {
16028
- const isEmpty = FORMATTING_KEYS.every((key) => run10[key] == null);
16142
+ function RunProperties({ run: run9 }) {
16143
+ const isEmpty = FORMATTING_KEYS.every((key) => run9[key] == null);
16029
16144
  if (isEmpty)
16030
16145
  return null;
16031
16146
  return /* @__PURE__ */ jsxDEV(w.rPr, {
16032
16147
  children: [
16033
- run10.color && /* @__PURE__ */ jsxDEV(w.color, {
16034
- "w-val": run10.color
16148
+ run9.color && /* @__PURE__ */ jsxDEV(w.color, {
16149
+ "w-val": run9.color
16035
16150
  }, undefined, false, undefined, this),
16036
- run10.highlight && /* @__PURE__ */ jsxDEV(w.highlight, {
16037
- "w-val": run10.highlight
16151
+ run9.highlight && /* @__PURE__ */ jsxDEV(w.highlight, {
16152
+ "w-val": run9.highlight
16038
16153
  }, undefined, false, undefined, this),
16039
- run10.bold && /* @__PURE__ */ jsxDEV(w.b, {}, undefined, false, undefined, this),
16040
- run10.italic && /* @__PURE__ */ jsxDEV(w.i, {}, undefined, false, undefined, this),
16041
- run10.underline && /* @__PURE__ */ jsxDEV(w.u, {
16042
- "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
16043
16158
  }, undefined, false, undefined, this),
16044
- run10.strike && /* @__PURE__ */ jsxDEV(w.strike, {}, undefined, false, undefined, this),
16045
- run10.font && /* @__PURE__ */ jsxDEV(w.rFonts, {
16046
- "w-ascii": run10.font,
16047
- "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
16048
16163
  }, undefined, false, undefined, this),
16049
- run10.sizeHalfPoints !== undefined && /* @__PURE__ */ jsxDEV(w.sz, {
16050
- "w-val": String(run10.sizeHalfPoints)
16164
+ run9.sizeHalfPoints !== undefined && /* @__PURE__ */ jsxDEV(w.sz, {
16165
+ "w-val": String(run9.sizeHalfPoints)
16051
16166
  }, undefined, false, undefined, this)
16052
16167
  ]
16053
16168
  }, undefined, true, undefined, this);
@@ -16079,7 +16194,7 @@ function textRunFromProps(props) {
16079
16194
  return { type: "text", text, ...formatting };
16080
16195
  }
16081
16196
  var FORMATTING_KEYS;
16082
- var init_emit = __esm(() => {
16197
+ var init_emit2 = __esm(() => {
16083
16198
  init_jsx();
16084
16199
  init_jsx_dev_runtime();
16085
16200
  FORMATTING_KEYS = [
@@ -16097,13 +16212,13 @@ var init_emit = __esm(() => {
16097
16212
  // src/cli/edit/index.tsx
16098
16213
  var exports_edit = {};
16099
16214
  __export(exports_edit, {
16100
- run: () => run10
16215
+ run: () => run9
16101
16216
  });
16102
- import { parseArgs as parseArgs9 } from "util";
16103
- async function run10(args) {
16217
+ import { parseArgs as parseArgs8 } from "util";
16218
+ async function run9(args) {
16104
16219
  let parsed;
16105
16220
  try {
16106
- parsed = parseArgs9({
16221
+ parsed = parseArgs8({
16107
16222
  args,
16108
16223
  allowPositionals: true,
16109
16224
  options: {
@@ -16115,31 +16230,32 @@ async function run10(args) {
16115
16230
  color: { type: "string" },
16116
16231
  bold: { type: "boolean" },
16117
16232
  italic: { type: "boolean" },
16233
+ output: { type: "string", short: "o" },
16118
16234
  "dry-run": { type: "boolean" },
16119
16235
  help: { type: "boolean", short: "h" }
16120
16236
  }
16121
16237
  });
16122
16238
  } catch (parseError) {
16123
16239
  const message = parseError instanceof Error ? parseError.message : String(parseError);
16124
- return fail("USAGE", message, HELP10);
16240
+ return fail("USAGE", message, HELP9);
16125
16241
  }
16126
16242
  if (parsed.values.help) {
16127
- await writeStdout(HELP10);
16243
+ await writeStdout(HELP9);
16128
16244
  return EXIT.OK;
16129
16245
  }
16130
16246
  const path = parsed.positionals[0];
16131
16247
  if (!path)
16132
- return fail("USAGE", "Missing FILE argument", HELP10);
16248
+ return fail("USAGE", "Missing FILE argument", HELP9);
16133
16249
  const at = parsed.values.at;
16134
16250
  if (!at)
16135
- return fail("USAGE", "Missing --at LOCATOR", HELP10);
16251
+ return fail("USAGE", "Missing --at LOCATOR", HELP9);
16136
16252
  const text = parsed.values.text;
16137
16253
  const runsJson = parsed.values.runs;
16138
16254
  if (!text && !runsJson) {
16139
- return fail("USAGE", "Missing content: pass --text or --runs", HELP10);
16255
+ return fail("USAGE", "Missing content: pass --text or --runs", HELP9);
16140
16256
  }
16141
16257
  if (text && runsJson) {
16142
- return fail("USAGE", "Pass either --text or --runs, not both", HELP10);
16258
+ return fail("USAGE", "Pass either --text or --runs, not both", HELP9);
16143
16259
  }
16144
16260
  const paragraphOptions = {};
16145
16261
  const styleValue = parsed.values.style;
@@ -16152,29 +16268,12 @@ async function run10(args) {
16152
16268
  }
16153
16269
  paragraphOptions.alignment = alignmentValue;
16154
16270
  }
16155
- let view;
16156
- try {
16157
- view = await openDocView(path);
16158
- } catch (openError) {
16159
- if (openError instanceof PkgError) {
16160
- if (openError.code === "FILE_NOT_FOUND") {
16161
- return fail("FILE_NOT_FOUND", openError.message);
16162
- }
16163
- if (openError.code === "NOT_A_ZIP") {
16164
- return fail("NOT_A_ZIP", openError.message);
16165
- }
16166
- }
16167
- throw openError;
16168
- }
16169
- let blockRef;
16170
- try {
16171
- blockRef = resolveBlock(view, at);
16172
- } catch (resolveError) {
16173
- if (resolveError instanceof LocatorResolveError) {
16174
- return fail("BLOCK_NOT_FOUND", resolveError.message);
16175
- }
16176
- throw resolveError;
16177
- }
16271
+ const view = await openOrFail(path);
16272
+ if (typeof view === "number")
16273
+ return view;
16274
+ const blockRef = await resolveBlockOrFail(view, at);
16275
+ if (typeof blockRef === "number")
16276
+ return blockRef;
16178
16277
  let paragraphNode;
16179
16278
  if (text !== undefined) {
16180
16279
  const color = parsed.values.color;
@@ -16205,22 +16304,81 @@ async function run10(args) {
16205
16304
  if (targetIndex === -1) {
16206
16305
  return fail("BLOCK_NOT_FOUND", "Block reference is stale (parent does not contain it)");
16207
16306
  }
16307
+ const outputPath = parsed.values.output;
16208
16308
  if (parsed.values["dry-run"]) {
16209
16309
  await respond({
16210
16310
  ok: true,
16211
16311
  operation: "edit",
16212
16312
  dryRun: true,
16213
16313
  path,
16214
- locator: at
16314
+ locator: at,
16315
+ ...outputPath ? { output: outputPath } : {}
16215
16316
  });
16216
16317
  return EXIT.OK;
16217
16318
  }
16218
- blockRef.parent.splice(targetIndex, 1, paragraphNode);
16219
- await saveDocView(view);
16220
- 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
+ });
16221
16331
  return EXIT.OK;
16222
16332
  }
16223
- 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
16224
16382
 
16225
16383
  Usage:
16226
16384
  docx edit FILE [options]
@@ -16241,6 +16399,7 @@ Run options (only with --text):
16241
16399
  --bold Bold
16242
16400
  --italic Italic
16243
16401
 
16402
+ -o, --output PATH Write to PATH instead of overwriting FILE
16244
16403
  --dry-run Print what would change; do not write the file
16245
16404
  -h, --help Show this help
16246
16405
 
@@ -16250,17 +16409,232 @@ Examples:
16250
16409
  `;
16251
16410
  var init_edit = __esm(() => {
16252
16411
  init_core();
16253
- init_emit();
16412
+ init_emit2();
16254
16413
  init_respond();
16255
16414
  init_jsx_dev_runtime();
16256
16415
  });
16257
16416
 
16417
+ // src/core/find/index.ts
16418
+ function findTextSpans(doc, query, options = {}) {
16419
+ const matcher = options.regex ? regexMatcher(query, options.ignoreCase ?? false) : literalMatcher(query, options.ignoreCase ?? false);
16420
+ const out = [];
16421
+ collectMatches(doc.blocks, matcher, out);
16422
+ return out;
16423
+ }
16424
+ function literalMatcher(query, ignoreCase) {
16425
+ if (query.length === 0) {
16426
+ throw new Error("query cannot be empty");
16427
+ }
16428
+ const needle = ignoreCase ? query.toLowerCase() : query;
16429
+ return (paragraphText) => {
16430
+ const haystack = ignoreCase ? paragraphText.toLowerCase() : paragraphText;
16431
+ const matches = [];
16432
+ let cursor = haystack.indexOf(needle);
16433
+ while (cursor !== -1) {
16434
+ matches.push({
16435
+ start: cursor,
16436
+ end: cursor + needle.length,
16437
+ text: paragraphText.slice(cursor, cursor + needle.length)
16438
+ });
16439
+ cursor = haystack.indexOf(needle, cursor + needle.length);
16440
+ }
16441
+ return matches;
16442
+ };
16443
+ }
16444
+ function regexMatcher(pattern, ignoreCase) {
16445
+ const flags = `g${ignoreCase ? "i" : ""}`;
16446
+ const regex = new RegExp(pattern, flags);
16447
+ return (paragraphText) => {
16448
+ const matches = [];
16449
+ regex.lastIndex = 0;
16450
+ let result = regex.exec(paragraphText);
16451
+ while (result !== null) {
16452
+ const matched = result[0];
16453
+ if (matched.length === 0) {
16454
+ regex.lastIndex += 1;
16455
+ result = regex.exec(paragraphText);
16456
+ continue;
16457
+ }
16458
+ matches.push({
16459
+ start: result.index,
16460
+ end: result.index + matched.length,
16461
+ text: matched
16462
+ });
16463
+ result = regex.exec(paragraphText);
16464
+ }
16465
+ return matches;
16466
+ };
16467
+ }
16468
+ function collectMatches(blocks, matcher, out) {
16469
+ for (const block of blocks) {
16470
+ if (block.type === "paragraph") {
16471
+ const paragraphText = block.runs.map((run10) => run10.type === "text" ? run10.text : "").join("");
16472
+ for (const span of matcher(paragraphText)) {
16473
+ const match = {
16474
+ blockId: block.id,
16475
+ start: span.start,
16476
+ end: span.end,
16477
+ text: span.text
16478
+ };
16479
+ const overlaps = trackedChangesOverlapping(block, span.start, span.end);
16480
+ if (overlaps.length > 0)
16481
+ match.trackedChanges = overlaps;
16482
+ out.push(match);
16483
+ }
16484
+ continue;
16485
+ }
16486
+ if (block.type === "table") {
16487
+ for (const row of block.rows) {
16488
+ for (const cell of row.cells) {
16489
+ collectMatches(cell.blocks, matcher, out);
16490
+ }
16491
+ }
16492
+ }
16493
+ }
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
+ }
16519
+
16520
+ // src/cli/find/index.ts
16521
+ var exports_find = {};
16522
+ __export(exports_find, {
16523
+ run: () => run10
16524
+ });
16525
+ import { parseArgs as parseArgs9 } from "util";
16526
+ async function run10(args) {
16527
+ let parsed;
16528
+ try {
16529
+ parsed = parseArgs9({
16530
+ args,
16531
+ allowPositionals: true,
16532
+ options: {
16533
+ regex: { type: "boolean" },
16534
+ "ignore-case": { type: "boolean" },
16535
+ all: { type: "boolean" },
16536
+ nth: { type: "string" },
16537
+ help: { type: "boolean", short: "h" }
16538
+ }
16539
+ });
16540
+ } catch (parseError) {
16541
+ const message = parseError instanceof Error ? parseError.message : String(parseError);
16542
+ return fail("USAGE", message, HELP10);
16543
+ }
16544
+ if (parsed.values.help) {
16545
+ await writeStdout(HELP10);
16546
+ return EXIT.OK;
16547
+ }
16548
+ const path = parsed.positionals[0];
16549
+ const query = parsed.positionals[1];
16550
+ if (!path)
16551
+ return fail("USAGE", "Missing FILE argument", HELP10);
16552
+ if (query == null)
16553
+ return fail("USAGE", "Missing QUERY argument", HELP10);
16554
+ const ignoreCase = Boolean(parsed.values["ignore-case"]);
16555
+ const useRegex = Boolean(parsed.values.regex);
16556
+ const wantAll = Boolean(parsed.values.all);
16557
+ const nthRaw = parsed.values.nth;
16558
+ const nth = nthRaw === undefined ? undefined : Number(nthRaw);
16559
+ if (nth !== undefined && (!Number.isInteger(nth) || nth < 0)) {
16560
+ return fail("USAGE", `--nth must be a non-negative integer, got "${nthRaw}"`);
16561
+ }
16562
+ const view = await openOrFail(path);
16563
+ if (typeof view === "number")
16564
+ return view;
16565
+ let allMatches;
16566
+ try {
16567
+ allMatches = findTextSpans(view.doc, query, {
16568
+ regex: useRegex,
16569
+ ignoreCase
16570
+ });
16571
+ } catch (matcherError) {
16572
+ const message = matcherError instanceof Error ? matcherError.message : String(matcherError);
16573
+ return fail("USAGE", `Invalid query: ${message}`);
16574
+ }
16575
+ let selected;
16576
+ if (nth !== undefined) {
16577
+ const single = allMatches[nth];
16578
+ if (!single) {
16579
+ return fail("MATCH_NOT_FOUND", `Only ${allMatches.length} match(es); --nth ${nth} is out of range`);
16580
+ }
16581
+ selected = [single];
16582
+ } else if (wantAll) {
16583
+ selected = allMatches;
16584
+ } else {
16585
+ selected = allMatches.slice(0, 1);
16586
+ }
16587
+ await respond({
16588
+ ok: true,
16589
+ operation: "find",
16590
+ path,
16591
+ query,
16592
+ regex: useRegex,
16593
+ ignoreCase,
16594
+ totalMatches: allMatches.length,
16595
+ matches: selected.map((match) => ({
16596
+ locator: `${match.blockId}:${match.start}-${match.end}`,
16597
+ ...match
16598
+ }))
16599
+ });
16600
+ return EXIT.OK;
16601
+ }
16602
+ var HELP10 = `docx find \u2014 locate text spans and return their locators
16603
+
16604
+ Usage:
16605
+ docx find FILE QUERY [options]
16606
+
16607
+ Required positional:
16608
+ QUERY literal substring (or regex if --regex)
16609
+
16610
+ Options:
16611
+ --regex treat QUERY as a JavaScript regular expression
16612
+ --ignore-case case-insensitive match
16613
+ --all return every match (default: just the first)
16614
+ --nth N return only the Nth match (0-indexed)
16615
+ -h, --help show this help
16616
+
16617
+ Within-paragraph matches only \u2014 cross-paragraph ranges aren't supported
16618
+ yet. Searches the concatenated text of each paragraph in document order,
16619
+ including paragraphs nested in table cells (locators look like
16620
+ tT:rRcC:pK:S-E for those).
16621
+
16622
+ Examples:
16623
+ docx find doc.docx "fox"
16624
+ docx find doc.docx "Action Item:" --all
16625
+ docx find doc.docx "TODO|FIXME" --regex --ignore-case
16626
+ docx comments add doc.docx --range "$(docx find doc.docx fox | jq -r .matches[0].locator)" --text "..."
16627
+ `;
16628
+ var init_find = __esm(() => {
16629
+ init_respond();
16630
+ });
16631
+
16258
16632
  // src/cli/images/extract.ts
16259
16633
  var exports_extract = {};
16260
16634
  __export(exports_extract, {
16261
16635
  run: () => run11
16262
16636
  });
16263
- import { join as join2 } from "path";
16637
+ import { join } from "path";
16264
16638
  import { parseArgs as parseArgs10 } from "util";
16265
16639
  async function run11(args) {
16266
16640
  let parsed;
@@ -16289,20 +16663,9 @@ async function run11(args) {
16289
16663
  if (!outputDir)
16290
16664
  return fail("USAGE", "Missing --to DIR", HELP11);
16291
16665
  const targetId = parsed.values.id;
16292
- let view;
16293
- try {
16294
- view = await openDocView(path);
16295
- } catch (openError) {
16296
- if (openError instanceof PkgError) {
16297
- if (openError.code === "FILE_NOT_FOUND") {
16298
- return fail("FILE_NOT_FOUND", openError.message);
16299
- }
16300
- if (openError.code === "NOT_A_ZIP") {
16301
- return fail("NOT_A_ZIP", openError.message);
16302
- }
16303
- }
16304
- throw openError;
16305
- }
16666
+ const view = await openOrFail(path);
16667
+ if (typeof view === "number")
16668
+ return view;
16306
16669
  await enrichImageHashes(view);
16307
16670
  const allImages = [];
16308
16671
  collectImages(view.doc.blocks, allImages);
@@ -16318,7 +16681,7 @@ async function run11(args) {
16318
16681
  continue;
16319
16682
  const extension = extensionFor(image.contentType, reference.partName);
16320
16683
  const fileName = `${image.hash}.${extension}`;
16321
- const outputPath = join2(outputDir, fileName);
16684
+ const outputPath = join(outputDir, fileName);
16322
16685
  if (!seenHashes.has(image.hash)) {
16323
16686
  const bytes = await view.pkg.readBytes(reference.partName);
16324
16687
  await Bun.write(outputPath, bytes);
@@ -16423,20 +16786,9 @@ async function run12(args) {
16423
16786
  const path = parsed.positionals[0];
16424
16787
  if (!path)
16425
16788
  return fail("USAGE", "Missing FILE argument", HELP12);
16426
- let view;
16427
- try {
16428
- view = await openDocView(path);
16429
- } catch (openError) {
16430
- if (openError instanceof PkgError) {
16431
- if (openError.code === "FILE_NOT_FOUND") {
16432
- return fail("FILE_NOT_FOUND", openError.message);
16433
- }
16434
- if (openError.code === "NOT_A_ZIP") {
16435
- return fail("NOT_A_ZIP", openError.message);
16436
- }
16437
- }
16438
- throw openError;
16439
- }
16789
+ const view = await openOrFail(path);
16790
+ if (typeof view === "number")
16791
+ return view;
16440
16792
  await enrichImageHashes(view);
16441
16793
  const images = [];
16442
16794
  collectImages2(view.doc.blocks, images);
@@ -16492,6 +16844,7 @@ async function run13(args) {
16492
16844
  options: {
16493
16845
  at: { type: "string" },
16494
16846
  with: { type: "string" },
16847
+ output: { type: "string", short: "o" },
16495
16848
  "dry-run": { type: "boolean" },
16496
16849
  help: { type: "boolean", short: "h" }
16497
16850
  }
@@ -16522,26 +16875,16 @@ async function run13(args) {
16522
16875
  if (!newExtension) {
16523
16876
  return fail("USAGE", `Unsupported replacement image type: ${newMimeType}`, "Supported: png, jpeg, gif, webp, bmp, tiff, svg, emf, wmf, ico.");
16524
16877
  }
16525
- let view;
16526
- try {
16527
- view = await openDocView(path);
16528
- } catch (openError) {
16529
- if (openError instanceof PkgError) {
16530
- if (openError.code === "FILE_NOT_FOUND") {
16531
- return fail("FILE_NOT_FOUND", openError.message);
16532
- }
16533
- if (openError.code === "NOT_A_ZIP") {
16534
- return fail("NOT_A_ZIP", openError.message);
16535
- }
16536
- }
16537
- throw openError;
16538
- }
16878
+ const view = await openOrFail(path);
16879
+ if (typeof view === "number")
16880
+ return view;
16539
16881
  const reference = view.imageById.get(targetId);
16540
16882
  if (!reference) {
16541
16883
  return fail("IMAGE_NOT_FOUND", `Image not found: ${targetId}`);
16542
16884
  }
16543
16885
  const originalPartName = reference.partName;
16544
16886
  const newPartName = renameExtension(originalPartName, newExtension);
16887
+ const outputPath = parsed.values.output;
16545
16888
  if (parsed.values["dry-run"]) {
16546
16889
  await respond({
16547
16890
  ok: true,
@@ -16550,7 +16893,8 @@ async function run13(args) {
16550
16893
  path,
16551
16894
  imageId: targetId,
16552
16895
  from: { partName: originalPartName, mimeType: reference.contentType },
16553
- to: { partName: newPartName, mimeType: newMimeType }
16896
+ to: { partName: newPartName, mimeType: newMimeType },
16897
+ ...outputPath ? { output: outputPath } : {}
16554
16898
  });
16555
16899
  return EXIT.OK;
16556
16900
  }
@@ -16565,11 +16909,11 @@ async function run13(args) {
16565
16909
  reference.partName = newPartName;
16566
16910
  reference.contentType = newMimeType;
16567
16911
  }
16568
- await saveDocView(view);
16912
+ await saveDocView(view, outputPath);
16569
16913
  await respond({
16570
16914
  ok: true,
16571
16915
  operation: "images.replace",
16572
- path,
16916
+ path: outputPath ?? path,
16573
16917
  imageId: targetId,
16574
16918
  partName: newPartName,
16575
16919
  mimeType: newMimeType,
@@ -16623,6 +16967,7 @@ Required:
16623
16967
  --with PATH New image file (any image MIME type)
16624
16968
 
16625
16969
  Optional:
16970
+ -o, --output PATH Write to PATH instead of overwriting FILE
16626
16971
  --dry-run Print what would change; do not write the file
16627
16972
  -h, --help Show this help
16628
16973
 
@@ -16691,9 +17036,118 @@ var init_images = __esm(() => {
16691
17036
  };
16692
17037
  });
16693
17038
 
16694
- // src/cli/insert/index.tsx
16695
- var exports_insert = {};
16696
- __export(exports_insert, {
17039
+ // src/core/ast/types.ts
17040
+ var types_default = `export type Doc = {
17041
+ schemaVersion: 1;
17042
+ path: string;
17043
+ properties: DocProperties;
17044
+ blocks: Block[];
17045
+ comments: Comment[];
17046
+ };
17047
+
17048
+ export type DocProperties = {
17049
+ title?: string;
17050
+ author?: string;
17051
+ created?: string;
17052
+ modified?: string;
17053
+ };
17054
+
17055
+ export type Block = Paragraph | Table | SectionBreak;
17056
+
17057
+ export type Paragraph = {
17058
+ id: string;
17059
+ type: "paragraph";
17060
+ style?: string;
17061
+ alignment?: "left" | "center" | "right" | "justify";
17062
+ list?: { level: number; numId: string };
17063
+ runs: Run[];
17064
+ };
17065
+
17066
+ export type Table = {
17067
+ id: string;
17068
+ type: "table";
17069
+ rows: TableRow[];
17070
+ };
17071
+
17072
+ export type TableRow = {
17073
+ cells: TableCell[];
17074
+ };
17075
+
17076
+ export type TableCell = {
17077
+ blocks: Block[];
17078
+ };
17079
+
17080
+ export type SectionBreak = {
17081
+ id: string;
17082
+ type: "sectionBreak";
17083
+ };
17084
+
17085
+ export type Run = TextRun | ImageRun | BreakRun | TabRun;
17086
+
17087
+ export type TextRun = {
17088
+ type: "text";
17089
+ text: string;
17090
+ color?: string;
17091
+ highlight?: string;
17092
+ bold?: boolean;
17093
+ italic?: boolean;
17094
+ underline?: string;
17095
+ strike?: boolean;
17096
+ font?: string;
17097
+ sizeHalfPoints?: number;
17098
+ comments?: string[];
17099
+ trackedChange?: TrackedChange;
17100
+ };
17101
+
17102
+ export type ImageRun = {
17103
+ type: "image";
17104
+ id: string;
17105
+ contentType: string;
17106
+ hash: string;
17107
+ widthEmu?: number;
17108
+ heightEmu?: number;
17109
+ alt?: string;
17110
+ };
17111
+
17112
+ export type BreakRun = {
17113
+ type: "break";
17114
+ kind: "page" | "line" | "column";
17115
+ };
17116
+
17117
+ export type TabRun = {
17118
+ type: "tab";
17119
+ };
17120
+
17121
+ export type TrackedChange = {
17122
+ kind: "ins" | "del";
17123
+ author: string;
17124
+ date: string;
17125
+ revisionId: string;
17126
+ };
17127
+
17128
+ export type Comment = {
17129
+ id: string;
17130
+ author: string;
17131
+ initials?: string;
17132
+ date: string;
17133
+ text: string;
17134
+ parentId?: string;
17135
+ resolved?: boolean;
17136
+ anchor: CommentAnchor;
17137
+ };
17138
+
17139
+ export type CommentAnchor = {
17140
+ startBlockId: string;
17141
+ startOffset: number;
17142
+ endBlockId: string;
17143
+ endOffset: number;
17144
+ };
17145
+ `;
17146
+ var init_types = () => {};
17147
+
17148
+ // src/cli/info/schema.ts
17149
+ var exports_schema = {};
17150
+ __export(exports_schema, {
16697
17151
  run: () => run15
16698
17152
  });
16699
17153
  import { parseArgs as parseArgs13 } from "util";
@@ -16704,16 +17158,8 @@ async function run15(args) {
16704
17158
  args,
16705
17159
  allowPositionals: true,
16706
17160
  options: {
16707
- after: { type: "string" },
16708
- before: { type: "string" },
16709
- text: { type: "string" },
16710
- runs: { type: "string" },
16711
- style: { type: "string" },
16712
- alignment: { type: "string" },
16713
- color: { type: "string" },
16714
- bold: { type: "boolean" },
16715
- italic: { type: "boolean" },
16716
- "dry-run": { type: "boolean" },
17161
+ json: { type: "boolean" },
17162
+ ts: { type: "boolean" },
16717
17163
  help: { type: "boolean", short: "h" }
16718
17164
  }
16719
17165
  });
@@ -16725,153 +17171,205 @@ async function run15(args) {
16725
17171
  await writeStdout(HELP15);
16726
17172
  return EXIT.OK;
16727
17173
  }
16728
- const path = parsed.positionals[0];
16729
- if (!path)
16730
- return fail("USAGE", "Missing FILE argument", HELP15);
16731
- const after = parsed.values.after;
16732
- const before = parsed.values.before;
16733
- if (!after && !before) {
16734
- return fail("USAGE", "Missing locator: pass --after or --before", HELP15);
16735
- }
16736
- if (after && before) {
16737
- return fail("USAGE", "Pass either --after or --before, not both", HELP15);
16738
- }
16739
- const text = parsed.values.text;
16740
- const runsJson = parsed.values.runs;
16741
- if (!text && !runsJson) {
16742
- return fail("USAGE", "Missing content: pass --text or --runs", HELP15);
16743
- }
16744
- if (text && runsJson) {
16745
- return fail("USAGE", "Pass either --text or --runs, not both", HELP15);
16746
- }
16747
- const paragraphOptions = {};
16748
- const styleValue = parsed.values.style;
16749
- if (styleValue)
16750
- paragraphOptions.style = styleValue;
16751
- const alignmentValue = parsed.values.alignment;
16752
- if (alignmentValue) {
16753
- if (alignmentValue !== "left" && alignmentValue !== "center" && alignmentValue !== "right" && alignmentValue !== "justify") {
16754
- return fail("USAGE", `Invalid --alignment: ${alignmentValue}`, "Valid values: left, center, right, justify");
16755
- }
16756
- paragraphOptions.alignment = alignmentValue;
16757
- }
16758
- let view;
16759
- try {
16760
- view = await openDocView(path);
16761
- } catch (openError) {
16762
- if (openError instanceof PkgError) {
16763
- if (openError.code === "FILE_NOT_FOUND") {
16764
- return fail("FILE_NOT_FOUND", openError.message);
16765
- }
16766
- if (openError.code === "NOT_A_ZIP") {
16767
- return fail("NOT_A_ZIP", openError.message);
16768
- }
16769
- }
16770
- throw openError;
16771
- }
16772
- const targetLocator = after ?? before;
16773
- let blockRef;
16774
- try {
16775
- blockRef = resolveBlock(view, targetLocator);
16776
- } catch (resolveError) {
16777
- if (resolveError instanceof LocatorResolveError) {
16778
- return fail("BLOCK_NOT_FOUND", resolveError.message);
16779
- }
16780
- throw resolveError;
16781
- }
16782
- let paragraphNode;
16783
- if (text !== undefined) {
16784
- const color = parsed.values.color;
16785
- paragraphNode = /* @__PURE__ */ jsxDEV(Paragraph, {
16786
- text,
16787
- ...paragraphOptions,
16788
- ...color ? { color } : {},
16789
- ...parsed.values.bold ? { bold: true } : {},
16790
- ...parsed.values.italic ? { italic: true } : {}
16791
- }, undefined, false, undefined, this);
16792
- } else {
16793
- let runsValue;
16794
- try {
16795
- runsValue = JSON.parse(runsJson);
16796
- } catch (jsonError) {
16797
- const message = jsonError instanceof Error ? jsonError.message : String(jsonError);
16798
- return fail("USAGE", `Invalid --runs JSON: ${message}`);
16799
- }
16800
- if (!Array.isArray(runsValue)) {
16801
- return fail("USAGE", "--runs must be a JSON array of Run objects");
16802
- }
16803
- paragraphNode = /* @__PURE__ */ jsxDEV(Paragraph, {
16804
- runs: runsValue,
16805
- ...paragraphOptions
16806
- }, undefined, false, undefined, this);
16807
- }
16808
- const targetIndex = blockRef.parent.indexOf(blockRef.node);
16809
- if (targetIndex === -1) {
16810
- return fail("BLOCK_NOT_FOUND", "Block reference is stale (parent does not contain it)");
16811
- }
16812
- const insertIndex = after !== undefined ? targetIndex + 1 : targetIndex;
16813
- if (parsed.values["dry-run"]) {
16814
- await respond({
16815
- ok: true,
16816
- operation: "insert",
16817
- dryRun: true,
16818
- path,
16819
- locator: targetLocator,
16820
- placement: after !== undefined ? "after" : "before"
16821
- });
17174
+ if (parsed.values.ts) {
17175
+ await writeStdout(types_default);
16822
17176
  return EXIT.OK;
16823
17177
  }
16824
- blockRef.parent.splice(insertIndex, 0, paragraphNode);
16825
- await saveDocView(view);
16826
- await respond({
16827
- ok: true,
16828
- operation: "insert",
16829
- path,
16830
- locator: targetLocator,
16831
- placement: after !== undefined ? "after" : "before"
16832
- });
17178
+ await respond(JSON_SCHEMA);
16833
17179
  return EXIT.OK;
16834
17180
  }
16835
- var HELP15 = `docx insert \u2014 insert a paragraph at a locator
17181
+ var HELP15 = `docx schema \u2014 print the AST type definitions
16836
17182
 
16837
17183
  Usage:
16838
- docx insert FILE [options]
16839
-
16840
- Locator (one required):
16841
- --after LOCATOR Insert after the block at LOCATOR (e.g., p3)
16842
- --before LOCATOR Insert before the block at LOCATOR
16843
-
16844
- Content (one required):
16845
- --text TEXT Insert a paragraph with this text
16846
- --runs JSON Insert a paragraph with custom runs (Run[] JSON)
16847
-
16848
- Paragraph options:
16849
- --style NAME Apply paragraph style (e.g., Heading1)
16850
- --alignment ALIGN left | center | right | justify
16851
-
16852
- Run options (only with --text):
16853
- --color HEX Run color, hex (e.g., 800080 for purple)
16854
- --bold Bold
16855
- --italic Italic
17184
+ docx schema [options]
16856
17185
 
16857
- --dry-run Print what would be inserted; do not write the file
16858
- -h, --help Show this help
17186
+ Options:
17187
+ --json Print as a JSON Schema document (default)
17188
+ --ts Print TypeScript type source (read live from src/core/ast/types.ts)
17189
+ -h, --help Show this help
16859
17190
 
16860
17191
  Examples:
16861
- docx insert doc.docx --after p3 --text "Section header" --style Heading2
16862
- docx insert doc.docx --before p0 --text "ALERT" --color CC0000 --bold
16863
- docx insert doc.docx --after p2 --runs '[{"type":"text","text":"X","bold":true}]'
16864
- `;
16865
- var init_insert = __esm(() => {
16866
- init_core();
17192
+ docx schema | jq '.definitions.Run'
17193
+ docx schema --ts > ast.d.ts
17194
+ `, JSON_SCHEMA;
17195
+ var init_schema = __esm(() => {
17196
+ init_types();
16867
17197
  init_respond();
16868
- init_emit();
16869
- init_jsx_dev_runtime();
17198
+ JSON_SCHEMA = {
17199
+ $schema: "https://json-schema.org/draft/2020-12/schema",
17200
+ $id: "https://github.com/kklimuk/docx-cli/schema",
17201
+ title: "docx-cli AST",
17202
+ type: "object",
17203
+ required: ["schemaVersion", "path", "properties", "blocks", "comments"],
17204
+ properties: {
17205
+ schemaVersion: { const: 1 },
17206
+ path: { type: "string" },
17207
+ properties: {
17208
+ type: "object",
17209
+ properties: {
17210
+ title: { type: "string" },
17211
+ author: { type: "string" },
17212
+ created: { type: "string" },
17213
+ modified: { type: "string" }
17214
+ }
17215
+ },
17216
+ blocks: { type: "array", items: { $ref: "#/$defs/Block" } },
17217
+ comments: { type: "array", items: { $ref: "#/$defs/Comment" } }
17218
+ },
17219
+ $defs: {
17220
+ Block: {
17221
+ oneOf: [
17222
+ { $ref: "#/$defs/Paragraph" },
17223
+ { $ref: "#/$defs/Table" },
17224
+ { $ref: "#/$defs/SectionBreak" }
17225
+ ]
17226
+ },
17227
+ Paragraph: {
17228
+ type: "object",
17229
+ required: ["id", "type", "runs"],
17230
+ properties: {
17231
+ id: { type: "string" },
17232
+ type: { const: "paragraph" },
17233
+ style: { type: "string" },
17234
+ alignment: { enum: ["left", "center", "right", "justify"] },
17235
+ list: {
17236
+ type: "object",
17237
+ required: ["level", "numId"],
17238
+ properties: {
17239
+ level: { type: "number" },
17240
+ numId: { type: "string" }
17241
+ }
17242
+ },
17243
+ runs: { type: "array", items: { $ref: "#/$defs/Run" } }
17244
+ }
17245
+ },
17246
+ Run: {
17247
+ oneOf: [
17248
+ { $ref: "#/$defs/TextRun" },
17249
+ { $ref: "#/$defs/ImageRun" },
17250
+ { $ref: "#/$defs/BreakRun" },
17251
+ { $ref: "#/$defs/TabRun" }
17252
+ ]
17253
+ },
17254
+ TextRun: {
17255
+ type: "object",
17256
+ required: ["type", "text"],
17257
+ properties: {
17258
+ type: { const: "text" },
17259
+ text: { type: "string" },
17260
+ color: { type: "string" },
17261
+ highlight: { type: "string" },
17262
+ bold: { type: "boolean" },
17263
+ italic: { type: "boolean" },
17264
+ underline: { type: "string" },
17265
+ strike: { type: "boolean" },
17266
+ font: { type: "string" },
17267
+ sizeHalfPoints: { type: "number" },
17268
+ comments: { type: "array", items: { type: "string" } },
17269
+ trackedChange: {
17270
+ type: "object",
17271
+ required: ["kind", "author", "date", "revisionId"],
17272
+ properties: {
17273
+ kind: { enum: ["ins", "del"] },
17274
+ author: { type: "string" },
17275
+ date: { type: "string" },
17276
+ revisionId: { type: "string" }
17277
+ }
17278
+ }
17279
+ }
17280
+ },
17281
+ ImageRun: {
17282
+ type: "object",
17283
+ required: ["type", "id", "contentType", "hash"],
17284
+ properties: {
17285
+ type: { const: "image" },
17286
+ id: { type: "string" },
17287
+ contentType: { type: "string" },
17288
+ hash: { type: "string" },
17289
+ widthEmu: { type: "number" },
17290
+ heightEmu: { type: "number" },
17291
+ alt: { type: "string" }
17292
+ }
17293
+ },
17294
+ BreakRun: {
17295
+ type: "object",
17296
+ required: ["type", "kind"],
17297
+ properties: {
17298
+ type: { const: "break" },
17299
+ kind: { enum: ["page", "line", "column"] }
17300
+ }
17301
+ },
17302
+ TabRun: {
17303
+ type: "object",
17304
+ required: ["type"],
17305
+ properties: { type: { const: "tab" } }
17306
+ },
17307
+ Table: {
17308
+ type: "object",
17309
+ required: ["id", "type", "rows"],
17310
+ properties: {
17311
+ id: { type: "string" },
17312
+ type: { const: "table" },
17313
+ rows: {
17314
+ type: "array",
17315
+ items: {
17316
+ type: "object",
17317
+ properties: {
17318
+ cells: {
17319
+ type: "array",
17320
+ items: {
17321
+ type: "object",
17322
+ properties: {
17323
+ blocks: {
17324
+ type: "array",
17325
+ items: { $ref: "#/$defs/Block" }
17326
+ }
17327
+ }
17328
+ }
17329
+ }
17330
+ }
17331
+ }
17332
+ }
17333
+ }
17334
+ },
17335
+ SectionBreak: {
17336
+ type: "object",
17337
+ required: ["id", "type"],
17338
+ properties: {
17339
+ id: { type: "string" },
17340
+ type: { const: "sectionBreak" }
17341
+ }
17342
+ },
17343
+ Comment: {
17344
+ type: "object",
17345
+ required: ["id", "author", "date", "text", "anchor"],
17346
+ properties: {
17347
+ id: { type: "string" },
17348
+ author: { type: "string" },
17349
+ initials: { type: "string" },
17350
+ date: { type: "string" },
17351
+ text: { type: "string" },
17352
+ parentId: { type: "string" },
17353
+ resolved: { type: "boolean" },
17354
+ anchor: {
17355
+ type: "object",
17356
+ required: ["startBlockId", "startOffset", "endBlockId", "endOffset"],
17357
+ properties: {
17358
+ startBlockId: { type: "string" },
17359
+ startOffset: { type: "number" },
17360
+ endBlockId: { type: "string" },
17361
+ endOffset: { type: "number" }
17362
+ }
17363
+ }
17364
+ }
17365
+ }
17366
+ }
17367
+ };
16870
17368
  });
16871
17369
 
16872
- // src/cli/locators-cmd/index.ts
16873
- var exports_locators_cmd = {};
16874
- __export(exports_locators_cmd, {
17370
+ // src/cli/info/locators.ts
17371
+ var exports_locators = {};
17372
+ __export(exports_locators, {
16875
17373
  run: () => run16
16876
17374
  });
16877
17375
  import { parseArgs as parseArgs14 } from "util";
@@ -16895,11 +17393,11 @@ async function run16(args) {
16895
17393
  return EXIT.OK;
16896
17394
  }
16897
17395
  if (parsed.values.json) {
16898
- await Bun.write(Bun.stdout, `${JSON.stringify(JSON_REFERENCE, null, 2)}
17396
+ await writeStdout(`${JSON.stringify(JSON_REFERENCE, null, 2)}
16899
17397
  `);
16900
17398
  return EXIT.OK;
16901
17399
  }
16902
- await Bun.write(Bun.stdout, REFERENCE);
17400
+ await writeStdout(REFERENCE);
16903
17401
  return EXIT.OK;
16904
17402
  }
16905
17403
  var HELP16 = `docx locators \u2014 print the locator grammar reference
@@ -16913,21 +17411,22 @@ Options:
16913
17411
  `, REFERENCE = `LOCATOR GRAMMAR
16914
17412
 
16915
17413
  Block locators:
16916
- pN Paragraph N (e.g., p3)
16917
- tN Table N
16918
- sN Section break N
16919
- cellpN Paragraph inside a table cell
17414
+ pN Paragraph N (e.g., p3)
17415
+ tN Table N
17416
+ sN Section break N
17417
+ tT:rRcC:pK Paragraph K of cell (R,C) in table T
16920
17418
 
16921
17419
  Span locators (within a single paragraph):
16922
- pN:S-E Characters S..E of paragraph N (inclusive start, exclusive end)
17420
+ pN:S-E Characters S..E of paragraph N (inclusive start, exclusive end)
17421
+ tT:rRcC:pK:S-E Characters S..E of a cell paragraph
16923
17422
 
16924
17423
  Range locators (across blocks):
16925
- pN:S-pM:E From char S of paragraph N to char E of paragraph M
17424
+ pN:S-pM:E From char S of paragraph N to char E of paragraph M
16926
17425
 
16927
17426
  Entity locators:
16928
- cN Comment id (e.g., c0)
16929
- imgN Image id (e.g., img2)
16930
- tN:rRcC Cell at row R, column C of table tN
17427
+ cN Comment id (e.g., c0)
17428
+ imgN Image id (e.g., img2)
17429
+ tN:rRcC Cell at row R, column C of table tN
16931
17430
 
16932
17431
  Examples:
16933
17432
  p3 -> the entire paragraph p3
@@ -16937,25 +17436,27 @@ Examples:
16937
17436
  img0 -> image img0
16938
17437
  t0:r1c2 -> cell at row 1, col 2 of table t0
16939
17438
  t0:r1c2:p0 -> first paragraph of that cell
17439
+ t0:r1c2:p0:5-10 -> chars 5..10 of that paragraph
16940
17440
 
16941
17441
  Notes:
16942
17442
  Block ids are positional and shift after structural edits \u2014 re-read
16943
17443
  between non-trivial mutations. Mutating commands print before/after
16944
17444
  locator info in their JSON ack so agents can chain operations.
16945
17445
  `, JSON_REFERENCE;
16946
- var init_locators_cmd = __esm(() => {
17446
+ var init_locators2 = __esm(() => {
16947
17447
  init_respond();
16948
17448
  JSON_REFERENCE = {
16949
17449
  blockLocators: {
16950
17450
  paragraph: { syntax: "pN", example: "p3" },
16951
17451
  table: { syntax: "tN", example: "t0" },
16952
17452
  sectionBreak: { syntax: "sN", example: "s0" },
16953
- cellParagraph: { syntax: "cellpN", example: "cellp0" }
17453
+ cellParagraph: { syntax: "tT:rRcC:pK", example: "t0:r1c2:p0" }
16954
17454
  },
16955
17455
  spanLocator: {
16956
17456
  syntax: "pN:S-E",
16957
17457
  example: "p3:5-20",
16958
- semantics: "Characters S..E within paragraph N (start inclusive, end exclusive)"
17458
+ semantics: "Characters S..E within paragraph N (start inclusive, end exclusive)",
17459
+ cellSyntax: "tT:rRcC:pK:S-E"
16959
17460
  },
16960
17461
  rangeLocator: {
16961
17462
  syntax: "pN:S-pM:E",
@@ -16976,16 +17477,251 @@ var init_locators_cmd = __esm(() => {
16976
17477
  };
16977
17478
  });
16978
17479
 
17480
+ // src/cli/info/index.ts
17481
+ var exports_info = {};
17482
+ __export(exports_info, {
17483
+ run: () => run17
17484
+ });
17485
+ async function run17(args) {
17486
+ const topic = args[0];
17487
+ if (!topic || topic === "--help" || topic === "-h" || topic === "help") {
17488
+ await writeStdout(HELP17);
17489
+ return topic ? 0 : 2;
17490
+ }
17491
+ const loader = SUBCOMMANDS3[topic];
17492
+ if (!loader) {
17493
+ return fail("USAGE", `Unknown info topic: ${topic}`, 'Run "docx info --help".');
17494
+ }
17495
+ const module_ = await loader();
17496
+ return module_.run(args.slice(1));
17497
+ }
17498
+ var SUBCOMMANDS3, HELP17 = `docx info \u2014 print reference material about the CLI
17499
+
17500
+ Usage:
17501
+ docx info <topic> [options]
17502
+
17503
+ Topics:
17504
+ schema Dump AST JSON Schema (or TS source via --ts)
17505
+ locators Dump locator grammar reference
17506
+
17507
+ Run "docx info <topic> --help" for topic-specific help.
17508
+ `;
17509
+ var init_info = __esm(() => {
17510
+ init_respond();
17511
+ SUBCOMMANDS3 = {
17512
+ schema: () => Promise.resolve().then(() => (init_schema(), exports_schema)),
17513
+ locators: () => Promise.resolve().then(() => (init_locators2(), exports_locators))
17514
+ };
17515
+ });
17516
+
17517
+ // src/cli/insert/index.tsx
17518
+ var exports_insert = {};
17519
+ __export(exports_insert, {
17520
+ run: () => run18
17521
+ });
17522
+ import { parseArgs as parseArgs15 } from "util";
17523
+ async function run18(args) {
17524
+ let parsed;
17525
+ try {
17526
+ parsed = parseArgs15({
17527
+ args,
17528
+ allowPositionals: true,
17529
+ options: {
17530
+ after: { type: "string" },
17531
+ before: { type: "string" },
17532
+ text: { type: "string" },
17533
+ runs: { type: "string" },
17534
+ style: { type: "string" },
17535
+ alignment: { type: "string" },
17536
+ color: { type: "string" },
17537
+ bold: { type: "boolean" },
17538
+ italic: { type: "boolean" },
17539
+ output: { type: "string", short: "o" },
17540
+ "dry-run": { type: "boolean" },
17541
+ help: { type: "boolean", short: "h" }
17542
+ }
17543
+ });
17544
+ } catch (parseError) {
17545
+ const message = parseError instanceof Error ? parseError.message : String(parseError);
17546
+ return fail("USAGE", message, HELP18);
17547
+ }
17548
+ if (parsed.values.help) {
17549
+ await writeStdout(HELP18);
17550
+ return EXIT.OK;
17551
+ }
17552
+ const path = parsed.positionals[0];
17553
+ if (!path)
17554
+ return fail("USAGE", "Missing FILE argument", HELP18);
17555
+ const after = parsed.values.after;
17556
+ const before = parsed.values.before;
17557
+ if (!after && !before) {
17558
+ return fail("USAGE", "Missing locator: pass --after or --before", HELP18);
17559
+ }
17560
+ if (after && before) {
17561
+ return fail("USAGE", "Pass either --after or --before, not both", HELP18);
17562
+ }
17563
+ const text = parsed.values.text;
17564
+ const runsJson = parsed.values.runs;
17565
+ if (!text && !runsJson) {
17566
+ return fail("USAGE", "Missing content: pass --text or --runs", HELP18);
17567
+ }
17568
+ if (text && runsJson) {
17569
+ return fail("USAGE", "Pass either --text or --runs, not both", HELP18);
17570
+ }
17571
+ const paragraphOptions = {};
17572
+ const styleValue = parsed.values.style;
17573
+ if (styleValue)
17574
+ paragraphOptions.style = styleValue;
17575
+ const alignmentValue = parsed.values.alignment;
17576
+ if (alignmentValue) {
17577
+ if (alignmentValue !== "left" && alignmentValue !== "center" && alignmentValue !== "right" && alignmentValue !== "justify") {
17578
+ return fail("USAGE", `Invalid --alignment: ${alignmentValue}`, "Valid values: left, center, right, justify");
17579
+ }
17580
+ paragraphOptions.alignment = alignmentValue;
17581
+ }
17582
+ const view = await openOrFail(path);
17583
+ if (typeof view === "number")
17584
+ return view;
17585
+ const targetLocator = after ?? before;
17586
+ const blockRef = await resolveBlockOrFail(view, targetLocator);
17587
+ if (typeof blockRef === "number")
17588
+ return blockRef;
17589
+ let paragraphNode;
17590
+ if (text !== undefined) {
17591
+ const color = parsed.values.color;
17592
+ paragraphNode = /* @__PURE__ */ jsxDEV(Paragraph, {
17593
+ text,
17594
+ ...paragraphOptions,
17595
+ ...color ? { color } : {},
17596
+ ...parsed.values.bold ? { bold: true } : {},
17597
+ ...parsed.values.italic ? { italic: true } : {}
17598
+ }, undefined, false, undefined, this);
17599
+ } else {
17600
+ let runsValue;
17601
+ try {
17602
+ runsValue = JSON.parse(runsJson);
17603
+ } catch (jsonError) {
17604
+ const message = jsonError instanceof Error ? jsonError.message : String(jsonError);
17605
+ return fail("USAGE", `Invalid --runs JSON: ${message}`);
17606
+ }
17607
+ if (!Array.isArray(runsValue)) {
17608
+ return fail("USAGE", "--runs must be a JSON array of Run objects");
17609
+ }
17610
+ paragraphNode = /* @__PURE__ */ jsxDEV(Paragraph, {
17611
+ runs: runsValue,
17612
+ ...paragraphOptions
17613
+ }, undefined, false, undefined, this);
17614
+ }
17615
+ const targetIndex = blockRef.parent.indexOf(blockRef.node);
17616
+ if (targetIndex === -1) {
17617
+ return fail("BLOCK_NOT_FOUND", "Block reference is stale (parent does not contain it)");
17618
+ }
17619
+ const insertIndex = after !== undefined ? targetIndex + 1 : targetIndex;
17620
+ if (isTrackChangesEnabled(view)) {
17621
+ applyTrackedInsertion(paragraphNode, view);
17622
+ }
17623
+ const outputPath = parsed.values.output;
17624
+ if (parsed.values["dry-run"]) {
17625
+ await respond({
17626
+ ok: true,
17627
+ operation: "insert",
17628
+ dryRun: true,
17629
+ path,
17630
+ locator: targetLocator,
17631
+ placement: after !== undefined ? "after" : "before",
17632
+ ...outputPath ? { output: outputPath } : {}
17633
+ });
17634
+ return EXIT.OK;
17635
+ }
17636
+ blockRef.parent.splice(insertIndex, 0, paragraphNode);
17637
+ await saveDocView(view, outputPath);
17638
+ await respond({
17639
+ ok: true,
17640
+ operation: "insert",
17641
+ path: outputPath ?? path,
17642
+ locator: targetLocator,
17643
+ placement: after !== undefined ? "after" : "before"
17644
+ });
17645
+ return EXIT.OK;
17646
+ }
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
17678
+
17679
+ Usage:
17680
+ docx insert FILE [options]
17681
+
17682
+ Locator (one required):
17683
+ --after LOCATOR Insert after the block at LOCATOR (e.g., p3)
17684
+ --before LOCATOR Insert before the block at LOCATOR
17685
+
17686
+ Content (one required):
17687
+ --text TEXT Insert a paragraph with this text
17688
+ --runs JSON Insert a paragraph with custom runs (Run[] JSON)
17689
+
17690
+ Paragraph options:
17691
+ --style NAME Apply paragraph style (e.g., Heading1)
17692
+ --alignment ALIGN left | center | right | justify
17693
+
17694
+ Run options (only with --text):
17695
+ --color HEX Run color, hex (e.g., 800080 for purple)
17696
+ --bold Bold
17697
+ --italic Italic
17698
+
17699
+ -o, --output PATH Write to PATH instead of overwriting FILE
17700
+ --dry-run Print what would be inserted; do not write the file
17701
+ -h, --help Show this help
17702
+
17703
+ Examples:
17704
+ docx insert doc.docx --after p3 --text "Section header" --style Heading2
17705
+ docx insert doc.docx --before p0 --text "ALERT" --color CC0000 --bold
17706
+ docx insert doc.docx --after p2 --runs '[{"type":"text","text":"X","bold":true}]'
17707
+ `;
17708
+ var init_insert = __esm(() => {
17709
+ init_core();
17710
+ init_respond();
17711
+ init_emit2();
17712
+ init_jsx_dev_runtime();
17713
+ });
17714
+
16979
17715
  // src/cli/read/index.ts
16980
17716
  var exports_read = {};
16981
17717
  __export(exports_read, {
16982
- run: () => run17
17718
+ run: () => run19
16983
17719
  });
16984
- import { parseArgs as parseArgs15 } from "util";
16985
- async function run17(args) {
17720
+ import { parseArgs as parseArgs16 } from "util";
17721
+ async function run19(args) {
16986
17722
  let parsed;
16987
17723
  try {
16988
- parsed = parseArgs15({
17724
+ parsed = parseArgs16({
16989
17725
  args,
16990
17726
  allowPositionals: true,
16991
17727
  options: {
@@ -16994,32 +17730,23 @@ async function run17(args) {
16994
17730
  }
16995
17731
  });
16996
17732
  } catch (e) {
16997
- return fail("USAGE", e instanceof Error ? e.message : String(e), HELP17);
17733
+ return fail("USAGE", e instanceof Error ? e.message : String(e), HELP19);
16998
17734
  }
16999
17735
  if (parsed.values.help) {
17000
- await writeStdout(HELP17);
17736
+ await writeStdout(HELP19);
17001
17737
  return EXIT.OK;
17002
17738
  }
17003
17739
  const path = parsed.positionals[0];
17004
17740
  if (!path)
17005
- return fail("USAGE", "Missing FILE argument", HELP17);
17006
- let view;
17007
- try {
17008
- view = await openDocView(path);
17009
- } catch (e) {
17010
- if (e instanceof PkgError) {
17011
- if (e.code === "FILE_NOT_FOUND")
17012
- return fail("FILE_NOT_FOUND", e.message);
17013
- if (e.code === "NOT_A_ZIP")
17014
- return fail("NOT_A_ZIP", e.message);
17015
- }
17016
- throw e;
17017
- }
17741
+ return fail("USAGE", "Missing FILE argument", HELP19);
17742
+ const view = await openOrFail(path);
17743
+ if (typeof view === "number")
17744
+ return view;
17018
17745
  await enrichImageHashes(view);
17019
17746
  await respond(view.doc);
17020
17747
  return EXIT.OK;
17021
17748
  }
17022
- var HELP17 = `docx read \u2014 print AST as JSON
17749
+ var HELP19 = `docx read \u2014 print AST as JSON
17023
17750
 
17024
17751
  Usage:
17025
17752
  docx read FILE [options]
@@ -17037,394 +17764,524 @@ var init_read2 = __esm(() => {
17037
17764
  init_respond();
17038
17765
  });
17039
17766
 
17040
- // src/core/ast/types.ts
17041
- var types_default = `export type Doc = {
17042
- schemaVersion: 1;
17043
- path: string;
17044
- properties: DocProperties;
17045
- blocks: Block[];
17046
- comments: Comment[];
17047
- };
17048
-
17049
- export type DocProperties = {
17050
- title?: string;
17051
- author?: string;
17052
- created?: string;
17053
- modified?: string;
17054
- };
17055
-
17056
- export type Block = Paragraph | Table | SectionBreak;
17057
-
17058
- export type Paragraph = {
17059
- id: string;
17060
- type: "paragraph";
17061
- style?: string;
17062
- alignment?: "left" | "center" | "right" | "justify";
17063
- list?: { level: number; numId: string };
17064
- runs: Run[];
17065
- };
17066
-
17067
- export type Table = {
17068
- id: string;
17069
- type: "table";
17070
- rows: TableRow[];
17071
- };
17072
-
17073
- export type TableRow = {
17074
- cells: TableCell[];
17075
- };
17076
-
17077
- export type TableCell = {
17078
- blocks: Block[];
17079
- };
17080
-
17081
- export type SectionBreak = {
17082
- id: string;
17083
- type: "sectionBreak";
17084
- };
17085
-
17086
- export type Run = TextRun | ImageRun | BreakRun | TabRun;
17087
-
17088
- export type TextRun = {
17089
- type: "text";
17090
- text: string;
17091
- color?: string;
17092
- highlight?: string;
17093
- bold?: boolean;
17094
- italic?: boolean;
17095
- underline?: string;
17096
- strike?: boolean;
17097
- font?: string;
17098
- sizeHalfPoints?: number;
17099
- comments?: string[];
17100
- trackedChange?: TrackedChange;
17101
- };
17102
-
17103
- export type ImageRun = {
17104
- type: "image";
17105
- id: string;
17106
- contentType: string;
17107
- hash: string;
17108
- widthEmu?: number;
17109
- heightEmu?: number;
17110
- alt?: string;
17111
- extractedPath?: string;
17112
- };
17113
-
17114
- export type BreakRun = {
17115
- type: "break";
17116
- kind: "page" | "line" | "column";
17117
- };
17118
-
17119
- export type TabRun = {
17120
- type: "tab";
17121
- };
17122
-
17123
- export type TrackedChange = {
17124
- kind: "ins" | "del";
17125
- author: string;
17126
- date: string;
17127
- revisionId: string;
17128
- };
17129
-
17130
- export type Comment = {
17131
- id: string;
17132
- author: string;
17133
- initials?: string;
17134
- date: string;
17135
- text: string;
17136
- parentId?: string;
17137
- resolved?: boolean;
17138
- anchor: CommentAnchor;
17139
- };
17140
-
17141
- export type CommentAnchor = {
17142
- startBlockId: string;
17143
- startOffset: number;
17144
- endBlockId: string;
17145
- endOffset: number;
17146
- };
17147
- `;
17148
- var init_types = () => {};
17767
+ // src/cli/replace/replace-span.tsx
17768
+ function replaceSpanInParagraph(paragraph, span, replacement, tracked) {
17769
+ if (span.start > span.end) {
17770
+ throw new Error(`replaceSpanInParagraph: invalid span ${span.start}-${span.end}`);
17771
+ }
17772
+ const slots = collectRunSlots(paragraph);
17773
+ const overlapping = slots.filter((slot) => slot.offsetBefore + slot.length > span.start && slot.offsetBefore < span.end);
17774
+ const firstSlot = overlapping[0];
17775
+ if (!firstSlot) {
17776
+ paragraph.children.push(/* @__PURE__ */ jsxDEV(ReplacementRun, {
17777
+ runProperties: null,
17778
+ text: replacement
17779
+ }, undefined, false, undefined, this));
17780
+ return;
17781
+ }
17782
+ const inheritedProperties = firstSlot.run.findChild("w:rPr")?.clone() ?? null;
17783
+ const firstParent = firstSlot.parent;
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;
17789
+ }
17790
+ rebuildAcrossBoundaries(paragraph, span, replacement, inheritedProperties, tracked ?? null);
17791
+ }
17792
+ function collectRunSlots(paragraph) {
17793
+ const slots = [];
17794
+ let offset = 0;
17795
+ for (const child of paragraph.children) {
17796
+ if (child.tag === "w:r") {
17797
+ const length = runTextLength(child);
17798
+ slots.push({
17799
+ parent: paragraph,
17800
+ run: child,
17801
+ offsetBefore: offset,
17802
+ length
17803
+ });
17804
+ offset += length;
17805
+ continue;
17806
+ }
17807
+ if (child.tag === "w:ins" || child.tag === "w:del") {
17808
+ for (const inner of child.children) {
17809
+ if (inner.tag !== "w:r")
17810
+ continue;
17811
+ const length = runTextLength(inner);
17812
+ slots.push({
17813
+ parent: child,
17814
+ run: inner,
17815
+ offsetBefore: offset,
17816
+ length
17817
+ });
17818
+ offset += length;
17819
+ }
17820
+ }
17821
+ }
17822
+ return slots;
17823
+ }
17824
+ function rebuildContainer(container, baseOffset, span, replacement, runProperties, isParagraph, tracked) {
17825
+ const newChildren = [];
17826
+ let offset = baseOffset;
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
+ };
17841
+ for (const child of container.children) {
17842
+ if (child.tag === "w:r") {
17843
+ const length = runTextLength(child);
17844
+ const runStart = offset;
17845
+ const runEnd = offset + length;
17846
+ offset = runEnd;
17847
+ if (runEnd <= span.start) {
17848
+ newChildren.push(child);
17849
+ continue;
17850
+ }
17851
+ if (runStart >= span.end) {
17852
+ placeReplacement();
17853
+ newChildren.push(child);
17854
+ continue;
17855
+ }
17856
+ const sliceStartInRun = Math.max(0, span.start - runStart);
17857
+ const sliceEndInRun = Math.min(length, span.end - runStart);
17858
+ if (sliceStartInRun > 0) {
17859
+ newChildren.push(sliceRun(child, 0, sliceStartInRun));
17860
+ }
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));
17868
+ }
17869
+ placeReplacement();
17870
+ if (sliceEndInRun < length) {
17871
+ newChildren.push(sliceRun(child, sliceEndInRun, length));
17872
+ }
17873
+ continue;
17874
+ }
17875
+ if (isParagraph && (child.tag === "w:ins" || child.tag === "w:del")) {
17876
+ let innerLength = 0;
17877
+ for (const inner of child.children) {
17878
+ if (inner.tag === "w:r")
17879
+ innerLength += runTextLength(inner);
17880
+ }
17881
+ offset += innerLength;
17882
+ newChildren.push(child);
17883
+ continue;
17884
+ }
17885
+ newChildren.push(child);
17886
+ }
17887
+ if (!placed)
17888
+ placeReplacement();
17889
+ container.children = newChildren;
17890
+ }
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);
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
+ }) {
18040
+ return /* @__PURE__ */ jsxDEV(w.r, {
18041
+ children: [
18042
+ runProperties,
18043
+ /* @__PURE__ */ jsxDEV(w.t, {
18044
+ "xml:space": "preserve",
18045
+ children: text
18046
+ }, undefined, false, undefined, this)
18047
+ ]
18048
+ }, undefined, true, undefined, this);
18049
+ }
18050
+ var init_replace_span = __esm(() => {
18051
+ init_core();
18052
+ init_jsx();
18053
+ init_parser();
18054
+ init_jsx_dev_runtime();
18055
+ });
17149
18056
 
17150
- // src/cli/schema/index.ts
17151
- var exports_schema = {};
17152
- __export(exports_schema, {
17153
- run: () => run18
18057
+ // src/cli/replace/index.ts
18058
+ var exports_replace2 = {};
18059
+ __export(exports_replace2, {
18060
+ run: () => run20
17154
18061
  });
17155
- import { parseArgs as parseArgs16 } from "util";
17156
- async function run18(args) {
18062
+ import { parseArgs as parseArgs17 } from "util";
18063
+ async function run20(args) {
17157
18064
  let parsed;
17158
18065
  try {
17159
- parsed = parseArgs16({
18066
+ parsed = parseArgs17({
17160
18067
  args,
17161
18068
  allowPositionals: true,
17162
18069
  options: {
17163
- json: { type: "boolean" },
17164
- ts: { type: "boolean" },
18070
+ regex: { type: "boolean" },
18071
+ "ignore-case": { type: "boolean" },
18072
+ all: { type: "boolean" },
18073
+ limit: { type: "string" },
18074
+ output: { type: "string", short: "o" },
18075
+ "dry-run": { type: "boolean" },
17165
18076
  help: { type: "boolean", short: "h" }
17166
18077
  }
17167
18078
  });
17168
- } catch (parseError) {
17169
- const message = parseError instanceof Error ? parseError.message : String(parseError);
17170
- return fail("USAGE", message, HELP18);
17171
- }
17172
- if (parsed.values.help) {
17173
- await writeStdout(HELP18);
18079
+ } catch (parseError) {
18080
+ const message = parseError instanceof Error ? parseError.message : String(parseError);
18081
+ return fail("USAGE", message, HELP20);
18082
+ }
18083
+ if (parsed.values.help) {
18084
+ await writeStdout(HELP20);
18085
+ return EXIT.OK;
18086
+ }
18087
+ const path = parsed.positionals[0];
18088
+ const pattern = parsed.positionals[1];
18089
+ const replacement = parsed.positionals[2];
18090
+ if (!path)
18091
+ return fail("USAGE", "Missing FILE argument", HELP20);
18092
+ if (pattern == null)
18093
+ return fail("USAGE", "Missing PATTERN argument", HELP20);
18094
+ if (replacement == null) {
18095
+ return fail("USAGE", "Missing REPLACEMENT argument", HELP20);
18096
+ }
18097
+ const ignoreCase = Boolean(parsed.values["ignore-case"]);
18098
+ const useRegex = Boolean(parsed.values.regex);
18099
+ const wantAll = Boolean(parsed.values.all);
18100
+ const limitRaw = parsed.values.limit;
18101
+ const limit = limitRaw === undefined ? undefined : Number(limitRaw);
18102
+ if (limit !== undefined && (!Number.isInteger(limit) || limit <= 0)) {
18103
+ return fail("USAGE", `--limit must be a positive integer, got "${limitRaw}"`);
18104
+ }
18105
+ const view = await openOrFail(path);
18106
+ if (typeof view === "number")
18107
+ return view;
18108
+ let allMatches;
18109
+ try {
18110
+ allMatches = findTextSpans(view.doc, pattern, {
18111
+ regex: useRegex,
18112
+ ignoreCase
18113
+ });
18114
+ } catch (matcherError) {
18115
+ const message = matcherError instanceof Error ? matcherError.message : String(matcherError);
18116
+ return fail("USAGE", `Invalid pattern: ${message}`);
18117
+ }
18118
+ let selected;
18119
+ if (limit !== undefined) {
18120
+ selected = allMatches.slice(0, limit);
18121
+ } else if (wantAll) {
18122
+ selected = allMatches;
18123
+ } else {
18124
+ selected = allMatches.slice(0, 1);
18125
+ }
18126
+ const matchesPayload = selected.map((match) => ({
18127
+ locator: `${match.blockId}:${match.start}-${match.end}`,
18128
+ blockId: match.blockId,
18129
+ start: match.start,
18130
+ end: match.end,
18131
+ text: match.text
18132
+ }));
18133
+ const outputPath = parsed.values.output;
18134
+ if (parsed.values["dry-run"]) {
18135
+ await respond({
18136
+ ok: true,
18137
+ operation: "replace",
18138
+ dryRun: true,
18139
+ path,
18140
+ pattern,
18141
+ replacement,
18142
+ regex: useRegex,
18143
+ ignoreCase,
18144
+ totalMatches: allMatches.length,
18145
+ replaced: selected.length,
18146
+ matches: matchesPayload,
18147
+ ...outputPath ? { output: outputPath } : {}
18148
+ });
17174
18149
  return EXIT.OK;
17175
18150
  }
17176
- if (parsed.values.ts) {
17177
- await Bun.write(Bun.stdout, types_default);
18151
+ if (selected.length === 0) {
18152
+ await respond({
18153
+ ok: true,
18154
+ operation: "replace",
18155
+ path,
18156
+ pattern,
18157
+ replacement,
18158
+ regex: useRegex,
18159
+ ignoreCase,
18160
+ totalMatches: 0,
18161
+ replaced: 0,
18162
+ matches: []
18163
+ });
17178
18164
  return EXIT.OK;
17179
18165
  }
17180
- await respond(JSON_SCHEMA);
18166
+ const reversed = [...selected].sort((leftMatch, rightMatch) => {
18167
+ if (leftMatch.blockId !== rightMatch.blockId) {
18168
+ return rightMatch.blockId.localeCompare(leftMatch.blockId);
18169
+ }
18170
+ return rightMatch.start - leftMatch.start;
18171
+ });
18172
+ const tracked = isTrackChangesEnabled(view) ? {
18173
+ meta: { author: resolveAuthor(), date: resolveDate() },
18174
+ allocator: createRevisionAllocator(view)
18175
+ } : undefined;
18176
+ const regexFlags = ignoreCase ? "i" : "";
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);
18181
+ }
18182
+ await saveDocView(view, outputPath);
18183
+ await respond({
18184
+ ok: true,
18185
+ operation: "replace",
18186
+ path: outputPath ?? path,
18187
+ pattern,
18188
+ replacement,
18189
+ regex: useRegex,
18190
+ ignoreCase,
18191
+ totalMatches: allMatches.length,
18192
+ replaced: selected.length,
18193
+ matches: matchesPayload
18194
+ });
17181
18195
  return EXIT.OK;
17182
18196
  }
17183
- var HELP18 = `docx schema \u2014 print the AST type definitions
18197
+ var HELP20 = `docx replace \u2014 substitute text spans (sed for docx)
17184
18198
 
17185
18199
  Usage:
17186
- docx schema [options]
18200
+ docx replace FILE PATTERN REPLACEMENT [options]
17187
18201
 
17188
18202
  Options:
17189
- --json Print as a JSON Schema document (default)
17190
- --ts Print TypeScript type source (read live from src/core/ast/types.ts)
17191
- -h, --help Show this help
18203
+ --regex treat PATTERN as a JavaScript regular expression
18204
+ --ignore-case case-insensitive match
18205
+ --all replace every match (default: just the first)
18206
+ --limit N replace at most N matches (in document order)
18207
+ -o, --output PATH write to PATH instead of overwriting FILE
18208
+ --dry-run report what would change without writing the file
18209
+ -h, --help show this help
18210
+
18211
+ Within-paragraph matches only. Run formatting (rPr) on the surrounding text
18212
+ is preserved; the replacement run inherits the rPr of the first run that
18213
+ overlaps the matched span. When a single invocation produces multiple
18214
+ replacements in the same paragraph, they're applied in reverse offset order
18215
+ so earlier offsets don't shift before being applied.
18216
+
18217
+ With --regex, REPLACEMENT supports JS String.replace substitution syntax:
18218
+ $1, $2, ... numbered capture groups
18219
+ $& the matched substring
18220
+ $\` text before the match
18221
+ $' text after the match
18222
+ $$ a literal $
17192
18223
 
17193
18224
  Examples:
17194
- docx schema | jq '.definitions.Run'
17195
- docx schema --ts > ast.d.ts
17196
- `, JSON_SCHEMA;
17197
- var init_schema = __esm(() => {
17198
- init_types();
18225
+ docx replace doc.docx "fox" "cat"
18226
+ docx replace doc.docx "fox" "cat" --all
18227
+ docx replace doc.docx "TODO|FIXME" "DONE" --regex --all
18228
+ docx replace doc.docx "(\\w+) (\\w+)" "$2 $1" --regex --all
18229
+ docx replace doc.docx "wordy phrase" "tighter phrase" --all --dry-run
18230
+ `;
18231
+ var init_replace2 = __esm(() => {
18232
+ init_core();
17199
18233
  init_respond();
17200
- JSON_SCHEMA = {
17201
- $schema: "https://json-schema.org/draft/2020-12/schema",
17202
- $id: "https://github.com/kklimuk/docx-cli/schema",
17203
- title: "docx-cli AST",
17204
- type: "object",
17205
- required: ["schemaVersion", "path", "properties", "blocks", "comments"],
17206
- properties: {
17207
- schemaVersion: { const: 1 },
17208
- path: { type: "string" },
17209
- properties: {
17210
- type: "object",
17211
- properties: {
17212
- title: { type: "string" },
17213
- author: { type: "string" },
17214
- created: { type: "string" },
17215
- modified: { type: "string" }
17216
- }
17217
- },
17218
- blocks: { type: "array", items: { $ref: "#/$defs/Block" } },
17219
- comments: { type: "array", items: { $ref: "#/$defs/Comment" } }
17220
- },
17221
- $defs: {
17222
- Block: {
17223
- oneOf: [
17224
- { $ref: "#/$defs/Paragraph" },
17225
- { $ref: "#/$defs/Table" },
17226
- { $ref: "#/$defs/SectionBreak" }
17227
- ]
17228
- },
17229
- Paragraph: {
17230
- type: "object",
17231
- required: ["id", "type", "runs"],
17232
- properties: {
17233
- id: { type: "string" },
17234
- type: { const: "paragraph" },
17235
- style: { type: "string" },
17236
- alignment: { enum: ["left", "center", "right", "justify"] },
17237
- list: {
17238
- type: "object",
17239
- required: ["level", "numId"],
17240
- properties: {
17241
- level: { type: "number" },
17242
- numId: { type: "string" }
17243
- }
17244
- },
17245
- runs: { type: "array", items: { $ref: "#/$defs/Run" } }
17246
- }
17247
- },
17248
- Run: {
17249
- oneOf: [
17250
- { $ref: "#/$defs/TextRun" },
17251
- { $ref: "#/$defs/ImageRun" },
17252
- { $ref: "#/$defs/BreakRun" },
17253
- { $ref: "#/$defs/TabRun" }
17254
- ]
17255
- },
17256
- TextRun: {
17257
- type: "object",
17258
- required: ["type", "text"],
17259
- properties: {
17260
- type: { const: "text" },
17261
- text: { type: "string" },
17262
- color: { type: "string" },
17263
- highlight: { type: "string" },
17264
- bold: { type: "boolean" },
17265
- italic: { type: "boolean" },
17266
- underline: { type: "string" },
17267
- strike: { type: "boolean" },
17268
- font: { type: "string" },
17269
- sizeHalfPoints: { type: "number" },
17270
- comments: { type: "array", items: { type: "string" } },
17271
- trackedChange: {
17272
- type: "object",
17273
- required: ["kind", "author", "date", "revisionId"],
17274
- properties: {
17275
- kind: { enum: ["ins", "del"] },
17276
- author: { type: "string" },
17277
- date: { type: "string" },
17278
- revisionId: { type: "string" }
17279
- }
17280
- }
17281
- }
17282
- },
17283
- ImageRun: {
17284
- type: "object",
17285
- required: ["type", "id", "contentType", "hash"],
17286
- properties: {
17287
- type: { const: "image" },
17288
- id: { type: "string" },
17289
- contentType: { type: "string" },
17290
- hash: { type: "string" },
17291
- widthEmu: { type: "number" },
17292
- heightEmu: { type: "number" },
17293
- alt: { type: "string" }
17294
- }
17295
- },
17296
- BreakRun: {
17297
- type: "object",
17298
- required: ["type", "kind"],
17299
- properties: {
17300
- type: { const: "break" },
17301
- kind: { enum: ["page", "line", "column"] }
17302
- }
17303
- },
17304
- TabRun: {
17305
- type: "object",
17306
- required: ["type"],
17307
- properties: { type: { const: "tab" } }
17308
- },
17309
- Table: {
17310
- type: "object",
17311
- required: ["id", "type", "rows"],
17312
- properties: {
17313
- id: { type: "string" },
17314
- type: { const: "table" },
17315
- rows: {
17316
- type: "array",
17317
- items: {
17318
- type: "object",
17319
- properties: {
17320
- cells: {
17321
- type: "array",
17322
- items: {
17323
- type: "object",
17324
- properties: {
17325
- blocks: {
17326
- type: "array",
17327
- items: { $ref: "#/$defs/Block" }
17328
- }
17329
- }
17330
- }
17331
- }
17332
- }
17333
- }
17334
- }
17335
- }
17336
- },
17337
- SectionBreak: {
17338
- type: "object",
17339
- required: ["id", "type"],
17340
- properties: {
17341
- id: { type: "string" },
17342
- type: { const: "sectionBreak" }
17343
- }
17344
- },
17345
- Comment: {
17346
- type: "object",
17347
- required: ["id", "author", "date", "text", "anchor"],
17348
- properties: {
17349
- id: { type: "string" },
17350
- author: { type: "string" },
17351
- initials: { type: "string" },
17352
- date: { type: "string" },
17353
- text: { type: "string" },
17354
- parentId: { type: "string" },
17355
- resolved: { type: "boolean" },
17356
- anchor: {
17357
- type: "object",
17358
- required: ["startBlockId", "startOffset", "endBlockId", "endOffset"],
17359
- properties: {
17360
- startBlockId: { type: "string" },
17361
- startOffset: { type: "number" },
17362
- endBlockId: { type: "string" },
17363
- endOffset: { type: "number" }
17364
- }
17365
- }
17366
- }
17367
- }
17368
- }
17369
- };
18234
+ init_replace_span();
17370
18235
  });
17371
18236
 
17372
18237
  // src/cli/track-changes/index.tsx
17373
18238
  var exports_track_changes = {};
17374
18239
  __export(exports_track_changes, {
17375
- run: () => run19
18240
+ run: () => run21
17376
18241
  });
17377
- import { parseArgs as parseArgs17 } from "util";
17378
- async function run19(args) {
18242
+ import { parseArgs as parseArgs18 } from "util";
18243
+ async function run21(args) {
17379
18244
  let parsed;
17380
18245
  try {
17381
- parsed = parseArgs17({
18246
+ parsed = parseArgs18({
17382
18247
  args,
17383
18248
  allowPositionals: true,
17384
18249
  options: {
18250
+ output: { type: "string", short: "o" },
17385
18251
  "dry-run": { type: "boolean" },
17386
18252
  help: { type: "boolean", short: "h" }
17387
18253
  }
17388
18254
  });
17389
18255
  } catch (parseError) {
17390
18256
  const message = parseError instanceof Error ? parseError.message : String(parseError);
17391
- return fail("USAGE", message, HELP19);
18257
+ return fail("USAGE", message, HELP21);
17392
18258
  }
17393
18259
  if (parsed.values.help) {
17394
- await writeStdout(HELP19);
18260
+ await writeStdout(HELP21);
17395
18261
  return EXIT.OK;
17396
18262
  }
17397
18263
  const path = parsed.positionals[0];
17398
18264
  if (!path)
17399
- return fail("USAGE", "Missing FILE argument", HELP19);
18265
+ return fail("USAGE", "Missing FILE argument", HELP21);
17400
18266
  const mode = parsed.positionals[1];
17401
18267
  if (mode !== "on" && mode !== "off") {
17402
- return fail("USAGE", `Expected "on" or "off", got: ${mode ?? "<nothing>"}`, HELP19);
17403
- }
17404
- let view;
17405
- try {
17406
- view = await openDocView(path);
17407
- } catch (openError) {
17408
- if (openError instanceof PkgError) {
17409
- if (openError.code === "FILE_NOT_FOUND") {
17410
- return fail("FILE_NOT_FOUND", openError.message);
17411
- }
17412
- if (openError.code === "NOT_A_ZIP") {
17413
- return fail("NOT_A_ZIP", openError.message);
17414
- }
17415
- }
17416
- throw openError;
17417
- }
17418
- const settingsXml = view.pkg.hasPart(SETTINGS_PART) ? await view.pkg.readText(SETTINGS_PART) : null;
17419
- const settingsTree = settingsXml ? XmlNode2.parse(settingsXml) : [];
17420
- let settingsRoot = XmlNode2.findRoot(settingsTree, "w:settings");
18268
+ return fail("USAGE", `Expected "on" or "off", got: ${mode ?? "<nothing>"}`, HELP21);
18269
+ }
18270
+ const view = await openOrFail(path);
18271
+ if (typeof view === "number")
18272
+ return view;
18273
+ const wasMissing = view.settingsTree === undefined;
18274
+ if (!view.settingsTree)
18275
+ view.settingsTree = [];
18276
+ let settingsRoot = XmlNode2.findRoot(view.settingsTree, "w:settings");
17421
18277
  if (!settingsRoot) {
17422
18278
  settingsRoot = /* @__PURE__ */ jsxDEV(w.settings, {
17423
18279
  "xmlns:w": NS_W2
17424
18280
  }, undefined, false, undefined, this);
17425
- settingsTree.push(settingsRoot);
18281
+ view.settingsTree.push(settingsRoot);
17426
18282
  }
17427
18283
  const hasTrackChanges = settingsRoot.children.some((child) => child.tag === "w:trackChanges");
18284
+ const outputPath = parsed.values.output;
17428
18285
  if (parsed.values["dry-run"]) {
17429
18286
  await respond({
17430
18287
  ok: true,
@@ -17432,7 +18289,8 @@ async function run19(args) {
17432
18289
  dryRun: true,
17433
18290
  path,
17434
18291
  mode,
17435
- previouslyOn: hasTrackChanges
18292
+ previouslyOn: hasTrackChanges,
18293
+ ...outputPath ? { output: outputPath } : {}
17436
18294
  });
17437
18295
  return EXIT.OK;
17438
18296
  }
@@ -17441,71 +18299,36 @@ async function run19(args) {
17441
18299
  } else if (mode === "off" && hasTrackChanges) {
17442
18300
  settingsRoot.children = settingsRoot.children.filter((child) => child.tag !== "w:trackChanges");
17443
18301
  }
17444
- view.pkg.writeText(SETTINGS_PART, XmlNode2.serializeWithDeclaration(settingsTree));
17445
- if (!settingsXml) {
17446
- registerSettingsPart(view);
18302
+ if (wasMissing) {
18303
+ registerPart(view.relationshipsTree, view.contentTypesTree, {
18304
+ partName: SETTINGS_PART,
18305
+ contentType: SETTINGS_CONTENT_TYPE,
18306
+ relationshipType: SETTINGS_REL_TYPE,
18307
+ target: "settings.xml"
18308
+ });
17447
18309
  }
17448
- view.pkg.writeText("word/_rels/document.xml.rels", XmlNode2.serializeWithDeclaration(view.relationshipsTree));
17449
- view.pkg.writeText("[Content_Types].xml", XmlNode2.serializeWithDeclaration(view.contentTypesTree));
17450
- await view.pkg.save();
18310
+ await saveDocView(view, outputPath);
17451
18311
  await respond({
17452
18312
  ok: true,
17453
18313
  operation: "track-changes",
17454
- path,
18314
+ path: outputPath ?? path,
17455
18315
  mode,
17456
18316
  previouslyOn: hasTrackChanges
17457
18317
  });
17458
18318
  return EXIT.OK;
17459
18319
  }
17460
- function registerSettingsPart(view) {
17461
- const relationships = XmlNode2.findRoot(view.relationshipsTree, "Relationships");
17462
- if (relationships) {
17463
- const alreadyLinked = relationships.children.some((child) => child.tag === "Relationship" && child.getAttribute("Type") === SETTINGS_REL_TYPE);
17464
- if (!alreadyLinked) {
17465
- relationships.children.push(new XmlNode2("Relationship", {
17466
- Id: nextRelationshipId2(relationships),
17467
- Type: SETTINGS_REL_TYPE,
17468
- Target: "settings.xml"
17469
- }));
17470
- }
17471
- }
17472
- const types = XmlNode2.findRoot(view.contentTypesTree, "Types");
17473
- if (types) {
17474
- const exists = types.children.some((child) => child.tag === "Override" && child.getAttribute("PartName") === `/${SETTINGS_PART}`);
17475
- if (!exists) {
17476
- types.children.push(new XmlNode2("Override", {
17477
- PartName: `/${SETTINGS_PART}`,
17478
- ContentType: SETTINGS_CONTENT_TYPE
17479
- }));
17480
- }
17481
- }
17482
- }
17483
- function nextRelationshipId2(relationships) {
17484
- let highest = 0;
17485
- for (const child of relationships.children) {
17486
- if (child.tag !== "Relationship")
17487
- continue;
17488
- const id = child.getAttribute("Id");
17489
- if (!id)
17490
- continue;
17491
- const match = id.match(/^rId(\d+)$/);
17492
- if (!match)
17493
- continue;
17494
- const numeric = Number(match[1]);
17495
- if (Number.isFinite(numeric) && numeric > highest)
17496
- highest = numeric;
17497
- }
17498
- return `rId${highest + 1}`;
17499
- }
17500
- var HELP19 = `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
17501
18321
 
17502
18322
  Usage:
17503
18323
  docx track-changes FILE on|off [options]
17504
18324
 
17505
- Sets <w:trackChanges/> in word/settings.xml. When on, Word records new
17506
- 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.
17507
18329
 
17508
18330
  Options:
18331
+ -o, --output PATH Write to PATH instead of overwriting FILE
17509
18332
  --dry-run Print what would change; do not write the file
17510
18333
  -h, --help Show this help
17511
18334
 
@@ -17513,17 +18336,79 @@ Examples:
17513
18336
  docx track-changes doc.docx on
17514
18337
  docx track-changes doc.docx off
17515
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";
17516
- var init_track_changes = __esm(() => {
18339
+ var init_track_changes2 = __esm(() => {
17517
18340
  init_core();
17518
18341
  init_jsx();
18342
+ init_package();
17519
18343
  init_parser();
17520
18344
  init_respond();
17521
18345
  init_jsx_dev_runtime();
17522
18346
  });
18347
+ // package.json
18348
+ var package_default = {
18349
+ name: "bun-docx",
18350
+ version: "0.3.0",
18351
+ description: "CLI for AI agents (Claude, Codex) to read, edit, and comment on .docx files with full format fidelity.",
18352
+ keywords: [
18353
+ "docx",
18354
+ "openxml",
18355
+ "ooxml",
18356
+ "word",
18357
+ "cli",
18358
+ "ai",
18359
+ "claude",
18360
+ "codex",
18361
+ "agentic"
18362
+ ],
18363
+ license: "MIT",
18364
+ author: "Kirill Klimuk",
18365
+ repository: {
18366
+ type: "git",
18367
+ url: "git+https://github.com/kklimuk/docx-cli.git"
18368
+ },
18369
+ homepage: "https://github.com/kklimuk/docx-cli#readme",
18370
+ bugs: "https://github.com/kklimuk/docx-cli/issues",
18371
+ type: "module",
18372
+ packageManager: "bun@1.3.13",
18373
+ bin: {
18374
+ docx: "./dist/index.js"
18375
+ },
18376
+ files: [
18377
+ "dist/index.js",
18378
+ "README.md",
18379
+ "LICENSE"
18380
+ ],
18381
+ engines: {
18382
+ bun: ">=1.3.0"
18383
+ },
18384
+ scripts: {
18385
+ dev: "bun src/index.ts",
18386
+ build: "bun build src/index.ts --outfile dist/index.js --target bun",
18387
+ "build:binary": "bun build --compile --outfile=dist/docx src/index.ts",
18388
+ check: "biome check . && knip-bun && bunx tsc --noEmit",
18389
+ test: "bun test",
18390
+ "test:unit": "bun test tests/core tests/cli",
18391
+ "test:integration": "bun test tests/integration",
18392
+ prepack: "bun run build",
18393
+ prepare: "husky"
18394
+ },
18395
+ devDependencies: {
18396
+ "@biomejs/biome": "^2.4.10",
18397
+ "@types/bun": "^1.3.13",
18398
+ husky: "^9.1.7",
18399
+ knip: "^6.3.0",
18400
+ typescript: "^6.0.2"
18401
+ },
18402
+ dependencies: {
18403
+ "fast-xml-builder": "^1.1.6",
18404
+ "fast-xml-parser": "^5.7.2",
18405
+ jszip: "^3.10.1"
18406
+ }
18407
+ };
17523
18408
 
17524
18409
  // src/cli/help.ts
17525
18410
  init_respond();
17526
- var VERSION = "0.1.0";
18411
+ var VERSION = package_default.version;
17527
18412
  var TOP_HELP = `docx ${VERSION} \u2014 read, edit, and comment on .docx files
17528
18413
 
17529
18414
  Usage:
@@ -17535,16 +18420,23 @@ Commands:
17535
18420
  edit FILE Replace text/properties at a locator
17536
18421
  insert FILE Insert a block or run
17537
18422
  delete FILE Remove a block or run range
17538
- comments \u2026 Add, reply, resolve, delete, restore, list comments
18423
+ find FILE QUERY Find text spans, return locators
18424
+ replace FILE PATTERN REPL Substitute text spans (sed for docx)
18425
+ comments \u2026 Add, reply, resolve, delete, list comments
17539
18426
  images \u2026 Extract, replace, list images
17540
18427
  track-changes FILE on|off Toggle tracked-changes mode
17541
- schema Dump AST JSON Schema
17542
- locators Dump locator grammar reference
18428
+ info \u2026 Reference material (schema, locators)
17543
18429
 
17544
18430
  Run "docx <command> --help" for command-specific help.
17545
18431
 
17546
18432
  Environment:
17547
- 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.
17548
18440
  `;
17549
18441
  async function printTopHelp() {
17550
18442
  await writeStdout(TOP_HELP);
@@ -17557,18 +18449,19 @@ var COMMANDS = {
17557
18449
  create: () => Promise.resolve().then(() => (init_create(), exports_create)),
17558
18450
  delete: () => Promise.resolve().then(() => (init_delete2(), exports_delete2)),
17559
18451
  edit: () => Promise.resolve().then(() => (init_edit(), exports_edit)),
18452
+ find: () => Promise.resolve().then(() => (init_find(), exports_find)),
17560
18453
  images: () => Promise.resolve().then(() => (init_images(), exports_images)),
18454
+ info: () => Promise.resolve().then(() => (init_info(), exports_info)),
17561
18455
  insert: () => Promise.resolve().then(() => (init_insert(), exports_insert)),
17562
- locators: () => Promise.resolve().then(() => (init_locators_cmd(), exports_locators_cmd)),
17563
18456
  read: () => Promise.resolve().then(() => (init_read2(), exports_read)),
17564
- schema: () => Promise.resolve().then(() => (init_schema(), exports_schema)),
17565
- "track-changes": () => Promise.resolve().then(() => (init_track_changes(), exports_track_changes))
18457
+ replace: () => Promise.resolve().then(() => (init_replace2(), exports_replace2)),
18458
+ "track-changes": () => Promise.resolve().then(() => (init_track_changes2(), exports_track_changes))
17566
18459
  };
17567
18460
  async function main(argv) {
17568
18461
  const args = argv.slice(2);
17569
18462
  const cmd = args[0];
17570
18463
  if (!cmd) {
17571
- process.stderr.write(`Usage: docx <command> [options]
18464
+ Bun.stderr.write(`Usage: docx <command> [options]
17572
18465
  Run "docx --help" for available commands.
17573
18466
  `);
17574
18467
  return 2;