@likecoin/epubcheck-ts 0.2.3 → 0.2.4
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 +8 -8
- package/bin/epubcheck.js +4 -4
- package/bin/epubcheck.ts +4 -4
- package/dist/index.cjs +265 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +265 -14
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/schemas/applications.rng +0 -429
- package/schemas/aria.rng +0 -3355
- package/schemas/block.rng +0 -488
- package/schemas/common.rng +0 -1076
- package/schemas/container.rng +0 -24
- package/schemas/core-scripting.rng +0 -950
- package/schemas/data.rng +0 -161
- package/schemas/datatypes.rng +0 -401
- package/schemas/embed.rng +0 -980
- package/schemas/epub-mathml3-inc.rng +0 -161
- package/schemas/epub-nav-30.rnc +0 -44
- package/schemas/epub-nav-30.rng +0 -19985
- package/schemas/epub-nav-30.sch +0 -87
- package/schemas/epub-prefix-attr.rng +0 -17
- package/schemas/epub-shared-inc.rng +0 -29
- package/schemas/epub-ssml-attrs.rng +0 -17
- package/schemas/epub-svg-30.rnc +0 -17
- package/schemas/epub-svg-30.rng +0 -19903
- package/schemas/epub-svg-30.sch +0 -7
- package/schemas/epub-svg-forgiving-inc.rng +0 -315
- package/schemas/epub-switch.rng +0 -121
- package/schemas/epub-trigger.rng +0 -90
- package/schemas/epub-type-attr.rng +0 -12
- package/schemas/epub-xhtml-30.rnc +0 -6
- package/schemas/epub-xhtml-30.rng +0 -19882
- package/schemas/epub-xhtml-30.sch +0 -409
- package/schemas/epub-xhtml-inc.rng +0 -151
- package/schemas/epub-xhtml-integration.rng +0 -565
- package/schemas/epub-xhtml-svg-mathml.rng +0 -17
- package/schemas/form-datatypes.rng +0 -54
- package/schemas/mathml3-common.rng +0 -336
- package/schemas/mathml3-content.rng +0 -1552
- package/schemas/mathml3-inc.rng +0 -30
- package/schemas/mathml3-presentation.rng +0 -2341
- package/schemas/mathml3-strict-content.rng +0 -205
- package/schemas/media.rng +0 -374
- package/schemas/meta.rng +0 -754
- package/schemas/microdata.rng +0 -192
- package/schemas/ncx.rng +0 -308
- package/schemas/ocf-container-30.rnc +0 -37
- package/schemas/ocf-container-30.rng +0 -568
- package/schemas/opf.rng +0 -15
- package/schemas/opf20.rng +0 -513
- package/schemas/package-30.rnc +0 -133
- package/schemas/package-30.rng +0 -1153
- package/schemas/package-30.sch +0 -444
- package/schemas/phrase.rng +0 -746
- package/schemas/rdfa.rng +0 -552
- package/schemas/revision.rng +0 -106
- package/schemas/ruby.rng +0 -141
- package/schemas/sectional.rng +0 -278
- package/schemas/structural.rng +0 -298
- package/schemas/tables.rng +0 -420
- package/schemas/web-components.rng +0 -184
- package/schemas/web-forms.rng +0 -975
- package/schemas/web-forms2.rng +0 -1236
package/README.md
CHANGED
|
@@ -6,13 +6,13 @@ A TypeScript port of [EPUBCheck](https://github.com/w3c/epubcheck) - the officia
|
|
|
6
6
|
[](https://www.npmjs.com/package/@likecoin/epubcheck-ts)
|
|
7
7
|
[](./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 (
|
|
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 (467 tests) and ~70% 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!
|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
13
|
- **CLI and programmatic API**: Use as a command-line tool or integrate into your application
|
|
14
14
|
- **Cross-platform**: Works in Node.js (18+) and modern browsers
|
|
15
|
-
- **Partial EPUB validation**: Currently ~
|
|
15
|
+
- **Partial EPUB validation**: Currently ~70% of EPUBCheck feature parity
|
|
16
16
|
- **Zero native dependencies**: Pure JavaScript/WebAssembly, no compilation required
|
|
17
17
|
- **TypeScript first**: Full type definitions included
|
|
18
18
|
- **Tree-shakable**: ESM with proper exports for optimal bundling
|
|
@@ -70,7 +70,7 @@ epubcheck-ts book.epub --quiet --fail-on-warnings
|
|
|
70
70
|
epubcheck-ts dictionary.epub --profile dict
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
**Note:** This CLI provides ~
|
|
73
|
+
**Note:** This CLI provides ~70% coverage of Java EPUBCheck features. For complete EPUB 3 conformance testing, use the [official Java EPUBCheck](https://github.com/w3c/epubcheck).
|
|
74
74
|
|
|
75
75
|
### ES Modules (recommended)
|
|
76
76
|
|
|
@@ -268,16 +268,16 @@ This library is a TypeScript port of the Java-based [EPUBCheck](https://github.c
|
|
|
268
268
|
| Package Document (OPF) | 🟡 Partial | ~70% | Metadata, manifest, spine, collections, version/date validation |
|
|
269
269
|
| Content Documents | 🟡 Partial | ~70% | XHTML structure, script/MathML/SVG detection, link validation |
|
|
270
270
|
| Navigation Document | 🟡 Partial | ~40% | Nav structure, NCX validation, remote link validation |
|
|
271
|
-
| Schema Validation | 🟡 Partial | ~
|
|
271
|
+
| Schema Validation | 🟡 Partial | ~50% | RelaxNG for OPF/container; XHTML/SVG disabled (libxml2 limitation) |
|
|
272
272
|
| CSS | 🟡 Partial | ~50% | @font-face, @import, media overlay classes, position warnings |
|
|
273
273
|
| Cross-reference Validation | 🟡 Partial | ~75% | Reference tracking, fragment validation, undeclared resources |
|
|
274
|
-
| Accessibility Checks | 🟡 Partial | ~
|
|
274
|
+
| Accessibility Checks | 🟡 Partial | ~30% | Basic checks only (empty links, image alt, SVG titles) |
|
|
275
275
|
| Media Overlays | ❌ Not Started | 0% | Planned |
|
|
276
276
|
| Media Validation | ❌ Not Started | 0% | Planned |
|
|
277
277
|
|
|
278
278
|
Legend: 🟢 Complete | 🟡 Partial | 🔴 Basic | ❌ Not Started
|
|
279
279
|
|
|
280
|
-
**Overall Progress: ~
|
|
280
|
+
**Overall Progress: ~70% of Java EPUBCheck features**
|
|
281
281
|
|
|
282
282
|
See [PROJECT_STATUS.md](./PROJECT_STATUS.md) for detailed comparison.
|
|
283
283
|
|
|
@@ -357,8 +357,8 @@ Legend: ✅ Implemented
|
|
|
357
357
|
| Aspect | epubcheck-ts | EPUBCheck (Java) |
|
|
358
358
|
|--------|--------------|------------------|
|
|
359
359
|
| Runtime | Node.js / Browser | JVM |
|
|
360
|
-
| Feature Parity | ~
|
|
361
|
-
| Bundle Size | ~
|
|
360
|
+
| Feature Parity | ~70% | 100% |
|
|
361
|
+
| Bundle Size | ~450KB JS + ~1.6MB WASM | ~15MB |
|
|
362
362
|
| Installation | `npm install` | Download JAR |
|
|
363
363
|
| Integration | Native JS/TS | CLI or Java API |
|
|
364
364
|
| Performance | Comparable | Baseline |
|
package/bin/epubcheck.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readFile, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
4
|
import { basename } from "node:path";
|
|
5
5
|
const { EpubCheck, toJSONReport } = await import("../dist/index.js");
|
|
6
|
-
const VERSION = "0.2.
|
|
6
|
+
const VERSION = "0.2.4";
|
|
7
7
|
const { values, positionals } = parseArgs({
|
|
8
8
|
options: {
|
|
9
9
|
json: { type: "string", short: "j" },
|
|
@@ -21,7 +21,7 @@ if (values.version) {
|
|
|
21
21
|
console.log(`EPUBCheck-TS v${VERSION}`);
|
|
22
22
|
console.log("TypeScript EPUB validator for Node.js and browsers");
|
|
23
23
|
console.log();
|
|
24
|
-
console.log("Note: This is ~
|
|
24
|
+
console.log("Note: This is ~70% feature-complete compared to Java EPUBCheck.");
|
|
25
25
|
console.log("For production validation: https://github.com/w3c/epubcheck");
|
|
26
26
|
process.exit(0);
|
|
27
27
|
}
|
|
@@ -53,7 +53,7 @@ Exit Codes:
|
|
|
53
53
|
1 Validation errors found (or warnings with --fail-on-warnings)
|
|
54
54
|
2 Runtime error (file not found, invalid arguments, etc.)
|
|
55
55
|
|
|
56
|
-
Note: This tool provides ~
|
|
56
|
+
Note: This tool provides ~70% coverage of Java EPUBCheck features.
|
|
57
57
|
Missing features: Media Overlays, advanced ARIA checks, encryption/signatures.
|
|
58
58
|
For complete EPUB 3 conformance testing, use: https://github.com/w3c/epubcheck
|
|
59
59
|
|
|
@@ -153,7 +153,7 @@ async function main() {
|
|
|
153
153
|
console.log();
|
|
154
154
|
if (result.errorCount === 0 && result.fatalCount === 0) {
|
|
155
155
|
console.log(
|
|
156
|
-
"\x1B[90mNote: This validator provides ~
|
|
156
|
+
"\x1B[90mNote: This validator provides ~70% coverage of Java EPUBCheck.\x1B[0m"
|
|
157
157
|
);
|
|
158
158
|
console.log("\x1B[90mFor complete validation: https://github.com/w3c/epubcheck\x1B[0m");
|
|
159
159
|
console.log();
|
package/bin/epubcheck.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { basename } from 'node:path';
|
|
|
14
14
|
// Dynamic import to support both ESM and CJS builds
|
|
15
15
|
const { EpubCheck, toJSONReport } = await import('../dist/index.js');
|
|
16
16
|
|
|
17
|
-
const VERSION = '0.2.
|
|
17
|
+
const VERSION = '0.2.4';
|
|
18
18
|
|
|
19
19
|
// Parse command line arguments
|
|
20
20
|
const { values, positionals } = parseArgs({
|
|
@@ -36,7 +36,7 @@ if (values.version) {
|
|
|
36
36
|
console.log(`EPUBCheck-TS v${VERSION}`);
|
|
37
37
|
console.log('TypeScript EPUB validator for Node.js and browsers');
|
|
38
38
|
console.log();
|
|
39
|
-
console.log('Note: This is ~
|
|
39
|
+
console.log('Note: This is ~70% feature-complete compared to Java EPUBCheck.');
|
|
40
40
|
console.log('For production validation: https://github.com/w3c/epubcheck');
|
|
41
41
|
process.exit(0);
|
|
42
42
|
}
|
|
@@ -70,7 +70,7 @@ Exit Codes:
|
|
|
70
70
|
1 Validation errors found (or warnings with --fail-on-warnings)
|
|
71
71
|
2 Runtime error (file not found, invalid arguments, etc.)
|
|
72
72
|
|
|
73
|
-
Note: This tool provides ~
|
|
73
|
+
Note: This tool provides ~70% coverage of Java EPUBCheck features.
|
|
74
74
|
Missing features: Media Overlays, advanced ARIA checks, encryption/signatures.
|
|
75
75
|
For complete EPUB 3 conformance testing, use: https://github.com/w3c/epubcheck
|
|
76
76
|
|
|
@@ -204,7 +204,7 @@ async function main(): Promise<void> {
|
|
|
204
204
|
// Show limitation notice if there were no major errors
|
|
205
205
|
if (result.errorCount === 0 && result.fatalCount === 0) {
|
|
206
206
|
console.log(
|
|
207
|
-
'\x1b[90mNote: This validator provides ~
|
|
207
|
+
'\x1b[90mNote: This validator provides ~70% coverage of Java EPUBCheck.\x1b[0m',
|
|
208
208
|
);
|
|
209
209
|
console.log('\x1b[90mFor complete validation: https://github.com/w3c/epubcheck\x1b[0m');
|
|
210
210
|
console.log();
|
package/dist/index.cjs
CHANGED
|
@@ -64,8 +64,61 @@ var CSSValidator = class {
|
|
|
64
64
|
this.checkDiscouragedProperties(context, ast, resourcePath);
|
|
65
65
|
this.checkAtRules(context, ast, resourcePath, result);
|
|
66
66
|
this.checkMediaOverlayClasses(context, ast, resourcePath);
|
|
67
|
+
this.extractUrlReferences(context, ast, resourcePath, result);
|
|
67
68
|
return result;
|
|
68
69
|
}
|
|
70
|
+
extractUrlReferences(context, ast, resourcePath, result) {
|
|
71
|
+
cssTree.walk(ast, (node) => {
|
|
72
|
+
if (node.type === "Atrule") {
|
|
73
|
+
const atRule = node;
|
|
74
|
+
if (atRule.name === "font-face") {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (atRule.block) {
|
|
78
|
+
cssTree.walk(atRule.block, (blockNode) => {
|
|
79
|
+
if (blockNode.type === "Declaration") {
|
|
80
|
+
this.processDeclarationForUrl(blockNode, resourcePath, result);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
} else if (node.type === "Rule") {
|
|
85
|
+
const rule = node;
|
|
86
|
+
cssTree.walk(rule.block, (blockNode) => {
|
|
87
|
+
if (blockNode.type === "Declaration") {
|
|
88
|
+
this.processDeclarationForUrl(blockNode, resourcePath, result);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
processDeclarationForUrl(declaration, resourcePath, result) {
|
|
95
|
+
const property = declaration.property.toLowerCase();
|
|
96
|
+
cssTree.walk(declaration.value, (valueNode) => {
|
|
97
|
+
if (valueNode.type === "Url") {
|
|
98
|
+
const urlValue = this.extractUrlValue(valueNode);
|
|
99
|
+
if (urlValue && !urlValue.startsWith("data:")) {
|
|
100
|
+
const loc = valueNode.loc;
|
|
101
|
+
const start = loc?.start;
|
|
102
|
+
if (start) {
|
|
103
|
+
start.line;
|
|
104
|
+
start.column;
|
|
105
|
+
}
|
|
106
|
+
let refType = "resource";
|
|
107
|
+
if (property.includes("font")) {
|
|
108
|
+
refType = "font";
|
|
109
|
+
} else if (property.includes("background") || property.includes("list-style") || property.includes("content") || property.includes("border-image") || property.includes("mask")) {
|
|
110
|
+
refType = "image";
|
|
111
|
+
}
|
|
112
|
+
result.references.push({
|
|
113
|
+
url: urlValue,
|
|
114
|
+
type: refType,
|
|
115
|
+
line: start?.line,
|
|
116
|
+
column: start?.column
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
69
122
|
/**
|
|
70
123
|
* Check for forbidden and discouraged CSS properties in EPUB
|
|
71
124
|
*/
|
|
@@ -434,6 +487,104 @@ function isPublicationResourceReference(type) {
|
|
|
434
487
|
|
|
435
488
|
// src/content/validator.ts
|
|
436
489
|
var DISCOURAGED_ELEMENTS = /* @__PURE__ */ new Set(["base", "embed"]);
|
|
490
|
+
var HTML_ENTITIES = /* @__PURE__ */ new Set([
|
|
491
|
+
"nbsp",
|
|
492
|
+
"iexcl",
|
|
493
|
+
"cent",
|
|
494
|
+
"pound",
|
|
495
|
+
"curren",
|
|
496
|
+
"yen",
|
|
497
|
+
"brvbar",
|
|
498
|
+
"sect",
|
|
499
|
+
"uml",
|
|
500
|
+
"copy",
|
|
501
|
+
"ordf",
|
|
502
|
+
"laquo",
|
|
503
|
+
"not",
|
|
504
|
+
"shy",
|
|
505
|
+
"reg",
|
|
506
|
+
"macr",
|
|
507
|
+
"deg",
|
|
508
|
+
"plusmn",
|
|
509
|
+
"sup2",
|
|
510
|
+
"sup3",
|
|
511
|
+
"acute",
|
|
512
|
+
"micro",
|
|
513
|
+
"para",
|
|
514
|
+
"middot",
|
|
515
|
+
"cedil",
|
|
516
|
+
"sup1",
|
|
517
|
+
"ordm",
|
|
518
|
+
"raquo",
|
|
519
|
+
"frac14",
|
|
520
|
+
"frac12",
|
|
521
|
+
"frac34",
|
|
522
|
+
"iquest",
|
|
523
|
+
"Agrave",
|
|
524
|
+
"Aacute",
|
|
525
|
+
"Acirc",
|
|
526
|
+
"Atilde",
|
|
527
|
+
"Auml",
|
|
528
|
+
"Aring",
|
|
529
|
+
"AElig",
|
|
530
|
+
"Ccedil",
|
|
531
|
+
"Egrave",
|
|
532
|
+
"Eacute",
|
|
533
|
+
"Ecirc",
|
|
534
|
+
"Euml",
|
|
535
|
+
"Igrave",
|
|
536
|
+
"Iacute",
|
|
537
|
+
"Icirc",
|
|
538
|
+
"Iuml",
|
|
539
|
+
"ETH",
|
|
540
|
+
"Ntilde",
|
|
541
|
+
"Ograve",
|
|
542
|
+
"Oacute",
|
|
543
|
+
"Ocirc",
|
|
544
|
+
"Otilde",
|
|
545
|
+
"Ouml",
|
|
546
|
+
"times",
|
|
547
|
+
"Oslash",
|
|
548
|
+
"Ugrave",
|
|
549
|
+
"Uacute",
|
|
550
|
+
"Ucirc",
|
|
551
|
+
"Uuml",
|
|
552
|
+
"Yacute",
|
|
553
|
+
"THORN",
|
|
554
|
+
"szlig",
|
|
555
|
+
"agrave",
|
|
556
|
+
"aacute",
|
|
557
|
+
"acirc",
|
|
558
|
+
"atilde",
|
|
559
|
+
"auml",
|
|
560
|
+
"aring",
|
|
561
|
+
"aelig",
|
|
562
|
+
"ccedil",
|
|
563
|
+
"egrave",
|
|
564
|
+
"eacute",
|
|
565
|
+
"ecirc",
|
|
566
|
+
"euml",
|
|
567
|
+
"igrave",
|
|
568
|
+
"iacute",
|
|
569
|
+
"icirc",
|
|
570
|
+
"iuml",
|
|
571
|
+
"eth",
|
|
572
|
+
"ntilde",
|
|
573
|
+
"ograve",
|
|
574
|
+
"oacute",
|
|
575
|
+
"ocirc",
|
|
576
|
+
"otilde",
|
|
577
|
+
"ouml",
|
|
578
|
+
"divide",
|
|
579
|
+
"oslash",
|
|
580
|
+
"ugrave",
|
|
581
|
+
"uacute",
|
|
582
|
+
"ucirc",
|
|
583
|
+
"uuml",
|
|
584
|
+
"yacute",
|
|
585
|
+
"thorn",
|
|
586
|
+
"yuml"
|
|
587
|
+
]);
|
|
437
588
|
var ContentValidator = class {
|
|
438
589
|
validate(context, registry, refValidator) {
|
|
439
590
|
const packageDoc = context.packageDocument;
|
|
@@ -459,7 +610,31 @@ var ContentValidator = class {
|
|
|
459
610
|
}
|
|
460
611
|
const cssContent = new TextDecoder().decode(cssData);
|
|
461
612
|
const cssValidator = new CSSValidator();
|
|
462
|
-
cssValidator.validate(context, cssContent, path);
|
|
613
|
+
const result = cssValidator.validate(context, cssContent, path);
|
|
614
|
+
const cssDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
|
|
615
|
+
for (const ref of result.references) {
|
|
616
|
+
if (ref.type === "font") {
|
|
617
|
+
const resolvedPath = this.resolveRelativePath(cssDir, ref.url, opfDir);
|
|
618
|
+
const hashIndex = resolvedPath.indexOf("#");
|
|
619
|
+
const targetResource = hashIndex >= 0 ? resolvedPath.slice(0, hashIndex) : resolvedPath;
|
|
620
|
+
refValidator.addReference({
|
|
621
|
+
url: ref.url,
|
|
622
|
+
targetResource,
|
|
623
|
+
type: "font" /* FONT */,
|
|
624
|
+
location: { path }
|
|
625
|
+
});
|
|
626
|
+
} else if (ref.type === "image") {
|
|
627
|
+
const resolvedPath = this.resolveRelativePath(cssDir, ref.url, opfDir);
|
|
628
|
+
const hashIndex = resolvedPath.indexOf("#");
|
|
629
|
+
const targetResource = hashIndex >= 0 ? resolvedPath.slice(0, hashIndex) : resolvedPath;
|
|
630
|
+
refValidator.addReference({
|
|
631
|
+
url: ref.url,
|
|
632
|
+
targetResource,
|
|
633
|
+
type: "image" /* IMAGE */,
|
|
634
|
+
location: { path }
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
}
|
|
463
638
|
this.extractCSSImports(path, cssContent, opfDir, refValidator);
|
|
464
639
|
}
|
|
465
640
|
validateXHTMLDocument(context, path, itemId, opfDir, registry, refValidator) {
|
|
@@ -479,19 +654,26 @@ var ContentValidator = class {
|
|
|
479
654
|
} catch (error) {
|
|
480
655
|
if (error instanceof Error) {
|
|
481
656
|
const { message, line, column } = this.parseLibxmlError(error.message);
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
657
|
+
const entityPattern = /Entity '(\w+)' not defined/;
|
|
658
|
+
const entityExec = entityPattern.exec(error.message);
|
|
659
|
+
const entityName = entityExec?.[1];
|
|
660
|
+
const isKnownHtmlEntity = entityName !== void 0 && HTML_ENTITIES.has(entityName);
|
|
661
|
+
const isEpub2 = context.version === "2.0";
|
|
662
|
+
if (!isEpub2 || !isKnownHtmlEntity) {
|
|
663
|
+
const location = { path };
|
|
664
|
+
if (line !== void 0) {
|
|
665
|
+
location.line = line;
|
|
666
|
+
}
|
|
667
|
+
if (column !== void 0) {
|
|
668
|
+
location.column = column;
|
|
669
|
+
}
|
|
670
|
+
context.messages.push({
|
|
671
|
+
id: "HTM-004",
|
|
672
|
+
severity: "error",
|
|
673
|
+
message,
|
|
674
|
+
location
|
|
675
|
+
});
|
|
488
676
|
}
|
|
489
|
-
context.messages.push({
|
|
490
|
-
id: "HTM-004",
|
|
491
|
-
severity: "error",
|
|
492
|
-
message,
|
|
493
|
-
location
|
|
494
|
-
});
|
|
495
677
|
}
|
|
496
678
|
return;
|
|
497
679
|
}
|
|
@@ -2558,6 +2740,7 @@ var OPFValidator = class {
|
|
|
2558
2740
|
context.packageDocument = this.packageDoc;
|
|
2559
2741
|
this.validatePackageAttributes(context, opfPath);
|
|
2560
2742
|
this.validateMetadata(context, opfPath);
|
|
2743
|
+
this.validateLinkElements(context, opfPath);
|
|
2561
2744
|
this.validateManifest(context, opfPath);
|
|
2562
2745
|
this.validateSpine(context, opfPath);
|
|
2563
2746
|
this.validateFallbackChains(context, opfPath);
|
|
@@ -2809,6 +2992,34 @@ var OPFValidator = class {
|
|
|
2809
2992
|
}
|
|
2810
2993
|
}
|
|
2811
2994
|
}
|
|
2995
|
+
/**
|
|
2996
|
+
* Validate EPUB 3 link elements in metadata
|
|
2997
|
+
*/
|
|
2998
|
+
validateLinkElements(context, opfPath) {
|
|
2999
|
+
if (!this.packageDoc) return;
|
|
3000
|
+
const opfDir = opfPath.includes("/") ? opfPath.substring(0, opfPath.lastIndexOf("/")) : "";
|
|
3001
|
+
for (const link of this.packageDoc.linkElements) {
|
|
3002
|
+
const href = link.href;
|
|
3003
|
+
const decodedHref = tryDecodeUriComponent(href);
|
|
3004
|
+
const basePath = href.includes("#") ? href.substring(0, href.indexOf("#")) : href;
|
|
3005
|
+
const basePathDecoded = decodedHref.includes("#") ? decodedHref.substring(0, decodedHref.indexOf("#")) : decodedHref;
|
|
3006
|
+
if (href.startsWith("#")) {
|
|
3007
|
+
continue;
|
|
3008
|
+
}
|
|
3009
|
+
const resolvedPath = resolvePath(opfDir, basePath);
|
|
3010
|
+
const resolvedPathDecoded = basePathDecoded !== basePath ? resolvePath(opfDir, basePathDecoded) : resolvedPath;
|
|
3011
|
+
const fileExists = context.files.has(resolvedPath) || context.files.has(resolvedPathDecoded);
|
|
3012
|
+
const inManifest = this.manifestByHref.has(basePath) || this.manifestByHref.has(basePathDecoded);
|
|
3013
|
+
if (!fileExists && !inManifest) {
|
|
3014
|
+
context.messages.push({
|
|
3015
|
+
id: "RSC-007",
|
|
3016
|
+
severity: "warning",
|
|
3017
|
+
message: `Referenced resource "${resolvedPath}" could not be found in the EPUB`,
|
|
3018
|
+
location: { path: opfPath }
|
|
3019
|
+
});
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
2812
3023
|
/**
|
|
2813
3024
|
* Validate manifest section
|
|
2814
3025
|
*/
|
|
@@ -2836,7 +3047,28 @@ var OPFValidator = class {
|
|
|
2836
3047
|
}
|
|
2837
3048
|
seenHrefs.add(item.href);
|
|
2838
3049
|
const fullPath = resolvePath(opfPath, item.href);
|
|
2839
|
-
if (
|
|
3050
|
+
if (fullPath === opfPath) {
|
|
3051
|
+
context.messages.push({
|
|
3052
|
+
id: "OPF-099",
|
|
3053
|
+
severity: "error",
|
|
3054
|
+
message: "The manifest must not list the package document",
|
|
3055
|
+
location: { path: opfPath }
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
if (!item.href.startsWith("http") && !item.href.startsWith("mailto:")) {
|
|
3059
|
+
const leaked = checkUrlLeaking(item.href);
|
|
3060
|
+
if (leaked) {
|
|
3061
|
+
context.messages.push({
|
|
3062
|
+
id: "RSC-026",
|
|
3063
|
+
severity: "error",
|
|
3064
|
+
message: `URL "${item.href}" leaks outside the container (it is not a valid-relative-ocf-URL-with-fragment string)`,
|
|
3065
|
+
location: { path: opfPath }
|
|
3066
|
+
});
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
const decodedHref = tryDecodeUriComponent(item.href);
|
|
3070
|
+
const fullPathDecoded = decodedHref !== item.href ? resolvePath(opfPath, decodedHref) : fullPath;
|
|
3071
|
+
if (!context.files.has(fullPath) && !context.files.has(fullPathDecoded) && !item.href.startsWith("http")) {
|
|
2840
3072
|
context.messages.push({
|
|
2841
3073
|
id: "RSC-001",
|
|
2842
3074
|
severity: "error",
|
|
@@ -3188,6 +3420,24 @@ function resolvePath(basePath, relativePath) {
|
|
|
3188
3420
|
}
|
|
3189
3421
|
return parts.join("/");
|
|
3190
3422
|
}
|
|
3423
|
+
function tryDecodeUriComponent(encoded) {
|
|
3424
|
+
try {
|
|
3425
|
+
return decodeURIComponent(encoded);
|
|
3426
|
+
} catch {
|
|
3427
|
+
return encoded;
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
function checkUrlLeaking(href) {
|
|
3431
|
+
const TEST_BASE_A = "https://a.example.org/A/";
|
|
3432
|
+
const TEST_BASE_B = "https://b.example.org/B/";
|
|
3433
|
+
try {
|
|
3434
|
+
const urlA = new URL(href, TEST_BASE_A).toString();
|
|
3435
|
+
const urlB = new URL(href, TEST_BASE_B).toString();
|
|
3436
|
+
return !urlA.startsWith(TEST_BASE_A) || !urlB.startsWith(TEST_BASE_B);
|
|
3437
|
+
} catch {
|
|
3438
|
+
return false;
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3191
3441
|
function isValidMimeType(mediaType) {
|
|
3192
3442
|
const mimeTypePattern = /^[a-zA-Z][a-zA-Z0-9!#$&\-^_.]*\/[a-zA-Z0-9][a-zA-Z0-9!#$&\-^_.+]*(?:\s*;\s*[a-zA-Z0-9-]+=[^;]+)?$/;
|
|
3193
3443
|
if (!mimeTypePattern.test(mediaType)) {
|
|
@@ -4168,6 +4418,7 @@ var MessageId = /* @__PURE__ */ ((MessageId2) => {
|
|
|
4168
4418
|
MessageId2["OPF_013"] = "OPF-013";
|
|
4169
4419
|
MessageId2["OPF_014"] = "OPF-014";
|
|
4170
4420
|
MessageId2["OPF_097"] = "OPF-097";
|
|
4421
|
+
MessageId2["OPF_099"] = "OPF-099";
|
|
4171
4422
|
MessageId2["OPF_015"] = "OPF-015";
|
|
4172
4423
|
MessageId2["RSC_001"] = "RSC-001";
|
|
4173
4424
|
MessageId2["RSC_002"] = "RSC-002";
|