@likecoin/epubcheck-ts 0.3.1 → 0.3.2

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
@@ -6,7 +6,7 @@ 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 (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!
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 (476 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
 
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.3.1";
6
+ const VERSION = "0.3.2";
7
7
  const { values, positionals } = parseArgs({
8
8
  options: {
9
9
  json: { type: "string", short: "j" },
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.3.1';
17
+ const VERSION = '0.3.2';
18
18
 
19
19
  // Parse command line arguments
20
20
  const { values, positionals } = parseArgs({
package/dist/index.cjs CHANGED
@@ -1758,9 +1758,27 @@ var ContentValidator = class {
1758
1758
  } else if (item.mediaType === "text/css" && refValidator) {
1759
1759
  const fullPath = opfDir ? `${opfDir}/${item.href}` : item.href;
1760
1760
  this.validateCSSDocument(context, fullPath, opfDir, refValidator);
1761
+ } else if (item.mediaType === "image/svg+xml" && registry) {
1762
+ const fullPath = opfDir ? `${opfDir}/${item.href}` : item.href;
1763
+ this.extractSVGIDs(context, fullPath, registry);
1761
1764
  }
1762
1765
  }
1763
1766
  }
1767
+ extractSVGIDs(context, path, registry) {
1768
+ const svgData = context.files.get(path);
1769
+ if (!svgData) {
1770
+ return;
1771
+ }
1772
+ const svgContent = new TextDecoder().decode(svgData);
1773
+ let doc;
1774
+ try {
1775
+ doc = libxml2Wasm.XmlDocument.fromString(svgContent);
1776
+ this.extractAndRegisterIDs(path, doc.root, registry);
1777
+ } catch {
1778
+ } finally {
1779
+ doc?.dispose();
1780
+ }
1781
+ }
1764
1782
  validateCSSDocument(context, path, opfDir, refValidator) {
1765
1783
  const cssData = context.files.get(path);
1766
1784
  if (!cssData) {
@@ -1958,9 +1976,11 @@ var ContentValidator = class {
1958
1976
  this.extractAndRegisterIDs(path, root, registry);
1959
1977
  }
1960
1978
  if (refValidator && opfDir !== void 0) {
1961
- this.extractAndRegisterHyperlinks(path, root, opfDir, refValidator);
1979
+ this.extractAndRegisterHyperlinks(context, path, root, opfDir, refValidator);
1962
1980
  this.extractAndRegisterStylesheets(path, root, opfDir, refValidator);
1963
1981
  this.extractAndRegisterImages(path, root, opfDir, refValidator);
1982
+ this.extractAndRegisterMathMLAltimg(path, root, opfDir, refValidator);
1983
+ this.extractAndRegisterScripts(path, root, opfDir, refValidator);
1964
1984
  this.extractAndRegisterCiteAttributes(path, root, opfDir, refValidator);
1965
1985
  this.extractAndRegisterMediaElements(path, root, opfDir, refValidator);
1966
1986
  }
@@ -2425,12 +2445,20 @@ var ContentValidator = class {
2425
2445
  }
2426
2446
  }
2427
2447
  }
2428
- extractAndRegisterHyperlinks(path, root, opfDir, refValidator) {
2448
+ extractAndRegisterHyperlinks(context, path, root, opfDir, refValidator) {
2429
2449
  const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
2430
2450
  const links = root.find(".//html:a[@href]", { html: "http://www.w3.org/1999/xhtml" });
2431
2451
  for (const link of links) {
2432
2452
  const href = this.getAttribute(link, "href");
2433
- if (!href) continue;
2453
+ if (href === null) continue;
2454
+ if (href.trim() === "") {
2455
+ pushMessage(context.messages, {
2456
+ id: MessageId.HTM_045,
2457
+ message: "Encountered empty href",
2458
+ location: { path, line: link.line }
2459
+ });
2460
+ continue;
2461
+ }
2434
2462
  const line = link.line;
2435
2463
  if (href.startsWith("http://") || href.startsWith("https://")) {
2436
2464
  continue;
@@ -2570,7 +2598,8 @@ var ContentValidator = class {
2570
2598
  const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
2571
2599
  const images = root.find(".//html:img[@src]", { html: "http://www.w3.org/1999/xhtml" });
2572
2600
  for (const img of images) {
2573
- const src = this.getAttribute(img, "src");
2601
+ const imgElem = img;
2602
+ const src = this.getAttribute(imgElem, "src");
2574
2603
  if (!src) continue;
2575
2604
  const line = img.line;
2576
2605
  if (src.startsWith("http://") || src.startsWith("https://")) {
@@ -2580,15 +2609,26 @@ var ContentValidator = class {
2580
2609
  type: "image" /* IMAGE */,
2581
2610
  location: { path, line }
2582
2611
  });
2583
- continue;
2612
+ } else {
2613
+ const resolvedPath = this.resolveRelativePath(docDir, src, opfDir);
2614
+ const hashIndex = resolvedPath.indexOf("#");
2615
+ const targetResource = hashIndex >= 0 ? resolvedPath.slice(0, hashIndex) : resolvedPath;
2616
+ const fragment = hashIndex >= 0 ? resolvedPath.slice(hashIndex + 1) : void 0;
2617
+ const ref = {
2618
+ url: src,
2619
+ targetResource,
2620
+ type: "image" /* IMAGE */,
2621
+ location: { path, line }
2622
+ };
2623
+ if (fragment) {
2624
+ ref.fragment = fragment;
2625
+ }
2626
+ refValidator.addReference(ref);
2627
+ }
2628
+ const srcset = this.getAttribute(imgElem, "srcset");
2629
+ if (srcset) {
2630
+ this.parseSrcset(srcset, docDir, opfDir, path, line, refValidator);
2584
2631
  }
2585
- const resolvedPath = this.resolveRelativePath(docDir, src, opfDir);
2586
- refValidator.addReference({
2587
- url: src,
2588
- targetResource: resolvedPath,
2589
- type: "image" /* IMAGE */,
2590
- location: { path, line }
2591
- });
2592
2632
  }
2593
2633
  let svgImages = [];
2594
2634
  try {
@@ -2618,12 +2658,19 @@ var ContentValidator = class {
2618
2658
  continue;
2619
2659
  }
2620
2660
  const resolvedPath = this.resolveRelativePath(docDir, href, opfDir);
2621
- refValidator.addReference({
2661
+ const hashIndex = resolvedPath.indexOf("#");
2662
+ const targetResource = hashIndex >= 0 ? resolvedPath.slice(0, hashIndex) : resolvedPath;
2663
+ const fragment = hashIndex >= 0 ? resolvedPath.slice(hashIndex + 1) : void 0;
2664
+ const svgImgRef = {
2622
2665
  url: href,
2623
- targetResource: resolvedPath,
2666
+ targetResource,
2624
2667
  type: "image" /* IMAGE */,
2625
2668
  location: { path, line }
2626
- });
2669
+ };
2670
+ if (fragment) {
2671
+ svgImgRef.fragment = fragment;
2672
+ }
2673
+ refValidator.addReference(svgImgRef);
2627
2674
  }
2628
2675
  const videos = root.find(".//html:video[@poster]", { html: "http://www.w3.org/1999/xhtml" });
2629
2676
  for (const video of videos) {
@@ -2648,6 +2695,58 @@ var ContentValidator = class {
2648
2695
  });
2649
2696
  }
2650
2697
  }
2698
+ extractAndRegisterMathMLAltimg(path, root, opfDir, refValidator) {
2699
+ const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
2700
+ const mathElements = root.find(".//math:math[@altimg]", {
2701
+ math: "http://www.w3.org/1998/Math/MathML"
2702
+ });
2703
+ for (const mathElem of mathElements) {
2704
+ const altimg = this.getAttribute(mathElem, "altimg");
2705
+ if (!altimg) continue;
2706
+ const line = mathElem.line;
2707
+ if (altimg.startsWith("http://") || altimg.startsWith("https://")) {
2708
+ refValidator.addReference({
2709
+ url: altimg,
2710
+ targetResource: altimg,
2711
+ type: "image" /* IMAGE */,
2712
+ location: { path, line }
2713
+ });
2714
+ continue;
2715
+ }
2716
+ const resolvedPath = this.resolveRelativePath(docDir, altimg, opfDir);
2717
+ refValidator.addReference({
2718
+ url: altimg,
2719
+ targetResource: resolvedPath,
2720
+ type: "image" /* IMAGE */,
2721
+ location: { path, line }
2722
+ });
2723
+ }
2724
+ }
2725
+ extractAndRegisterScripts(path, root, opfDir, refValidator) {
2726
+ const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
2727
+ const scripts = root.find(".//html:script[@src]", { html: "http://www.w3.org/1999/xhtml" });
2728
+ for (const script of scripts) {
2729
+ const src = this.getAttribute(script, "src");
2730
+ if (!src) continue;
2731
+ const line = script.line;
2732
+ if (src.startsWith("http://") || src.startsWith("https://")) {
2733
+ refValidator.addReference({
2734
+ url: src,
2735
+ targetResource: src,
2736
+ type: "generic" /* GENERIC */,
2737
+ location: { path, line }
2738
+ });
2739
+ continue;
2740
+ }
2741
+ const resolvedPath = this.resolveRelativePath(docDir, src, opfDir);
2742
+ refValidator.addReference({
2743
+ url: src,
2744
+ targetResource: resolvedPath,
2745
+ type: "generic" /* GENERIC */,
2746
+ location: { path, line }
2747
+ });
2748
+ }
2749
+ }
2651
2750
  /**
2652
2751
  * Extract cite attribute references from blockquote, q, ins, del elements
2653
2752
  * These need to be validated as RSC-007 if the referenced resource is missing
@@ -2798,6 +2897,32 @@ var ContentValidator = class {
2798
2897
  }
2799
2898
  }
2800
2899
  }
2900
+ parseSrcset(srcset, docDir, opfDir, path, line, refValidator) {
2901
+ const entries = srcset.split(",");
2902
+ for (const entry of entries) {
2903
+ const trimmed = entry.trim();
2904
+ if (!trimmed) continue;
2905
+ const url = trimmed.split(/\s+/)[0];
2906
+ if (!url) continue;
2907
+ const location = line !== void 0 ? { path, line } : { path };
2908
+ if (url.startsWith("http://") || url.startsWith("https://")) {
2909
+ refValidator.addReference({
2910
+ url,
2911
+ targetResource: url,
2912
+ type: "image" /* IMAGE */,
2913
+ location
2914
+ });
2915
+ } else {
2916
+ const resolvedPath = this.resolveRelativePath(docDir, url, opfDir);
2917
+ refValidator.addReference({
2918
+ url,
2919
+ targetResource: resolvedPath,
2920
+ type: "image" /* IMAGE */,
2921
+ location
2922
+ });
2923
+ }
2924
+ }
2925
+ }
2801
2926
  resolveRelativePath(docDir, href, _opfDir) {
2802
2927
  const hrefWithoutFragment = href.split("#")[0] ?? href;
2803
2928
  const fragment = href.includes("#") ? href.split("#")[1] : "";
@@ -4838,11 +4963,19 @@ var ReferenceValidator = class {
4838
4963
  }
4839
4964
  if (isDataURL(url)) {
4840
4965
  if (this.version.startsWith("3.")) {
4841
- pushMessage(context.messages, {
4842
- id: MessageId.RSC_029,
4843
- message: "Data URLs are not allowed in EPUB 3",
4844
- location: reference.location
4845
- });
4966
+ const forbiddenDataUrlTypes = [
4967
+ "hyperlink" /* HYPERLINK */,
4968
+ "nav-toc-link" /* NAV_TOC_LINK */,
4969
+ "nav-pagelist-link" /* NAV_PAGELIST_LINK */,
4970
+ "cite" /* CITE */
4971
+ ];
4972
+ if (forbiddenDataUrlTypes.includes(reference.type)) {
4973
+ pushMessage(context.messages, {
4974
+ id: MessageId.RSC_029,
4975
+ message: "Data URLs are not allowed in this context",
4976
+ location: reference.location
4977
+ });
4978
+ }
4846
4979
  }
4847
4980
  return;
4848
4981
  }
@@ -4982,6 +5115,14 @@ var ReferenceValidator = class {
4982
5115
  });
4983
5116
  return;
4984
5117
  }
5118
+ if (reference.type === "image" /* IMAGE */ && resource?.mimeType !== "image/svg+xml") {
5119
+ pushMessage(context.messages, {
5120
+ id: MessageId.RSC_009,
5121
+ message: `Fragment identifier used on a non-SVG image resource: ${resourcePath}#${fragment}`,
5122
+ location: reference.location
5123
+ });
5124
+ return;
5125
+ }
4985
5126
  if (resource?.mimeType === "image/svg+xml") {
4986
5127
  const hasSVGView = fragment.includes("svgView(") || fragment.includes("viewBox(");
4987
5128
  if (hasSVGView && reference.type === "hyperlink" /* HYPERLINK */) {