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 +94 -30
- package/dist/index.js +31706 -12339
- 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,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 |
|
|
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
|
|
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
|
-
# --
|
|
299
|
-
# --
|
|
300
|
-
# --
|
|
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`
|
|
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
|
|
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`, `
|
|
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:
|
|
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
|
|