bun-docx 0.11.0 → 0.13.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/LGPL-3.0.txt +165 -0
- package/NOTICES +181 -0
- package/README.md +216 -115
- package/dist/index.js +72361 -45925
- package/dist/pdfium-jj0zq23w.wasm +0 -0
- package/package.json +19 -4
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.loom.com/share/da70269a970f42caa138fb3389b4b9cc)
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**A `.docx` CLI built for AI agents.** Leave comments, suggest redlines, and edit Word documents without breaking the formatting or losing content — a human accepts or rejects in Word afterward.
|
|
6
6
|
|
|
7
7
|
- Hand a `.docx` to Claude or Codex and get back a redlined copy with comments — open it in Word, accept or reject as usual.
|
|
8
|
-
- Agents
|
|
9
|
-
- Custom styles, theme colors, embedded objects — all of it survives.
|
|
8
|
+
- Agents address text by **stable locators** with character offsets (`p3:5-20`); humans see normal Word formatting on disk.
|
|
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
11
|
## Install
|
|
12
12
|
|
|
@@ -38,19 +38,19 @@ bunx bun-docx read doc.docx
|
|
|
38
38
|
The repo includes a Common Paper Mutual NDA template at `tests/fixtures/mnda.docx`. Below are the primitives an agent would compose to fill in the cover page and leave redline edits — the same flow shown in the video above. Every command was verified end-to-end against the fixture:
|
|
39
39
|
|
|
40
40
|
```sh
|
|
41
|
-
# Make a copy first — there's no undo
|
|
41
|
+
# Make a copy first — there's no undo (git is the history; the CLI overwrites in place)
|
|
42
42
|
cp tests/fixtures/mnda.docx mnda-filled.docx
|
|
43
43
|
|
|
44
44
|
# Read the cover-page table so the agent knows what placeholders exist
|
|
45
|
-
docx read mnda-filled.docx --to t1
|
|
45
|
+
docx read mnda-filled.docx --from t1 --to t1
|
|
46
46
|
|
|
47
47
|
# Fill the yellow-highlighted bracketed placeholders
|
|
48
|
-
docx replace mnda-filled.docx "Fill in: today
|
|
48
|
+
docx replace mnda-filled.docx "Fill in: today's date" "May 6, 2026"
|
|
49
49
|
docx replace mnda-filled.docx "fill in state and/or county" "California"
|
|
50
50
|
docx replace mnda-filled.docx "fill in state" "California"
|
|
51
51
|
docx replace mnda-filled.docx "Fill in, if any." "None."
|
|
52
52
|
|
|
53
|
-
# Verify nothing's left to fill
|
|
53
|
+
# Verify nothing's left to fill (bare locator lines, one per match; nothing → exit 0)
|
|
54
54
|
docx find mnda-filled.docx '\[(Fill|fill)[^]]*\]' --regex --all
|
|
55
55
|
|
|
56
56
|
# Flip on tracked changes for the redline pass
|
|
@@ -61,76 +61,128 @@ docx replace mnda-filled.docx \
|
|
|
61
61
|
"having a reasonable need to know" \
|
|
62
62
|
"with a documented need to know"
|
|
63
63
|
|
|
64
|
-
# Leave a comment for the human reviewer
|
|
65
|
-
docx comments add mnda-filled.docx --
|
|
64
|
+
# Leave a comment for the human reviewer — addresses an existing span with --at
|
|
65
|
+
docx comments add mnda-filled.docx --at p7:0-30 \
|
|
66
66
|
--text "Should we narrow 'representatives' to a named list?"
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
Open `mnda-filled.docx` in Word: tracked changes and comments appear in the review pane, ready to accept, reject, or reply. Or run `docx track-changes accept
|
|
69
|
+
Open `mnda-filled.docx` in Word: tracked changes and comments appear in the review pane, ready to accept, reject, or reply. Or run `docx track-changes accept mnda-filled.docx --all` to bake them in from the CLI.
|
|
70
70
|
|
|
71
|
-
##
|
|
71
|
+
## `docx <command> --help` is the authoritative contract
|
|
72
|
+
|
|
73
|
+
> **Agents: run `docx <command> --help` before composing a call.** Every command's `--help` is the source of truth for its flags, locator forms, and exact output shape — this README is a map, not the territory. Two more must-reads:
|
|
74
|
+
>
|
|
75
|
+
> - **`docx info locators`** — the canonical locator grammar (`--json` for a machine-readable form). The top-level `docx --help` says it outright: *"It is highly recommended to agents to run `docx info locators` to understand their capabilities."*
|
|
76
|
+
> - **`docx info schema`** — the AST type definitions (`--ts` for TypeScript source) that `read --ast` emits.
|
|
77
|
+
|
|
78
|
+
## Command reference
|
|
79
|
+
|
|
80
|
+
`docx <verb>` and `docx <noun> <verb>`. Every command has `--help`. Two groups: **read/query** commands print data to stdout; **mutate** commands change the file (and accept `--dry-run`, `-o/--output PATH`, `-v/--verbose`).
|
|
81
|
+
|
|
82
|
+
### Read & query (print to stdout, never write the file)
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
docx read FILE [--from LOC] [--to LOC] [--accepted | --baseline | --current] [--comments]
|
|
86
|
+
docx read FILE --ast # JSON-AST instead of Markdown (disables the markdown-only flags)
|
|
87
|
+
docx find FILE QUERY [--regex] [--ignore-case] [--all] [--nth N] [--current | --baseline] [--exact] [--json]
|
|
88
|
+
docx find FILE (--highlight COLOR|any | --color HEX | --bold | --italic | --underline) [--all] [--json] # find by formatting (no QUERY)
|
|
89
|
+
docx wc FILE [LOCATOR] [--accepted | --baseline | --current] [--json]
|
|
90
|
+
docx outline FILE [--style-prefix S] [--json]
|
|
91
|
+
docx styles FILE [--used] [--at STYLEID] [--json] # the style catalog (not in the body) — what --style NAMEs exist
|
|
92
|
+
docx render FILE [--out DIR] [--engine word|libreoffice|auto] [--dpi N] [--pages 1-N] [--format png|jpg]
|
|
93
|
+
|
|
94
|
+
docx comments list FILE [--include-resolved] [--thread cN]
|
|
95
|
+
docx footnotes list FILE
|
|
96
|
+
docx endnotes list FILE
|
|
97
|
+
docx images list FILE
|
|
98
|
+
docx hyperlinks list FILE
|
|
99
|
+
docx track-changes list FILE
|
|
100
|
+
|
|
101
|
+
docx info schema [--ts]
|
|
102
|
+
docx info locators [--json]
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`docx read` surfaces structural facts the Markdown body can't show as HTML-comment
|
|
106
|
+
annotations (`<!-- docx:TYPE … -->`). These are **read-time visibility hints** — the
|
|
107
|
+
agent can SEE the structure, but the importer drops them (the structure survives
|
|
108
|
+
normal edits in place, `read --ast` is the lossless view, and `docx columns` /
|
|
109
|
+
`insert --section` / `docx tables …` manage it). They're emitted **deviation-only**
|
|
110
|
+
(only when a value differs from the document default, so a plain document stays
|
|
111
|
+
clean):
|
|
112
|
+
|
|
113
|
+
- **Section breaks** render as `<!-- docx:section sN cols="2" type="continuous" -->`
|
|
114
|
+
on their own line — never a bare `---` (that's a thematic break, and emitting it
|
|
115
|
+
for a section silently turned layout into border paragraphs). A hand-authored
|
|
116
|
+
`---` now unambiguously means a thematic break.
|
|
117
|
+
- **Page geometry** rides a leading `<!-- docx:page orientation="landscape"
|
|
118
|
+
size="…in" margins="…in" text-width="…in" -->` note when the page deviates from
|
|
119
|
+
US-Letter-portrait-1″ — `text-width` is the usable column width. Exact twips are
|
|
120
|
+
in `read --ast` (on each section break: `pageWidth`/`pageHeight`/`pageOrientation`/
|
|
121
|
+
`margin*`).
|
|
122
|
+
- **Tables** carry a leading `<!-- docx:table t0 widths="1,2,3in" borders="double" -->`
|
|
123
|
+
when columns are uneven or borders deviate from the default, plus a per-cell
|
|
124
|
+
`<!-- docx:cell t0:r0c0 gridSpan="2" vMerge="continue" shading="FFE699" -->`
|
|
125
|
+
note on merged/shaded cells — so structure invisible in GFM is visible
|
|
126
|
+
(`Table.borders` / `TableCell.shading` in `read --ast`).
|
|
127
|
+
- **Images** trail a `<!-- docx:image img0 size="6.2x4.1in" float="yes" wrap="square" align="center" overflow="yes" -->`
|
|
128
|
+
note: `size` always (the `` alone doesn't say "6in wide"), and
|
|
129
|
+
`float`/`wrap`/`align`/`overflow` only when they deviate (an inline, in-bounds
|
|
130
|
+
image shows just its size). `overflow` flags an image wider than the usable text
|
|
131
|
+
column (`ImageRun.floating`/`wrap`/`align` + EMU extents in `read --ast`).
|
|
132
|
+
|
|
133
|
+
### Mutate (change FILE in place; `--dry-run`, `-v` everywhere; `-o PATH` on every mutator except `create`, whose positional FILE is already the output)
|
|
72
134
|
|
|
73
135
|
```sh
|
|
74
|
-
docx create FILE [--title T] [--author A] [--text "..."]
|
|
75
|
-
docx
|
|
76
|
-
docx
|
|
77
|
-
docx
|
|
78
|
-
docx
|
|
79
|
-
docx
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
docx
|
|
85
|
-
docx insert
|
|
86
|
-
docx
|
|
87
|
-
docx
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
docx comments add FILE --range p3:5-20 --text "..." [--author NAME] [--current | --baseline]
|
|
136
|
+
docx create FILE [--title T] [--author A] [--text "..." | --from PATH.md | --from -] [--force]
|
|
137
|
+
docx insert FILE (--after | --before) LOCATOR <content> # LOCATOR = pN | tN | sN | tN:rRcC:pK
|
|
138
|
+
docx edit FILE --at LOCATOR <content> # LOCATOR = pN | pN:S-E | pN-pM | sN | eqN | tN:rRcC:pK[:S-E]
|
|
139
|
+
docx delete FILE --at LOCATOR # LOCATOR = pN | pN-pM | tN | sN
|
|
140
|
+
docx columns FILE --at LOCATOR --count N [--type T] # LOCATOR = pN-pM | pN (wrap a range) | sN (recount an existing section)
|
|
141
|
+
docx replace FILE PATTERN REPLACEMENT [--regex] [--ignore-case] [--all] [--limit N] [--current | --baseline] [--exact] [--track] [--dry-run]
|
|
142
|
+
|
|
143
|
+
# Batch — apply many changes from ONE read (no re-reading between edits). Keys
|
|
144
|
+
# on each JSONL line mirror the command's flags; all locators address the doc as
|
|
145
|
+
# read. insert/edit also accept --batch - to read JSONL from stdin.
|
|
146
|
+
docx edit FILE --batch fills.jsonl # { at, <one of: text|clear|markdown|runs|code|task>, style?, … }
|
|
147
|
+
docx insert FILE --batch additions.jsonl # { after|before, <content>, style?, color?, … }
|
|
148
|
+
docx replace FILE --batch script.jsonl # { pattern, replacement, regex?, all?, limit?, … } applied in order
|
|
149
|
+
docx delete FILE --batch drop.jsonl # { at } per line — whole blocks (pN/tN/cell), resolved live-first
|
|
150
|
+
|
|
151
|
+
# All four of insert/edit/delete/replace accept --track to record that one
|
|
152
|
+
# invocation as a tracked change even when the doc's track-changes toggle is off.
|
|
153
|
+
#
|
|
154
|
+
# insert/edit content selectors (run "docx insert --help" / "docx edit --help" for the full list):
|
|
155
|
+
# --text "..." [--style NAME] [--alignment A] [--color HEX] [--bold] [--italic] [--url URL]
|
|
156
|
+
# (a newline in --text becomes a line break <w:br/>, a tab becomes <w:tab/> — verse/addresses stay line-per-line)
|
|
157
|
+
# --runs '[{"type":"text","text":"X","bold":true}]'
|
|
158
|
+
# --markdown "..." | --markdown-file PATH # GFM + math + CriticMarkup + inline HTML formatting → blocks
|
|
159
|
+
# --code "..." | --code-file PATH [--language LANG]
|
|
160
|
+
# --equation "x^2 + y^2" [--display] (insert; edit also accepts --inline)
|
|
161
|
+
# --clear bold,italic,highlight,color,size,font,…|all (edit; strip run formatting, keep text)
|
|
162
|
+
# --task checked|unchecked | --list bullet|ordered [--list-level N] (insert)
|
|
163
|
+
# --task checked|unchecked (edit, flip in place)
|
|
164
|
+
# --table --rows N --cols N [--widths "A,B,C"] [--table-width V] [--borders S] [--layout L] (insert)
|
|
165
|
+
# --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)
|
|
166
|
+
# --page-break | --column-break | --section [--columns N] [--type T] (insert)
|
|
167
|
+
|
|
168
|
+
docx comments add FILE --at LOCATOR --text "..." [--author NAME] [--current | --baseline]
|
|
108
169
|
docx comments add FILE --anchor "phrase" --text "..." [--occurrence N]
|
|
109
|
-
docx comments add FILE --batch reviews.jsonl
|
|
110
|
-
docx comments reply FILE --
|
|
111
|
-
docx comments resolve FILE --
|
|
112
|
-
docx comments
|
|
113
|
-
|
|
114
|
-
docx
|
|
115
|
-
docx
|
|
116
|
-
docx
|
|
117
|
-
docx
|
|
118
|
-
|
|
119
|
-
docx
|
|
120
|
-
|
|
121
|
-
docx
|
|
122
|
-
docx footnotes list FILE
|
|
123
|
-
docx endnotes add FILE --at pN[:offset] --text "..."
|
|
124
|
-
docx endnotes edit FILE --id enN --text "..."
|
|
125
|
-
docx endnotes delete FILE --id enN
|
|
126
|
-
docx endnotes list FILE
|
|
127
|
-
|
|
128
|
-
docx images list FILE
|
|
129
|
-
docx images extract FILE --to ./media [--id imgN]
|
|
170
|
+
docx comments add FILE --batch reviews.jsonl # JSONL: { at | anchor (+occurrence), text, author? }
|
|
171
|
+
docx comments reply FILE --at cN --text "..."
|
|
172
|
+
docx comments resolve FILE --at cN [--at cM ...] [--unset] | --batch resolutions.jsonl
|
|
173
|
+
docx comments delete FILE --at cN [--at cM ...] | --batch removals.jsonl
|
|
174
|
+
|
|
175
|
+
docx footnotes add FILE --at pN[:offset] (--text "..." | --runs JSON | --markdown TEXT)
|
|
176
|
+
docx footnotes edit FILE --at fnN (--text "..." | --runs JSON | --markdown TEXT)
|
|
177
|
+
docx footnotes delete FILE --at fnN
|
|
178
|
+
docx endnotes add FILE --at pN[:offset] (--text "..." | --runs JSON | --markdown TEXT)
|
|
179
|
+
docx endnotes edit FILE --at enN (--text "..." | --runs JSON | --markdown TEXT)
|
|
180
|
+
docx endnotes delete FILE --at enN
|
|
181
|
+
|
|
182
|
+
docx images extract FILE --to DIR [--at imgN] # --to = output directory; --at picks one image
|
|
130
183
|
docx images replace FILE --at imgN --with ./new.png
|
|
131
184
|
docx images delete FILE --at imgN
|
|
132
185
|
|
|
133
|
-
docx hyperlinks list FILE
|
|
134
186
|
docx hyperlinks add FILE --at pN:S-E --url URL
|
|
135
187
|
docx hyperlinks replace FILE --at linkN --with URL
|
|
136
188
|
docx hyperlinks delete FILE --at linkN
|
|
@@ -144,97 +196,146 @@ docx tables merge FILE --at tN:rR1cC1-rR2cC2
|
|
|
144
196
|
docx tables unmerge FILE --at tN:rRcC
|
|
145
197
|
docx tables borders FILE --at tN [--style single|double|none] [--size N] [--color HEX]
|
|
146
198
|
|
|
147
|
-
docx track-changes
|
|
148
|
-
docx track-changes list FILE
|
|
199
|
+
docx track-changes on|off FILE
|
|
149
200
|
docx track-changes accept FILE (--at tcN [--at tcM ...] | --all)
|
|
150
201
|
docx track-changes reject FILE (--at tcN [--at tcM ...] | --all)
|
|
151
|
-
docx info schema [--ts]
|
|
152
|
-
docx info locators [--json]
|
|
153
202
|
```
|
|
154
203
|
|
|
155
|
-
|
|
204
|
+
> **One rule to memorize: addressing an existing thing is always `--at`.**
|
|
205
|
+
> `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`; `read` slices with `--from`/`--to LOCATOR`; `wc` takes a positional `[LOCATOR]`; `find`/`replace` take a positional `QUERY`/`PATTERN`. `images extract --to DIR` is an *output directory*, not a locator.
|
|
156
206
|
|
|
157
|
-
|
|
207
|
+
## Output contract
|
|
158
208
|
|
|
159
|
-
|
|
209
|
+
The CLI is built for non-interactive agents. **Exit code is the success signal**, output is data:
|
|
160
210
|
|
|
161
|
-
|
|
211
|
+
| Exit | Meaning | Error codes |
|
|
212
|
+
| ---- | ------- | ----------- |
|
|
213
|
+
| `0` | success | — |
|
|
214
|
+
| `2` | usage / bad locator | `USAGE`, `INVALID_LOCATOR` |
|
|
215
|
+
| `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` |
|
|
216
|
+
| `1` | general failure | `NOT_A_ZIP`, `TRACKED_CHANGE_CONFLICT`, `TABLE_STRUCTURE`, `IMAGE_SOURCE`, `RENDER_ENGINE`, `RENDER_FAILED`, `UNHANDLED` |
|
|
162
217
|
|
|
163
|
-
|
|
218
|
+
**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.
|
|
164
219
|
|
|
165
|
-
**
|
|
220
|
+
**The `ok` field appears in exactly one place: the `--verbose` success ack** (`{ok:true, operation, path, …}`). Without `-v`, success output is shaped for the next command:
|
|
166
221
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
222
|
+
| Command class | Default stdout on success | `--verbose` |
|
|
223
|
+
| ------------- | ------------------------- | ----------- |
|
|
224
|
+
| **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 |
|
|
225
|
+
| **Mutator with no new handle** — `edit`, `delete`, `replace`, `create`, `comments resolve/delete`, `images replace/delete`, `hyperlinks replace/delete`, `footnotes/endnotes edit/delete`, `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 |
|
|
226
|
+
| `find` | matched span locators, one per line (no matches → nothing, exit `0`) | `--json` → `{ totalMatches, query, view, matches:[…], normalizedQuery? }` |
|
|
227
|
+
| `wc` | the bare count (whole-doc adds a tab-separated `sections` column, like `wc`) | `--json` → `{ words, scope, view, sections? }` |
|
|
228
|
+
| `outline` | indented `LOCATOR⇥TEXT` tree (two spaces per level) | `--json` → nested `[{ id, locator, level, style, text, children }]` |
|
|
229
|
+
| `read` | GFM Markdown, each paragraph trailed by `<!-- pN -->` | `--ast` → the JSON AST body (`docx info schema`) |
|
|
230
|
+
| `render` | image paths, one per line | `--verbose` → `{ok, operation, path, engine, output, pages}` |
|
|
231
|
+
| `* list` (all six `list` verbs) | a **bare JSON array**; each item's `id` is its `--at` handle | — |
|
|
170
232
|
|
|
171
|
-
|
|
233
|
+
`--dry-run` always prints a preview object (no `ok`) and writes nothing; it wins over `-o/--output`.
|
|
172
234
|
|
|
173
|
-
|
|
235
|
+
## Discovering ids
|
|
174
236
|
|
|
175
|
-
|
|
237
|
+
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`:
|
|
176
238
|
|
|
177
|
-
|
|
239
|
+
| Id | Discover with | Used by |
|
|
240
|
+
| -- | ------------- | ------- |
|
|
241
|
+
| `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 |
|
|
242
|
+
| `cN` (comment) | `docx comments list FILE` | `comments reply/resolve/delete --at` |
|
|
243
|
+
| `fnN` / `enN` (foot/endnote) | `docx footnotes list FILE` / `docx endnotes list FILE` | `footnotes/endnotes edit/delete --at` |
|
|
244
|
+
| `imgN` (image) | `docx images list FILE` | `images extract/replace/delete --at` |
|
|
245
|
+
| `linkN` (hyperlink) | `docx hyperlinks list FILE` | `hyperlinks replace/delete --at` |
|
|
246
|
+
| `tcN` (tracked change) | `docx track-changes list FILE` | `track-changes accept/reject --at` |
|
|
247
|
+
| `eqN` (equation) | `docx read FILE --ast` (run `latex` field) | `edit --at eqN --equation` |
|
|
178
248
|
|
|
179
|
-
|
|
249
|
+
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")'`).
|
|
180
250
|
|
|
181
|
-
|
|
251
|
+
## Locators
|
|
182
252
|
|
|
183
|
-
`
|
|
253
|
+
`docx info locators` (`--json` for machine-readable) is the canonical reference. The grammar in brief:
|
|
184
254
|
|
|
185
|
-
|
|
255
|
+
```
|
|
256
|
+
pN paragraph N pN:S-E chars S..E within paragraph N
|
|
257
|
+
pN-pM whole-paragraph range pN:S-pM:E cross-paragraph character range
|
|
258
|
+
sN section break N tN table N
|
|
259
|
+
tN:rRcC cell at row R, col C tN:rRcC:pK paragraph K of that cell (chainable)
|
|
260
|
+
tN:rR / tN:cC table row R / column C tN:rR1cC1-rR2cC2 rectangular cell region (merge)
|
|
261
|
+
cN imgN linkN fnN enN tcN eqN entity ids (comment / image / hyperlink /
|
|
262
|
+
footnote / endnote / tracked-change / equation)
|
|
263
|
+
```
|
|
186
264
|
|
|
187
|
-
|
|
265
|
+
**Offset semantics: character offsets are 0-based, start-inclusive, end-exclusive** — `p3:5-20` is the 15 characters at indices 5..19 of paragraph 3. Offsets count the *visible* text of the paragraph in the selected view (accepted by default).
|
|
188
266
|
|
|
189
|
-
|
|
267
|
+
**Nested tables chain the same syntax** arbitrarily deep — `t0:r2c1:t0:r0c0:p0` is the first paragraph of the (0,0) cell of the first table nested inside the (2,1) cell of the document's first table.
|
|
190
268
|
|
|
191
|
-
|
|
269
|
+
**Not every command accepts every form** — each command's `--at`/`--from`/positional help lists exactly what it takes. The shapes:
|
|
192
270
|
|
|
193
|
-
|
|
271
|
+
| Form | Accepted by |
|
|
272
|
+
| ---- | ----------- |
|
|
273
|
+
| `pN`, `tN`, `sN`, `tN:rRcC:pK` (blocks) | `read --from/--to`, `insert --after/--before`, `wc`, `comments add` |
|
|
274
|
+
| `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) |
|
|
275
|
+
| `pN`, `pN-pM`, `tN`, `sN` | `delete --at` |
|
|
276
|
+
| `pN:S-E`, `pN:S-pM:E`, `tN:rRcC:pK:S-E` (spans) | `comments add --at`, `hyperlinks add --at` (single paragraph), `find`/`wc` results |
|
|
277
|
+
| `pN[:offset]` (point) | `footnotes/endnotes add --at` |
|
|
278
|
+
| `cN` / `fnN` / `enN` / `imgN` / `linkN` / `tcN` (entities) | the matching noun's `--at` (the `c`/`fn`/`en`/`img`/`link`/`tc` prefix is optional) |
|
|
279
|
+
| `tN`, `tN:rR`, `tN:cC`, `tN:rRcC`, `tN:rR1cC1-rR2cC2` | the `tables` verbs |
|
|
194
280
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
281
|
+
## Common workflows
|
|
282
|
+
|
|
283
|
+
**find → comment.** `find` emits bare locators that drop straight into `comments add --at` (same default view, so offsets line up — no coordinate translation):
|
|
284
|
+
|
|
285
|
+
```sh
|
|
286
|
+
docx comments add doc.docx --at "$(docx find doc.docx 'fatally flawed' | head -1)" \
|
|
287
|
+
--text "Cite a source here?"
|
|
288
|
+
# or anchor by phrase directly:
|
|
289
|
+
docx comments add doc.docx --anchor "fatally flawed" --text "Cite a source here?"
|
|
204
290
|
```
|
|
205
291
|
|
|
206
|
-
|
|
292
|
+
**read → edit markdown round-trip.** `read` emits a markdown dialect that `edit --markdown` re-parses, so render → LLM-rewrite → splice-back is lossless for paragraphs/lists/quotes:
|
|
207
293
|
|
|
208
|
-
|
|
294
|
+
```sh
|
|
295
|
+
docx read doc.docx --from p3 --to p3 # → markdown (with <!-- p3 --> trailer)
|
|
296
|
+
# … hand to an LLM, get a revised block back …
|
|
297
|
+
docx edit doc.docx --at p3 --markdown-file revised.md # multi-block source expands naturally
|
|
298
|
+
```
|
|
209
299
|
|
|
210
|
-
|
|
300
|
+
Use `--markdown-file` (not `--markdown TEXT`) when the source starts with `-` — Node's `parseArgs` rejects leading-dash flag values.
|
|
211
301
|
|
|
212
|
-
**
|
|
302
|
+
**track-changes review loop.** Toggle tracking on, make edits (they auto-emit `<w:ins>`/`<w:del>`), then inventory and resolve:
|
|
213
303
|
|
|
214
|
-
|
|
304
|
+
```sh
|
|
305
|
+
docx track-changes doc.docx on
|
|
306
|
+
docx replace doc.docx "old phrasing" "new phrasing" --all
|
|
307
|
+
docx track-changes list doc.docx # → JSON array of { id:tcN, kind, author, text, … }
|
|
308
|
+
docx read doc.docx --current # → CriticMarkup {++ins++}[^tcN] / {--del--}[^tcN]
|
|
309
|
+
docx track-changes accept doc.docx --at tc0 --at tc2 # or --all
|
|
310
|
+
```
|
|
215
311
|
|
|
216
|
-
|
|
312
|
+
`read` has three tracked-change views: default **`--accepted`** renders clean text — drops subtractive edits and inlines additive ones (the post-accept document); **`--current`** shows CriticMarkup with `[^tcN]` footnotes; **`--baseline`** does the reverse of accepted (the pre-change document). `find`, `replace`, `wc`, and `comments add` honor the same `--accepted`/`--baseline`/`--current` flags so offsets stay consistent across commands. Add `--comments` to `read` to append `[^cN]` footnotes for comment spans.
|
|
313
|
+
|
|
314
|
+
## How It Works
|
|
217
315
|
|
|
218
|
-
**
|
|
316
|
+
**In-place XML mutation.** The AST returned by `read` is a _view_ over the parsed XML tree, not a separate model. When you `edit` or `comments add`, the CLI mutates the underlying XML nodes directly and serializes back. Anything not modeled in the AST (custom styles, theme colors, schema extensions) survives because untouched regions are never re-emitted. Never delete a relationship something still references — that corrupts the file — so part/relationship pruning is gated on a reference scan; unreferenced orphans are left in place.
|
|
219
317
|
|
|
220
|
-
**
|
|
318
|
+
**JSX for emitters.** Constructing OOXML fragments imperatively (`<w:rPr>` → `<w:b/>` → `<w:color w:val="800080"/>`) is verbose, so fresh XML is authored in JSX with a custom factory: `<w.rPr><w.b/><w.color w-val="800080"/></w.rPr>` becomes the right `XmlNode` tree.
|
|
221
319
|
|
|
222
|
-
**
|
|
320
|
+
**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.
|
|
223
321
|
|
|
224
|
-
**
|
|
322
|
+
**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>`).
|
|
225
323
|
|
|
226
|
-
**
|
|
324
|
+
**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.
|
|
227
325
|
|
|
228
|
-
**
|
|
326
|
+
**Markdown dialect.** `create --from`, `insert/edit --markdown`, and the note bodies all parse the same GFM + math + CriticMarkup + inline-HTML-formatting dialect (remark + remark-gfm + remark-math + an in-house inline-surgery transform), composing the existing OOXML emitters. `read` emits a compatible dialect, so the read → edit → write loop round-trips (lossless for paragraphs, lists, and nested blockquotes; code/tables/math/headings inside a blockquote intentionally escape to top level on import). `read --ast` is the fully lossless JSON form.
|
|
229
327
|
|
|
230
|
-
**
|
|
328
|
+
**Run formatting beyond bold/italic.** Properties markdown has no native syntax for — text color, theme color, highlight, shading, underline (all 18 styles + color), super/subscript, small/all caps, font, and size — are emitted as the **HTML a markdown reader actually renders**, so the output looks right in GitHub, VS Code, Obsidian, and browsers (Pandoc `[text]{…}` spans render as literal brackets in all of those). `read` emits semantic tags where they exist — `<mark>overdue</mark>`, `<sup>x</sup>`, `<sub>2</sub>` — a `<span style="color:#C00000">…</span>` for the CSS-expressible properties, and `data-*` attributes for the OOXML-only ones CSS can't express (theme colors, underline styles); `insert/edit --markdown` parses them back losslessly, and a leading `<!-- docx:base font="Arial" size="8pt" -->` note declares the document's dominant font/size once so the body isn't buried in per-run repetition. Bold/italic/strike/code/links stay native (`**`/`*`/`~~`/`` ` ``/`[](…)`). Because the inline-surgery transform scans whole sibling sequences, a CriticMarkup marker or span can straddle other formatting — `{++**bold insertion**++}` is tracked correctly. An invalid enum value (e.g. a bogus highlight name) fails with a clear error rather than silently vanishing. Inserted plain content inherits the surrounding paragraph's font/size so it blends in.
|
|
231
329
|
|
|
232
|
-
**
|
|
330
|
+
**Visual verification.** `docx render` is the only command that needs an external runtime: it drives Microsoft Word (macOS via `osascript`, Windows via PowerShell COM — the ground-truth renderer) or LibreOffice (`soffice`, cross-platform) to produce a PDF, then rasterizes in-process via the bundled `@hyzyla/pdfium` WASM package — no poppler/pdftoppm/ImageMagick needed. Agents that consume PNGs use this to verify edits, diff accept/reject before-vs-after, or generate screenshots.
|
|
233
331
|
|
|
234
332
|
## Stack
|
|
235
333
|
|
|
236
334
|
- **Runtime**: Bun (`node:util` parseArgs, JSX with custom factory, native zlib)
|
|
237
335
|
- **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)
|
|
336
|
+
- **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)
|
|
337
|
+
- **Math**: [`temml`](https://www.npmjs.com/package/temml) (MIT) compiles LaTeX → MathML; an in-house MathML → OMML adapter handles the OOXML side bidirectionally
|
|
338
|
+
- **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
|
|
238
339
|
- **Images**: [`heic-convert`](https://www.npmjs.com/package/heic-convert) (wasm libheif) transcodes HEIC/HEIF input to JPEG on insert
|
|
239
340
|
- **Quality**: Biome + Knip + tsc; LibreOffice headless for round-trip integration tests
|
|
240
341
|
- **Standard**: ECMA-376 Part 1 §17 (WordprocessingML), Transitional profile
|