@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/README.md +36 -23
- package/bin/epubcheck.js +173 -40
- package/bin/epubcheck.ts +225 -50
- package/dist/index.cjs +8710 -6629
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +511 -394
- package/dist/index.d.ts +511 -394
- package/dist/index.js +8710 -6631
- package/dist/index.js.map +1 -1
- package/package.json +21 -5
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 {
|
|
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.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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 ~
|
|
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
|
|
115
|
+
Usage: epubcheck-ts <file> [options]
|
|
59
116
|
|
|
60
117
|
Arguments:
|
|
61
|
-
<file
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
149
|
-
const errors =
|
|
150
|
-
const warnings =
|
|
151
|
-
const info =
|
|
152
|
-
const 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 &&
|
|
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 ~
|
|
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));
|