@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 CHANGED
@@ -1,25 +1,24 @@
1
1
  # epubcheck-ts
2
2
 
3
- A TypeScript port of [EPUBCheck](https://github.com/w3c/epubcheck) - the official conformance checker for EPUB publications.
3
+ Validate EPUB files in Node.js and the browser. A TypeScript implementation of [EPUBCheck](https://github.com/w3c/epubcheck).
4
4
 
5
5
  [![CI](https://github.com/likecoin/epubcheck-ts/actions/workflows/ci.yml/badge.svg)](https://github.com/likecoin/epubcheck-ts/actions/workflows/ci.yml)
6
6
  [![npm](https://img.shields.io/npm/v/%40likecoin%2Fepubcheck-ts)](https://www.npmjs.com/package/@likecoin/epubcheck-ts)
7
7
  [![License](https://img.shields.io/npm/l/%40likecoin%2Fepubcheck-ts)](./LICENSE)
8
8
 
9
- > **Note**: This library is primarily developed for internal use at [3ook.com](https://3ook.com/about) and is built with AI-assisted development. While it has comprehensive test coverage (892 passing / 945 total tests) and ~88% feature parity with Java EPUBCheck, it may not be suitable for mission-critical production workloads. For production environments requiring full EPUB validation, consider using the official [Java EPUBCheck](https://github.com/w3c/epubcheck). Contributions and feedback are welcome!
9
+ > **Status**: 93% feature parity with Java EPUBCheck (1061 tests passing, 74 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
 
13
- - **CLI and programmatic API**: Use as a command-line tool or integrate into your application
14
- - **Cross-platform**: Works in Node.js (18+) and modern browsers
15
- - **Partial EPUB validation**: Currently ~88% of EPUBCheck feature parity
16
- - **Zero native dependencies**: Pure JavaScript/WebAssembly, no compilation required
17
- - **TypeScript first**: Full type definitions included
18
- - **Tree-shakable**: ESM with proper exports for optimal bundling
13
+ - **CLI and API** Use as a CLI tool (`npx @likecoin/epubcheck-ts book.epub`) or import as a library
14
+ - **Browser support** — Works in Node.js 18+ and modern browsers via pure JS + WASM
15
+ - **No native dependencies** No Java, no compilation — `npm install` and go
16
+ - **TypeScript** Full type definitions included
17
+ - **Tree-shakable** ESM with proper exports for minimal bundle impact
19
18
 
20
19
  ## Try it Online
21
20
 
22
- Try the live demo at **[likecoin.github.io/epubcheck-ts](https://likecoin.github.io/epubcheck-ts/)** - validate your EPUB files directly in the browser without uploading to any server.
21
+ Live demo at **[likecoin.github.io/epubcheck-ts](https://likecoin.github.io/epubcheck-ts/)** validate EPUB files in the browser without uploading to any server.
23
22
 
24
23
  ## Installation
25
24
 
@@ -50,7 +49,13 @@ Options:
50
49
  -j, --json <file> Output JSON report to file (use '-' for stdout)
51
50
  -q, --quiet Suppress console output (errors only)
52
51
  -p, --profile <name> Validation profile (default|dict|edupub|idx|preview)
52
+ -u, --usage Include usage messages (best practices)
53
+ -f, --fatal Show only fatal errors
54
+ -e, --error Show fatal errors and errors
55
+ --warn Show fatal errors, errors, and warnings
56
+ -c, --customMessages <file> Override message severities (TSV: ID<tab>SEVERITY)
53
57
  -w, --fail-on-warnings Exit with code 1 if warnings are found
58
+ -l, --listChecks List all message IDs and severities
54
59
  -v, --version Show version information
55
60
  -h, --help Show this help message
56
61
  ```
@@ -68,9 +73,14 @@ epubcheck-ts book.epub --quiet --fail-on-warnings
68
73
 
69
74
  # Validate with specific profile
70
75
  epubcheck-ts dictionary.epub --profile dict
71
- ```
72
76
 
73
- **Note:** This CLI provides ~88% coverage of Java EPUBCheck features. For complete EPUB 3 conformance testing, use the [official Java EPUBCheck](https://github.com/w3c/epubcheck).
77
+ # Show only errors (hide warnings/info)
78
+ epubcheck-ts book.epub --error
79
+
80
+ # Enable suppressed accessibility checks
81
+ printf "ACC-004\tWARNING\nACC-005\tWARNING\n" > overrides.txt
82
+ epubcheck-ts book.epub -c overrides.txt
83
+ ```
74
84
 
75
85
  ### ES Modules (recommended)
76
86
 
@@ -181,6 +191,9 @@ interface EpubCheckOptions {
181
191
 
182
192
  /** Locale for messages (default: 'en') */
183
193
  locale?: string;
194
+
195
+ /** Custom message severity overrides (message ID → severity) */
196
+ customMessages?: Map<string, MessageSeverity>;
184
197
  }
185
198
  ```
186
199
 
@@ -266,20 +279,20 @@ This library is a TypeScript port of the Java-based [EPUBCheck](https://github.c
266
279
 
267
280
  | Component | Status | Completeness | Notes |
268
281
  |-----------|--------|--------------|-------|
269
- | OCF Container | 🟡 Partial | ~90% | ZIP structure, mimetype, container.xml, encryption.xml obfuscation |
270
- | Package Document (OPF) | 🟢 Complete | ~90% | Metadata, manifest, spine, collections, Schematron-equivalent checks |
271
- | Content Documents | 🟡 Partial | ~85% | XHTML structure, CSS url(), @import, SVG, entities, title, SSML, XML version |
272
- | Navigation Document | 🟢 Complete | ~85% | Nav content model, landmarks, labels, reading order, hidden |
273
- | Schema Validation | 🟡 Partial | ~50% | RelaxNG for OPF/container; XHTML/SVG disabled (libxml2 limitation) |
274
- | CSS | 🟡 Partial | ~70% | @font-face, @import, url() extraction, position, forbidden properties |
275
- | Cross-reference Validation | 🟢 Complete | ~90% | Reference tracking, fragments, fallbacks, remote resources |
276
- | Accessibility Checks | 🟡 Partial | ~30% | Basic checks only (empty links, image alt, SVG titles) |
277
- | Media Overlays | Not Started | 0% | Planned |
282
+ | OCF Container | 🟢 Complete | ~92% | ZIP structure, mimetype, container.xml, encryption.xml obfuscation |
283
+ | Package Document (OPF) | 🟢 Complete | ~92% | Metadata, manifest, spine, collections, Schematron-equivalent checks |
284
+ | Content Documents | 🟢 Complete | ~93% | XHTML structure, CSS url(), @import, SVG, entities, title, SSML, XML version |
285
+ | Navigation Document | 🟢 Complete | ~95% | Nav content model, landmarks, labels, reading order, hidden, nested-ol |
286
+ | Schema Validation | 🟡 Partial | ~55% | RelaxNG for OPF/container; XHTML/SVG disabled (libxml2 limitation) |
287
+ | CSS | 🟡 Partial | ~85% | @font-face, @import, url() extraction, position, forbidden properties, alt style tags |
288
+ | Cross-reference Validation | 🟢 Complete | ~92% | Reference tracking, fragments, fallbacks, remote resources, cross-document features |
289
+ | Accessibility Checks | 🟢 Complete | ~71% | 12/17 ACC checks: table, image alt, hyperlink, MathML, SVG, epub:type, OPF metadata |
290
+ | Media Overlays | 🟡 Partial | ~70% | SMIL structure, timing, audio, OPF metadata, duration validation |
278
291
  | Media Validation | ❌ Not Started | 0% | Planned |
279
292
 
280
293
  Legend: 🟢 Complete | 🟡 Partial | 🔴 Basic | ❌ Not Started
281
294
 
282
- **Overall Progress: ~88% of Java EPUBCheck features**
295
+ **Overall Progress: ~93% of Java EPUBCheck features**
283
296
 
284
297
  See [PROJECT_STATUS.md](./PROJECT_STATUS.md) for detailed comparison.
285
298
 
@@ -364,13 +377,13 @@ Legend: ✅ Implemented
364
377
  | Aspect | epubcheck-ts | EPUBCheck (Java) |
365
378
  |--------|--------------|------------------|
366
379
  | Runtime | Node.js / Browser | JVM |
367
- | Feature Parity | ~88% | 100% |
380
+ | Feature Parity | ~93% | 100% |
368
381
  | Bundle Size | ~450KB JS + ~1.6MB WASM | ~15MB |
369
382
  | Installation | `npm install` | Download JAR |
370
383
  | Integration | Native JS/TS | CLI or Java API |
371
384
  | Performance | Comparable | Baseline |
372
385
 
373
- **Note:** epubcheck-ts is currently in active development. See [PROJECT_STATUS.md](./PROJECT_STATUS.md) for detailed feature comparison.
386
+ See [PROJECT_STATUS.md](./PROJECT_STATUS.md) for detailed feature comparison.
374
387
 
375
388
  ## Contributing
376
389
 
package/bin/epubcheck.js CHANGED
@@ -1,28 +1,60 @@
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.3.9";
7
- const { values, positionals } = parseArgs({
8
- options: {
9
- json: { type: "string", short: "j" },
10
- quiet: { type: "boolean", short: "q", default: false },
11
- profile: { type: "string", short: "p" },
12
- usage: { type: "boolean", short: "u", default: false },
13
- version: { type: "boolean", short: "v", default: false },
14
- help: { type: "boolean", short: "h", default: false },
15
- "fail-on-warnings": { type: "boolean", short: "w", default: false },
16
- listChecks: { type: "boolean", short: "l", default: false }
17
- },
18
- allowPositionals: true,
19
- strict: false
20
- });
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.0";
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
+ }
21
53
  if (values.version) {
22
54
  console.log(`EPUBCheck-TS v${VERSION}`);
23
55
  console.log("TypeScript EPUB validator for Node.js and browsers");
24
56
  console.log();
25
- console.log("Note: This is ~88% feature-complete compared to Java EPUBCheck.");
57
+ console.log("Note: This is ~93% feature-complete compared to Java EPUBCheck.");
26
58
  console.log("For production validation: https://github.com/w3c/epubcheck");
27
59
  process.exit(0);
28
60
  }
@@ -35,65 +67,165 @@ if (values.listChecks) {
35
67
  if (values.help || positionals.length === 0) {
36
68
  console.log(`EPUBCheck-TS v${VERSION} - EPUB Validator
37
69
 
38
- Usage: epubcheck-ts <file.epub> [options]
70
+ Usage: epubcheck-ts <file> [options]
39
71
 
40
72
  Arguments:
41
- <file.epub> Path to EPUB file to validate
73
+ <file> Path to EPUB file, directory, or single file to validate
42
74
 
43
75
  Options:
44
76
  -j, --json <file> Output JSON report to file (use '-' for stdout)
45
77
  -q, --quiet Suppress console output (errors only)
46
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)
47
81
  -u, --usage Include usage messages (best practices)
48
- -w, --fail-on-warnings Exit with code 1 if warnings are found
82
+ -f, --fatal Show only fatal errors
83
+ -e, --error Show fatal errors and errors
84
+ -w, --warn Show fatal errors, errors, and warnings
85
+ -i, --info Show fatal, error, warning, and info messages
86
+ -c, --customMessages <file> Override message severities (TSV: ID\\tSEVERITY)
87
+ --fail-on-warnings Exit with code 1 if warnings are found
88
+ (also accepts --failonwarnings for Java compatibility)
49
89
  -l, --listChecks List all message IDs and severities
50
- -v, --version Show version information
90
+ -V, --version Show version information
51
91
  -h, --help Show this help message
52
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
+
53
101
  Examples:
54
102
  epubcheck-ts book.epub
55
103
  epubcheck-ts book.epub --json report.json
56
104
  epubcheck-ts book.epub --quiet --fail-on-warnings
57
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
58
112
 
59
113
  Exit Codes:
60
114
  0 No errors (or only warnings if --fail-on-warnings not set)
61
115
  1 Validation errors found (or warnings with --fail-on-warnings)
62
116
  2 Runtime error (file not found, invalid arguments, etc.)
63
117
 
64
- Note: This tool provides ~88% coverage of Java EPUBCheck features.
65
- Missing features: Media Overlays, advanced ARIA checks, encryption/signatures.
66
- For complete EPUB 3 conformance testing, use: https://github.com/w3c/epubcheck
67
-
68
118
  Report issues: https://github.com/likecoin/epubcheck-ts/issues
69
119
  `);
70
120
  process.exit(0);
71
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
+ }
72
140
  async function main() {
73
141
  const filePath = positionals[0];
74
142
  if (!filePath) {
75
- console.error("Error: No EPUB file specified");
143
+ console.error("Error: No file specified");
76
144
  console.error("Run with --help for usage information");
77
145
  process.exit(2);
78
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
+ }
79
164
  try {
80
165
  if (!values.quiet) {
81
166
  console.log(`Validating: ${basename(filePath)}`);
82
167
  console.log();
83
168
  }
84
- const epubData = await readFile(filePath);
85
- const startTime = Date.now();
86
169
  const options = {};
87
170
  if (values.profile) {
88
171
  options.profile = values.profile;
89
172
  }
173
+ if (epubVersion) {
174
+ options.version = epubVersion;
175
+ }
176
+ if (mode) {
177
+ options.mode = mode;
178
+ }
90
179
  if (values.usage) {
91
180
  options.includeUsage = true;
92
181
  }
93
- const result = await EpubCheck.validate(epubData, options);
182
+ if (typeof values.customMessages === "string") {
183
+ const { parseCustomMessages } = await import("../dist/index.js");
184
+ const cmContent = await readFile(values.customMessages, "utf-8");
185
+ options.customMessages = parseCustomMessages(cmContent);
186
+ }
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
+ }
94
209
  const elapsedMs = Date.now() - startTime;
210
+ const severityRank = {
211
+ fatal: 0,
212
+ error: 1,
213
+ warning: 2,
214
+ info: 3,
215
+ usage: 4
216
+ };
217
+ let maxRank = 4;
218
+ if (values.fatal) maxRank = 0;
219
+ else if (values.error) maxRank = 1;
220
+ else if (values.warn) maxRank = 2;
221
+ else if (values.info) maxRank = 3;
222
+ const isFiltered = values.fatal || values.error || values.warn || values.info;
223
+ const displayMessages = result.messages.filter(
224
+ (m) => severityRank[m.severity] <= maxRank
225
+ );
95
226
  if (values.json !== void 0) {
96
- const jsonContent = toJSONReport(result);
227
+ const filteredResult = isFiltered ? { ...result, messages: displayMessages } : result;
228
+ const jsonContent = toJSONReport(filteredResult);
97
229
  if (values.json === "-") {
98
230
  if (values.quiet) {
99
231
  console.log(jsonContent);
@@ -109,11 +241,11 @@ async function main() {
109
241
  }
110
242
  }
111
243
  if (!values.quiet) {
112
- const fatal = result.messages.filter((m) => m.severity === "fatal");
113
- const errors = result.messages.filter((m) => m.severity === "error");
114
- const warnings = result.messages.filter((m) => m.severity === "warning");
115
- const info = result.messages.filter((m) => m.severity === "info");
116
- const usage = result.messages.filter((m) => m.severity === "usage");
244
+ const fatal = displayMessages.filter((m) => m.severity === "fatal");
245
+ const errors = displayMessages.filter((m) => m.severity === "error");
246
+ const warnings = displayMessages.filter((m) => m.severity === "warning");
247
+ const info = displayMessages.filter((m) => m.severity === "info");
248
+ const usage = displayMessages.filter((m) => m.severity === "usage");
117
249
  const printMessages = (messages, color, label) => {
118
250
  if (messages.length === 0) return;
119
251
  for (const msg of messages) {
@@ -134,7 +266,7 @@ async function main() {
134
266
  if (warnings.length > 0) {
135
267
  printMessages(warnings, "\x1B[33m", "WARNING");
136
268
  }
137
- if (info.length > 0 && result.messages.length < 20) {
269
+ if (info.length > 0 && displayMessages.length < 20) {
138
270
  printMessages(info, "\x1B[36m", "INFO");
139
271
  }
140
272
  if (usage.length > 0) {
@@ -159,13 +291,14 @@ async function main() {
159
291
  console.log();
160
292
  if (result.errorCount === 0 && result.fatalCount === 0) {
161
293
  console.log(
162
- "\x1B[90mNote: This validator provides ~88% coverage of Java EPUBCheck.\x1B[0m"
294
+ "\x1B[90mNote: This validator provides ~93% coverage of Java EPUBCheck.\x1B[0m"
163
295
  );
164
296
  console.log("\x1B[90mFor complete validation: https://github.com/w3c/epubcheck\x1B[0m");
165
297
  console.log();
166
298
  }
167
299
  }
168
- const shouldFail = result.errorCount > 0 || result.fatalCount > 0 || values["fail-on-warnings"] && result.warningCount > 0;
300
+ const failOnWarnings = values["fail-on-warnings"] || values.failonwarnings;
301
+ const shouldFail = result.errorCount > 0 || result.fatalCount > 0 || failOnWarnings && result.warningCount > 0;
169
302
  process.exit(shouldFail ? 1 : 0);
170
303
  } catch (error) {
171
304
  console.error("\x1B[31mError:\x1B[0m", error instanceof Error ? error.message : String(error));