document-cli 3.0.11 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -5
- package/dist/cli.js +101 -2
- package/dist/index.cjs +105 -6
- package/dist/index.js +105 -6
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/ExaDev/document-cli) [](https://www.npmjs.com/package/document-cli) [](https://github.com/ExaDev/document-cli/releases/latest) [](https://github.com/ExaDev/document-cli/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, 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 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`.
|
|
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
|
|
|
@@ -15,6 +15,7 @@ graph TD
|
|
|
15
15
|
mdcodec("markdown-codec")
|
|
16
16
|
bytecodec("byte-codec")
|
|
17
17
|
documents("documents.js")
|
|
18
|
+
outline("document-outline.js")
|
|
18
19
|
mcp("document-mcp")
|
|
19
20
|
cli("document-cli")
|
|
20
21
|
|
|
@@ -23,6 +24,7 @@ graph TD
|
|
|
23
24
|
schema --> pdfcodec
|
|
24
25
|
schema --> mdcodec
|
|
25
26
|
schema --> documents
|
|
27
|
+
schema --> outline
|
|
26
28
|
ooxml --> documents
|
|
27
29
|
odf --> documents
|
|
28
30
|
pdfcodec --> documents
|
|
@@ -32,6 +34,7 @@ graph TD
|
|
|
32
34
|
documents --> mcp
|
|
33
35
|
pdfcodec --> mcp
|
|
34
36
|
documents --> cli
|
|
37
|
+
outline --> cli
|
|
35
38
|
odf --> cli
|
|
36
39
|
pdfcodec --> cli
|
|
37
40
|
|
|
@@ -42,6 +45,7 @@ graph TD
|
|
|
42
45
|
click mdcodec "https://github.com/ExaDev/markdown-codec" "markdown-codec"
|
|
43
46
|
click bytecodec "https://github.com/ExaDev/byte-codec" "byte-codec"
|
|
44
47
|
click documents "https://github.com/ExaDev/documents.js" "documents.js"
|
|
48
|
+
click outline "https://github.com/ExaDev/document-outline.js" "document-outline.js"
|
|
45
49
|
click mcp "https://github.com/ExaDev/document-mcp" "document-mcp"
|
|
46
50
|
click cli "https://github.com/ExaDev/document-cli" "document-cli"
|
|
47
51
|
|
|
@@ -93,7 +97,7 @@ document-cli convert report.docx report.pdf
|
|
|
93
97
|
document-cli convert report.docx - --to pdf > report.pdf
|
|
94
98
|
```
|
|
95
99
|
|
|
96
|
-
**`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`).
|
|
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`).
|
|
97
101
|
|
|
98
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.
|
|
99
103
|
|
|
@@ -164,6 +168,13 @@ document-cli set-metadata report.docx report.docx --set-title "Q3 Report" --set-
|
|
|
164
168
|
document-cli set-metadata report.docx report.odt --set-keywords "draft,internal"
|
|
165
169
|
```
|
|
166
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):
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
document-cli outline report.docx
|
|
175
|
+
document-cli outline slides.pptx --json
|
|
176
|
+
```
|
|
177
|
+
|
|
167
178
|
**`tui [file]`** — launches the interactive terminal app; see [The TUI](#the-tui) below.
|
|
168
179
|
|
|
169
180
|
### Shared flags
|
|
@@ -180,7 +191,7 @@ The explicit conversions, `convert`, `odm-to-pdf`, `odb-to-xlsx`, `odb-to-csv`,
|
|
|
180
191
|
|
|
181
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:
|
|
182
193
|
|
|
183
|
-
`--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; `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 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.
|
|
184
195
|
|
|
185
196
|
### Real fonts
|
|
186
197
|
|
|
@@ -251,7 +262,7 @@ The global bindings below apply everywhere; individual screens (a docx run's own
|
|
|
251
262
|
|
|
252
263
|
The package splits into two independent layers sharing one thin format-detection module, `src/format.ts` (extension ⇄ `DocumentFormat` inference), so a change to how a format is recognised from a path never needs making twice:
|
|
253
264
|
|
|
254
|
-
- **`src/commands/` + `src/runtime/`** is the CLI proper. `commands/shared.ts`'s `buildConversionAction(source, target)` is the one implementation behind every `<source>-to-<target>` command and the generic `convert` — it partially applies a format pair and hands back a ready commander action, so the conversion-running logic (read input, call `createLocalDocumentConverter().convert`, write output, report diagnostics, map errors to exit codes) exists exactly once regardless of which pair is invoked. `commands/{odm,odb,pdf-inspect,fonts,docx-extras,metadata}.ts` each call their own documents.js function directly instead, since none of them fits the generic `DocumentConverter` port's bytes-in/bytes-out shape (`odmToPdf` needs a `resolveSubDocument` callback, `.odb` extraction/query/report-rendering has no PDF conversion or port entry at all, `pdf-inspect` reads without converting, and `fonts`/`docx-extras`/`metadata` each read a document without producing one). `commands/set-metadata.ts` also bypasses `buildConversionAction` despite writing a file, since `setDocumentMetadata(source, target, bytes, overrides)` patches a document rather than converting one and requires source and target to match. `src/odb-structure.ts` sits alongside `src/format.ts` as a module shared by both layers: it turns an `OdbForm`/`OdbReport` into a flat array of already-indented lines, which the `odb-forms`/`odb-reports` commands join with newlines and the TUI's own form/report detail screens render one per list row; `src/docx-extras-format.ts` and `src/sql-result-format.ts` are the same idea for their own commands — the first turns a `DocxExtras` value into the flat line list `docx-extras` prints and the TUI's own `DocxExtrasScreen` renders, the second turns an `SqlResultSet` into an aligned plain-text table for `odb-query`. `src/runtime/` holds the process-level concerns every command shares: `abort.ts`'s `createRuntimeSignal` (one `SIGINT` listener and an optional timeout, combined into a single signal), `io.ts`'s stdin/stdout/file `-`-aware read and write helpers, `exit-codes.ts`'s exit-code constants and `mapErrorToExit`, `diagnostics.ts`'s stderr reporter, `fonts.ts`'s `loadProvidedFonts` (validating every `--font-file` path into documents.js's own `ProvidedFont` shape, shared by every font-accepting command and the TUI's own PDF-export and report-render screens), and `metadata-format.ts`'s `formatMetadataLines` (shared by `metadata`, `pdf-inspect`, and the TUI's own metadata screen).
|
|
265
|
+
- **`src/commands/` + `src/runtime/`** is the CLI proper. `commands/shared.ts`'s `buildConversionAction(source, target)` is the one implementation behind every `<source>-to-<target>` command and the generic `convert` — it partially applies a format pair and hands back a ready commander action, so the conversion-running logic (read input, call `createLocalDocumentConverter().convert`, write output, report diagnostics, map errors to exit codes) exists exactly once regardless of which pair is invoked. `commands/{odm,odb,pdf-inspect,fonts,docx-extras,metadata}.ts` each call their own documents.js function directly instead, since none of them fits the generic `DocumentConverter` port's bytes-in/bytes-out shape (`odmToPdf` needs a `resolveSubDocument` callback, `.odb` extraction/query/report-rendering has no PDF conversion or port entry at all, `pdf-inspect` reads without converting, and `fonts`/`docx-extras`/`metadata` each read a document without producing one); `commands/outline.ts` does go through the port, but only to take `ConversionResult.package` — its output bytes are discarded and document-outline.js's `buildOutline` projects the tree instead, the one command here whose product is neither the converted bytes nor a direct documents.js read. `commands/set-metadata.ts` also bypasses `buildConversionAction` despite writing a file, since `setDocumentMetadata(source, target, bytes, overrides)` patches a document rather than converting one and requires source and target to match. `src/odb-structure.ts` sits alongside `src/format.ts` as a module shared by both layers: it turns an `OdbForm`/`OdbReport` into a flat array of already-indented lines, which the `odb-forms`/`odb-reports` commands join with newlines and the TUI's own form/report detail screens render one per list row; `src/docx-extras-format.ts` and `src/sql-result-format.ts` are the same idea for their own commands — the first turns a `DocxExtras` value into the flat line list `docx-extras` prints and the TUI's own `DocxExtrasScreen` renders, the second turns an `SqlResultSet` into an aligned plain-text table for `odb-query`. `src/runtime/` holds the process-level concerns every command shares: `abort.ts`'s `createRuntimeSignal` (one `SIGINT` listener and an optional timeout, combined into a single signal), `io.ts`'s stdin/stdout/file `-`-aware read and write helpers, `exit-codes.ts`'s exit-code constants and `mapErrorToExit`, `diagnostics.ts`'s stderr reporter, `fonts.ts`'s `loadProvidedFonts` (validating every `--font-file` path into documents.js's own `ProvidedFont` shape, shared by every font-accepting command and the TUI's own PDF-export and report-render screens), and `metadata-format.ts`'s `formatMetadataLines` (shared by `metadata`, `pdf-inspect`, and the TUI's own metadata screen).
|
|
255
266
|
- **`src/tui/`** is the Ink app, entered lazily. `src/cli.ts` only imports `./tui/index.js` inside a dynamic `import()`, called just once dispatch has already decided the TUI is actually running — a plain `document-cli docx-to-pdf a b` invocation never loads React, Ink, or any TUI screen module at all, and `tsdown.config.ts`'s bin build correctly code-splits the TUI into its own lazily-loaded chunk as a result. Inside the TUI, `state/reducer.ts` and `state/types.ts` hold the single `AppState` (a screen stack, the open document, undo history, overlays), `format/open-document.ts` is the one place bytes become an open document for every format, and `screens/editors/<format>/` holds each format's own screen components — reusing shared building blocks (`screens/shared/paragraph-family.tsx`, `slide-family.tsx`) between docx/odt and pptx/odp respectively, the same way documents.js's own odp editor reuses its odt paragraph/run classes.
|
|
256
267
|
- **`src/index.ts`** re-exports the CLI's command-layer, format, and exit-code logic (not the TUI, which stays behind its own lazy import) as this package's `"."` library export, for a caller that wants `document-cli`'s conversion-running logic directly rather than spawning the bin as a subprocess.
|
|
257
268
|
- **One package, two npm names.** `package.json`'s `bin` field lists `document-cli` and `doculi` unconditionally, both pointing at the same built entry point — there is no separate build, alias package, or npm alias mechanism involved, just two keys in one `bin` object.
|
|
@@ -270,7 +281,7 @@ pnpm test:workers # turbo run _test:workers -> vitest run --config vitest.worke
|
|
|
270
281
|
|
|
271
282
|
## Gotchas
|
|
272
283
|
|
|
273
|
-
- **The lazy TUI import is load-bearing, not incidental.** `src/cli.ts` computes the dispatch token before doing anything else and only reaches `await import('./tui/index.js')` on the bare/`tui` branch — every other command path (all explicit conversions, `convert`, `formats`, `from-package`, `odm-to-pdf`, `odb-*`, `pdf-inspect`, `fonts`, `docx-extras`, `metadata`, `set-metadata`) never touches that import at all. This is what keeps a scripted, high-frequency CLI invocation from paying React/Ink's module-load cost on every call.
|
|
284
|
+
- **The lazy TUI import is load-bearing, not incidental.** `src/cli.ts` computes the dispatch token before doing anything else and only reaches `await import('./tui/index.js')` on the bare/`tui` branch — every other command path (all explicit conversions, `convert`, `formats`, `from-package`, `odm-to-pdf`, `odb-*`, `pdf-inspect`, `fonts`, `docx-extras`, `metadata`, `set-metadata`, `outline`) never touches that import at all. This is what keeps a scripted, high-frequency CLI invocation from paying React/Ink's module-load cost on every call.
|
|
274
285
|
- **A bare invocation and an explicit `tui` invocation fail differently on non-interactive stdout.** `document-cli` with no arguments and redirected stdout prints help and exits `0`, on the assumption that a bare invocation piped somewhere was more likely a forgotten argument than a deliberate TUI request. `document-cli tui` with redirected stdout is unambiguous, so it fails outright (exit `2`) rather than silently reinterpreting it as a help request.
|
|
275
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.
|
|
276
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.
|
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import { writeFile } from "node:fs/promises";
|
|
|
5
5
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
6
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";
|
|
7
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { buildOutline, isOutlineNode, outlineLeafText } from "document-outline.js";
|
|
8
9
|
//#region src/runtime/abort.ts
|
|
9
10
|
function combineSignals(a, b) {
|
|
10
11
|
const controller = new AbortController();
|
|
@@ -381,7 +382,7 @@ function registerFontsCommand(program) {
|
|
|
381
382
|
}
|
|
382
383
|
//#endregion
|
|
383
384
|
//#region src/commands/formats.ts
|
|
384
|
-
const COMMANDS_NOT_LISTED = "odm-to-pdf, odb-to-csv, odb-to-xlsx, odb-tables, odb-forms, odb-reports, odb-query, odb-render-report, pdf-inspect, from-package, fonts, docx-extras, metadata, set-metadata";
|
|
385
|
+
const COMMANDS_NOT_LISTED = "odm-to-pdf, odb-to-csv, odb-to-xlsx, odb-tables, odb-forms, odb-reports, odb-query, odb-render-report, pdf-inspect, from-package, fonts, docx-extras, metadata, set-metadata, outline";
|
|
385
386
|
function registerFormatsCommand(program) {
|
|
386
387
|
program.command("formats").description("list every source -> target conversion this CLI supports via a <source>-to-<target> command").option("--json", "emit the conversion list as a JSON array instead of a human-readable table", false).action((options) => {
|
|
387
388
|
const { conversions } = createLocalDocumentConverter();
|
|
@@ -956,6 +957,103 @@ function registerOdmCommand(program) {
|
|
|
956
957
|
});
|
|
957
958
|
}
|
|
958
959
|
//#endregion
|
|
960
|
+
//#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
|
+
const INDENT = " ";
|
|
976
|
+
function singleLineText(text) {
|
|
977
|
+
return text.replaceAll(/\s+/g, " ").trim();
|
|
978
|
+
}
|
|
979
|
+
function leafKindLabel(leaf) {
|
|
980
|
+
if ("kind" in leaf) return leaf.kind;
|
|
981
|
+
if ("objectKind" in leaf) return leaf.objectKind;
|
|
982
|
+
return "formula";
|
|
983
|
+
}
|
|
984
|
+
function appendOutlineLines(children, depth, lines) {
|
|
985
|
+
for (const child of children) if (isOutlineNode(child)) {
|
|
986
|
+
lines.push(`${INDENT.repeat(depth)}${singleLineText(child.text)}`);
|
|
987
|
+
appendOutlineLines(child.children, depth + 1, lines);
|
|
988
|
+
} else {
|
|
989
|
+
const text = singleLineText(outlineLeafText(child));
|
|
990
|
+
lines.push(text === "" ? `${INDENT.repeat(depth)}[${leafKindLabel(child)}]` : `${INDENT.repeat(depth)}${text}`);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
function resolveSourceFormat(input, from) {
|
|
994
|
+
if (from !== void 0) {
|
|
995
|
+
if (!isDocumentFormat(from)) return { errorMessage: `unknown --from format '${from}'; expected one of ${KNOWN_DOCUMENT_FORMATS}` };
|
|
996
|
+
return { format: from };
|
|
997
|
+
}
|
|
998
|
+
if (input === "-") return { errorMessage: `cannot infer a source format from stdin; pass --from <format> (${KNOWN_DOCUMENT_FORMATS})` };
|
|
999
|
+
const inferred = inferFormatFromExtension(input);
|
|
1000
|
+
if (inferred === void 0) return { errorMessage: `cannot infer a source format from '${input}'; rename the file with a recognised extension (${KNOWN_DOCUMENT_FORMATS}) or pass --from <format>` };
|
|
1001
|
+
return { format: inferred };
|
|
1002
|
+
}
|
|
1003
|
+
async function runOutline(input, options) {
|
|
1004
|
+
const command = "outline";
|
|
1005
|
+
const source = resolveSourceFormat(input, options.from);
|
|
1006
|
+
if ("errorMessage" in source) {
|
|
1007
|
+
process.stderr.write(`[${command}] ${source.errorMessage}\n`);
|
|
1008
|
+
return 2;
|
|
1009
|
+
}
|
|
1010
|
+
const target = OUTLINE_CONVERSION_TARGET[source.format];
|
|
1011
|
+
const { signal, getAbortReason } = createRuntimeSignal({ timeoutMs: options.timeout });
|
|
1012
|
+
const reporter = createDiagnosticReporter({
|
|
1013
|
+
json: options.json,
|
|
1014
|
+
quiet: options.quiet,
|
|
1015
|
+
command
|
|
1016
|
+
});
|
|
1017
|
+
try {
|
|
1018
|
+
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
|
+
}, {
|
|
1026
|
+
signal,
|
|
1027
|
+
images: createFilesystemMarkdownImageResolver(input === "-" ? "." : dirname(resolve(input)))
|
|
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);
|
|
1032
|
+
if (options.json) {
|
|
1033
|
+
process.stdout.write(`${JSON.stringify(outline, void 0, 2)}\n`);
|
|
1034
|
+
return 0;
|
|
1035
|
+
}
|
|
1036
|
+
const lines = [];
|
|
1037
|
+
appendOutlineLines(outline, 0, lines);
|
|
1038
|
+
if (lines.length > 0) process.stdout.write(`${lines.join("\n")}\n`);
|
|
1039
|
+
return 0;
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
process.stderr.write(`[${command}] ${formatError(error, options.verbose)}\n`);
|
|
1042
|
+
return mapErrorToExit(error, getAbortReason());
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
function registerOutlineCommand(program) {
|
|
1046
|
+
const command = program.command("outline <input>").description(`print a document's outline -- headings, list items, and slide/sheet/page groups as indented text (${KNOWN_DOCUMENT_FORMATS})`);
|
|
1047
|
+
addTimeoutOption(command);
|
|
1048
|
+
command.option("--json", "emit the outline tree as JSON instead of indented text (diagnostics as NDJSON on stderr)", false);
|
|
1049
|
+
addQuietOption(command);
|
|
1050
|
+
addVerboseOption(command);
|
|
1051
|
+
command.option("--from <format>", `source format when it cannot be inferred from the input path, e.g. reading from stdin (${KNOWN_DOCUMENT_FORMATS})`);
|
|
1052
|
+
command.action(async (input, options) => {
|
|
1053
|
+
process.exitCode = await runOutline(input, options);
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
//#endregion
|
|
959
1057
|
//#region src/commands/pdf-inspect.ts
|
|
960
1058
|
function buildItemKindHistogram(items) {
|
|
961
1059
|
const histogram = /* @__PURE__ */ new Map();
|
|
@@ -1103,7 +1201,7 @@ function registerSetMetadataCommand(program) {
|
|
|
1103
1201
|
}
|
|
1104
1202
|
//#endregion
|
|
1105
1203
|
//#region package.json
|
|
1106
|
-
var version = "3.0
|
|
1204
|
+
var version = "3.1.0";
|
|
1107
1205
|
//#endregion
|
|
1108
1206
|
//#region src/program.ts
|
|
1109
1207
|
function createProgram() {
|
|
@@ -1123,6 +1221,7 @@ function createProgram() {
|
|
|
1123
1221
|
registerFontsCommand(program);
|
|
1124
1222
|
registerDocxExtrasCommand(program);
|
|
1125
1223
|
registerMetadataCommand(program);
|
|
1224
|
+
registerOutlineCommand(program);
|
|
1126
1225
|
registerSetMetadataCommand(program);
|
|
1127
1226
|
return program;
|
|
1128
1227
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -4,6 +4,7 @@ let node_path = require("node:path");
|
|
|
4
4
|
let documents_js = require("documents.js");
|
|
5
5
|
let node_fs = require("node:fs");
|
|
6
6
|
let commander = require("commander");
|
|
7
|
+
let document_outline_js = require("document-outline.js");
|
|
7
8
|
//#region src/format.ts
|
|
8
9
|
const EXTENSION_TO_FORMAT = {
|
|
9
10
|
docx: "docx",
|
|
@@ -433,9 +434,9 @@ function registerConversionCommands(program) {
|
|
|
433
434
|
}
|
|
434
435
|
//#endregion
|
|
435
436
|
//#region src/docx-extras-format.ts
|
|
436
|
-
const INDENT$
|
|
437
|
+
const INDENT$2 = " ";
|
|
437
438
|
function indent$1(depth) {
|
|
438
|
-
return INDENT$
|
|
439
|
+
return INDENT$2.repeat(depth);
|
|
439
440
|
}
|
|
440
441
|
function commentLine(comment, position) {
|
|
441
442
|
const author = comment.author ?? "(no author)";
|
|
@@ -557,7 +558,7 @@ function registerFontsCommand(program) {
|
|
|
557
558
|
}
|
|
558
559
|
//#endregion
|
|
559
560
|
//#region src/commands/formats.ts
|
|
560
|
-
const COMMANDS_NOT_LISTED = "odm-to-pdf, odb-to-csv, odb-to-xlsx, odb-tables, odb-forms, odb-reports, odb-query, odb-render-report, pdf-inspect, from-package, fonts, docx-extras, metadata, set-metadata";
|
|
561
|
+
const COMMANDS_NOT_LISTED = "odm-to-pdf, odb-to-csv, odb-to-xlsx, odb-tables, odb-forms, odb-reports, odb-query, odb-render-report, pdf-inspect, from-package, fonts, docx-extras, metadata, set-metadata, outline";
|
|
561
562
|
function registerFormatsCommand(program) {
|
|
562
563
|
program.command("formats").description("list every source -> target conversion this CLI supports via a <source>-to-<target> command").option("--json", "emit the conversion list as a JSON array instead of a human-readable table", false).action((options) => {
|
|
563
564
|
const { conversions } = (0, documents_js.createLocalDocumentConverter)();
|
|
@@ -715,9 +716,9 @@ function registerMetadataCommand(program) {
|
|
|
715
716
|
}
|
|
716
717
|
//#endregion
|
|
717
718
|
//#region src/odb-structure.ts
|
|
718
|
-
const INDENT = " ";
|
|
719
|
+
const INDENT$1 = " ";
|
|
719
720
|
function indent(depth) {
|
|
720
|
-
return INDENT.repeat(depth);
|
|
721
|
+
return INDENT$1.repeat(depth);
|
|
721
722
|
}
|
|
722
723
|
function quoted(value) {
|
|
723
724
|
return `"${value}"`;
|
|
@@ -1288,6 +1289,103 @@ function registerOdmCommand(program) {
|
|
|
1288
1289
|
});
|
|
1289
1290
|
}
|
|
1290
1291
|
//#endregion
|
|
1292
|
+
//#region src/commands/outline.ts
|
|
1293
|
+
const OUTLINE_CONVERSION_TARGET = {
|
|
1294
|
+
docx: "odt",
|
|
1295
|
+
odt: "docx",
|
|
1296
|
+
markdown: "docx",
|
|
1297
|
+
pptx: "odp",
|
|
1298
|
+
odp: "pptx",
|
|
1299
|
+
xlsx: "ods",
|
|
1300
|
+
ods: "xlsx",
|
|
1301
|
+
csv: "xlsx",
|
|
1302
|
+
odg: "odp",
|
|
1303
|
+
svg: "odg",
|
|
1304
|
+
pdf: "docx",
|
|
1305
|
+
odf: "pdf"
|
|
1306
|
+
};
|
|
1307
|
+
const INDENT = " ";
|
|
1308
|
+
function singleLineText(text) {
|
|
1309
|
+
return text.replaceAll(/\s+/g, " ").trim();
|
|
1310
|
+
}
|
|
1311
|
+
function leafKindLabel(leaf) {
|
|
1312
|
+
if ("kind" in leaf) return leaf.kind;
|
|
1313
|
+
if ("objectKind" in leaf) return leaf.objectKind;
|
|
1314
|
+
return "formula";
|
|
1315
|
+
}
|
|
1316
|
+
function appendOutlineLines(children, depth, lines) {
|
|
1317
|
+
for (const child of children) if ((0, document_outline_js.isOutlineNode)(child)) {
|
|
1318
|
+
lines.push(`${INDENT.repeat(depth)}${singleLineText(child.text)}`);
|
|
1319
|
+
appendOutlineLines(child.children, depth + 1, lines);
|
|
1320
|
+
} else {
|
|
1321
|
+
const text = singleLineText((0, document_outline_js.outlineLeafText)(child));
|
|
1322
|
+
lines.push(text === "" ? `${INDENT.repeat(depth)}[${leafKindLabel(child)}]` : `${INDENT.repeat(depth)}${text}`);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
function resolveSourceFormat(input, from) {
|
|
1326
|
+
if (from !== void 0) {
|
|
1327
|
+
if (!isDocumentFormat(from)) return { errorMessage: `unknown --from format '${from}'; expected one of ${KNOWN_DOCUMENT_FORMATS}` };
|
|
1328
|
+
return { format: from };
|
|
1329
|
+
}
|
|
1330
|
+
if (input === "-") return { errorMessage: `cannot infer a source format from stdin; pass --from <format> (${KNOWN_DOCUMENT_FORMATS})` };
|
|
1331
|
+
const inferred = inferFormatFromExtension(input);
|
|
1332
|
+
if (inferred === void 0) return { errorMessage: `cannot infer a source format from '${input}'; rename the file with a recognised extension (${KNOWN_DOCUMENT_FORMATS}) or pass --from <format>` };
|
|
1333
|
+
return { format: inferred };
|
|
1334
|
+
}
|
|
1335
|
+
async function runOutline(input, options) {
|
|
1336
|
+
const command = "outline";
|
|
1337
|
+
const source = resolveSourceFormat(input, options.from);
|
|
1338
|
+
if ("errorMessage" in source) {
|
|
1339
|
+
process.stderr.write(`[${command}] ${source.errorMessage}\n`);
|
|
1340
|
+
return 2;
|
|
1341
|
+
}
|
|
1342
|
+
const target = OUTLINE_CONVERSION_TARGET[source.format];
|
|
1343
|
+
const { signal, getAbortReason } = createRuntimeSignal({ timeoutMs: options.timeout });
|
|
1344
|
+
const reporter = createDiagnosticReporter({
|
|
1345
|
+
json: options.json,
|
|
1346
|
+
quiet: options.quiet,
|
|
1347
|
+
command
|
|
1348
|
+
});
|
|
1349
|
+
try {
|
|
1350
|
+
const inputBytes = await readInput(input, { signal });
|
|
1351
|
+
const result = await (0, documents_js.createLocalDocumentConverter)().convert({
|
|
1352
|
+
source: {
|
|
1353
|
+
format: source.format,
|
|
1354
|
+
bytes: new Uint8Array(inputBytes)
|
|
1355
|
+
},
|
|
1356
|
+
targetFormat: target
|
|
1357
|
+
}, {
|
|
1358
|
+
signal,
|
|
1359
|
+
images: createFilesystemMarkdownImageResolver(input === "-" ? "." : (0, node_path.dirname)((0, node_path.resolve)(input)))
|
|
1360
|
+
});
|
|
1361
|
+
for (const diagnostic of result.diagnostics) reporter.report(diagnostic);
|
|
1362
|
+
if (result.package === void 0) throw new Error(`the ${source.format}-to-${target} conversion produced no intermediate DocumentPackage`);
|
|
1363
|
+
const outline = (0, document_outline_js.buildOutline)(result.package);
|
|
1364
|
+
if (options.json) {
|
|
1365
|
+
process.stdout.write(`${JSON.stringify(outline, void 0, 2)}\n`);
|
|
1366
|
+
return 0;
|
|
1367
|
+
}
|
|
1368
|
+
const lines = [];
|
|
1369
|
+
appendOutlineLines(outline, 0, lines);
|
|
1370
|
+
if (lines.length > 0) process.stdout.write(`${lines.join("\n")}\n`);
|
|
1371
|
+
return 0;
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
process.stderr.write(`[${command}] ${formatError(error, options.verbose)}\n`);
|
|
1374
|
+
return mapErrorToExit(error, getAbortReason());
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
function registerOutlineCommand(program) {
|
|
1378
|
+
const command = program.command("outline <input>").description(`print a document's outline -- headings, list items, and slide/sheet/page groups as indented text (${KNOWN_DOCUMENT_FORMATS})`);
|
|
1379
|
+
addTimeoutOption(command);
|
|
1380
|
+
command.option("--json", "emit the outline tree as JSON instead of indented text (diagnostics as NDJSON on stderr)", false);
|
|
1381
|
+
addQuietOption(command);
|
|
1382
|
+
addVerboseOption(command);
|
|
1383
|
+
command.option("--from <format>", `source format when it cannot be inferred from the input path, e.g. reading from stdin (${KNOWN_DOCUMENT_FORMATS})`);
|
|
1384
|
+
command.action(async (input, options) => {
|
|
1385
|
+
process.exitCode = await runOutline(input, options);
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
//#endregion
|
|
1291
1389
|
//#region src/commands/pdf-inspect.ts
|
|
1292
1390
|
function buildItemKindHistogram(items) {
|
|
1293
1391
|
const histogram = /* @__PURE__ */ new Map();
|
|
@@ -1435,7 +1533,7 @@ function registerSetMetadataCommand(program) {
|
|
|
1435
1533
|
}
|
|
1436
1534
|
//#endregion
|
|
1437
1535
|
//#region package.json
|
|
1438
|
-
var version = "3.0
|
|
1536
|
+
var version = "3.1.0";
|
|
1439
1537
|
//#endregion
|
|
1440
1538
|
//#region src/program.ts
|
|
1441
1539
|
function createProgram() {
|
|
@@ -1455,6 +1553,7 @@ function createProgram() {
|
|
|
1455
1553
|
registerFontsCommand(program);
|
|
1456
1554
|
registerDocxExtrasCommand(program);
|
|
1457
1555
|
registerMetadataCommand(program);
|
|
1556
|
+
registerOutlineCommand(program);
|
|
1458
1557
|
registerSetMetadataCommand(program);
|
|
1459
1558
|
return program;
|
|
1460
1559
|
}
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { basename, dirname, extname, join, resolve } from "node:path";
|
|
|
3
3
|
import { CsvSheetNotFoundError, CsvSheetNotSpecifiedError, HsqldbSqlEvaluationError, HsqldbSqlParseError, HsqldbSqlUnsupportedError, OdbNoEmbeddedDataSourceError, OdbReportNotSpecifiedError, OdbTableNotFoundError, OdbTableNotSpecifiedError, OdbUnsupportedFormatError, OdmUnresolvedSectionError, PdfEncryptedError, PdfParseError, SvgMultiPageNotSpecifiedError, SvgPageNotFoundError, UnrecognizedDocumentSchemaError, UnsupportedFontSourceFormatError, buildCsvText, buildDocumentBytes, buildSvgText, createLocalDocumentConverter, decodeOdbPackage, decodePackage, describeFontFace, 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";
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { Command, InvalidArgumentError } from "commander";
|
|
6
|
+
import { buildOutline, isOutlineNode, outlineLeafText } from "document-outline.js";
|
|
6
7
|
//#region src/format.ts
|
|
7
8
|
const EXTENSION_TO_FORMAT = {
|
|
8
9
|
docx: "docx",
|
|
@@ -432,9 +433,9 @@ function registerConversionCommands(program) {
|
|
|
432
433
|
}
|
|
433
434
|
//#endregion
|
|
434
435
|
//#region src/docx-extras-format.ts
|
|
435
|
-
const INDENT$
|
|
436
|
+
const INDENT$2 = " ";
|
|
436
437
|
function indent$1(depth) {
|
|
437
|
-
return INDENT$
|
|
438
|
+
return INDENT$2.repeat(depth);
|
|
438
439
|
}
|
|
439
440
|
function commentLine(comment, position) {
|
|
440
441
|
const author = comment.author ?? "(no author)";
|
|
@@ -556,7 +557,7 @@ function registerFontsCommand(program) {
|
|
|
556
557
|
}
|
|
557
558
|
//#endregion
|
|
558
559
|
//#region src/commands/formats.ts
|
|
559
|
-
const COMMANDS_NOT_LISTED = "odm-to-pdf, odb-to-csv, odb-to-xlsx, odb-tables, odb-forms, odb-reports, odb-query, odb-render-report, pdf-inspect, from-package, fonts, docx-extras, metadata, set-metadata";
|
|
560
|
+
const COMMANDS_NOT_LISTED = "odm-to-pdf, odb-to-csv, odb-to-xlsx, odb-tables, odb-forms, odb-reports, odb-query, odb-render-report, pdf-inspect, from-package, fonts, docx-extras, metadata, set-metadata, outline";
|
|
560
561
|
function registerFormatsCommand(program) {
|
|
561
562
|
program.command("formats").description("list every source -> target conversion this CLI supports via a <source>-to-<target> command").option("--json", "emit the conversion list as a JSON array instead of a human-readable table", false).action((options) => {
|
|
562
563
|
const { conversions } = createLocalDocumentConverter();
|
|
@@ -714,9 +715,9 @@ function registerMetadataCommand(program) {
|
|
|
714
715
|
}
|
|
715
716
|
//#endregion
|
|
716
717
|
//#region src/odb-structure.ts
|
|
717
|
-
const INDENT = " ";
|
|
718
|
+
const INDENT$1 = " ";
|
|
718
719
|
function indent(depth) {
|
|
719
|
-
return INDENT.repeat(depth);
|
|
720
|
+
return INDENT$1.repeat(depth);
|
|
720
721
|
}
|
|
721
722
|
function quoted(value) {
|
|
722
723
|
return `"${value}"`;
|
|
@@ -1287,6 +1288,103 @@ function registerOdmCommand(program) {
|
|
|
1287
1288
|
});
|
|
1288
1289
|
}
|
|
1289
1290
|
//#endregion
|
|
1291
|
+
//#region src/commands/outline.ts
|
|
1292
|
+
const OUTLINE_CONVERSION_TARGET = {
|
|
1293
|
+
docx: "odt",
|
|
1294
|
+
odt: "docx",
|
|
1295
|
+
markdown: "docx",
|
|
1296
|
+
pptx: "odp",
|
|
1297
|
+
odp: "pptx",
|
|
1298
|
+
xlsx: "ods",
|
|
1299
|
+
ods: "xlsx",
|
|
1300
|
+
csv: "xlsx",
|
|
1301
|
+
odg: "odp",
|
|
1302
|
+
svg: "odg",
|
|
1303
|
+
pdf: "docx",
|
|
1304
|
+
odf: "pdf"
|
|
1305
|
+
};
|
|
1306
|
+
const INDENT = " ";
|
|
1307
|
+
function singleLineText(text) {
|
|
1308
|
+
return text.replaceAll(/\s+/g, " ").trim();
|
|
1309
|
+
}
|
|
1310
|
+
function leafKindLabel(leaf) {
|
|
1311
|
+
if ("kind" in leaf) return leaf.kind;
|
|
1312
|
+
if ("objectKind" in leaf) return leaf.objectKind;
|
|
1313
|
+
return "formula";
|
|
1314
|
+
}
|
|
1315
|
+
function appendOutlineLines(children, depth, lines) {
|
|
1316
|
+
for (const child of children) if (isOutlineNode(child)) {
|
|
1317
|
+
lines.push(`${INDENT.repeat(depth)}${singleLineText(child.text)}`);
|
|
1318
|
+
appendOutlineLines(child.children, depth + 1, lines);
|
|
1319
|
+
} else {
|
|
1320
|
+
const text = singleLineText(outlineLeafText(child));
|
|
1321
|
+
lines.push(text === "" ? `${INDENT.repeat(depth)}[${leafKindLabel(child)}]` : `${INDENT.repeat(depth)}${text}`);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
function resolveSourceFormat(input, from) {
|
|
1325
|
+
if (from !== void 0) {
|
|
1326
|
+
if (!isDocumentFormat(from)) return { errorMessage: `unknown --from format '${from}'; expected one of ${KNOWN_DOCUMENT_FORMATS}` };
|
|
1327
|
+
return { format: from };
|
|
1328
|
+
}
|
|
1329
|
+
if (input === "-") return { errorMessage: `cannot infer a source format from stdin; pass --from <format> (${KNOWN_DOCUMENT_FORMATS})` };
|
|
1330
|
+
const inferred = inferFormatFromExtension(input);
|
|
1331
|
+
if (inferred === void 0) return { errorMessage: `cannot infer a source format from '${input}'; rename the file with a recognised extension (${KNOWN_DOCUMENT_FORMATS}) or pass --from <format>` };
|
|
1332
|
+
return { format: inferred };
|
|
1333
|
+
}
|
|
1334
|
+
async function runOutline(input, options) {
|
|
1335
|
+
const command = "outline";
|
|
1336
|
+
const source = resolveSourceFormat(input, options.from);
|
|
1337
|
+
if ("errorMessage" in source) {
|
|
1338
|
+
process.stderr.write(`[${command}] ${source.errorMessage}\n`);
|
|
1339
|
+
return 2;
|
|
1340
|
+
}
|
|
1341
|
+
const target = OUTLINE_CONVERSION_TARGET[source.format];
|
|
1342
|
+
const { signal, getAbortReason } = createRuntimeSignal({ timeoutMs: options.timeout });
|
|
1343
|
+
const reporter = createDiagnosticReporter({
|
|
1344
|
+
json: options.json,
|
|
1345
|
+
quiet: options.quiet,
|
|
1346
|
+
command
|
|
1347
|
+
});
|
|
1348
|
+
try {
|
|
1349
|
+
const inputBytes = await readInput(input, { signal });
|
|
1350
|
+
const result = await createLocalDocumentConverter().convert({
|
|
1351
|
+
source: {
|
|
1352
|
+
format: source.format,
|
|
1353
|
+
bytes: new Uint8Array(inputBytes)
|
|
1354
|
+
},
|
|
1355
|
+
targetFormat: target
|
|
1356
|
+
}, {
|
|
1357
|
+
signal,
|
|
1358
|
+
images: createFilesystemMarkdownImageResolver(input === "-" ? "." : dirname(resolve(input)))
|
|
1359
|
+
});
|
|
1360
|
+
for (const diagnostic of result.diagnostics) reporter.report(diagnostic);
|
|
1361
|
+
if (result.package === void 0) throw new Error(`the ${source.format}-to-${target} conversion produced no intermediate DocumentPackage`);
|
|
1362
|
+
const outline = buildOutline(result.package);
|
|
1363
|
+
if (options.json) {
|
|
1364
|
+
process.stdout.write(`${JSON.stringify(outline, void 0, 2)}\n`);
|
|
1365
|
+
return 0;
|
|
1366
|
+
}
|
|
1367
|
+
const lines = [];
|
|
1368
|
+
appendOutlineLines(outline, 0, lines);
|
|
1369
|
+
if (lines.length > 0) process.stdout.write(`${lines.join("\n")}\n`);
|
|
1370
|
+
return 0;
|
|
1371
|
+
} catch (error) {
|
|
1372
|
+
process.stderr.write(`[${command}] ${formatError(error, options.verbose)}\n`);
|
|
1373
|
+
return mapErrorToExit(error, getAbortReason());
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
function registerOutlineCommand(program) {
|
|
1377
|
+
const command = program.command("outline <input>").description(`print a document's outline -- headings, list items, and slide/sheet/page groups as indented text (${KNOWN_DOCUMENT_FORMATS})`);
|
|
1378
|
+
addTimeoutOption(command);
|
|
1379
|
+
command.option("--json", "emit the outline tree as JSON instead of indented text (diagnostics as NDJSON on stderr)", false);
|
|
1380
|
+
addQuietOption(command);
|
|
1381
|
+
addVerboseOption(command);
|
|
1382
|
+
command.option("--from <format>", `source format when it cannot be inferred from the input path, e.g. reading from stdin (${KNOWN_DOCUMENT_FORMATS})`);
|
|
1383
|
+
command.action(async (input, options) => {
|
|
1384
|
+
process.exitCode = await runOutline(input, options);
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
//#endregion
|
|
1290
1388
|
//#region src/commands/pdf-inspect.ts
|
|
1291
1389
|
function buildItemKindHistogram(items) {
|
|
1292
1390
|
const histogram = /* @__PURE__ */ new Map();
|
|
@@ -1434,7 +1532,7 @@ function registerSetMetadataCommand(program) {
|
|
|
1434
1532
|
}
|
|
1435
1533
|
//#endregion
|
|
1436
1534
|
//#region package.json
|
|
1437
|
-
var version = "3.0
|
|
1535
|
+
var version = "3.1.0";
|
|
1438
1536
|
//#endregion
|
|
1439
1537
|
//#region src/program.ts
|
|
1440
1538
|
function createProgram() {
|
|
@@ -1454,6 +1552,7 @@ function createProgram() {
|
|
|
1454
1552
|
registerFontsCommand(program);
|
|
1455
1553
|
registerDocxExtrasCommand(program);
|
|
1456
1554
|
registerMetadataCommand(program);
|
|
1555
|
+
registerOutlineCommand(program);
|
|
1457
1556
|
registerSetMetadataCommand(program);
|
|
1458
1557
|
return program;
|
|
1459
1558
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "document-cli",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "CLI and interactive Ink TUI for documents.js: every docx/pptx/odt/odp/ods/odg/odf/pdf/odm/odb/xlsx/csv/svg/markdown conversion, bridge, and editor as a scriptable command or a terminal app.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -88,6 +88,7 @@
|
|
|
88
88
|
"packageManager": "pnpm@11.6.0",
|
|
89
89
|
"dependencies": {
|
|
90
90
|
"commander": "^15.0.0",
|
|
91
|
+
"document-outline.js": "^1.0.0",
|
|
91
92
|
"documents.js": "^3.1.2",
|
|
92
93
|
"ink": "^7.1.1",
|
|
93
94
|
"ink-text-input": "^6.0.0",
|