Skip to content
This repository was archived by the owner on Aug 20, 2026. It is now read-only.

Commit 0848086

Browse files
committed
refactor(fonts): extract via documents.js's extractSourceFontsForFormat
The format-to-FontSourcePackage dispatch (docx/pptx via ooxml.js's decodePackage, odt/odp/ods/odg via odf.js's) now lives in documents.js's own extractSourceFontsForFormat, which rejects an unsupported format (xlsx/pdf/markdown/odf) by throwing UnsupportedFontSourceFormatError rather than a plain Error -- mapped here to EXIT_USAGE_ERROR, matching this CLI's existing exit-code convention for a bad invocation choice. Format validation now happens after the input bytes are read (inside extractSourceFontsForFormat itself) rather than before, so the unsupported-format rejection test needs a real (if trivial) input file instead of a path that was never actually opened.
1 parent 4dee32a commit 0848086

3 files changed

Lines changed: 11 additions & 31 deletions

File tree

src/commands/fonts.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ beforeAll(async () => {
5050
const plain = createDocx();
5151
plain.body.appendParagraph().appendRun({ text: 'No fonts embedded here.' });
5252
await writeFile(join(workspace, 'plain.docx'), plain.toBytes());
53+
// extractSourceFontsForFormat (documents.js) validates the format itself rather than the CLI pre-checking it, so the input file is now genuinely read before that rejection fires -- unlike a bare nonexistent path, this needs to exist. Its content is never parsed: the rejection below fires purely on the '.xlsx' extension.
54+
await writeFile(join(workspace, 'unused.xlsx'), new Uint8Array([0]));
5355
});
5456

5557
afterAll(async () => {
@@ -106,7 +108,7 @@ describe('fonts', () => {
106108
});
107109

108110
it('rejects a format with no source-embedded-font concept, naming the restriction', async () => {
109-
const { exitCode, stderr } = await runCli(['fonts', 'unused.xlsx']);
111+
const { exitCode, stderr } = await runCli(['fonts', join(workspace, 'unused.xlsx')]);
110112

111113
expect(exitCode).toBe(EXIT_USAGE_ERROR);
112114
expect(stderr).toContain('xlsx');

src/commands/fonts.ts

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { type Command } from 'commander';
2-
import { decodePackage, extractSourceFonts, type DocumentFormat, type FontSourcePackage } from 'documents.js';
3-
import { decodePackage as decodeOdfPackage } from 'odf.js';
2+
import { extractSourceFontsForFormat } from 'documents.js';
43
import { inferFormatFromExtension } from '../format';
54
import { createRuntimeSignal } from '../runtime/abort';
65
import { EXIT_SUCCESS, EXIT_USAGE_ERROR, mapErrorToExit } from '../runtime/exit-codes';
@@ -11,28 +10,6 @@ interface FontsCliOptions {
1110
readonly json: boolean;
1211
}
1312

14-
// The only formats `extractSourceFonts` (documents.js's `src/fonts/registry.ts`) knows how to read a source-embedded face from at all: docx/pptx via OOXML's own fontTable.xml/embeddedFontLst, odt/odp/ods/odg via ODF's own office:font-face-decls. xlsx has no OOXML font-embedding vocabulary of its own; pdf/markdown carry no source-package concept to embed a font declaration in; a standalone .odf formula document embeds only the STIX Two Math font pdf-codec itself carries, never a caller-resolvable face; .odb has no font concept at all.
15-
const FONT_SOURCE_FORMATS: Readonly<Record<'docx' | 'pptx' | 'odt' | 'odp' | 'ods' | 'odg', true>> = {
16-
docx: true,
17-
pptx: true,
18-
odt: true,
19-
odp: true,
20-
ods: true,
21-
odg: true,
22-
};
23-
24-
function isFontSourceFormat(format: DocumentFormat): format is keyof typeof FONT_SOURCE_FORMATS {
25-
return format in FONT_SOURCE_FORMATS;
26-
}
27-
28-
// docx/pptx dispatch through ooxml.js's own decodePackage (re-exported from documents.js under its own name); odt/odp/ods/odg dispatch through odf.js's decodePackage instead, aliased on import exactly as commands/odb.ts already does to avoid the naming collision between the two same-named functions.
29-
function resolveFontSourcePackage(format: keyof typeof FONT_SOURCE_FORMATS, bytes: Uint8Array<ArrayBuffer>): FontSourcePackage {
30-
if (format === 'docx' || format === 'pptx') {
31-
return { kind: format, package: decodePackage(bytes) };
32-
}
33-
return { kind: 'odf', package: decodeOdfPackage(bytes) };
34-
}
35-
3613
// ProvidedFont (pdf-codec, re-exported by documents.js) carries `bytes` directly, with no `byteLength` field of its own -- computed here rather than exposing the raw font bytes themselves, which no caller of this command's summary output has asked for and which would bloat --json output by however large the embedded face is.
3714
interface FontFaceSummary {
3815
readonly family: string;
@@ -51,14 +28,10 @@ async function runFonts(input: string, options: FontsCliOptions): Promise<number
5128
process.stderr.write(`[${command}] cannot infer a document format from '${input}'; expected one of docx, pptx, odt, odp, ods, odg\n`);
5229
return EXIT_USAGE_ERROR;
5330
}
54-
if (!isFontSourceFormat(format)) {
55-
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`);
56-
return EXIT_USAGE_ERROR;
57-
}
5831

5932
const inputBytes = await readInput(input, { signal });
60-
const source = resolveFontSourcePackage(format, new Uint8Array(inputBytes));
61-
const faces = extractSourceFonts(source);
33+
// extractSourceFontsForFormat itself rejects a format with no source-embedded font concept (xlsx, pdf, markdown, odf), naming the six it supports -- no separate isFontSourceFormat guard needed here any more.
34+
const faces = extractSourceFontsForFormat(format, new Uint8Array(inputBytes));
6235
const summaries: readonly FontFaceSummary[] = faces.map((face) => ({ family: face.family, bold: face.bold, italic: face.italic, byteLength: face.bytes.length }));
6336

6437
if (options.json) {

src/runtime/exit-codes.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
OdmUnresolvedSectionError,
1111
PdfEncryptedError,
1212
PdfParseError,
13+
UnsupportedFontSourceFormatError,
1314
} from 'documents.js';
1415

1516
// Mirrors sysexits.h-style convention loosely: 0 is success, 1 is a generic failure, 2 is a usage error (matching coreutils' own convention for bad invocation), and the two signal-derived codes (124, 130) match `timeout(1)` and 128+SIGINT respectively, so a caller scripting against this CLI sees the same exit codes it would from any other well-behaved Unix tool.
@@ -43,6 +44,10 @@ export function mapErrorToExit(error: unknown, abortReason: 'interrupt' | 'timeo
4344
if (error instanceof HsqldbSqlUnsupportedError || error instanceof HsqldbSqlParseError || error instanceof HsqldbSqlEvaluationError) {
4445
return EXIT_INPUT_ERROR;
4546
}
47+
// fonts' own extractSourceFontsForFormat: the given DocumentFormat is a real, recognised format, but not one with a source-embedded-font concept at all (xlsx, pdf, markdown, odf) -- a bad invocation choice, not an unusable file, so this maps like every other usage error rather than EXIT_INPUT_ERROR's "the file itself is the problem".
48+
if (error instanceof UnsupportedFontSourceFormatError) {
49+
return EXIT_USAGE_ERROR;
50+
}
4651
// PdfEncryptedError extends PdfParseError, so this branch is redundant with the default fall-through below -- kept explicit anyway so the mapping documents its intent (these two error classes are unusable-input failures, not a catch-all) rather than relying on an implicit default to cover a case this function is specifically supposed to name.
4752
if (error instanceof PdfEncryptedError || error instanceof PdfParseError) {
4853
return EXIT_INPUT_ERROR;

0 commit comments

Comments
 (0)