bun-docx 0.1.0 → 0.2.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 +16 -14
  2. package/dist/index.js +1773 -1170
  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) {
@@ -13708,70 +13571,245 @@ var init_xml_node = __esm(() => {
13708
13571
  };
13709
13572
  });
13710
13573
 
13574
+ // src/core/parser/run-ops.ts
13575
+ function runTextLength(run) {
13576
+ let total = 0;
13577
+ for (const child of run.children) {
13578
+ if (child.tag === "w:t")
13579
+ total += child.collectText().length;
13580
+ }
13581
+ return total;
13582
+ }
13583
+ function sliceRun(run, start, end) {
13584
+ const sliced = new XmlNode2("w:r", { ...run.attributes });
13585
+ let consumed = 0;
13586
+ for (const child of run.children) {
13587
+ if (child.tag === "w:t") {
13588
+ const text = child.collectText();
13589
+ const localStart = Math.max(0, start - consumed);
13590
+ const localEnd = Math.min(text.length, end - consumed);
13591
+ if (localStart < localEnd) {
13592
+ const slicedText = new XmlNode2("w:t", { "xml:space": "preserve" });
13593
+ slicedText.children.push(XmlNode2.textNode(text.slice(localStart, localEnd)));
13594
+ sliced.children.push(slicedText);
13595
+ }
13596
+ consumed += text.length;
13597
+ continue;
13598
+ }
13599
+ sliced.children.push(deepCloneNode(child));
13600
+ }
13601
+ return sliced;
13602
+ }
13603
+ function deepCloneNode(node) {
13604
+ const clone = new XmlNode2(node.tag, { ...node.attributes });
13605
+ if (node.text !== undefined)
13606
+ clone.text = node.text;
13607
+ for (const child of node.children) {
13608
+ clone.children.push(deepCloneNode(child));
13609
+ }
13610
+ return clone;
13611
+ }
13612
+ var init_run_ops = __esm(() => {
13613
+ init_xml_node();
13614
+ });
13615
+
13711
13616
  // src/core/parser/index.ts
13712
13617
  var init_parser = __esm(() => {
13618
+ init_run_ops();
13713
13619
  init_xml_node();
13714
13620
  });
13715
13621
 
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 });
13622
+ // src/core/package/parts.ts
13623
+ function nextRelationshipId(relationships) {
13624
+ let highest = 0;
13625
+ for (const child of relationships.children) {
13626
+ if (child.tag !== "Relationship")
13747
13627
  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 });
13628
+ const id = child.getAttribute("Id");
13629
+ if (!id)
13753
13630
  continue;
13631
+ const match = id.match(/^rId(\d+)$/);
13632
+ if (!match)
13633
+ continue;
13634
+ const numeric = Number(match[1]);
13635
+ if (Number.isFinite(numeric) && numeric > highest)
13636
+ highest = numeric;
13637
+ }
13638
+ return `rId${highest + 1}`;
13639
+ }
13640
+ function registerPart(relationshipsTree, contentTypesTree, part) {
13641
+ const relationships = XmlNode2.findRoot(relationshipsTree, "Relationships");
13642
+ if (relationships) {
13643
+ const alreadyLinked = relationships.children.some((child) => child.tag === "Relationship" && child.getAttribute("Type") === part.relationshipType);
13644
+ if (!alreadyLinked) {
13645
+ relationships.children.push(new XmlNode2("Relationship", {
13646
+ Id: nextRelationshipId(relationships),
13647
+ Type: part.relationshipType,
13648
+ Target: part.target
13649
+ }));
13754
13650
  }
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 });
13651
+ }
13652
+ const types = XmlNode2.findRoot(contentTypesTree, "Types");
13653
+ if (types) {
13654
+ const overrideExists = types.children.some((child) => child.tag === "Override" && child.getAttribute("PartName") === `/${part.partName}`);
13655
+ if (!overrideExists) {
13656
+ types.children.push(new XmlNode2("Override", {
13657
+ PartName: `/${part.partName}`,
13658
+ ContentType: part.contentType
13659
+ }));
13759
13660
  }
13760
13661
  }
13761
- return blocks;
13762
13662
  }
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);
13768
- }
13769
- const activeComments = new Set;
13770
- let offset = 0;
13771
- for (const child of node.children) {
13772
- if (child.tag === "w:pPr")
13773
- continue;
13774
- if (child.tag === "w:commentRangeStart") {
13663
+ var init_parts = __esm(() => {
13664
+ init_parser();
13665
+ });
13666
+
13667
+ // src/core/package/index.ts
13668
+ class Pkg {
13669
+ zip;
13670
+ path;
13671
+ constructor(zip, path) {
13672
+ this.zip = zip;
13673
+ this.path = path;
13674
+ }
13675
+ static async open(path) {
13676
+ const file = Bun.file(path);
13677
+ if (!await file.exists()) {
13678
+ throw new PkgError("FILE_NOT_FOUND", `File not found: ${path}`);
13679
+ }
13680
+ const buf = await file.arrayBuffer();
13681
+ let zip;
13682
+ try {
13683
+ zip = await import_jszip.default.loadAsync(buf);
13684
+ } catch (err) {
13685
+ const reason = err instanceof Error ? err.message : String(err);
13686
+ throw new PkgError("NOT_A_ZIP", `Not a valid .docx (zip): ${reason}`);
13687
+ }
13688
+ return new Pkg(zip, path);
13689
+ }
13690
+ hasPart(name) {
13691
+ return this.zip.file(name) !== null;
13692
+ }
13693
+ listParts() {
13694
+ const names = [];
13695
+ this.zip.forEach((p) => {
13696
+ names.push(p);
13697
+ });
13698
+ return names;
13699
+ }
13700
+ async readText(name) {
13701
+ const entry = this.zip.file(name);
13702
+ if (!entry) {
13703
+ throw new PkgError("PART_NOT_FOUND", `Part not found: ${name}`);
13704
+ }
13705
+ return entry.async("string");
13706
+ }
13707
+ async readBytes(name) {
13708
+ const entry = this.zip.file(name);
13709
+ if (!entry) {
13710
+ throw new PkgError("PART_NOT_FOUND", `Part not found: ${name}`);
13711
+ }
13712
+ return entry.async("uint8array");
13713
+ }
13714
+ writeText(name, content) {
13715
+ this.zip.file(name, content);
13716
+ }
13717
+ writeBytes(name, content) {
13718
+ this.zip.file(name, content);
13719
+ }
13720
+ deletePart(name) {
13721
+ this.zip.remove(name);
13722
+ }
13723
+ async save(path) {
13724
+ const target = path ?? this.path;
13725
+ const buf = await this.zip.generateAsync({
13726
+ type: "uint8array",
13727
+ compression: "DEFLATE",
13728
+ compressionOptions: { level: 6 }
13729
+ });
13730
+ await Bun.write(target, buf);
13731
+ }
13732
+ async toBytes() {
13733
+ return this.zip.generateAsync({
13734
+ type: "uint8array",
13735
+ compression: "DEFLATE",
13736
+ compressionOptions: { level: 6 }
13737
+ });
13738
+ }
13739
+ }
13740
+ var import_jszip, PkgError;
13741
+ var init_package = __esm(() => {
13742
+ init_parts();
13743
+ import_jszip = __toESM(require_lib3(), 1);
13744
+ PkgError = class PkgError extends Error {
13745
+ code;
13746
+ constructor(code, message) {
13747
+ super(message);
13748
+ this.code = code;
13749
+ this.name = "PkgError";
13750
+ }
13751
+ };
13752
+ });
13753
+
13754
+ // src/core/ast/read.ts
13755
+ function buildDoc(view, path) {
13756
+ readImageRelationships(view);
13757
+ const properties = readDocProperties(view);
13758
+ const documentRoot = XmlNode2.findRoot(view.documentTree, "w:document");
13759
+ if (!documentRoot) {
13760
+ throw new Error("Invalid .docx: missing <w:document>");
13761
+ }
13762
+ const body = documentRoot.findChild("w:body");
13763
+ if (!body) {
13764
+ throw new Error("Invalid .docx: missing <w:body>");
13765
+ }
13766
+ const state = {
13767
+ imageIndex: 0,
13768
+ commentAnchors: new Map,
13769
+ openComments: new Map
13770
+ };
13771
+ const blocks = readBlocks(view, body, state);
13772
+ const comments = readComments(view, state.commentAnchors);
13773
+ return { schemaVersion: 1, path, properties, blocks, comments };
13774
+ }
13775
+ function readBlocks(view, body, state) {
13776
+ const blocks = [];
13777
+ let paragraphIndex = 0;
13778
+ let tableIndex = 0;
13779
+ let sectionIndex = 0;
13780
+ for (const child of body.children) {
13781
+ if (child.tag === "w:p") {
13782
+ const id = `p${paragraphIndex++}`;
13783
+ blocks.push(readParagraph(view, child, id, state));
13784
+ view.blockReferences.set(id, { node: child, parent: body.children });
13785
+ continue;
13786
+ }
13787
+ if (child.tag === "w:tbl") {
13788
+ const id = `t${tableIndex++}`;
13789
+ blocks.push(readTable(view, child, id, state));
13790
+ view.blockReferences.set(id, { node: child, parent: body.children });
13791
+ continue;
13792
+ }
13793
+ if (child.tag === "w:sectPr") {
13794
+ const id = `s${sectionIndex++}`;
13795
+ blocks.push({ id, type: "sectionBreak" });
13796
+ view.blockReferences.set(id, { node: child, parent: body.children });
13797
+ }
13798
+ }
13799
+ return blocks;
13800
+ }
13801
+ function readParagraph(view, node, id, state) {
13802
+ const paragraph = { id, type: "paragraph", runs: [] };
13803
+ const paragraphProperties = node.findChild("w:pPr");
13804
+ if (paragraphProperties) {
13805
+ applyParagraphProperties(paragraph, paragraphProperties);
13806
+ }
13807
+ const activeComments = new Set;
13808
+ let offset = 0;
13809
+ for (const child of node.children) {
13810
+ if (child.tag === "w:pPr")
13811
+ continue;
13812
+ if (child.tag === "w:commentRangeStart") {
13775
13813
  const commentId = child.getAttribute("w:id");
13776
13814
  if (commentId) {
13777
13815
  const key = `c${commentId}`;
@@ -13815,6 +13853,33 @@ function readParagraph(view, node, id, state) {
13815
13853
  revisionId: child.getAttribute("w:id") ?? ""
13816
13854
  };
13817
13855
  for (const inner of child.children) {
13856
+ if (inner.tag === "w:commentRangeStart") {
13857
+ const commentId = inner.getAttribute("w:id");
13858
+ if (commentId) {
13859
+ const key = `c${commentId}`;
13860
+ activeComments.add(key);
13861
+ state.openComments.set(key, { blockId: id, offset });
13862
+ }
13863
+ continue;
13864
+ }
13865
+ if (inner.tag === "w:commentRangeEnd") {
13866
+ const commentId = inner.getAttribute("w:id");
13867
+ if (commentId) {
13868
+ const key = `c${commentId}`;
13869
+ activeComments.delete(key);
13870
+ const opened = state.openComments.get(key);
13871
+ if (opened) {
13872
+ state.commentAnchors.set(key, {
13873
+ startBlockId: opened.blockId,
13874
+ startOffset: opened.offset,
13875
+ endBlockId: id,
13876
+ endOffset: offset
13877
+ });
13878
+ state.openComments.delete(key);
13879
+ }
13880
+ }
13881
+ continue;
13882
+ }
13818
13883
  if (inner.tag !== "w:r")
13819
13884
  continue;
13820
13885
  const run = readRun(view, inner, activeComments, change, state);
@@ -13954,26 +14019,33 @@ function readImageFromDrawing(view, drawing, state) {
13954
14019
  }
13955
14020
  function readTable(view, node, id, state) {
13956
14021
  const rows = [];
14022
+ let rowIndex = 0;
13957
14023
  for (const child of node.children) {
13958
14024
  if (child.tag !== "w:tr")
13959
14025
  continue;
13960
14026
  const cells = [];
14027
+ let columnIndex = 0;
13961
14028
  for (const cellNode of child.children) {
13962
14029
  if (cellNode.tag !== "w:tc")
13963
14030
  continue;
13964
- cells.push({ blocks: readCellBlocks(view, cellNode, state) });
14031
+ cells.push({
14032
+ blocks: readCellBlocks(view, cellNode, id, rowIndex, columnIndex, state)
14033
+ });
14034
+ columnIndex++;
13965
14035
  }
13966
14036
  rows.push({ cells });
14037
+ rowIndex++;
13967
14038
  }
13968
14039
  return { id, type: "table", rows };
13969
14040
  }
13970
- function readCellBlocks(view, cell, state) {
14041
+ function readCellBlocks(view, cell, tableId, rowIndex, columnIndex, state) {
13971
14042
  const blocks = [];
13972
14043
  let paragraphIndex = 0;
13973
14044
  for (const child of cell.children) {
13974
14045
  if (child.tag === "w:p") {
13975
- const id = `cellp${paragraphIndex++}`;
14046
+ const id = `${tableId}:r${rowIndex}c${columnIndex}:p${paragraphIndex++}`;
13976
14047
  blocks.push(readParagraph(view, child, id, state));
14048
+ view.blockReferences.set(id, { node: child, parent: cell.children });
13977
14049
  }
13978
14050
  }
13979
14051
  return blocks;
@@ -14136,6 +14208,7 @@ async function openDocView(path) {
14136
14208
  const commentsTree = pkg.hasPart("word/comments.xml") ? XmlNode2.parse(await pkg.readText("word/comments.xml")) : undefined;
14137
14209
  const commentsExtTree = pkg.hasPart("word/commentsExtended.xml") ? XmlNode2.parse(await pkg.readText("word/commentsExtended.xml")) : undefined;
14138
14210
  const corePropertiesTree = pkg.hasPart("docProps/core.xml") ? XmlNode2.parse(await pkg.readText("docProps/core.xml")) : undefined;
14211
+ const settingsTree = pkg.hasPart("word/settings.xml") ? XmlNode2.parse(await pkg.readText("word/settings.xml")) : undefined;
14139
14212
  const view = {
14140
14213
  pkg,
14141
14214
  documentTree,
@@ -14144,6 +14217,7 @@ async function openDocView(path) {
14144
14217
  relationshipsTree,
14145
14218
  contentTypesTree,
14146
14219
  corePropertiesTree,
14220
+ settingsTree,
14147
14221
  doc: undefined,
14148
14222
  blockReferences: new Map,
14149
14223
  commentReferences: new Map,
@@ -14163,6 +14237,9 @@ async function saveDocView(view, path) {
14163
14237
  if (view.commentsExtTree) {
14164
14238
  view.pkg.writeText("word/commentsExtended.xml", XmlNode2.serialize(view.commentsExtTree));
14165
14239
  }
14240
+ if (view.settingsTree) {
14241
+ view.pkg.writeText("word/settings.xml", XmlNode2.serialize(view.settingsTree));
14242
+ }
14166
14243
  await view.pkg.save(path);
14167
14244
  }
14168
14245
  async function enrichImageHashes(view) {
@@ -14290,7 +14367,7 @@ var init_parse = __esm(() => {
14290
14367
  this.name = "LocatorParseError";
14291
14368
  }
14292
14369
  };
14293
- BLOCK_RE = /^(p|t|s|cellp)(\d+)$/;
14370
+ BLOCK_RE = /^(p|t|s)(\d+)$/;
14294
14371
  SPAN_RE = /^p(\d+):(\d+)-(\d+)$/;
14295
14372
  RANGE_RE = /^p(\d+):(\d+)-p(\d+):(\d+)$/;
14296
14373
  COMMENT_RE = /^c(\d+)$/;
@@ -14299,6 +14376,26 @@ var init_parse = __esm(() => {
14299
14376
  });
14300
14377
 
14301
14378
  // src/core/locators/resolve.ts
14379
+ function locatorToBlockTarget(locator) {
14380
+ if (locator.kind === "block")
14381
+ return { blockId: locator.blockId };
14382
+ if (locator.kind === "blockSpan") {
14383
+ return {
14384
+ blockId: locator.blockId,
14385
+ span: { start: locator.start, end: locator.end }
14386
+ };
14387
+ }
14388
+ if (locator.kind === "cell" && locator.inner) {
14389
+ const inner = locatorToBlockTarget(locator.inner);
14390
+ if (!inner)
14391
+ return null;
14392
+ return {
14393
+ blockId: `${locator.tableId}:r${locator.row}c${locator.col}:${inner.blockId}`,
14394
+ span: inner.span
14395
+ };
14396
+ }
14397
+ return null;
14398
+ }
14302
14399
  function resolveBlock(view, blockId) {
14303
14400
  const reference = view.blockReferences.get(blockId);
14304
14401
  if (!reference) {
@@ -14332,6 +14429,78 @@ var init_core = __esm(() => {
14332
14429
  init_parser();
14333
14430
  });
14334
14431
 
14432
+ // src/cli/respond.ts
14433
+ async function respond(payload) {
14434
+ await Bun.write(Bun.stdout, `${JSON.stringify(payload)}
14435
+ `);
14436
+ }
14437
+ async function writeStdout(text) {
14438
+ await Bun.write(Bun.stdout, text);
14439
+ }
14440
+ async function fail(code, message, hint) {
14441
+ const payload = {
14442
+ ok: false,
14443
+ code,
14444
+ error: message
14445
+ };
14446
+ if (hint)
14447
+ payload.hint = hint;
14448
+ await respond(payload);
14449
+ return exitCodeFor(code);
14450
+ }
14451
+ function exitCodeFor(code) {
14452
+ switch (code) {
14453
+ case "USAGE":
14454
+ case "INVALID_LOCATOR":
14455
+ return EXIT.USAGE_ERROR;
14456
+ case "FILE_NOT_FOUND":
14457
+ case "PART_NOT_FOUND":
14458
+ case "BLOCK_NOT_FOUND":
14459
+ case "COMMENT_NOT_FOUND":
14460
+ case "IMAGE_NOT_FOUND":
14461
+ case "MATCH_NOT_FOUND":
14462
+ return EXIT.NOT_FOUND;
14463
+ case "NOT_A_ZIP":
14464
+ case "TRACKED_CHANGE_CONFLICT":
14465
+ case "UNHANDLED":
14466
+ return EXIT.GENERAL_ERROR;
14467
+ }
14468
+ }
14469
+ async function openOrFail(path) {
14470
+ try {
14471
+ return await openDocView(path);
14472
+ } catch (err) {
14473
+ if (err instanceof PkgError) {
14474
+ if (err.code === "FILE_NOT_FOUND") {
14475
+ return await fail("FILE_NOT_FOUND", err.message);
14476
+ }
14477
+ if (err.code === "NOT_A_ZIP")
14478
+ return await fail("NOT_A_ZIP", err.message);
14479
+ }
14480
+ throw err;
14481
+ }
14482
+ }
14483
+ async function resolveBlockOrFail(view, locator) {
14484
+ try {
14485
+ return resolveBlock(view, locator);
14486
+ } catch (err) {
14487
+ if (err instanceof LocatorResolveError) {
14488
+ return await fail("BLOCK_NOT_FOUND", err.message);
14489
+ }
14490
+ throw err;
14491
+ }
14492
+ }
14493
+ var EXIT;
14494
+ var init_respond = __esm(() => {
14495
+ init_core();
14496
+ EXIT = {
14497
+ OK: 0,
14498
+ GENERAL_ERROR: 1,
14499
+ USAGE_ERROR: 2,
14500
+ NOT_FOUND: 3
14501
+ };
14502
+ });
14503
+
14335
14504
  // src/core/jsx/index.ts
14336
14505
  function namespace(prefix, tags) {
14337
14506
  const result = {};
@@ -14548,7 +14717,7 @@ function ensureCommentsPart(view) {
14548
14717
  "xmlns:w14": NS_W14
14549
14718
  }, undefined, false, undefined, this);
14550
14719
  view.commentsTree = [root];
14551
- registerPart(view, {
14720
+ registerPart(view.relationshipsTree, view.contentTypesTree, {
14552
14721
  partName: "word/comments.xml",
14553
14722
  contentType: COMMENTS_CONTENT_TYPE,
14554
14723
  relationshipType: COMMENTS_REL_TYPE,
@@ -14568,7 +14737,7 @@ function ensureCommentsExtPart(view) {
14568
14737
  "mc:Ignorable": "w15"
14569
14738
  }, undefined, false, undefined, this);
14570
14739
  view.commentsExtTree = [root];
14571
- registerPart(view, {
14740
+ registerPart(view.relationshipsTree, view.contentTypesTree, {
14572
14741
  partName: "word/commentsExtended.xml",
14573
14742
  contentType: COMMENTS_EXT_CONTENT_TYPE,
14574
14743
  relationshipType: COMMENTS_EXT_REL_TYPE,
@@ -14576,77 +14745,66 @@ function ensureCommentsExtPart(view) {
14576
14745
  });
14577
14746
  return root;
14578
14747
  }
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
- }));
14748
+ function paragraphTextLength(paragraph) {
14749
+ let total = 0;
14750
+ for (const child of paragraph.children) {
14751
+ if (child.tag === "w:r")
14752
+ total += runTextLength(child);
14753
+ else if (child.tag === "w:ins" || child.tag === "w:del") {
14754
+ for (const inner of child.children) {
14755
+ if (inner.tag === "w:r")
14756
+ total += runTextLength(inner);
14757
+ }
14589
14758
  }
14590
14759
  }
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
- }
14760
+ return total;
14761
+ }
14762
+ function addCommentMarkersToParagraph(paragraph, commentId, span) {
14763
+ const total = paragraphTextLength(paragraph);
14764
+ const range = span ?? { start: 0, end: total };
14765
+ if (range.start < 0 || range.end > total || range.start > range.end) {
14766
+ throw new SpanOutOfRangeError(`Span ${range.start}-${range.end} out of paragraph length ${total}`);
14600
14767
  }
14768
+ placeMarkersInParagraph(paragraph, [
14769
+ { offset: range.start, node: commentRangeStartMarker(commentId) },
14770
+ {
14771
+ offset: range.end,
14772
+ node: commentRangeEndMarker(commentId),
14773
+ follower: commentReferenceRun(commentId)
14774
+ }
14775
+ ]);
14601
14776
  }
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;
14777
+ function addCommentRangeMarkers(startParagraph, startOffset, endParagraph, endOffset, commentId) {
14778
+ if (startParagraph === endParagraph) {
14779
+ addCommentMarkersToParagraph(startParagraph, commentId, {
14780
+ start: startOffset,
14781
+ end: endOffset
14782
+ });
14783
+ return;
14616
14784
  }
14617
- return `rId${highest + 1}`;
14618
- }
14619
- function paragraphTextLength(paragraph) {
14620
- let total = 0;
14621
- for (const child of paragraph.children) {
14622
- if (child.tag === "w:r")
14623
- total += runTextLength(child);
14624
- else if (child.tag === "w:ins" || child.tag === "w:del") {
14625
- for (const inner of child.children) {
14626
- if (inner.tag === "w:r")
14627
- total += runTextLength(inner);
14628
- }
14785
+ placeMarkersInParagraph(startParagraph, [
14786
+ { offset: startOffset, node: commentRangeStartMarker(commentId) }
14787
+ ]);
14788
+ placeMarkersInParagraph(endParagraph, [
14789
+ {
14790
+ offset: endOffset,
14791
+ node: commentRangeEndMarker(commentId),
14792
+ follower: commentReferenceRun(commentId)
14629
14793
  }
14630
- }
14631
- return total;
14794
+ ]);
14632
14795
  }
14633
- function addCommentMarkersToParagraph(paragraph, commentId, span) {
14634
- const total = paragraphTextLength(paragraph);
14635
- const range = span ?? { start: 0, end: total };
14636
- if (range.start < 0 || range.end > total || range.start > range.end) {
14637
- throw new SpanOutOfRangeError(`Span ${range.start}-${range.end} out of paragraph length ${total}`);
14638
- }
14639
- const newChildren = [];
14640
- let offset = 0;
14641
- let placedStart = false;
14642
- let placedEnd = false;
14643
- const startMarker = /* @__PURE__ */ jsxDEV(w.commentRangeStart, {
14796
+ function commentRangeStartMarker(commentId) {
14797
+ return /* @__PURE__ */ jsxDEV(w.commentRangeStart, {
14644
14798
  "w-id": commentId
14645
14799
  }, undefined, false, undefined, this);
14646
- const endMarker = /* @__PURE__ */ jsxDEV(w.commentRangeEnd, {
14800
+ }
14801
+ function commentRangeEndMarker(commentId) {
14802
+ return /* @__PURE__ */ jsxDEV(w.commentRangeEnd, {
14647
14803
  "w-id": commentId
14648
14804
  }, undefined, false, undefined, this);
14649
- const referenceRun = /* @__PURE__ */ jsxDEV(w.r, {
14805
+ }
14806
+ function commentReferenceRun(commentId) {
14807
+ return /* @__PURE__ */ jsxDEV(w.r, {
14650
14808
  children: [
14651
14809
  /* @__PURE__ */ jsxDEV(w.rPr, {
14652
14810
  children: /* @__PURE__ */ jsxDEV(w.rStyle, {
@@ -14658,108 +14816,97 @@ function addCommentMarkersToParagraph(paragraph, commentId, span) {
14658
14816
  }, undefined, false, undefined, this)
14659
14817
  ]
14660
14818
  }, 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);
14819
+ }
14820
+ function placeMarkersInParagraph(paragraph, markers) {
14821
+ if (markers.length === 0)
14822
+ return;
14823
+ const total = paragraphTextLength(paragraph);
14824
+ for (const marker of markers) {
14825
+ if (marker.offset < 0 || marker.offset > total) {
14826
+ throw new SpanOutOfRangeError(`Marker offset ${marker.offset} out of paragraph length ${total}`);
14827
+ }
14828
+ }
14829
+ const pending = markers.slice();
14830
+ const state = { offset: 0, placedCount: 0 };
14831
+ paragraph.children = walkAndPlace(paragraph.children, pending, true, state);
14832
+ flushAtCurrentOffset(paragraph.children, pending, state);
14833
+ if (state.placedCount !== markers.length) {
14834
+ throw new SpanOutOfRangeError(`Could not place comment markers (placed ${state.placedCount} of ${markers.length})`);
14835
+ }
14836
+ }
14837
+ function walkAndPlace(children, pending, isParagraphLevel, state) {
14838
+ const result = [];
14839
+ for (const child of children) {
14840
+ if (child.tag === "w:r") {
14841
+ const length = runTextLength(child);
14842
+ const runStart = state.offset;
14843
+ const runEnd = state.offset + length;
14844
+ const splits = [];
14845
+ for (let i = 0;i < pending.length; i++) {
14846
+ const marker = pending[i];
14847
+ if (!marker)
14668
14848
  continue;
14849
+ if (runStart <= marker.offset && marker.offset <= runEnd) {
14850
+ splits.push({ at: marker.offset - runStart, index: i });
14669
14851
  }
14670
- newChildren.push(startMarker);
14671
14852
  }
14672
- if (!placedEnd && offset === range.end && placedStart) {
14673
- placedEnd = true;
14674
- newChildren.push(endMarker);
14675
- newChildren.push(referenceRun);
14853
+ splits.sort((left, right) => left.at - right.at || left.index - right.index);
14854
+ if (splits.length === 0) {
14855
+ result.push(child);
14856
+ } else {
14857
+ let cursor = 0;
14858
+ for (const split of splits) {
14859
+ if (split.at > cursor) {
14860
+ result.push(sliceRun(child, cursor, split.at));
14861
+ }
14862
+ const marker = pending[split.index];
14863
+ if (!marker)
14864
+ continue;
14865
+ result.push(marker.node);
14866
+ if (marker.follower)
14867
+ result.push(marker.follower);
14868
+ pending[split.index] = null;
14869
+ state.placedCount++;
14870
+ cursor = split.at;
14871
+ }
14872
+ if (cursor < length) {
14873
+ result.push(sliceRun(child, cursor, length));
14874
+ }
14676
14875
  }
14677
- newChildren.push(child);
14876
+ state.offset = runEnd;
14678
14877
  continue;
14679
14878
  }
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
- }
14879
+ if (isParagraphLevel && (child.tag === "w:ins" || child.tag === "w:del")) {
14880
+ flushAtCurrentOffset(result, pending, state);
14881
+ const innerChildren = walkAndPlace(child.children, pending, false, state);
14882
+ const wrapper = new XmlNode2(child.tag, { ...child.attributes });
14883
+ wrapper.children = innerChildren;
14884
+ result.push(wrapper);
14885
+ continue;
14710
14886
  }
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;
14887
+ if (child.tag === "w:pPr") {
14888
+ result.push(child);
14889
+ flushAtCurrentOffset(result, pending, state);
14749
14890
  continue;
14750
14891
  }
14751
- sliced.children.push(deepCloneNode(child));
14892
+ flushAtCurrentOffset(result, pending, state);
14893
+ result.push(child);
14752
14894
  }
14753
- return sliced;
14895
+ return result;
14754
14896
  }
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));
14897
+ function flushAtCurrentOffset(out, pending, state) {
14898
+ for (let i = 0;i < pending.length; i++) {
14899
+ const marker = pending[i];
14900
+ if (!marker)
14901
+ continue;
14902
+ if (marker.offset !== state.offset)
14903
+ continue;
14904
+ out.push(marker.node);
14905
+ if (marker.follower)
14906
+ out.push(marker.follower);
14907
+ pending[i] = null;
14908
+ state.placedCount++;
14761
14909
  }
14762
- return clone;
14763
14910
  }
14764
14911
  function CommentBody({
14765
14912
  options
@@ -14871,6 +15018,7 @@ function containsCommentReference(run, numericId) {
14871
15018
  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
15019
  var init_helpers = __esm(() => {
14873
15020
  init_jsx();
15021
+ init_package();
14874
15022
  init_parser();
14875
15023
  init_jsx_dev_runtime();
14876
15024
  SpanOutOfRangeError = class SpanOutOfRangeError extends Error {
@@ -14927,34 +15075,75 @@ async function run(args) {
14927
15075
  }
14928
15076
  throw error;
14929
15077
  }
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;
15078
+ if (locator.kind === "range") {
15079
+ return runCrossBlock(parsed, path, rangeInput, locator, text);
15080
+ }
15081
+ const target = locatorToBlockTarget(locator);
15082
+ if (!target) {
15083
+ 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.");
15084
+ }
15085
+ const blockId = target.blockId;
15086
+ const view = await openOrFail(path);
15087
+ if (typeof view === "number")
15088
+ return view;
15089
+ const paragraphRef = await resolveBlockOrFail(view, blockId);
15090
+ if (typeof paragraphRef === "number")
15091
+ return paragraphRef;
15092
+ const span = target.span;
15093
+ const author = parsed.values.author ?? Bun.env.DOCX_AUTHOR ?? "";
15094
+ const date = new Date().toISOString();
15095
+ const numericId = nextCommentId(view);
15096
+ const paraId = generateParaId();
15097
+ if (parsed.values["dry-run"]) {
15098
+ await respond({
15099
+ ok: true,
15100
+ operation: "comments.add",
15101
+ dryRun: true,
15102
+ path,
15103
+ commentId: `c${numericId}`,
15104
+ locator: rangeInput
15105
+ });
15106
+ return EXIT.OK;
14947
15107
  }
14948
- let paragraphRef;
14949
15108
  try {
14950
- paragraphRef = resolveBlock(view, blockId);
15109
+ addCommentMarkersToParagraph(paragraphRef.node, numericId, span);
14951
15110
  } catch (error) {
14952
- if (error instanceof LocatorResolveError) {
14953
- return fail("BLOCK_NOT_FOUND", error.message);
15111
+ if (error instanceof SpanOutOfRangeError) {
15112
+ return fail("INVALID_LOCATOR", error.message);
14954
15113
  }
14955
15114
  throw error;
14956
15115
  }
14957
- const span = locator.kind === "blockSpan" ? { start: locator.start, end: locator.end } : undefined;
15116
+ const commentsRoot = ensureCommentsPart(view);
15117
+ commentsRoot.children.push(/* @__PURE__ */ jsxDEV(CommentBody, {
15118
+ options: {
15119
+ id: numericId,
15120
+ author,
15121
+ date,
15122
+ initials: authorInitials(author),
15123
+ paraId,
15124
+ text
15125
+ }
15126
+ }, undefined, false, undefined, this));
15127
+ await saveDocView(view);
15128
+ await respond({
15129
+ ok: true,
15130
+ operation: "comments.add",
15131
+ path,
15132
+ commentId: `c${numericId}`,
15133
+ locator: rangeInput
15134
+ });
15135
+ return EXIT.OK;
15136
+ }
15137
+ async function runCrossBlock(parsed, path, rangeInput, locator, text) {
15138
+ const view = await openOrFail(path);
15139
+ if (typeof view === "number")
15140
+ return view;
15141
+ const startRef = await resolveBlockOrFail(view, locator.start.blockId);
15142
+ if (typeof startRef === "number")
15143
+ return startRef;
15144
+ const endRef = await resolveBlockOrFail(view, locator.end.blockId);
15145
+ if (typeof endRef === "number")
15146
+ return endRef;
14958
15147
  const author = parsed.values.author ?? Bun.env.DOCX_AUTHOR ?? "";
14959
15148
  const date = new Date().toISOString();
14960
15149
  const numericId = nextCommentId(view);
@@ -14971,7 +15160,7 @@ async function run(args) {
14971
15160
  return EXIT.OK;
14972
15161
  }
14973
15162
  try {
14974
- addCommentMarkersToParagraph(paragraphRef.node, numericId, span);
15163
+ addCommentRangeMarkers(startRef.node, locator.start.offset, endRef.node, locator.end.offset, numericId);
14975
15164
  } catch (error) {
14976
15165
  if (error instanceof SpanOutOfRangeError) {
14977
15166
  return fail("INVALID_LOCATOR", error.message);
@@ -15005,8 +15194,12 @@ Usage:
15005
15194
  docx comments add FILE [options]
15006
15195
 
15007
15196
  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)
15197
+ --range LOCATOR Where to anchor. Supports:
15198
+ pN whole paragraph
15199
+ pN:S-E chars S..E of pN
15200
+ pN:S-pM:E chars S of pN through char E of pM (cross-paragraph)
15201
+ tT:rRcC:pK whole cell paragraph
15202
+ tT:rRcC:pK:S-E chars S..E of cell paragraph
15010
15203
  --text TEXT Comment body
15011
15204
 
15012
15205
  Optional:
@@ -15017,6 +15210,7 @@ Optional:
15017
15210
  Examples:
15018
15211
  docx comments add doc.docx --range p3 --text "Reconsider this paragraph"
15019
15212
  docx comments add doc.docx --range p3:5-20 --text "Sharper wording?" --author "Jane"
15213
+ docx comments add doc.docx --range p3:5-p5:10 --text "Whole section?" --author "Reviewer"
15020
15214
  `;
15021
15215
  var init_add = __esm(() => {
15022
15216
  init_core();
@@ -15106,20 +15300,9 @@ async function run2(args) {
15106
15300
  return fail("USAGE", "Missing --id COMMENT_ID", HELP2);
15107
15301
  const numericId = idInput.startsWith("c") ? idInput.slice(1) : idInput;
15108
15302
  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
- }
15303
+ const view = await openOrFail(path);
15304
+ if (typeof view === "number")
15305
+ return view;
15123
15306
  const commentReference = findCommentByNumericId(view, numericId);
15124
15307
  if (!commentReference) {
15125
15308
  return fail("COMMENT_NOT_FOUND", `Comment not found: ${commentId}`);
@@ -15236,20 +15419,9 @@ async function run3(args) {
15236
15419
  const path = parsed.positionals[0];
15237
15420
  if (!path)
15238
15421
  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
- }
15422
+ const view = await openOrFail(path);
15423
+ if (typeof view === "number")
15424
+ return view;
15253
15425
  let comments = view.doc.comments;
15254
15426
  if (!parsed.values["include-resolved"]) {
15255
15427
  comments = comments.filter((comment) => !comment.resolved);
@@ -15293,7 +15465,6 @@ Examples:
15293
15465
  docx comments list doc.docx --include-resolved | jq '.[] | select(.author == "Jane")'
15294
15466
  `;
15295
15467
  var init_list = __esm(() => {
15296
- init_core();
15297
15468
  init_respond();
15298
15469
  });
15299
15470
 
@@ -15335,20 +15506,9 @@ async function run4(args) {
15335
15506
  if (!text)
15336
15507
  return fail("USAGE", "Missing --text TEXT", HELP4);
15337
15508
  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
- }
15509
+ const view = await openOrFail(path);
15510
+ if (typeof view === "number")
15511
+ return view;
15352
15512
  const parentReference = findCommentByNumericId(view, parentNumericId);
15353
15513
  if (!parentReference) {
15354
15514
  return fail("COMMENT_NOT_FOUND", `Parent comment not found: c${parentNumericId}`);
@@ -15459,20 +15619,9 @@ async function run5(args) {
15459
15619
  return fail("USAGE", "Missing --id COMMENT_ID", HELP5);
15460
15620
  const numericId = idInput.startsWith("c") ? idInput.slice(1) : idInput;
15461
15621
  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
- }
15622
+ const view = await openOrFail(path);
15623
+ if (typeof view === "number")
15624
+ return view;
15476
15625
  const commentReference = findCommentByNumericId(view, numericId);
15477
15626
  if (!commentReference) {
15478
15627
  return fail("COMMENT_NOT_FOUND", `Comment not found: c${numericId}`);
@@ -15584,34 +15733,19 @@ async function run6(args) {
15584
15733
  if (!entry) {
15585
15734
  return fail("COMMENT_NOT_FOUND", `No trashed entry for ${commentId} in ${path}`, "Trash lives at <dir>/.docx-cli/trash.json \u2014 make sure it's the same directory.");
15586
15735
  }
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;
15736
+ const view = await openOrFail(path);
15737
+ if (typeof view === "number")
15738
+ return view;
15739
+ const startBlock = view.blockReferences.get(entry.anchor.startBlockId);
15740
+ if (!startBlock) {
15741
+ return fail("BLOCK_NOT_FOUND", `Original anchor block ${entry.anchor.startBlockId} no longer exists`);
15603
15742
  }
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`);
15743
+ const endBlock = view.blockReferences.get(entry.anchor.endBlockId);
15744
+ if (!endBlock) {
15745
+ return fail("BLOCK_NOT_FOUND", `Original anchor block ${entry.anchor.endBlockId} no longer exists`);
15608
15746
  }
15609
- const span = {
15610
- start: entry.anchor.startOffset,
15611
- end: entry.anchor.endOffset
15612
- };
15613
15747
  try {
15614
- addCommentMarkersToParagraph(block.node, numericId, span);
15748
+ addCommentRangeMarkers(startBlock.node, entry.anchor.startOffset, endBlock.node, entry.anchor.endOffset, numericId);
15615
15749
  } catch (error) {
15616
15750
  if (error instanceof SpanOutOfRangeError) {
15617
15751
  return fail("INVALID_LOCATOR", `Saved span no longer fits the block: ${error.message}`);
@@ -15836,7 +15970,7 @@ async function run8(args) {
15836
15970
  if (await Bun.file(path).exists() && !parsed.values.force) {
15837
15971
  return fail("USAGE", `File already exists: ${path}`, "Pass --force to overwrite.");
15838
15972
  }
15839
- const author = parsed.values.author ?? process.env.DOCX_AUTHOR ?? "";
15973
+ const author = parsed.values.author ?? Bun.env.DOCX_AUTHOR ?? "";
15840
15974
  const title = parsed.values.title ?? "";
15841
15975
  const text = parsed.values.text;
15842
15976
  const now = new Date().toISOString();
@@ -15915,29 +16049,12 @@ async function run9(args) {
15915
16049
  const at = parsed.values.at;
15916
16050
  if (!at)
15917
16051
  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
- }
16052
+ const view = await openOrFail(path);
16053
+ if (typeof view === "number")
16054
+ return view;
16055
+ const blockRef = await resolveBlockOrFail(view, at);
16056
+ if (typeof blockRef === "number")
16057
+ return blockRef;
15941
16058
  const targetIndex = blockRef.parent.indexOf(blockRef.node);
15942
16059
  if (targetIndex === -1) {
15943
16060
  return fail("BLOCK_NOT_FOUND", "Block reference is stale (parent does not contain it)");
@@ -16152,29 +16269,12 @@ async function run10(args) {
16152
16269
  }
16153
16270
  paragraphOptions.alignment = alignmentValue;
16154
16271
  }
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
- }
16272
+ const view = await openOrFail(path);
16273
+ if (typeof view === "number")
16274
+ return view;
16275
+ const blockRef = await resolveBlockOrFail(view, at);
16276
+ if (typeof blockRef === "number")
16277
+ return blockRef;
16178
16278
  let paragraphNode;
16179
16279
  if (text !== undefined) {
16180
16280
  const color = parsed.values.color;
@@ -16255,12 +16355,86 @@ var init_edit = __esm(() => {
16255
16355
  init_jsx_dev_runtime();
16256
16356
  });
16257
16357
 
16258
- // src/cli/images/extract.ts
16259
- var exports_extract = {};
16260
- __export(exports_extract, {
16358
+ // src/core/find/index.ts
16359
+ function findTextSpans(doc, query, options = {}) {
16360
+ const matcher = options.regex ? regexMatcher(query, options.ignoreCase ?? false) : literalMatcher(query, options.ignoreCase ?? false);
16361
+ const out = [];
16362
+ collectMatches(doc.blocks, matcher, out);
16363
+ return out;
16364
+ }
16365
+ function literalMatcher(query, ignoreCase) {
16366
+ if (query.length === 0) {
16367
+ throw new Error("query cannot be empty");
16368
+ }
16369
+ const needle = ignoreCase ? query.toLowerCase() : query;
16370
+ return (paragraphText) => {
16371
+ const haystack = ignoreCase ? paragraphText.toLowerCase() : paragraphText;
16372
+ const matches = [];
16373
+ let cursor = haystack.indexOf(needle);
16374
+ while (cursor !== -1) {
16375
+ matches.push({
16376
+ start: cursor,
16377
+ end: cursor + needle.length,
16378
+ text: paragraphText.slice(cursor, cursor + needle.length)
16379
+ });
16380
+ cursor = haystack.indexOf(needle, cursor + needle.length);
16381
+ }
16382
+ return matches;
16383
+ };
16384
+ }
16385
+ function regexMatcher(pattern, ignoreCase) {
16386
+ const flags = `g${ignoreCase ? "i" : ""}`;
16387
+ const regex = new RegExp(pattern, flags);
16388
+ return (paragraphText) => {
16389
+ const matches = [];
16390
+ regex.lastIndex = 0;
16391
+ let result = regex.exec(paragraphText);
16392
+ while (result !== null) {
16393
+ const matched = result[0];
16394
+ if (matched.length === 0) {
16395
+ regex.lastIndex += 1;
16396
+ result = regex.exec(paragraphText);
16397
+ continue;
16398
+ }
16399
+ matches.push({
16400
+ start: result.index,
16401
+ end: result.index + matched.length,
16402
+ text: matched
16403
+ });
16404
+ result = regex.exec(paragraphText);
16405
+ }
16406
+ return matches;
16407
+ };
16408
+ }
16409
+ function collectMatches(blocks, matcher, out) {
16410
+ for (const block of blocks) {
16411
+ if (block.type === "paragraph") {
16412
+ const paragraphText = block.runs.map((run11) => run11.type === "text" ? run11.text : "").join("");
16413
+ for (const span of matcher(paragraphText)) {
16414
+ out.push({
16415
+ blockId: block.id,
16416
+ start: span.start,
16417
+ end: span.end,
16418
+ text: span.text
16419
+ });
16420
+ }
16421
+ continue;
16422
+ }
16423
+ if (block.type === "table") {
16424
+ for (const row of block.rows) {
16425
+ for (const cell of row.cells) {
16426
+ collectMatches(cell.blocks, matcher, out);
16427
+ }
16428
+ }
16429
+ }
16430
+ }
16431
+ }
16432
+
16433
+ // src/cli/find/index.ts
16434
+ var exports_find = {};
16435
+ __export(exports_find, {
16261
16436
  run: () => run11
16262
16437
  });
16263
- import { join as join2 } from "path";
16264
16438
  import { parseArgs as parseArgs10 } from "util";
16265
16439
  async function run11(args) {
16266
16440
  let parsed;
@@ -16269,8 +16443,10 @@ async function run11(args) {
16269
16443
  args,
16270
16444
  allowPositionals: true,
16271
16445
  options: {
16272
- to: { type: "string" },
16273
- id: { type: "string" },
16446
+ regex: { type: "boolean" },
16447
+ "ignore-case": { type: "boolean" },
16448
+ all: { type: "boolean" },
16449
+ nth: { type: "string" },
16274
16450
  help: { type: "boolean", short: "h" }
16275
16451
  }
16276
16452
  });
@@ -16283,43 +16459,143 @@ async function run11(args) {
16283
16459
  return EXIT.OK;
16284
16460
  }
16285
16461
  const path = parsed.positionals[0];
16462
+ const query = parsed.positionals[1];
16286
16463
  if (!path)
16287
16464
  return fail("USAGE", "Missing FILE argument", HELP11);
16288
- const outputDir = parsed.values.to;
16289
- if (!outputDir)
16290
- return fail("USAGE", "Missing --to DIR", HELP11);
16291
- const targetId = parsed.values.id;
16292
- let view;
16465
+ if (query == null)
16466
+ return fail("USAGE", "Missing QUERY argument", HELP11);
16467
+ const ignoreCase = Boolean(parsed.values["ignore-case"]);
16468
+ const useRegex = Boolean(parsed.values.regex);
16469
+ const wantAll = Boolean(parsed.values.all);
16470
+ const nthRaw = parsed.values.nth;
16471
+ const nth = nthRaw === undefined ? undefined : Number(nthRaw);
16472
+ if (nth !== undefined && (!Number.isInteger(nth) || nth < 0)) {
16473
+ return fail("USAGE", `--nth must be a non-negative integer, got "${nthRaw}"`);
16474
+ }
16475
+ const view = await openOrFail(path);
16476
+ if (typeof view === "number")
16477
+ return view;
16478
+ let allMatches;
16293
16479
  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
- }
16306
- await enrichImageHashes(view);
16307
- const allImages = [];
16308
- collectImages(view.doc.blocks, allImages);
16309
- const targets = targetId ? allImages.filter((image) => image.id === targetId) : allImages;
16310
- if (targetId && targets.length === 0) {
16311
- return fail("IMAGE_NOT_FOUND", `Image not found: ${targetId}`);
16480
+ allMatches = findTextSpans(view.doc, query, {
16481
+ regex: useRegex,
16482
+ ignoreCase
16483
+ });
16484
+ } catch (matcherError) {
16485
+ const message = matcherError instanceof Error ? matcherError.message : String(matcherError);
16486
+ return fail("USAGE", `Invalid query: ${message}`);
16487
+ }
16488
+ let selected;
16489
+ if (nth !== undefined) {
16490
+ const single = allMatches[nth];
16491
+ if (!single) {
16492
+ return fail("MATCH_NOT_FOUND", `Only ${allMatches.length} match(es); --nth ${nth} is out of range`);
16493
+ }
16494
+ selected = [single];
16495
+ } else if (wantAll) {
16496
+ selected = allMatches;
16497
+ } else {
16498
+ selected = allMatches.slice(0, 1);
16312
16499
  }
16313
- const manifest = [];
16314
- const seenHashes = new Set;
16315
- for (const image of targets) {
16316
- const reference = view.imageById.get(image.id);
16317
- if (!reference)
16318
- continue;
16319
- const extension = extensionFor(image.contentType, reference.partName);
16320
- const fileName = `${image.hash}.${extension}`;
16321
- const outputPath = join2(outputDir, fileName);
16322
- if (!seenHashes.has(image.hash)) {
16500
+ await respond({
16501
+ ok: true,
16502
+ operation: "find",
16503
+ path,
16504
+ query,
16505
+ regex: useRegex,
16506
+ ignoreCase,
16507
+ totalMatches: allMatches.length,
16508
+ matches: selected.map((match) => ({
16509
+ locator: `${match.blockId}:${match.start}-${match.end}`,
16510
+ ...match
16511
+ }))
16512
+ });
16513
+ return EXIT.OK;
16514
+ }
16515
+ var HELP11 = `docx find \u2014 locate text spans and return their locators
16516
+
16517
+ Usage:
16518
+ docx find FILE QUERY [options]
16519
+
16520
+ Required positional:
16521
+ QUERY literal substring (or regex if --regex)
16522
+
16523
+ Options:
16524
+ --regex treat QUERY as a JavaScript regular expression
16525
+ --ignore-case case-insensitive match
16526
+ --all return every match (default: just the first)
16527
+ --nth N return only the Nth match (0-indexed)
16528
+ -h, --help show this help
16529
+
16530
+ Within-paragraph matches only \u2014 cross-paragraph ranges aren't supported
16531
+ yet. Searches the concatenated text of each paragraph in document order,
16532
+ including paragraphs nested in table cells (locators look like
16533
+ tT:rRcC:pK:S-E for those).
16534
+
16535
+ Examples:
16536
+ docx find doc.docx "fox"
16537
+ docx find doc.docx "Action Item:" --all
16538
+ docx find doc.docx "TODO|FIXME" --regex --ignore-case
16539
+ docx comments add doc.docx --range "$(docx find doc.docx fox | jq -r .matches[0].locator)" --text "..."
16540
+ `;
16541
+ var init_find = __esm(() => {
16542
+ init_respond();
16543
+ });
16544
+
16545
+ // src/cli/images/extract.ts
16546
+ var exports_extract = {};
16547
+ __export(exports_extract, {
16548
+ run: () => run12
16549
+ });
16550
+ import { join as join2 } from "path";
16551
+ import { parseArgs as parseArgs11 } from "util";
16552
+ async function run12(args) {
16553
+ let parsed;
16554
+ try {
16555
+ parsed = parseArgs11({
16556
+ args,
16557
+ allowPositionals: true,
16558
+ options: {
16559
+ to: { type: "string" },
16560
+ id: { type: "string" },
16561
+ help: { type: "boolean", short: "h" }
16562
+ }
16563
+ });
16564
+ } catch (parseError) {
16565
+ const message = parseError instanceof Error ? parseError.message : String(parseError);
16566
+ return fail("USAGE", message, HELP12);
16567
+ }
16568
+ if (parsed.values.help) {
16569
+ await writeStdout(HELP12);
16570
+ return EXIT.OK;
16571
+ }
16572
+ const path = parsed.positionals[0];
16573
+ if (!path)
16574
+ return fail("USAGE", "Missing FILE argument", HELP12);
16575
+ const outputDir = parsed.values.to;
16576
+ if (!outputDir)
16577
+ return fail("USAGE", "Missing --to DIR", HELP12);
16578
+ const targetId = parsed.values.id;
16579
+ const view = await openOrFail(path);
16580
+ if (typeof view === "number")
16581
+ return view;
16582
+ await enrichImageHashes(view);
16583
+ const allImages = [];
16584
+ collectImages(view.doc.blocks, allImages);
16585
+ const targets = targetId ? allImages.filter((image) => image.id === targetId) : allImages;
16586
+ if (targetId && targets.length === 0) {
16587
+ return fail("IMAGE_NOT_FOUND", `Image not found: ${targetId}`);
16588
+ }
16589
+ const manifest = [];
16590
+ const seenHashes = new Set;
16591
+ for (const image of targets) {
16592
+ const reference = view.imageById.get(image.id);
16593
+ if (!reference)
16594
+ continue;
16595
+ const extension = extensionFor(image.contentType, reference.partName);
16596
+ const fileName = `${image.hash}.${extension}`;
16597
+ const outputPath = join2(outputDir, fileName);
16598
+ if (!seenHashes.has(image.hash)) {
16323
16599
  const bytes = await view.pkg.readBytes(reference.partName);
16324
16600
  await Bun.write(outputPath, bytes);
16325
16601
  seenHashes.add(image.hash);
@@ -16336,9 +16612,9 @@ async function run11(args) {
16336
16612
  function collectImages(blocks, out) {
16337
16613
  for (const block of blocks) {
16338
16614
  if (block.type === "paragraph") {
16339
- for (const run12 of block.runs) {
16340
- if (run12.type === "image")
16341
- out.push(run12);
16615
+ for (const run13 of block.runs) {
16616
+ if (run13.type === "image")
16617
+ out.push(run13);
16342
16618
  }
16343
16619
  continue;
16344
16620
  }
@@ -16358,7 +16634,7 @@ function extensionFor(contentType, partName) {
16358
16634
  const fromName = partName.split(".").pop()?.toLowerCase();
16359
16635
  return fromName ?? "bin";
16360
16636
  }
16361
- var HELP11 = `docx images extract \u2014 dump image bytes to a directory
16637
+ var HELP12 = `docx images extract \u2014 dump image bytes to a directory
16362
16638
 
16363
16639
  Usage:
16364
16640
  docx images extract FILE --to DIR [options]
@@ -16399,13 +16675,13 @@ var init_extract = __esm(() => {
16399
16675
  // src/cli/images/list.ts
16400
16676
  var exports_list2 = {};
16401
16677
  __export(exports_list2, {
16402
- run: () => run12
16678
+ run: () => run13
16403
16679
  });
16404
- import { parseArgs as parseArgs11 } from "util";
16405
- async function run12(args) {
16680
+ import { parseArgs as parseArgs12 } from "util";
16681
+ async function run13(args) {
16406
16682
  let parsed;
16407
16683
  try {
16408
- parsed = parseArgs11({
16684
+ parsed = parseArgs12({
16409
16685
  args,
16410
16686
  allowPositionals: true,
16411
16687
  options: {
@@ -16414,29 +16690,18 @@ async function run12(args) {
16414
16690
  });
16415
16691
  } catch (parseError) {
16416
16692
  const message = parseError instanceof Error ? parseError.message : String(parseError);
16417
- return fail("USAGE", message, HELP12);
16693
+ return fail("USAGE", message, HELP13);
16418
16694
  }
16419
16695
  if (parsed.values.help) {
16420
- await writeStdout(HELP12);
16696
+ await writeStdout(HELP13);
16421
16697
  return EXIT.OK;
16422
16698
  }
16423
16699
  const path = parsed.positionals[0];
16424
16700
  if (!path)
16425
- 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
- }
16701
+ return fail("USAGE", "Missing FILE argument", HELP13);
16702
+ const view = await openOrFail(path);
16703
+ if (typeof view === "number")
16704
+ return view;
16440
16705
  await enrichImageHashes(view);
16441
16706
  const images = [];
16442
16707
  collectImages2(view.doc.blocks, images);
@@ -16446,9 +16711,9 @@ async function run12(args) {
16446
16711
  function collectImages2(blocks, out) {
16447
16712
  for (const block of blocks) {
16448
16713
  if (block.type === "paragraph") {
16449
- for (const run13 of block.runs) {
16450
- if (run13.type === "image")
16451
- out.push(run13);
16714
+ for (const run14 of block.runs) {
16715
+ if (run14.type === "image")
16716
+ out.push(run14);
16452
16717
  }
16453
16718
  continue;
16454
16719
  }
@@ -16461,7 +16726,7 @@ function collectImages2(blocks, out) {
16461
16726
  }
16462
16727
  }
16463
16728
  }
16464
- var HELP12 = `docx images list \u2014 print image manifest as JSON
16729
+ var HELP13 = `docx images list \u2014 print image manifest as JSON
16465
16730
 
16466
16731
  Usage:
16467
16732
  docx images list FILE [options]
@@ -16480,13 +16745,13 @@ var init_list2 = __esm(() => {
16480
16745
  // src/cli/images/replace.ts
16481
16746
  var exports_replace = {};
16482
16747
  __export(exports_replace, {
16483
- run: () => run13
16748
+ run: () => run14
16484
16749
  });
16485
- import { parseArgs as parseArgs12 } from "util";
16486
- async function run13(args) {
16750
+ import { parseArgs as parseArgs13 } from "util";
16751
+ async function run14(args) {
16487
16752
  let parsed;
16488
16753
  try {
16489
- parsed = parseArgs12({
16754
+ parsed = parseArgs13({
16490
16755
  args,
16491
16756
  allowPositionals: true,
16492
16757
  options: {
@@ -16498,21 +16763,21 @@ async function run13(args) {
16498
16763
  });
16499
16764
  } catch (parseError) {
16500
16765
  const message = parseError instanceof Error ? parseError.message : String(parseError);
16501
- return fail("USAGE", message, HELP13);
16766
+ return fail("USAGE", message, HELP14);
16502
16767
  }
16503
16768
  if (parsed.values.help) {
16504
- await writeStdout(HELP13);
16769
+ await writeStdout(HELP14);
16505
16770
  return EXIT.OK;
16506
16771
  }
16507
16772
  const path = parsed.positionals[0];
16508
16773
  if (!path)
16509
- return fail("USAGE", "Missing FILE argument", HELP13);
16774
+ return fail("USAGE", "Missing FILE argument", HELP14);
16510
16775
  const targetId = parsed.values.at;
16511
16776
  if (!targetId)
16512
- return fail("USAGE", "Missing --at IMG_ID", HELP13);
16777
+ return fail("USAGE", "Missing --at IMG_ID", HELP14);
16513
16778
  const sourcePath = parsed.values.with;
16514
16779
  if (!sourcePath)
16515
- return fail("USAGE", "Missing --with PATH", HELP13);
16780
+ return fail("USAGE", "Missing --with PATH", HELP14);
16516
16781
  const sourceFile = Bun.file(sourcePath);
16517
16782
  if (!await sourceFile.exists()) {
16518
16783
  return fail("FILE_NOT_FOUND", `Replacement file not found: ${sourcePath}`);
@@ -16522,20 +16787,9 @@ async function run13(args) {
16522
16787
  if (!newExtension) {
16523
16788
  return fail("USAGE", `Unsupported replacement image type: ${newMimeType}`, "Supported: png, jpeg, gif, webp, bmp, tiff, svg, emf, wmf, ico.");
16524
16789
  }
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
- }
16790
+ const view = await openOrFail(path);
16791
+ if (typeof view === "number")
16792
+ return view;
16539
16793
  const reference = view.imageById.get(targetId);
16540
16794
  if (!reference) {
16541
16795
  return fail("IMAGE_NOT_FOUND", `Image not found: ${targetId}`);
@@ -16613,7 +16867,7 @@ function ensureContentTypeDefault(contentTypesTree, extension, mimeType) {
16613
16867
  }
16614
16868
  types.children.push(new XmlNode2("Default", { Extension: extension, ContentType: mimeType }));
16615
16869
  }
16616
- var HELP13 = `docx images replace \u2014 swap an image's bytes
16870
+ var HELP14 = `docx images replace \u2014 swap an image's bytes
16617
16871
 
16618
16872
  Usage:
16619
16873
  docx images replace FILE --at IMG_ID --with PATH [options]
@@ -16655,12 +16909,12 @@ var init_replace = __esm(() => {
16655
16909
  // src/cli/images/index.ts
16656
16910
  var exports_images = {};
16657
16911
  __export(exports_images, {
16658
- run: () => run14
16912
+ run: () => run15
16659
16913
  });
16660
- async function run14(args) {
16914
+ async function run15(args) {
16661
16915
  const verb = args[0];
16662
16916
  if (!verb || verb === "--help" || verb === "-h" || verb === "help") {
16663
- await writeStdout(HELP14);
16917
+ await writeStdout(HELP15);
16664
16918
  return verb ? 0 : 2;
16665
16919
  }
16666
16920
  const loader = SUBCOMMANDS2[verb];
@@ -16670,7 +16924,7 @@ async function run14(args) {
16670
16924
  const module_ = await loader();
16671
16925
  return module_.run(args.slice(1));
16672
16926
  }
16673
- var SUBCOMMANDS2, HELP14 = `docx images \u2014 manage embedded images
16927
+ var SUBCOMMANDS2, HELP15 = `docx images \u2014 manage embedded images
16674
16928
 
16675
16929
  Usage:
16676
16930
  docx images <verb> FILE [options]
@@ -16691,58 +16945,536 @@ var init_images = __esm(() => {
16691
16945
  };
16692
16946
  });
16693
16947
 
16694
- // src/cli/insert/index.tsx
16695
- var exports_insert = {};
16696
- __export(exports_insert, {
16697
- run: () => run15
16948
+ // src/core/ast/types.ts
16949
+ var types_default = `export type Doc = {
16950
+ schemaVersion: 1;
16951
+ path: string;
16952
+ properties: DocProperties;
16953
+ blocks: Block[];
16954
+ comments: Comment[];
16955
+ };
16956
+
16957
+ export type DocProperties = {
16958
+ title?: string;
16959
+ author?: string;
16960
+ created?: string;
16961
+ modified?: string;
16962
+ };
16963
+
16964
+ export type Block = Paragraph | Table | SectionBreak;
16965
+
16966
+ export type Paragraph = {
16967
+ id: string;
16968
+ type: "paragraph";
16969
+ style?: string;
16970
+ alignment?: "left" | "center" | "right" | "justify";
16971
+ list?: { level: number; numId: string };
16972
+ runs: Run[];
16973
+ };
16974
+
16975
+ export type Table = {
16976
+ id: string;
16977
+ type: "table";
16978
+ rows: TableRow[];
16979
+ };
16980
+
16981
+ export type TableRow = {
16982
+ cells: TableCell[];
16983
+ };
16984
+
16985
+ export type TableCell = {
16986
+ blocks: Block[];
16987
+ };
16988
+
16989
+ export type SectionBreak = {
16990
+ id: string;
16991
+ type: "sectionBreak";
16992
+ };
16993
+
16994
+ export type Run = TextRun | ImageRun | BreakRun | TabRun;
16995
+
16996
+ export type TextRun = {
16997
+ type: "text";
16998
+ text: string;
16999
+ color?: string;
17000
+ highlight?: string;
17001
+ bold?: boolean;
17002
+ italic?: boolean;
17003
+ underline?: string;
17004
+ strike?: boolean;
17005
+ font?: string;
17006
+ sizeHalfPoints?: number;
17007
+ comments?: string[];
17008
+ trackedChange?: TrackedChange;
17009
+ };
17010
+
17011
+ export type ImageRun = {
17012
+ type: "image";
17013
+ id: string;
17014
+ contentType: string;
17015
+ hash: string;
17016
+ widthEmu?: number;
17017
+ heightEmu?: number;
17018
+ alt?: string;
17019
+ };
17020
+
17021
+ export type BreakRun = {
17022
+ type: "break";
17023
+ kind: "page" | "line" | "column";
17024
+ };
17025
+
17026
+ export type TabRun = {
17027
+ type: "tab";
17028
+ };
17029
+
17030
+ export type TrackedChange = {
17031
+ kind: "ins" | "del";
17032
+ author: string;
17033
+ date: string;
17034
+ revisionId: string;
17035
+ };
17036
+
17037
+ export type Comment = {
17038
+ id: string;
17039
+ author: string;
17040
+ initials?: string;
17041
+ date: string;
17042
+ text: string;
17043
+ parentId?: string;
17044
+ resolved?: boolean;
17045
+ anchor: CommentAnchor;
17046
+ };
17047
+
17048
+ export type CommentAnchor = {
17049
+ startBlockId: string;
17050
+ startOffset: number;
17051
+ endBlockId: string;
17052
+ endOffset: number;
17053
+ };
17054
+ `;
17055
+ var init_types = () => {};
17056
+
17057
+ // src/cli/info/schema.ts
17058
+ var exports_schema = {};
17059
+ __export(exports_schema, {
17060
+ run: () => run16
16698
17061
  });
16699
- import { parseArgs as parseArgs13 } from "util";
16700
- async function run15(args) {
17062
+ import { parseArgs as parseArgs14 } from "util";
17063
+ async function run16(args) {
16701
17064
  let parsed;
16702
17065
  try {
16703
- parsed = parseArgs13({
17066
+ parsed = parseArgs14({
16704
17067
  args,
16705
17068
  allowPositionals: true,
16706
17069
  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" },
17070
+ json: { type: "boolean" },
17071
+ ts: { type: "boolean" },
16717
17072
  help: { type: "boolean", short: "h" }
16718
17073
  }
16719
17074
  });
16720
17075
  } catch (parseError) {
16721
17076
  const message = parseError instanceof Error ? parseError.message : String(parseError);
16722
- return fail("USAGE", message, HELP15);
17077
+ return fail("USAGE", message, HELP16);
16723
17078
  }
16724
17079
  if (parsed.values.help) {
16725
- await writeStdout(HELP15);
17080
+ await writeStdout(HELP16);
16726
17081
  return EXIT.OK;
16727
17082
  }
16728
- const path = parsed.positionals[0];
16729
- if (!path)
16730
- return fail("USAGE", "Missing FILE argument", HELP15);
16731
- const after = parsed.values.after;
17083
+ if (parsed.values.ts) {
17084
+ await writeStdout(types_default);
17085
+ return EXIT.OK;
17086
+ }
17087
+ await respond(JSON_SCHEMA);
17088
+ return EXIT.OK;
17089
+ }
17090
+ var HELP16 = `docx schema \u2014 print the AST type definitions
17091
+
17092
+ Usage:
17093
+ docx schema [options]
17094
+
17095
+ Options:
17096
+ --json Print as a JSON Schema document (default)
17097
+ --ts Print TypeScript type source (read live from src/core/ast/types.ts)
17098
+ -h, --help Show this help
17099
+
17100
+ Examples:
17101
+ docx schema | jq '.definitions.Run'
17102
+ docx schema --ts > ast.d.ts
17103
+ `, JSON_SCHEMA;
17104
+ var init_schema = __esm(() => {
17105
+ init_types();
17106
+ init_respond();
17107
+ JSON_SCHEMA = {
17108
+ $schema: "https://json-schema.org/draft/2020-12/schema",
17109
+ $id: "https://github.com/kklimuk/docx-cli/schema",
17110
+ title: "docx-cli AST",
17111
+ type: "object",
17112
+ required: ["schemaVersion", "path", "properties", "blocks", "comments"],
17113
+ properties: {
17114
+ schemaVersion: { const: 1 },
17115
+ path: { type: "string" },
17116
+ properties: {
17117
+ type: "object",
17118
+ properties: {
17119
+ title: { type: "string" },
17120
+ author: { type: "string" },
17121
+ created: { type: "string" },
17122
+ modified: { type: "string" }
17123
+ }
17124
+ },
17125
+ blocks: { type: "array", items: { $ref: "#/$defs/Block" } },
17126
+ comments: { type: "array", items: { $ref: "#/$defs/Comment" } }
17127
+ },
17128
+ $defs: {
17129
+ Block: {
17130
+ oneOf: [
17131
+ { $ref: "#/$defs/Paragraph" },
17132
+ { $ref: "#/$defs/Table" },
17133
+ { $ref: "#/$defs/SectionBreak" }
17134
+ ]
17135
+ },
17136
+ Paragraph: {
17137
+ type: "object",
17138
+ required: ["id", "type", "runs"],
17139
+ properties: {
17140
+ id: { type: "string" },
17141
+ type: { const: "paragraph" },
17142
+ style: { type: "string" },
17143
+ alignment: { enum: ["left", "center", "right", "justify"] },
17144
+ list: {
17145
+ type: "object",
17146
+ required: ["level", "numId"],
17147
+ properties: {
17148
+ level: { type: "number" },
17149
+ numId: { type: "string" }
17150
+ }
17151
+ },
17152
+ runs: { type: "array", items: { $ref: "#/$defs/Run" } }
17153
+ }
17154
+ },
17155
+ Run: {
17156
+ oneOf: [
17157
+ { $ref: "#/$defs/TextRun" },
17158
+ { $ref: "#/$defs/ImageRun" },
17159
+ { $ref: "#/$defs/BreakRun" },
17160
+ { $ref: "#/$defs/TabRun" }
17161
+ ]
17162
+ },
17163
+ TextRun: {
17164
+ type: "object",
17165
+ required: ["type", "text"],
17166
+ properties: {
17167
+ type: { const: "text" },
17168
+ text: { type: "string" },
17169
+ color: { type: "string" },
17170
+ highlight: { type: "string" },
17171
+ bold: { type: "boolean" },
17172
+ italic: { type: "boolean" },
17173
+ underline: { type: "string" },
17174
+ strike: { type: "boolean" },
17175
+ font: { type: "string" },
17176
+ sizeHalfPoints: { type: "number" },
17177
+ comments: { type: "array", items: { type: "string" } },
17178
+ trackedChange: {
17179
+ type: "object",
17180
+ required: ["kind", "author", "date", "revisionId"],
17181
+ properties: {
17182
+ kind: { enum: ["ins", "del"] },
17183
+ author: { type: "string" },
17184
+ date: { type: "string" },
17185
+ revisionId: { type: "string" }
17186
+ }
17187
+ }
17188
+ }
17189
+ },
17190
+ ImageRun: {
17191
+ type: "object",
17192
+ required: ["type", "id", "contentType", "hash"],
17193
+ properties: {
17194
+ type: { const: "image" },
17195
+ id: { type: "string" },
17196
+ contentType: { type: "string" },
17197
+ hash: { type: "string" },
17198
+ widthEmu: { type: "number" },
17199
+ heightEmu: { type: "number" },
17200
+ alt: { type: "string" }
17201
+ }
17202
+ },
17203
+ BreakRun: {
17204
+ type: "object",
17205
+ required: ["type", "kind"],
17206
+ properties: {
17207
+ type: { const: "break" },
17208
+ kind: { enum: ["page", "line", "column"] }
17209
+ }
17210
+ },
17211
+ TabRun: {
17212
+ type: "object",
17213
+ required: ["type"],
17214
+ properties: { type: { const: "tab" } }
17215
+ },
17216
+ Table: {
17217
+ type: "object",
17218
+ required: ["id", "type", "rows"],
17219
+ properties: {
17220
+ id: { type: "string" },
17221
+ type: { const: "table" },
17222
+ rows: {
17223
+ type: "array",
17224
+ items: {
17225
+ type: "object",
17226
+ properties: {
17227
+ cells: {
17228
+ type: "array",
17229
+ items: {
17230
+ type: "object",
17231
+ properties: {
17232
+ blocks: {
17233
+ type: "array",
17234
+ items: { $ref: "#/$defs/Block" }
17235
+ }
17236
+ }
17237
+ }
17238
+ }
17239
+ }
17240
+ }
17241
+ }
17242
+ }
17243
+ },
17244
+ SectionBreak: {
17245
+ type: "object",
17246
+ required: ["id", "type"],
17247
+ properties: {
17248
+ id: { type: "string" },
17249
+ type: { const: "sectionBreak" }
17250
+ }
17251
+ },
17252
+ Comment: {
17253
+ type: "object",
17254
+ required: ["id", "author", "date", "text", "anchor"],
17255
+ properties: {
17256
+ id: { type: "string" },
17257
+ author: { type: "string" },
17258
+ initials: { type: "string" },
17259
+ date: { type: "string" },
17260
+ text: { type: "string" },
17261
+ parentId: { type: "string" },
17262
+ resolved: { type: "boolean" },
17263
+ anchor: {
17264
+ type: "object",
17265
+ required: ["startBlockId", "startOffset", "endBlockId", "endOffset"],
17266
+ properties: {
17267
+ startBlockId: { type: "string" },
17268
+ startOffset: { type: "number" },
17269
+ endBlockId: { type: "string" },
17270
+ endOffset: { type: "number" }
17271
+ }
17272
+ }
17273
+ }
17274
+ }
17275
+ }
17276
+ };
17277
+ });
17278
+
17279
+ // src/cli/info/locators.ts
17280
+ var exports_locators = {};
17281
+ __export(exports_locators, {
17282
+ run: () => run17
17283
+ });
17284
+ import { parseArgs as parseArgs15 } from "util";
17285
+ async function run17(args) {
17286
+ let parsed;
17287
+ try {
17288
+ parsed = parseArgs15({
17289
+ args,
17290
+ allowPositionals: true,
17291
+ options: {
17292
+ json: { type: "boolean" },
17293
+ help: { type: "boolean", short: "h" }
17294
+ }
17295
+ });
17296
+ } catch (parseError) {
17297
+ const message = parseError instanceof Error ? parseError.message : String(parseError);
17298
+ return fail("USAGE", message, HELP17);
17299
+ }
17300
+ if (parsed.values.help) {
17301
+ await writeStdout(HELP17);
17302
+ return EXIT.OK;
17303
+ }
17304
+ if (parsed.values.json) {
17305
+ await writeStdout(`${JSON.stringify(JSON_REFERENCE, null, 2)}
17306
+ `);
17307
+ return EXIT.OK;
17308
+ }
17309
+ await writeStdout(REFERENCE);
17310
+ return EXIT.OK;
17311
+ }
17312
+ var HELP17 = `docx locators \u2014 print the locator grammar reference
17313
+
17314
+ Usage:
17315
+ docx locators [options]
17316
+
17317
+ Options:
17318
+ --json Print as JSON
17319
+ -h, --help Show this help
17320
+ `, REFERENCE = `LOCATOR GRAMMAR
17321
+
17322
+ Block locators:
17323
+ pN Paragraph N (e.g., p3)
17324
+ tN Table N
17325
+ sN Section break N
17326
+ tT:rRcC:pK Paragraph K of cell (R,C) in table T
17327
+
17328
+ Span locators (within a single paragraph):
17329
+ pN:S-E Characters S..E of paragraph N (inclusive start, exclusive end)
17330
+ tT:rRcC:pK:S-E Characters S..E of a cell paragraph
17331
+
17332
+ Range locators (across blocks):
17333
+ pN:S-pM:E From char S of paragraph N to char E of paragraph M
17334
+
17335
+ Entity locators:
17336
+ cN Comment id (e.g., c0)
17337
+ imgN Image id (e.g., img2)
17338
+ tN:rRcC Cell at row R, column C of table tN
17339
+
17340
+ Examples:
17341
+ p3 -> the entire paragraph p3
17342
+ p3:5-20 -> characters 5..20 of p3
17343
+ p3:5-p5:10 -> from char 5 of p3 to char 10 of p5
17344
+ c1 -> comment c1
17345
+ img0 -> image img0
17346
+ t0:r1c2 -> cell at row 1, col 2 of table t0
17347
+ t0:r1c2:p0 -> first paragraph of that cell
17348
+ t0:r1c2:p0:5-10 -> chars 5..10 of that paragraph
17349
+
17350
+ Notes:
17351
+ Block ids are positional and shift after structural edits \u2014 re-read
17352
+ between non-trivial mutations. Mutating commands print before/after
17353
+ locator info in their JSON ack so agents can chain operations.
17354
+ `, JSON_REFERENCE;
17355
+ var init_locators2 = __esm(() => {
17356
+ init_respond();
17357
+ JSON_REFERENCE = {
17358
+ blockLocators: {
17359
+ paragraph: { syntax: "pN", example: "p3" },
17360
+ table: { syntax: "tN", example: "t0" },
17361
+ sectionBreak: { syntax: "sN", example: "s0" },
17362
+ cellParagraph: { syntax: "tT:rRcC:pK", example: "t0:r1c2:p0" }
17363
+ },
17364
+ spanLocator: {
17365
+ syntax: "pN:S-E",
17366
+ example: "p3:5-20",
17367
+ semantics: "Characters S..E within paragraph N (start inclusive, end exclusive)",
17368
+ cellSyntax: "tT:rRcC:pK:S-E"
17369
+ },
17370
+ rangeLocator: {
17371
+ syntax: "pN:S-pM:E",
17372
+ example: "p3:5-p5:10",
17373
+ semantics: "From char S of paragraph N to char E of paragraph M"
17374
+ },
17375
+ entityLocators: {
17376
+ comment: { syntax: "cN", example: "c1" },
17377
+ image: { syntax: "imgN", example: "img0" },
17378
+ cell: { syntax: "tN:rRcC", example: "t0:r1c2" },
17379
+ nestedCell: { syntax: "tN:rRcC:pK", example: "t0:r1c2:p0" }
17380
+ },
17381
+ notes: [
17382
+ "Block ids are positional and shift after structural edits.",
17383
+ "Re-read between non-trivial mutations.",
17384
+ "Mutating commands print before/after locator info in their JSON ack."
17385
+ ]
17386
+ };
17387
+ });
17388
+
17389
+ // src/cli/info/index.ts
17390
+ var exports_info = {};
17391
+ __export(exports_info, {
17392
+ run: () => run18
17393
+ });
17394
+ async function run18(args) {
17395
+ const topic = args[0];
17396
+ if (!topic || topic === "--help" || topic === "-h" || topic === "help") {
17397
+ await writeStdout(HELP18);
17398
+ return topic ? 0 : 2;
17399
+ }
17400
+ const loader = SUBCOMMANDS3[topic];
17401
+ if (!loader) {
17402
+ return fail("USAGE", `Unknown info topic: ${topic}`, 'Run "docx info --help".');
17403
+ }
17404
+ const module_ = await loader();
17405
+ return module_.run(args.slice(1));
17406
+ }
17407
+ var SUBCOMMANDS3, HELP18 = `docx info \u2014 print reference material about the CLI
17408
+
17409
+ Usage:
17410
+ docx info <topic> [options]
17411
+
17412
+ Topics:
17413
+ schema Dump AST JSON Schema (or TS source via --ts)
17414
+ locators Dump locator grammar reference
17415
+
17416
+ Run "docx info <topic> --help" for topic-specific help.
17417
+ `;
17418
+ var init_info = __esm(() => {
17419
+ init_respond();
17420
+ SUBCOMMANDS3 = {
17421
+ schema: () => Promise.resolve().then(() => (init_schema(), exports_schema)),
17422
+ locators: () => Promise.resolve().then(() => (init_locators2(), exports_locators))
17423
+ };
17424
+ });
17425
+
17426
+ // src/cli/insert/index.tsx
17427
+ var exports_insert = {};
17428
+ __export(exports_insert, {
17429
+ run: () => run19
17430
+ });
17431
+ import { parseArgs as parseArgs16 } from "util";
17432
+ async function run19(args) {
17433
+ let parsed;
17434
+ try {
17435
+ parsed = parseArgs16({
17436
+ args,
17437
+ allowPositionals: true,
17438
+ options: {
17439
+ after: { type: "string" },
17440
+ before: { type: "string" },
17441
+ text: { type: "string" },
17442
+ runs: { type: "string" },
17443
+ style: { type: "string" },
17444
+ alignment: { type: "string" },
17445
+ color: { type: "string" },
17446
+ bold: { type: "boolean" },
17447
+ italic: { type: "boolean" },
17448
+ "dry-run": { type: "boolean" },
17449
+ help: { type: "boolean", short: "h" }
17450
+ }
17451
+ });
17452
+ } catch (parseError) {
17453
+ const message = parseError instanceof Error ? parseError.message : String(parseError);
17454
+ return fail("USAGE", message, HELP19);
17455
+ }
17456
+ if (parsed.values.help) {
17457
+ await writeStdout(HELP19);
17458
+ return EXIT.OK;
17459
+ }
17460
+ const path = parsed.positionals[0];
17461
+ if (!path)
17462
+ return fail("USAGE", "Missing FILE argument", HELP19);
17463
+ const after = parsed.values.after;
16732
17464
  const before = parsed.values.before;
16733
17465
  if (!after && !before) {
16734
- return fail("USAGE", "Missing locator: pass --after or --before", HELP15);
17466
+ return fail("USAGE", "Missing locator: pass --after or --before", HELP19);
16735
17467
  }
16736
17468
  if (after && before) {
16737
- return fail("USAGE", "Pass either --after or --before, not both", HELP15);
17469
+ return fail("USAGE", "Pass either --after or --before, not both", HELP19);
16738
17470
  }
16739
17471
  const text = parsed.values.text;
16740
17472
  const runsJson = parsed.values.runs;
16741
17473
  if (!text && !runsJson) {
16742
- return fail("USAGE", "Missing content: pass --text or --runs", HELP15);
17474
+ return fail("USAGE", "Missing content: pass --text or --runs", HELP19);
16743
17475
  }
16744
17476
  if (text && runsJson) {
16745
- return fail("USAGE", "Pass either --text or --runs, not both", HELP15);
17477
+ return fail("USAGE", "Pass either --text or --runs, not both", HELP19);
16746
17478
  }
16747
17479
  const paragraphOptions = {};
16748
17480
  const styleValue = parsed.values.style;
@@ -16755,30 +17487,13 @@ async function run15(args) {
16755
17487
  }
16756
17488
  paragraphOptions.alignment = alignmentValue;
16757
17489
  }
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
- }
17490
+ const view = await openOrFail(path);
17491
+ if (typeof view === "number")
17492
+ return view;
16772
17493
  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
- }
17494
+ const blockRef = await resolveBlockOrFail(view, targetLocator);
17495
+ if (typeof blockRef === "number")
17496
+ return blockRef;
16782
17497
  let paragraphNode;
16783
17498
  if (text !== undefined) {
16784
17499
  const color = parsed.values.color;
@@ -16832,7 +17547,7 @@ async function run15(args) {
16832
17547
  });
16833
17548
  return EXIT.OK;
16834
17549
  }
16835
- var HELP15 = `docx insert \u2014 insert a paragraph at a locator
17550
+ var HELP19 = `docx insert \u2014 insert a paragraph at a locator
16836
17551
 
16837
17552
  Usage:
16838
17553
  docx insert FILE [options]
@@ -16855,137 +17570,30 @@ Run options (only with --text):
16855
17570
  --italic Italic
16856
17571
 
16857
17572
  --dry-run Print what would be inserted; do not write the file
16858
- -h, --help Show this help
16859
-
16860
- 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();
16867
- init_respond();
16868
- init_emit();
16869
- init_jsx_dev_runtime();
16870
- });
16871
-
16872
- // src/cli/locators-cmd/index.ts
16873
- var exports_locators_cmd = {};
16874
- __export(exports_locators_cmd, {
16875
- run: () => run16
16876
- });
16877
- import { parseArgs as parseArgs14 } from "util";
16878
- async function run16(args) {
16879
- let parsed;
16880
- try {
16881
- parsed = parseArgs14({
16882
- args,
16883
- allowPositionals: true,
16884
- options: {
16885
- json: { type: "boolean" },
16886
- help: { type: "boolean", short: "h" }
16887
- }
16888
- });
16889
- } catch (parseError) {
16890
- const message = parseError instanceof Error ? parseError.message : String(parseError);
16891
- return fail("USAGE", message, HELP16);
16892
- }
16893
- if (parsed.values.help) {
16894
- await writeStdout(HELP16);
16895
- return EXIT.OK;
16896
- }
16897
- if (parsed.values.json) {
16898
- await Bun.write(Bun.stdout, `${JSON.stringify(JSON_REFERENCE, null, 2)}
16899
- `);
16900
- return EXIT.OK;
16901
- }
16902
- await Bun.write(Bun.stdout, REFERENCE);
16903
- return EXIT.OK;
16904
- }
16905
- var HELP16 = `docx locators \u2014 print the locator grammar reference
16906
-
16907
- Usage:
16908
- docx locators [options]
16909
-
16910
- Options:
16911
- --json Print as JSON
16912
- -h, --help Show this help
16913
- `, REFERENCE = `LOCATOR GRAMMAR
16914
-
16915
- 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
16920
-
16921
- Span locators (within a single paragraph):
16922
- pN:S-E Characters S..E of paragraph N (inclusive start, exclusive end)
16923
-
16924
- Range locators (across blocks):
16925
- pN:S-pM:E From char S of paragraph N to char E of paragraph M
16926
-
16927
- 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
17573
+ -h, --help Show this help
16931
17574
 
16932
17575
  Examples:
16933
- p3 -> the entire paragraph p3
16934
- p3:5-20 -> characters 5..20 of p3
16935
- p3:5-p5:10 -> from char 5 of p3 to char 10 of p5
16936
- c1 -> comment c1
16937
- img0 -> image img0
16938
- t0:r1c2 -> cell at row 1, col 2 of table t0
16939
- t0:r1c2:p0 -> first paragraph of that cell
16940
-
16941
- Notes:
16942
- Block ids are positional and shift after structural edits \u2014 re-read
16943
- between non-trivial mutations. Mutating commands print before/after
16944
- locator info in their JSON ack so agents can chain operations.
16945
- `, JSON_REFERENCE;
16946
- var init_locators_cmd = __esm(() => {
17576
+ docx insert doc.docx --after p3 --text "Section header" --style Heading2
17577
+ docx insert doc.docx --before p0 --text "ALERT" --color CC0000 --bold
17578
+ docx insert doc.docx --after p2 --runs '[{"type":"text","text":"X","bold":true}]'
17579
+ `;
17580
+ var init_insert = __esm(() => {
17581
+ init_core();
16947
17582
  init_respond();
16948
- JSON_REFERENCE = {
16949
- blockLocators: {
16950
- paragraph: { syntax: "pN", example: "p3" },
16951
- table: { syntax: "tN", example: "t0" },
16952
- sectionBreak: { syntax: "sN", example: "s0" },
16953
- cellParagraph: { syntax: "cellpN", example: "cellp0" }
16954
- },
16955
- spanLocator: {
16956
- syntax: "pN:S-E",
16957
- example: "p3:5-20",
16958
- semantics: "Characters S..E within paragraph N (start inclusive, end exclusive)"
16959
- },
16960
- rangeLocator: {
16961
- syntax: "pN:S-pM:E",
16962
- example: "p3:5-p5:10",
16963
- semantics: "From char S of paragraph N to char E of paragraph M"
16964
- },
16965
- entityLocators: {
16966
- comment: { syntax: "cN", example: "c1" },
16967
- image: { syntax: "imgN", example: "img0" },
16968
- cell: { syntax: "tN:rRcC", example: "t0:r1c2" },
16969
- nestedCell: { syntax: "tN:rRcC:pK", example: "t0:r1c2:p0" }
16970
- },
16971
- notes: [
16972
- "Block ids are positional and shift after structural edits.",
16973
- "Re-read between non-trivial mutations.",
16974
- "Mutating commands print before/after locator info in their JSON ack."
16975
- ]
16976
- };
17583
+ init_emit();
17584
+ init_jsx_dev_runtime();
16977
17585
  });
16978
17586
 
16979
17587
  // src/cli/read/index.ts
16980
17588
  var exports_read = {};
16981
17589
  __export(exports_read, {
16982
- run: () => run17
17590
+ run: () => run20
16983
17591
  });
16984
- import { parseArgs as parseArgs15 } from "util";
16985
- async function run17(args) {
17592
+ import { parseArgs as parseArgs17 } from "util";
17593
+ async function run20(args) {
16986
17594
  let parsed;
16987
17595
  try {
16988
- parsed = parseArgs15({
17596
+ parsed = parseArgs17({
16989
17597
  args,
16990
17598
  allowPositionals: true,
16991
17599
  options: {
@@ -16994,32 +17602,23 @@ async function run17(args) {
16994
17602
  }
16995
17603
  });
16996
17604
  } catch (e) {
16997
- return fail("USAGE", e instanceof Error ? e.message : String(e), HELP17);
17605
+ return fail("USAGE", e instanceof Error ? e.message : String(e), HELP20);
16998
17606
  }
16999
17607
  if (parsed.values.help) {
17000
- await writeStdout(HELP17);
17608
+ await writeStdout(HELP20);
17001
17609
  return EXIT.OK;
17002
17610
  }
17003
17611
  const path = parsed.positionals[0];
17004
17612
  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
- }
17613
+ return fail("USAGE", "Missing FILE argument", HELP20);
17614
+ const view = await openOrFail(path);
17615
+ if (typeof view === "number")
17616
+ return view;
17018
17617
  await enrichImageHashes(view);
17019
17618
  await respond(view.doc);
17020
17619
  return EXIT.OK;
17021
17620
  }
17022
- var HELP17 = `docx read \u2014 print AST as JSON
17621
+ var HELP20 = `docx read \u2014 print AST as JSON
17023
17622
 
17024
17623
  Usage:
17025
17624
  docx read FILE [options]
@@ -17037,348 +17636,336 @@ var init_read2 = __esm(() => {
17037
17636
  init_respond();
17038
17637
  });
17039
17638
 
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 = () => {};
17639
+ // src/cli/replace/replace-span.tsx
17640
+ function replaceSpanInParagraph(paragraph, span, replacement) {
17641
+ if (span.start > span.end) {
17642
+ throw new Error(`replaceSpanInParagraph: invalid span ${span.start}-${span.end}`);
17643
+ }
17644
+ const slots = collectRunSlots(paragraph);
17645
+ const overlapping = slots.filter((slot) => slot.offsetBefore + slot.length > span.start && slot.offsetBefore < span.end);
17646
+ const firstSlot = overlapping[0];
17647
+ if (!firstSlot) {
17648
+ paragraph.children.push(replacementRun(null, replacement));
17649
+ return;
17650
+ }
17651
+ const firstParent = firstSlot.parent;
17652
+ if (overlapping.some((slot) => slot.parent !== firstParent)) {
17653
+ throw new TrackedChangeBoundaryError("Span crosses a tracked-change (<w:ins>/<w:del>) boundary; accept or reject the tracked change first.");
17654
+ }
17655
+ const firstRunProperties = firstSlot.run.findChild("w:rPr");
17656
+ const inheritedProperties = firstRunProperties ? deepCloneNode(firstRunProperties) : null;
17657
+ const containerStart = firstParent === paragraph ? 0 : firstSlot.offsetBefore;
17658
+ rebuildContainer(firstParent, containerStart, span, replacement, inheritedProperties, firstParent === paragraph);
17659
+ }
17660
+ function collectRunSlots(paragraph) {
17661
+ const slots = [];
17662
+ let offset = 0;
17663
+ for (const child of paragraph.children) {
17664
+ if (child.tag === "w:r") {
17665
+ const length = runTextLength(child);
17666
+ slots.push({
17667
+ parent: paragraph,
17668
+ run: child,
17669
+ offsetBefore: offset,
17670
+ length
17671
+ });
17672
+ offset += length;
17673
+ continue;
17674
+ }
17675
+ if (child.tag === "w:ins" || child.tag === "w:del") {
17676
+ for (const inner of child.children) {
17677
+ if (inner.tag !== "w:r")
17678
+ continue;
17679
+ const length = runTextLength(inner);
17680
+ slots.push({
17681
+ parent: child,
17682
+ run: inner,
17683
+ offsetBefore: offset,
17684
+ length
17685
+ });
17686
+ offset += length;
17687
+ }
17688
+ }
17689
+ }
17690
+ return slots;
17691
+ }
17692
+ function rebuildContainer(container, baseOffset, span, replacement, runProperties, isParagraph) {
17693
+ const newChildren = [];
17694
+ let offset = baseOffset;
17695
+ let placedReplacement = false;
17696
+ for (const child of container.children) {
17697
+ if (child.tag === "w:r") {
17698
+ const length = runTextLength(child);
17699
+ const runStart = offset;
17700
+ const runEnd = offset + length;
17701
+ offset = runEnd;
17702
+ if (runEnd <= span.start) {
17703
+ newChildren.push(child);
17704
+ continue;
17705
+ }
17706
+ if (runStart >= span.end) {
17707
+ if (!placedReplacement) {
17708
+ newChildren.push(replacementRun(runProperties, replacement));
17709
+ placedReplacement = true;
17710
+ }
17711
+ newChildren.push(child);
17712
+ continue;
17713
+ }
17714
+ const sliceStartInRun = Math.max(0, span.start - runStart);
17715
+ const sliceEndInRun = Math.min(length, span.end - runStart);
17716
+ if (sliceStartInRun > 0) {
17717
+ newChildren.push(sliceRun(child, 0, sliceStartInRun));
17718
+ }
17719
+ if (!placedReplacement) {
17720
+ newChildren.push(replacementRun(runProperties, replacement));
17721
+ placedReplacement = true;
17722
+ }
17723
+ if (sliceEndInRun < length) {
17724
+ newChildren.push(sliceRun(child, sliceEndInRun, length));
17725
+ }
17726
+ continue;
17727
+ }
17728
+ if (isParagraph && (child.tag === "w:ins" || child.tag === "w:del")) {
17729
+ let innerLength = 0;
17730
+ for (const inner of child.children) {
17731
+ if (inner.tag === "w:r")
17732
+ innerLength += runTextLength(inner);
17733
+ }
17734
+ offset += innerLength;
17735
+ newChildren.push(child);
17736
+ continue;
17737
+ }
17738
+ newChildren.push(child);
17739
+ }
17740
+ if (!placedReplacement) {
17741
+ newChildren.push(replacementRun(runProperties, replacement));
17742
+ }
17743
+ container.children = newChildren;
17744
+ }
17745
+ function replacementRun(runProperties, text) {
17746
+ if (text.length === 0) {
17747
+ return /* @__PURE__ */ jsxDEV(w.r, {
17748
+ children: [
17749
+ runProperties,
17750
+ /* @__PURE__ */ jsxDEV(w.t, {
17751
+ "xml:space": "preserve",
17752
+ children: ""
17753
+ }, undefined, false, undefined, this)
17754
+ ]
17755
+ }, undefined, true, undefined, this);
17756
+ }
17757
+ return /* @__PURE__ */ jsxDEV(w.r, {
17758
+ children: [
17759
+ runProperties,
17760
+ /* @__PURE__ */ jsxDEV(w.t, {
17761
+ "xml:space": "preserve",
17762
+ children: text
17763
+ }, undefined, false, undefined, this)
17764
+ ]
17765
+ }, undefined, true, undefined, this);
17766
+ }
17767
+ var TrackedChangeBoundaryError;
17768
+ var init_replace_span = __esm(() => {
17769
+ init_jsx();
17770
+ init_parser();
17771
+ init_jsx_dev_runtime();
17772
+ TrackedChangeBoundaryError = class TrackedChangeBoundaryError extends Error {
17773
+ constructor(message) {
17774
+ super(message);
17775
+ this.name = "TrackedChangeBoundaryError";
17776
+ }
17777
+ };
17778
+ });
17149
17779
 
17150
- // src/cli/schema/index.ts
17151
- var exports_schema = {};
17152
- __export(exports_schema, {
17153
- run: () => run18
17780
+ // src/cli/replace/index.ts
17781
+ var exports_replace2 = {};
17782
+ __export(exports_replace2, {
17783
+ run: () => run21
17154
17784
  });
17155
- import { parseArgs as parseArgs16 } from "util";
17156
- async function run18(args) {
17785
+ import { parseArgs as parseArgs18 } from "util";
17786
+ async function run21(args) {
17157
17787
  let parsed;
17158
17788
  try {
17159
- parsed = parseArgs16({
17789
+ parsed = parseArgs18({
17160
17790
  args,
17161
17791
  allowPositionals: true,
17162
17792
  options: {
17163
- json: { type: "boolean" },
17164
- ts: { type: "boolean" },
17793
+ regex: { type: "boolean" },
17794
+ "ignore-case": { type: "boolean" },
17795
+ all: { type: "boolean" },
17796
+ limit: { type: "string" },
17797
+ "dry-run": { type: "boolean" },
17165
17798
  help: { type: "boolean", short: "h" }
17166
17799
  }
17167
17800
  });
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);
17801
+ } catch (parseError) {
17802
+ const message = parseError instanceof Error ? parseError.message : String(parseError);
17803
+ return fail("USAGE", message, HELP21);
17804
+ }
17805
+ if (parsed.values.help) {
17806
+ await writeStdout(HELP21);
17807
+ return EXIT.OK;
17808
+ }
17809
+ const path = parsed.positionals[0];
17810
+ const pattern = parsed.positionals[1];
17811
+ const replacement = parsed.positionals[2];
17812
+ if (!path)
17813
+ return fail("USAGE", "Missing FILE argument", HELP21);
17814
+ if (pattern == null)
17815
+ return fail("USAGE", "Missing PATTERN argument", HELP21);
17816
+ if (replacement == null) {
17817
+ return fail("USAGE", "Missing REPLACEMENT argument", HELP21);
17818
+ }
17819
+ const ignoreCase = Boolean(parsed.values["ignore-case"]);
17820
+ const useRegex = Boolean(parsed.values.regex);
17821
+ const wantAll = Boolean(parsed.values.all);
17822
+ const limitRaw = parsed.values.limit;
17823
+ const limit = limitRaw === undefined ? undefined : Number(limitRaw);
17824
+ if (limit !== undefined && (!Number.isInteger(limit) || limit <= 0)) {
17825
+ return fail("USAGE", `--limit must be a positive integer, got "${limitRaw}"`);
17826
+ }
17827
+ const view = await openOrFail(path);
17828
+ if (typeof view === "number")
17829
+ return view;
17830
+ let allMatches;
17831
+ try {
17832
+ allMatches = findTextSpans(view.doc, pattern, {
17833
+ regex: useRegex,
17834
+ ignoreCase
17835
+ });
17836
+ } catch (matcherError) {
17837
+ const message = matcherError instanceof Error ? matcherError.message : String(matcherError);
17838
+ return fail("USAGE", `Invalid pattern: ${message}`);
17839
+ }
17840
+ let selected;
17841
+ if (limit !== undefined) {
17842
+ selected = allMatches.slice(0, limit);
17843
+ } else if (wantAll) {
17844
+ selected = allMatches;
17845
+ } else {
17846
+ selected = allMatches.slice(0, 1);
17847
+ }
17848
+ const matchesPayload = selected.map((match) => ({
17849
+ locator: `${match.blockId}:${match.start}-${match.end}`,
17850
+ blockId: match.blockId,
17851
+ start: match.start,
17852
+ end: match.end,
17853
+ text: match.text
17854
+ }));
17855
+ if (parsed.values["dry-run"]) {
17856
+ await respond({
17857
+ ok: true,
17858
+ operation: "replace",
17859
+ dryRun: true,
17860
+ path,
17861
+ pattern,
17862
+ replacement,
17863
+ regex: useRegex,
17864
+ ignoreCase,
17865
+ totalMatches: allMatches.length,
17866
+ replaced: selected.length,
17867
+ matches: matchesPayload
17868
+ });
17174
17869
  return EXIT.OK;
17175
17870
  }
17176
- if (parsed.values.ts) {
17177
- await Bun.write(Bun.stdout, types_default);
17871
+ if (selected.length === 0) {
17872
+ await respond({
17873
+ ok: true,
17874
+ operation: "replace",
17875
+ path,
17876
+ pattern,
17877
+ replacement,
17878
+ regex: useRegex,
17879
+ ignoreCase,
17880
+ totalMatches: 0,
17881
+ replaced: 0,
17882
+ matches: []
17883
+ });
17178
17884
  return EXIT.OK;
17179
17885
  }
17180
- await respond(JSON_SCHEMA);
17886
+ const reversed = [...selected].sort((leftMatch, rightMatch) => {
17887
+ if (leftMatch.blockId !== rightMatch.blockId) {
17888
+ return rightMatch.blockId.localeCompare(leftMatch.blockId);
17889
+ }
17890
+ return rightMatch.start - leftMatch.start;
17891
+ });
17892
+ const regexFlags = ignoreCase ? "i" : "";
17893
+ try {
17894
+ for (const match of reversed) {
17895
+ const concreteReplacement = useRegex ? match.text.replace(new RegExp(pattern, regexFlags), replacement) : replacement;
17896
+ const blockRef = resolveBlock(view, match.blockId);
17897
+ replaceSpanInParagraph(blockRef.node, { start: match.start, end: match.end }, concreteReplacement);
17898
+ }
17899
+ } catch (error) {
17900
+ if (error instanceof TrackedChangeBoundaryError) {
17901
+ return fail("TRACKED_CHANGE_CONFLICT", error.message, "Use `docx track-changes off` (or accept/reject the change in Word) before replacing.");
17902
+ }
17903
+ throw error;
17904
+ }
17905
+ await saveDocView(view);
17906
+ await respond({
17907
+ ok: true,
17908
+ operation: "replace",
17909
+ path,
17910
+ pattern,
17911
+ replacement,
17912
+ regex: useRegex,
17913
+ ignoreCase,
17914
+ totalMatches: allMatches.length,
17915
+ replaced: selected.length,
17916
+ matches: matchesPayload
17917
+ });
17181
17918
  return EXIT.OK;
17182
17919
  }
17183
- var HELP18 = `docx schema \u2014 print the AST type definitions
17920
+ var HELP21 = `docx replace \u2014 substitute text spans (sed for docx)
17184
17921
 
17185
17922
  Usage:
17186
- docx schema [options]
17923
+ docx replace FILE PATTERN REPLACEMENT [options]
17187
17924
 
17188
17925
  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
17926
+ --regex treat PATTERN as a JavaScript regular expression
17927
+ --ignore-case case-insensitive match
17928
+ --all replace every match (default: just the first)
17929
+ --limit N replace at most N matches (in document order)
17930
+ --dry-run report what would change without writing the file
17931
+ -h, --help show this help
17932
+
17933
+ Within-paragraph matches only. Run formatting (rPr) on the surrounding text
17934
+ is preserved; the replacement run inherits the rPr of the first run that
17935
+ overlaps the matched span. When a single invocation produces multiple
17936
+ replacements in the same paragraph, they're applied in reverse offset order
17937
+ so earlier offsets don't shift before being applied.
17938
+
17939
+ With --regex, REPLACEMENT supports JS String.replace substitution syntax:
17940
+ $1, $2, ... numbered capture groups
17941
+ $& the matched substring
17942
+ $\` text before the match
17943
+ $' text after the match
17944
+ $$ a literal $
17192
17945
 
17193
17946
  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();
17947
+ docx replace doc.docx "fox" "cat"
17948
+ docx replace doc.docx "fox" "cat" --all
17949
+ docx replace doc.docx "TODO|FIXME" "DONE" --regex --all
17950
+ docx replace doc.docx "(\\w+) (\\w+)" "$2 $1" --regex --all
17951
+ docx replace doc.docx "wordy phrase" "tighter phrase" --all --dry-run
17952
+ `;
17953
+ var init_replace2 = __esm(() => {
17954
+ init_core();
17199
17955
  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
- };
17956
+ init_replace_span();
17370
17957
  });
17371
17958
 
17372
17959
  // src/cli/track-changes/index.tsx
17373
17960
  var exports_track_changes = {};
17374
17961
  __export(exports_track_changes, {
17375
- run: () => run19
17962
+ run: () => run22
17376
17963
  });
17377
- import { parseArgs as parseArgs17 } from "util";
17378
- async function run19(args) {
17964
+ import { parseArgs as parseArgs19 } from "util";
17965
+ async function run22(args) {
17379
17966
  let parsed;
17380
17967
  try {
17381
- parsed = parseArgs17({
17968
+ parsed = parseArgs19({
17382
17969
  args,
17383
17970
  allowPositionals: true,
17384
17971
  options: {
@@ -17388,41 +17975,31 @@ async function run19(args) {
17388
17975
  });
17389
17976
  } catch (parseError) {
17390
17977
  const message = parseError instanceof Error ? parseError.message : String(parseError);
17391
- return fail("USAGE", message, HELP19);
17978
+ return fail("USAGE", message, HELP22);
17392
17979
  }
17393
17980
  if (parsed.values.help) {
17394
- await writeStdout(HELP19);
17981
+ await writeStdout(HELP22);
17395
17982
  return EXIT.OK;
17396
17983
  }
17397
17984
  const path = parsed.positionals[0];
17398
17985
  if (!path)
17399
- return fail("USAGE", "Missing FILE argument", HELP19);
17986
+ return fail("USAGE", "Missing FILE argument", HELP22);
17400
17987
  const mode = parsed.positionals[1];
17401
17988
  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");
17989
+ return fail("USAGE", `Expected "on" or "off", got: ${mode ?? "<nothing>"}`, HELP22);
17990
+ }
17991
+ const view = await openOrFail(path);
17992
+ if (typeof view === "number")
17993
+ return view;
17994
+ const wasMissing = view.settingsTree === undefined;
17995
+ if (!view.settingsTree)
17996
+ view.settingsTree = [];
17997
+ let settingsRoot = XmlNode2.findRoot(view.settingsTree, "w:settings");
17421
17998
  if (!settingsRoot) {
17422
17999
  settingsRoot = /* @__PURE__ */ jsxDEV(w.settings, {
17423
18000
  "xmlns:w": NS_W2
17424
18001
  }, undefined, false, undefined, this);
17425
- settingsTree.push(settingsRoot);
18002
+ view.settingsTree.push(settingsRoot);
17426
18003
  }
17427
18004
  const hasTrackChanges = settingsRoot.children.some((child) => child.tag === "w:trackChanges");
17428
18005
  if (parsed.values["dry-run"]) {
@@ -17441,13 +18018,15 @@ async function run19(args) {
17441
18018
  } else if (mode === "off" && hasTrackChanges) {
17442
18019
  settingsRoot.children = settingsRoot.children.filter((child) => child.tag !== "w:trackChanges");
17443
18020
  }
17444
- view.pkg.writeText(SETTINGS_PART, XmlNode2.serializeWithDeclaration(settingsTree));
17445
- if (!settingsXml) {
17446
- registerSettingsPart(view);
18021
+ if (wasMissing) {
18022
+ registerPart(view.relationshipsTree, view.contentTypesTree, {
18023
+ partName: SETTINGS_PART,
18024
+ contentType: SETTINGS_CONTENT_TYPE,
18025
+ relationshipType: SETTINGS_REL_TYPE,
18026
+ target: "settings.xml"
18027
+ });
17447
18028
  }
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();
18029
+ await saveDocView(view);
17451
18030
  await respond({
17452
18031
  ok: true,
17453
18032
  operation: "track-changes",
@@ -17457,47 +18036,7 @@ async function run19(args) {
17457
18036
  });
17458
18037
  return EXIT.OK;
17459
18038
  }
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
18039
+ var HELP22 = `docx track-changes \u2014 toggle the document's tracked-changes mode
17501
18040
 
17502
18041
  Usage:
17503
18042
  docx track-changes FILE on|off [options]
@@ -17516,14 +18055,76 @@ Examples:
17516
18055
  var init_track_changes = __esm(() => {
17517
18056
  init_core();
17518
18057
  init_jsx();
18058
+ init_package();
17519
18059
  init_parser();
17520
18060
  init_respond();
17521
18061
  init_jsx_dev_runtime();
17522
18062
  });
18063
+ // package.json
18064
+ var package_default = {
18065
+ name: "bun-docx",
18066
+ version: "0.2.0",
18067
+ description: "CLI for AI agents (Claude, Codex) to read, edit, and comment on .docx files with full format fidelity.",
18068
+ keywords: [
18069
+ "docx",
18070
+ "openxml",
18071
+ "ooxml",
18072
+ "word",
18073
+ "cli",
18074
+ "ai",
18075
+ "claude",
18076
+ "codex",
18077
+ "agentic"
18078
+ ],
18079
+ license: "MIT",
18080
+ author: "Kirill Klimuk",
18081
+ repository: {
18082
+ type: "git",
18083
+ url: "git+https://github.com/kklimuk/docx-cli.git"
18084
+ },
18085
+ homepage: "https://github.com/kklimuk/docx-cli#readme",
18086
+ bugs: "https://github.com/kklimuk/docx-cli/issues",
18087
+ type: "module",
18088
+ packageManager: "bun@1.3.13",
18089
+ bin: {
18090
+ docx: "./dist/index.js"
18091
+ },
18092
+ files: [
18093
+ "dist/index.js",
18094
+ "README.md",
18095
+ "LICENSE"
18096
+ ],
18097
+ engines: {
18098
+ bun: ">=1.3.0"
18099
+ },
18100
+ scripts: {
18101
+ dev: "bun src/index.ts",
18102
+ build: "bun build src/index.ts --outfile dist/index.js --target bun",
18103
+ "build:binary": "bun build --compile --outfile=dist/docx src/index.ts",
18104
+ check: "biome check . && knip-bun && bunx tsc --noEmit",
18105
+ test: "bun test",
18106
+ "test:unit": "bun test tests/core tests/cli",
18107
+ "test:integration": "bun test tests/integration",
18108
+ prepack: "bun run build",
18109
+ prepare: "husky"
18110
+ },
18111
+ devDependencies: {
18112
+ "@biomejs/biome": "^2.4.10",
18113
+ "@types/bun": "^1.3.13",
18114
+ husky: "^9.1.7",
18115
+ knip: "^6.3.0",
18116
+ typescript: "^6.0.2"
18117
+ },
18118
+ dependencies: {
18119
+ "fast-xml-builder": "^1.1.6",
18120
+ "fast-xml-parser": "^5.7.2",
18121
+ jszip: "^3.10.1"
18122
+ }
18123
+ };
17523
18124
 
17524
18125
  // src/cli/help.ts
17525
18126
  init_respond();
17526
- var VERSION = "0.1.0";
18127
+ var VERSION = package_default.version;
17527
18128
  var TOP_HELP = `docx ${VERSION} \u2014 read, edit, and comment on .docx files
17528
18129
 
17529
18130
  Usage:
@@ -17535,11 +18136,12 @@ Commands:
17535
18136
  edit FILE Replace text/properties at a locator
17536
18137
  insert FILE Insert a block or run
17537
18138
  delete FILE Remove a block or run range
18139
+ find FILE QUERY Find text spans, return locators
18140
+ replace FILE PATTERN REPL Substitute text spans (sed for docx)
17538
18141
  comments \u2026 Add, reply, resolve, delete, restore, list comments
17539
18142
  images \u2026 Extract, replace, list images
17540
18143
  track-changes FILE on|off Toggle tracked-changes mode
17541
- schema Dump AST JSON Schema
17542
- locators Dump locator grammar reference
18144
+ info \u2026 Reference material (schema, locators)
17543
18145
 
17544
18146
  Run "docx <command> --help" for command-specific help.
17545
18147
 
@@ -17557,18 +18159,19 @@ var COMMANDS = {
17557
18159
  create: () => Promise.resolve().then(() => (init_create(), exports_create)),
17558
18160
  delete: () => Promise.resolve().then(() => (init_delete2(), exports_delete2)),
17559
18161
  edit: () => Promise.resolve().then(() => (init_edit(), exports_edit)),
18162
+ find: () => Promise.resolve().then(() => (init_find(), exports_find)),
17560
18163
  images: () => Promise.resolve().then(() => (init_images(), exports_images)),
18164
+ info: () => Promise.resolve().then(() => (init_info(), exports_info)),
17561
18165
  insert: () => Promise.resolve().then(() => (init_insert(), exports_insert)),
17562
- locators: () => Promise.resolve().then(() => (init_locators_cmd(), exports_locators_cmd)),
17563
18166
  read: () => Promise.resolve().then(() => (init_read2(), exports_read)),
17564
- schema: () => Promise.resolve().then(() => (init_schema(), exports_schema)),
18167
+ replace: () => Promise.resolve().then(() => (init_replace2(), exports_replace2)),
17565
18168
  "track-changes": () => Promise.resolve().then(() => (init_track_changes(), exports_track_changes))
17566
18169
  };
17567
18170
  async function main(argv) {
17568
18171
  const args = argv.slice(2);
17569
18172
  const cmd = args[0];
17570
18173
  if (!cmd) {
17571
- process.stderr.write(`Usage: docx <command> [options]
18174
+ Bun.stderr.write(`Usage: docx <command> [options]
17572
18175
  Run "docx --help" for available commands.
17573
18176
  `);
17574
18177
  return 2;