@likecoin/epubcheck-ts 0.6.0 → 0.6.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/bin/epubcheck.js CHANGED
@@ -2,8 +2,8 @@
2
2
  import { readFile, readdir, stat, writeFile } from "node:fs/promises";
3
3
  import { parseArgs } from "node:util";
4
4
  import { basename, join, relative, sep } from "node:path";
5
- const { EpubCheck, EPUB_VERSIONS, toJSONReport } = await import("../dist/index.js");
6
- const VERSION = "0.6.0";
5
+ const { EpubCheck, EPUB_VERSIONS, MessageId, toJSONReport } = await import("../dist/index.js");
6
+ const VERSION = "0.6.1";
7
7
  const VALID_MODES = /* @__PURE__ */ new Set([
8
8
  "exp",
9
9
  "opf",
@@ -301,6 +301,19 @@ async function main() {
301
301
  const shouldFail = result.errorCount > 0 || result.fatalCount > 0 || failOnWarnings && result.warningCount > 0;
302
302
  process.exit(shouldFail ? 1 : 0);
303
303
  } catch (error) {
304
+ const code = error?.code;
305
+ if (code === "ENOENT") {
306
+ console.error(`\x1B[31m\x1B[1mFATAL (${filePath}):\x1B[0m EPUB file could not be found`);
307
+ console.error(` \x1B[90mID: ${MessageId.PKG_018}\x1B[0m`);
308
+ process.exit(1);
309
+ }
310
+ if (code === "EACCES" || code === "EISDIR" || code === "EIO") {
311
+ console.error(
312
+ `\x1B[31m\x1B[1mFATAL (${filePath}):\x1B[0m Unable to read EPUB contents: ${error instanceof Error ? error.message : String(error)}`
313
+ );
314
+ console.error(` \x1B[90mID: ${MessageId.PKG_015}\x1B[0m`);
315
+ process.exit(1);
316
+ }
304
317
  console.error("\x1B[31mError:\x1B[0m", error instanceof Error ? error.message : String(error));
305
318
  if (error instanceof Error && error.stack && !values.quiet) {
306
319
  console.error("\x1B[90m" + error.stack + "\x1B[0m");
package/bin/epubcheck.ts CHANGED
@@ -20,9 +20,9 @@ import type {
20
20
  } from '../src/types.js';
21
21
 
22
22
  // Dynamic import to support both ESM and CJS builds
23
- const { EpubCheck, EPUB_VERSIONS, toJSONReport } = await import('../dist/index.js');
23
+ const { EpubCheck, EPUB_VERSIONS, MessageId, toJSONReport } = await import('../dist/index.js');
24
24
 
25
- const VERSION = '0.6.0';
25
+ const VERSION = '0.6.1';
26
26
  const VALID_MODES: ReadonlySet<ValidationMode> = new Set([
27
27
  'exp',
28
28
  'opf',
@@ -399,6 +399,19 @@ async function main(): Promise<void> {
399
399
  result.errorCount > 0 || result.fatalCount > 0 || (failOnWarnings && result.warningCount > 0);
400
400
  process.exit(shouldFail ? 1 : 0);
401
401
  } catch (error) {
402
+ const code = (error as NodeJS.ErrnoException | undefined)?.code;
403
+ if (code === 'ENOENT') {
404
+ console.error(`\x1b[31m\x1b[1mFATAL (${filePath}):\x1b[0m EPUB file could not be found`);
405
+ console.error(` \x1b[90mID: ${MessageId.PKG_018}\x1b[0m`);
406
+ process.exit(1);
407
+ }
408
+ if (code === 'EACCES' || code === 'EISDIR' || code === 'EIO') {
409
+ console.error(
410
+ `\x1b[31m\x1b[1mFATAL (${filePath}):\x1b[0m Unable to read EPUB contents: ${error instanceof Error ? error.message : String(error)}`,
411
+ );
412
+ console.error(` \x1b[90mID: ${MessageId.PKG_015}\x1b[0m`);
413
+ process.exit(1);
414
+ }
402
415
  console.error('\x1b[31mError:\x1b[0m', error instanceof Error ? error.message : String(error));
403
416
  if (error instanceof Error && error.stack && !values.quiet) {
404
417
  console.error('\x1b[90m' + error.stack + '\x1b[0m');
package/dist/index.cjs CHANGED
@@ -2693,6 +2693,31 @@ var PROFILE_DC_TYPE = {
2693
2693
  dict: "dictionary",
2694
2694
  preview: "preview"
2695
2695
  };
2696
+ var TYPE_TO_PROFILE = {
2697
+ dictionary: "dict",
2698
+ edupub: "edupub",
2699
+ index: "idx",
2700
+ preview: "preview"
2701
+ };
2702
+ var RESERVED_PREFIX_URIS = {
2703
+ dcterms: "http://purl.org/dc/terms/",
2704
+ marc: "http://id.loc.gov/vocabulary/",
2705
+ media: "http://www.idpf.org/epub/vocab/overlays/#",
2706
+ onix: "http://www.editeur.org/ONIX/book/codelists/current.html#",
2707
+ rendition: "http://www.idpf.org/vocab/rendition/#",
2708
+ schema: "http://schema.org/",
2709
+ xsd: "http://www.w3.org/2001/XMLSchema#",
2710
+ a11y: "http://www.idpf.org/epub/vocab/package/a11y/#"
2711
+ };
2712
+ function isValidURI(uri) {
2713
+ if (!uri) return false;
2714
+ try {
2715
+ new URL(uri);
2716
+ return true;
2717
+ } catch {
2718
+ return false;
2719
+ }
2720
+ }
2696
2721
  var DICTIONARY_TYPE_VALUES = /* @__PURE__ */ new Set([
2697
2722
  "monolingual",
2698
2723
  "bilingual",
@@ -2800,11 +2825,13 @@ var OPFValidator = class {
2800
2825
  this.validatePackageAttributes(context, opfPath);
2801
2826
  this.validateMetadata(context, opfPath);
2802
2827
  if (this.packageDoc.version !== "2.0") {
2828
+ this.validatePrefixDeclarations(context, opfPath, opfXml);
2803
2829
  this.validateMetaPrefixes(context, opfPath, opfXml);
2804
2830
  }
2805
2831
  this.validateLinkElements(context, opfPath);
2806
2832
  this.validateManifest(context, opfPath);
2807
2833
  this.validateSpine(context, opfPath);
2834
+ this.validatePageMap(context, opfPath, opfXml);
2808
2835
  this.validateFallbackChains(context, opfPath);
2809
2836
  this.validateUndeclaredResources(context, opfPath);
2810
2837
  if (this.packageDoc.version === "2.0") {
@@ -2835,6 +2862,7 @@ var OPFValidator = class {
2835
2862
  if (this.packageDoc.version.startsWith("3.")) {
2836
2863
  this.validateAccessibilityMetadata(context, opfPath);
2837
2864
  this.validateProfileDcType(context, opfPath);
2865
+ this.validateDcTypeProfileSwitch(context, opfPath);
2838
2866
  this.validateEdupubMetadata(context, opfPath);
2839
2867
  this.validateDictionaryMetadata(context, opfPath);
2840
2868
  this.validatePreviewMetadata(context, opfPath);
@@ -3038,6 +3066,22 @@ var OPFValidator = class {
3038
3066
  });
3039
3067
  }
3040
3068
  }
3069
+ // Mirrors Java's EPUBProfile.makeTypeCompatible flow.
3070
+ validateDcTypeProfileSwitch(context, opfPath) {
3071
+ if (!this.packageDoc) return;
3072
+ for (const dc of this.packageDoc.dcElements) {
3073
+ if (dc.name !== "type") continue;
3074
+ const inferred = TYPE_TO_PROFILE[dc.value.trim().toLowerCase()];
3075
+ if (inferred && inferred !== context.options.profile) {
3076
+ pushMessage(context.messages, {
3077
+ id: MessageId.OPF_064,
3078
+ message: `OPF declares type "${dc.value.trim().toLowerCase()}"; consider validating using the "${inferred}" profile.`,
3079
+ location: { path: opfPath }
3080
+ });
3081
+ return;
3082
+ }
3083
+ }
3084
+ }
3041
3085
  /**
3042
3086
  * Build lookup maps for manifest items
3043
3087
  */
@@ -3054,6 +3098,13 @@ var OPFValidator = class {
3054
3098
  */
3055
3099
  validatePackageAttributes(context, opfPath) {
3056
3100
  if (!this.packageDoc) return;
3101
+ if (this.packageDoc.isLegacyOebps12) {
3102
+ pushMessage(context.messages, {
3103
+ id: MessageId.OPF_047,
3104
+ message: "OPF file is using OEBPS 1.2 syntax allowing backwards compatibility.",
3105
+ location: { path: opfPath }
3106
+ });
3107
+ }
3057
3108
  if (this.packageDoc.versionDeclared === false) {
3058
3109
  pushMessage(context.messages, {
3059
3110
  id: MessageId.OPF_001,
@@ -3978,14 +4029,24 @@ var OPFValidator = class {
3978
4029
  const resolvedPath = resolvePath(opfPath, basePathNoQuery);
3979
4030
  const resolvedPathDecoded = basePathDecodedNoQuery !== basePathNoQuery ? resolvePath(opfPath, basePathDecodedNoQuery) : resolvedPath;
3980
4031
  const fileExists = context.files.has(resolvedPath) || context.files.has(resolvedPathDecoded);
3981
- const inManifest = this.manifestByHref.has(basePathNoQuery) || this.manifestByHref.has(basePathDecodedNoQuery);
3982
- if (!fileExists && !inManifest) {
4032
+ const manifestItem = this.manifestByHref.get(basePathNoQuery) ?? this.manifestByHref.get(basePathDecodedNoQuery);
4033
+ if (!fileExists && !manifestItem) {
3983
4034
  pushMessage(context.messages, {
3984
4035
  id: MessageId.RSC_007w,
3985
4036
  message: `Referenced resource "${resolvedPath}" could not be found in the EPUB`,
3986
4037
  location: { path: opfPath }
3987
4038
  });
3988
4039
  }
4040
+ if (manifestItem) {
4041
+ const inSpine = this.packageDoc.spine.some((ref) => ref.idref === manifestItem.id);
4042
+ if (!inSpine) {
4043
+ pushMessage(context.messages, {
4044
+ id: MessageId.OPF_067,
4045
+ message: `Resource "${manifestItem.href}" is referenced as a link but is also declared as a manifest item.`,
4046
+ location: { path: opfPath }
4047
+ });
4048
+ }
4049
+ }
3989
4050
  }
3990
4051
  }
3991
4052
  /**
@@ -4267,6 +4328,56 @@ var OPFValidator = class {
4267
4328
  }
4268
4329
  }
4269
4330
  }
4331
+ // Mirrors Java's PrefixDeclarationParser + VocabUtil.parsePrefixDeclaration,
4332
+ // but emits only the four main IDs (not Java's OPF-004a..f sub-codes).
4333
+ validatePrefixDeclarations(context, opfPath, opfXml) {
4334
+ const stripped = stripXmlComments(opfXml);
4335
+ const match = /<package[^>]*\sprefix\s*=\s*["']([^"']*)["']/.exec(stripped);
4336
+ if (!match) return;
4337
+ const raw = match[1] ?? "";
4338
+ if (raw !== raw.trim()) {
4339
+ pushMessage(context.messages, {
4340
+ id: MessageId.OPF_004,
4341
+ message: "The value of the prefix attribute has leading or trailing whitespace.",
4342
+ location: { path: opfPath }
4343
+ });
4344
+ }
4345
+ const parts = raw.trim().split(/\s+/).filter(Boolean);
4346
+ for (let i = 0; i < parts.length; ) {
4347
+ const token = parts[i] ?? "";
4348
+ if (token.endsWith(":") && token.length > 1) {
4349
+ const prefix = token.slice(0, -1);
4350
+ const uri = parts[i + 1];
4351
+ if (!uri || uri.endsWith(":")) {
4352
+ pushMessage(context.messages, {
4353
+ id: MessageId.OPF_005,
4354
+ message: `The prefix "${prefix}" is declared but no URI is bound to it.`,
4355
+ location: { path: opfPath }
4356
+ });
4357
+ i += 1;
4358
+ continue;
4359
+ }
4360
+ if (!isValidURI(uri)) {
4361
+ pushMessage(context.messages, {
4362
+ id: MessageId.OPF_006,
4363
+ message: `The value "${uri}" bound to prefix "${prefix}" is not a valid URI.`,
4364
+ location: { path: opfPath }
4365
+ });
4366
+ }
4367
+ const reservedUri = RESERVED_PREFIX_URIS[prefix];
4368
+ if (reservedUri !== void 0 && reservedUri !== uri) {
4369
+ pushMessage(context.messages, {
4370
+ id: MessageId.OPF_007,
4371
+ message: `The prefix "${prefix}" is reserved and must not be re-declared.`,
4372
+ location: { path: opfPath }
4373
+ });
4374
+ }
4375
+ i += 2;
4376
+ } else {
4377
+ i += 1;
4378
+ }
4379
+ }
4380
+ }
4270
4381
  /**
4271
4382
  * RSC-005: all id attributes on elements in the OPF document must be unique.
4272
4383
  * Mirrors Java's id-unique.sch / opf.sch opf_idAttrUnique pattern, which
@@ -4275,16 +4386,7 @@ var OPFValidator = class {
4275
4386
  */
4276
4387
  validateMetaPrefixes(context, opfPath, opfXml) {
4277
4388
  if (!this.packageDoc) return;
4278
- const RESERVED = /* @__PURE__ */ new Set([
4279
- "dcterms",
4280
- "marc",
4281
- "onix",
4282
- "schema",
4283
- "xsd",
4284
- "a11y",
4285
- "media",
4286
- "rendition"
4287
- ]);
4389
+ const RESERVED = new Set(Object.keys(RESERVED_PREFIX_URIS));
4288
4390
  const declared = new Set(Object.keys(this.packageDoc.prefixes ?? {}));
4289
4391
  const reported = /* @__PURE__ */ new Set();
4290
4392
  const reportIfUndeclared = (prefix) => {
@@ -4491,6 +4593,26 @@ var OPFValidator = class {
4491
4593
  }
4492
4594
  }
4493
4595
  }
4596
+ validatePageMap(context, opfPath, opfXml) {
4597
+ if (!this.packageDoc) return;
4598
+ const stripped = stripXmlComments(opfXml);
4599
+ const m = /<spine\b[^>]*\spage-map\s*=\s*["']([^"']*)["']/.exec(stripped);
4600
+ if (!m) return;
4601
+ const pageMapId = (m[1] ?? "").trim();
4602
+ pushMessage(context.messages, {
4603
+ id: MessageId.OPF_062,
4604
+ message: `Found Adobe page-map attribute on spine element (page-map="${pageMapId}")`,
4605
+ location: { path: opfPath }
4606
+ });
4607
+ if (!pageMapId) return;
4608
+ if (!this.manifestById.has(pageMapId)) {
4609
+ pushMessage(context.messages, {
4610
+ id: MessageId.OPF_063,
4611
+ message: `The Adobe page-map item "${pageMapId}" was not found in the manifest`,
4612
+ location: { path: opfPath }
4613
+ });
4614
+ }
4615
+ }
4494
4616
  /**
4495
4617
  * Validate fallback chains
4496
4618
  */
@@ -7214,6 +7336,9 @@ var ContentValidator = class {
7214
7336
  const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
7215
7337
  const opfDir = context.opfPath?.includes("/") ? context.opfPath.substring(0, context.opfPath.lastIndexOf("/")) : "";
7216
7338
  const tocAnchors = tocNav.find(".//html:a[@href]", HTML_NS);
7339
+ if (context.contentFeatures) {
7340
+ context.contentFeatures.tocLinkCount = (context.contentFeatures.tocLinkCount ?? 0) + tocAnchors.length;
7341
+ }
7217
7342
  const tocLinks = [];
7218
7343
  for (const anchor of tocAnchors) {
7219
7344
  const href = this.getAttribute(anchor, "href")?.trim();
@@ -8336,6 +8461,10 @@ var ContentValidator = class {
8336
8461
  if (!features.hasRDFa && root.get(".//*[@property]")) {
8337
8462
  features.hasRDFa = true;
8338
8463
  }
8464
+ if (context.options.profile === "edupub") {
8465
+ const sections = root.find(".//html:body//html:section", XHTML_NS);
8466
+ features.sectionCount = (features.sectionCount ?? 0) + sections.length;
8467
+ }
8339
8468
  }
8340
8469
  validateImages(context, path, root) {
8341
8470
  const packageDoc = context.packageDocument;
@@ -10271,6 +10400,13 @@ var NCXValidator = class {
10271
10400
  });
10272
10401
  return;
10273
10402
  }
10403
+ if (uidContent !== uidContent.trim()) {
10404
+ pushMessage(context.messages, {
10405
+ id: MessageId.NCX_004,
10406
+ message: "NCX dtb:uid meta content has leading or trailing whitespace.",
10407
+ location: { path, line: uidElement.line }
10408
+ });
10409
+ }
10274
10410
  context.ncxUid = uidContent.trim();
10275
10411
  }
10276
10412
  checkNavMap(context, root, path) {
@@ -10900,8 +11036,8 @@ var OCFValidator = class {
10900
11036
  zip = ZipReader.open(context.data);
10901
11037
  } catch (error) {
10902
11038
  pushMessage(context.messages, {
10903
- id: MessageId.PKG_001,
10904
- message: `Failed to open EPUB file: ${error instanceof Error ? error.message : "Unknown error"}`
11039
+ id: MessageId.PKG_004,
11040
+ message: `Failed to open EPUB ZIP: ${error instanceof Error ? error.message : "Unknown error"}`
10905
11041
  });
10906
11042
  return;
10907
11043
  }
@@ -10935,8 +11071,8 @@ var OCFValidator = class {
10935
11071
  const compressionInfo = zip.getMimetypeCompressionInfo();
10936
11072
  if (compressionInfo === null) {
10937
11073
  pushMessage(messages, {
10938
- id: MessageId.PKG_006,
10939
- message: "Could not read ZIP header",
11074
+ id: MessageId.PKG_003,
11075
+ message: "Unable to read EPUB file header, likely corrupted",
10940
11076
  location: { path: "mimetype" }
10941
11077
  });
10942
11078
  return;
@@ -12201,7 +12337,7 @@ var EpubCheck = class _EpubCheck {
12201
12337
  await this.runPipeline(context);
12202
12338
  } catch (error) {
12203
12339
  pushMessage(context.messages, {
12204
- id: MessageId.PKG_025,
12340
+ id: MessageId.PKG_008,
12205
12341
  message: error instanceof Error ? error.message : "Unknown validation error"
12206
12342
  });
12207
12343
  } finally {
@@ -12243,7 +12379,7 @@ var EpubCheck = class _EpubCheck {
12243
12379
  await this.runPipeline(context);
12244
12380
  } catch (error) {
12245
12381
  pushMessage(context.messages, {
12246
- id: MessageId.PKG_025,
12382
+ id: MessageId.PKG_008,
12247
12383
  message: error instanceof Error ? error.message : "Unknown validation error"
12248
12384
  });
12249
12385
  } finally {
@@ -12310,7 +12446,7 @@ var EpubCheck = class _EpubCheck {
12310
12446
  }
12311
12447
  } catch (error) {
12312
12448
  pushMessage(context.messages, {
12313
- id: MessageId.PKG_025,
12449
+ id: MessageId.PKG_008,
12314
12450
  message: error instanceof Error ? error.message : "Unknown validation error"
12315
12451
  });
12316
12452
  } finally {
@@ -12360,6 +12496,15 @@ var EpubCheck = class _EpubCheck {
12360
12496
  const profile = context.options.profile;
12361
12497
  const opfPath = context.opfPath ?? "";
12362
12498
  if (profile === "edupub") {
12499
+ const sectionCount = features.sectionCount ?? 0;
12500
+ const tocLinkCount = features.tocLinkCount ?? 0;
12501
+ if (sectionCount > 0 && sectionCount !== tocLinkCount) {
12502
+ pushMessage(context.messages, {
12503
+ id: MessageId.NAV_004,
12504
+ message: "The Navigation Document should contain the full hierarchy of headings in the document for EDUPUB.",
12505
+ location: { path: opfPath }
12506
+ });
12507
+ }
12363
12508
  if (features.hasPageBreak && !features.hasPageList) {
12364
12509
  pushMessage(context.messages, {
12365
12510
  id: MessageId.NAV_003,
@@ -12700,7 +12845,14 @@ var EpubCheck = class _EpubCheck {
12700
12845
  message: "For maximum compatibility, use only lowercase characters for the EPUB file extension.",
12701
12846
  location: { path: filename }
12702
12847
  });
12848
+ return;
12703
12849
  }
12850
+ const isEpub2 = context.version.startsWith("2");
12851
+ pushMessage(context.messages, {
12852
+ id: isEpub2 ? MessageId.PKG_017 : MessageId.PKG_024,
12853
+ message: `EPUB file has an uncommon extension "${extension}".`,
12854
+ location: { path: filename }
12855
+ });
12704
12856
  }
12705
12857
  /**
12706
12858
  * Build a filtered report from validation context