bun-docx 0.14.1 → 0.16.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 +101 -14
  2. package/dist/index.js +5699 -1776
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -89,11 +89,16 @@ docx find FILE (--highlight COLOR|any | --color HEX | --bold | --italic | --u
89
89
  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
+ 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
92
95
  docx render FILE [--out DIR] [--engine word|libreoffice|auto] [--dpi N] [--pages 1-N] [--format png|jpg]
93
96
 
94
97
  docx comments list FILE [--include-resolved] [--thread cN]
95
98
  docx footnotes list FILE
96
99
  docx endnotes list FILE
100
+ docx headers list FILE
101
+ docx footers list FILE
97
102
  docx images list FILE
98
103
  docx hyperlinks list FILE
99
104
  docx track-changes list FILE
@@ -110,15 +115,37 @@ normal edits in place, `read --ast` is the lossless view, and `docx sections` /
110
115
  (only when a value differs from the document default, so a plain document stays
111
116
  clean):
112
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`.
113
127
  - **Section breaks** render as `<!-- docx:section sN cols="2" type="continuous" -->`
114
128
  on their own line — never a bare `---` (that's a thematic break, and emitting it
115
129
  for a section silently turned layout into border paragraphs). A hand-authored
116
130
  `---` now unambiguously means a thematic break.
117
- - **Page geometry** rides a leading `<!-- docx:page orientation="landscape"
131
+ - **Page geometry** rides a leading `<!-- docx:page sN orientation="landscape"
118
132
  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*`).
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.
122
149
  - **Tables** carry a leading `<!-- docx:table t0 widths="1,2,3in" borders="double" -->`
123
150
  when columns are uneven or borders deviate from the default, plus a per-cell
124
151
  `<!-- docx:cell t0:r0c0 gridSpan="2" vMerge="continue" shading="FFE699" -->`
@@ -129,16 +156,38 @@ clean):
129
156
  `float`/`wrap`/`align`/`overflow` only when they deviate (an inline, in-bounds
130
157
  image shows just its size). `overflow` flags an image wider than the usable text
131
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.
132
176
 
133
177
  ### Mutate (change FILE in place; `--dry-run`, `-v` everywhere; `-o PATH` on every mutator except `create`, whose positional FILE is already the output)
134
178
 
135
179
  ```sh
136
- docx create FILE [--title T] [--author A] [--text "..." | --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]
137
181
  docx insert FILE (--after | --before) LOCATOR <content> # LOCATOR = pN | tN | sN | tN:rRcC:pK
182
+ docx insert FILE (--at-start | --at-end) <content> # no locator — prepend / append to the document
138
183
  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 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.
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
141
187
  docx replace FILE PATTERN REPLACEMENT [--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
+ # A span edit (find → edit --at pN:S-E --text NEW) does the same by locator.
142
191
 
143
192
  # Batch — apply many changes from ONE read (no re-reading between edits). Keys
144
193
  # on each JSONL line mirror the command's flags; all locators address the doc as
@@ -154,17 +203,37 @@ docx delete FILE --batch drop.jsonl # { at } per line — whole blocks (
154
203
  # insert/edit content selectors (run "docx insert --help" / "docx edit --help" for the full list):
155
204
  # --text "..." [--style NAME] [--alignment A] [--color HEX] [--bold] [--italic] [--url URL]
156
205
  # (a newline in --text becomes a line break <w:br/>, a tab becomes <w:tab/> — verse/addresses stay line-per-line)
206
+ # paragraph spacing/indent (insert + edit, alone or with content, per-entry in --batch, across a range):
207
+ # --space-before PT --space-after PT --line-spacing N(=1|1.5|2|single|double, or 15pt / "15pt atLeast")
208
+ # --indent-left IN --indent-right IN --first-line IN --hanging IN (points / inches; first-line ⊥ hanging;
209
+ # left/right/first-line accept a negative value to outdent into the margin; hanging stays non-negative)
210
+ # Under track-changes these record a tracked <w:pPrChange> (accept/reject in Word) — even when they ride
211
+ # along with --text; read surfaces them as a deviation-only <!-- docx:p … space-after="6pt" --> hint.
157
212
  # edit --tabs right fix a line whose tabbed-over content WRAPS (read flags it as `docx:layout … warn`,
158
213
  # and prints ONE consolidated fix-all summary at the top): swaps the fragile LEFT tab for a RIGHT tab
159
214
  # flush at the margin so a long value (e.g. a city) never wraps. Rides along with --text, works
160
215
  # per-entry in --batch, and on a RANGE (edit --at pN-pM --tabs right) cures every tab line at once.
161
- # edit --text "" rejected (it would leave an invisible blank paragraph) use `delete` to remove the
162
- # line, or `--runs '[]'` to blank it but keep an empty spacer.
216
+ # edit --text "" REMOVES the line (same as `delete`; a table cell's last paragraph is blanked, not
217
+ # deleted, so the cell stays valid). In --batch, `{"at":"pN","text":""}` or `{"at":"pN","delete":true}`
218
+ # removes a line — so a form-fill is ONE sweep: fill the cells with values, drop the leftover
219
+ # placeholder lines. Use `--runs '[]'` to blank a paragraph but keep an empty spacer. (Empty
220
+ # `--text` can't ride along with --clear/run-formatting/--style/--alignment/--tabs — those exit
221
+ # with a USAGE error; use `--runs '[]'` to keep a formatted empty spacer instead.) A SPAN's
222
+ # `--text ""` (pN:S-E) still deletes just those characters.
163
223
  # --runs '[{"type":"text","text":"X","bold":true}]'
224
+ # --text-file PATH # (insert/create) LITERAL multi-paragraph text, NOT parsed — every char verbatim,
225
+ # each newline = a new paragraph. For prose GFM would corrupt: "3. note" stays "3.", *x* / [t](u) / bare URLs / {++x++} untouched.
164
226
  # --markdown "..." | --markdown-file PATH # GFM + math + CriticMarkup + inline HTML formatting → blocks
165
227
  # --code "..." | --code-file PATH [--language LANG]
166
228
  # --equation "x^2 + y^2" [--display] (insert; edit also accepts --inline)
167
229
  # --clear bold,italic,highlight,color,size,font,…|all (edit; strip run formatting, keep text)
230
+ # --bold --italic --underline --strike --color HEX --highlight NAME --shade HEX --font NAME --size PT
231
+ # --caps --smallcaps --superscript --subscript (edit; SET run formatting on EXISTING text —
232
+ # the inverse of --clear. Alone they format a span/paragraph/range in place; with --text they
233
+ # fill AND format. Like --clear, applied directly — not recorded as a tracked change.)
234
+ # NOTE: in a single no-content call (or one --batch entry) these run-format SET flags and the
235
+ # paragraph properties (--style/--alignment/--space-*/--line-spacing/--indent-*/--first-line/
236
+ # --hanging/--tabs) can't ride together — use separate calls/entries, or add --text to set both.
168
237
  # --task checked|unchecked | --list bullet|ordered [--list-level N] (insert)
169
238
  # --task checked|unchecked (edit, flip in place)
170
239
  # --table --rows N --cols N [--widths "A,B,C"] [--table-width V] [--borders S] [--layout L] (insert)
@@ -185,6 +254,17 @@ docx endnotes add FILE --at pN[:offset] (--text "..." | --runs JSON | --mark
185
254
  docx endnotes edit FILE --at enN (--text "..." | --runs JSON | --markdown TEXT)
186
255
  docx endnotes delete FILE --at enN
187
256
 
257
+ # Headers & footers (one shared impl — "marginals"). Default placement is every
258
+ # page, all sections (--at sN targets one). Content: ONE primary source, except
259
+ # --text + one field = two-zone (text left, field right at a content-edge tab).
260
+ docx headers set FILE [--at sN] [--type default|first|even | --first-page | --even | --odd] \
261
+ [--text "..."] [--align left|center|right] \
262
+ [--page-number [--of-pages] | --date [--date-format FMT] | --style-ref STYLE | --field filename|title|author] \
263
+ [--track] [--author NAME]
264
+ docx headers clear FILE [--at sN] [--type T | --first-page | --even | --odd]
265
+ docx footers set FILE … # identical flags, kind=footer (e.g. --page-number --of-pages → "Page X of Y")
266
+ docx footers clear FILE …
267
+
188
268
  docx images extract FILE --to DIR [--at imgN] # --to = output directory; --at picks one image
189
269
  docx images replace FILE --at imgN --with ./new.png
190
270
  docx images delete FILE --at imgN
@@ -210,7 +290,7 @@ docx track-changes reject FILE (--at tcN [--at tcM ...] | --at revN | --all)
210
290
  ```
211
291
 
212
292
  > **One rule to memorize: addressing an existing thing is always `--at`.**
213
- > `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.
293
+ > `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.
214
294
 
215
295
  ## Output contract
216
296
 
@@ -230,13 +310,13 @@ The CLI is built for non-interactive agents. **Exit code is the success signal**
230
310
  | Command class | Default stdout on success | `--verbose` |
231
311
  | ------------- | ------------------------- | ----------- |
232
312
  | **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 |
233
- | **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 |
313
+ | **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 |
234
314
  | `find` | matched span locators, one per line (no matches → nothing, exit `0`) | `--json` → `{ totalMatches, query, view, matches:[…], normalizedQuery? }` |
235
315
  | `wc` | the bare count (whole-doc adds a tab-separated `sections` column, like `wc`) | `--json` → `{ words, scope, view, sections? }` |
236
316
  | `outline` | indented `LOCATOR⇥TEXT` tree (two spaces per level) | `--json` → nested `[{ id, locator, level, style, text, children }]` |
237
- | `read` | GFM Markdown, each paragraph trailed by `<!-- pN -->` | `--ast` → the JSON AST body (`docx info schema`) |
317
+ | `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`) |
238
318
  | `render` | image paths, one per line | `--verbose` → `{ok, operation, path, engine, output, pages}` |
239
- | `* list` (all six `list` verbs) | a **bare JSON array**; each item's `id` is its `--at` handle | — |
319
+ | `* list` (all eight `list` verbs) | a **bare JSON array**; each item's `id` is its `--at` handle | — |
240
320
 
241
321
  `--dry-run` always prints a preview object (no `ok`) and writes nothing; it wins over `-o/--output`.
242
322
 
@@ -249,6 +329,7 @@ Locators come in two flavors. **Positional block ids** (`pN`, `tN`, `sN`) are de
249
329
  | `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 |
250
330
  | `cN` (comment) | `docx comments list FILE` | `comments reply/resolve/delete --at` |
251
331
  | `fnN` / `enN` (foot/endnote) | `docx footnotes list FILE` / `docx endnotes list FILE` | `footnotes/endnotes edit/delete --at` |
332
+ | `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` |
252
333
  | `imgN` (image) | `docx images list FILE` | `images extract/replace/delete --at` |
253
334
  | `linkN` (hyperlink) | `docx hyperlinks list FILE` | `hyperlinks replace/delete --at` |
254
335
  | `tcN` (tracked change) | `docx track-changes list FILE` | `track-changes accept/reject --at` |
@@ -280,7 +361,7 @@ cN imgN linkN fnN enN tcN eqN entity ids (comment / image / hyper
280
361
  | ---- | ----------- |
281
362
  | `pN`, `tN`, `sN`, `tN:rRcC:pK` (blocks) | `read --from/--to`, `insert --after/--before`, `wc`, `comments add` |
282
363
  | `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) |
283
- | `pN`, `pN-pM`, `tN`, `sN` | `delete --at` |
364
+ | `pN`, `pN-pM`, `tN`, `sN`, `tN:rRcC:pK` | `delete --at` |
284
365
  | `pN:S-E`, `pN:S-pM:E`, `tN:rRcC:pK:S-E` (spans) | `comments add --at`, `hyperlinks add --at` (single paragraph), `find`/`wc` results |
285
366
  | `pN[:offset]` (point) | `footnotes/endnotes add --at` |
286
367
  | `cN` / `fnN` / `enN` / `imgN` / `linkN` / `tcN` (entities) | the matching noun's `--at` (the `c`/`fn`/`en`/`img`/`link`/`tc` prefix is optional) |
@@ -333,6 +414,12 @@ docx track-changes accept doc.docx --at tc0 --at tc2 # or --all
333
414
 
334
415
  **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.
335
416
 
417
+ **Literal text — the parser-free channel.** When you want prose inserted *exactly* — reviewer notes, quoted excerpts, anything where Markdown would misfire — use `create --text-file PATH` / `insert --text-file PATH` (or `-` for stdin). Every character lands verbatim and each newline starts a new paragraph; nothing is interpreted, so `3. note` stays `3.` (no ordered-list renumber), and `*x*`, `[t](u)`, bare URLs, and `{++x++}` are kept as written. This exists because GFM corruption isn't always escapable: bare URLs autolink with no escape sequence at all, and CriticMarkup eats `{++…++}` regardless of backslashes — so a literal path is the only safe way to author untouched prose.
418
+
419
+ **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.
420
+
421
+ **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.
422
+
336
423
  **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.
337
424
 
338
425
  **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.