document-cli 0.0.0 → 1.0.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.
@@ -0,0 +1,32 @@
1
+ import { DocumentFormat } from "documents.js";
2
+ import { Command } from "commander";
3
+ //#region src/format.d.ts
4
+ declare function isDocumentFormat(value: string): value is DocumentFormat;
5
+ declare function inferFormatFromExtension(path: string): DocumentFormat | undefined;
6
+ declare function formatToExtension(format: DocumentFormat): string;
7
+ //#endregion
8
+ //#region src/commands/shared.d.ts
9
+ interface ConversionCommandOptions {
10
+ readonly out?: string;
11
+ readonly timeoutMs?: number;
12
+ readonly json: boolean;
13
+ readonly quiet: boolean;
14
+ readonly verbose: boolean;
15
+ readonly dumpPackage?: string;
16
+ }
17
+ declare function formatError(error: unknown, verbose: boolean): string;
18
+ declare function buildConversionAction(source: DocumentFormat, target: DocumentFormat): (input: string, output: string | undefined, options: ConversionCommandOptions) => Promise<number>;
19
+ //#endregion
20
+ //#region src/program.d.ts
21
+ declare function createProgram(): Command;
22
+ //#endregion
23
+ //#region src/runtime/exit-codes.d.ts
24
+ declare const EXIT_SUCCESS = 0;
25
+ declare const EXIT_INPUT_ERROR = 1;
26
+ declare const EXIT_USAGE_ERROR = 2;
27
+ declare const EXIT_NEEDS_INFO = 3;
28
+ declare const EXIT_TIMEOUT = 124;
29
+ declare const EXIT_INTERRUPTED = 130;
30
+ declare function mapErrorToExit(error: unknown, abortReason: 'interrupt' | 'timeout' | undefined): number;
31
+ //#endregion
32
+ export { type ConversionCommandOptions, EXIT_INPUT_ERROR, EXIT_INTERRUPTED, EXIT_NEEDS_INFO, EXIT_SUCCESS, EXIT_TIMEOUT, EXIT_USAGE_ERROR, buildConversionAction, createProgram, formatError, formatToExtension, inferFormatFromExtension, isDocumentFormat, mapErrorToExit };
package/dist/index.js ADDED
@@ -0,0 +1,651 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { OdbNoEmbeddedDataSourceError, OdbTableNotFoundError, OdbTableNotSpecifiedError, OdbUnsupportedFormatError, OdmUnresolvedSectionError, PdfEncryptedError, PdfParseError, createLocalDocumentConverter, odbToCsv, odbToXlsx, odmToPdf, readOdbTables, readPdf } from "documents.js";
3
+ import { basename, dirname, extname, join } from "node:path";
4
+ import { Command, InvalidArgumentError } from "commander";
5
+ import { decodePackage } from "odf.js";
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ //#region src/format.ts
8
+ const EXTENSION_TO_FORMAT = {
9
+ docx: "docx",
10
+ pptx: "pptx",
11
+ xlsx: "xlsx",
12
+ odt: "odt",
13
+ odp: "odp",
14
+ ods: "ods",
15
+ odg: "odg",
16
+ odf: "odf",
17
+ pdf: "pdf"
18
+ };
19
+ const FORMAT_TO_EXTENSION = {
20
+ docx: "docx",
21
+ pptx: "pptx",
22
+ xlsx: "xlsx",
23
+ odt: "odt",
24
+ odp: "odp",
25
+ ods: "ods",
26
+ odg: "odg",
27
+ odf: "odf",
28
+ pdf: "pdf"
29
+ };
30
+ function isDocumentFormat(value) {
31
+ return value in FORMAT_TO_EXTENSION;
32
+ }
33
+ function inferFormatFromExtension(path) {
34
+ if (path === "-") return;
35
+ const lastSegment = path.split(/[/\\]/).pop() ?? path;
36
+ const dotIndex = lastSegment.lastIndexOf(".");
37
+ if (dotIndex <= 0) return;
38
+ const extension = lastSegment.slice(dotIndex + 1).toLowerCase();
39
+ return EXTENSION_TO_FORMAT[extension];
40
+ }
41
+ function formatToExtension(format) {
42
+ return FORMAT_TO_EXTENSION[format];
43
+ }
44
+ //#endregion
45
+ //#region src/runtime/abort.ts
46
+ function combineSignals(a, b) {
47
+ const controller = new AbortController();
48
+ const forward = (signal) => {
49
+ controller.abort(signal.reason);
50
+ };
51
+ if (a.aborted) forward(a);
52
+ else a.addEventListener("abort", () => forward(a), { once: true });
53
+ if (b.aborted) forward(b);
54
+ else b.addEventListener("abort", () => forward(b), { once: true });
55
+ return controller.signal;
56
+ }
57
+ function createRuntimeSignal(options) {
58
+ let abortReason;
59
+ const interruptController = new AbortController();
60
+ process.on("SIGINT", () => {
61
+ abortReason ??= "interrupt";
62
+ interruptController.abort(/* @__PURE__ */ new Error("Interrupted by SIGINT"));
63
+ });
64
+ const getAbortReason = () => abortReason;
65
+ if (options.timeoutMs === void 0) return {
66
+ signal: interruptController.signal,
67
+ getAbortReason
68
+ };
69
+ const timeoutController = new AbortController();
70
+ setTimeout(() => {
71
+ abortReason ??= "timeout";
72
+ timeoutController.abort(/* @__PURE__ */ new Error(`Timed out after ${options.timeoutMs}ms`));
73
+ }, options.timeoutMs).unref();
74
+ return {
75
+ signal: combineSignals(interruptController.signal, timeoutController.signal),
76
+ getAbortReason
77
+ };
78
+ }
79
+ //#endregion
80
+ //#region src/runtime/diagnostics.ts
81
+ function createDiagnosticReporter(options) {
82
+ const { json, quiet, command } = options;
83
+ return {
84
+ report(diagnostic) {
85
+ if (quiet) return;
86
+ if (json) {
87
+ process.stderr.write(`${JSON.stringify({
88
+ type: "diagnostic",
89
+ command,
90
+ ...diagnostic
91
+ })}\n`);
92
+ return;
93
+ }
94
+ const pageClause = diagnostic.pageIndex === void 0 ? "" : ` (page ${diagnostic.pageIndex})`;
95
+ process.stderr.write(`[${command}] ${diagnostic.severity} ${diagnostic.code}: ${diagnostic.message}${pageClause}\n`);
96
+ },
97
+ summarize(result) {
98
+ if (json) {
99
+ process.stderr.write(`${JSON.stringify({
100
+ type: "result",
101
+ ...result
102
+ })}\n`);
103
+ return;
104
+ }
105
+ if (quiet) return;
106
+ const diagnosticClause = `${result.diagnosticCount} diagnostic${result.diagnosticCount === 1 ? "" : "s"}`;
107
+ process.stderr.write(`[${command}] wrote ${result.bytes} bytes to ${result.output} (${diagnosticClause})\n`);
108
+ }
109
+ };
110
+ }
111
+ function substitutionToDiagnostic(substitution, pageIndex) {
112
+ return {
113
+ severity: "warning",
114
+ code: "win-ansi-substitution",
115
+ message: `Character '${substitution.from}' has no glyph in the standard font; substituted with '${substitution.to}'`,
116
+ pageIndex
117
+ };
118
+ }
119
+ function pdfDiagnosticToDiagnostic(diagnostic) {
120
+ return {
121
+ severity: diagnostic.severity,
122
+ code: diagnostic.code,
123
+ message: diagnostic.message,
124
+ pageIndex: diagnostic.pageIndex
125
+ };
126
+ }
127
+ //#endregion
128
+ //#region src/runtime/exit-codes.ts
129
+ const EXIT_SUCCESS = 0;
130
+ const EXIT_INPUT_ERROR = 1;
131
+ const EXIT_USAGE_ERROR = 2;
132
+ const EXIT_NEEDS_INFO = 3;
133
+ const EXIT_TIMEOUT = 124;
134
+ const EXIT_INTERRUPTED = 130;
135
+ function mapErrorToExit(error, abortReason) {
136
+ if (abortReason === "interrupt") return 130;
137
+ if (abortReason === "timeout") return 124;
138
+ if (error instanceof OdmUnresolvedSectionError || error instanceof OdbTableNotSpecifiedError || error instanceof OdbTableNotFoundError || error instanceof OdbNoEmbeddedDataSourceError || error instanceof OdbUnsupportedFormatError) return 3;
139
+ if (error instanceof PdfEncryptedError || error instanceof PdfParseError) return 1;
140
+ return 1;
141
+ }
142
+ //#endregion
143
+ //#region src/runtime/io.ts
144
+ function isUint8Array(value) {
145
+ return value instanceof Uint8Array;
146
+ }
147
+ async function readStdin(signal) {
148
+ const chunks = [];
149
+ for await (const chunk of process.stdin) {
150
+ if (signal?.aborted === true) throw new Error("Reading from stdin was aborted");
151
+ if (!isUint8Array(chunk)) throw new Error("Unexpected non-buffer chunk read from stdin");
152
+ chunks.push(chunk);
153
+ }
154
+ return new Uint8Array(Buffer.concat(chunks));
155
+ }
156
+ async function readInput(pathOrDash, options) {
157
+ if (pathOrDash === "-") return readStdin(options?.signal);
158
+ const buffer = await readFile(pathOrDash, { signal: options?.signal });
159
+ return new Uint8Array(buffer);
160
+ }
161
+ async function writeOutput(pathOrDash, bytes) {
162
+ if (pathOrDash === "-") {
163
+ await new Promise((resolve, reject) => {
164
+ process.stdout.write(bytes, (error) => {
165
+ if (error) {
166
+ reject(error);
167
+ return;
168
+ }
169
+ resolve();
170
+ });
171
+ });
172
+ return;
173
+ }
174
+ await writeFile(pathOrDash, bytes);
175
+ }
176
+ function resolveDefaultOutputPath(inputPath, targetFormat) {
177
+ const directory = dirname(inputPath);
178
+ const stem = basename(inputPath, extname(inputPath));
179
+ return join(directory, `${stem}.${formatToExtension(targetFormat)}`);
180
+ }
181
+ //#endregion
182
+ //#region src/commands/shared.ts
183
+ function formatError(error, verbose) {
184
+ if (!(error instanceof Error)) return `error: ${String(error)}`;
185
+ const stackClause = verbose && error.stack !== void 0 ? `\n${error.stack}` : "";
186
+ return `error: ${error.message}${stackClause}`;
187
+ }
188
+ function buildConversionAction(source, target) {
189
+ const command = `${source}-to-${target}`;
190
+ return async (input, output, options) => {
191
+ if (output !== void 0 && options.out !== void 0 && output !== options.out) {
192
+ process.stderr.write(`[${command}] conflicting output destinations: positional '${output}' and --out '${options.out}'\n`);
193
+ return 2;
194
+ }
195
+ const resolvedOutput = output ?? options.out ?? (input === "-" ? "-" : resolveDefaultOutputPath(input, target));
196
+ const { signal, getAbortReason } = createRuntimeSignal({ timeoutMs: options.timeoutMs });
197
+ try {
198
+ const inputBytes = await readInput(input, { signal });
199
+ const result = await createLocalDocumentConverter().convert({
200
+ source: {
201
+ format: source,
202
+ bytes: new Uint8Array(inputBytes)
203
+ },
204
+ targetFormat: target
205
+ }, { signal });
206
+ await writeOutput(resolvedOutput, result.document.bytes);
207
+ const reporter = createDiagnosticReporter({
208
+ json: options.json,
209
+ quiet: options.quiet,
210
+ command
211
+ });
212
+ for (const diagnostic of result.diagnostics) reporter.report(diagnostic);
213
+ if (options.dumpPackage !== void 0) if (result.package === void 0) process.stderr.write(`[${command}] this conversion does not produce an intermediate DocumentPackage\n`);
214
+ else await writeFile(options.dumpPackage, JSON.stringify(result.package, void 0, 2));
215
+ reporter.summarize({
216
+ output: resolvedOutput,
217
+ bytes: result.document.bytes.byteLength,
218
+ diagnosticCount: result.diagnostics.length
219
+ });
220
+ return 0;
221
+ } catch (error) {
222
+ process.stderr.write(`${formatError(error, options.verbose)}\n`);
223
+ return mapErrorToExit(error, getAbortReason());
224
+ }
225
+ };
226
+ }
227
+ //#endregion
228
+ //#region src/commands/options.ts
229
+ function addOutOption(command) {
230
+ return command.option("-o, --out <file>", "output file path (defaults to the input path with the target format's extension); use - for stdout");
231
+ }
232
+ function addTimeoutOption(command) {
233
+ return command.option("--timeout <ms>", "abort the run after this many milliseconds", (value) => Number.parseInt(value, 10));
234
+ }
235
+ function addJsonOption(command) {
236
+ return command.option("--json", "emit diagnostics and the result summary as newline-delimited JSON on stderr", false);
237
+ }
238
+ function addQuietOption(command) {
239
+ return command.option("-q, --quiet", "suppress diagnostic and summary output", false);
240
+ }
241
+ function addVerboseOption(command) {
242
+ return command.option("--verbose", "include a full stack trace when the run fails", false);
243
+ }
244
+ function addDumpPackageOption(command) {
245
+ return command.option("--dump-package <file>", "write the intermediate DocumentPackage (content + layout) this conversion built to a JSON file");
246
+ }
247
+ function addConversionFlags(command) {
248
+ addOutOption(command);
249
+ addTimeoutOption(command);
250
+ addJsonOption(command);
251
+ addQuietOption(command);
252
+ addVerboseOption(command);
253
+ return command;
254
+ }
255
+ //#endregion
256
+ //#region src/commands/convert.ts
257
+ function toConversionCommandOptions(options) {
258
+ return {
259
+ out: options.out,
260
+ timeoutMs: options.timeout,
261
+ json: options.json,
262
+ quiet: options.quiet,
263
+ verbose: options.verbose,
264
+ dumpPackage: options.dumpPackage
265
+ };
266
+ }
267
+ const KNOWN_FORMATS = "docx, pptx, xlsx, odt, odp, ods, odg, odf, pdf";
268
+ function resolveGenericTarget(output, options) {
269
+ if (options.to !== void 0) {
270
+ if (!isDocumentFormat(options.to)) return { errorMessage: `unknown --to format '${options.to}'; expected one of ${KNOWN_FORMATS}` };
271
+ return { format: options.to };
272
+ }
273
+ const destination = output ?? options.out;
274
+ if (destination === void 0) return { errorMessage: "cannot infer a target format -- pass an output path with a recognised extension, --out with one, or --to <format>" };
275
+ const inferred = inferFormatFromExtension(destination);
276
+ if (inferred === void 0) return { errorMessage: `cannot infer a target format from '${destination}'; pass --to <format> instead` };
277
+ return { format: inferred };
278
+ }
279
+ async function runGenericConvert(input, output, options) {
280
+ const extension = extname(input).toLowerCase();
281
+ if (extension === ".odm") {
282
+ process.stderr.write("convert: '.odm' master documents are not supported by the generic convert command -- use 'odm-to-pdf' instead\n");
283
+ return 2;
284
+ }
285
+ if (extension === ".odb") {
286
+ process.stderr.write("convert: '.odb' embedded databases are not supported by the generic convert command -- use 'odb-to-csv', 'odb-to-xlsx', or 'odb-tables' instead\n");
287
+ return 2;
288
+ }
289
+ const source = inferFormatFromExtension(input);
290
+ if (source === void 0) {
291
+ process.stderr.write(`convert: cannot infer a source format from '${input}'; rename the file with a recognised extension (${KNOWN_FORMATS}) or use one of the explicit '<source>-to-<target>' commands\n`);
292
+ return 2;
293
+ }
294
+ const target = resolveGenericTarget(output, options);
295
+ if ("errorMessage" in target) {
296
+ process.stderr.write(`convert: ${target.errorMessage}\n`);
297
+ return 2;
298
+ }
299
+ return buildConversionAction(source, target.format)(input, output, toConversionCommandOptions(options));
300
+ }
301
+ function registerConversionCommands(program) {
302
+ const { conversions } = createLocalDocumentConverter();
303
+ for (const { source, target } of conversions) {
304
+ const commandName = `${source}-to-${target}`;
305
+ const command = program.command(`${commandName} <input> [output]`).description(`convert a ${source} document to ${target}`);
306
+ addConversionFlags(command);
307
+ addDumpPackageOption(command);
308
+ command.action(async (input, output, options) => {
309
+ process.exitCode = await buildConversionAction(source, target)(input, output, toConversionCommandOptions(options));
310
+ });
311
+ }
312
+ const generic = program.command("convert <input> [output]").description("convert between any two supported document formats, inferring source/target from file extensions where possible");
313
+ addConversionFlags(generic);
314
+ addDumpPackageOption(generic);
315
+ generic.option("--to <format>", `target format when it cannot be inferred from the output path (${KNOWN_FORMATS})`);
316
+ generic.action(async (input, output, options) => {
317
+ process.exitCode = await runGenericConvert(input, output, options);
318
+ });
319
+ }
320
+ //#endregion
321
+ //#region src/commands/formats.ts
322
+ const COMMANDS_NOT_LISTED = "odm-to-pdf, odb-to-csv, odb-to-xlsx, odb-tables, pdf-inspect";
323
+ function registerFormatsCommand(program) {
324
+ 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) => {
325
+ const { conversions } = createLocalDocumentConverter();
326
+ if (options.json) {
327
+ process.stdout.write(`${JSON.stringify(conversions)}\n`);
328
+ return;
329
+ }
330
+ for (const { source, target } of conversions) process.stdout.write(`${source} -> ${target}\n`);
331
+ process.stdout.write(`\nnot covered by this list, each its own command: ${COMMANDS_NOT_LISTED}\n`);
332
+ });
333
+ }
334
+ //#endregion
335
+ //#region src/commands/odb.ts
336
+ function reportOdbError(command, error, verbose, abortReason) {
337
+ if (error instanceof OdbNoEmbeddedDataSourceError || error instanceof OdbUnsupportedFormatError) {
338
+ process.stderr.write(`[${command}] ${error.message}\n`);
339
+ return mapErrorToExit(error, abortReason);
340
+ }
341
+ process.stderr.write(`[${command}] ${formatError(error, verbose)}\n`);
342
+ return mapErrorToExit(error, abortReason);
343
+ }
344
+ function reportOdbCsvError(command, error, verbose, abortReason) {
345
+ if (error instanceof OdbTableNotSpecifiedError || error instanceof OdbTableNotFoundError) {
346
+ process.stderr.write(`[${command}] ${error.message}\nrun 'odb-tables' first to see the available tables\n`);
347
+ return mapErrorToExit(error, abortReason);
348
+ }
349
+ return reportOdbError(command, error, verbose, abortReason);
350
+ }
351
+ function resolveDefaultCsvOutputPath(inputPath) {
352
+ const directory = dirname(inputPath);
353
+ const stem = basename(inputPath, extname(inputPath));
354
+ return join(directory, `${stem}.csv`);
355
+ }
356
+ async function runOdbToXlsx(input, output, options) {
357
+ const command = "odb-to-xlsx";
358
+ if (output !== void 0 && options.out !== void 0 && output !== options.out) {
359
+ process.stderr.write(`[${command}] conflicting output destinations: positional '${output}' and --out '${options.out}'\n`);
360
+ return 2;
361
+ }
362
+ const resolvedOutput = output ?? options.out ?? (input === "-" ? "-" : resolveDefaultOutputPath(input, "xlsx"));
363
+ const { signal, getAbortReason } = createRuntimeSignal({ timeoutMs: options.timeout });
364
+ try {
365
+ const inputBytes = await readInput(input, { signal });
366
+ const bytes = odbToXlsx(new Uint8Array(inputBytes), { signal });
367
+ await writeOutput(resolvedOutput, bytes);
368
+ createDiagnosticReporter({
369
+ json: options.json,
370
+ quiet: options.quiet,
371
+ command
372
+ }).summarize({
373
+ output: resolvedOutput,
374
+ bytes: bytes.byteLength,
375
+ diagnosticCount: 0
376
+ });
377
+ return 0;
378
+ } catch (error) {
379
+ return reportOdbError(command, error, options.verbose, getAbortReason());
380
+ }
381
+ }
382
+ async function runOdbToCsv(input, output, options) {
383
+ const command = "odb-to-csv";
384
+ if (output !== void 0 && options.out !== void 0 && output !== options.out) {
385
+ process.stderr.write(`[${command}] conflicting output destinations: positional '${output}' and --out '${options.out}'\n`);
386
+ return 2;
387
+ }
388
+ const resolvedOutput = output ?? options.out ?? (input === "-" ? "-" : resolveDefaultCsvOutputPath(input));
389
+ const { signal, getAbortReason } = createRuntimeSignal({ timeoutMs: options.timeout });
390
+ try {
391
+ const inputBytes = await readInput(input, { signal });
392
+ const bytes = odbToCsv(new Uint8Array(inputBytes), {
393
+ signal,
394
+ table: options.table
395
+ });
396
+ await writeOutput(resolvedOutput, bytes);
397
+ createDiagnosticReporter({
398
+ json: options.json,
399
+ quiet: options.quiet,
400
+ command
401
+ }).summarize({
402
+ output: resolvedOutput,
403
+ bytes: bytes.byteLength,
404
+ diagnosticCount: 0
405
+ });
406
+ return 0;
407
+ } catch (error) {
408
+ return reportOdbCsvError(command, error, options.verbose, getAbortReason());
409
+ }
410
+ }
411
+ async function runOdbTables(input, options) {
412
+ const command = "odb-tables";
413
+ const { signal, getAbortReason } = createRuntimeSignal({});
414
+ try {
415
+ const inputBytes = await readInput(input, { signal });
416
+ const pkg = decodePackage(new Uint8Array(inputBytes));
417
+ const tables = readOdbTables(pkg);
418
+ if (options.json) {
419
+ const summary = tables.map((table) => ({
420
+ tableName: table.tableName,
421
+ columns: table.columns,
422
+ rowCount: table.rows.length
423
+ }));
424
+ process.stdout.write(`${JSON.stringify(summary)}\n`);
425
+ return 0;
426
+ }
427
+ for (const table of tables) {
428
+ process.stdout.write(`${table.tableName} (${table.rows.length} row${table.rows.length === 1 ? "" : "s"})\n`);
429
+ for (const column of table.columns) process.stdout.write(` ${column.name}: ${column.type}\n`);
430
+ }
431
+ return 0;
432
+ } catch (error) {
433
+ return reportOdbError(command, error, false, getAbortReason());
434
+ }
435
+ }
436
+ function registerOdbToXlsxCommand(program) {
437
+ const command = program.command("odb-to-xlsx <input> [output]").description("extract every table an embedded .odb database declares into one xlsx workbook, one sheet per table");
438
+ addOutOption(command);
439
+ addTimeoutOption(command);
440
+ addJsonOption(command);
441
+ addQuietOption(command);
442
+ addVerboseOption(command);
443
+ command.action(async (input, output, options) => {
444
+ process.exitCode = await runOdbToXlsx(input, output, options);
445
+ });
446
+ }
447
+ function registerOdbToCsvCommand(program) {
448
+ const command = program.command("odb-to-csv <input> [output]").description("extract exactly one named table from an embedded .odb database as CSV");
449
+ addOutOption(command);
450
+ addTimeoutOption(command);
451
+ addJsonOption(command);
452
+ addQuietOption(command);
453
+ addVerboseOption(command);
454
+ command.option("--table <name>", "the table to export -- required when the .odb declares more than one table");
455
+ command.action(async (input, output, options) => {
456
+ process.exitCode = await runOdbToCsv(input, output, options);
457
+ });
458
+ }
459
+ function registerOdbTablesCommand(program) {
460
+ program.command("odb-tables <input>").description("list every table an embedded .odb database declares, with column names/types and row counts").option("--json", "emit the table list as a JSON array instead of a human-readable report", false).action(async (input, options) => {
461
+ process.exitCode = await runOdbTables(input, options);
462
+ });
463
+ }
464
+ function registerOdbCommands(program) {
465
+ registerOdbToXlsxCommand(program);
466
+ registerOdbToCsvCommand(program);
467
+ registerOdbTablesCommand(program);
468
+ }
469
+ //#endregion
470
+ //#region src/commands/odm.ts
471
+ function collectChapterOverride(value, previous) {
472
+ const separatorIndex = value.indexOf("=");
473
+ if (separatorIndex === -1) throw new InvalidArgumentError(`--chapter must be formatted as <href>=<file>, got '${value}'`);
474
+ const next = new Map(previous);
475
+ next.set(value.slice(0, separatorIndex), value.slice(separatorIndex + 1));
476
+ return next;
477
+ }
478
+ function createResolveSubDocument(overrides, chaptersDir) {
479
+ return (href) => {
480
+ const overridePath = overrides.get(href);
481
+ if (overridePath !== void 0) return new Uint8Array(readFileSync(overridePath));
482
+ if (chaptersDir === void 0) return;
483
+ const candidate = join(chaptersDir, basename(href));
484
+ if (!existsSync(candidate)) return;
485
+ return new Uint8Array(readFileSync(candidate));
486
+ };
487
+ }
488
+ async function runOdmToPdf(input, output, options) {
489
+ const command = "odm-to-pdf";
490
+ if (output !== void 0 && options.out !== void 0 && output !== options.out) {
491
+ process.stderr.write(`[${command}] conflicting output destinations: positional '${output}' and --out '${options.out}'\n`);
492
+ return 2;
493
+ }
494
+ const resolvedOutput = output ?? options.out ?? (input === "-" ? "-" : resolveDefaultOutputPath(input, "pdf"));
495
+ const { signal, getAbortReason } = createRuntimeSignal({ timeoutMs: options.timeout });
496
+ const resolveSubDocument = createResolveSubDocument(options.chapter, options.chaptersDir);
497
+ const reporter = createDiagnosticReporter({
498
+ json: options.json,
499
+ quiet: options.quiet,
500
+ command
501
+ });
502
+ let diagnosticCount = 0;
503
+ try {
504
+ const inputBytes = await readInput(input, { signal });
505
+ const bytes = odmToPdf(new Uint8Array(inputBytes), {
506
+ signal,
507
+ resolveSubDocument,
508
+ onSubstitution: (substitution, context) => {
509
+ diagnosticCount += 1;
510
+ reporter.report(substitutionToDiagnostic(substitution, context.pageIndex));
511
+ }
512
+ });
513
+ await writeOutput(resolvedOutput, bytes);
514
+ reporter.summarize({
515
+ output: resolvedOutput,
516
+ bytes: bytes.byteLength,
517
+ diagnosticCount
518
+ });
519
+ return 0;
520
+ } catch (error) {
521
+ if (error instanceof OdmUnresolvedSectionError) {
522
+ process.stderr.write(`${error.message}\npass --chapters-dir <dir> containing these files, or --chapter <href>=<file>\n`);
523
+ return mapErrorToExit(error, getAbortReason());
524
+ }
525
+ process.stderr.write(`${formatError(error, options.verbose)}\n`);
526
+ return mapErrorToExit(error, getAbortReason());
527
+ }
528
+ }
529
+ function registerOdmCommand(program) {
530
+ const command = program.command("odm-to-pdf <input> [output]").description("convert a .odm master document to pdf, resolving each chapter's external .odt reference via --chapters-dir and/or --chapter");
531
+ addOutOption(command);
532
+ addTimeoutOption(command);
533
+ addJsonOption(command);
534
+ addQuietOption(command);
535
+ addVerboseOption(command);
536
+ command.option("--chapters-dir <dir>", "directory to search for each unresolved chapter href, matched by the href's own basename");
537
+ command.option("--chapter <href>=<file>", "resolve one chapter href to a local file explicitly; repeatable", collectChapterOverride, /* @__PURE__ */ new Map());
538
+ command.action(async (input, output, options) => {
539
+ process.exitCode = await runOdmToPdf(input, output, options);
540
+ });
541
+ }
542
+ //#endregion
543
+ //#region src/commands/pdf-inspect.ts
544
+ function buildItemKindHistogram(items) {
545
+ const histogram = /* @__PURE__ */ new Map();
546
+ for (const item of items) histogram.set(item.kind, (histogram.get(item.kind) ?? 0) + 1);
547
+ return histogram;
548
+ }
549
+ function countImagesByFormat(images) {
550
+ const counts = /* @__PURE__ */ new Map();
551
+ for (const asset of Object.values(images)) counts.set(asset.format, (counts.get(asset.format) ?? 0) + 1);
552
+ return counts;
553
+ }
554
+ function isPresent(entry) {
555
+ return entry[1] !== void 0;
556
+ }
557
+ function formatMetadataValue(value) {
558
+ return typeof value === "string" ? value : value.join(", ");
559
+ }
560
+ async function runPdfInspect(input, options) {
561
+ const command = "pdf-inspect";
562
+ const { signal, getAbortReason } = createRuntimeSignal({});
563
+ const reporter = createDiagnosticReporter({
564
+ json: options.json,
565
+ quiet: false,
566
+ command
567
+ });
568
+ try {
569
+ const inputBytes = await readInput(input, { signal });
570
+ const layout = readPdf(new Uint8Array(inputBytes), {
571
+ signal,
572
+ sink: (diagnostic) => {
573
+ reporter.report(pdfDiagnosticToDiagnostic(diagnostic));
574
+ }
575
+ });
576
+ if (options.full) {
577
+ process.stdout.write(`${JSON.stringify(layout, void 0, 2)}\n`);
578
+ return 0;
579
+ }
580
+ const imagesByFormat = countImagesByFormat(layout.images);
581
+ if (options.json) {
582
+ const summary = {
583
+ pageCount: layout.pages.length,
584
+ pages: layout.pages.map((page) => ({
585
+ widthPt: page.widthPt,
586
+ heightPt: page.heightPt,
587
+ itemKinds: Object.fromEntries(buildItemKindHistogram(page.items))
588
+ })),
589
+ metadata: layout.metadata,
590
+ imagesByFormat: Object.fromEntries(imagesByFormat)
591
+ };
592
+ process.stdout.write(`${JSON.stringify(summary)}\n`);
593
+ return 0;
594
+ }
595
+ process.stdout.write(`${layout.pages.length} page${layout.pages.length === 1 ? "" : "s"}\n`);
596
+ layout.pages.forEach((page, index) => {
597
+ const histogram = buildItemKindHistogram(page.items);
598
+ const histogramText = Array.from(histogram.entries()).map(([kind, count]) => `${kind}=${count}`).join(", ");
599
+ process.stdout.write(` page ${index + 1}: ${page.widthPt}pt x ${page.heightPt}pt${histogramText === "" ? "" : ` (${histogramText})`}\n`);
600
+ });
601
+ const presentMetadata = [
602
+ ["title", layout.metadata.title],
603
+ ["author", layout.metadata.author],
604
+ ["subject", layout.metadata.subject],
605
+ ["keywords", layout.metadata.keywords],
606
+ ["creator", layout.metadata.creator],
607
+ ["producer", layout.metadata.producer],
608
+ ["createdIso", layout.metadata.createdIso],
609
+ ["modifiedIso", layout.metadata.modifiedIso]
610
+ ].filter(isPresent);
611
+ if (presentMetadata.length > 0) {
612
+ process.stdout.write("metadata:\n");
613
+ for (const [key, value] of presentMetadata) process.stdout.write(` ${key}: ${formatMetadataValue(value)}\n`);
614
+ }
615
+ if (imagesByFormat.size > 0) {
616
+ process.stdout.write("images:\n");
617
+ for (const [format, count] of imagesByFormat) process.stdout.write(` ${format}: ${count}\n`);
618
+ }
619
+ return 0;
620
+ } catch (error) {
621
+ process.stderr.write(`${formatError(error, false)}\n`);
622
+ return mapErrorToExit(error, getAbortReason());
623
+ }
624
+ }
625
+ function registerPdfInspectCommand(program) {
626
+ program.command("pdf-inspect <input>").description("inspect a PDF: page count, per-page size and item-kind histogram, document metadata, and embedded image formats").option("--json", "emit the summary as JSON instead of a human-readable report", false).option("--full", "dump the entire parsed LayoutDocument as JSON instead of a summary", false).action(async (input, options) => {
627
+ process.exitCode = await runPdfInspect(input, options);
628
+ });
629
+ }
630
+ //#endregion
631
+ //#region package.json
632
+ var version = "1.0.0";
633
+ //#endregion
634
+ //#region src/program.ts
635
+ function createProgram() {
636
+ const program = new Command("document-cli");
637
+ program.description("every documents.js docx/pptx/odt/odp/ods/odg/odf/odm/odb conversion, bridge, and inspector as a scriptable command");
638
+ program.version(version);
639
+ program.exitOverride((error) => {
640
+ process.exitCode = error.exitCode === 0 ? 0 : 2;
641
+ throw error;
642
+ });
643
+ registerConversionCommands(program);
644
+ registerFormatsCommand(program);
645
+ registerOdmCommand(program);
646
+ registerOdbCommands(program);
647
+ registerPdfInspectCommand(program);
648
+ return program;
649
+ }
650
+ //#endregion
651
+ export { EXIT_INPUT_ERROR, EXIT_INTERRUPTED, EXIT_NEEDS_INFO, EXIT_SUCCESS, EXIT_TIMEOUT, EXIT_USAGE_ERROR, buildConversionAction, createProgram, formatError, formatToExtension, inferFormatFromExtension, isDocumentFormat, mapErrorToExit };