@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.
Files changed (66) hide show
  1. package/README.md +8 -8
  2. package/bin/epubcheck.js +4 -4
  3. package/bin/epubcheck.ts +4 -4
  4. package/dist/index.cjs +265 -14
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +1 -0
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +265 -14
  9. package/dist/index.js.map +1 -1
  10. package/package.json +2 -2
  11. package/schemas/applications.rng +0 -429
  12. package/schemas/aria.rng +0 -3355
  13. package/schemas/block.rng +0 -488
  14. package/schemas/common.rng +0 -1076
  15. package/schemas/container.rng +0 -24
  16. package/schemas/core-scripting.rng +0 -950
  17. package/schemas/data.rng +0 -161
  18. package/schemas/datatypes.rng +0 -401
  19. package/schemas/embed.rng +0 -980
  20. package/schemas/epub-mathml3-inc.rng +0 -161
  21. package/schemas/epub-nav-30.rnc +0 -44
  22. package/schemas/epub-nav-30.rng +0 -19985
  23. package/schemas/epub-nav-30.sch +0 -87
  24. package/schemas/epub-prefix-attr.rng +0 -17
  25. package/schemas/epub-shared-inc.rng +0 -29
  26. package/schemas/epub-ssml-attrs.rng +0 -17
  27. package/schemas/epub-svg-30.rnc +0 -17
  28. package/schemas/epub-svg-30.rng +0 -19903
  29. package/schemas/epub-svg-30.sch +0 -7
  30. package/schemas/epub-svg-forgiving-inc.rng +0 -315
  31. package/schemas/epub-switch.rng +0 -121
  32. package/schemas/epub-trigger.rng +0 -90
  33. package/schemas/epub-type-attr.rng +0 -12
  34. package/schemas/epub-xhtml-30.rnc +0 -6
  35. package/schemas/epub-xhtml-30.rng +0 -19882
  36. package/schemas/epub-xhtml-30.sch +0 -409
  37. package/schemas/epub-xhtml-inc.rng +0 -151
  38. package/schemas/epub-xhtml-integration.rng +0 -565
  39. package/schemas/epub-xhtml-svg-mathml.rng +0 -17
  40. package/schemas/form-datatypes.rng +0 -54
  41. package/schemas/mathml3-common.rng +0 -336
  42. package/schemas/mathml3-content.rng +0 -1552
  43. package/schemas/mathml3-inc.rng +0 -30
  44. package/schemas/mathml3-presentation.rng +0 -2341
  45. package/schemas/mathml3-strict-content.rng +0 -205
  46. package/schemas/media.rng +0 -374
  47. package/schemas/meta.rng +0 -754
  48. package/schemas/microdata.rng +0 -192
  49. package/schemas/ncx.rng +0 -308
  50. package/schemas/ocf-container-30.rnc +0 -37
  51. package/schemas/ocf-container-30.rng +0 -568
  52. package/schemas/opf.rng +0 -15
  53. package/schemas/opf20.rng +0 -513
  54. package/schemas/package-30.rnc +0 -133
  55. package/schemas/package-30.rng +0 -1153
  56. package/schemas/package-30.sch +0 -444
  57. package/schemas/phrase.rng +0 -746
  58. package/schemas/rdfa.rng +0 -552
  59. package/schemas/revision.rng +0 -106
  60. package/schemas/ruby.rng +0 -141
  61. package/schemas/sectional.rng +0 -278
  62. package/schemas/structural.rng +0 -298
  63. package/schemas/tables.rng +0 -420
  64. package/schemas/web-components.rng +0 -184
  65. package/schemas/web-forms.rng +0 -975
  66. 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
  [![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 (208 tests) and ~65% 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
+ > **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 ~65% of EPUBCheck feature parity
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 ~65% coverage of Java EPUBCheck features. For complete EPUB 3 conformance testing, use the [official Java EPUBCheck](https://github.com/w3c/epubcheck).
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 | ~70% | RelaxNG, XSD, Schematron working |
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 | ~75% | Empty links, image alt, SVG titles, MathML alttext |
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: ~65% of Java EPUBCheck features**
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 | ~65% | 100% |
361
- | Bundle Size | ~55KB JS + ~1.5MB WASM | ~15MB |
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.3";
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 ~65% feature-complete compared to Java EPUBCheck.");
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 ~65% coverage of Java EPUBCheck features.
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 ~65% coverage of Java EPUBCheck.\x1B[0m"
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.3';
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 ~65% feature-complete compared to Java EPUBCheck.');
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 ~65% coverage of Java EPUBCheck features.
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 ~65% coverage of Java EPUBCheck.\x1b[0m',
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 location = { path };
483
- if (line !== void 0) {
484
- location.line = line;
485
- }
486
- if (column !== void 0) {
487
- location.column = column;
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 (!context.files.has(fullPath) && !item.href.startsWith("http")) {
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";