bun-docx 0.20.4 → 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 +87 -30
- package/dist/index.js +36055 -17201
- package/package.json +3 -1
- package/skills/docx-cli/SKILL.md +2 -1
- package/skills/docx-cli/references/commands.md +2 -0
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
|
-
|
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
| Tasks solved (of 6)
|
|
21
|
-
| Rendered correctly (of 6)
|
|
22
|
-
| Outright-broken documents
|
|
23
|
-
|
|
|
24
|
-
|
|
|
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 (~
|
|
27
|
-
- **The cost and speed penalties are model-independent** — ~2.2–2.
|
|
28
|
-
- **Word couldn't reliably open the default skill's work** — it failed to open
|
|
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
|
|
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 **
|
|
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 [--
|
|
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
|
|
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,7 +264,7 @@ 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 |
|
|
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
|
|
@@ -260,7 +285,7 @@ docx replace FILE PATTERN REPLACEMENT [--at pN] [--regex] [--ignore-case] [--all
|
|
|
260
285
|
# Batch — apply many changes from ONE read (no re-reading between edits). Keys
|
|
261
286
|
# on each JSONL line mirror the command's flags; all locators address the doc as
|
|
262
287
|
# read. insert/edit also accept --batch - to read JSONL from stdin.
|
|
263
|
-
docx edit FILE --batch fills.jsonl # { at, <one of: text|clear|markdown|runs
|
|
288
|
+
docx edit FILE --batch fills.jsonl # { at, <one of: text|clear|markdown|runs>, style?, … }
|
|
264
289
|
docx insert FILE --batch additions.jsonl # { after|before, <content>, style?, color?, … }
|
|
265
290
|
docx replace FILE --batch script.jsonl # { pattern, replacement, at?, regex?, all?, limit?, … } applied in order ("at" scopes that entry to one paragraph)
|
|
266
291
|
docx delete FILE --batch drop.jsonl # { at } per line — whole blocks (pN/tN/cell), resolved live-first
|
|
@@ -302,10 +327,9 @@ docx delete FILE --batch drop.jsonl # { at } per line — whole blocks (
|
|
|
302
327
|
# NOTE: in a single no-content call (or one --batch entry) these run-format SET flags and the
|
|
303
328
|
# paragraph properties (--style/--alignment/--space-*/--line-spacing/--indent-*/--first-line/
|
|
304
329
|
# --hanging/--tabs) can't ride together — use separate calls/entries, or add --text to set both.
|
|
305
|
-
# --
|
|
306
|
-
# --
|
|
307
|
-
# --
|
|
308
|
-
# --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)
|
|
309
333
|
# --page-break | --column-break | --section [--columns N] [--type T] (insert)
|
|
310
334
|
|
|
311
335
|
docx comments add FILE --at LOCATOR --text "..." [--author NAME] [--current | --baseline]
|
|
@@ -333,6 +357,8 @@ docx headers clear FILE [--at sN] [--type T | --first-page | --even | --odd]
|
|
|
333
357
|
docx footers set FILE … # identical flags, kind=footer (e.g. --page-number --of-pages → "Page X of Y")
|
|
334
358
|
docx footers clear FILE …
|
|
335
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
|
|
336
362
|
docx images extract FILE --to DIR [--at imgN] # --to = output directory; --at picks one image
|
|
337
363
|
docx images replace FILE --at imgN --with ./new.png
|
|
338
364
|
docx images delete FILE --at imgN
|
|
@@ -341,6 +367,7 @@ docx hyperlinks add FILE --at pN:S-E --url URL
|
|
|
341
367
|
docx hyperlinks replace FILE --at linkN --with URL
|
|
342
368
|
docx hyperlinks delete FILE --at linkN
|
|
343
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]
|
|
344
371
|
docx tables insert-row FILE --at tN [--position INDEX] [--cells "a,b,c"]
|
|
345
372
|
docx tables delete-row FILE --at tN:rR
|
|
346
373
|
docx tables insert-column FILE --at tN [--position INDEX] [--width TWIPS]
|
|
@@ -359,6 +386,15 @@ docx lists set FILE --at pN [--start N] [--format FMT] [--restart] [-
|
|
|
359
386
|
# --restart splits a fresh list off here; --continue picks up the previous
|
|
360
387
|
# list's numbering instead of restarting. Untracked (Word records no revision).
|
|
361
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
|
+
|
|
362
398
|
docx track-changes on|off FILE
|
|
363
399
|
docx track-changes list FILE [--json]
|
|
364
400
|
docx track-changes accept FILE (--at tcN [--at tcM ...] | --at revN | --all)
|
|
@@ -374,6 +410,25 @@ docx track-changes apply FILE [--accept H ...] [--reject H ...]
|
|
|
374
410
|
# re-lists what remains with its renumbered handles.
|
|
375
411
|
```
|
|
376
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
|
+
|
|
377
432
|
> **One rule to memorize: addressing an existing thing is always `--at`.**
|
|
378
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.
|
|
379
434
|
|
|
@@ -384,9 +439,9 @@ The CLI is built for non-interactive agents. **Exit code is the success signal**
|
|
|
384
439
|
| Exit | Meaning | Error codes |
|
|
385
440
|
| ---- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
386
441
|
| `0` | success | — |
|
|
387
|
-
| `2` | usage / bad locator | `USAGE`, `INVALID_LOCATOR`
|
|
442
|
+
| `2` | usage / bad locator | `USAGE`, `INVALID_LOCATOR`, `INVALID_XML` |
|
|
388
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` |
|
|
389
|
-
| `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` |
|
|
390
445
|
|
|
391
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.
|
|
392
447
|
|
|
@@ -407,18 +462,18 @@ The CLI is built for non-interactive agents. **Exit code is the success signal**
|
|
|
407
462
|
|
|
408
463
|
## Discovering ids
|
|
409
464
|
|
|
410
|
-
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`:
|
|
411
466
|
|
|
412
467
|
| Id | Discover with | Used by |
|
|
413
468
|
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
414
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 |
|
|
415
470
|
| `cN` (comment) | `docx comments list FILE` | `comments reply/resolve/delete --at` |
|
|
416
471
|
| `fnN` / `enN` (foot/endnote) | `docx footnotes list FILE` / `docx endnotes list FILE` | `footnotes/endnotes edit/delete --at` |
|
|
417
|
-
| `hdrN` / `ftrN` (header/footer) | `docx headers list FILE` / `docx footers list FILE` (or `read --ast`
|
|
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`) |
|
|
418
473
|
| `imgN` (image) | `docx images list FILE` | `images extract/replace/delete --at` |
|
|
419
474
|
| `linkN` (hyperlink) | `docx hyperlinks list FILE` | `hyperlinks replace/delete --at` |
|
|
420
475
|
| `tcN` (tracked change) | `docx track-changes list FILE` | `track-changes accept/reject --at` |
|
|
421
|
-
| `eqN` (equation) | `docx read FILE --ast` (run `latex` field) | `edit --at eqN
|
|
476
|
+
| `eqN` (equation) | `docx read FILE --ast` (run `latex` field) | `equations edit --at eqN` |
|
|
422
477
|
|
|
423
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")'`).
|
|
424
479
|
|
|
@@ -445,7 +500,7 @@ cN imgN linkN fnN enN tcN eqN entity ids (comment / image / hyper
|
|
|
445
500
|
| Form | Accepted by |
|
|
446
501
|
| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
447
502
|
| `pN`, `tN`, `sN`, `tN:rRcC:pK` (blocks) | `read --from/--to`, `insert --after/--before`, `wc`, `comments add` |
|
|
448
|
-
| `pN`, `pN:S-E`, `pN-pM`, `
|
|
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`) |
|
|
449
504
|
| `pN`, `pN-pM`, `tN`, `sN`, `tN:rRcC:pK` | `delete --at` |
|
|
450
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 |
|
|
451
506
|
| `pN[:offset]` (point) | `footnotes/endnotes add --at` |
|
|
@@ -493,7 +548,7 @@ docx track-changes accept doc.docx --at tc0 --at tc2 # or --all
|
|
|
493
548
|
|
|
494
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.
|
|
495
550
|
|
|
496
|
-
**Tracked changes.** With `<w:
|
|
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>`).
|
|
497
552
|
|
|
498
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.
|
|
499
554
|
|
|
@@ -516,9 +571,11 @@ docx track-changes accept doc.docx --at tc0 --at tc2 # or --all
|
|
|
516
571
|
- **Runtime**: Bun (`node:util` parseArgs, JSX with custom factory, native zlib)
|
|
517
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)
|
|
518
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
|
|
519
575
|
- **Math**: [`temml`](https://www.npmjs.com/package/temml) (MIT) compiles LaTeX → MathML; an in-house MathML → OMML adapter handles the OOXML side bidirectionally
|
|
520
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
|
|
521
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
|
|
522
579
|
- **Quality**: Biome + Knip + tsc; LibreOffice headless for round-trip integration tests
|
|
523
580
|
- **Standard**: ECMA-376 Part 1 §17 (WordprocessingML), Transitional profile
|
|
524
581
|
|