@likecoin/epubcheck-ts 0.3.1 → 0.3.3

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 (505 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.3";
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.3';
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
  }
@@ -2000,8 +2020,8 @@ var ContentValidator = class {
2000
2020
  }
2001
2021
  }
2002
2022
  checkNavDocument(context, path, doc, root) {
2003
- const nav = root.get(".//html:nav", { html: "http://www.w3.org/1999/xhtml" });
2004
- if (!nav) {
2023
+ const navElements = root.find(".//html:nav", { html: "http://www.w3.org/1999/xhtml" });
2024
+ if (navElements.length === 0) {
2005
2025
  pushMessage(context.messages, {
2006
2026
  id: MessageId.NAV_001,
2007
2027
  message: "Navigation document must have a nav element",
@@ -2009,20 +2029,28 @@ var ContentValidator = class {
2009
2029
  });
2010
2030
  return;
2011
2031
  }
2012
- if (!("attrs" in nav)) {
2013
- return;
2032
+ let tocNav;
2033
+ let tocEpubTypeValue = "";
2034
+ for (const nav of navElements) {
2035
+ if (!("attrs" in nav)) continue;
2036
+ const epubTypeAttr = nav.attrs.find(
2037
+ (attr) => attr.name === "type" && attr.prefix === "epub" && attr.namespaceUri === "http://www.idpf.org/2007/ops"
2038
+ );
2039
+ if (epubTypeAttr?.value.includes("toc")) {
2040
+ tocNav = nav;
2041
+ tocEpubTypeValue = epubTypeAttr.value;
2042
+ break;
2043
+ }
2014
2044
  }
2015
- const epubTypeAttr = nav.attrs.find(
2016
- (attr) => attr.name === "type" && attr.prefix === "epub" && attr.namespaceUri === "http://www.idpf.org/2007/ops"
2017
- );
2018
- if (!epubTypeAttr?.value.includes("toc")) {
2045
+ if (!tocNav) {
2019
2046
  pushMessage(context.messages, {
2020
2047
  id: MessageId.NAV_001,
2021
2048
  message: 'Navigation document nav element must have epub:type="toc"',
2022
2049
  location: { path }
2023
2050
  });
2051
+ return;
2024
2052
  }
2025
- const ol = nav.get(".//html:ol", { html: "http://www.w3.org/1999/xhtml" });
2053
+ const ol = tocNav.get(".//html:ol", { html: "http://www.w3.org/1999/xhtml" });
2026
2054
  if (!ol) {
2027
2055
  pushMessage(context.messages, {
2028
2056
  id: MessageId.NAV_002,
@@ -2030,7 +2058,7 @@ var ContentValidator = class {
2030
2058
  location: { path }
2031
2059
  });
2032
2060
  }
2033
- this.checkNavRemoteLinks(context, path, root, epubTypeAttr?.value ?? "");
2061
+ this.checkNavRemoteLinks(context, path, root, tocEpubTypeValue);
2034
2062
  }
2035
2063
  checkNavRemoteLinks(context, path, root, epubTypeValue) {
2036
2064
  const navTypes = epubTypeValue.split(/\s+/);
@@ -2425,12 +2453,20 @@ var ContentValidator = class {
2425
2453
  }
2426
2454
  }
2427
2455
  }
2428
- extractAndRegisterHyperlinks(path, root, opfDir, refValidator) {
2456
+ extractAndRegisterHyperlinks(context, path, root, opfDir, refValidator) {
2429
2457
  const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
2430
2458
  const links = root.find(".//html:a[@href]", { html: "http://www.w3.org/1999/xhtml" });
2431
2459
  for (const link of links) {
2432
2460
  const href = this.getAttribute(link, "href");
2433
- if (!href) continue;
2461
+ if (href === null) continue;
2462
+ if (href.trim() === "") {
2463
+ pushMessage(context.messages, {
2464
+ id: MessageId.HTM_045,
2465
+ message: "Encountered empty href",
2466
+ location: { path, line: link.line }
2467
+ });
2468
+ continue;
2469
+ }
2434
2470
  const line = link.line;
2435
2471
  if (href.startsWith("http://") || href.startsWith("https://")) {
2436
2472
  continue;
@@ -2438,6 +2474,9 @@ var ContentValidator = class {
2438
2474
  if (href.startsWith("mailto:") || href.startsWith("tel:")) {
2439
2475
  continue;
2440
2476
  }
2477
+ if (href.includes("#epubcfi(")) {
2478
+ continue;
2479
+ }
2441
2480
  if (href.startsWith("#")) {
2442
2481
  const targetResource2 = path;
2443
2482
  const fragment = href.slice(1);
@@ -2570,7 +2609,8 @@ var ContentValidator = class {
2570
2609
  const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
2571
2610
  const images = root.find(".//html:img[@src]", { html: "http://www.w3.org/1999/xhtml" });
2572
2611
  for (const img of images) {
2573
- const src = this.getAttribute(img, "src");
2612
+ const imgElem = img;
2613
+ const src = this.getAttribute(imgElem, "src");
2574
2614
  if (!src) continue;
2575
2615
  const line = img.line;
2576
2616
  if (src.startsWith("http://") || src.startsWith("https://")) {
@@ -2580,15 +2620,26 @@ var ContentValidator = class {
2580
2620
  type: "image" /* IMAGE */,
2581
2621
  location: { path, line }
2582
2622
  });
2583
- continue;
2623
+ } else {
2624
+ const resolvedPath = this.resolveRelativePath(docDir, src, opfDir);
2625
+ const hashIndex = resolvedPath.indexOf("#");
2626
+ const targetResource = hashIndex >= 0 ? resolvedPath.slice(0, hashIndex) : resolvedPath;
2627
+ const fragment = hashIndex >= 0 ? resolvedPath.slice(hashIndex + 1) : void 0;
2628
+ const ref = {
2629
+ url: src,
2630
+ targetResource,
2631
+ type: "image" /* IMAGE */,
2632
+ location: { path, line }
2633
+ };
2634
+ if (fragment) {
2635
+ ref.fragment = fragment;
2636
+ }
2637
+ refValidator.addReference(ref);
2638
+ }
2639
+ const srcset = this.getAttribute(imgElem, "srcset");
2640
+ if (srcset) {
2641
+ this.parseSrcset(srcset, docDir, opfDir, path, line, refValidator);
2584
2642
  }
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
2643
  }
2593
2644
  let svgImages = [];
2594
2645
  try {
@@ -2618,12 +2669,19 @@ var ContentValidator = class {
2618
2669
  continue;
2619
2670
  }
2620
2671
  const resolvedPath = this.resolveRelativePath(docDir, href, opfDir);
2621
- refValidator.addReference({
2672
+ const hashIndex = resolvedPath.indexOf("#");
2673
+ const targetResource = hashIndex >= 0 ? resolvedPath.slice(0, hashIndex) : resolvedPath;
2674
+ const fragment = hashIndex >= 0 ? resolvedPath.slice(hashIndex + 1) : void 0;
2675
+ const svgImgRef = {
2622
2676
  url: href,
2623
- targetResource: resolvedPath,
2677
+ targetResource,
2624
2678
  type: "image" /* IMAGE */,
2625
2679
  location: { path, line }
2626
- });
2680
+ };
2681
+ if (fragment) {
2682
+ svgImgRef.fragment = fragment;
2683
+ }
2684
+ refValidator.addReference(svgImgRef);
2627
2685
  }
2628
2686
  const videos = root.find(".//html:video[@poster]", { html: "http://www.w3.org/1999/xhtml" });
2629
2687
  for (const video of videos) {
@@ -2648,6 +2706,58 @@ var ContentValidator = class {
2648
2706
  });
2649
2707
  }
2650
2708
  }
2709
+ extractAndRegisterMathMLAltimg(path, root, opfDir, refValidator) {
2710
+ const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
2711
+ const mathElements = root.find(".//math:math[@altimg]", {
2712
+ math: "http://www.w3.org/1998/Math/MathML"
2713
+ });
2714
+ for (const mathElem of mathElements) {
2715
+ const altimg = this.getAttribute(mathElem, "altimg");
2716
+ if (!altimg) continue;
2717
+ const line = mathElem.line;
2718
+ if (altimg.startsWith("http://") || altimg.startsWith("https://")) {
2719
+ refValidator.addReference({
2720
+ url: altimg,
2721
+ targetResource: altimg,
2722
+ type: "image" /* IMAGE */,
2723
+ location: { path, line }
2724
+ });
2725
+ continue;
2726
+ }
2727
+ const resolvedPath = this.resolveRelativePath(docDir, altimg, opfDir);
2728
+ refValidator.addReference({
2729
+ url: altimg,
2730
+ targetResource: resolvedPath,
2731
+ type: "image" /* IMAGE */,
2732
+ location: { path, line }
2733
+ });
2734
+ }
2735
+ }
2736
+ extractAndRegisterScripts(path, root, opfDir, refValidator) {
2737
+ const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
2738
+ const scripts = root.find(".//html:script[@src]", { html: "http://www.w3.org/1999/xhtml" });
2739
+ for (const script of scripts) {
2740
+ const src = this.getAttribute(script, "src");
2741
+ if (!src) continue;
2742
+ const line = script.line;
2743
+ if (src.startsWith("http://") || src.startsWith("https://")) {
2744
+ refValidator.addReference({
2745
+ url: src,
2746
+ targetResource: src,
2747
+ type: "generic" /* GENERIC */,
2748
+ location: { path, line }
2749
+ });
2750
+ continue;
2751
+ }
2752
+ const resolvedPath = this.resolveRelativePath(docDir, src, opfDir);
2753
+ refValidator.addReference({
2754
+ url: src,
2755
+ targetResource: resolvedPath,
2756
+ type: "generic" /* GENERIC */,
2757
+ location: { path, line }
2758
+ });
2759
+ }
2760
+ }
2651
2761
  /**
2652
2762
  * Extract cite attribute references from blockquote, q, ins, del elements
2653
2763
  * These need to be validated as RSC-007 if the referenced resource is missing
@@ -2773,6 +2883,30 @@ var ContentValidator = class {
2773
2883
  });
2774
2884
  }
2775
2885
  }
2886
+ const iframeElements = root.find(".//html:iframe[@src]", {
2887
+ html: "http://www.w3.org/1999/xhtml"
2888
+ });
2889
+ for (const iframe of iframeElements) {
2890
+ const src = this.getAttribute(iframe, "src");
2891
+ if (!src) continue;
2892
+ const line = iframe.line;
2893
+ if (src.startsWith("http://") || src.startsWith("https://")) {
2894
+ refValidator.addReference({
2895
+ url: src,
2896
+ targetResource: src,
2897
+ type: "generic" /* GENERIC */,
2898
+ location: line !== void 0 ? { path, line } : { path }
2899
+ });
2900
+ } else {
2901
+ const resolvedPath = this.resolveRelativePath(docDir, src, opfDir);
2902
+ refValidator.addReference({
2903
+ url: src,
2904
+ targetResource: resolvedPath,
2905
+ type: "generic" /* GENERIC */,
2906
+ location: line !== void 0 ? { path, line } : { path }
2907
+ });
2908
+ }
2909
+ }
2776
2910
  const trackElements = root.find(".//html:track[@src]", {
2777
2911
  html: "http://www.w3.org/1999/xhtml"
2778
2912
  });
@@ -2798,6 +2932,32 @@ var ContentValidator = class {
2798
2932
  }
2799
2933
  }
2800
2934
  }
2935
+ parseSrcset(srcset, docDir, opfDir, path, line, refValidator) {
2936
+ const entries = srcset.split(",");
2937
+ for (const entry of entries) {
2938
+ const trimmed = entry.trim();
2939
+ if (!trimmed) continue;
2940
+ const url = trimmed.split(/\s+/)[0];
2941
+ if (!url) continue;
2942
+ const location = line !== void 0 ? { path, line } : { path };
2943
+ if (url.startsWith("http://") || url.startsWith("https://")) {
2944
+ refValidator.addReference({
2945
+ url,
2946
+ targetResource: url,
2947
+ type: "image" /* IMAGE */,
2948
+ location
2949
+ });
2950
+ } else {
2951
+ const resolvedPath = this.resolveRelativePath(docDir, url, opfDir);
2952
+ refValidator.addReference({
2953
+ url,
2954
+ targetResource: resolvedPath,
2955
+ type: "image" /* IMAGE */,
2956
+ location
2957
+ });
2958
+ }
2959
+ }
2960
+ }
2801
2961
  resolveRelativePath(docDir, href, _opfDir) {
2802
2962
  const hrefWithoutFragment = href.split("#")[0] ?? href;
2803
2963
  const fragment = href.includes("#") ? href.split("#")[1] : "";
@@ -4204,6 +4364,17 @@ var OPFValidator = class {
4204
4364
  if (href.startsWith("#")) {
4205
4365
  continue;
4206
4366
  }
4367
+ const isRemote = /^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(href);
4368
+ if (isRemote) {
4369
+ continue;
4370
+ }
4371
+ if (!link.mediaType) {
4372
+ pushMessage(context.messages, {
4373
+ id: MessageId.OPF_093,
4374
+ message: 'The "media-type" attribute is required for linked resources located in the EPUB container',
4375
+ location: { path: opfPath }
4376
+ });
4377
+ }
4207
4378
  const resolvedPath = resolvePath(opfDir, basePath);
4208
4379
  const resolvedPathDecoded = basePathDecoded !== basePath ? resolvePath(opfDir, basePathDecoded) : resolvedPath;
4209
4380
  const fileExists = context.files.has(resolvedPath) || context.files.has(resolvedPathDecoded);
@@ -4297,10 +4468,10 @@ var OPFValidator = class {
4297
4468
  }
4298
4469
  if (this.packageDoc.version !== "2.0" && item.properties) {
4299
4470
  for (const prop of item.properties) {
4300
- if (!ITEM_PROPERTIES.has(prop) && !prop.includes(":")) {
4471
+ if (!ITEM_PROPERTIES.has(prop)) {
4301
4472
  pushMessage(context.messages, {
4302
- id: MessageId.OPF_012,
4303
- message: `Unknown item property: "${prop}" on item "${item.id}"`,
4473
+ id: MessageId.OPF_027,
4474
+ message: `Undefined property: "${prop}"`,
4304
4475
  location: { path: opfPath }
4305
4476
  });
4306
4477
  }
@@ -4838,11 +5009,19 @@ var ReferenceValidator = class {
4838
5009
  }
4839
5010
  if (isDataURL(url)) {
4840
5011
  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
- });
5012
+ const forbiddenDataUrlTypes = [
5013
+ "hyperlink" /* HYPERLINK */,
5014
+ "nav-toc-link" /* NAV_TOC_LINK */,
5015
+ "nav-pagelist-link" /* NAV_PAGELIST_LINK */,
5016
+ "cite" /* CITE */
5017
+ ];
5018
+ if (forbiddenDataUrlTypes.includes(reference.type)) {
5019
+ pushMessage(context.messages, {
5020
+ id: MessageId.RSC_029,
5021
+ message: "Data URLs are not allowed in this context",
5022
+ location: reference.location
5023
+ });
5024
+ }
4846
5025
  }
4847
5026
  return;
4848
5027
  }
@@ -4982,6 +5161,14 @@ var ReferenceValidator = class {
4982
5161
  });
4983
5162
  return;
4984
5163
  }
5164
+ if (reference.type === "image" /* IMAGE */ && resource?.mimeType !== "image/svg+xml") {
5165
+ pushMessage(context.messages, {
5166
+ id: MessageId.RSC_009,
5167
+ message: `Fragment identifier used on a non-SVG image resource: ${resourcePath}#${fragment}`,
5168
+ location: reference.location
5169
+ });
5170
+ return;
5171
+ }
4985
5172
  if (resource?.mimeType === "image/svg+xml") {
4986
5173
  const hasSVGView = fragment.includes("svgView(") || fragment.includes("viewBox(");
4987
5174
  if (hasSVGView && reference.type === "hyperlink" /* HYPERLINK */) {