bun-docx 0.20.3 → 0.21.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.
package/README.md CHANGED
@@ -12,22 +12,23 @@
12
12
 
13
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
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:
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. Haiku columns: July 2026 rerun on v0.20.4; Sonnet columns: June 2026 bake-off (**not** Sonnet 5):
16
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) | 6 | 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) |
17
+ | | Haiku (weak, cheap) | | Sonnet (strong, June run — not Sonnet 5) | |
18
+ | :------------------------- | ------------------: | --------------------: | ---------------------------------------: | --------------------: |
19
+ | | **docx-cli** | default skill | **docx-cli** | default skill |
20
+ | Tasks solved (of 6) | **5.0** (5–5) | 0.7 (0–2) | **6.0** (6–6) | 4.0 (4–4) |
21
+ | Rendered correctly (of 6) | **6.0** | 4.3 | 6.0 | 4.7 |
22
+ | Outright-broken documents | **0** | ~1.3/run (up to 2) | **0** | 0 |
23
+ | Effective input tokens | **3.2M** | 7.9M (2.5×) | **1.6M** | 3.6M (2.2×) |
24
+ | Output tokens | **78k** | 155k (2.0×) | | |
25
+ | Wall-clock | **1,186 s** | 2,438 s (2.1× slower) | **1,175 s** | 2,029 s (1.7× slower) |
25
26
 
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.
27
+ - **The correctness gap is widest on the cheap Haiku tier (~7×)**, 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.
28
+ - **The cost and speed penalties are model-independent** — ~2.2–2.5× more input tokens and ~1.7–2.1× slower at *both* tiers, with token/time ranges that never overlap. (Output tokens, measured in the July rerun: 2× more for the default skill. The June Sonnet run measured input only.)
29
+ - **Word couldn't reliably open the default skill's work** — in the July Haiku runs it failed to open 4 of the skill's 18 outputs (both legal-redline deliverables in 2 of 3 runs); all 18 of docx-cli's opened on the first try.
29
30
 
30
- Full methodology, per-task rubric, and side-by-side renders: **[the bake-off writeup](https://kklimuk.github.io/docx-cli/)**.
31
+ Full methodology, per-task rubrics, and the harness that produced these numbers: [`.claude/skills/weak-agent-test`](.claude/skills/weak-agent-test).
31
32
 
32
33
  ## Install
33
34
 
@@ -86,7 +87,7 @@ Open `mnda-filled.docx` in Word: tracked changes and comments appear in the revi
86
87
 
87
88
  docx-cli ships as an [Agent Skill](https://agentskills.io) — one `SKILL.md` that works across Claude Code, Codex, Pi, and the other harnesses that read the open skill format. The skill teaches the locator model and the redline / comment / fill workflows, then defers to `docx <command> --help` at runtime, so it can't go stale.
88
89
 
89
- **Why a skill?** docx-cli is built for the _weakest, cheapest_ agents. In our weak-agent benchmark — 6 real document tasks (fill a contract, redline, comment, restyle, author from scratch), graded against Word renders, 3 runs each — Haiku driving docx-cli completed **4.3/6** tasks versus **0.7/6** for the default Claude skill, at roughly **2.5× fewer tokens**; with Sonnet it's **6/6 vs 4/6**, with roughly 2x fewer tokens. And every docx-cli output opened cleanly in Word on the first try — it never emits a file the renderer rejects. (Methodology and harness: [`.claude/skills/weak-agent-test`](.claude/skills/weak-agent-test).)
90
+ **Why a skill?** docx-cli is built for the _weakest, cheapest_ agents. In our weak-agent benchmark — 6 real document tasks (fill a contract, redline, comment, restyle, author from scratch), graded against Word renders, 3 runs each — Haiku driving docx-cli completed **5.0/6** tasks versus **0.7/6** for the default Claude skill (~7×), at roughly **2.5× fewer input tokens and 2× fewer output tokens**; with Sonnet (June run — not Sonnet 5) it's **6/6 vs 4/6**, with roughly 2x fewer tokens. And every docx-cli output opened cleanly in Word on the first try — it never emits a file the renderer rejects. (Methodology and harness: [`.claude/skills/weak-agent-test`](.claude/skills/weak-agent-test).)
90
91
 
91
92
  ### Install
92
93
 
@@ -146,13 +147,16 @@ docx find FILE QUERY [--regex] [--ignore-case] [--all] [--nth N] [--current |
146
147
  docx find FILE (--highlight COLOR|any | --color HEX | --bold | --italic | --underline) [--all] [--json] # find by formatting (no QUERY)
147
148
  docx wc FILE [LOCATOR] [--accepted | --baseline | --current] [--json]
148
149
  docx outline FILE [--style-prefix S] [--json]
150
+ docx diff FILE --against SRC [--from LOC] [--to LOC] [--comments] [--json] # what changed vs another version
151
+ docx validate FILE [--json] # ECMA-376 schema check, per WML part (exit 0 = clean)
152
+ docx raw get FILE --at LOCATOR [--json] # a block's exact XML — the read half of the raw patch loop
149
153
  docx styles FILE [--used] [--at STYLEID] [--json] # the style catalog (not in the body) — what --style NAMEs exist
150
154
  docx styles --catalog [--json] # built-in styles you can apply on demand (Title, Heading1–9, Quote, …), no FILE needed
151
155
  docx styles set FILE --at STYLEID [--bold --color HEX --size PT --font NAME --space-before PT --indent-left IN …] # restyle every paragraph/run that uses the style
152
156
  docx styles create FILE STYLEID [--type paragraph|character] [--name "…"] [--based-on STYLEID] [--next STYLEID] [formatting] # define a new custom style
153
157
  docx render FILE [--out DIR] [--engine word|libreoffice|auto] [--dpi N] [--pages 1-N] [--format png|jpg]
154
158
 
155
- docx comments list FILE [--include-resolved] [--thread cN]
159
+ docx comments list FILE [--open] [--thread cN]
156
160
  docx footnotes list FILE
157
161
  docx endnotes list FILE
158
162
  docx headers list FILE
@@ -165,6 +169,25 @@ docx info schema [--ts]
165
169
  docx info locators [--json]
166
170
  ```
167
171
 
172
+ `docx diff` shows **what you changed** as a git-style unified diff — it renders both
173
+ the current file and a baseline to their `read` markdown and diffs them, so `+` lines
174
+ are what you added and `-` lines what you removed (a one-line edit shows inline as
175
+ `[-removed-]{+added+}`). There's no undo/history, so **snapshot before editing** and
176
+ diff against the snapshot:
177
+
178
+ ```sh
179
+ cp report.docx report.orig.docx # keep the original
180
+ docx edit report.docx --at p3 --text "…" # make changes
181
+ docx diff report.docx --against report.orig.docx
182
+ ```
183
+
184
+ `--against` also takes a saved `docx read` output (a text file), or `-` to read the
185
+ baseline from stdin — so you can diff against a git branch without any git integration:
186
+ `git show main:report.docx | docx diff report.docx --against -`. Positional locators
187
+ (`<!-- p3 -->`, cell/row ids) are normalized out so a structural edit doesn't renumber
188
+ every following line; formatting/structure changes (shading, borders, tracked-changes
189
+ state) still show. It's a read-only report, not a patch to apply.
190
+
168
191
  `docx read` surfaces structural facts the Markdown body can't show as HTML-comment
169
192
  annotations (`<!-- docx:TYPE … -->`). These are **read-time visibility hints** — the
170
193
  agent can SEE the structure, but the importer drops them (the structure survives
@@ -214,8 +237,10 @@ size="…in" margins="…in" text-width="…in" -->` note when the page deviates
214
237
  `float`/`wrap`/`align`/`overflow` only when they deviate (an inline, in-bounds
215
238
  image shows just its size). `overflow` flags an image wider than the usable text
216
239
  column (`ImageRun.floating`/`wrap`/`align` + EMU extents in `read --ast`).
217
- - **Headers / footers** surface as `<!-- docx:header text="Quarterly Report" -->`
218
- / `<!-- docx:footer text="Page {page} of {pages}" -->` notes (the `type` attr
240
+ - **Headers / footers** surface as `<!-- docx:header hdr0 text="Quarterly Report" -->`
241
+ / `<!-- docx:footer ftr0 text="Page {page} of {pages}" -->` notes led by the
242
+ marginal's `hdrN`/`ftrN` id, the handle `raw get --at` and `headers`/`footers
243
+ set/clear --at` accept (the `type` attr
219
244
  appears only for `first`/`even`). Fields read as tokens — `{page}` `{pages}`
220
245
  `{date}` `{time}` `{styleref:NAME}` `{filename}` `{title}` `{author}` (`{time}`
221
246
  read-only). A marginal that's
@@ -239,21 +264,28 @@ headers`/`docx footers`.
239
264
  docx create FILE [--title T] [--author A] [--text "..." | --text-file PATH | --from PATH.md | --from -] [--orientation O] [--size SIZE] [--margins M] [--header "..."] [--footer "..." | --page-numbers] [--force]
240
265
  docx insert FILE (--after | --before) LOCATOR <content> # LOCATOR = pN | tN | sN | tN:rRcC:pK
241
266
  docx insert FILE (--at-start | --at-end) <content> # no locator — prepend / append to the document
242
- docx edit FILE --at LOCATOR <content> # LOCATOR = pN | pN:S-E | pN-pM | sN | eqN | tN:rRcC:pK[:S-E]
267
+ docx edit FILE --at LOCATOR <content> # LOCATOR = pN | pN:S-E | pN-pM | tN:rRcC:pK[:S-E] (sections → docx sections, equations → docx equations edit)
243
268
  docx delete FILE --at LOCATOR # LOCATOR = pN | pN-pM | tN | sN | tN:rRcC:pK (cell paragraph)
244
269
  docx sections FILE [--at LOCATOR] [--columns N] [--type T] [--orientation O] [--size SIZE] [--margins M] # LOCATOR = pN-pM | pN (wrap a range in N columns) | sN (edit one section's columns/type/page geometry). Multi-column layout AND page setup live HERE. PAGE GEOMETRY (margins/orientation/size) with NO --at applies to the WHOLE document (every section); --at sN targets one. Columns/type need --at.
245
270
  docx styles set-default-font FILE "Font Name" [--size N] [--all] # document-wide font: sets styles.xml docDefaults + theme major/minor; --all also repoints styles/runs that pin their own font
246
271
  docx replace FILE PATTERN REPLACEMENT [--at pN] [--regex] [--ignore-case] [--all] [--limit N] [--current | --baseline] [--exact] [--track] [--dry-run]
247
272
  # Keeps the run's formatting (bold/font) and any tabs — the no-rebuild way to fill a
248
273
  # formatted/tabbed template line (e.g. "**Org Name**⇥Date"); don't hand-build --runs to refill it.
274
+ # A TAB matches as one character (pattern "City State Zip" fills a tab-separated line), and
275
+ # TAB/NBSP/bullet-glyph variants match their plain equivalents unless --exact.
249
276
  # --at pN (or a cell paragraph tT:rRcC:pN) CONFINES the replace to one paragraph — use it when the
250
277
  # SAME placeholder repeats across the doc (a résumé's "City, State" in every entry) and you want THE
251
278
  # one in a specific paragraph, instead of find → edit --at pN:S-E span surgery. Batch entries take "at" too.
279
+ # MULTI-LINE (editor-style): "\n" in PATTERN matches a line break OR a paragraph boundary
280
+ # (consecutive paragraphs in the body or one table cell; never across a table/section break/cell wall); the REPLACEMENT's own
281
+ # "\n"s then define the result — single-line replacement MERGES the paragraphs (first one's
282
+ # formatting governs), "\n" in the replacement inserts a paragraph mark (splits). Untracked
283
+ # only: refuses under tracking rather than skip the journal. Block ids shift; re-read after.
252
284
 
253
285
  # Batch — apply many changes from ONE read (no re-reading between edits). Keys
254
286
  # on each JSONL line mirror the command's flags; all locators address the doc as
255
287
  # read. insert/edit also accept --batch - to read JSONL from stdin.
256
- docx edit FILE --batch fills.jsonl # { at, <one of: text|clear|markdown|runs|code|task>, style?, … }
288
+ docx edit FILE --batch fills.jsonl # { at, <one of: text|clear|markdown|runs>, style?, … }
257
289
  docx insert FILE --batch additions.jsonl # { after|before, <content>, style?, color?, … }
258
290
  docx replace FILE --batch script.jsonl # { pattern, replacement, at?, regex?, all?, limit?, … } applied in order ("at" scopes that entry to one paragraph)
259
291
  docx delete FILE --batch drop.jsonl # { at } per line — whole blocks (pN/tN/cell), resolved live-first
@@ -295,10 +327,9 @@ docx delete FILE --batch drop.jsonl # { at } per line — whole blocks (
295
327
  # NOTE: in a single no-content call (or one --batch entry) these run-format SET flags and the
296
328
  # paragraph properties (--style/--alignment/--space-*/--line-spacing/--indent-*/--first-line/
297
329
  # --hanging/--tabs) can't ride together — use separate calls/entries, or add --text to set both.
298
- # --task checked|unchecked | --list bullet|ordered [--list-level N] (insert)
299
- # --task checked|unchecked (edit, flip in place)
300
- # --table --rows N --cols N [--widths "A,B,C"] [--table-width V] [--borders S] [--layout L] (insert)
301
- # --image SRC [--alt T] [--width IN] [--height IN] [--caption "Figure 1: …"] (insert; SRC = path, data: URI, or http(s) URL; --caption adds a Word "Caption"-styled line under the figure)
330
+ # --list bullet|ordered [--list-level N] (insert; for a task-list CHECKBOX use `docx tasks add`)
331
+ # --rows N --cols N [--widths "A,B,C"] [--table-width V] [--borders S] [--layout L] (docx tables create)
332
+ # --image SRC [--alt T] [--width IN] [--height IN] [--caption "Figure 1: …"] (docx images add; SRC = path, data: URI, or http(s) URL; --caption adds a Word "Caption"-styled line under the figure)
302
333
  # --page-break | --column-break | --section [--columns N] [--type T] (insert)
303
334
 
304
335
  docx comments add FILE --at LOCATOR --text "..." [--author NAME] [--current | --baseline]
@@ -326,6 +357,8 @@ docx headers clear FILE [--at sN] [--type T | --first-page | --even | --odd]
326
357
  docx footers set FILE … # identical flags, kind=footer (e.g. --page-number --of-pages → "Page X of Y")
327
358
  docx footers clear FILE …
328
359
 
360
+ docx images add FILE (--after | --before) LOCATOR --image SRC [--alt T] [--width IN] [--height IN] [--caption "..."]
361
+ docx images add FILE (--at-start | --at-end) --image SRC [options] # SRC = path, data: URI, or http(s) URL
329
362
  docx images extract FILE --to DIR [--at imgN] # --to = output directory; --at picks one image
330
363
  docx images replace FILE --at imgN --with ./new.png
331
364
  docx images delete FILE --at imgN
@@ -334,6 +367,7 @@ docx hyperlinks add FILE --at pN:S-E --url URL
334
367
  docx hyperlinks replace FILE --at linkN --with URL
335
368
  docx hyperlinks delete FILE --at linkN
336
369
 
370
+ docx tables create FILE (--after|--before LOCATOR | --at-start|--at-end) --rows N --cols M [--widths "A,B,C"] [--table-width V] [--borders S] [--layout L]
337
371
  docx tables insert-row FILE --at tN [--position INDEX] [--cells "a,b,c"]
338
372
  docx tables delete-row FILE --at tN:rR
339
373
  docx tables insert-column FILE --at tN [--position INDEX] [--width TWIPS]
@@ -352,6 +386,15 @@ docx lists set FILE --at pN [--start N] [--format FMT] [--restart] [-
352
386
  # --restart splits a fresh list off here; --continue picks up the previous
353
387
  # list's numbering instead of restarting. Untracked (Word records no revision).
354
388
 
389
+ docx tasks add FILE (--after | --before) LOCATOR (--text LABEL | --runs JSON) [--checked | --unchecked] [--list-level N]
390
+ docx tasks add FILE (--at-start | --at-end) (--text LABEL | --runs JSON) [--checked | --unchecked]
391
+ # insert a GFM task-list checkbox item (☐ default, ☒ with --checked). Consecutive
392
+ # adds after a list anchor build ONE contiguous list.
393
+ docx tasks check FILE --at pN # mark an existing task done (☒), in place
394
+ docx tasks uncheck FILE --at pN # clear an existing task (☐), in place
395
+ # check/uncheck take --track to redline the toggle even with the doc toggle off
396
+ # (surfaces as a checkboxToggle in `track-changes list`).
397
+
355
398
  docx track-changes on|off FILE
356
399
  docx track-changes list FILE [--json]
357
400
  docx track-changes accept FILE (--at tcN [--at tcM ...] | --at revN | --all)
@@ -367,6 +410,25 @@ docx track-changes apply FILE [--accept H ...] [--reject H ...]
367
410
  # re-lists what remains with its renumbered handles.
368
411
  ```
369
412
 
413
+ #### Raw OOXML escape hatch (last resort)
414
+
415
+ ```sh
416
+ docx raw get FILE --at LOCATOR # a block/section/relationship's exact XML
417
+ docx raw edit FILE --at LOCATOR --find S --with S # patch it in place, one gated call
418
+ docx raw replace FILE --at LOCATOR --xml '…' # swap it wholesale (also --batch)
419
+ docx raw insert FILE --after pN --xml '…' # splice new block XML (or a <Relationship/>)
420
+ docx raw part list|get|add|replace|edit FILE … # OPC parts, addressed by --name
421
+ docx validate FILE [--json] # schema-check against ECMA-376 (transitional)
422
+ ```
423
+
424
+ For constructs no modeled verb covers — drop caps, TOC field codes, embedded
425
+ objects. Every mutation runs a gate pipeline (well-formedness, addressable
426
+ roots, ECMA-376 child order, reference/id integrity, and a schema check
427
+ against the bundled transitional XSDs — a change may not ADD errors) and
428
+ **nothing is written when any gate fails**. Inline `--xml`/`--find`/`--with`
429
+ are verbatim; raw changes are never tracked. Details and examples:
430
+ `docx raw --help`.
431
+
370
432
  > **One rule to memorize: addressing an existing thing is always `--at`.**
371
433
  > `comments reply/resolve/delete`, `footnotes/endnotes edit/delete`, `images extract/replace/delete`, `hyperlinks replace/delete`, `tables *`, `track-changes accept/reject`, `edit`, and `delete` all take `--at LOCATOR`. The exceptions are positional or directional by nature: `insert` uses `--after`/`--before LOCATOR` (or `--at-start`/`--at-end` for the document boundaries, no locator); `read` slices with `--from`/`--to LOCATOR`; `wc` takes a positional `[LOCATOR]`; `find`/`replace` take a positional `QUERY`/`PATTERN` (and `replace` accepts an optional `--at pN` to _confine_ the substitution to one paragraph). `images extract --to DIR` is an _output directory_, not a locator.
372
434
 
@@ -377,9 +439,9 @@ The CLI is built for non-interactive agents. **Exit code is the success signal**
377
439
  | Exit | Meaning | Error codes |
378
440
  | ---- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
379
441
  | `0` | success | — |
380
- | `2` | usage / bad locator | `USAGE`, `INVALID_LOCATOR` |
442
+ | `2` | usage / bad locator | `USAGE`, `INVALID_LOCATOR`, `INVALID_XML` |
381
443
  | `3` | addressed thing not found | `FILE_NOT_FOUND`, `PART_NOT_FOUND`, `BLOCK_NOT_FOUND`, `COMMENT_NOT_FOUND`, `IMAGE_NOT_FOUND`, `HYPERLINK_NOT_FOUND`, `TRACKED_CHANGE_NOT_FOUND`, `MATCH_NOT_FOUND` |
382
- | `1` | general failure | `NOT_A_ZIP`, `TRACKED_CHANGE_CONFLICT`, `TABLE_STRUCTURE`, `IMAGE_SOURCE`, `RENDER_ENGINE`, `RENDER_FAILED`, `UNHANDLED` |
444
+ | `1` | general failure | `NOT_A_ZIP`, `TRACKED_CHANGE_CONFLICT`, `TABLE_STRUCTURE`, `VALIDATION_FAILED`, `IMAGE_SOURCE`, `RENDER_ENGINE`, `RENDER_FAILED`, `UNHANDLED` |
383
445
 
384
446
  **Errors** print `{code, error, hint?}` JSON to stdout with a nonzero exit — note there is **no `ok` field**; the exit code plus `code` are the unambiguous signal.
385
447
 
@@ -400,18 +462,18 @@ The CLI is built for non-interactive agents. **Exit code is the success signal**
400
462
 
401
463
  ## Discovering ids
402
464
 
403
- Locators come in two flavors. **Positional block ids** (`pN`, `tN`, `sN`) are derived from document order and **shift after structural edits** — re-read between non-trivial mutations. **Entity ids** (`cN`, `imgN`, `linkN`, `fnN`, `enN`, `tcN`, `eqN`) are surfaced by a `list` verb (or `read --ast`) and are what you pass to `--at`:
465
+ Locators come in two flavors. **Positional block ids** (`pN`, `tN`, `sN`) are derived from document order and **shift after structural edits** — re-read between non-trivial mutations. **Entity ids** (`cN`, `imgN`, `linkN`, `fnN`, `enN`, `tcN`, `eqN`, `hdrN`, `ftrN`) are surfaced by a `list` verb (or `read --ast`) and are what you pass to `--at`:
404
466
 
405
467
  | Id | Discover with | Used by |
406
468
  | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
407
469
  | `pN` / `tN` / `sN` (block ids) | `docx read FILE` (the `<!-- pN -->` trailers), `docx read FILE --ast`, `docx outline FILE` (heading `pN`s), or `docx render` page images | `read`, `edit`, `insert`, `delete`, `wc`, `find` results |
408
470
  | `cN` (comment) | `docx comments list FILE` | `comments reply/resolve/delete --at` |
409
471
  | `fnN` / `enN` (foot/endnote) | `docx footnotes list FILE` / `docx endnotes list FILE` | `footnotes/endnotes edit/delete --at` |
410
- | `hdrN` / `ftrN` (header/footer) | `docx headers list FILE` / `docx footers list FILE` (or `read --ast`) | addressed by section+type, not the id: `headers/footers set --at sN --type T` |
472
+ | `hdrN` / `ftrN` (header/footer) | `docx headers list FILE` / `docx footers list FILE` (or `read --ast` / the `docx:header`/`docx:footer` note) | `raw get --at`, `headers/footers set/clear --at` (or scope by section: `--at sN --type T`) |
411
473
  | `imgN` (image) | `docx images list FILE` | `images extract/replace/delete --at` |
412
474
  | `linkN` (hyperlink) | `docx hyperlinks list FILE` | `hyperlinks replace/delete --at` |
413
475
  | `tcN` (tracked change) | `docx track-changes list FILE` | `track-changes accept/reject --at` |
414
- | `eqN` (equation) | `docx read FILE --ast` (run `latex` field) | `edit --at eqN --equation` |
476
+ | `eqN` (equation) | `docx read FILE --ast` (run `latex` field) | `equations edit --at eqN` |
415
477
 
416
478
  Each `list` verb prints a bare JSON array where every item's `id` is exactly the handle you feed back to `--at` — pipe through `jq` to filter (`docx comments list doc.docx | jq '.[] | select(.author=="Jane")'`).
417
479
 
@@ -438,7 +500,7 @@ cN imgN linkN fnN enN tcN eqN entity ids (comment / image / hyper
438
500
  | Form | Accepted by |
439
501
  | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
440
502
  | `pN`, `tN`, `sN`, `tN:rRcC:pK` (blocks) | `read --from/--to`, `insert --after/--before`, `wc`, `comments add` |
441
- | `pN`, `pN:S-E`, `pN-pM`, `sN`, `eqN`, `tN:rRcC:pK`, `tN:rRcC:pK:S-E` | `edit --at` (span/cell forms strip or replace just that range) |
503
+ | `pN`, `pN:S-E`, `pN-pM`, `tN:rRcC:pK`, `tN:rRcC:pK:S-E` | `edit --at` (span/cell forms strip or replace just that range; sections → `docx sections`, equations → `docx equations edit`) |
442
504
  | `pN`, `pN-pM`, `tN`, `sN`, `tN:rRcC:pK` | `delete --at` |
443
505
  | `pN:S-E`, `pN:S-pM:E`, `tN:rRcC:pK:S-E` (spans) | `comments add --at`, `hyperlinks add --at` (single paragraph), `find`/`wc` results |
444
506
  | `pN[:offset]` (point) | `footnotes/endnotes add --at` |
@@ -486,7 +548,7 @@ docx track-changes accept doc.docx --at tc0 --at tc2 # or --all
486
548
 
487
549
  **Span-aware comments & hyperlinks.** `comments add --at p3:5-20` (and `hyperlinks add`) find the runs containing offsets 5 and 20, split them at the boundaries (preserving `<w:rPr>` on both halves), and insert markers between the slices. Comments authored by older tools that lack `w14:paraId` (required by `commentsExtended.xml`) get a fresh paraId injected automatically on resolve/reply.
488
550
 
489
- **Tracked changes.** With `<w:trackChanges/>` set, `insert`/`edit`/`delete`/`replace` emit native `<w:ins>`/`<w:del>` (attributed via `--author`, `$DOCX_AUTHOR`, or `Reviewer`); pass `--track` to one of those commands (or the `tables` verbs / `images delete`) to track just that invocation even when the doc toggle is off. `edit --at pN --text` runs a word-level diff so unchanged words keep their formatting and only changed words are wrapped — the same shape Word produces mid-tracking. `accept`/`reject` handle run-level ins/del/moveFrom/moveTo, `sectPrChange`, paragraph-mark ins/del, and the table-structural revisions (rowIns/rowDel, cellIns/cellDel, tblGridChange, tcPrChange). OOXML has no tracked-change construct for hyperlink edits or image swaps, so under tracking those emit a `[docx-cli]` audit comment instead of a fake revision (image _deletion_ is honest removal — it wraps a real `<w:del>`).
551
+ **Tracked changes.** With `<w:trackRevisions/>` set, `insert`/`edit`/`delete`/`replace` emit native `<w:ins>`/`<w:del>` (attributed via `--author`, `$DOCX_AUTHOR`, or `Reviewer`); pass `--track` to one of those commands (or the `tables` verbs / `images delete`) to track just that invocation even when the doc toggle is off. `edit --at pN --text` runs a word-level diff so unchanged words keep their formatting and only changed words are wrapped — the same shape Word produces mid-tracking. `accept`/`reject` handle run-level ins/del/moveFrom/moveTo, `sectPrChange`, paragraph-mark ins/del, and the table-structural revisions (rowIns/rowDel, cellIns/cellDel, tblGridChange, tcPrChange). OOXML has no tracked-change construct for hyperlink edits or image swaps, so under tracking those emit a `[docx-cli]` audit comment instead of a fake revision (image _deletion_ is honest removal — it wraps a real `<w:del>`).
490
552
 
491
553
  **Rich content.** Images insert from a path, `data:` URI, or `http(s)` URL (bounded fetch; HEIC→JPEG transcode; SVG sanitized; non-public/metadata addresses refused at every redirect hop). Equations round-trip OOXML `<m:oMath>` ↔ LaTeX (reconstructed, not legacy plaintext) — authored via temml (LaTeX→MathML) plus an in-house MathML→OMML adapter, no LGPL deps. Code blocks emit one `CodeBlock`-styled paragraph per line with optional lowlight syntax highlighting (37 bundled languages); they collapse back to a GFM fenced block on read. GFM task lists round-trip Word's checkbox content control (and the Word-for-Web Wingdings-glyph variant), surfacing as `taskState` in the AST. Tables operate on a merge-aware logical grid so `gridSpan`/`vMerge` cells map onto physical `<w:tc>`, and structural edits refuse to bisect an existing merge.
492
554
 
@@ -509,9 +571,11 @@ docx track-changes accept doc.docx --at tc0 --at tc2 # or --all
509
571
  - **Runtime**: Bun (`node:util` parseArgs, JSX with custom factory, native zlib)
510
572
  - **Parser**: [`jszip`](https://www.npmjs.com/package/jszip) + [`fast-xml-parser`](https://www.npmjs.com/package/fast-xml-parser) + [`fast-xml-builder`](https://www.npmjs.com/package/fast-xml-builder)
511
573
  - **Markdown**: [`unified`](https://www.npmjs.com/package/unified) + [`remark-parse`](https://www.npmjs.com/package/remark-parse) + [`remark-gfm`](https://www.npmjs.com/package/remark-gfm) + [`remark-math`](https://www.npmjs.com/package/remark-math)
574
+ - **Diff**: [`diff`](https://www.npmjs.com/package/diff) (jsdiff, BSD-3) backs `docx diff` — unified line hunks + HTML-aware word-level refinement of the read view
512
575
  - **Math**: [`temml`](https://www.npmjs.com/package/temml) (MIT) compiles LaTeX → MathML; an in-house MathML → OMML adapter handles the OOXML side bidirectionally
513
576
  - **Render**: [`@hyzyla/pdfium`](https://www.npmjs.com/package/@hyzyla/pdfium) (MIT wrapper + Apache-2.0 PDFium-as-WASM) for the PDF → PNG/JPG step, plus [`pngjs`](https://www.npmjs.com/package/pngjs) / [`jpeg-js`](https://www.npmjs.com/package/jpeg-js) for image encoding
514
577
  - **Images**: [`heic-convert`](https://www.npmjs.com/package/heic-convert) (wasm libheif) transcodes HEIC/HEIF input to JPEG on insert
578
+ - **Validation**: [`libxml2-wasm`](https://www.npmjs.com/package/libxml2-wasm) (MIT, libxml2 compiled to WASM) + the bundled ECMA-376 5th-edition transitional XSDs power `docx validate` and the `docx raw` schema gate — in-process, ~55 ms one-time schema compile, single-digit ms per document
515
579
  - **Quality**: Biome + Knip + tsc; LibreOffice headless for round-trip integration tests
516
580
  - **Standard**: ECMA-376 Part 1 §17 (WordprocessingML), Transitional profile
517
581