bun-docx 0.19.1 → 0.20.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 +22 -1
  2. package/dist/index.js +90 -100
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -8,6 +8,27 @@
8
8
  - Agents address text by **stable locators** with character offsets (`p3:5-20`); humans see normal Word formatting on disk.
9
9
  - Custom styles, theme colors, embedded objects — all of it survives. The CLI mutates XML in place rather than re-emitting from a lossy model.
10
10
 
11
+ ## Why docx-cli?
12
+
13
+ The default way agents edit Word docs is to unzip the `.docx` and hand-write the OOXML inside. That takes a strong model to get right, burns tokens, and routinely produces a file Word won't open. `docx-cli` hands the agent plain commands plus an annotated-Markdown read view, so it never has to reason about the XML.
14
+
15
+ We measured it — a controlled A/B bake-off: **six real document tasks** (fill an NDA, fill an invoice, restyle a résumé, redline a contract, finalize a contract, author a journal), the same starting files, and one independent judge grading every result from the **Word-rendered pages**. Three runs per arm at each of two model tiers:
16
+
17
+ | | Haiku (weak, cheap) | | Sonnet (strong) | |
18
+ | :------------------------ | ------------------: | --------------------: | --------------: | --------------------: |
19
+ | | **docx-cli** | default skill | **docx-cli** | default skill |
20
+ | Tasks solved (of 6) | **4.3** (4–5) | 0.7 (0–1) | **6.0** (6–6) | 4.0 (4–4) |
21
+ | Rendered correctly (of 6) | 5.7 | 3.7 | 6.0 | 4.7 |
22
+ | Outright-broken documents | **0** | ~1/run (up to 2) | **0** | 0 |
23
+ | Input tokens | **2.4M** | 6.1M (2.6×) | **1.6M** | 3.6M (2.2×) |
24
+ | Wall-clock | **924 s** | 1,882 s (2.0× slower) | **1,175 s** | 2,029 s (1.7× slower) |
25
+
26
+ - **The correctness gap is widest on the cheap Haiku tier (~6×)**, and a frontier model never closes it — the default skill caps at 4/6, losing the contract redline and the résumé every Sonnet run.
27
+ - **The cost and speed penalties are model-independent** — ~2.2–2.6× more tokens and ~1.7–2× slower at *both* tiers, with token/time ranges that never overlap.
28
+ - **Word couldn't reliably open the default skill's work** — it failed to open 5 of 36 of its outputs; all 36 of docx-cli's opened on the first try.
29
+
30
+ Full methodology, per-task rubric, and side-by-side renders: **[the bake-off writeup](https://kklimuk.github.io/docx-cli/)**.
31
+
11
32
  ## Install
12
33
 
13
34
  **npm** — the simplest path (requires Bun >= 1.3):
@@ -366,7 +387,7 @@ The CLI is built for non-interactive agents. **Exit code is the success signal**
366
387
  | Command class | Default stdout on success | `--verbose` |
367
388
  | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
368
389
  | **Mutator that mints a new handle** — `comments add`→`cN`, `comments reply`→`cN`, `footnotes/endnotes add`→`fnN`/`enN`, `hyperlinks add`→`linkN`, `insert`→the new `pN` | the bare locator(s), **one per line** (a multi-block `--markdown` insert prints several) | full `{ok:true,…}` ack |
369
- | **Mutator with no new handle** — `edit`, `delete`, `replace`, `create`, `comments resolve/delete`, `images replace/delete`, `hyperlinks replace/delete`, `footnotes/endnotes edit/delete`, `headers/footers set/clear`, `tables *`, `track-changes accept/reject` & toggle | **one-line confirmation** — `<operation> <target>` (e.g. `edit t1:r0c1:p0`, `edit 7 changes`, `replace 0 occurrences replaced`) (exit `0`) | full `{ok:true,…}` ack |
390
+ | **Mutator with no new handle** — `edit`, `delete`, `replace`, `create`, `comments resolve/delete`, `images replace/delete`, `hyperlinks replace/delete`, `footnotes/endnotes edit/delete`, `headers/footers set/clear`, `tables *`, `track-changes accept/reject` & toggle | **one-line confirmation** — `<operation> <target>` (e.g. `edit t1:r0c1:p0`, `edit 7 changes`, `replace 3 occurrences replaced`) (exit `0`) | full `{ok:true,…}` ack |
370
391
  | `find` | matched span locators, one per line (no matches → nothing, exit `0`) | `--json` → `{ totalMatches, query, view, matches:[…], normalizedQuery? }` |
371
392
  | `wc` | the bare count (whole-doc adds a tab-separated `sections` column, like `wc`) | `--json` → `{ words, scope, view, sections? }` |
372
393
  | `outline` | indented `LOCATOR⇥TEXT` tree (two spaces per level) | `--json` → nested `[{ id, locator, level, style, text, children }]` |
package/dist/index.js CHANGED
@@ -4435,6 +4435,20 @@ class XmlNode2 {
4435
4435
  }
4436
4436
  return out;
4437
4437
  }
4438
+ collectTextWithBreaks() {
4439
+ if (this.isText)
4440
+ return this.text ?? "";
4441
+ if (this.tag === "w:br" || this.tag === "w:cr")
4442
+ return `
4443
+ `;
4444
+ if (this.tag === "w:tab")
4445
+ return "\t";
4446
+ let out = "";
4447
+ for (const child of this.children) {
4448
+ out += child.collectTextWithBreaks();
4449
+ }
4450
+ return out;
4451
+ }
4438
4452
  clone() {
4439
4453
  const cloned = new XmlNode2(this.tag, { ...this.attributes });
4440
4454
  if (this.text !== undefined)
@@ -5290,6 +5304,11 @@ function RunElement({ run }) {
5290
5304
  }
5291
5305
  return null;
5292
5306
  }
5307
+ function textToRunElements(text) {
5308
+ return textToRuns(text, {}).map((run) => /* @__PURE__ */ jsxDEV(RunElement, {
5309
+ run
5310
+ }, undefined, false, undefined, this)).filter((node) => node !== null);
5311
+ }
5293
5312
  function applyParagraphOptionsInPlace(rebuilt, options) {
5294
5313
  if (!options.style && !options.alignment && !options.tabs && !options.spacing && !options.indent)
5295
5314
  return;
@@ -17744,14 +17763,7 @@ function noteFirstParagraphRuns({
17744
17763
  return runs;
17745
17764
  if (text2 === undefined)
17746
17765
  return [];
17747
- return [
17748
- /* @__PURE__ */ jsxDEV(w.r, {
17749
- children: /* @__PURE__ */ jsxDEV(w.t, {
17750
- "xml:space": "preserve",
17751
- children: ` ${text2}`
17752
- }, undefined, false, undefined, this)
17753
- }, undefined, false, undefined, this)
17754
- ];
17766
+ return textToRunElements(` ${text2}`);
17755
17767
  }
17756
17768
  function TrackedNoteBody({
17757
17769
  config,
@@ -17761,6 +17773,19 @@ function TrackedNoteBody({
17761
17773
  }) {
17762
17774
  const Note = config.kind === "footnote" ? w.footnote : w.endnote;
17763
17775
  const BodyRef = config.kind === "footnote" ? w.footnoteRef : w.endnoteRef;
17776
+ const insRuns = [
17777
+ /* @__PURE__ */ jsxDEV(w.r, {
17778
+ children: [
17779
+ /* @__PURE__ */ jsxDEV(w.rPr, {
17780
+ children: /* @__PURE__ */ jsxDEV(w.rStyle, {
17781
+ "w-val": config.referenceStyle
17782
+ }, undefined, false, undefined, this)
17783
+ }, undefined, false, undefined, this),
17784
+ /* @__PURE__ */ jsxDEV(BodyRef, {}, undefined, false, undefined, this)
17785
+ ]
17786
+ }, undefined, true, undefined, this),
17787
+ ...textToRunElements(` ${text2}`)
17788
+ ];
17764
17789
  return /* @__PURE__ */ jsxDEV(Note, {
17765
17790
  "w-id": id,
17766
17791
  children: /* @__PURE__ */ jsxDEV(w.p, {
@@ -17772,25 +17797,8 @@ function TrackedNoteBody({
17772
17797
  }, undefined, false, undefined, this),
17773
17798
  /* @__PURE__ */ jsxDEV(Ins, {
17774
17799
  meta,
17775
- children: [
17776
- /* @__PURE__ */ jsxDEV(w.r, {
17777
- children: [
17778
- /* @__PURE__ */ jsxDEV(w.rPr, {
17779
- children: /* @__PURE__ */ jsxDEV(w.rStyle, {
17780
- "w-val": config.referenceStyle
17781
- }, undefined, false, undefined, this)
17782
- }, undefined, false, undefined, this),
17783
- /* @__PURE__ */ jsxDEV(BodyRef, {}, undefined, false, undefined, this)
17784
- ]
17785
- }, undefined, true, undefined, this),
17786
- /* @__PURE__ */ jsxDEV(w.r, {
17787
- children: /* @__PURE__ */ jsxDEV(w.t, {
17788
- "xml:space": "preserve",
17789
- children: ` ${text2}`
17790
- }, undefined, false, undefined, this)
17791
- }, undefined, false, undefined, this)
17792
- ]
17793
- }, undefined, true, undefined, this)
17800
+ children: insRuns
17801
+ }, undefined, false, undefined, this)
17794
17802
  ]
17795
17803
  }, undefined, true, undefined, this)
17796
17804
  }, undefined, false, undefined, this);
@@ -17831,15 +17839,9 @@ function wrapNoteBodyAsEdited(noteBody, newText, insMeta, delMeta) {
17831
17839
  continue;
17832
17840
  }
17833
17841
  if (!inserted) {
17834
- const newRun = /* @__PURE__ */ jsxDEV(w.r, {
17835
- children: /* @__PURE__ */ jsxDEV(w.t, {
17836
- "xml:space": "preserve",
17837
- children: newText
17838
- }, undefined, false, undefined, this)
17839
- }, undefined, false, undefined, this);
17840
17842
  result.push(/* @__PURE__ */ jsxDEV(Ins, {
17841
17843
  meta: insMeta,
17842
- children: newRun
17844
+ children: textToRunElements(newText)
17843
17845
  }, undefined, false, undefined, this));
17844
17846
  inserted = true;
17845
17847
  }
@@ -17849,15 +17851,9 @@ function wrapNoteBodyAsEdited(noteBody, newText, insMeta, delMeta) {
17849
17851
  }, undefined, false, undefined, this));
17850
17852
  }
17851
17853
  if (!inserted) {
17852
- const newRun = /* @__PURE__ */ jsxDEV(w.r, {
17853
- children: /* @__PURE__ */ jsxDEV(w.t, {
17854
- "xml:space": "preserve",
17855
- children: newText
17856
- }, undefined, false, undefined, this)
17857
- }, undefined, false, undefined, this);
17858
17854
  result.push(/* @__PURE__ */ jsxDEV(Ins, {
17859
17855
  meta: insMeta,
17860
- children: newRun
17856
+ children: textToRunElements(newText)
17861
17857
  }, undefined, false, undefined, this));
17862
17858
  }
17863
17859
  paragraph.children = result;
@@ -17885,6 +17881,7 @@ function isBareWhitespaceRun(run) {
17885
17881
  return saw;
17886
17882
  }
17887
17883
  var init_emit3 = __esm(() => {
17884
+ init_blocks();
17888
17885
  init_jsx();
17889
17886
  init_track_changes();
17890
17887
  init_emit2();
@@ -21138,6 +21135,9 @@ function nodeText(node) {
21138
21135
  out += child2.collectText();
21139
21136
  else if (child2.tag === "w:tab")
21140
21137
  out += "\t";
21138
+ else if (child2.tag === "w:br" || child2.tag === "w:cr")
21139
+ out += `
21140
+ `;
21141
21141
  }
21142
21142
  return out;
21143
21143
  }
@@ -23700,7 +23700,7 @@ class CommentsView {
23700
23700
  const author = child2.getAttribute("w:author") ?? "";
23701
23701
  const date = child2.getAttribute("w:date") ?? "";
23702
23702
  const initials = child2.getAttribute("w:initials");
23703
- const text2 = child2.collectText();
23703
+ const text2 = child2.collectTextWithBreaks();
23704
23704
  const anchor = anchors.get(commentId) ?? {
23705
23705
  startBlockId: "",
23706
23706
  startOffset: 0,
@@ -24218,7 +24218,7 @@ class NotesView {
24218
24218
  const numericId = child2.getAttribute("w:id");
24219
24219
  if (numericId == null)
24220
24220
  continue;
24221
- const text2 = child2.collectText().replace(/\s+/g, " ").trim();
24221
+ const text2 = child2.collectTextWithBreaks().trim();
24222
24222
  out.push({ id: `${config.idPrefix}${numericId}`, text: text2 });
24223
24223
  }
24224
24224
  return out;
@@ -35447,12 +35447,7 @@ function CommentBody({
35447
35447
  children: /* @__PURE__ */ jsxDEV(w.p, {
35448
35448
  "w14:paraId": options.paraId,
35449
35449
  "w14:textId": "00000000",
35450
- children: /* @__PURE__ */ jsxDEV(w.r, {
35451
- children: /* @__PURE__ */ jsxDEV(w.t, {
35452
- "xml:space": "preserve",
35453
- children: options.text
35454
- }, undefined, false, undefined, this)
35455
- }, undefined, false, undefined, this)
35450
+ children: textToRunElements(options.text)
35456
35451
  }, undefined, false, undefined, this)
35457
35452
  }, undefined, false, undefined, this);
35458
35453
  }
@@ -35519,6 +35514,7 @@ function containsCommentReference(run, numericId) {
35519
35514
  var SpanOutOfRangeError;
35520
35515
  var init_markers = __esm(() => {
35521
35516
  init_comments();
35517
+ init_blocks();
35522
35518
  init_jsx();
35523
35519
  init_parser();
35524
35520
  init_jsx_dev_runtime();
@@ -74332,7 +74328,7 @@ function buildContentParagraph(spec, contentWidth) {
74332
74328
  }, undefined, false, undefined, this)
74333
74329
  }, undefined, false, undefined, this)
74334
74330
  }, undefined, false, undefined, this),
74335
- textRun(spec.text),
74331
+ textToRunElements(spec.text),
74336
74332
  /* @__PURE__ */ jsxDEV(w.r, {
74337
74333
  children: /* @__PURE__ */ jsxDEV(w.tab, {}, undefined, false, undefined, this)
74338
74334
  }, undefined, false, undefined, this),
@@ -74357,7 +74353,7 @@ function buildContentParagraph(spec, contentWidth) {
74357
74353
  alignment ? /* @__PURE__ */ jsxDEV(w.pPr, {
74358
74354
  children: alignment
74359
74355
  }, undefined, false, undefined, this) : null,
74360
- hasText ? textRun(spec.text) : null
74356
+ hasText ? textToRunElements(spec.text) : null
74361
74357
  ]
74362
74358
  }, undefined, true, undefined, this);
74363
74359
  }
@@ -74368,22 +74364,14 @@ function alignChild(align) {
74368
74364
  "w-val": align
74369
74365
  }, undefined, false, undefined, this);
74370
74366
  }
74371
- function textRun(text6) {
74372
- return /* @__PURE__ */ jsxDEV(w.r, {
74373
- children: /* @__PURE__ */ jsxDEV(w.t, {
74374
- "xml:space": "preserve",
74375
- children: text6
74376
- }, undefined, false, undefined, this)
74377
- }, undefined, false, undefined, this);
74378
- }
74379
74367
  function fieldRuns(field) {
74380
74368
  switch (field.type) {
74381
74369
  case "page":
74382
74370
  if (field.ofPages) {
74383
74371
  return [
74384
- textRun("Page "),
74372
+ ...textToRunElements("Page "),
74385
74373
  fieldSimple(" PAGE ", "1"),
74386
- textRun(" of "),
74374
+ ...textToRunElements(" of "),
74387
74375
  fieldSimple(" NUMPAGES ", "1")
74388
74376
  ];
74389
74377
  }
@@ -74445,6 +74433,7 @@ function relationshipTarget(partName) {
74445
74433
  }
74446
74434
  var NS_W3 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main", NS_R2 = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
74447
74435
  var init_marginals2 = __esm(() => {
74436
+ init_blocks();
74448
74437
  init_jsx();
74449
74438
  init_sections();
74450
74439
  init_track_changes();
@@ -82797,6 +82786,12 @@ async function rejectShellMangledValue(text6, help, label = "this value") {
82797
82786
  return await fail("USAGE", `${label} contains "${fragment}" \u2014 a number with no integer part, the signature of a "$" amount gutted by the shell (bash turns double-quoted "$300.00" into ".00" and "$10,000" into ",000"). docx would write the corrupted value verbatim.`, `Wrap any "$"-bearing value in SINGLE quotes ('$300.00') so bash leaves it alone, or supply it via --batch FILE (JSONL never touches the shell). If you really mean "${fragment}", write its integer part (0${fragment}) or use --batch. Help:
82798
82787
  ${help}`);
82799
82788
  }
82789
+ function decodeInlineEscapes(value) {
82790
+ if (value === undefined)
82791
+ return;
82792
+ return value.replace(/\\r\\n|\\[nrt]/g, (match) => match === "\\t" ? "\t" : `
82793
+ `);
82794
+ }
82800
82795
  function parseTaskFlag(value) {
82801
82796
  const normalized = value.toLowerCase();
82802
82797
  if (normalized === "checked" || normalized === "true" || normalized === "1")
@@ -83206,7 +83201,7 @@ async function run(args) {
83206
83201
  const atInput = parsed.values.at;
83207
83202
  const anchorInput = parsed.values.anchor;
83208
83203
  const batchInput = parsed.values.batch;
83209
- const text6 = parsed.values.text;
83204
+ const text6 = decodeInlineEscapes(parsed.values.text);
83210
83205
  const occurrenceRaw = parsed.values.occurrence;
83211
83206
  const defaultAuthor = resolveAuthor(parsed.values.author);
83212
83207
  const outputPath = parsed.values.output;
@@ -83690,7 +83685,7 @@ async function run4(args) {
83690
83685
  if (!path2)
83691
83686
  return fail("USAGE", "Missing FILE argument", HELP4);
83692
83687
  const parentInput = parsed.values.at;
83693
- const text6 = parsed.values.text;
83688
+ const text6 = decodeInlineEscapes(parsed.values.text);
83694
83689
  if (!parentInput)
83695
83690
  return fail("USAGE", "Missing --at cN", HELP4);
83696
83691
  if (!text6)
@@ -83764,6 +83759,7 @@ Examples:
83764
83759
  `;
83765
83760
  var init_reply = __esm(() => {
83766
83761
  init_core2();
83762
+ init_parse_helpers();
83767
83763
  init_respond();
83768
83764
  });
83769
83765
 
@@ -84077,12 +84073,7 @@ function DocumentBody({ text: text6 }) {
84077
84073
  children: /* @__PURE__ */ jsxDEV(w.body, {
84078
84074
  children: [
84079
84075
  text6 !== undefined ? /* @__PURE__ */ jsxDEV(w.p, {
84080
- children: /* @__PURE__ */ jsxDEV(w.r, {
84081
- children: /* @__PURE__ */ jsxDEV(w.t, {
84082
- "xml:space": "preserve",
84083
- children: text6
84084
- }, undefined, false, undefined, this)
84085
- }, undefined, false, undefined, this)
84076
+ children: textToRunElements(text6)
84086
84077
  }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV(w.p, {}, undefined, false, undefined, this),
84087
84078
  /* @__PURE__ */ jsxDEV(DefaultSectionProperties, {}, undefined, false, undefined, this)
84088
84079
  ]
@@ -84180,6 +84171,7 @@ function serializeWithDeclaration(root2) {
84180
84171
  }
84181
84172
  var DOC_NAMESPACES, CONTENT_TYPES, ROOT_RELS, DOCUMENT_RELS;
84182
84173
  var init_template = __esm(() => {
84174
+ init_blocks();
84183
84175
  init_jsx();
84184
84176
  init_parser();
84185
84177
  init_canonical_parts();
@@ -84260,7 +84252,7 @@ async function run7(args) {
84260
84252
  if (!path2) {
84261
84253
  return fail("USAGE", "Missing FILE argument", HELP7);
84262
84254
  }
84263
- const text6 = parsed.values.text;
84255
+ const text6 = decodeInlineEscapes(parsed.values.text);
84264
84256
  const fromPath = parsed.values.from;
84265
84257
  const textFilePath = parsed.values["text-file"];
84266
84258
  const contentSources = [text6, fromPath, textFilePath].filter((value) => value !== undefined);
@@ -85841,7 +85833,7 @@ async function validateParagraphEdit(values2, paragraphOptions) {
85841
85833
  return { kind: "clear", tags };
85842
85834
  }
85843
85835
  }
85844
- const text6 = values2.text;
85836
+ const text6 = decodeInlineEscapes(values2.text);
85845
85837
  const runsJson = values2.runs;
85846
85838
  const codeInline = values2.code;
85847
85839
  const codeFile = values2["code-file"];
@@ -85895,7 +85887,7 @@ async function validateParagraphEdit(values2, paragraphOptions) {
85895
85887
  }
85896
85888
  return { kind: "task", checked };
85897
85889
  }
85898
- const markdownInline = values2.markdown;
85890
+ const markdownInline = decodeInlineEscapes(values2.markdown);
85899
85891
  const markdownFile = values2["markdown-file"];
85900
85892
  const contentFlags = [
85901
85893
  text6 !== undefined,
@@ -86442,9 +86434,9 @@ async function runAddNote(args, kind) {
86442
86434
  if (anchorCount > 1) {
86443
86435
  return fail("USAGE", "--at and --anchor are mutually exclusive", help);
86444
86436
  }
86445
- const text6 = parsed.values.text;
86437
+ const text6 = decodeInlineEscapes(parsed.values.text);
86446
86438
  const runsJson = parsed.values.runs;
86447
- const markdown2 = parsed.values.markdown;
86439
+ const markdown2 = decodeInlineEscapes(parsed.values.markdown);
86448
86440
  const markdownFile = parsed.values["markdown-file"];
86449
86441
  const bodyCount = (text6 !== undefined ? 1 : 0) + (runsJson !== undefined ? 1 : 0) + (markdown2 !== undefined ? 1 : 0) + (markdownFile !== undefined ? 1 : 0);
86450
86442
  if (bodyCount === 0) {
@@ -86868,9 +86860,9 @@ async function runEditNote(args, kind) {
86868
86860
  const config = noteConfig(kind);
86869
86861
  if (!idInput)
86870
86862
  return fail("USAGE", `Missing --at ${config.idPrefix}N`, help);
86871
- const text6 = parsed.values.text;
86863
+ const text6 = decodeInlineEscapes(parsed.values.text);
86872
86864
  const runsJson = parsed.values.runs;
86873
- const markdown2 = parsed.values.markdown;
86865
+ const markdown2 = decodeInlineEscapes(parsed.values.markdown);
86874
86866
  const markdownFile = parsed.values["markdown-file"];
86875
86867
  const bodyCount = (text6 !== undefined ? 1 : 0) + (runsJson !== undefined ? 1 : 0) + (markdown2 !== undefined ? 1 : 0) + (markdownFile !== undefined ? 1 : 0);
86876
86868
  if (bodyCount === 0) {
@@ -87645,7 +87637,7 @@ async function runSetMarginal(args, kind) {
87645
87637
  if (align !== undefined && !isAlign(align)) {
87646
87638
  return fail("USAGE", `Invalid --align: ${align}`, "Valid: left, center, right.");
87647
87639
  }
87648
- const text6 = parsed.values.text;
87640
+ const text6 = decodeInlineEscapes(parsed.values.text);
87649
87641
  if (text6 === undefined && field === undefined) {
87650
87642
  return fail("USAGE", `Nothing to set \u2014 pass --text and/or a field (--page-number/--date/--style-ref/--field).`, helpFor7(kind));
87651
87643
  }
@@ -87740,6 +87732,7 @@ var OPTION_SPEC4, SAVE_FLAGS_HELP = `Output:
87740
87732
  -h, --help Show this help`;
87741
87733
  var init_set = __esm(() => {
87742
87734
  init_core2();
87735
+ init_parse_helpers();
87743
87736
  init_respond();
87744
87737
  init_shared();
87745
87738
  OPTION_SPEC4 = {
@@ -88879,10 +88872,10 @@ async function buildSingleShotOptions(filePath, values2) {
88879
88872
  if (typeof spec === "number")
88880
88873
  return spec;
88881
88874
  if (spec.kind === "text") {
88882
- const rejected = await rejectMarkdownInText(values2.text, HELP20);
88875
+ const rejected = await rejectMarkdownInText(spec.text, HELP20);
88883
88876
  if (typeof rejected === "number")
88884
88877
  return rejected;
88885
- const mangled = await rejectShellMangledValue(values2.text, HELP20, "--text");
88878
+ const mangled = await rejectShellMangledValue(spec.text, HELP20, "--text");
88886
88879
  if (typeof mangled === "number")
88887
88880
  return mangled;
88888
88881
  }
@@ -89022,7 +89015,7 @@ async function chooseContentSpec(values2) {
89022
89015
  }
89023
89016
  async function resolveMarkdownSpec(values2, flag) {
89024
89017
  if (flag === "markdown") {
89025
- const source2 = values2.markdown;
89018
+ const source2 = decodeInlineEscapes(values2.markdown);
89026
89019
  return { kind: "markdown", source: source2 };
89027
89020
  }
89028
89021
  const path2 = values2["markdown-file"];
@@ -89067,7 +89060,7 @@ async function parseImageFlags(values2) {
89067
89060
  const alt = values2.alt;
89068
89061
  if (alt !== undefined)
89069
89062
  out.alt = alt;
89070
- const caption = values2.caption;
89063
+ const caption = decodeInlineEscapes(values2.caption);
89071
89064
  if (caption !== undefined)
89072
89065
  out.caption = caption;
89073
89066
  const widthRaw = values2.width;
@@ -89092,7 +89085,7 @@ function buildTextSpec(values2) {
89092
89085
  const url = values2.url;
89093
89086
  return {
89094
89087
  kind: "text",
89095
- text: values2.text,
89088
+ text: decodeInlineEscapes(values2.text),
89096
89089
  format: {
89097
89090
  color: values2.color,
89098
89091
  bold: values2.bold,
@@ -91608,7 +91601,7 @@ function formatNote(type, pairs, bareTokens = []) {
91608
91601
  return `<!-- ${parts.join(" ")} -->`;
91609
91602
  }
91610
91603
  function htmlAttr(key, value) {
91611
- const escaped = value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
91604
+ const escaped = value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/\n/g, "&#10;").replace(/\r/g, "&#13;").replace(/\t/g, "&#9;");
91612
91605
  return `${key}="${escaped}"`;
91613
91606
  }
91614
91607
  function twipsToInches(twips) {
@@ -93257,6 +93250,13 @@ async function runReplaceBatch(filePath, batchSource, values2) {
93257
93250
  return EXIT2.OK;
93258
93251
  }
93259
93252
  await document4.save(outputPath);
93253
+ const noops = results.filter((entry) => entry.replaced === 0);
93254
+ if (noops.length > 0) {
93255
+ const list3 = noops.map((entry) => JSON.stringify(entry.pattern)).join(", ");
93256
+ const applied = results.length - noops.length;
93257
+ const savedNote = applied === 0 ? "No entry matched, so the document is unchanged." : `The other ${applied} ${applied === 1 ? "entry was" : "entries were"} applied and SAVED \u2014 this nonzero exit means "some entries missed," not "nothing changed."`;
93258
+ return await fail("MATCH_NOT_FOUND", `${noops.length} of ${results.length} replace ${results.length === 1 ? "entry" : "entries"} matched nothing (0 occurrences): ${list3}. ${savedNote}`, `Those patterns weren't found as LITERAL document text \u2014 check for read-view markup (\`<mark>\`/\`<u>\`) or a locator in the pattern. \`docx find\` locates the real text; --ignore-case/--regex/--at scope the search.`);
93259
+ }
93260
93260
  await respondAck({
93261
93261
  ok: true,
93262
93262
  operation: "replace",
@@ -93459,22 +93459,8 @@ async function run40(args) {
93459
93459
  return EXIT2.OK;
93460
93460
  }
93461
93461
  if (selected.length === 0) {
93462
- await respondAck({
93463
- ok: true,
93464
- operation: "replace",
93465
- path: path2,
93466
- pattern,
93467
- replacement,
93468
- regex: useRegex,
93469
- ignoreCase,
93470
- view: findView,
93471
- ...atScope ? { at: atScope } : {},
93472
- totalMatches: 0,
93473
- replaced: 0,
93474
- matches: [],
93475
- ...normalizationFields
93476
- });
93477
- return EXIT2.OK;
93462
+ const scopeNote = atScope ? ` within ${atScope}` : "";
93463
+ return await fail("MATCH_NOT_FOUND", `Pattern not found${scopeNote}: ${JSON.stringify(pattern)} \u2014 0 occurrences, nothing changed.`, `Match LITERAL document text, not read-view markup (\`<mark>\`/\`<u>\`/\`**\`\u2026) or a locator. Run \`docx find ${JSON.stringify(pattern)} ${path2}\` to see if/where it occurs; add --ignore-case, --regex, --at <locator> to scope, or --current/--baseline to search tracked-change text.`);
93478
93464
  }
93479
93465
  const reversed = [...selected].sort((leftMatch, rightMatch) => {
93480
93466
  if (leftMatch.blockId !== rightMatch.blockId) {
@@ -93580,6 +93566,10 @@ Output:
93580
93566
  addressable handle (matched-span locators shift as text changes; re-read or
93581
93567
  use --dry-run to see them). --verbose / --dry-run print
93582
93568
  {ok:true, operation, totalMatches, replaced, matches:[{locator,\u2026}], \u2026}.
93569
+ A PATTERN that matches NOTHING is an error, not a silent success \u2014 0 occurrences
93570
+ exits nonzero (MATCH_NOT_FOUND), so a no-op replace can't read as "done." Unsure it
93571
+ matches? Probe with \`docx find PATTERN FILE\` or --dry-run and READ the reported count
93572
+ \u2014 both exit 0 whether or not it matches; only the real replace exits nonzero on 0.
93583
93573
  Errors print {code, error, hint?} with a nonzero exit.
93584
93574
 
93585
93575
  Examples:
@@ -98024,7 +98014,7 @@ Examples:
98024
98014
  // package.json
98025
98015
  var package_default = {
98026
98016
  name: "bun-docx",
98027
- version: "0.19.1",
98017
+ version: "0.20.0",
98028
98018
  description: "Read, edit, redline, and comment on Microsoft Word .docx files from the command line \u2014 built for AI agents.",
98029
98019
  keywords: [
98030
98020
  "docx",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bun-docx",
3
- "version": "0.19.1",
3
+ "version": "0.20.0",
4
4
  "description": "Read, edit, redline, and comment on Microsoft Word .docx files from the command line — built for AI agents.",
5
5
  "keywords": [
6
6
  "docx",