@likecoin/epubcheck-ts 0.4.0 → 0.5.1
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 +4 -4
- package/bin/epubcheck.js +141 -37
- package/bin/epubcheck.ts +192 -47
- package/dist/index.cjs +8551 -7097
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +85 -5
- package/dist/index.d.ts +85 -5
- package/dist/index.js +8552 -7099
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Validate EPUB files in Node.js and the browser. A TypeScript implementation of [
|
|
|
6
6
|
[](https://www.npmjs.com/package/@likecoin/epubcheck-ts)
|
|
7
7
|
[](./LICENSE)
|
|
8
8
|
|
|
9
|
-
> **Status**:
|
|
9
|
+
> **Status**: ~97% feature parity with Java EPUBCheck (1333 tests passing, 34 skipped). See [PROJECT_STATUS.md](./PROJECT_STATUS.md) for details. For full EPUB 3 conformance testing, use the official [Java EPUBCheck](https://github.com/w3c/epubcheck).
|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
@@ -288,11 +288,11 @@ This library is a TypeScript port of the Java-based [EPUBCheck](https://github.c
|
|
|
288
288
|
| Cross-reference Validation | 🟢 Complete | ~92% | Reference tracking, fragments, fallbacks, remote resources, cross-document features |
|
|
289
289
|
| Accessibility Checks | 🟢 Complete | ~71% | 12/17 ACC checks: table, image alt, hyperlink, MathML, SVG, epub:type, OPF metadata |
|
|
290
290
|
| Media Overlays | 🟡 Partial | ~70% | SMIL structure, timing, audio, OPF metadata, duration validation |
|
|
291
|
-
| Media Validation |
|
|
291
|
+
| Media Validation | 🟡 Partial | ~25% | Magic number checks (MED-004/OPF-029/PKG-022); deep format parsing planned |
|
|
292
292
|
|
|
293
293
|
Legend: 🟢 Complete | 🟡 Partial | 🔴 Basic | ❌ Not Started
|
|
294
294
|
|
|
295
|
-
**Overall Progress: ~
|
|
295
|
+
**Overall Progress: ~97% of Java EPUBCheck features**
|
|
296
296
|
|
|
297
297
|
See [PROJECT_STATUS.md](./PROJECT_STATUS.md) for detailed comparison.
|
|
298
298
|
|
|
@@ -377,7 +377,7 @@ Legend: ✅ Implemented
|
|
|
377
377
|
| Aspect | epubcheck-ts | EPUBCheck (Java) |
|
|
378
378
|
|--------|--------------|------------------|
|
|
379
379
|
| Runtime | Node.js / Browser | JVM |
|
|
380
|
-
| Feature Parity | ~
|
|
380
|
+
| Feature Parity | ~97% | 100% |
|
|
381
381
|
| Bundle Size | ~450KB JS + ~1.6MB WASM | ~15MB |
|
|
382
382
|
| Installation | `npm install` | Download JAR |
|
|
383
383
|
| Integration | Native JS/TS | CLI or Java API |
|
package/bin/epubcheck.js
CHANGED
|
@@ -1,27 +1,55 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
|
-
import { basename } from "node:path";
|
|
5
|
-
const { EpubCheck, toJSONReport } = await import("../dist/index.js");
|
|
6
|
-
const VERSION = "0.
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
4
|
+
import { basename, join, relative, sep } from "node:path";
|
|
5
|
+
const { EpubCheck, EPUB_VERSIONS, toJSONReport } = await import("../dist/index.js");
|
|
6
|
+
const VERSION = "0.5.1";
|
|
7
|
+
const VALID_MODES = /* @__PURE__ */ new Set([
|
|
8
|
+
"exp",
|
|
9
|
+
"opf",
|
|
10
|
+
"xhtml",
|
|
11
|
+
"svg",
|
|
12
|
+
"nav",
|
|
13
|
+
"mo"
|
|
14
|
+
]);
|
|
15
|
+
let values;
|
|
16
|
+
let positionals;
|
|
17
|
+
try {
|
|
18
|
+
({ values, positionals } = parseArgs({
|
|
19
|
+
options: {
|
|
20
|
+
json: { type: "string", short: "j" },
|
|
21
|
+
quiet: { type: "boolean", short: "q", default: false },
|
|
22
|
+
profile: { type: "string", short: "p" },
|
|
23
|
+
mode: { type: "string", short: "m" },
|
|
24
|
+
"epub-version": { type: "string", short: "v" },
|
|
25
|
+
usage: { type: "boolean", short: "u", default: false },
|
|
26
|
+
fatal: { type: "boolean", short: "f", default: false },
|
|
27
|
+
error: { type: "boolean", short: "e", default: false },
|
|
28
|
+
warn: { type: "boolean", short: "w", default: false },
|
|
29
|
+
info: { type: "boolean", short: "i", default: false },
|
|
30
|
+
customMessages: { type: "string", short: "c" },
|
|
31
|
+
version: { type: "boolean", short: "V", default: false },
|
|
32
|
+
help: { type: "boolean", short: "h", default: false },
|
|
33
|
+
"fail-on-warnings": { type: "boolean", default: false },
|
|
34
|
+
failonwarnings: { type: "boolean", default: false },
|
|
35
|
+
listChecks: { type: "boolean", short: "l", default: false }
|
|
36
|
+
},
|
|
37
|
+
allowPositionals: true,
|
|
38
|
+
strict: true
|
|
39
|
+
}));
|
|
40
|
+
} catch (err) {
|
|
41
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
42
|
+
console.error(`\x1B[31mError:\x1B[0m ${message}`);
|
|
43
|
+
const unsupportedJavaFlags = ["--out", "-o", "--xmp", "-x", "--save", "--locale"];
|
|
44
|
+
const matched = unsupportedJavaFlags.find((f) => message.includes(f));
|
|
45
|
+
if (matched) {
|
|
46
|
+
console.error(
|
|
47
|
+
`\x1B[90mNote: ${matched} is a Java EPUBCheck flag that epubcheck-ts does not yet support.\x1B[0m`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
console.error("Run with --help for usage information");
|
|
51
|
+
process.exit(2);
|
|
52
|
+
}
|
|
25
53
|
if (values.version) {
|
|
26
54
|
console.log(`EPUBCheck-TS v${VERSION}`);
|
|
27
55
|
console.log("TypeScript EPUB validator for Node.js and browsers");
|
|
@@ -39,62 +67,115 @@ if (values.listChecks) {
|
|
|
39
67
|
if (values.help || positionals.length === 0) {
|
|
40
68
|
console.log(`EPUBCheck-TS v${VERSION} - EPUB Validator
|
|
41
69
|
|
|
42
|
-
Usage: epubcheck-ts <file
|
|
70
|
+
Usage: epubcheck-ts <file> [options]
|
|
43
71
|
|
|
44
72
|
Arguments:
|
|
45
|
-
<file
|
|
73
|
+
<file> Path to EPUB file, directory, or single file to validate
|
|
46
74
|
|
|
47
75
|
Options:
|
|
48
76
|
-j, --json <file> Output JSON report to file (use '-' for stdout)
|
|
49
77
|
-q, --quiet Suppress console output (errors only)
|
|
50
78
|
-p, --profile <name> Validation profile (default|dict|edupub|idx|preview)
|
|
79
|
+
-m, --mode <type> Validation mode: exp (expanded directory), opf, xhtml, svg, nav, mo
|
|
80
|
+
-v, --epub-version <ver> EPUB version for single-file mode (2|2.0|3|3.0|3.1|3.2|3.3)
|
|
51
81
|
-u, --usage Include usage messages (best practices)
|
|
52
82
|
-f, --fatal Show only fatal errors
|
|
53
83
|
-e, --error Show fatal errors and errors
|
|
54
|
-
|
|
84
|
+
-w, --warn Show fatal errors, errors, and warnings
|
|
85
|
+
-i, --info Show fatal, error, warning, and info messages
|
|
55
86
|
-c, --customMessages <file> Override message severities (TSV: ID\\tSEVERITY)
|
|
56
|
-
|
|
87
|
+
--fail-on-warnings Exit with code 1 if warnings are found
|
|
88
|
+
(also accepts --failonwarnings for Java compatibility)
|
|
57
89
|
-l, --listChecks List all message IDs and severities
|
|
58
|
-
-
|
|
90
|
+
-V, --version Show version information
|
|
59
91
|
-h, --help Show this help message
|
|
60
92
|
|
|
93
|
+
Modes:
|
|
94
|
+
--mode exp Validate an expanded (unpacked) EPUB directory
|
|
95
|
+
--mode opf -v 3.0 Validate a standalone OPF package document
|
|
96
|
+
--mode xhtml -v 3.0 Validate a standalone XHTML content document
|
|
97
|
+
--mode svg -v 3.0 Validate a standalone SVG content document
|
|
98
|
+
--mode nav -v 3.0 Validate a standalone Navigation Document (EPUB 3 only)
|
|
99
|
+
--mode mo -v 3.0 Validate a standalone SMIL media overlay document
|
|
100
|
+
|
|
61
101
|
Examples:
|
|
62
102
|
epubcheck-ts book.epub
|
|
63
103
|
epubcheck-ts book.epub --json report.json
|
|
64
104
|
epubcheck-ts book.epub --quiet --fail-on-warnings
|
|
65
105
|
epubcheck-ts book.epub --profile dict
|
|
106
|
+
epubcheck-ts ./unpacked-epub/ --mode exp
|
|
107
|
+
epubcheck-ts chapter.xhtml --mode xhtml -v 3.0
|
|
108
|
+
epubcheck-ts package.opf --mode opf -v 3.0
|
|
109
|
+
epubcheck-ts image.svg --mode svg -v 3.0
|
|
110
|
+
epubcheck-ts nav.xhtml --mode nav -v 3.0
|
|
111
|
+
epubcheck-ts overlay.smil --mode mo -v 3.0
|
|
66
112
|
|
|
67
113
|
Exit Codes:
|
|
68
114
|
0 No errors (or only warnings if --fail-on-warnings not set)
|
|
69
115
|
1 Validation errors found (or warnings with --fail-on-warnings)
|
|
70
116
|
2 Runtime error (file not found, invalid arguments, etc.)
|
|
71
117
|
|
|
72
|
-
Note: This tool provides ~93% coverage of Java EPUBCheck features.
|
|
73
|
-
Missing features: single-file/directory validation, advanced ARIA, XHTML/SVG schema.
|
|
74
|
-
For complete EPUB 3 conformance testing, use: https://github.com/w3c/epubcheck
|
|
75
|
-
|
|
76
118
|
Report issues: https://github.com/likecoin/epubcheck-ts/issues
|
|
77
119
|
`);
|
|
78
120
|
process.exit(0);
|
|
79
121
|
}
|
|
122
|
+
async function readDirectoryFiles(dirPath) {
|
|
123
|
+
const files = /* @__PURE__ */ new Map();
|
|
124
|
+
async function walk(dir) {
|
|
125
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
const fullPath = join(dir, entry.name);
|
|
128
|
+
if (entry.isDirectory()) {
|
|
129
|
+
await walk(fullPath);
|
|
130
|
+
} else if (entry.isFile()) {
|
|
131
|
+
const relPath = relative(dirPath, fullPath).split(sep).join("/");
|
|
132
|
+
const data = await readFile(fullPath);
|
|
133
|
+
files.set(relPath, data);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
await walk(dirPath);
|
|
138
|
+
return files;
|
|
139
|
+
}
|
|
80
140
|
async function main() {
|
|
81
141
|
const filePath = positionals[0];
|
|
82
142
|
if (!filePath) {
|
|
83
|
-
console.error("Error: No
|
|
143
|
+
console.error("Error: No file specified");
|
|
84
144
|
console.error("Run with --help for usage information");
|
|
85
145
|
process.exit(2);
|
|
86
146
|
}
|
|
147
|
+
const mode = values.mode;
|
|
148
|
+
if (mode && !VALID_MODES.has(mode)) {
|
|
149
|
+
console.error(`Error: Invalid mode "${mode}". Valid modes: ${[...VALID_MODES].join(", ")}`);
|
|
150
|
+
process.exit(2);
|
|
151
|
+
}
|
|
152
|
+
const rawVersion = values["epub-version"];
|
|
153
|
+
const epubVersion = rawVersion === "2" ? "2.0" : rawVersion === "3" ? "3.0" : rawVersion;
|
|
154
|
+
if (epubVersion && !EPUB_VERSIONS.includes(epubVersion)) {
|
|
155
|
+
console.error(
|
|
156
|
+
`Error: Invalid EPUB version "${epubVersion}". Valid versions: ${EPUB_VERSIONS.join(", ")}`
|
|
157
|
+
);
|
|
158
|
+
process.exit(2);
|
|
159
|
+
}
|
|
160
|
+
if (mode && mode !== "exp" && !epubVersion) {
|
|
161
|
+
console.error(`Error: --epub-version (-v) is required when using --mode ${mode}`);
|
|
162
|
+
process.exit(2);
|
|
163
|
+
}
|
|
87
164
|
try {
|
|
88
165
|
if (!values.quiet) {
|
|
89
166
|
console.log(`Validating: ${basename(filePath)}`);
|
|
90
167
|
console.log();
|
|
91
168
|
}
|
|
92
|
-
const epubData = await readFile(filePath);
|
|
93
|
-
const startTime = Date.now();
|
|
94
169
|
const options = {};
|
|
95
170
|
if (values.profile) {
|
|
96
171
|
options.profile = values.profile;
|
|
97
172
|
}
|
|
173
|
+
if (epubVersion) {
|
|
174
|
+
options.version = epubVersion;
|
|
175
|
+
}
|
|
176
|
+
if (mode) {
|
|
177
|
+
options.mode = mode;
|
|
178
|
+
}
|
|
98
179
|
if (values.usage) {
|
|
99
180
|
options.includeUsage = true;
|
|
100
181
|
}
|
|
@@ -103,7 +184,28 @@ async function main() {
|
|
|
103
184
|
const cmContent = await readFile(values.customMessages, "utf-8");
|
|
104
185
|
options.customMessages = parseCustomMessages(cmContent);
|
|
105
186
|
}
|
|
106
|
-
const
|
|
187
|
+
const startTime = Date.now();
|
|
188
|
+
let result;
|
|
189
|
+
let effectiveMode = mode;
|
|
190
|
+
if (!mode || mode === "exp") {
|
|
191
|
+
const fileStat = await stat(filePath);
|
|
192
|
+
if (fileStat.isDirectory()) {
|
|
193
|
+
effectiveMode = "exp";
|
|
194
|
+
} else if (mode === "exp") {
|
|
195
|
+
console.error("Error: --mode exp requires a directory path");
|
|
196
|
+
process.exit(2);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (effectiveMode === "exp") {
|
|
200
|
+
const files = await readDirectoryFiles(filePath);
|
|
201
|
+
result = await EpubCheck.validateExpanded(files, options);
|
|
202
|
+
} else if (effectiveMode === "opf" || effectiveMode === "xhtml" || effectiveMode === "svg" || effectiveMode === "nav" || effectiveMode === "mo") {
|
|
203
|
+
const fileData = await readFile(filePath);
|
|
204
|
+
result = await EpubCheck.validateSingleFile(fileData, basename(filePath), options);
|
|
205
|
+
} else {
|
|
206
|
+
const epubData = await readFile(filePath);
|
|
207
|
+
result = await EpubCheck.validate(epubData, options, basename(filePath));
|
|
208
|
+
}
|
|
107
209
|
const elapsedMs = Date.now() - startTime;
|
|
108
210
|
const severityRank = {
|
|
109
211
|
fatal: 0,
|
|
@@ -116,7 +218,8 @@ async function main() {
|
|
|
116
218
|
if (values.fatal) maxRank = 0;
|
|
117
219
|
else if (values.error) maxRank = 1;
|
|
118
220
|
else if (values.warn) maxRank = 2;
|
|
119
|
-
|
|
221
|
+
else if (values.info) maxRank = 3;
|
|
222
|
+
const isFiltered = values.fatal || values.error || values.warn || values.info;
|
|
120
223
|
const displayMessages = result.messages.filter(
|
|
121
224
|
(m) => severityRank[m.severity] <= maxRank
|
|
122
225
|
);
|
|
@@ -194,7 +297,8 @@ async function main() {
|
|
|
194
297
|
console.log();
|
|
195
298
|
}
|
|
196
299
|
}
|
|
197
|
-
const
|
|
300
|
+
const failOnWarnings = values["fail-on-warnings"] || values.failonwarnings;
|
|
301
|
+
const shouldFail = result.errorCount > 0 || result.fatalCount > 0 || failOnWarnings && result.warningCount > 0;
|
|
198
302
|
process.exit(shouldFail ? 1 : 0);
|
|
199
303
|
} catch (error) {
|
|
200
304
|
console.error("\x1B[31mError:\x1B[0m", error instanceof Error ? error.message : String(error));
|
package/bin/epubcheck.ts
CHANGED
|
@@ -7,35 +7,88 @@
|
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
23
|
+
const { EpubCheck, EPUB_VERSIONS, toJSONReport } = await import('../dist/index.js');
|
|
24
|
+
|
|
25
|
+
const VERSION = '0.5.1';
|
|
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
|
+
}
|
|
39
92
|
|
|
40
93
|
// Show version
|
|
41
94
|
if (values.version) {
|
|
@@ -59,70 +112,131 @@ if (values.listChecks) {
|
|
|
59
112
|
if (values.help || positionals.length === 0) {
|
|
60
113
|
console.log(`EPUBCheck-TS v${VERSION} - EPUB Validator
|
|
61
114
|
|
|
62
|
-
Usage: epubcheck-ts <file
|
|
115
|
+
Usage: epubcheck-ts <file> [options]
|
|
63
116
|
|
|
64
117
|
Arguments:
|
|
65
|
-
<file
|
|
118
|
+
<file> Path to EPUB file, directory, or single file to validate
|
|
66
119
|
|
|
67
120
|
Options:
|
|
68
121
|
-j, --json <file> Output JSON report to file (use '-' for stdout)
|
|
69
122
|
-q, --quiet Suppress console output (errors only)
|
|
70
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)
|
|
71
126
|
-u, --usage Include usage messages (best practices)
|
|
72
127
|
-f, --fatal Show only fatal errors
|
|
73
128
|
-e, --error Show fatal errors and errors
|
|
74
|
-
|
|
129
|
+
-w, --warn Show fatal errors, errors, and warnings
|
|
130
|
+
-i, --info Show fatal, error, warning, and info messages
|
|
75
131
|
-c, --customMessages <file> Override message severities (TSV: ID\\tSEVERITY)
|
|
76
|
-
|
|
132
|
+
--fail-on-warnings Exit with code 1 if warnings are found
|
|
133
|
+
(also accepts --failonwarnings for Java compatibility)
|
|
77
134
|
-l, --listChecks List all message IDs and severities
|
|
78
|
-
-
|
|
135
|
+
-V, --version Show version information
|
|
79
136
|
-h, --help Show this help message
|
|
80
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
|
+
|
|
81
146
|
Examples:
|
|
82
147
|
epubcheck-ts book.epub
|
|
83
148
|
epubcheck-ts book.epub --json report.json
|
|
84
149
|
epubcheck-ts book.epub --quiet --fail-on-warnings
|
|
85
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
|
|
86
157
|
|
|
87
158
|
Exit Codes:
|
|
88
159
|
0 No errors (or only warnings if --fail-on-warnings not set)
|
|
89
160
|
1 Validation errors found (or warnings with --fail-on-warnings)
|
|
90
161
|
2 Runtime error (file not found, invalid arguments, etc.)
|
|
91
162
|
|
|
92
|
-
Note: This tool provides ~93% coverage of Java EPUBCheck features.
|
|
93
|
-
Missing features: single-file/directory validation, advanced ARIA, XHTML/SVG schema.
|
|
94
|
-
For complete EPUB 3 conformance testing, use: https://github.com/w3c/epubcheck
|
|
95
|
-
|
|
96
163
|
Report issues: https://github.com/likecoin/epubcheck-ts/issues
|
|
97
164
|
`);
|
|
98
165
|
process.exit(0);
|
|
99
166
|
}
|
|
100
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
|
+
|
|
101
192
|
// Main validation logic
|
|
102
193
|
async function main(): Promise<void> {
|
|
103
194
|
const filePath = positionals[0];
|
|
104
195
|
|
|
105
196
|
if (!filePath) {
|
|
106
|
-
console.error('Error: No
|
|
197
|
+
console.error('Error: No file specified');
|
|
107
198
|
console.error('Run with --help for usage information');
|
|
108
199
|
process.exit(2);
|
|
109
200
|
}
|
|
110
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
|
+
|
|
111
223
|
try {
|
|
112
|
-
// Read EPUB file
|
|
113
224
|
if (!values.quiet) {
|
|
114
225
|
console.log(`Validating: ${basename(filePath)}`);
|
|
115
226
|
console.log();
|
|
116
227
|
}
|
|
117
228
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// Validate
|
|
121
|
-
const startTime = Date.now();
|
|
229
|
+
// Build options
|
|
122
230
|
const options: EpubCheckOptions = {};
|
|
123
231
|
if (values.profile) {
|
|
124
232
|
options.profile = values.profile as EPUBProfile;
|
|
125
233
|
}
|
|
234
|
+
if (epubVersion) {
|
|
235
|
+
options.version = epubVersion as EPUBVersion;
|
|
236
|
+
}
|
|
237
|
+
if (mode) {
|
|
238
|
+
options.mode = mode;
|
|
239
|
+
}
|
|
126
240
|
if (values.usage) {
|
|
127
241
|
options.includeUsage = true;
|
|
128
242
|
}
|
|
@@ -131,10 +245,41 @@ async function main(): Promise<void> {
|
|
|
131
245
|
const cmContent = await readFile(values.customMessages, 'utf-8');
|
|
132
246
|
options.customMessages = parseCustomMessages(cmContent);
|
|
133
247
|
}
|
|
134
|
-
|
|
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
|
+
}
|
|
135
280
|
const elapsedMs = Date.now() - startTime;
|
|
136
281
|
|
|
137
|
-
// Most restrictive severity flag wins (--fatal
|
|
282
|
+
// Most restrictive severity flag wins (--fatal > --error > --warn > --info)
|
|
138
283
|
const severityRank: Record<Severity, number> = {
|
|
139
284
|
fatal: 0,
|
|
140
285
|
error: 1,
|
|
@@ -146,7 +291,8 @@ async function main(): Promise<void> {
|
|
|
146
291
|
if (values.fatal) maxRank = 0;
|
|
147
292
|
else if (values.error) maxRank = 1;
|
|
148
293
|
else if (values.warn) maxRank = 2;
|
|
149
|
-
|
|
294
|
+
else if (values.info) maxRank = 3;
|
|
295
|
+
const isFiltered = values.fatal || values.error || values.warn || values.info;
|
|
150
296
|
const displayMessages = result.messages.filter(
|
|
151
297
|
(m: ValidationMessage) => severityRank[m.severity] <= maxRank,
|
|
152
298
|
);
|
|
@@ -248,10 +394,9 @@ async function main(): Promise<void> {
|
|
|
248
394
|
}
|
|
249
395
|
|
|
250
396
|
// Determine exit code
|
|
397
|
+
const failOnWarnings = values['fail-on-warnings'] || values.failonwarnings;
|
|
251
398
|
const shouldFail =
|
|
252
|
-
result.errorCount > 0 ||
|
|
253
|
-
result.fatalCount > 0 ||
|
|
254
|
-
(values['fail-on-warnings'] && result.warningCount > 0);
|
|
399
|
+
result.errorCount > 0 || result.fatalCount > 0 || (failOnWarnings && result.warningCount > 0);
|
|
255
400
|
process.exit(shouldFail ? 1 : 0);
|
|
256
401
|
} catch (error) {
|
|
257
402
|
console.error('\x1b[31mError:\x1b[0m', error instanceof Error ? error.message : String(error));
|