@likecoin/epubcheck-ts 0.3.9 → 0.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/bin/epubcheck.ts CHANGED
@@ -7,38 +7,95 @@
7
7
  * https://github.com/w3c/epubcheck
8
8
  */
9
9
 
10
- import { readFile, writeFile } from 'node:fs/promises';
10
+ import { readFile, readdir, stat, writeFile } from 'node:fs/promises';
11
11
  import { parseArgs } from 'node:util';
12
- import { basename } from 'node:path';
13
- import type { EpubCheckOptions, EPUBProfile, ValidationMessage } from '../src/types.js';
12
+ import { basename, join, relative, sep } from 'node:path';
13
+ import type {
14
+ EpubCheckOptions,
15
+ EPUBProfile,
16
+ EPUBVersion,
17
+ Severity,
18
+ ValidationMessage,
19
+ ValidationMode,
20
+ } from '../src/types.js';
14
21
 
15
22
  // Dynamic import to support both ESM and CJS builds
16
- const { EpubCheck, toJSONReport } = await import('../dist/index.js');
17
-
18
- const VERSION = '0.3.9';
19
-
20
- // Parse command line arguments
21
- const { values, positionals } = parseArgs({
22
- options: {
23
- json: { type: 'string', short: 'j' },
24
- quiet: { type: 'boolean', short: 'q', default: false },
25
- profile: { type: 'string', short: 'p' },
26
- usage: { type: 'boolean', short: 'u', default: false },
27
- version: { type: 'boolean', short: 'v', default: false },
28
- help: { type: 'boolean', short: 'h', default: false },
29
- 'fail-on-warnings': { type: 'boolean', short: 'w', default: false },
30
- listChecks: { type: 'boolean', short: 'l', default: false },
31
- },
32
- allowPositionals: true,
33
- strict: false,
34
- });
23
+ const { EpubCheck, EPUB_VERSIONS, toJSONReport } = await import('../dist/index.js');
24
+
25
+ const VERSION = '0.5.0';
26
+ const VALID_MODES: ReadonlySet<ValidationMode> = new Set([
27
+ 'exp',
28
+ 'opf',
29
+ 'xhtml',
30
+ 'svg',
31
+ 'nav',
32
+ 'mo',
33
+ ]);
34
+
35
+ interface CliValues {
36
+ json?: string;
37
+ quiet: boolean;
38
+ profile?: string;
39
+ mode?: string;
40
+ 'epub-version'?: string;
41
+ usage: boolean;
42
+ fatal: boolean;
43
+ error: boolean;
44
+ warn: boolean;
45
+ info: boolean;
46
+ customMessages?: string;
47
+ version: boolean;
48
+ help: boolean;
49
+ 'fail-on-warnings': boolean;
50
+ failonwarnings: boolean;
51
+ listChecks: boolean;
52
+ }
53
+
54
+ let values: CliValues;
55
+ let positionals: string[];
56
+ try {
57
+ ({ values, positionals } = parseArgs({
58
+ options: {
59
+ json: { type: 'string', short: 'j' },
60
+ quiet: { type: 'boolean', short: 'q', default: false },
61
+ profile: { type: 'string', short: 'p' },
62
+ mode: { type: 'string', short: 'm' },
63
+ 'epub-version': { type: 'string', short: 'v' },
64
+ usage: { type: 'boolean', short: 'u', default: false },
65
+ fatal: { type: 'boolean', short: 'f', default: false },
66
+ error: { type: 'boolean', short: 'e', default: false },
67
+ warn: { type: 'boolean', short: 'w', default: false },
68
+ info: { type: 'boolean', short: 'i', default: false },
69
+ customMessages: { type: 'string', short: 'c' },
70
+ version: { type: 'boolean', short: 'V', default: false },
71
+ help: { type: 'boolean', short: 'h', default: false },
72
+ 'fail-on-warnings': { type: 'boolean', default: false },
73
+ failonwarnings: { type: 'boolean', default: false },
74
+ listChecks: { type: 'boolean', short: 'l', default: false },
75
+ },
76
+ allowPositionals: true,
77
+ strict: true,
78
+ }) as unknown as { values: CliValues; positionals: string[] });
79
+ } catch (err) {
80
+ const message = err instanceof Error ? err.message : String(err);
81
+ console.error(`\x1b[31mError:\x1b[0m ${message}`);
82
+ const unsupportedJavaFlags = ['--out', '-o', '--xmp', '-x', '--save', '--locale'];
83
+ const matched = unsupportedJavaFlags.find((f) => message.includes(f));
84
+ if (matched) {
85
+ console.error(
86
+ `\x1b[90mNote: ${matched} is a Java EPUBCheck flag that epubcheck-ts does not yet support.\x1b[0m`,
87
+ );
88
+ }
89
+ console.error('Run with --help for usage information');
90
+ process.exit(2);
91
+ }
35
92
 
36
93
  // Show version
37
94
  if (values.version) {
38
95
  console.log(`EPUBCheck-TS v${VERSION}`);
39
96
  console.log('TypeScript EPUB validator for Node.js and browsers');
40
97
  console.log();
41
- console.log('Note: This is ~88% feature-complete compared to Java EPUBCheck.');
98
+ console.log('Note: This is ~93% feature-complete compared to Java EPUBCheck.');
42
99
  console.log('For production validation: https://github.com/w3c/epubcheck');
43
100
  process.exit(0);
44
101
  }
@@ -55,75 +112,195 @@ if (values.listChecks) {
55
112
  if (values.help || positionals.length === 0) {
56
113
  console.log(`EPUBCheck-TS v${VERSION} - EPUB Validator
57
114
 
58
- Usage: epubcheck-ts <file.epub> [options]
115
+ Usage: epubcheck-ts <file> [options]
59
116
 
60
117
  Arguments:
61
- <file.epub> Path to EPUB file to validate
118
+ <file> Path to EPUB file, directory, or single file to validate
62
119
 
63
120
  Options:
64
121
  -j, --json <file> Output JSON report to file (use '-' for stdout)
65
122
  -q, --quiet Suppress console output (errors only)
66
123
  -p, --profile <name> Validation profile (default|dict|edupub|idx|preview)
124
+ -m, --mode <type> Validation mode: exp (expanded directory), opf, xhtml, svg, nav, mo
125
+ -v, --epub-version <ver> EPUB version for single-file mode (2|2.0|3|3.0|3.1|3.2|3.3)
67
126
  -u, --usage Include usage messages (best practices)
68
- -w, --fail-on-warnings Exit with code 1 if warnings are found
127
+ -f, --fatal Show only fatal errors
128
+ -e, --error Show fatal errors and errors
129
+ -w, --warn Show fatal errors, errors, and warnings
130
+ -i, --info Show fatal, error, warning, and info messages
131
+ -c, --customMessages <file> Override message severities (TSV: ID\\tSEVERITY)
132
+ --fail-on-warnings Exit with code 1 if warnings are found
133
+ (also accepts --failonwarnings for Java compatibility)
69
134
  -l, --listChecks List all message IDs and severities
70
- -v, --version Show version information
135
+ -V, --version Show version information
71
136
  -h, --help Show this help message
72
137
 
138
+ Modes:
139
+ --mode exp Validate an expanded (unpacked) EPUB directory
140
+ --mode opf -v 3.0 Validate a standalone OPF package document
141
+ --mode xhtml -v 3.0 Validate a standalone XHTML content document
142
+ --mode svg -v 3.0 Validate a standalone SVG content document
143
+ --mode nav -v 3.0 Validate a standalone Navigation Document (EPUB 3 only)
144
+ --mode mo -v 3.0 Validate a standalone SMIL media overlay document
145
+
73
146
  Examples:
74
147
  epubcheck-ts book.epub
75
148
  epubcheck-ts book.epub --json report.json
76
149
  epubcheck-ts book.epub --quiet --fail-on-warnings
77
150
  epubcheck-ts book.epub --profile dict
151
+ epubcheck-ts ./unpacked-epub/ --mode exp
152
+ epubcheck-ts chapter.xhtml --mode xhtml -v 3.0
153
+ epubcheck-ts package.opf --mode opf -v 3.0
154
+ epubcheck-ts image.svg --mode svg -v 3.0
155
+ epubcheck-ts nav.xhtml --mode nav -v 3.0
156
+ epubcheck-ts overlay.smil --mode mo -v 3.0
78
157
 
79
158
  Exit Codes:
80
159
  0 No errors (or only warnings if --fail-on-warnings not set)
81
160
  1 Validation errors found (or warnings with --fail-on-warnings)
82
161
  2 Runtime error (file not found, invalid arguments, etc.)
83
162
 
84
- Note: This tool provides ~88% coverage of Java EPUBCheck features.
85
- Missing features: Media Overlays, advanced ARIA checks, encryption/signatures.
86
- For complete EPUB 3 conformance testing, use: https://github.com/w3c/epubcheck
87
-
88
163
  Report issues: https://github.com/likecoin/epubcheck-ts/issues
89
164
  `);
90
165
  process.exit(0);
91
166
  }
92
167
 
168
+ /**
169
+ * Recursively read all files in a directory into a Map
170
+ */
171
+ async function readDirectoryFiles(dirPath: string): Promise<Map<string, Uint8Array>> {
172
+ const files = new Map<string, Uint8Array>();
173
+
174
+ async function walk(dir: string): Promise<void> {
175
+ const entries = await readdir(dir, { withFileTypes: true });
176
+ for (const entry of entries) {
177
+ const fullPath = join(dir, entry.name);
178
+ if (entry.isDirectory()) {
179
+ await walk(fullPath);
180
+ } else if (entry.isFile()) {
181
+ const relPath = relative(dirPath, fullPath).split(sep).join('/');
182
+ const data = await readFile(fullPath);
183
+ files.set(relPath, data);
184
+ }
185
+ }
186
+ }
187
+
188
+ await walk(dirPath);
189
+ return files;
190
+ }
191
+
93
192
  // Main validation logic
94
193
  async function main(): Promise<void> {
95
194
  const filePath = positionals[0];
96
195
 
97
196
  if (!filePath) {
98
- console.error('Error: No EPUB file specified');
197
+ console.error('Error: No file specified');
99
198
  console.error('Run with --help for usage information');
100
199
  process.exit(2);
101
200
  }
102
201
 
202
+ const mode = values.mode as ValidationMode | undefined;
203
+ if (mode && !VALID_MODES.has(mode)) {
204
+ console.error(`Error: Invalid mode "${mode}". Valid modes: ${[...VALID_MODES].join(', ')}`);
205
+ process.exit(2);
206
+ }
207
+
208
+ const rawVersion = values['epub-version'];
209
+ const epubVersion = rawVersion === '2' ? '2.0' : rawVersion === '3' ? '3.0' : rawVersion;
210
+ if (epubVersion && !(EPUB_VERSIONS as readonly string[]).includes(epubVersion)) {
211
+ console.error(
212
+ `Error: Invalid EPUB version "${epubVersion}". Valid versions: ${EPUB_VERSIONS.join(', ')}`,
213
+ );
214
+ process.exit(2);
215
+ }
216
+
217
+ // Single-file modes require a version
218
+ if (mode && mode !== 'exp' && !epubVersion) {
219
+ console.error(`Error: --epub-version (-v) is required when using --mode ${mode}`);
220
+ process.exit(2);
221
+ }
222
+
103
223
  try {
104
- // Read EPUB file
105
224
  if (!values.quiet) {
106
225
  console.log(`Validating: ${basename(filePath)}`);
107
226
  console.log();
108
227
  }
109
228
 
110
- const epubData = await readFile(filePath);
111
-
112
- // Validate
113
- const startTime = Date.now();
229
+ // Build options
114
230
  const options: EpubCheckOptions = {};
115
231
  if (values.profile) {
116
232
  options.profile = values.profile as EPUBProfile;
117
233
  }
234
+ if (epubVersion) {
235
+ options.version = epubVersion as EPUBVersion;
236
+ }
237
+ if (mode) {
238
+ options.mode = mode;
239
+ }
118
240
  if (values.usage) {
119
241
  options.includeUsage = true;
120
242
  }
121
- const result = await EpubCheck.validate(epubData, options);
243
+ if (typeof values.customMessages === 'string') {
244
+ const { parseCustomMessages } = await import('../dist/index.js');
245
+ const cmContent = await readFile(values.customMessages, 'utf-8');
246
+ options.customMessages = parseCustomMessages(cmContent);
247
+ }
248
+
249
+ const startTime = Date.now();
250
+ let result;
251
+
252
+ // Determine effective mode (auto-detect directory as expanded)
253
+ let effectiveMode = mode;
254
+ if (!mode || mode === 'exp') {
255
+ const fileStat = await stat(filePath);
256
+ if (fileStat.isDirectory()) {
257
+ effectiveMode = 'exp';
258
+ } else if (mode === 'exp') {
259
+ console.error('Error: --mode exp requires a directory path');
260
+ process.exit(2);
261
+ }
262
+ }
263
+
264
+ if (effectiveMode === 'exp') {
265
+ const files = await readDirectoryFiles(filePath);
266
+ result = await EpubCheck.validateExpanded(files, options);
267
+ } else if (
268
+ effectiveMode === 'opf' ||
269
+ effectiveMode === 'xhtml' ||
270
+ effectiveMode === 'svg' ||
271
+ effectiveMode === 'nav' ||
272
+ effectiveMode === 'mo'
273
+ ) {
274
+ const fileData = await readFile(filePath);
275
+ result = await EpubCheck.validateSingleFile(fileData, basename(filePath), options);
276
+ } else {
277
+ const epubData = await readFile(filePath);
278
+ result = await EpubCheck.validate(epubData, options, basename(filePath));
279
+ }
122
280
  const elapsedMs = Date.now() - startTime;
123
281
 
282
+ // Most restrictive severity flag wins (--fatal > --error > --warn > --info)
283
+ const severityRank: Record<Severity, number> = {
284
+ fatal: 0,
285
+ error: 1,
286
+ warning: 2,
287
+ info: 3,
288
+ usage: 4,
289
+ };
290
+ let maxRank = 4;
291
+ if (values.fatal) maxRank = 0;
292
+ else if (values.error) maxRank = 1;
293
+ else if (values.warn) maxRank = 2;
294
+ else if (values.info) maxRank = 3;
295
+ const isFiltered = values.fatal || values.error || values.warn || values.info;
296
+ const displayMessages = result.messages.filter(
297
+ (m: ValidationMessage) => severityRank[m.severity] <= maxRank,
298
+ );
299
+
124
300
  // Output JSON report if requested
125
301
  if (values.json !== undefined) {
126
- const jsonContent = toJSONReport(result); // Already stringified
302
+ const filteredResult = isFiltered ? { ...result, messages: displayMessages } : result;
303
+ const jsonContent = toJSONReport(filteredResult); // Already stringified
127
304
 
128
305
  if (values.json === '-') {
129
306
  // Output to stdout - suppress other output
@@ -145,11 +322,11 @@ async function main(): Promise<void> {
145
322
  // Console output (unless quiet mode)
146
323
  if (!values.quiet) {
147
324
  // Group messages by severity
148
- const fatal = result.messages.filter((m: ValidationMessage) => m.severity === 'fatal');
149
- const errors = result.messages.filter((m: ValidationMessage) => m.severity === 'error');
150
- const warnings = result.messages.filter((m: ValidationMessage) => m.severity === 'warning');
151
- const info = result.messages.filter((m: ValidationMessage) => m.severity === 'info');
152
- const usage = result.messages.filter((m: ValidationMessage) => m.severity === 'usage');
325
+ const fatal = displayMessages.filter((m: ValidationMessage) => m.severity === 'fatal');
326
+ const errors = displayMessages.filter((m: ValidationMessage) => m.severity === 'error');
327
+ const warnings = displayMessages.filter((m: ValidationMessage) => m.severity === 'warning');
328
+ const info = displayMessages.filter((m: ValidationMessage) => m.severity === 'info');
329
+ const usage = displayMessages.filter((m: ValidationMessage) => m.severity === 'usage');
153
330
 
154
331
  // Print messages with colors
155
332
  const printMessages = (
@@ -180,8 +357,7 @@ async function main(): Promise<void> {
180
357
  if (warnings.length > 0) {
181
358
  printMessages(warnings, '\x1b[33m', 'WARNING');
182
359
  }
183
- if (info.length > 0 && result.messages.length < 20) {
184
- // Only show info if total messages is small
360
+ if (info.length > 0 && displayMessages.length < 20) {
185
361
  printMessages(info, '\x1b[36m', 'INFO');
186
362
  }
187
363
  if (usage.length > 0) {
@@ -210,7 +386,7 @@ async function main(): Promise<void> {
210
386
  // Show limitation notice if there were no major errors
211
387
  if (result.errorCount === 0 && result.fatalCount === 0) {
212
388
  console.log(
213
- '\x1b[90mNote: This validator provides ~88% coverage of Java EPUBCheck.\x1b[0m',
389
+ '\x1b[90mNote: This validator provides ~93% coverage of Java EPUBCheck.\x1b[0m',
214
390
  );
215
391
  console.log('\x1b[90mFor complete validation: https://github.com/w3c/epubcheck\x1b[0m');
216
392
  console.log();
@@ -218,10 +394,9 @@ async function main(): Promise<void> {
218
394
  }
219
395
 
220
396
  // Determine exit code
397
+ const failOnWarnings = values['fail-on-warnings'] || values.failonwarnings;
221
398
  const shouldFail =
222
- result.errorCount > 0 ||
223
- result.fatalCount > 0 ||
224
- (values['fail-on-warnings'] && result.warningCount > 0);
399
+ result.errorCount > 0 || result.fatalCount > 0 || (failOnWarnings && result.warningCount > 0);
225
400
  process.exit(shouldFail ? 1 : 0);
226
401
  } catch (error) {
227
402
  console.error('\x1b[31mError:\x1b[0m', error instanceof Error ? error.message : String(error));