document-cli 1.3.0 → 1.5.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/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { readFile, writeFile } from "node:fs/promises";
2
- import { HsqldbSqlEvaluationError, HsqldbSqlParseError, HsqldbSqlUnsupportedError, OdbNoEmbeddedDataSourceError, OdbReportNotSpecifiedError, OdbTableNotFoundError, OdbTableNotSpecifiedError, OdbUnsupportedFormatError, OdmUnresolvedSectionError, PdfEncryptedError, PdfParseError, UnrecognizedDocumentSchemaError, buildDocxPackage, buildMarkdownText, buildOdgPackage, buildOdpPackage, buildOdsPackage, buildOdtPackage, buildPptxPackage, convertWordprocessingToLayout, createFontMeasurer, createFontRegistry, createLocalDocumentConverter, documentFromJson, documentPackageWithSchema, encodeMarkdownText, encodePackage, evaluateSelect, hsqldbCellDisplayText, odbToCsv, odbToXlsx, odmToPdf, parseSelect, readOdbForms, readOdbInventory, readOdbReportContent, readOdbReports, readOdbTables, readPdf, writePdf } from "documents.js";
2
+ import { HsqldbSqlEvaluationError, HsqldbSqlParseError, HsqldbSqlUnsupportedError, OdbNoEmbeddedDataSourceError, OdbReportNotSpecifiedError, OdbTableNotFoundError, OdbTableNotSpecifiedError, OdbUnsupportedFormatError, OdmUnresolvedSectionError, PdfEncryptedError, PdfParseError, UnrecognizedDocumentSchemaError, buildDocxPackage, buildMarkdownText, buildOdgPackage, buildOdpPackage, buildOdsPackage, buildOdtPackage, buildPptxPackage, convertWordprocessingToLayout, createFontMeasurer, createFontRegistry, createLocalDocumentConverter, decodeMarkdownText, decodePackage, documentFromJson, documentPackageWithSchema, encodeMarkdownText, encodePackage, evaluateSelect, extractSourceFonts, hsqldbCellDisplayText, layoutDocumentWithSchema, odbToCsv, odbToXlsx, odmToPdf, parseSelect, readDocxContent, readDocxExtras, readMarkdownContent, readOdbForms, readOdbInventory, readOdbReportContent, readOdbReports, readOdbTables, readOdfFormulaContent, readOdgContent, readOdpContent, readOdsContent, readOdtContent, readPdf, readPptxContent, writePdf, xlsxToPdf } from "documents.js";
3
3
  import { basename, dirname, extname, join } from "node:path";
4
4
  import { Command, InvalidArgumentError } from "commander";
5
- import { decodePackage, encodePackage as encodePackage$1 } from "odf.js";
5
+ import { decodePackage as decodePackage$1, encodePackage as encodePackage$1 } from "odf.js";
6
6
  import { existsSync, readFileSync } from "node:fs";
7
7
  //#region src/format.ts
8
8
  const EXTENSION_TO_FORMAT = {
@@ -532,8 +532,158 @@ function registerConversionCommands(program) {
532
532
  });
533
533
  }
534
534
  //#endregion
535
+ //#region src/docx-extras-format.ts
536
+ const INDENT$1 = " ";
537
+ function indent$1(depth) {
538
+ return INDENT$1.repeat(depth);
539
+ }
540
+ function commentLine(comment, position) {
541
+ const author = comment.author ?? "(no author)";
542
+ return `${indent$1(1)}[${position}] ${author}: ${comment.text}`;
543
+ }
544
+ function footnoteLine(footnote, position) {
545
+ const typeSuffix = footnote.type === void 0 ? "" : ` (${footnote.type})`;
546
+ return `${indent$1(1)}[${position}]${typeSuffix} ${footnote.text}`;
547
+ }
548
+ function commentsSection(comments) {
549
+ if (comments.length === 0) return [];
550
+ return ["comments", ...comments.map((comment, index) => commentLine(comment, index + 1))];
551
+ }
552
+ function footnotesSection(footnotes) {
553
+ if (footnotes.length === 0) return [];
554
+ return ["footnotes", ...footnotes.map((footnote, index) => footnoteLine(footnote, index + 1))];
555
+ }
556
+ function headerOrFooterSection(label, values) {
557
+ if (values.length === 0) return [];
558
+ return [label, ...values.map((text, index) => `${indent$1(1)}[${index + 1}] ${text}`)];
559
+ }
560
+ function numberingLevelLine(ilvl, level) {
561
+ const restartSuffix = level.restart === void 0 ? "" : `, restarts at level ${level.restart}`;
562
+ return `${indent$1(2)}level ${ilvl}: ${level.format} ${JSON.stringify(level.text)} starting at ${level.startAt}${restartSuffix}`;
563
+ }
564
+ function numberingSection(numbering) {
565
+ const numIds = Object.keys(numbering);
566
+ if (numIds.length === 0) return [];
567
+ const lines = ["numbering"];
568
+ for (const numId of numIds) {
569
+ const definition = numbering[numId];
570
+ if (definition === void 0) continue;
571
+ lines.push(`${indent$1(1)}numId ${numId}`);
572
+ const ilvls = Object.keys(definition.levels).sort((a, b) => Number(a) - Number(b));
573
+ for (const ilvl of ilvls) {
574
+ const level = definition.levels[ilvl];
575
+ if (level === void 0) continue;
576
+ lines.push(numberingLevelLine(ilvl, level));
577
+ }
578
+ }
579
+ return lines;
580
+ }
581
+ function formatDocxExtrasLines(extras) {
582
+ const nonEmptySections = [
583
+ commentsSection(extras.comments),
584
+ footnotesSection(extras.footnotes),
585
+ headerOrFooterSection("headers", extras.headers),
586
+ headerOrFooterSection("footers", extras.footers),
587
+ numberingSection(extras.numbering)
588
+ ].filter((section) => section.length > 0);
589
+ if (nonEmptySections.length === 0) return ["This document carries no comments, footnotes, headers, footers, or numbering definitions."];
590
+ return nonEmptySections.flatMap((section, index) => index === 0 ? section : ["", ...section]);
591
+ }
592
+ //#endregion
593
+ //#region src/commands/docx-extras.ts
594
+ async function runDocxExtras(input, options) {
595
+ const command = "docx-extras";
596
+ const { signal, getAbortReason } = createRuntimeSignal({});
597
+ try {
598
+ const inputBytes = await readInput(input, { signal });
599
+ const pkg = decodePackage(new Uint8Array(inputBytes));
600
+ const extras = readDocxExtras(pkg);
601
+ if (options.json) {
602
+ process.stdout.write(`${JSON.stringify(extras)}\n`);
603
+ return 0;
604
+ }
605
+ for (const line of formatDocxExtrasLines(extras)) process.stdout.write(`${line}\n`);
606
+ return 0;
607
+ } catch (error) {
608
+ process.stderr.write(`[${command}] ${formatError(error, false)}\n`);
609
+ return mapErrorToExit(error, getAbortReason());
610
+ }
611
+ }
612
+ function registerDocxExtrasCommand(program) {
613
+ program.command("docx-extras <input>").description("print a docx's own comments, footnotes, headers, footers, and numbering definitions -- data readDocxContent's ContentDocument cannot carry").option("--json", "emit the raw DocxExtras object as JSON instead of a human-readable report", false).action(async (input, options) => {
614
+ process.exitCode = await runDocxExtras(input, options);
615
+ });
616
+ }
617
+ //#endregion
618
+ //#region src/commands/fonts.ts
619
+ const FONT_SOURCE_FORMATS = {
620
+ docx: true,
621
+ pptx: true,
622
+ odt: true,
623
+ odp: true,
624
+ ods: true,
625
+ odg: true
626
+ };
627
+ function isFontSourceFormat(format) {
628
+ return format in FONT_SOURCE_FORMATS;
629
+ }
630
+ function resolveFontSourcePackage(format, bytes) {
631
+ if (format === "docx" || format === "pptx") return {
632
+ kind: format,
633
+ package: decodePackage(bytes)
634
+ };
635
+ return {
636
+ kind: "odf",
637
+ package: decodePackage$1(bytes)
638
+ };
639
+ }
640
+ async function runFonts(input, options) {
641
+ const command = "fonts";
642
+ const { signal, getAbortReason } = createRuntimeSignal({});
643
+ try {
644
+ const format = inferFormatFromExtension(input);
645
+ if (format === void 0) {
646
+ process.stderr.write(`[${command}] cannot infer a document format from '${input}'; expected one of docx, pptx, odt, odp, ods, odg\n`);
647
+ return 2;
648
+ }
649
+ if (!isFontSourceFormat(format)) {
650
+ process.stderr.write(`[${command}] '${format}' documents carry no source-embedded font faces this command can extract; expected one of docx, pptx, odt, odp, ods, odg\n`);
651
+ return 2;
652
+ }
653
+ const inputBytes = await readInput(input, { signal });
654
+ const source = resolveFontSourcePackage(format, new Uint8Array(inputBytes));
655
+ const summaries = extractSourceFonts(source).map((face) => ({
656
+ family: face.family,
657
+ bold: face.bold,
658
+ italic: face.italic,
659
+ byteLength: face.bytes.length
660
+ }));
661
+ if (options.json) {
662
+ process.stdout.write(`${JSON.stringify(summaries)}\n`);
663
+ return 0;
664
+ }
665
+ if (summaries.length === 0) {
666
+ process.stdout.write("This document embeds no source fonts.\n");
667
+ return 0;
668
+ }
669
+ for (const face of summaries) {
670
+ const style = [face.bold ? "bold" : void 0, face.italic ? "italic" : void 0].filter((value) => value !== void 0).join(" ");
671
+ process.stdout.write(`${face.family}${style === "" ? "" : ` (${style})`} -- ${face.byteLength} bytes\n`);
672
+ }
673
+ return 0;
674
+ } catch (error) {
675
+ process.stderr.write(`${formatError(error, false)}\n`);
676
+ return mapErrorToExit(error, getAbortReason());
677
+ }
678
+ }
679
+ function registerFontsCommand(program) {
680
+ program.command("fonts <input>").description("list every source-embedded font face a docx/pptx/odt/odp/ods/odg document carries (family, weight/style, byte length)").option("--json", "emit the face list as a JSON array instead of a human-readable report", false).action(async (input, options) => {
681
+ process.exitCode = await runFonts(input, options);
682
+ });
683
+ }
684
+ //#endregion
535
685
  //#region src/commands/formats.ts
536
- const COMMANDS_NOT_LISTED = "odm-to-pdf, odb-to-csv, odb-to-xlsx, odb-tables, odb-forms, odb-reports, pdf-inspect, from-package";
686
+ const COMMANDS_NOT_LISTED = "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";
537
687
  function registerFormatsCommand(program) {
538
688
  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) => {
539
689
  const { conversions } = createLocalDocumentConverter();
@@ -626,6 +776,78 @@ function registerFromPackageCommand(program) {
626
776
  });
627
777
  }
628
778
  //#endregion
779
+ //#region src/runtime/metadata-format.ts
780
+ const METADATA_KEYS = [
781
+ "title",
782
+ "author",
783
+ "subject",
784
+ "keywords",
785
+ "creator",
786
+ "producer",
787
+ "createdIso",
788
+ "modifiedIso"
789
+ ];
790
+ function isPresent(entry) {
791
+ return entry[1] !== void 0;
792
+ }
793
+ function formatMetadataValue(value) {
794
+ return typeof value === "string" ? value : value.join(", ");
795
+ }
796
+ function presentMetadataEntries(metadata) {
797
+ return METADATA_KEYS.map((key) => [key, metadata[key]]).filter(isPresent);
798
+ }
799
+ function formatMetadataLines(metadata) {
800
+ return presentMetadataEntries(metadata).map(([key, value]) => `${key}: ${formatMetadataValue(value)}`);
801
+ }
802
+ //#endregion
803
+ //#region src/commands/metadata.ts
804
+ function readMetadataForFormat(format, bytes, signal) {
805
+ switch (format) {
806
+ case "docx": return readDocxContent(decodePackage(bytes)).metadata;
807
+ case "pptx": return readPptxContent(decodePackage(bytes)).metadata;
808
+ case "odt": return readOdtContent(decodePackage$1(bytes)).metadata;
809
+ case "odp": return readOdpContent(decodePackage$1(bytes)).metadata;
810
+ case "ods": return readOdsContent(decodePackage$1(bytes)).metadata;
811
+ case "odg": return readOdgContent(decodePackage$1(bytes)).metadata;
812
+ case "odf": return readOdfFormulaContent(decodePackage$1(bytes)).metadata;
813
+ case "markdown": return readMarkdownContent(decodeMarkdownText(bytes)).metadata;
814
+ case "pdf": return readPdf(bytes, { signal }).metadata;
815
+ case "xlsx": return readPdf(xlsxToPdf(bytes, { signal }), { signal }).metadata;
816
+ }
817
+ }
818
+ async function runMetadata(input, options) {
819
+ const command = "metadata";
820
+ const source = inferFormatFromExtension(input);
821
+ if (source === void 0) {
822
+ process.stderr.write(`[${command}] cannot infer a source format from '${input}'; rename the file with a recognised extension (${KNOWN_DOCUMENT_FORMATS})\n`);
823
+ return 2;
824
+ }
825
+ const { signal, getAbortReason } = createRuntimeSignal({});
826
+ try {
827
+ const inputBytes = await readInput(input, { signal });
828
+ const metadata = readMetadataForFormat(source, new Uint8Array(inputBytes), signal);
829
+ if (options.json) {
830
+ process.stdout.write(`${JSON.stringify(metadata)}\n`);
831
+ return 0;
832
+ }
833
+ const lines = formatMetadataLines(metadata);
834
+ if (lines.length === 0) {
835
+ process.stdout.write("This document carries no metadata.\n");
836
+ return 0;
837
+ }
838
+ for (const line of lines) process.stdout.write(`${line}\n`);
839
+ return 0;
840
+ } catch (error) {
841
+ process.stderr.write(`[${command}] ${formatError(error, false)}\n`);
842
+ return mapErrorToExit(error, getAbortReason());
843
+ }
844
+ }
845
+ function registerMetadataCommand(program) {
846
+ program.command("metadata <input>").description(`print a document's own title/author/subject/keywords/creator/producer/created/modified metadata (${KNOWN_DOCUMENT_FORMATS})`).option("--json", "emit the metadata as a JSON object instead of a human-readable report", false).action(async (input, options) => {
847
+ process.exitCode = await runMetadata(input, options);
848
+ });
849
+ }
850
+ //#endregion
629
851
  //#region src/odb-structure.ts
630
852
  const INDENT = " ";
631
853
  function indent(depth) {
@@ -867,7 +1089,7 @@ async function runOdbTables(input, options) {
867
1089
  const { signal, getAbortReason } = createRuntimeSignal({});
868
1090
  try {
869
1091
  const inputBytes = await readInput(input, { signal });
870
- const pkg = decodePackage(new Uint8Array(inputBytes));
1092
+ const pkg = decodePackage$1(new Uint8Array(inputBytes));
871
1093
  const tables = readOdbTables(pkg);
872
1094
  if (options.json) {
873
1095
  const summary = tables.map((table) => ({
@@ -912,7 +1134,7 @@ async function runOdbQuery(input, options) {
912
1134
  const { signal, getAbortReason } = createRuntimeSignal({});
913
1135
  try {
914
1136
  const inputBytes = await readInput(input, { signal });
915
- const pkg = decodePackage(new Uint8Array(inputBytes));
1137
+ const pkg = decodePackage$1(new Uint8Array(inputBytes));
916
1138
  const resolved = resolveQuerySql(pkg, options);
917
1139
  if ("errorMessage" in resolved) {
918
1140
  process.stderr.write(`[${command}] ${resolved.errorMessage}\n`);
@@ -934,7 +1156,7 @@ async function runOdbForms(input, options) {
934
1156
  const { signal, getAbortReason } = createRuntimeSignal({});
935
1157
  try {
936
1158
  const inputBytes = await readInput(input, { signal });
937
- const forms = readOdbForms(decodePackage(new Uint8Array(inputBytes)));
1159
+ const forms = readOdbForms(decodePackage$1(new Uint8Array(inputBytes)));
938
1160
  if (options.json) {
939
1161
  process.stdout.write(`${JSON.stringify(forms.map((form) => odbFormSummary(form)))}\n`);
940
1162
  return 0;
@@ -957,7 +1179,7 @@ async function runOdbReports(input, options) {
957
1179
  const { signal, getAbortReason } = createRuntimeSignal({});
958
1180
  try {
959
1181
  const inputBytes = await readInput(input, { signal });
960
- const reports = readOdbReports(decodePackage(new Uint8Array(inputBytes)));
1182
+ const reports = readOdbReports(decodePackage$1(new Uint8Array(inputBytes)));
961
1183
  if (options.json) {
962
1184
  process.stdout.write(`${JSON.stringify(reports)}\n`);
963
1185
  return 0;
@@ -1041,7 +1263,7 @@ async function runOdbRenderReport(input, output, options) {
1041
1263
  try {
1042
1264
  const inputBytes = await readInput(input, { signal });
1043
1265
  const fonts = await loadProvidedFonts(options.fontFile ?? [], { signal });
1044
- const pkg = decodePackage(new Uint8Array(inputBytes));
1266
+ const pkg = decodePackage$1(new Uint8Array(inputBytes));
1045
1267
  const bytes = renderOdbReportBytes(readOdbReportContent(pkg, { report: options.report }), targetFormat, {
1046
1268
  fonts,
1047
1269
  signal,
@@ -1229,12 +1451,6 @@ function countImagesByFormat(images) {
1229
1451
  for (const asset of Object.values(images)) counts.set(asset.format, (counts.get(asset.format) ?? 0) + 1);
1230
1452
  return counts;
1231
1453
  }
1232
- function isPresent(entry) {
1233
- return entry[1] !== void 0;
1234
- }
1235
- function formatMetadataValue(value) {
1236
- return typeof value === "string" ? value : value.join(", ");
1237
- }
1238
1454
  async function runPdfInspect(input, options) {
1239
1455
  const command = "pdf-inspect";
1240
1456
  const { signal, getAbortReason } = createRuntimeSignal({});
@@ -1252,7 +1468,7 @@ async function runPdfInspect(input, options) {
1252
1468
  }
1253
1469
  });
1254
1470
  if (options.full) {
1255
- process.stdout.write(`${JSON.stringify(layout, void 0, 2)}\n`);
1471
+ process.stdout.write(`${JSON.stringify(layoutDocumentWithSchema(layout), void 0, 2)}\n`);
1256
1472
  return 0;
1257
1473
  }
1258
1474
  const imagesByFormat = countImagesByFormat(layout.images);
@@ -1276,19 +1492,9 @@ async function runPdfInspect(input, options) {
1276
1492
  const histogramText = Array.from(histogram.entries()).map(([kind, count]) => `${kind}=${count}`).join(", ");
1277
1493
  process.stdout.write(` page ${index + 1}: ${page.widthPt}pt x ${page.heightPt}pt${histogramText === "" ? "" : ` (${histogramText})`}\n`);
1278
1494
  });
1279
- const presentMetadata = [
1280
- ["title", layout.metadata.title],
1281
- ["author", layout.metadata.author],
1282
- ["subject", layout.metadata.subject],
1283
- ["keywords", layout.metadata.keywords],
1284
- ["creator", layout.metadata.creator],
1285
- ["producer", layout.metadata.producer],
1286
- ["createdIso", layout.metadata.createdIso],
1287
- ["modifiedIso", layout.metadata.modifiedIso]
1288
- ].filter(isPresent);
1289
- if (presentMetadata.length > 0) {
1495
+ if (presentMetadataEntries(layout.metadata).length > 0) {
1290
1496
  process.stdout.write("metadata:\n");
1291
- for (const [key, value] of presentMetadata) process.stdout.write(` ${key}: ${formatMetadataValue(value)}\n`);
1497
+ for (const line of formatMetadataLines(layout.metadata)) process.stdout.write(` ${line}\n`);
1292
1498
  }
1293
1499
  if (imagesByFormat.size > 0) {
1294
1500
  process.stdout.write("images:\n");
@@ -1306,8 +1512,155 @@ function registerPdfInspectCommand(program) {
1306
1512
  });
1307
1513
  }
1308
1514
  //#endregion
1515
+ //#region src/commands/set-metadata.ts
1516
+ const REBUILD_FORMATS = {
1517
+ docx: true,
1518
+ pptx: true,
1519
+ odt: true,
1520
+ odp: true,
1521
+ ods: true,
1522
+ odg: true,
1523
+ markdown: true
1524
+ };
1525
+ function isRebuildFormat(format) {
1526
+ return format in REBUILD_FORMATS;
1527
+ }
1528
+ function readContentForFormat(format, bytes) {
1529
+ switch (format) {
1530
+ case "docx": return readDocxContent(decodePackage(bytes));
1531
+ case "pptx": return readPptxContent(decodePackage(bytes));
1532
+ case "odt": return readOdtContent(decodePackage$1(bytes));
1533
+ case "odp": return readOdpContent(decodePackage$1(bytes));
1534
+ case "ods": return readOdsContent(decodePackage$1(bytes));
1535
+ case "odg": return readOdgContent(decodePackage$1(bytes));
1536
+ case "markdown": return readMarkdownContent(decodeMarkdownText(bytes));
1537
+ }
1538
+ }
1539
+ function buildBytesForRebuildFormat(format, content) {
1540
+ switch (format) {
1541
+ case "docx": return encodePackage(buildDocxPackage(content));
1542
+ case "pptx": return encodePackage(buildPptxPackage(content));
1543
+ case "odt": return encodePackage$1(buildOdtPackage(content));
1544
+ case "odp": return encodePackage$1(buildOdpPackage(content));
1545
+ case "ods": return encodePackage$1(buildOdsPackage(content));
1546
+ case "odg": return encodePackage$1(buildOdgPackage(content));
1547
+ case "markdown": return encodeMarkdownText(buildMarkdownText(content));
1548
+ }
1549
+ }
1550
+ function mergeMetadata(current, overrides) {
1551
+ return {
1552
+ ...current,
1553
+ ...overrides.title !== void 0 ? { title: overrides.title } : {},
1554
+ ...overrides.author !== void 0 ? { author: overrides.author } : {},
1555
+ ...overrides.subject !== void 0 ? { subject: overrides.subject } : {},
1556
+ ...overrides.keywords !== void 0 ? { keywords: overrides.keywords } : {}
1557
+ };
1558
+ }
1559
+ function parseKeywords(csv) {
1560
+ return csv.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
1561
+ }
1562
+ function classifyWritePath(source, target) {
1563
+ if (source === "pdf" && target === "pdf") return { kind: "pdf" };
1564
+ if (target === "xlsx" || source === "xlsx") return { errorMessage: "'xlsx' is not a supported set-metadata source or target -- documents.js does not re-export a ContentDocument-to-xlsx builder or a readXlsxContent from its own public surface (see that package's own README, Architecture section); convert with 'xlsx-to-ods'/'ods-to-xlsx' first, then set metadata on the ods" };
1565
+ if (target === "odf" || source === "odf") return { errorMessage: "'odf' (a standalone formula document) is not a supported set-metadata source or target -- it has no write path back out at all" };
1566
+ if (!isRebuildFormat(source) || !isRebuildFormat(target)) return { errorMessage: `set-metadata only patches metadata in place; it does not convert format -- source ('${source}') and target ('${target}') must be the same format (or both 'pdf'). Run 'convert'/'from-package' first if you need a different target format.` };
1567
+ if (source !== target) return { errorMessage: `set-metadata only patches metadata in place; it does not convert format -- source ('${source}') and target ('${target}') must be the same format. Run 'convert'/'from-package' first if you need a different target format.` };
1568
+ return {
1569
+ kind: "rebuild",
1570
+ format: source
1571
+ };
1572
+ }
1573
+ async function runSetMetadata(input, output, options) {
1574
+ const command = "set-metadata";
1575
+ if (output !== void 0 && options.out !== void 0 && output !== options.out) {
1576
+ process.stderr.write(`[${command}] conflicting output destinations: positional '${output}' and --out '${options.out}'\n`);
1577
+ return 2;
1578
+ }
1579
+ const target = resolveTargetFormat(output, options.out, options.to);
1580
+ if ("errorMessage" in target) {
1581
+ process.stderr.write(`[${command}] ${target.errorMessage}\n`);
1582
+ return 2;
1583
+ }
1584
+ const source = inferFormatFromExtension(input);
1585
+ if (source === void 0) {
1586
+ process.stderr.write(`[${command}] cannot infer a source format from '${input}'; rename the file with a recognised extension (${KNOWN_DOCUMENT_FORMATS})\n`);
1587
+ return 2;
1588
+ }
1589
+ const writePath = classifyWritePath(source, target.format);
1590
+ if ("errorMessage" in writePath) {
1591
+ process.stderr.write(`[${command}] ${writePath.errorMessage}\n`);
1592
+ return 2;
1593
+ }
1594
+ const overrides = {
1595
+ title: options.setTitle,
1596
+ author: options.setAuthor,
1597
+ subject: options.setSubject,
1598
+ keywords: options.setKeywords === void 0 ? void 0 : parseKeywords(options.setKeywords)
1599
+ };
1600
+ const resolvedOutput = output ?? options.out ?? (input === "-" ? "-" : resolveDefaultOutputPath(input, target.format));
1601
+ const { signal, getAbortReason } = createRuntimeSignal({ timeoutMs: options.timeout });
1602
+ try {
1603
+ const inputBytes = await readInput(input, { signal });
1604
+ const bytes = writePath.kind === "pdf" ? (() => {
1605
+ const layout = readPdf(new Uint8Array(inputBytes), { signal });
1606
+ const patched = {
1607
+ ...layout,
1608
+ metadata: mergeMetadata(layout.metadata, overrides)
1609
+ };
1610
+ return writePdf(patched, { signal });
1611
+ })() : (() => {
1612
+ const content = readContentForFormat(writePath.format, new Uint8Array(inputBytes));
1613
+ const nextContent = {
1614
+ ...content,
1615
+ metadata: mergeMetadata(content.metadata, overrides)
1616
+ };
1617
+ return buildBytesForRebuildFormat(writePath.format, nextContent);
1618
+ })();
1619
+ await writeOutput(resolvedOutput, bytes);
1620
+ createDiagnosticReporter({
1621
+ json: options.json,
1622
+ quiet: options.quiet,
1623
+ command
1624
+ }).summarize({
1625
+ output: resolvedOutput,
1626
+ bytes: bytes.byteLength,
1627
+ diagnosticCount: 0
1628
+ });
1629
+ return 0;
1630
+ } catch (error) {
1631
+ process.stderr.write(`${formatError(error, options.verbose)}\n`);
1632
+ return mapErrorToExit(error, getAbortReason());
1633
+ }
1634
+ }
1635
+ function registerSetMetadataCommand(program) {
1636
+ 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", [
1637
+ "",
1638
+ "Two write paths: a pdf source/target patches the metadata directly on the parsed PDF (writePdf), with no layout engine",
1639
+ "involved at all -- genuinely lossless for everything else on the page. Every other supported format (docx, pptx, odt,",
1640
+ "odp, ods, odg, markdown) rebuilds a fresh package from that format's own ContentDocument -- for docx specifically,",
1641
+ "this is LOSSY: it drops anything docx-extras covers (comments, footnotes, headers/footers, numbering definitions),",
1642
+ "since buildDocxPackage builds a fresh package from the ContentDocument alone, with no way to carry that data through.",
1643
+ "",
1644
+ "set-metadata does not convert format -- source and target must match. Run convert/from-package first, then",
1645
+ "set-metadata on the result, if you need a different target format."
1646
+ ].join("\n"));
1647
+ addOutOption(command);
1648
+ addTimeoutOption(command);
1649
+ addJsonOption(command);
1650
+ addQuietOption(command);
1651
+ addVerboseOption(command);
1652
+ command.option("--to <format>", `target format when it cannot be inferred from the output path (${KNOWN_DOCUMENT_FORMATS})`);
1653
+ command.option("--set-title <text>", "set the title field");
1654
+ command.option("--set-author <text>", "set the author field");
1655
+ command.option("--set-subject <text>", "set the subject field");
1656
+ command.option("--set-keywords <csv>", "set the keywords field, comma-separated (trimmed, empty entries dropped)");
1657
+ command.action(async (input, output, options) => {
1658
+ process.exitCode = await runSetMetadata(input, output, options);
1659
+ });
1660
+ }
1661
+ //#endregion
1309
1662
  //#region package.json
1310
- var version = "1.3.0";
1663
+ var version = "1.5.0";
1311
1664
  //#endregion
1312
1665
  //#region src/program.ts
1313
1666
  function createProgram() {
@@ -1324,6 +1677,10 @@ function createProgram() {
1324
1677
  registerOdmCommand(program);
1325
1678
  registerOdbCommands(program);
1326
1679
  registerPdfInspectCommand(program);
1680
+ registerFontsCommand(program);
1681
+ registerDocxExtrasCommand(program);
1682
+ registerMetadataCommand(program);
1683
+ registerSetMetadataCommand(program);
1327
1684
  return program;
1328
1685
  }
1329
1686
  //#endregion
@@ -247,6 +247,88 @@ function resolveDefaultOutputPath(inputPath, targetFormat) {
247
247
  return join(directory, `${stem}.${formatToExtension(targetFormat)}`);
248
248
  }
249
249
  //#endregion
250
+ //#region src/docx-extras-format.ts
251
+ const INDENT$1 = " ";
252
+ function indent$1(depth) {
253
+ return INDENT$1.repeat(depth);
254
+ }
255
+ function commentLine(comment, position) {
256
+ const author = comment.author ?? "(no author)";
257
+ return `${indent$1(1)}[${position}] ${author}: ${comment.text}`;
258
+ }
259
+ function footnoteLine(footnote, position) {
260
+ const typeSuffix = footnote.type === void 0 ? "" : ` (${footnote.type})`;
261
+ return `${indent$1(1)}[${position}]${typeSuffix} ${footnote.text}`;
262
+ }
263
+ function commentsSection(comments) {
264
+ if (comments.length === 0) return [];
265
+ return ["comments", ...comments.map((comment, index) => commentLine(comment, index + 1))];
266
+ }
267
+ function footnotesSection(footnotes) {
268
+ if (footnotes.length === 0) return [];
269
+ return ["footnotes", ...footnotes.map((footnote, index) => footnoteLine(footnote, index + 1))];
270
+ }
271
+ function headerOrFooterSection(label, values) {
272
+ if (values.length === 0) return [];
273
+ return [label, ...values.map((text, index) => `${indent$1(1)}[${index + 1}] ${text}`)];
274
+ }
275
+ function numberingLevelLine(ilvl, level) {
276
+ const restartSuffix = level.restart === void 0 ? "" : `, restarts at level ${level.restart}`;
277
+ return `${indent$1(2)}level ${ilvl}: ${level.format} ${JSON.stringify(level.text)} starting at ${level.startAt}${restartSuffix}`;
278
+ }
279
+ function numberingSection(numbering) {
280
+ const numIds = Object.keys(numbering);
281
+ if (numIds.length === 0) return [];
282
+ const lines = ["numbering"];
283
+ for (const numId of numIds) {
284
+ const definition = numbering[numId];
285
+ if (definition === void 0) continue;
286
+ lines.push(`${indent$1(1)}numId ${numId}`);
287
+ const ilvls = Object.keys(definition.levels).sort((a, b) => Number(a) - Number(b));
288
+ for (const ilvl of ilvls) {
289
+ const level = definition.levels[ilvl];
290
+ if (level === void 0) continue;
291
+ lines.push(numberingLevelLine(ilvl, level));
292
+ }
293
+ }
294
+ return lines;
295
+ }
296
+ function formatDocxExtrasLines(extras) {
297
+ const nonEmptySections = [
298
+ commentsSection(extras.comments),
299
+ footnotesSection(extras.footnotes),
300
+ headerOrFooterSection("headers", extras.headers),
301
+ headerOrFooterSection("footers", extras.footers),
302
+ numberingSection(extras.numbering)
303
+ ].filter((section) => section.length > 0);
304
+ if (nonEmptySections.length === 0) return ["This document carries no comments, footnotes, headers, footers, or numbering definitions."];
305
+ return nonEmptySections.flatMap((section, index) => index === 0 ? section : ["", ...section]);
306
+ }
307
+ //#endregion
308
+ //#region src/runtime/metadata-format.ts
309
+ const METADATA_KEYS = [
310
+ "title",
311
+ "author",
312
+ "subject",
313
+ "keywords",
314
+ "creator",
315
+ "producer",
316
+ "createdIso",
317
+ "modifiedIso"
318
+ ];
319
+ function isPresent(entry) {
320
+ return entry[1] !== void 0;
321
+ }
322
+ function formatMetadataValue(value) {
323
+ return typeof value === "string" ? value : value.join(", ");
324
+ }
325
+ function presentMetadataEntries(metadata) {
326
+ return METADATA_KEYS.map((key) => [key, metadata[key]]).filter(isPresent);
327
+ }
328
+ function formatMetadataLines(metadata) {
329
+ return presentMetadataEntries(metadata).map(([key, value]) => `${key}: ${formatMetadataValue(value)}`);
330
+ }
331
+ //#endregion
250
332
  //#region src/odb-structure.ts
251
333
  const INDENT = " ";
252
334
  function indent(depth) {
@@ -379,4 +461,4 @@ function formatOdbReportLines(report) {
379
461
  return lines;
380
462
  }
381
463
  //#endregion
382
- export { odbFormSummary as a, writeOutput as c, inferFormatFromExtension as d, isDocumentFormat as f, formatOdbReportLines as i, loadProvidedFonts as l, describeOdbReport as n, readInput as o, formatOdbFormLines as r, resolveDefaultOutputPath as s, describeOdbForm as t, formatToExtension as u };
464
+ export { odbFormSummary as a, formatDocxExtrasLines as c, writeOutput as d, loadProvidedFonts as f, isDocumentFormat as h, formatOdbReportLines as i, readInput as l, inferFormatFromExtension as m, describeOdbReport as n, formatMetadataLines as o, formatToExtension as p, formatOdbFormLines as r, presentMetadataEntries as s, describeOdbForm as t, resolveDefaultOutputPath as u };