bun-docx 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +106 -18
  2. package/dist/index.js +5716 -1934
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -90,11 +90,15 @@ docx wc FILE [LOCATOR] [--accepted | --baseline | --current] [--json]
90
90
  docx outline FILE [--style-prefix S] [--json]
91
91
  docx styles FILE [--used] [--at STYLEID] [--json] # the style catalog (not in the body) — what --style NAMEs exist
92
92
  docx styles --catalog [--json] # built-in styles you can apply on demand (Title, Heading1–9, Quote, …), no FILE needed
93
+ 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
94
+ docx styles create FILE STYLEID [--type paragraph|character] [--name "…"] [--based-on STYLEID] [--next STYLEID] [formatting] # define a new custom style
93
95
  docx render FILE [--out DIR] [--engine word|libreoffice|auto] [--dpi N] [--pages 1-N] [--format png|jpg]
94
96
 
95
97
  docx comments list FILE [--include-resolved] [--thread cN]
96
98
  docx footnotes list FILE
97
99
  docx endnotes list FILE
100
+ docx headers list FILE
101
+ docx footers list FILE
98
102
  docx images list FILE
99
103
  docx hyperlinks list FILE
100
104
  docx track-changes list FILE
@@ -111,15 +115,37 @@ normal edits in place, `read --ast` is the lossless view, and `docx sections` /
111
115
  (only when a value differs from the document default, so a plain document stays
112
116
  clean):
113
117
 
118
+ - **Per-paragraph style/spacing/indent** — the most common annotation — rides a
119
+ `<!-- docx:p pN style="Caption" align="center" space-after="6pt" line-spacing="1" indent-left="0.25in" -->`
120
+ note, emitted deviation-only (only the attrs that differ from the style/document
121
+ default). Each attribute maps to the matching `edit`/`insert` flag (`--style`,
122
+ `--alignment`, `--space-before`/`--space-after`, `--line-spacing`,
123
+ `--indent-left`/`--indent-right`/`--first-line`/`--hanging`), so an agent reads a
124
+ value and re-applies it. The paragraph's locator rides this note as its leading `pN`
125
+ token, so an annotated paragraph does NOT also get a bare `<!-- pN -->` (only
126
+ undeviating paragraphs get the bare locator). Full properties are in `read --ast`.
114
127
  - **Section breaks** render as `<!-- docx:section sN cols="2" type="continuous" -->`
115
128
  on their own line — never a bare `---` (that's a thematic break, and emitting it
116
129
  for a section silently turned layout into border paragraphs). A hand-authored
117
130
  `---` now unambiguously means a thematic break.
118
- - **Page geometry** rides a leading `<!-- docx:page orientation="landscape"
131
+ - **Page geometry** rides a leading `<!-- docx:page sN orientation="landscape"
119
132
  size="…in" margins="…in" text-width="…in" -->` note when the page deviates from
120
- US-Letter-portrait-1″ — `text-width` is the usable column width. Exact twips are
121
- in `read --ast` (on each section break: `pageWidth`/`pageHeight`/`pageOrientation`/
122
- `margin*`).
133
+ US-Letter-portrait-1″ — `text-width` is the usable column width, and the leading
134
+ `sN` is the section to re-apply against. A `varies="by-section"` attribute is added
135
+ when a later section's page setup differs from the leading one — and in that case
136
+ the note fires **even if page 1 is plain default Letter-portrait-1″** (it then shows
137
+ just `text-width` + `varies="by-section"`), warning that the geometry shown describes
138
+ only the leading section; use `read --ast` for every section's exact geometry.
139
+ Exact twips are in `read --ast` (on each
140
+ section break: `pageWidth`/`pageHeight`/`pageOrientation`/`margin*`). Set it for the
141
+ WHOLE document with `docx sections --orientation/--size/--margins` (no `--at` →
142
+ every section gets it, so a multi-section doc doesn't leave the trailing section
143
+ behind), one section with `docx sections --at sN …`, or at `create` time; under
144
+ track-changes it records as one `<w:sectPrChange>` per section (accept/reject in Word).
145
+ Changing margins/size also **auto-realigns right-edge tab columns** (résumé
146
+ dates/locations): a LEFT tab calibrated to the old margins would overflow and wrap
147
+ at the new width, so page setup converts each to a RIGHT tab flush at the new
148
+ margin and reports how many it fixed — no second `--tabs right` step needed.
123
149
  - **Tables** carry a leading `<!-- docx:table t0 widths="1,2,3in" borders="double" -->`
124
150
  when columns are uneven or borders deviate from the default, plus a per-cell
125
151
  `<!-- docx:cell t0:r0c0 gridSpan="2" vMerge="continue" shading="FFE699" -->`
@@ -130,25 +156,47 @@ clean):
130
156
  `float`/`wrap`/`align`/`overflow` only when they deviate (an inline, in-bounds
131
157
  image shows just its size). `overflow` flags an image wider than the usable text
132
158
  column (`ImageRun.floating`/`wrap`/`align` + EMU extents in `read --ast`).
159
+ - **Headers / footers** surface as `<!-- docx:header text="Quarterly Report" -->`
160
+ / `<!-- docx:footer text="Page {page} of {pages}" -->` notes (the `type` attr
161
+ appears only for `first`/`even`). Fields read as tokens — `{page}` `{pages}`
162
+ `{date}` `{time}` `{styleref:NAME}` `{filename}` `{title}` `{author}` (`{time}`
163
+ read-only). A marginal that's
164
+ the same on every section rides the top; one that **differs by section** renders at
165
+ that section's **start** (alongside the `docx:section` note, which also renders at
166
+ the section's start with `applies-to="… (below)"`), so each hint reads right before
167
+ the content it governs. The text lives in the comment attribute
168
+ so the importer drops it (it can't re-inject into the body); full entries are in
169
+ `read --ast` under `headers`/`footers` (`Marginal[]`). Set with `docx
170
+ headers`/`docx footers`.
171
+ - **Track-changes state** rides a head `<!-- docx:track-changes on -->` line when the
172
+ document's tracking toggle is enabled (deviation-only — off emits nothing), so an
173
+ agent sees that subsequent edits will be redlined without inspecting `settings.xml`.
174
+ Toggle it with `docx track-changes FILE on|off`; the three tracked-change read views
175
+ (`--accepted`/`--current`/`--baseline`) are covered under the review loop below.
133
176
 
134
177
  ### Mutate (change FILE in place; `--dry-run`, `-v` everywhere; `-o PATH` on every mutator except `create`, whose positional FILE is already the output)
135
178
 
136
179
  ```sh
137
- docx create FILE [--title T] [--author A] [--text "..." | --text-file PATH | --from PATH.md | --from -] [--force]
180
+ 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]
138
181
  docx insert FILE (--after | --before) LOCATOR <content> # LOCATOR = pN | tN | sN | tN:rRcC:pK
139
182
  docx insert FILE (--at-start | --at-end) <content> # no locator — prepend / append to the document
140
183
  docx edit FILE --at LOCATOR <content> # LOCATOR = pN | pN:S-E | pN-pM | sN | eqN | tN:rRcC:pK[:S-E]
141
- docx delete FILE --at LOCATOR # LOCATOR = pN | pN-pM | tN | sN
142
- docx sections FILE --at LOCATOR --columns N [--type T] # LOCATOR = pN-pM | pN (wrap a range in N columns) | sN (recount an existing section). Multi-column layout lives HERE, not in insert.
184
+ docx delete FILE --at LOCATOR # LOCATOR = pN | pN-pM | tN | sN | tN:rRcC:pK (cell paragraph)
185
+ 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.
143
186
  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
144
- docx replace FILE PATTERN REPLACEMENT [--regex] [--ignore-case] [--all] [--limit N] [--current | --baseline] [--exact] [--track] [--dry-run]
187
+ docx replace FILE PATTERN REPLACEMENT [--at pN] [--regex] [--ignore-case] [--all] [--limit N] [--current | --baseline] [--exact] [--track] [--dry-run]
188
+ # Keeps the run's formatting (bold/font) and any tabs — the no-rebuild way to fill a
189
+ # formatted/tabbed template line (e.g. "**Org Name**⇥Date"); don't hand-build --runs to refill it.
190
+ # --at pN (or a cell paragraph tT:rRcC:pN) CONFINES the replace to one paragraph — use it when the
191
+ # SAME placeholder repeats across the doc (a résumé's "City, State" in every entry) and you want THE
192
+ # one in a specific paragraph, instead of find → edit --at pN:S-E span surgery. Batch entries take "at" too.
145
193
 
146
194
  # Batch — apply many changes from ONE read (no re-reading between edits). Keys
147
195
  # on each JSONL line mirror the command's flags; all locators address the doc as
148
196
  # read. insert/edit also accept --batch - to read JSONL from stdin.
149
197
  docx edit FILE --batch fills.jsonl # { at, <one of: text|clear|markdown|runs|code|task>, style?, … }
150
198
  docx insert FILE --batch additions.jsonl # { after|before, <content>, style?, color?, … }
151
- docx replace FILE --batch script.jsonl # { pattern, replacement, regex?, all?, limit?, … } applied in order
199
+ docx replace FILE --batch script.jsonl # { pattern, replacement, at?, regex?, all?, limit?, … } applied in order ("at" scopes that entry to one paragraph)
152
200
  docx delete FILE --batch drop.jsonl # { at } per line — whole blocks (pN/tN/cell), resolved live-first
153
201
 
154
202
  # All four of insert/edit/delete/replace accept --track to record that one
@@ -157,12 +205,23 @@ docx delete FILE --batch drop.jsonl # { at } per line — whole blocks (
157
205
  # insert/edit content selectors (run "docx insert --help" / "docx edit --help" for the full list):
158
206
  # --text "..." [--style NAME] [--alignment A] [--color HEX] [--bold] [--italic] [--url URL]
159
207
  # (a newline in --text becomes a line break <w:br/>, a tab becomes <w:tab/> — verse/addresses stay line-per-line)
208
+ # paragraph spacing/indent (insert + edit, alone or with content, per-entry in --batch, across a range):
209
+ # --space-before PT --space-after PT --line-spacing N(=1|1.5|2|single|double, or 15pt / "15pt atLeast")
210
+ # --indent-left IN --indent-right IN --first-line IN --hanging IN (points / inches; first-line ⊥ hanging;
211
+ # left/right/first-line accept a negative value to outdent into the margin; hanging stays non-negative)
212
+ # Under track-changes these record a tracked <w:pPrChange> (accept/reject in Word) — even when they ride
213
+ # along with --text; read surfaces them as a deviation-only <!-- docx:p … space-after="6pt" --> hint.
160
214
  # edit --tabs right fix a line whose tabbed-over content WRAPS (read flags it as `docx:layout … warn`,
161
215
  # and prints ONE consolidated fix-all summary at the top): swaps the fragile LEFT tab for a RIGHT tab
162
216
  # flush at the margin so a long value (e.g. a city) never wraps. Rides along with --text, works
163
217
  # per-entry in --batch, and on a RANGE (edit --at pN-pM --tabs right) cures every tab line at once.
164
- # edit --text "" rejected (it would leave an invisible blank paragraph) use `delete` to remove the
165
- # line, or `--runs '[]'` to blank it but keep an empty spacer.
218
+ # edit --text "" REMOVES the line (same as `delete`; a table cell's last paragraph is blanked, not
219
+ # deleted, so the cell stays valid). In --batch, `{"at":"pN","text":""}` or `{"at":"pN","delete":true}`
220
+ # removes a line — so a form-fill is ONE sweep: fill the cells with values, drop the leftover
221
+ # placeholder lines. Use `--runs '[]'` to blank a paragraph but keep an empty spacer. (Empty
222
+ # `--text` can't ride along with --clear/run-formatting/--style/--alignment/--tabs — those exit
223
+ # with a USAGE error; use `--runs '[]'` to keep a formatted empty spacer instead.) A SPAN's
224
+ # `--text ""` (pN:S-E) still deletes just those characters.
166
225
  # --runs '[{"type":"text","text":"X","bold":true}]'
167
226
  # --text-file PATH # (insert/create) LITERAL multi-paragraph text, NOT parsed — every char verbatim,
168
227
  # each newline = a new paragraph. For prose GFM would corrupt: "3. note" stays "3.", *x* / [t](u) / bare URLs / {++x++} untouched.
@@ -170,6 +229,13 @@ docx delete FILE --batch drop.jsonl # { at } per line — whole blocks (
170
229
  # --code "..." | --code-file PATH [--language LANG]
171
230
  # --equation "x^2 + y^2" [--display] (insert; edit also accepts --inline)
172
231
  # --clear bold,italic,highlight,color,size,font,…|all (edit; strip run formatting, keep text)
232
+ # --bold --italic --underline --strike --color HEX --highlight NAME --shade HEX --font NAME --size PT
233
+ # --caps --smallcaps --superscript --subscript (edit; SET run formatting on EXISTING text —
234
+ # the inverse of --clear. Alone they format a span/paragraph/range in place; with --text they
235
+ # fill AND format. Like --clear, applied directly — not recorded as a tracked change.)
236
+ # NOTE: in a single no-content call (or one --batch entry) these run-format SET flags and the
237
+ # paragraph properties (--style/--alignment/--space-*/--line-spacing/--indent-*/--first-line/
238
+ # --hanging/--tabs) can't ride together — use separate calls/entries, or add --text to set both.
173
239
  # --task checked|unchecked | --list bullet|ordered [--list-level N] (insert)
174
240
  # --task checked|unchecked (edit, flip in place)
175
241
  # --table --rows N --cols N [--widths "A,B,C"] [--table-width V] [--borders S] [--layout L] (insert)
@@ -190,6 +256,17 @@ docx endnotes add FILE --at pN[:offset] (--text "..." | --runs JSON | --mark
190
256
  docx endnotes edit FILE --at enN (--text "..." | --runs JSON | --markdown TEXT)
191
257
  docx endnotes delete FILE --at enN
192
258
 
259
+ # Headers & footers (one shared impl — "marginals"). Default placement is every
260
+ # page, all sections (--at sN targets one). Content: ONE primary source, except
261
+ # --text + one field = two-zone (text left, field right at a content-edge tab).
262
+ docx headers set FILE [--at sN] [--type default|first|even | --first-page | --even | --odd] \
263
+ [--text "..."] [--align left|center|right] \
264
+ [--page-number [--of-pages] | --date [--date-format FMT] | --style-ref STYLE | --field filename|title|author] \
265
+ [--track] [--author NAME]
266
+ docx headers clear FILE [--at sN] [--type T | --first-page | --even | --odd]
267
+ docx footers set FILE … # identical flags, kind=footer (e.g. --page-number --of-pages → "Page X of Y")
268
+ docx footers clear FILE …
269
+
193
270
  docx images extract FILE --to DIR [--at imgN] # --to = output directory; --at picks one image
194
271
  docx images replace FILE --at imgN --with ./new.png
195
272
  docx images delete FILE --at imgN
@@ -208,14 +285,22 @@ docx tables unmerge FILE --at tN:rRcC
208
285
  docx tables borders FILE --at tN [--style single|double|none] [--size N] [--color HEX]
209
286
 
210
287
  docx track-changes on|off FILE
288
+ docx track-changes list FILE [--json]
211
289
  docx track-changes accept FILE (--at tcN [--at tcM ...] | --at revN | --all)
212
290
  docx track-changes reject FILE (--at tcN [--at tcM ...] | --at revN | --all)
213
- # A del+ins REPLACE pair shares a "group": "revN" in `list`; `--at revN` accepts/rejects
214
- # both halves in one call (no re-list between them tcN ids renumber after each accept).
291
+ docx track-changes apply FILE [--accept H ...] [--reject H ...]
292
+ # `list` defaults to a text table, one LOGICAL change per line (revN collapses a del+ins
293
+ # pair onto one line); `--json` for the raw array. A del+ins REPLACE pair shares a
294
+ # "group": "revN"; `--at revN` accepts/rejects both halves in one call.
295
+ # To FINALIZE a review (accept some, reject the rest), use `apply` — it takes both decision
296
+ # lists in ONE call, resolved against the original ids, so nothing renumbers mid-operation
297
+ # and the file is never left half-finalized. Doing it as separate accept then reject calls
298
+ # renumbers the ids between them. After a subset accept/reject/apply, the confirmation
299
+ # re-lists what remains with its renumbered handles.
215
300
  ```
216
301
 
217
302
  > **One rule to memorize: addressing an existing thing is always `--at`.**
218
- > `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`. `images extract --to DIR` is an *output directory*, not a locator.
303
+ > `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.
219
304
 
220
305
  ## Output contract
221
306
 
@@ -235,13 +320,13 @@ The CLI is built for non-interactive agents. **Exit code is the success signal**
235
320
  | Command class | Default stdout on success | `--verbose` |
236
321
  | ------------- | ------------------------- | ----------- |
237
322
  | **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 |
238
- | **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 |
323
+ | **Mutator with no new handle** — `edit`, `delete`, `replace`, `create`, `comments resolve/delete`, `images replace/delete`, `hyperlinks replace/delete`, `footnotes/endnotes edit/delete`, `headers/footers set/clear`, `tables *`, `track-changes accept/reject` & toggle | **one-line confirmation** — `<operation> <target>` (e.g. `edit t1:r0c1:p0`, `edit 7 changes`, `replace 0 occurrences replaced`) (exit `0`) | full `{ok:true,…}` ack |
239
324
  | `find` | matched span locators, one per line (no matches → nothing, exit `0`) | `--json` → `{ totalMatches, query, view, matches:[…], normalizedQuery? }` |
240
325
  | `wc` | the bare count (whole-doc adds a tab-separated `sections` column, like `wc`) | `--json` → `{ words, scope, view, sections? }` |
241
326
  | `outline` | indented `LOCATOR⇥TEXT` tree (two spaces per level) | `--json` → nested `[{ id, locator, level, style, text, children }]` |
242
- | `read` | GFM Markdown, each paragraph trailed by `<!-- pN -->` | `--ast` → the JSON AST body (`docx info schema`) |
327
+ | `read` | GFM Markdown; each paragraph carries its `pN` locator once — a trailing bare `<!-- pN -->` on plain paragraphs, or the leading token of its `<!-- docx:p pN … -->` note when one is emitted | `--ast` → the JSON AST body (`docx info schema`) |
243
328
  | `render` | image paths, one per line | `--verbose` → `{ok, operation, path, engine, output, pages}` |
244
- | `* list` (all six `list` verbs) | a **bare JSON array**; each item's `id` is its `--at` handle | — |
329
+ | `* list` (all eight `list` verbs) | a **bare JSON array**; each item's `id` is its `--at` handle | — |
245
330
 
246
331
  `--dry-run` always prints a preview object (no `ok`) and writes nothing; it wins over `-o/--output`.
247
332
 
@@ -254,6 +339,7 @@ Locators come in two flavors. **Positional block ids** (`pN`, `tN`, `sN`) are de
254
339
  | `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 |
255
340
  | `cN` (comment) | `docx comments list FILE` | `comments reply/resolve/delete --at` |
256
341
  | `fnN` / `enN` (foot/endnote) | `docx footnotes list FILE` / `docx endnotes list FILE` | `footnotes/endnotes edit/delete --at` |
342
+ | `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` |
257
343
  | `imgN` (image) | `docx images list FILE` | `images extract/replace/delete --at` |
258
344
  | `linkN` (hyperlink) | `docx hyperlinks list FILE` | `hyperlinks replace/delete --at` |
259
345
  | `tcN` (tracked change) | `docx track-changes list FILE` | `track-changes accept/reject --at` |
@@ -285,7 +371,7 @@ cN imgN linkN fnN enN tcN eqN entity ids (comment / image / hyper
285
371
  | ---- | ----------- |
286
372
  | `pN`, `tN`, `sN`, `tN:rRcC:pK` (blocks) | `read --from/--to`, `insert --after/--before`, `wc`, `comments add` |
287
373
  | `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) |
288
- | `pN`, `pN-pM`, `tN`, `sN` | `delete --at` |
374
+ | `pN`, `pN-pM`, `tN`, `sN`, `tN:rRcC:pK` | `delete --at` |
289
375
  | `pN:S-E`, `pN:S-pM:E`, `tN:rRcC:pK:S-E` (spans) | `comments add --at`, `hyperlinks add --at` (single paragraph), `find`/`wc` results |
290
376
  | `pN[:offset]` (point) | `footnotes/endnotes add --at` |
291
377
  | `cN` / `fnN` / `enN` / `imgN` / `linkN` / `tcN` (entities) | the matching noun's `--at` (the `c`/`fn`/`en`/`img`/`link`/`tc` prefix is optional) |
@@ -342,6 +428,8 @@ docx track-changes accept doc.docx --at tc0 --at tc2 # or --all
342
428
 
343
429
  **Document-wide font.** `docx styles set-default-font FILE "Times New Roman"` sets the font in the two places a font actually lives — `word/styles.xml` `<w:docDefaults>` (the formal default) *and* the theme font scheme (`word/theme/theme1.xml`, major + minor `<a:latin>`), since real Word docs resolve their fonts *through* the theme and touching only one silently loses to the other. Body text and theme-following headings both adopt it; styles or runs that pin their own font (a code block's monospace, a deliberately-Arial run) are preserved and named in the ack, with `--all` to repoint even those. `--size N` sets the default size on the same write.
344
430
 
431
+ **Edit & create styles.** `docx styles set FILE --at Heading1 --color 1F4E79 --size 16 --bold` rewrites the *style definition* in `word/styles.xml`, so every paragraph or run that uses the style picks up the change at once ("make all Heading 1s green") — the same run-/paragraph-formatting flags as `edit` (color/font/size/highlight/underline/caps + alignment/spacing/indentation for paragraph styles). `docx styles create FILE Callout --color C00000 --bold` mints a new paragraph or character style that `insert/edit --style Callout` can then apply. Editing an un-materialized built-in (`--at Heading3` on a doc that never used it) provisions it first; a paragraph with its own *direct* formatting keeps it (the override wins — the definition edit never touches the body). Style edits are **not** tracked changes even under track-changes — matching Word, which applies style-definition edits to `styles.xml` directly with no redline.
432
+
345
433
  **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.
346
434
 
347
435
  **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.