doculi 3.1.7 → 5.7.10

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joseph Mearman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # document-cli
2
2
 
3
- [![GitHub](https://img.shields.io/badge/GitHub-181717?logo=github&logoColor=white)](https://github.com/ExaDev/document-cli) [![npm](https://img.shields.io/badge/npm-CB3837?logo=npm&logoColor=white)](https://www.npmjs.com/package/document-cli) [![Release](https://img.shields.io/github/v/release/ExaDev/document-cli)](https://github.com/ExaDev/document-cli/releases/latest) [![CI](https://img.shields.io/github/actions/workflow/status/ExaDev/document-cli/ci.yml?branch=main)](https://github.com/ExaDev/document-cli/actions)
3
+ [![GitHub](https://img.shields.io/badge/GitHub-181717?logo=github&logoColor=white)](https://github.com/ExaDev/documents.js/tree/main/packages/document-cli) [![npm](https://img.shields.io/badge/npm-CB3837?logo=npm&logoColor=white)](https://www.npmjs.com/package/document-cli) [![npm version](https://img.shields.io/npm/v/document-cli)](https://www.npmjs.com/package/document-cli) [![CI](https://img.shields.io/github/actions/workflow/status/ExaDev/documents.js/ci.yml?branch=main)](https://github.com/ExaDev/documents.js/actions)
4
4
 
5
- > A command-line interface and an interactive terminal (Ink) app for [`documents.js`](https://github.com/ExaDev/documents.js): every docx/pptx/odt/odp/ods/odg/odf/pdf/odm/odb/xlsx/csv/svg/markdown conversion, bridge, and editor documents.js exposes, plus the outline projection [`document-outline.js`](https://github.com/ExaDev/document-outline.js) builds over any readable document, wired up as a scriptable subcommand or a full-screen terminal editor. Installs as either `document-cli` or `doculi`.
5
+ > A command-line interface and an interactive terminal (Ink) app for [`documents.js`](https://github.com/ExaDev/documents.js): every docx/pptx/odt/odp/ods/odg/odf/pdf/odm/odb/xlsx/csv/svg/markdown/rtf/wpd/doc/xls/ppt/epub conversion, bridge, and editor documents.js exposes, plus the outline projection [`document-outline.js`](../document-outline.js/README.md) builds over any readable document, wired up as a scriptable subcommand or a full-screen terminal editor. Installs as either `document-cli` or `doculi`.
6
6
 
7
7
  `document-cli` adds no conversion or editing logic of its own — it is a dispatch layer over `documents.js`'s existing conversion functions, `DocumentConverter` port, live-view editors, and `.odb`/PDF readers. What it adds is two ways to drive them without writing TypeScript: a scriptable, Unix-shaped CLI (stdin/stdout, exit codes, `--json` diagnostics) for pipelines, and a full-screen Ink terminal app for browsing and editing a document interactively.
8
8
 
@@ -38,16 +38,16 @@ graph TD
38
38
  odf --> cli
39
39
  pdfcodec --> cli
40
40
 
41
- click schema "https://github.com/ExaDev/document-schema.js" "document-schema.js"
42
- click ooxml "https://github.com/ExaDev/ooxml.js" "ooxml.js"
43
- click odf "https://github.com/ExaDev/odf.js" "odf.js"
44
- click pdfcodec "https://github.com/ExaDev/pdf-codec" "pdf-codec"
45
- click mdcodec "https://github.com/ExaDev/markdown-codec" "markdown-codec"
46
- click bytecodec "https://github.com/ExaDev/byte-codec" "byte-codec"
41
+ click schema "https://github.com/ExaDev/documents.js/tree/main/packages/document-schema.js" "document-schema.js"
42
+ click ooxml "https://github.com/ExaDev/documents.js/tree/main/packages/ooxml.js" "ooxml.js"
43
+ click odf "https://github.com/ExaDev/documents.js/tree/main/packages/odf.js" "odf.js"
44
+ click pdfcodec "https://github.com/ExaDev/documents.js/tree/main/packages/pdf-codec" "pdf-codec"
45
+ click mdcodec "https://github.com/ExaDev/documents.js/tree/main/packages/markdown-codec" "markdown-codec"
46
+ click bytecodec "https://github.com/ExaDev/documents.js/tree/main/packages/byte-codec" "byte-codec"
47
47
  click documents "https://github.com/ExaDev/documents.js" "documents.js"
48
- click outline "https://github.com/ExaDev/document-outline.js" "document-outline.js"
49
- click mcp "https://github.com/ExaDev/document-mcp" "document-mcp"
50
- click cli "https://github.com/ExaDev/document-cli" "document-cli"
48
+ click outline "https://github.com/ExaDev/documents.js/tree/main/packages/document-outline.js" "document-outline.js"
49
+ click mcp "https://github.com/ExaDev/documents.js/tree/main/packages/document-mcp" "document-mcp"
50
+ click cli "https://github.com/ExaDev/documents.js/tree/main/packages/document-cli" "document-cli"
51
51
 
52
52
  style cli fill:#f9a825,stroke:#333,stroke-width:3px
53
53
  ```
@@ -56,9 +56,9 @@ graph TD
56
56
 
57
57
  `documents.js` is a library, not a tool — everything it does happens through function calls from TypeScript/JavaScript. Most people who want to convert a docx to a PDF, extract an `.odb` table to CSV, or poke at a PDF's structure from a terminal don't want to write a script to do it. `document-cli` is that missing entry point: every one of documents.js's direct conversion pairs, its generic converter, its `.odm`/`.odb` extraction functions (including a bounded SQL engine over an `.odb`'s own tables and full report rendering), its PDF inspector, and its document metadata/source-font introspection become a single command-line invocation, and its seven live-view editors (docx/pptx/odt/odp/ods/odg/markdown) become a keyboard-driven terminal app that never needs a code editor open at all.
58
58
 
59
- [`document-mcp`](https://github.com/ExaDev/document-mcp) is the sibling frontend over the identical `documents.js` library — an MCP server rather than a terminal CLI/TUI — so the two are independent consumers of one shared implementation, each exposing whatever subset of it suits a human at a terminal versus an MCP-speaking agent.
59
+ [`document-mcp`](../document-mcp/README.md) is the sibling frontend over the identical `documents.js` library — an MCP server rather than a terminal CLI/TUI — so the two are independent consumers of one shared implementation, each exposing whatever subset of it suits a human at a terminal versus an MCP-speaking agent.
60
60
 
61
- The CLI and the TUI are deliberately not two separate implementations of the same logic. The TUI's own document-opening, saving, and PDF-export code (`src/tui/format/`) calls the identical `documents.js` functions the CLI commands call — `openDocx`/`createDocx`/`docxToPdf` and their five siblings per format, plus `readOdbTables`/`readPdf` for the two read-only sources — so there is exactly one place either surface can drift from what documents.js itself does: nowhere. This CLI adds no conversion logic of its own, so a conversion's fidelity — which pairs round-trip losslessly, which are a best-effort reconstruction, and why — is exactly what [`documents.js`'s own Fidelity section](https://github.com/ExaDev/documents.js#fidelity) documents, table included; it is not restated here.
61
+ The CLI and the TUI are deliberately not two separate implementations of the same logic. The TUI's own document-opening, saving, and PDF-export code (`src/tui/format/`) calls the identical `documents.js` functions the CLI commands call — `openDocx`/`createDocx`/`docxToPdf` and their five siblings per format, plus `readOdbTables`/`readPdf` for the two read-only sources — so there is exactly one place either surface can drift from what documents.js itself does: nowhere. This CLI adds no conversion logic of its own, so a conversion's fidelity — which pairs round-trip losslessly, which are a best-effort reconstruction, and why — is exactly what [`documents.js`'s own Fidelity section](../documents.js/README.md#fidelity) documents, table included; it is not restated here.
62
62
 
63
63
  ## Getting started
64
64
 
@@ -70,7 +70,7 @@ npm i -g document-cli
70
70
  npm i -g doculi
71
71
  ```
72
72
 
73
- Both names install the exact same package and the exact same binary — `package.json`'s `bin` field declares both `document-cli` and `doculi` pointing at the one built entry point unconditionally, so there is no "real" name and an alias; pick whichever you find easier to type. This mirrors the alias-publishing pattern already established elsewhere in this package family (`documents.js`'s own scoped `@exadev/documents.js` republish to GitHub Packages) a second name for the same build, not a second build.
73
+ Both names install the exact same package and the exact same binary — `package.json`'s `bin` field declares both `document-cli` and `doculi` pointing at the one built entry point unconditionally, so there is no "real" name and an alias; pick whichever you find easier to type. Unlike a sibling's second _npm package name_ (`documents.js`'s own `js.documents` — see that package's README, and note the older per-repo pipeline's GitHub Packages republish this pattern used to mirror is no longer running, per [ExaDev/documents.js#732](https://github.com/ExaDev/documents.js/issues/732)), this is one package with two `bin` entries: a second name for the same build, not a second build, and unaffected by that gap.
74
74
 
75
75
  ## Usage
76
76
 
@@ -99,7 +99,7 @@ document-cli convert report.docx - --to pdf > report.pdf
99
99
 
100
100
  **`formats`** — lists every `source -> target` pair the commands above support (`--json` for a machine-readable array), plus a pointer to the commands not on that list because they don't fit the source/target shape (`odm-to-pdf`, `odb-to-csv`, `odb-to-xlsx`, `odb-tables`, `odb-forms`, `odb-reports`, `pdf-inspect`, `from-package`, `fonts`, `docx-extras`, `metadata`, `set-metadata`, `outline`).
101
101
 
102
- **`from-package <input> [output]`** — reads back a `DocumentPackage` JSON file a previous conversion wrote via `--dump-package` (below) and exports it to a real target format, closing the round trip `--dump-package` otherwise has no return path for. Target resolution matches `convert`: an output path's own extension, or `--to <format>` when it doesn't have one. The package this command reads is the tree form since documents.js 3.0.0 — content grouped one group per container (section, slide, sheet, draw page) under `children`, with the minted `styles` table at the root — flattened once at this boundary, exactly as documents.js's own builders do. `pdf` rebuilds the layout from the package's own recorded positions — each content node's `frames` plus the package's `pages` geometry (`layoutDocumentFromPackage` → `writePdf`; a package no longer carries a separate `layout` half at all); every other format builds a fresh package from the flattened content through the identical `buildXPackage` function the matching `pdf-to-X`/bridge conversion already uses — `xlsx` included, via `ooxml.js`'s own `buildXlsxPackage`. `odf` is the one target rejected outright, since a standalone formula document has no write path from `ContentDocument` at all; a `csv` or `svg` target is written through the identical `buildCsvText`/`buildSvgText` functions the codec registry's own write wrappers call, so the `--delimiter`/`--sheet`/`--page` selection flags below reach it exactly as they do a live conversion. Only a file genuinely written by a current `--dump-package` round-trips here: the `$schema` URI a dump carries pins the document-schema.js release that wrote it, and any pre-4.0.0 dump — the flat `{ formatVersion, content, pages }` envelope documents.js 1.x/2.x wrote, whatever its `formatVersion` — is rejected with an error naming the pinned release, the flat-to-tree change, and the remedy, rather than a raw validation wall. A layout-document dump (a document-schema.js 3.x layout artefact, e.g. an old `pdf-inspect --full` output) gets its own pointer: that schema moved to pdf-codec.
102
+ **`from-package <input> [output]`** — reads back a `DocumentTree` JSON file a previous conversion wrote via `--dump-package` (below) and exports it to a real target format, closing the round trip `--dump-package` otherwise has no return path for. Target resolution matches `convert`: an output path's own extension, or `--to <format>` when it doesn't have one. The package this command reads is the tree form since documents.js 3.0.0 — content grouped one group per container (section, slide, sheet, draw page) under `children`, with the minted `styles` table at the root — flattened once at this boundary, exactly as documents.js's own builders do. `pdf` rebuilds the layout from the package's own recorded positions — each content node's `frames` plus the package's `pages` geometry (`layoutDocumentFromPackage` → `writePdf`; a package no longer carries a separate `layout` half at all); every other format builds a fresh package from the flattened content through the identical `buildXPackage` function the matching `pdf-to-X`/bridge conversion already uses — `xlsx` included, via `ooxml.js`'s own `buildXlsxPackage`. `odf` is the one target rejected outright, since a standalone formula document has no write path from `ContentDocument` at all; a `csv` or `svg` target is written through the identical `buildCsvText`/`buildSvgText` functions the codec registry's own write wrappers call, so the `--delimiter`/`--sheet`/`--page` selection flags below reach it exactly as they do a live conversion. Only a file genuinely written by a current `--dump-package` round-trips here: the `$schema` URI a dump carries pins the document-schema.js release that wrote it, and any pre-4.0.0 dump — the flat `{ formatVersion, content, pages }` envelope documents.js 1.x/2.x wrote, whatever its `formatVersion` — is rejected with an error naming the pinned release, the flat-to-tree change, and the remedy, rather than a raw validation wall. A layout-document dump (a document-schema.js 3.x layout artefact, e.g. an old `pdf-inspect --full` output) gets its own pointer: that schema moved to pdf-codec.
103
103
 
104
104
  ```sh
105
105
  document-cli docx-to-pdf report.docx report.pdf --dump-package report.package.json
@@ -116,7 +116,7 @@ document-cli odb-tables customers.odb
116
116
  document-cli odb-to-csv customers.odb --table CUSTOMERS customers.csv
117
117
  ```
118
118
 
119
- **`odb-forms <input>`**, **`odb-reports <input>`** — read a `.odb`'s form and report *structure* rather than its table data. A form or report is a static ODF sub-document inside the package, so neither command consults the embedded database at all: they work on an `.odb` whose connection points at an external server just as well as on an embedded one. `odb-forms` prints each form's own data source (table or saved query) and its field-bound controls, sub-forms nested under their parent with their own separate command; `odb-reports` prints each report's data-source command, its band structure (report/page headers and footers, groups, detail), every `rpt:` formula expression (`field:[AMOUNT]`, `rpt:SUM([AMOUNT])`), and any user-defined report functions. `--json` emits the same structure machine-readably — for a form that is the structure only, with the form's own parsed sub-document dropped:
119
+ **`odb-forms <input>`**, **`odb-reports <input>`** — read a `.odb`'s form and report _structure_ rather than its table data. A form or report is a static ODF sub-document inside the package, so neither command consults the embedded database at all: they work on an `.odb` whose connection points at an external server just as well as on an embedded one. `odb-forms` prints each form's own data source (table or saved query) and its field-bound controls, sub-forms nested under their parent with their own separate command; `odb-reports` prints each report's data-source command, its band structure (report/page headers and footers, groups, detail), every `rpt:` formula expression (`field:[AMOUNT]`, `rpt:SUM([AMOUNT])`), and any user-defined report functions. `--json` emits the same structure machine-readably — for a form that is the structure only, with the form's own parsed sub-document dropped:
120
120
 
121
121
  ```sh
122
122
  document-cli odb-forms sales.odb
@@ -143,7 +143,7 @@ document-cli pdf-inspect report.pdf
143
143
  document-cli pdf-inspect report.pdf --json
144
144
  ```
145
145
 
146
- **`fonts <input>`** — lists every source-embedded font face a docx/pptx/odt/odp/ods/odg document carries (family, weight/style, byte length) — the same embedded faces every `<format>-to-pdf` conversion already extracts and renders through automatically (see [Real fonts](#real-fonts) below); this command just reports what's there without converting anything. Rejects a format with no source-embedded-font concept at all (xlsx, csv, svg, pdf, markdown, odf), naming it:
146
+ **`fonts <input>`** — lists every source-embedded font face a docx/pptx/odt/odp/ods/odg document carries (family, weight/style, byte length) — the same embedded faces every `<format>-to-pdf` conversion already extracts and renders through automatically (see [Real fonts](#real-fonts) below); this command just reports what's there without converting anything. Rejects a format with no source-embedded-font concept at all (xlsx, csv, svg, pdf, markdown, odf, rtf, wpd, doc, xls, ppt) or with no font-embedding support in this ecosystem's own reader yet (epub — EPUB 3 can embed fonts via CSS `@font-face`, but epub-codec's own reader doesn't extract them), naming it:
147
147
 
148
148
  ```sh
149
149
  document-cli fonts report.docx
@@ -155,20 +155,20 @@ document-cli fonts report.docx
155
155
  document-cli docx-extras report.docx
156
156
  ```
157
157
 
158
- **`metadata <input>`** — prints a document's own title/author/subject/keywords/creator/producer/created/modified metadata, for any of the twelve supported formats (docx, pptx, xlsx, odt, odp, ods, odg, svg, odf, csv, markdown, pdf); csv and svg carry no metadata container of their own, so they always report none. `--json` emits the raw metadata object:
158
+ **`metadata <input>`** — prints a document's own title/author/subject/keywords/creator/producer/created/modified metadata, for any of the eighteen supported formats (docx, pptx, xlsx, odt, odp, ods, odg, svg, odf, csv, markdown, rtf, wpd, doc, xls, ppt, epub, pdf); csv, svg, doc, xls, and ppt carry no metadata container of their own (doc-codec/xls-codec/ppt-codec don't read one yet — see each package's own README), so they always report none, and epub reports title/author/keywords but never subject (epub-codec's OPF reader has no Dublin Core mapping for it). `--json` emits the raw metadata object:
159
159
 
160
160
  ```sh
161
161
  document-cli metadata report.pdf
162
162
  ```
163
163
 
164
- **`set-metadata <input> [output]`** — patches a document's own title/author/subject/keywords, leaving every other field untouched (`--set-title`, `--set-author`, `--set-subject`, `--set-keywords` — the last a comma-separated list); source and target format must match, so run `convert`/`from-package` first if a different target format is also needed. A `pdf` source/target patches the parsed PDF directly (`writePdf`) with no layout engine involved genuinely lossless for everything else on the page; every other format rebuilds a fresh package from that format's own `ContentDocument`, which for docx specifically is lossy (it drops everything `docx-extras` covers, since `buildDocxPackage` has no way to carry that data through a `ContentDocument`-only rebuild); a `csv` or `svg` source/target is rejected outright, since plain text has no metadata container and a rebuild would silently drop the override:
164
+ **`set-metadata <input> [output]`** — patches a document's own title/author/subject/keywords, leaving every other field untouched (`--set-title`, `--set-author`, `--set-subject`, `--set-keywords` — the last a comma-separated list); source and target format must match, so run `convert`/`from-package` first if a different target format is also needed. A `pdf` source/target patches the parsed PDF directly (`writePdf`) with no layout engine involved, and a `docx` source/target patches `docProps/core.xml` directly on the decoded package (`patchDocxMetadata`) with no `ContentDocument` rebuild involved both genuinely lossless for everything else in the document, the docx path included: comments, footnotes, headers/footers, and numbering definitions (everything `docx-extras` covers) survive untouched, since nothing but the metadata part itself is ever written. Every other format (pptx, xlsx, odt, odp, ods, odg, markdown, rtf) rebuilds a fresh package from that format's own `ContentDocument`; a `csv` or `svg` source/target is rejected outright, since plain text has no metadata container and a rebuild would silently drop the override:
165
165
 
166
166
  ```sh
167
167
  document-cli set-metadata report.docx report.docx --set-title "Q3 Report" --set-author "Finance"
168
168
  document-cli set-metadata report.docx report.odt --set-keywords "draft,internal"
169
169
  ```
170
170
 
171
- **`outline <input>`** — prints a document's outline: the table-of-contents projection over the tree-form `DocumentPackage` a conversion of that document builds — headings nested by heading level, list items nested under their heading or slide, one group per slide (labelled `Slide N`), sheet (labelled with the sheet's own name), or draw page (labelled `Page N`) — rendered as indented text, two spaces per nesting depth. Leaves render their own text (a paragraph's runs, a table's cell text, an image's alt text, a formula's LaTeX) or their kind in brackets when they carry none (`[page-break]`, `[vector]`, `[embeddedObject]`). Works on any of the twelve readable formats: the command runs the cheapest conversion that still leaves a package behind a PDF-bypassing bridge to a sibling format, so no layout engine runs (`docx`→`odt`, `markdown`→`docx`, `ods`→`xlsx`, ...); `pdf` reconstructs through `pdf-to-docx` and `odf` renders through `odf-to-pdf`, the one conversion each of those formats has — and projects that package's tree through [`document-outline.js`](https://github.com/ExaDev/document-outline.js)'s own `buildOutline`, this command being that package's first real consumer. `--json` emits the outline tree itself — groups as `{ text, level, children }`, leaves as the package leaves they are — rather than a CLI-private shape; the internal conversion's diagnostics (a pdf reconstruction's parse warnings, say) reach stderr exactly as they would on the matching conversion command. `--from <format>` names the source format when the input path carries no recognised extension to infer it from — the only way to outline a document read from stdin (`-`), which otherwise has no extension to read at all. Heading nesting depends on the source document actually carrying a heading-level signal on disk (`w:outlineLvl` for docx, `text:outline-level` for odt) — present in anything authored by Word or LibreOffice, and always present for a markdown source, whose own reader parses `#`/`##` headings directly; a docx or odt this ecosystem's own writers produce does not currently stamp that signal even though the paragraph still carries its `Heading1`/`Heading2`-style reference, so outlining a docx/odt generated by this CLI's own conversions loses heading nesting specifically (list nesting and slide/sheet/page grouping are unaffected):
171
+ **`outline <input>`** — prints a document's outline: the table-of-contents projection over the tree-form `DocumentTree` read straight off the source document's own bytes — headings nested by heading level, list items nested under their heading or slide, one group per slide (labelled `Slide N`), sheet (labelled with the sheet's own name), or draw page (labelled `Page N`) — rendered as indented text, two spaces per nesting depth. Leaves render their own text (a paragraph's runs, a table's cell text, an image's alt text, a formula's LaTeX) or their kind in brackets when they carry none (`[page-break]`, `[vector]`, `[embeddedObject]`). Works on any of the seventeen readable formats: the command reads the source's own native tree directly (documents.js's `readNativeDocumentTree`) — no bridging conversion runs and no output bytes are discarded — and projects that tree through [`document-outline.js`](../document-outline.js/README.md)'s own `buildOutline`, this command being that package's first real consumer. `--json` emits the outline tree itself — groups as `{ text, level, children }`, leaves as the package leaves they are — rather than a CLI-private shape; a `pdf` source's own `readPdf` parse diagnostics still reach stderr exactly as they would on the matching `pdf-to-docx` command. `--from <format>` names the source format when the input path carries no recognised extension to infer it from — the only way to outline a document read from stdin (`-`), which otherwise has no extension to read at all. Heading nesting depends on the source document carrying a heading-level signal on disk (`w:outlineLvl` for docx, `text:outline-level` for odt) — present in anything authored by Word or LibreOffice, always present for a markdown source (whose own reader parses `#`/`##` headings directly), and also stamped by this ecosystem's own docx/odt writers (`buildDocxPackage`'s `w:outlineLvl`, `buildOdtPackage`'s promotion to a real `text:h`), so outlining a docx/odt this CLI's own conversions produce nests headings correctly too:
172
172
 
173
173
  ```sh
174
174
  document-cli outline report.docx
@@ -181,17 +181,17 @@ document-cli outline slides.pptx --json
181
181
 
182
182
  The explicit conversions, `convert`, `odm-to-pdf`, `odb-to-xlsx`, `odb-to-csv`, `set-metadata`, and `odb-render-report` — every command that reads one file and writes one — share:
183
183
 
184
- | Flag | Meaning |
185
- |---|---|
186
- | `-o, --out <file>` | Output path; defaults to the input path with the target format's own extension. Conflicts with a positional output argument that names a different path. |
187
- | `--timeout <ms>` | Abort the run after this many milliseconds. |
188
- | `--json` | Emit diagnostics and the result summary as newline-delimited JSON on stderr, instead of human-readable lines. |
189
- | `-q, --quiet` | Suppress diagnostic and summary output (the JSON result-summary line still prints in `--json` mode, so a script consuming NDJSON always gets a terminating record). |
190
- | `--verbose` | Include a full stack trace in the error line when the run fails. |
184
+ | Flag | Meaning |
185
+ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
186
+ | `-o, --out <file>` | Output path; defaults to the input path with the target format's own extension. Conflicts with a positional output argument that names a different path. |
187
+ | `--timeout <ms>` | Abort the run after this many milliseconds. |
188
+ | `--json` | Emit diagnostics and the result summary as newline-delimited JSON on stderr, instead of human-readable lines. |
189
+ | `-q, --quiet` | Suppress diagnostic and summary output (the JSON result-summary line still prints in `--json` mode, so a script consuming NDJSON always gets a terminating record). |
190
+ | `--verbose` | Include a full stack trace in the error line when the run fails. |
191
191
 
192
192
  Three further flags select what a csv or svg edge of a conversion works on, threaded straight into documents.js's own `ConversionOptions`: `--delimiter <char>` (the field delimiter a csv source reads with, or a csv target writes with — default `,`), `--sheet <name>` (the sheet a csv target writes, required when the source document has more than one), and `--page <index>` (the 0-based page an svg target draws, required when the source document has more than one). On the explicit commands they are registered only where the pair can reach the edge in question — `--delimiter` on any pair with a csv edge, `--sheet` on a csv target, `--page` on an svg target — and unconditionally on `convert` and `from-package`, whose target is only known once the output path or `--to` resolves at run time (the same registration reasoning the font flags below document). Leaving `--sheet` or `--page` unanswered on an ambiguous document fails with exit `3`, naming the sheets or page count to pick from:
193
193
 
194
- `--dump-package <file>` is one flag further, registered only on the explicit conversions and `convert` — it writes the intermediate `DocumentPackage` (the tree form: content grouped one group per container, the content nodes carrying their own rendered `frames`, plus the `pages` geometry those frames index into and the minted `styles` table at the root) that conversion built to a JSON file, tagged with its own version-pinned `$schema` — the URI *is* the package's version — so `from-package` (above) can read it back in. Every conversion populates one, `odf-to-pdf` included its dump carries a `formula`-kind content and the one A4 page it renders (the formula's glyphs travel through the PDF writer's own positioned-formula channel rather than as frame-stamped page content); every PDF-bypassing bridge (`ods-to-xlsx`, `docx-to-pptx`, `odg-to-svg`, and every other content-format pair above) populates one too, just with `pages` always absent, since a bridge never runs a layout engine. `odm-to-pdf`/`odb-*`/`set-metadata` don't expose the flag at all, since none of them goes through `DocumentConverter.convert` in the first place. `odb-tables`, `odb-forms`, `odb-reports`, `fonts`, `docx-extras`, `metadata`, `formats`, and `pdf-inspect` each take only their own `--json` (plus `pdf-inspect`'s own `--full`); `odb-query` takes `--sql <text>`/`--query <savedName>` (mutually exclusive) alongside its own `--json`, with none of the shared flags above since it only reads and writes nothing; `from-package` and `set-metadata` each take `--to <format>` alongside the shared flags in this table; `outline` takes the shared flags except `--out` (it prints to stdout and writes no file), plus its own `--from <format>` for a source format extension inference can't resolve — it runs a conversion internally to obtain the package it projects, so its diagnostics and `--timeout` behave like any conversion's; `odb-render-report` takes `--report <name>` and `--to <format>` alongside the shared flags and the font flags below; `tui` takes no flags at all, only an optional positional file.
194
+ `--dump-package <file>` is one flag further, registered only on the explicit conversions and `convert` — it writes the SOURCE document's own native `DocumentTree` (documents.js's `readNativeDocumentTree`, read straight off the input bytes a second time, independent of `--to`/the output path) to a JSON file, tagged with its own version-pinned `$schema` — the URI _is_ the package's version — so `from-package` (above) can read it back in. This is deliberately not the same tree `--to`'s own conversion produces internally: a target sharing no `ContentDocument` variant with the source composes through a lossy cross-variant bridge or a pdf pivot to get there (`xlsx-to-markdown`, say), and reporting that intermediate hop's shape would mean an xlsx source's own dump carries a wordprocessing tree with no sheet/cell/formula/A1 data at all rather than the workbook it actually is ([ExaDev/documents.js#823](https://github.com/ExaDev/documents.js/issues/823)) `--dump-package` is about what the source carries, not about how `--to` got there. Every source format populates one: a `pdf` source's dump carries `pages` and per-node `frames` (a PDF has no representation other than positioned layout — the identical reconstruction `pdf-to-docx` runs), and every other source's dump is content-only, with no `pages` and no `frames` at all, `odf` included (its dump carries a `formula`-kind content with no invented page geometry — a standalone formula document has no page concept of its own until something renders it). `odm-to-pdf`/`odb-*`/`set-metadata` don't expose the flag at all, since none of them goes through `DocumentConverter.convert` in the first place. `odb-tables`, `odb-forms`, `odb-reports`, `fonts`, `docx-extras`, `metadata`, `formats`, and `pdf-inspect` each take only their own `--json` (plus `pdf-inspect`'s own `--full`); `odb-query` takes `--sql <text>`/`--query <savedName>` (mutually exclusive) alongside its own `--json`, with none of the shared flags above since it only reads and writes nothing; `from-package` and `set-metadata` each take `--to <format>` alongside the shared flags in this table; `outline` takes the shared flags except `--out` (it prints to stdout and writes no file), plus its own `--from <format>` for a source format extension inference can't resolve — it reads the source's own native tree directly (the same `readNativeDocumentTree` primitive `--dump-package` uses), so its diagnostics and `--timeout` behave like any conversion's; `odb-render-report` takes `--report <name>` and `--to <format>` alongside the shared flags and the font flags below; `tui` takes no flags at all, only an optional positional file.
195
195
 
196
196
  ### Real fonts
197
197
 
@@ -204,7 +204,7 @@ document-cli docx-to-pdf report.docx report.pdf \
204
204
  --report-font-substitutions
205
205
  ```
206
206
 
207
- The flag is repeatable, takes a `.ttf`/`.otf` path, and needs **no accompanying family flag**: the family, weight, and slope are read from the font file's own `name` and `OS/2` tables. That is a deliberate choice over the alternative of a parallel `--font-family`/`--font-bold`/`--font-italic` set — three repeatable flags whose values must stay index-aligned with a fourth is a silent-misalignment hazard (pass two font files and one `--font-family` and the second face is mis-declared, with nothing reporting it), and every real font already states all three facts itself. The consequence to know about: a font file can only be supplied *as the family it says it is*. There is no way to say "draw Calibri using this file instead" — for that, the family in the document has to match the family in the font. A file that is not a readable font (a `.woff`, a `.ttc` collection, a mistyped path pointing at something else) fails the run outright, naming the file, rather than being quietly skipped.
207
+ The flag is repeatable, takes a `.ttf`/`.otf` path, and needs **no accompanying family flag**: the family, weight, and slope are read from the font file's own `name` and `OS/2` tables. That is a deliberate choice over the alternative of a parallel `--font-family`/`--font-bold`/`--font-italic` set — three repeatable flags whose values must stay index-aligned with a fourth is a silent-misalignment hazard (pass two font files and one `--font-family` and the second face is mis-declared, with nothing reporting it), and every real font already states all three facts itself. The consequence to know about: a font file can only be supplied _as the family it says it is_. There is no way to say "draw Calibri using this file instead" — for that, the family in the document has to match the family in the font. A file that is not a readable font (a `.woff`, a `.ttc` collection, a mistyped path pointing at something else) fails the run outright, naming the file, rather than being quietly skipped.
208
208
 
209
209
  `--report-font-substitutions` prints each face that resolved to something other than what the document asked for, as it happens, with its structured fields intact (`--json` makes it one more NDJSON record: `{"type":"font-substitution","requestedFamily":"Calibri",…}`). Without it, the same fallbacks are still reported — the `font/substituted` diagnostic lines every conversion already emits — just as rendered messages after the fact rather than structured events as they occur.
210
210
 
@@ -214,24 +214,24 @@ Diagnostics and the summary line always go to stderr; stdout is reserved for the
214
214
 
215
215
  ### Exit codes
216
216
 
217
- | Code | Meaning |
218
- |---|---|
219
- | `0` | Success. |
220
- | `1` | The input was unusable — a malformed or encrypted PDF, or any other conversion failure not covered by the codes below. |
221
- | `2` | A usage error — bad flags, conflicting output destinations, an unrecognised format, or (for a bare/`--help`/`--version` invocation) commander's own exit path. |
222
- | `3` | documents.js needs more information to finish, not a different file — an unresolved `.odm` chapter, a `.odb` table that wasn't specified (or wasn't found, or has no embedded engine at all, or uses an unsupported HSQLDB script serialisation), a `.odb` report that wasn't specified when the database declares more than one, a csv target whose source carries more than one sheet (or a `--sheet` that names one it doesn't), or an svg target whose source carries more than one page (or a `--page` that indexes past the last). |
223
- | `124` | The run's own `--timeout` elapsed before it finished. |
224
- | `130` | Interrupted by `SIGINT` (Ctrl+C). |
217
+ | Code | Meaning |
218
+ | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
219
+ | `0` | Success. |
220
+ | `1` | The input was unusable — a malformed or encrypted PDF, or any other conversion failure not covered by the codes below. |
221
+ | `2` | A usage error — bad flags, conflicting output destinations, an unrecognised format, or (for a bare/`--help`/`--version` invocation) commander's own exit path. |
222
+ | `3` | documents.js needs more information to finish, not a different file — an unresolved `.odm` chapter, a `.odb` table that wasn't specified (or wasn't found, or has no embedded engine at all, or uses an unsupported HSQLDB script serialisation), a `.odb` report that wasn't specified when the database declares more than one, a csv target whose source carries more than one sheet (or a `--sheet` that names one it doesn't), or an svg target whose source carries more than one page (or a `--page` that indexes past the last). |
223
+ | `124` | The run's own `--timeout` elapsed before it finished. |
224
+ | `130` | Interrupted by `SIGINT` (Ctrl+C). |
225
225
 
226
226
  ## The TUI
227
227
 
228
228
  Launch it either bare (`document-cli`, with no arguments) or explicitly with `document-cli tui [file]` — both open the same app; the explicit form additionally opens `file` immediately, skipping the launcher screen. The TUI needs an interactive terminal: a bare invocation with redirected stdout prints help text instead, and an explicit `tui` invocation with redirected stdout fails outright, since there's no terminal for Ink to draw into.
229
229
 
230
- It supports the same seven formats documents.js's live-view editors cover — docx, pptx, odt, odp, ods, odg, markdown — each with a full navigate/edit/save experience built on that format's editor (paragraphs and runs for docx/odt/markdown, slides and shapes for pptx/odp, sheets and cells for ods, pages and vectors/shapes for odg), plus undo (whole-document snapshots taken before each committed mutation), search, a command palette, and PDF export straight from the open document. On a pptx or odp slide, `a` from the shape list also adds a real table (rows then columns, a two-step prompt) alongside the existing textbox/image choices, and `n` opens the slide's own speaker notes for either format — `PptxSlide` and `OdpSlide` both carry a real `.notes` getter/setter, so notes editing was never odp-specific, only gated that way until this phase removed the gate. On a docx document, `x` from the paragraph/table list opens a read-only view of the document's own comments, footnotes, headers/footers, and numbering definitions — a docx-only concept with no odt equivalent, and the TUI counterpart to the `docx-extras` command, rendered through the identical `src/docx-extras-format.ts` line formatter so the two can't drift apart. `m`, global to every screen with a document open, shows that document's own title/author/subject/keywords/etc metadata read-only — every format the `metadata` command covers, including `.odb`/`.pdf`/`.xlsx`; documents.js's live-view editors have no metadata setter to mutate in place, so this TUI has no matching write screen (use the `set-metadata` command for that).
230
+ It supports the same formats documents.js's live-view editors cover — docx, pptx, odt, odp, ods, odg, markdown, and since their codecs gained writers the three legacy binary formats doc, xls, and ppt — each with a full navigate/edit/save experience built on that format's editor (paragraphs and runs for docx/odt/markdown/doc, slides and shapes for pptx/odp/ppt, sheets and cells for ods/xls, pages and vectors/shapes for odg), plus undo (whole-document snapshots taken before each committed mutation), search, a command palette, and PDF export straight from the open document. On a pptx or odp slide, `a` from the shape list also adds a real table (rows then columns, a two-step prompt) alongside the existing textbox/image choices, and `n` opens the slide's own speaker notes for either format — `PptxSlide` and `OdpSlide` both carry a real `.notes` getter/setter, so notes editing was never odp-specific, only gated that way until this phase removed the gate. On a docx document, `x` from the paragraph/table list opens a read-only view of the document's own comments, footnotes, headers/footers, and numbering definitions — a docx-only concept with no odt equivalent, and the TUI counterpart to the `docx-extras` command, rendered through the identical `src/docx-extras-format.ts` line formatter so the two can't drift apart. `m`, global to every screen with a document open, shows that document's own title/author/subject/keywords/etc metadata — every format the `metadata` command covers, including `.odb`/`.pdf`/`.xlsx`. For the seven live-view-editor formats, title/author/subject/keywords are editable in place through documents.js's own `editor.metadata` setter (ExaDev/documents.js#933): select a field and Enter opens it for editing, committing a partial merge that leaves every other field untouched. Every other format (and creator/producer/created/modified on the editable formats too, since those are derived/producer-stamped rather than user-authored) stays read-only here — use the `set-metadata` command for a format with no live-view editor.
231
231
 
232
232
  Markdown (`.md`/`.markdown`) shares the same paragraph/run/table body-list screens docx and odt already use, through documents.js's own `MarkdownEditor` (`openMarkdown`/`createMarkdownEditor`) — a genuine live view over a mutable `ContentDocument`, the same live-view contract every other editor here follows, even though there is no `XmlElement` tree underneath it the way there is for docx/odt (`MarkdownEditor.toMarkdownText()` re-serialises the whole document fresh on every call, rather than exposing a `toBytes()`). Appending a paragraph, appending a run, and toggling bold/italic all go through the identical reducer actions docx/odt use; a markdown run has no underline, colour, font family, or font size at all (CommonMark/GFM has no construct for any of the four), so those keys — along with image insertion, which `MarkdownParagraph` has no counterpart for — are simply absent from a markdown paragraph's own key hints rather than opening a prompt that could only end in a warning. A markdown table can be created and its cells edited through the same 'T' wizard and table-view screens docx/odt use, but GFM tables have no cell-merge concept, so a merge requested alongside table creation still creates the table (unmerged) and reports why the merge itself didn't happen. `:view-source` (markdown documents only) shows the literal text the document was opened with side by side with what a save would write right now — these can genuinely differ even with no edits made this session, from a heading-style, bullet-marker, or line-ending choice the writer normalises. Every save re-serialises the whole document fresh through `buildMarkdownText`, a deliberate, permanent consequence of structured editing rather than something to work around. Diagnostics from the read side (a clamped heading level, a dropped front-matter key, a fenced code block's own info string with nowhere to go, …) now surface into the same diagnostics panel a PDF export's own substitutions already populate, the moment a `.md` file is opened, not only on export. documents.js's own `createMarkdownEditor()` exists now, but this TUI does not yet wire a "new markdown document" flow into `:new`/the new-document picker, so a markdown document can still only be opened from an existing file.
233
233
 
234
- Three further kinds of format open read-only: a `.odb` browses its tables and rows with no write path at all (documents.js's own `.odb` support has no write direction to offer), a `.pdf` browses its pages and positioned items rather than being edited in place, and a `.xlsx`, `.csv`, or `.svg` opens as a converted PDF preview — documents.js has no spreadsheet or svg editor to hold a live view into, so opening one runs `xlsxToPdf`, `csvToPdf`, or `svgToPdf` once at open time and browses the result through the identical page-list/page-items/item-detail screens a real `.pdf` uses, with the original bytes kept alongside so a later export re-runs the same conversion with the caller's own fonts and diagnostics rather than reusing the fixed preview conversion. A `.odb` additionally browses its *structure* alongside its data: `f` from the table list opens the form browser and `r` the report browser, each listing what the database declares and opening one to show it in full — a form's own data source and field-bound controls (sub-forms nested under their parent), a report's data-source command, band and group structure, and every `rpt:` formula. Both are rendered through the same `src/odb-structure.ts` the `odb-forms`/`odb-reports` commands print, so the two views cannot drift apart, and search filters by line (`/SUM` narrows a long report to its aggregate expressions). `Enter` on a report's own detail screen renders it — its data-source command resolved, its `rpt:` formulas evaluated, its bands laid out — to a real `docx`/`odt`/`pdf` file, through the same two-field destination-path-then-font-list form the PDF-export screen below uses; the TUI counterpart to the `odb-render-report` command. A standalone `.odf` formula document has no TUI editor either — nothing to edit interactively, only a PDF conversion.
234
+ Three further kinds of format open read-only: a `.odb` browses its tables and rows with no write path at all (documents.js's own `.odb` support has no write direction to offer), a `.pdf` browses its pages and positioned items rather than being edited in place, and a `.xlsx`, `.csv`, `.svg`, `.rtf`, `.wpd`, or `.epub` opens as a converted PDF preview — documents.js has no spreadsheet, svg, rtf, wpd, or epub editor to hold a live view into (wpd-codec ships no writer at all, so a wpd document could never gain one) — so opening one runs `xlsxToPdf`, `csvToPdf`, `svgToPdf`, `rtfToPdf`, `convertDocument("wpd", "pdf", ...)`, or `epubToPdf` once at open time and browses the result through the identical page-list/page-items/item-detail screens a real `.pdf` uses, with the original bytes kept alongside so a later export re-runs the same conversion with the caller's own fonts and diagnostics rather than reusing the fixed preview conversion. A `.doc`, `.xls`, or `.ppt` is NOT in that read-only group any more: each opens through its own live-view editor (`DocEditor`/`XlsEditor`/`PptEditor`) exactly the way docx/odt do — doc through the shared paragraph/run/table screens, xls through the sheet-list/grid pair (cell values of every kind, with the value editor the ods grid already uses), ppt through the slide list plus a detail screen scoped to what a .ppt can state (shape text, speaker notes, adding text boxes). A later export runs `docToPdf`/`xlsToPdf`/`pptToPdf` from the editor's current bytes, so it reflects edits made this session. Two honest limits carried over from the codecs themselves: an xls cell's formula is readable but never written back (xls-codec's writer has no formula path), and an xls merge is set as colSpan/rowSpan on the anchor cell rather than through a rectangle prompt. A `.odb` additionally browses its _structure_ alongside its data: `f` from the table list opens the form browser and `r` the report browser, each listing what the database declares and opening one to show it in full — a form's own data source and field-bound controls (sub-forms nested under their parent), a report's data-source command, band and group structure, and every `rpt:` formula. Both are rendered through the same `src/odb-structure.ts` the `odb-forms`/`odb-reports` commands print, so the two views cannot drift apart, and search filters by line (`/SUM` narrows a long report to its aggregate expressions). `Enter` on a report's own detail screen renders it — its data-source command resolved, its `rpt:` formulas evaluated, its bands laid out — to a real `docx`/`odt`/`pdf` file, through the same two-field destination-path-then-font-list form the PDF-export screen below uses; the TUI counterpart to the `odb-render-report` command. A standalone `.odf` formula document has no TUI editor either — nothing to edit interactively, only a PDF conversion.
235
235
 
236
236
  The export-to-PDF screen (`e` from any editor screen) is a two-field form: a destination path, then an optional comma-separated list of local `.ttf`/`.otf` paths, which are the same `--font-file` faces the CLI takes and are derived the same way — each font's family, weight, and slope come from the file itself. `Enter` on the path field moves to the fonts field and `Enter` there exports, so leaving fonts empty is still "type a path, press Enter twice". Comma-separated rather than space-separated because a font path routinely contains spaces and almost never a comma. A face falling back to a substitute is reported into the same diagnostics panel a character substitution already is, and a bad font path fails the export with the file named, before anything is written to the destination.
237
237
 
@@ -239,24 +239,24 @@ The export-to-PDF screen (`e` from any editor screen) is a two-field form: a des
239
239
 
240
240
  The global bindings below apply everywhere; individual screens (a docx run's own bold/italic toggles, an ods cell's own value-kind picker) add their own on top:
241
241
 
242
- | Keys | Action |
243
- |---|---|
244
- | `↑` / `k` | Move the selection up |
245
- | `↓` / `j` | Move the selection down |
246
- | `Enter` / `→` / `l` | Open or edit the selected item |
247
- | `Esc` / `←` / `h` | Go back to the previous screen |
248
- | `PageUp` / `PageDown` | Scroll a page at a time |
249
- | `Home` / `End` | Jump to the first or last item |
250
- | `a` | Append a new item to the current list |
251
- | `m` | Show the open document's metadata (read-only) |
252
- | `Ctrl+S` | Save the open document |
253
- | `Ctrl+W` | Close the open document |
254
- | `Ctrl+Z` | Undo the last change |
255
- | `q` / `Ctrl+C` | Quit |
256
- | `:` | Open the command palette |
257
- | `/` | Search within the current screen |
258
- | `?` | Show this help |
259
- | `Ctrl+D` | Show the diagnostics panel |
242
+ | Keys | Action |
243
+ | --------------------- | ------------------------------------------------------------------------------ |
244
+ | `↑` / `k` | Move the selection up |
245
+ | `↓` / `j` | Move the selection down |
246
+ | `Enter` / `→` / `l` | Open or edit the selected item |
247
+ | `Esc` / `←` / `h` | Go back to the previous screen |
248
+ | `PageUp` / `PageDown` | Scroll a page at a time |
249
+ | `Home` / `End` | Jump to the first or last item |
250
+ | `a` | Append a new item to the current list |
251
+ | `m` | Show the open document's metadata (editable for docx/pptx/odt/odp/ods/odg/pdf) |
252
+ | `Ctrl+S` | Save the open document |
253
+ | `Ctrl+W` | Close the open document |
254
+ | `Ctrl+Z` | Undo the last change |
255
+ | `q` / `Ctrl+C` | Quit |
256
+ | `:` | Open the command palette |
257
+ | `/` | Search within the current screen |
258
+ | `?` | Show this help |
259
+ | `Ctrl+D` | Show the diagnostics panel |
260
260
 
261
261
  ## Architecture
262
262
 
@@ -286,7 +286,11 @@ pnpm test:workers # turbo run _test:workers -> vitest run --config vitest.worke
286
286
  - **`tsdown.config.ts` disables `fixedExtension` on both build passes.** `platform: 'node'` defaults tsdown's `fixedExtension` to `true`, which would emit `dist/cli.mjs`/`dist/index.mjs` regardless of `package.json`'s own `"type": "module"` — mismatching the `.js`/`.cjs` paths `bin` and `exports` actually name. Both entries set `fixedExtension: false` explicitly so the build output matches what's published.
287
287
  - **`readOdbTables` needs `decodeOdbPackage` from `documents.js`, not `decodePackage`.** `documents.js`'s own `decodePackage` re-exports `ooxml.js`'s OOXML-only reader and cannot decode an ODF `.odb` container at all; `decodeDocumentPackage(format, bytes)` dispatches to `odf.js` internally for every real `DocumentFormat` member (odt/odp/ods/odg/odf), but `.odb` is deliberately not one of those (it has no PDF conversion and no write direction — see documents.js's own README), so it has no format string to pass either. `decodeOdbPackage` is the `.odb`-specific sibling that decodes the identical raw ODF container directly — `commands/odb.ts` and the TUI's `format/open-document.ts` both use it for exactly this reason. `odf.js` and `pdf-codec` are devDependencies only: every runtime reach into them (the spreadsheet grid's `cellReference`/`columnIndexToLetters` for A1-style cell/column labelling, font-file inspection via `describeFontFace`) now goes through `documents.js`'s own re-exports, and `odf.js` survives in `devDependencies` solely because `src/test-support/embedded-font-fixture.ts` builds real ODF package fixtures from its low-level XML primitives.
288
288
  - **A TUI screen must call an editor's own accessors fresh on every render, never cache them.** documents.js's live-view editors (`DocxRun`, `OdtParagraph`, `OdsCell`, ...) mutate the real XML tree in place — `editor.paragraphs()`/`slide.shapes()`/`sheet.cell(r, c)` called once and stored in `useState`/`useMemo` goes stale the instant any screen mutates the underlying document, with nothing in the type system or in React to catch it (see `src/tui/state/types.ts`'s own doc comment on `Screen`).
289
- - **The conversions this CLI runs carry the same fidelity limits documents.js itself documents** — most notably that PDF-pivot conversions are not round-trip-lossless (line wrapping drifts under standard-14 font substitution, justified text renders left-aligned, tables and vector shapes don't reconstruct from a PDF), and that `pdf-to-ods` recovers only what a spreadsheet printed, never a real number/date/formula. None of that is specific to this CLI or the TUI; see documents.js's own README, particularly its [Fidelity](https://github.com/ExaDev/documents.js#fidelity) section, for the full, format-by-format account.
289
+ - **The conversions this CLI runs carry the same fidelity limits documents.js itself documents** — most notably that PDF-pivot conversions are not round-trip-lossless (line wrapping drifts under standard-14 font substitution, justified text renders left-aligned, tables and vector shapes don't reconstruct from a PDF), and that `pdf-to-ods` recovers only what a spreadsheet printed, never a real number/date/formula. None of that is specific to this CLI or the TUI; see documents.js's own README, particularly its [Fidelity](../documents.js/README.md#fidelity) section, for the full, format-by-format account.
290
+
291
+ ## Contributing
292
+
293
+ Release, CI, and commit-message conventions are all workspace-wide, not package-local — see the [monorepo root README](../../README.md#releases) for the release mechanism and [CONTRIBUTING.md](../../CONTRIBUTING.md) for the shared git hooks and history conventions. Work inside `packages/document-cli/`.
290
294
 
291
295
  ## License
292
296
 
package/dist/cli.js CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { a as odbFormSummary, c as formatDocxExtrasLines, d as writeOutput, f as loadProvidedFonts, h as isDocumentFormat, i as formatOdbReportLines, l as readInput, m as inferFormatFromExtension, n as describeOdbReport, o as formatMetadataLines, r as formatOdbFormLines, s as presentMetadataEntries, t as describeOdbForm, u as resolveDefaultOutputPath } from "./odb-structure-B1ygPCEa.js";
2
+ import { a as odbFormSummary, c as presentMetadataEntries, d as resolveDefaultOutputPath, f as writeOutput, g as isDocumentFormat, h as inferFormatFromExtension, i as formatOdbReportLines, l as formatDocxExtrasLines, n as describeOdbReport, o as formatMetadataLines, p as loadProvidedFonts, r as formatOdbFormLines, t as describeOdbForm, u as readInput } from "./odb-structure-CItnH50p.js";
3
3
  import { Command, CommanderError, InvalidArgumentError } from "commander";
4
4
  import { writeFile } from "node:fs/promises";
5
5
  import { basename, dirname, extname, join, resolve } from "node:path";
6
- import { CsvSheetNotFoundError, CsvSheetNotSpecifiedError, HsqldbSqlEvaluationError, HsqldbSqlParseError, HsqldbSqlUnsupportedError, OdbNoEmbeddedDataSourceError, OdbReportNotSpecifiedError, OdbTableNotFoundError, OdbTableNotSpecifiedError, OdbUnsupportedFormatError, OdmUnresolvedSectionError, PdfEncryptedError, PdfParseError, SvgMultiPageNotSpecifiedError, SvgPageNotFoundError, UnrecognizedDocumentSchemaError, UnsupportedFontSourceFormatError, buildCsvText, buildDocumentBytes, buildSvgText, createLocalDocumentConverter, decodeOdbPackage, decodePackage, documentFromJson, documentPackageWithSchema, encodeCsvText, encodeSvgText, evaluateSelect, extractSourceFontsForFormat, flattenPackage, hsqldbCellDisplayText, odbReportToDocx, odbReportToOdt, odbReportToPdf, odbToCsv, odbToXlsx, odmToPdf, parseSelect, readDocumentMetadata, readDocxExtras, readOdbForms, readOdbInventory, readOdbReportContent, readOdbReports, readOdbTables, readPdf, setDocumentMetadata } from "documents.js";
6
+ import { CsvSheetNotFoundError, CsvSheetNotSpecifiedError, HsqldbSqlEvaluationError, HsqldbSqlParseError, HsqldbSqlUnsupportedError, OdbNoEmbeddedDataSourceError, OdbReportNotSpecifiedError, OdbTableNotFoundError, OdbTableNotSpecifiedError, OdbUnsupportedFormatError, OdmUnresolvedSectionError, PdfEncryptedError, PdfParseError, SvgMultiPageNotSpecifiedError, SvgPageNotFoundError, UnrecognizedDocumentSchemaError, UnsupportedFontSourceFormatError, buildCsvText, buildDocumentBytes, buildSvgText, createLocalDocumentConverter, decodeOdbPackage, decodePackage, documentFromJson, documentTreeWithSchema, encodeCsvText, encodeSvgText, evaluateSelect, extractSourceFontsForFormat, hsqldbCellDisplayText, odbReportToDocx, odbReportToOdt, odbReportToPdf, odbToCsv, odbToXlsx, odmToPdf, parseSelect, readDocumentMetadata, readDocxExtras, readNativeDocumentTree, readOdbForms, readOdbInventory, readOdbReportContent, readOdbReports, readOdbTables, readPdf, setDocumentMetadata } from "documents.js";
7
7
  import { existsSync, readFileSync } from "node:fs";
8
+ import { flattenTree } from "document-schema.js";
8
9
  import { buildOutline, isOutlineNode, outlineLeafText } from "document-outline.js";
9
10
  //#region src/runtime/abort.ts
10
11
  function combineSignals(a, b) {
@@ -13,9 +14,13 @@ function combineSignals(a, b) {
13
14
  controller.abort(signal.reason);
14
15
  };
15
16
  if (a.aborted) forward(a);
16
- else a.addEventListener("abort", () => forward(a), { once: true });
17
+ else a.addEventListener("abort", () => {
18
+ forward(a);
19
+ }, { once: true });
17
20
  if (b.aborted) forward(b);
18
- else b.addEventListener("abort", () => forward(b), { once: true });
21
+ else b.addEventListener("abort", () => {
22
+ forward(b);
23
+ }, { once: true });
19
24
  return controller.signal;
20
25
  }
21
26
  function createRuntimeSignal(options) {
@@ -134,7 +139,7 @@ function createFilesystemMarkdownImageResolver(baseDir) {
134
139
  }
135
140
  //#endregion
136
141
  //#region src/commands/shared.ts
137
- const KNOWN_DOCUMENT_FORMATS = "docx, pptx, xlsx, odt, odp, ods, odg, svg, odf, csv, markdown, pdf";
142
+ const KNOWN_DOCUMENT_FORMATS = "docx, pptx, xlsx, odt, odp, ods, odg, svg, odf, csv, markdown, rtf, wpd, doc, xls, ppt, epub, pdf";
138
143
  function resolveTargetFormat(output, out, to) {
139
144
  if (to !== void 0) {
140
145
  if (!isDocumentFormat(to)) return { errorMessage: `unknown --to format '${to}'; expected one of ${KNOWN_DOCUMENT_FORMATS}` };
@@ -163,6 +168,7 @@ function buildConversionAction(source, target) {
163
168
  try {
164
169
  const inputBytes = await readInput(input, { signal });
165
170
  const fonts = await loadProvidedFonts(options.fontFiles ?? [], { signal });
171
+ const images = createFilesystemMarkdownImageResolver(input === "-" ? "." : dirname(resolve(input)));
166
172
  const result = await createLocalDocumentConverter().convert({
167
173
  source: {
168
174
  format: source,
@@ -177,7 +183,7 @@ function buildConversionAction(source, target) {
177
183
  quiet: options.quiet,
178
184
  command
179
185
  }) : void 0,
180
- images: createFilesystemMarkdownImageResolver(input === "-" ? "." : dirname(resolve(input))),
186
+ images,
181
187
  delimiter: options.delimiter,
182
188
  sheet: options.sheet,
183
189
  page: options.page
@@ -189,8 +195,13 @@ function buildConversionAction(source, target) {
189
195
  command
190
196
  });
191
197
  for (const diagnostic of result.diagnostics) reporter.report(diagnostic);
192
- if (options.dumpPackage !== void 0) if (result.package === void 0) process.stderr.write(`[${command}] this conversion does not produce an intermediate DocumentPackage\n`);
193
- else await writeFile(options.dumpPackage, JSON.stringify(documentPackageWithSchema(result.package), void 0, 2));
198
+ if (options.dumpPackage !== void 0) {
199
+ const nativeTree = readNativeDocumentTree(source, new Uint8Array(inputBytes), {
200
+ signal,
201
+ images
202
+ });
203
+ await writeFile(options.dumpPackage, JSON.stringify(documentTreeWithSchema(nativeTree), void 0, 2));
204
+ }
194
205
  reporter.summarize({
195
206
  output: resolvedOutput,
196
207
  bytes: result.document.bytes.byteLength,
@@ -221,7 +232,7 @@ function addVerboseOption(command) {
221
232
  return command.option("--verbose", "include a full stack trace when the run fails", false);
222
233
  }
223
234
  function addDumpPackageOption(command) {
224
- return command.option("--dump-package <file>", "write the intermediate DocumentPackage (the tree form: container-grouped content carrying per-node rendered frames, plus page sizes) this conversion built to a JSON file");
235
+ return command.option("--dump-package <file>", "write the intermediate DocumentTree (the tree form: container-grouped content carrying per-node rendered frames, plus page sizes) this conversion built to a JSON file");
225
236
  }
226
237
  function collectFontFile(value, previous) {
227
238
  return [...previous, value];
@@ -404,6 +415,9 @@ function isSchemaVersionMismatchError(error) {
404
415
  function isLayoutSchemaDemotedError(error) {
405
416
  return error instanceof Error && error.name === "LayoutSchemaDemotedError";
406
417
  }
418
+ function isDocumentPackageRenamedError(error) {
419
+ return error instanceof Error && error.name === "DocumentPackageRenamedError";
420
+ }
407
421
  async function runFromPackage(input, output, options) {
408
422
  const command = "from-package";
409
423
  if (output !== void 0 && options.out !== void 0 && output !== options.out) {
@@ -428,11 +442,11 @@ async function runFromPackage(input, output, options) {
428
442
  return 1;
429
443
  }
430
444
  const result = documentFromJson(parsed);
431
- if (result.kind !== "DocumentPackage") {
432
- process.stderr.write(`[${command}] '${input}' is a ${result.kind}, not a DocumentPackage -- only a file written by --dump-package can be read back by this command\n`);
445
+ if (result.kind !== "DocumentTree") {
446
+ process.stderr.write(`[${command}] '${input}' is a ${result.kind}, not a DocumentTree -- only a file written by --dump-package can be read back by this command\n`);
433
447
  return 2;
434
448
  }
435
- const content = flattenPackage(result.value);
449
+ const content = flattenTree(result.value);
436
450
  const bytes = target.format === "csv" ? encodeCsvText(buildCsvText(content, {
437
451
  delimiter: options.delimiter,
438
452
  sheet: options.sheet
@@ -454,19 +468,23 @@ async function runFromPackage(input, output, options) {
454
468
  return 1;
455
469
  }
456
470
  if (isSchemaVersionMismatchError(error)) {
457
- process.stderr.write(`[${command}] '${input}' is a DocumentPackage dump from document-schema.js@${error.dumpVersion}, but this CLI's documents.js reads only @${error.installedVersion}-major dumps -- 4.0.0 replaced the flat { formatVersion, content, pages } shape with the tree-form DocumentPackage (ExaDev/document-schema.js#20); re-run the source conversion with --dump-package to write a current dump\n`);
471
+ process.stderr.write(`[${command}] '${input}' is a DocumentTree dump from document-schema.js@${error.dumpVersion}, but this CLI's documents.js reads only @${error.installedVersion}-major dumps -- 4.0.0 replaced the flat { formatVersion, content, pages } shape with the tree-form DocumentTree (ExaDev/document-schema.js#20); re-run the source conversion with --dump-package to write a current dump\n`);
458
472
  return 1;
459
473
  }
460
474
  if (isLayoutSchemaDemotedError(error)) {
461
475
  process.stderr.write(`[${command}] '${input}' is a LayoutDocument dump -- LayoutDocument moved to pdf-codec in document-schema.js 4.0.0 and is no longer a schema-stamped input; re-run the source conversion with --dump-package and read that package back instead\n`);
462
476
  return 1;
463
477
  }
478
+ if (isDocumentPackageRenamedError(error)) {
479
+ process.stderr.write(`[${command}] '${input}' is a document-package dump -- DocumentPackage was renamed to DocumentTree in document-schema.js 5.0.0 (ExaDev/documents.js#661); re-run the source conversion with --dump-package to write a current tree-form DocumentTree dump\n`);
480
+ return 1;
481
+ }
464
482
  process.stderr.write(`${formatError(error, options.verbose)}\n`);
465
483
  return mapErrorToExit(error, getAbortReason());
466
484
  }
467
485
  }
468
486
  function registerFromPackageCommand(program) {
469
- const command = program.command("from-package <input> [output]").description("read a DocumentPackage previously written by --dump-package and export it to a real target format");
487
+ const command = program.command("from-package <input> [output]").description("read a DocumentTree previously written by --dump-package and export it to a real target format");
470
488
  addOutOption(command);
471
489
  addTimeoutOption(command);
472
490
  addJsonOption(command);
@@ -958,20 +976,6 @@ function registerOdmCommand(program) {
958
976
  }
959
977
  //#endregion
960
978
  //#region src/commands/outline.ts
961
- const OUTLINE_CONVERSION_TARGET = {
962
- docx: "odt",
963
- odt: "docx",
964
- markdown: "docx",
965
- pptx: "odp",
966
- odp: "pptx",
967
- xlsx: "ods",
968
- ods: "xlsx",
969
- csv: "xlsx",
970
- odg: "odp",
971
- svg: "odg",
972
- pdf: "docx",
973
- odf: "pdf"
974
- };
975
979
  const INDENT = " ";
976
980
  function singleLineText(text) {
977
981
  return text.replaceAll(/\s+/g, " ").trim();
@@ -1007,7 +1011,6 @@ async function runOutline(input, options) {
1007
1011
  process.stderr.write(`[${command}] ${source.errorMessage}\n`);
1008
1012
  return 2;
1009
1013
  }
1010
- const target = OUTLINE_CONVERSION_TARGET[source.format];
1011
1014
  const { signal, getAbortReason } = createRuntimeSignal({ timeoutMs: options.timeout });
1012
1015
  const reporter = createDiagnosticReporter({
1013
1016
  json: options.json,
@@ -1016,19 +1019,14 @@ async function runOutline(input, options) {
1016
1019
  });
1017
1020
  try {
1018
1021
  const inputBytes = await readInput(input, { signal });
1019
- const result = await createLocalDocumentConverter().convert({
1020
- source: {
1021
- format: source.format,
1022
- bytes: new Uint8Array(inputBytes)
1023
- },
1024
- targetFormat: target
1025
- }, {
1022
+ const tree = readNativeDocumentTree(source.format, new Uint8Array(inputBytes), {
1026
1023
  signal,
1027
- images: createFilesystemMarkdownImageResolver(input === "-" ? "." : dirname(resolve(input)))
1024
+ images: createFilesystemMarkdownImageResolver(input === "-" ? "." : dirname(resolve(input))),
1025
+ sink: (diagnostic) => {
1026
+ reporter.report(diagnostic);
1027
+ }
1028
1028
  });
1029
- for (const diagnostic of result.diagnostics) reporter.report(diagnostic);
1030
- if (result.package === void 0) throw new Error(`the ${source.format}-to-${target} conversion produced no intermediate DocumentPackage`);
1031
- const outline = buildOutline(result.package);
1029
+ const outline = buildOutline(tree);
1032
1030
  if (options.json) {
1033
1031
  process.stdout.write(`${JSON.stringify(outline, void 0, 2)}\n`);
1034
1032
  return 0;
@@ -1176,11 +1174,11 @@ async function runSetMetadata(input, output, options) {
1176
1174
  function registerSetMetadataCommand(program) {
1177
1175
  const command = program.command("set-metadata <input> [output]").description("patch a document's own title/author/subject/keywords, leaving every other field and every other flag as-is").addHelpText("after", [
1178
1176
  "",
1179
- "Two write paths: a pdf source/target patches the metadata directly on the parsed PDF (writePdf), with no layout engine",
1180
- "involved at all -- genuinely lossless for everything else on the page. Every other supported format (docx, pptx, odt,",
1181
- "odp, ods, odg, markdown) rebuilds a fresh package from that format's own ContentDocument -- for docx specifically,",
1182
- "this is LOSSY: it drops anything docx-extras covers (comments, footnotes, headers/footers, numbering definitions),",
1183
- "since buildDocxPackage builds a fresh package from the ContentDocument alone, with no way to carry that data through.",
1177
+ "Three write paths: a pdf source/target patches the metadata directly on the parsed PDF (writePdf), and a docx source/target",
1178
+ "patches docProps/core.xml directly on the decoded package -- both with no layout engine or ContentDocument rebuild involved",
1179
+ "at all, so everything else on the page (pdf) or in the package (docx -- comments, footnotes, headers/footers, numbering",
1180
+ "definitions included) survives byte-faithful. Every other supported format (pptx, xlsx, odt, odp, ods, odg, markdown, rtf)",
1181
+ "rebuilds a fresh package from that format's own ContentDocument instead.",
1184
1182
  "",
1185
1183
  "set-metadata does not convert format -- source and target must match. Run convert/from-package first, then",
1186
1184
  "set-metadata on the result, if you need a different target format."
@@ -1201,12 +1199,12 @@ function registerSetMetadataCommand(program) {
1201
1199
  }
1202
1200
  //#endregion
1203
1201
  //#region package.json
1204
- var version = "3.1.7";
1202
+ var version = "5.7.10";
1205
1203
  //#endregion
1206
1204
  //#region src/program.ts
1207
1205
  function createProgram() {
1208
1206
  const program = new Command("document-cli");
1209
- program.description("every documents.js docx/pptx/odt/odp/ods/odg/odf/pdf/odm/odb/xlsx/csv/svg/markdown conversion, bridge, and inspector as a scriptable command");
1207
+ program.description("every documents.js docx/pptx/odt/odp/ods/odg/odf/pdf/odm/odb/xlsx/csv/svg/markdown/rtf conversion, bridge, and inspector as a scriptable command");
1210
1208
  program.version(version);
1211
1209
  program.exitOverride((error) => {
1212
1210
  process.exitCode = error.exitCode === 0 ? 0 : 2;
@@ -1229,7 +1227,7 @@ function createProgram() {
1229
1227
  //#region src/cli.ts
1230
1228
  async function launchTui(startPath, signal) {
1231
1229
  try {
1232
- const { runTui } = await import("./tui-tT3An-cI.js");
1230
+ const { runTui } = await import("./tui-CGo4rAhe.js");
1233
1231
  await runTui({
1234
1232
  startPath,
1235
1233
  signal
@@ -1248,7 +1246,7 @@ async function main() {
1248
1246
  const args = process.argv.slice(2);
1249
1247
  const [dispatchToken] = args;
1250
1248
  if (dispatchToken === void 0 || dispatchToken === "tui") {
1251
- if (process.stdout.isTTY !== true) {
1249
+ if (!process.stdout.isTTY) {
1252
1250
  if (dispatchToken === "tui") {
1253
1251
  process.stderr.write("the TUI requires an interactive terminal (TTY); stdout is currently redirected\n");
1254
1252
  process.exitCode = 2;