@likecoin/epubcheck-ts 0.2.0 → 0.2.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 CHANGED
@@ -10,6 +10,7 @@ A TypeScript port of [EPUBCheck](https://github.com/w3c/epubcheck) - the officia
10
10
 
11
11
  ## Features
12
12
 
13
+ - **CLI and programmatic API**: Use as a command-line tool or integrate into your application
13
14
  - **Cross-platform**: Works in Node.js (18+) and modern browsers
14
15
  - **Partial EPUB validation**: Currently ~65% of EPUBCheck feature parity
15
16
  - **Zero native dependencies**: Pure JavaScript/WebAssembly, no compilation required
@@ -28,6 +29,49 @@ npm install @likecoin/epubcheck-ts
28
29
 
29
30
  ## Quick Start
30
31
 
32
+ ### Command Line Interface (CLI)
33
+
34
+ **Quick validation:**
35
+ ```bash
36
+ npx @likecoin/epubcheck-ts book.epub
37
+ ```
38
+
39
+ **Or install globally:**
40
+ ```bash
41
+ npm install -g @likecoin/epubcheck-ts
42
+ epubcheck-ts book.epub
43
+ ```
44
+
45
+ **CLI Options:**
46
+ ```bash
47
+ epubcheck-ts <file.epub> [options]
48
+
49
+ Options:
50
+ -j, --json <file> Output JSON report to file (use '-' for stdout)
51
+ -q, --quiet Suppress console output (errors only)
52
+ -p, --profile <name> Validation profile (default|dict|edupub|idx|preview)
53
+ -w, --fail-on-warnings Exit with code 1 if warnings are found
54
+ -v, --version Show version information
55
+ -h, --help Show this help message
56
+ ```
57
+
58
+ **Examples:**
59
+ ```bash
60
+ # Basic validation
61
+ epubcheck-ts book.epub
62
+
63
+ # Generate JSON report
64
+ epubcheck-ts book.epub --json report.json
65
+
66
+ # Quiet mode for CI/CD
67
+ epubcheck-ts book.epub --quiet --fail-on-warnings
68
+
69
+ # Validate with specific profile
70
+ epubcheck-ts dictionary.epub --profile dict
71
+ ```
72
+
73
+ **Note:** This CLI provides ~65% coverage of Java EPUBCheck features. For complete EPUB 3 conformance testing, use the [official Java EPUBCheck](https://github.com/w3c/epubcheck).
74
+
31
75
  ### ES Modules (recommended)
32
76
 
33
77
  ```typescript
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env node
2
+ import { readFile, writeFile } from "node:fs/promises";
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.2.1";
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
+ },
17
+ allowPositionals: true,
18
+ strict: false
19
+ });
20
+ if (values.version) {
21
+ console.log(`EPUBCheck-TS v${VERSION}`);
22
+ console.log("TypeScript EPUB validator for Node.js and browsers");
23
+ console.log();
24
+ console.log("Note: This is ~65% feature-complete compared to Java EPUBCheck.");
25
+ console.log("For production validation: https://github.com/w3c/epubcheck");
26
+ process.exit(0);
27
+ }
28
+ if (values.help || positionals.length === 0) {
29
+ console.log(`EPUBCheck-TS v${VERSION} - EPUB Validator
30
+
31
+ Usage: epubcheck-ts <file.epub> [options]
32
+
33
+ Arguments:
34
+ <file.epub> Path to EPUB file to validate
35
+
36
+ Options:
37
+ -j, --json <file> Output JSON report to file (use '-' for stdout)
38
+ -q, --quiet Suppress console output (errors only)
39
+ -p, --profile <name> Validation profile (default|dict|edupub|idx|preview)
40
+ -u, --usage Include usage messages (best practices)
41
+ -w, --fail-on-warnings Exit with code 1 if warnings are found
42
+ -v, --version Show version information
43
+ -h, --help Show this help message
44
+
45
+ Examples:
46
+ epubcheck-ts book.epub
47
+ epubcheck-ts book.epub --json report.json
48
+ epubcheck-ts book.epub --quiet --fail-on-warnings
49
+ epubcheck-ts book.epub --profile dict
50
+
51
+ Exit Codes:
52
+ 0 No errors (or only warnings if --fail-on-warnings not set)
53
+ 1 Validation errors found (or warnings with --fail-on-warnings)
54
+ 2 Runtime error (file not found, invalid arguments, etc.)
55
+
56
+ Note: This tool provides ~65% coverage of Java EPUBCheck features.
57
+ Missing features: Media Overlays, advanced ARIA checks, encryption/signatures.
58
+ For complete EPUB 3 conformance testing, use: https://github.com/w3c/epubcheck
59
+
60
+ Report issues: https://github.com/likecoin/epubcheck-ts/issues
61
+ `);
62
+ process.exit(0);
63
+ }
64
+ async function main() {
65
+ const filePath = positionals[0];
66
+ if (!filePath) {
67
+ console.error("Error: No EPUB file specified");
68
+ console.error("Run with --help for usage information");
69
+ process.exit(2);
70
+ }
71
+ try {
72
+ if (!values.quiet) {
73
+ console.log(`Validating: ${basename(filePath)}`);
74
+ console.log();
75
+ }
76
+ const epubData = await readFile(filePath);
77
+ const startTime = Date.now();
78
+ const options = {};
79
+ if (values.profile) {
80
+ options.profile = values.profile;
81
+ }
82
+ if (values.usage) {
83
+ options.includeUsage = true;
84
+ }
85
+ const result = await EpubCheck.validate(epubData, options);
86
+ const elapsedMs = Date.now() - startTime;
87
+ if (values.json !== void 0) {
88
+ const jsonContent = toJSONReport(result);
89
+ if (values.json === "-") {
90
+ if (values.quiet) {
91
+ console.log(jsonContent);
92
+ } else {
93
+ console.log("\nJSON Report:");
94
+ console.log(jsonContent);
95
+ }
96
+ } else if (typeof values.json === "string") {
97
+ await writeFile(values.json, jsonContent, "utf-8");
98
+ if (!values.quiet) {
99
+ console.log(`JSON report written to: ${values.json}`);
100
+ }
101
+ }
102
+ }
103
+ if (!values.quiet) {
104
+ const fatal = result.messages.filter((m) => m.severity === "fatal");
105
+ const errors = result.messages.filter((m) => m.severity === "error");
106
+ const warnings = result.messages.filter((m) => m.severity === "warning");
107
+ const info = result.messages.filter((m) => m.severity === "info");
108
+ const usage = result.messages.filter((m) => m.severity === "usage");
109
+ const printMessages = (messages, color, label) => {
110
+ if (messages.length === 0) return;
111
+ for (const msg of messages) {
112
+ const locationStr = msg.location ? ` (${msg.location.path}${msg.location.line !== void 0 ? `:${String(msg.location.line)}` : ""})` : "";
113
+ console.log(`${color}${label}${locationStr}: ${msg.message}\x1B[0m`);
114
+ if (msg.id) {
115
+ console.log(` \x1B[90mID: ${msg.id}\x1B[0m`);
116
+ }
117
+ }
118
+ console.log();
119
+ };
120
+ if (fatal.length > 0) {
121
+ printMessages(fatal, "\x1B[31m\x1B[1m", "FATAL");
122
+ }
123
+ if (errors.length > 0) {
124
+ printMessages(errors, "\x1B[31m", "ERROR");
125
+ }
126
+ if (warnings.length > 0) {
127
+ printMessages(warnings, "\x1B[33m", "WARNING");
128
+ }
129
+ if (info.length > 0 && result.messages.length < 20) {
130
+ printMessages(info, "\x1B[36m", "INFO");
131
+ }
132
+ if (usage.length > 0) {
133
+ printMessages(usage, "\x1B[90m", "USAGE");
134
+ }
135
+ console.log("\u2500".repeat(60));
136
+ const summaryColor = result.valid ? "\x1B[32m" : "\x1B[31m";
137
+ const summaryIcon = result.valid ? "\u2713" : "\u2717";
138
+ console.log(
139
+ `${summaryColor}${summaryIcon} ${result.valid ? "Valid EPUB" : "Invalid EPUB"}\x1B[0m`
140
+ );
141
+ console.log();
142
+ console.log(` Errors: ${String(result.errorCount + result.fatalCount)}`);
143
+ console.log(` Warnings: ${String(result.warningCount)}`);
144
+ if (info.length > 0) {
145
+ console.log(` Info: ${String(result.infoCount)}`);
146
+ }
147
+ if (usage.length > 0) {
148
+ console.log(` Usages: ${String(result.usageCount)}`);
149
+ }
150
+ console.log(` Time: ${String(elapsedMs)}ms`);
151
+ console.log();
152
+ if (result.errorCount === 0 && result.fatalCount === 0) {
153
+ console.log(
154
+ "\x1B[90mNote: This validator provides ~65% coverage of Java EPUBCheck.\x1B[0m"
155
+ );
156
+ console.log("\x1B[90mFor complete validation: https://github.com/w3c/epubcheck\x1B[0m");
157
+ console.log();
158
+ }
159
+ }
160
+ const shouldFail = result.errorCount > 0 || result.fatalCount > 0 || values["fail-on-warnings"] && result.warningCount > 0;
161
+ process.exit(shouldFail ? 1 : 0);
162
+ } catch (error) {
163
+ console.error("\x1B[31mError:\x1B[0m", error instanceof Error ? error.message : String(error));
164
+ if (error instanceof Error && error.stack && !values.quiet) {
165
+ console.error("\x1B[90m" + error.stack + "\x1B[0m");
166
+ }
167
+ process.exit(2);
168
+ }
169
+ }
170
+ void main();
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * EPUBCheck-TS CLI
4
+ *
5
+ * A minimalist command-line interface for EPUB validation.
6
+ * For full EPUBCheck features, use the official Java version:
7
+ * https://github.com/w3c/epubcheck
8
+ */
9
+
10
+ import { readFile, writeFile } from 'node:fs/promises';
11
+ import { parseArgs } from 'node:util';
12
+ import { basename } from 'node:path';
13
+
14
+ // Dynamic import to support both ESM and CJS builds
15
+ const { EpubCheck, toJSONReport } = await import('../dist/index.js');
16
+
17
+ const VERSION = '0.2.1';
18
+
19
+ // Parse command line arguments
20
+ const { values, positionals } = parseArgs({
21
+ options: {
22
+ json: { type: 'string', short: 'j' },
23
+ quiet: { type: 'boolean', short: 'q', default: false },
24
+ profile: { type: 'string', short: 'p' },
25
+ usage: { type: 'boolean', short: 'u', default: false },
26
+ version: { type: 'boolean', short: 'v', default: false },
27
+ help: { type: 'boolean', short: 'h', default: false },
28
+ 'fail-on-warnings': { type: 'boolean', short: 'w', default: false },
29
+ },
30
+ allowPositionals: true,
31
+ strict: false,
32
+ });
33
+
34
+ // Show version
35
+ if (values.version) {
36
+ console.log(`EPUBCheck-TS v${VERSION}`);
37
+ console.log('TypeScript EPUB validator for Node.js and browsers');
38
+ console.log();
39
+ console.log('Note: This is ~65% feature-complete compared to Java EPUBCheck.');
40
+ console.log('For production validation: https://github.com/w3c/epubcheck');
41
+ process.exit(0);
42
+ }
43
+
44
+ // Show help
45
+ if (values.help || positionals.length === 0) {
46
+ console.log(`EPUBCheck-TS v${VERSION} - EPUB Validator
47
+
48
+ Usage: epubcheck-ts <file.epub> [options]
49
+
50
+ Arguments:
51
+ <file.epub> Path to EPUB file to validate
52
+
53
+ Options:
54
+ -j, --json <file> Output JSON report to file (use '-' for stdout)
55
+ -q, --quiet Suppress console output (errors only)
56
+ -p, --profile <name> Validation profile (default|dict|edupub|idx|preview)
57
+ -u, --usage Include usage messages (best practices)
58
+ -w, --fail-on-warnings Exit with code 1 if warnings are found
59
+ -v, --version Show version information
60
+ -h, --help Show this help message
61
+
62
+ Examples:
63
+ epubcheck-ts book.epub
64
+ epubcheck-ts book.epub --json report.json
65
+ epubcheck-ts book.epub --quiet --fail-on-warnings
66
+ epubcheck-ts book.epub --profile dict
67
+
68
+ Exit Codes:
69
+ 0 No errors (or only warnings if --fail-on-warnings not set)
70
+ 1 Validation errors found (or warnings with --fail-on-warnings)
71
+ 2 Runtime error (file not found, invalid arguments, etc.)
72
+
73
+ Note: This tool provides ~65% coverage of Java EPUBCheck features.
74
+ Missing features: Media Overlays, advanced ARIA checks, encryption/signatures.
75
+ For complete EPUB 3 conformance testing, use: https://github.com/w3c/epubcheck
76
+
77
+ Report issues: https://github.com/likecoin/epubcheck-ts/issues
78
+ `);
79
+ process.exit(0);
80
+ }
81
+
82
+ // Main validation logic
83
+ async function main(): Promise<void> {
84
+ const filePath = positionals[0];
85
+
86
+ if (!filePath) {
87
+ console.error('Error: No EPUB file specified');
88
+ console.error('Run with --help for usage information');
89
+ process.exit(2);
90
+ }
91
+
92
+ try {
93
+ // Read EPUB file
94
+ if (!values.quiet) {
95
+ console.log(`Validating: ${basename(filePath)}`);
96
+ console.log();
97
+ }
98
+
99
+ const epubData = await readFile(filePath);
100
+
101
+ // Validate
102
+ const startTime = Date.now();
103
+ const options: {
104
+ profile?: 'default' | 'dict' | 'edupub' | 'idx' | 'preview';
105
+ includeUsage?: boolean;
106
+ } = {};
107
+ if (values.profile) {
108
+ options.profile = values.profile as 'default' | 'dict' | 'edupub' | 'idx' | 'preview';
109
+ }
110
+ if (values.usage) {
111
+ options.includeUsage = true;
112
+ }
113
+ const result = await EpubCheck.validate(epubData, options);
114
+ const elapsedMs = Date.now() - startTime;
115
+
116
+ // Output JSON report if requested
117
+ if (values.json !== undefined) {
118
+ const jsonContent = toJSONReport(result); // Already stringified
119
+
120
+ if (values.json === '-') {
121
+ // Output to stdout - suppress other output
122
+ if (values.quiet) {
123
+ console.log(jsonContent);
124
+ } else {
125
+ // If not quiet, output after other messages
126
+ console.log('\nJSON Report:');
127
+ console.log(jsonContent);
128
+ }
129
+ } else if (typeof values.json === 'string') {
130
+ await writeFile(values.json, jsonContent, 'utf-8');
131
+ if (!values.quiet) {
132
+ console.log(`JSON report written to: ${values.json}`);
133
+ }
134
+ }
135
+ }
136
+
137
+ // Console output (unless quiet mode)
138
+ if (!values.quiet) {
139
+ // Group messages by severity
140
+ const fatal = result.messages.filter((m) => m.severity === 'fatal');
141
+ const errors = result.messages.filter((m) => m.severity === 'error');
142
+ const warnings = result.messages.filter((m) => m.severity === 'warning');
143
+ const info = result.messages.filter((m) => m.severity === 'info');
144
+ const usage = result.messages.filter((m) => m.severity === 'usage');
145
+
146
+ // Print messages with colors
147
+ const printMessages = (
148
+ messages: typeof result.messages,
149
+ color: string,
150
+ label: string,
151
+ ): void => {
152
+ if (messages.length === 0) return;
153
+
154
+ for (const msg of messages) {
155
+ const locationStr = msg.location
156
+ ? ` (${msg.location.path}${msg.location.line !== undefined ? `:${String(msg.location.line)}` : ''})`
157
+ : '';
158
+ console.log(`${color}${label}${locationStr}: ${msg.message}\x1b[0m`);
159
+ if (msg.id) {
160
+ console.log(` \x1b[90mID: ${msg.id}\x1b[0m`);
161
+ }
162
+ }
163
+ console.log();
164
+ };
165
+
166
+ if (fatal.length > 0) {
167
+ printMessages(fatal, '\x1b[31m\x1b[1m', 'FATAL');
168
+ }
169
+ if (errors.length > 0) {
170
+ printMessages(errors, '\x1b[31m', 'ERROR');
171
+ }
172
+ if (warnings.length > 0) {
173
+ printMessages(warnings, '\x1b[33m', 'WARNING');
174
+ }
175
+ if (info.length > 0 && result.messages.length < 20) {
176
+ // Only show info if total messages is small
177
+ printMessages(info, '\x1b[36m', 'INFO');
178
+ }
179
+ if (usage.length > 0) {
180
+ printMessages(usage, '\x1b[90m', 'USAGE');
181
+ }
182
+
183
+ // Summary
184
+ console.log('─'.repeat(60));
185
+ const summaryColor = result.valid ? '\x1b[32m' : '\x1b[31m';
186
+ const summaryIcon = result.valid ? '✓' : '✗';
187
+ console.log(
188
+ `${summaryColor}${summaryIcon} ${result.valid ? 'Valid EPUB' : 'Invalid EPUB'}\x1b[0m`,
189
+ );
190
+ console.log();
191
+ console.log(` Errors: ${String(result.errorCount + result.fatalCount)}`);
192
+ console.log(` Warnings: ${String(result.warningCount)}`);
193
+ if (info.length > 0) {
194
+ console.log(` Info: ${String(result.infoCount)}`);
195
+ }
196
+ if (usage.length > 0) {
197
+ console.log(` Usages: ${String(result.usageCount)}`);
198
+ }
199
+ console.log(` Time: ${String(elapsedMs)}ms`);
200
+ console.log();
201
+
202
+ // Show limitation notice if there were no major errors
203
+ if (result.errorCount === 0 && result.fatalCount === 0) {
204
+ console.log(
205
+ '\x1b[90mNote: This validator provides ~65% coverage of Java EPUBCheck.\x1b[0m',
206
+ );
207
+ console.log('\x1b[90mFor complete validation: https://github.com/w3c/epubcheck\x1b[0m');
208
+ console.log();
209
+ }
210
+ }
211
+
212
+ // Determine exit code
213
+ const shouldFail =
214
+ result.errorCount > 0 ||
215
+ result.fatalCount > 0 ||
216
+ (values['fail-on-warnings'] && result.warningCount > 0);
217
+ process.exit(shouldFail ? 1 : 0);
218
+ } catch (error) {
219
+ console.error('\x1b[31mError:\x1b[0m', error instanceof Error ? error.message : String(error));
220
+ if (error instanceof Error && error.stack && !values.quiet) {
221
+ console.error('\x1b[90m' + error.stack + '\x1b[0m');
222
+ }
223
+ process.exit(2);
224
+ }
225
+ }
226
+
227
+ void main();
package/dist/index.cjs CHANGED
@@ -1380,6 +1380,7 @@ var OCFValidator = class {
1380
1380
  "metadata.xml"
1381
1381
  ]);
1382
1382
  for (const file of metaInfFiles) {
1383
+ if (file === "META-INF/") continue;
1383
1384
  const filename = file.replace("META-INF/", "");
1384
1385
  if (!allowedFiles.has(filename)) {
1385
1386
  messages.push({
@@ -1397,6 +1398,7 @@ var OCFValidator = class {
1397
1398
  validateFilenames(zip, messages) {
1398
1399
  for (const path of zip.paths) {
1399
1400
  if (path === "mimetype") continue;
1401
+ if (path.endsWith("/")) continue;
1400
1402
  const filename = path.includes("/") ? path.split("/").pop() ?? path : path;
1401
1403
  if (filename === "" || filename === "." || filename === "..") {
1402
1404
  messages.push({
@@ -2220,12 +2222,13 @@ var OPFValidator = class {
2220
2222
  }
2221
2223
  declaredPaths.add(opfPath);
2222
2224
  for (const filePath of context.files.keys()) {
2225
+ if (filePath.endsWith("/")) continue;
2223
2226
  if (filePath.startsWith("META-INF/")) continue;
2224
2227
  if (filePath === "mimetype") continue;
2225
2228
  if (declaredPaths.has(filePath)) continue;
2226
2229
  context.messages.push({
2227
2230
  id: "RSC-008",
2228
- severity: "warning",
2231
+ severity: "error",
2229
2232
  message: `File in container is not declared in manifest: ${filePath}`,
2230
2233
  location: { path: filePath }
2231
2234
  });
@@ -3309,7 +3312,16 @@ var EpubCheck = class _EpubCheck {
3309
3312
  });
3310
3313
  }
3311
3314
  const elapsedMs = performance.now() - startTime;
3312
- return buildReport(context.messages, context.version, elapsedMs);
3315
+ const filteredMessages = context.messages.filter((msg) => {
3316
+ if (!this.options.includeUsage && msg.severity === "usage") {
3317
+ return false;
3318
+ }
3319
+ if (!this.options.includeInfo && msg.severity === "info") {
3320
+ return false;
3321
+ }
3322
+ return true;
3323
+ });
3324
+ return buildReport(filteredMessages, context.version, elapsedMs);
3313
3325
  }
3314
3326
  /**
3315
3327
  * Static method to validate an EPUB file with default options
@@ -3353,20 +3365,20 @@ var EpubCheck = class _EpubCheck {
3353
3365
  const ncxContent = new TextDecoder().decode(ncxData);
3354
3366
  const ncxValidator = new NCXValidator();
3355
3367
  ncxValidator.validate(context, ncxContent, ncxPath);
3356
- if (context.ncxUid) {
3357
- const dcIdentifiers = context.packageDocument.dcElements.filter(
3358
- (dc) => dc.name === "identifier"
3368
+ if (context.ncxUid && context.packageDocument.uniqueIdentifier) {
3369
+ const uniqueIdRef = context.packageDocument.uniqueIdentifier;
3370
+ const matchingIdentifier = context.packageDocument.dcElements.find(
3371
+ (dc) => dc.name === "identifier" && dc.id === uniqueIdRef
3359
3372
  );
3360
- for (const dc of dcIdentifiers) {
3361
- const opfUid = dc.value.trim();
3373
+ if (matchingIdentifier) {
3374
+ const opfUid = matchingIdentifier.value.trim();
3362
3375
  if (context.ncxUid !== opfUid) {
3363
3376
  context.messages.push({
3364
3377
  id: "NCX-001",
3365
- severity: "warning",
3378
+ severity: "error",
3366
3379
  message: `NCX uid "${context.ncxUid}" does not match OPF unique identifier "${opfUid}"`,
3367
3380
  location: { path: ncxPath }
3368
3381
  });
3369
- break;
3370
3382
  }
3371
3383
  }
3372
3384
  }