@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/dist/index.d.cts CHANGED
@@ -1936,6 +1936,8 @@ interface ValidationContext {
1936
1936
  hasLOV?: boolean;
1937
1937
  hasMicrodata?: boolean;
1938
1938
  hasRDFa?: boolean;
1939
+ sectionCount?: number;
1940
+ tocLinkCount?: number;
1939
1941
  };
1940
1942
  }
1941
1943
  /**
package/dist/index.d.ts CHANGED
@@ -1936,6 +1936,8 @@ interface ValidationContext {
1936
1936
  hasLOV?: boolean;
1937
1937
  hasMicrodata?: boolean;
1938
1938
  hasRDFa?: boolean;
1939
+ sectionCount?: number;
1940
+ tocLinkCount?: number;
1939
1941
  };
1940
1942
  }
1941
1943
  /**
package/dist/index.js CHANGED
@@ -2691,6 +2691,31 @@ var PROFILE_DC_TYPE = {
2691
2691
  dict: "dictionary",
2692
2692
  preview: "preview"
2693
2693
  };
2694
+ var TYPE_TO_PROFILE = {
2695
+ dictionary: "dict",
2696
+ edupub: "edupub",
2697
+ index: "idx",
2698
+ preview: "preview"
2699
+ };
2700
+ var RESERVED_PREFIX_URIS = {
2701
+ dcterms: "http://purl.org/dc/terms/",
2702
+ marc: "http://id.loc.gov/vocabulary/",
2703
+ media: "http://www.idpf.org/epub/vocab/overlays/#",
2704
+ onix: "http://www.editeur.org/ONIX/book/codelists/current.html#",
2705
+ rendition: "http://www.idpf.org/vocab/rendition/#",
2706
+ schema: "http://schema.org/",
2707
+ xsd: "http://www.w3.org/2001/XMLSchema#",
2708
+ a11y: "http://www.idpf.org/epub/vocab/package/a11y/#"
2709
+ };
2710
+ function isValidURI(uri) {
2711
+ if (!uri) return false;
2712
+ try {
2713
+ new URL(uri);
2714
+ return true;
2715
+ } catch {
2716
+ return false;
2717
+ }
2718
+ }
2694
2719
  var DICTIONARY_TYPE_VALUES = /* @__PURE__ */ new Set([
2695
2720
  "monolingual",
2696
2721
  "bilingual",
@@ -2798,11 +2823,13 @@ var OPFValidator = class {
2798
2823
  this.validatePackageAttributes(context, opfPath);
2799
2824
  this.validateMetadata(context, opfPath);
2800
2825
  if (this.packageDoc.version !== "2.0") {
2826
+ this.validatePrefixDeclarations(context, opfPath, opfXml);
2801
2827
  this.validateMetaPrefixes(context, opfPath, opfXml);
2802
2828
  }
2803
2829
  this.validateLinkElements(context, opfPath);
2804
2830
  this.validateManifest(context, opfPath);
2805
2831
  this.validateSpine(context, opfPath);
2832
+ this.validatePageMap(context, opfPath, opfXml);
2806
2833
  this.validateFallbackChains(context, opfPath);
2807
2834
  this.validateUndeclaredResources(context, opfPath);
2808
2835
  if (this.packageDoc.version === "2.0") {
@@ -2833,6 +2860,7 @@ var OPFValidator = class {
2833
2860
  if (this.packageDoc.version.startsWith("3.")) {
2834
2861
  this.validateAccessibilityMetadata(context, opfPath);
2835
2862
  this.validateProfileDcType(context, opfPath);
2863
+ this.validateDcTypeProfileSwitch(context, opfPath);
2836
2864
  this.validateEdupubMetadata(context, opfPath);
2837
2865
  this.validateDictionaryMetadata(context, opfPath);
2838
2866
  this.validatePreviewMetadata(context, opfPath);
@@ -3036,6 +3064,22 @@ var OPFValidator = class {
3036
3064
  });
3037
3065
  }
3038
3066
  }
3067
+ // Mirrors Java's EPUBProfile.makeTypeCompatible flow.
3068
+ validateDcTypeProfileSwitch(context, opfPath) {
3069
+ if (!this.packageDoc) return;
3070
+ for (const dc of this.packageDoc.dcElements) {
3071
+ if (dc.name !== "type") continue;
3072
+ const inferred = TYPE_TO_PROFILE[dc.value.trim().toLowerCase()];
3073
+ if (inferred && inferred !== context.options.profile) {
3074
+ pushMessage(context.messages, {
3075
+ id: MessageId.OPF_064,
3076
+ message: `OPF declares type "${dc.value.trim().toLowerCase()}"; consider validating using the "${inferred}" profile.`,
3077
+ location: { path: opfPath }
3078
+ });
3079
+ return;
3080
+ }
3081
+ }
3082
+ }
3039
3083
  /**
3040
3084
  * Build lookup maps for manifest items
3041
3085
  */
@@ -3052,6 +3096,13 @@ var OPFValidator = class {
3052
3096
  */
3053
3097
  validatePackageAttributes(context, opfPath) {
3054
3098
  if (!this.packageDoc) return;
3099
+ if (this.packageDoc.isLegacyOebps12) {
3100
+ pushMessage(context.messages, {
3101
+ id: MessageId.OPF_047,
3102
+ message: "OPF file is using OEBPS 1.2 syntax allowing backwards compatibility.",
3103
+ location: { path: opfPath }
3104
+ });
3105
+ }
3055
3106
  if (this.packageDoc.versionDeclared === false) {
3056
3107
  pushMessage(context.messages, {
3057
3108
  id: MessageId.OPF_001,
@@ -3976,14 +4027,24 @@ var OPFValidator = class {
3976
4027
  const resolvedPath = resolvePath(opfPath, basePathNoQuery);
3977
4028
  const resolvedPathDecoded = basePathDecodedNoQuery !== basePathNoQuery ? resolvePath(opfPath, basePathDecodedNoQuery) : resolvedPath;
3978
4029
  const fileExists = context.files.has(resolvedPath) || context.files.has(resolvedPathDecoded);
3979
- const inManifest = this.manifestByHref.has(basePathNoQuery) || this.manifestByHref.has(basePathDecodedNoQuery);
3980
- if (!fileExists && !inManifest) {
4030
+ const manifestItem = this.manifestByHref.get(basePathNoQuery) ?? this.manifestByHref.get(basePathDecodedNoQuery);
4031
+ if (!fileExists && !manifestItem) {
3981
4032
  pushMessage(context.messages, {
3982
4033
  id: MessageId.RSC_007w,
3983
4034
  message: `Referenced resource "${resolvedPath}" could not be found in the EPUB`,
3984
4035
  location: { path: opfPath }
3985
4036
  });
3986
4037
  }
4038
+ if (manifestItem) {
4039
+ const inSpine = this.packageDoc.spine.some((ref) => ref.idref === manifestItem.id);
4040
+ if (!inSpine) {
4041
+ pushMessage(context.messages, {
4042
+ id: MessageId.OPF_067,
4043
+ message: `Resource "${manifestItem.href}" is referenced as a link but is also declared as a manifest item.`,
4044
+ location: { path: opfPath }
4045
+ });
4046
+ }
4047
+ }
3987
4048
  }
3988
4049
  }
3989
4050
  /**
@@ -4265,6 +4326,56 @@ var OPFValidator = class {
4265
4326
  }
4266
4327
  }
4267
4328
  }
4329
+ // Mirrors Java's PrefixDeclarationParser + VocabUtil.parsePrefixDeclaration,
4330
+ // but emits only the four main IDs (not Java's OPF-004a..f sub-codes).
4331
+ validatePrefixDeclarations(context, opfPath, opfXml) {
4332
+ const stripped = stripXmlComments(opfXml);
4333
+ const match = /<package[^>]*\sprefix\s*=\s*["']([^"']*)["']/.exec(stripped);
4334
+ if (!match) return;
4335
+ const raw = match[1] ?? "";
4336
+ if (raw !== raw.trim()) {
4337
+ pushMessage(context.messages, {
4338
+ id: MessageId.OPF_004,
4339
+ message: "The value of the prefix attribute has leading or trailing whitespace.",
4340
+ location: { path: opfPath }
4341
+ });
4342
+ }
4343
+ const parts = raw.trim().split(/\s+/).filter(Boolean);
4344
+ for (let i = 0; i < parts.length; ) {
4345
+ const token = parts[i] ?? "";
4346
+ if (token.endsWith(":") && token.length > 1) {
4347
+ const prefix = token.slice(0, -1);
4348
+ const uri = parts[i + 1];
4349
+ if (!uri || uri.endsWith(":")) {
4350
+ pushMessage(context.messages, {
4351
+ id: MessageId.OPF_005,
4352
+ message: `The prefix "${prefix}" is declared but no URI is bound to it.`,
4353
+ location: { path: opfPath }
4354
+ });
4355
+ i += 1;
4356
+ continue;
4357
+ }
4358
+ if (!isValidURI(uri)) {
4359
+ pushMessage(context.messages, {
4360
+ id: MessageId.OPF_006,
4361
+ message: `The value "${uri}" bound to prefix "${prefix}" is not a valid URI.`,
4362
+ location: { path: opfPath }
4363
+ });
4364
+ }
4365
+ const reservedUri = RESERVED_PREFIX_URIS[prefix];
4366
+ if (reservedUri !== void 0 && reservedUri !== uri) {
4367
+ pushMessage(context.messages, {
4368
+ id: MessageId.OPF_007,
4369
+ message: `The prefix "${prefix}" is reserved and must not be re-declared.`,
4370
+ location: { path: opfPath }
4371
+ });
4372
+ }
4373
+ i += 2;
4374
+ } else {
4375
+ i += 1;
4376
+ }
4377
+ }
4378
+ }
4268
4379
  /**
4269
4380
  * RSC-005: all id attributes on elements in the OPF document must be unique.
4270
4381
  * Mirrors Java's id-unique.sch / opf.sch opf_idAttrUnique pattern, which
@@ -4273,16 +4384,7 @@ var OPFValidator = class {
4273
4384
  */
4274
4385
  validateMetaPrefixes(context, opfPath, opfXml) {
4275
4386
  if (!this.packageDoc) return;
4276
- const RESERVED = /* @__PURE__ */ new Set([
4277
- "dcterms",
4278
- "marc",
4279
- "onix",
4280
- "schema",
4281
- "xsd",
4282
- "a11y",
4283
- "media",
4284
- "rendition"
4285
- ]);
4387
+ const RESERVED = new Set(Object.keys(RESERVED_PREFIX_URIS));
4286
4388
  const declared = new Set(Object.keys(this.packageDoc.prefixes ?? {}));
4287
4389
  const reported = /* @__PURE__ */ new Set();
4288
4390
  const reportIfUndeclared = (prefix) => {
@@ -4489,6 +4591,26 @@ var OPFValidator = class {
4489
4591
  }
4490
4592
  }
4491
4593
  }
4594
+ validatePageMap(context, opfPath, opfXml) {
4595
+ if (!this.packageDoc) return;
4596
+ const stripped = stripXmlComments(opfXml);
4597
+ const m = /<spine\b[^>]*\spage-map\s*=\s*["']([^"']*)["']/.exec(stripped);
4598
+ if (!m) return;
4599
+ const pageMapId = (m[1] ?? "").trim();
4600
+ pushMessage(context.messages, {
4601
+ id: MessageId.OPF_062,
4602
+ message: `Found Adobe page-map attribute on spine element (page-map="${pageMapId}")`,
4603
+ location: { path: opfPath }
4604
+ });
4605
+ if (!pageMapId) return;
4606
+ if (!this.manifestById.has(pageMapId)) {
4607
+ pushMessage(context.messages, {
4608
+ id: MessageId.OPF_063,
4609
+ message: `The Adobe page-map item "${pageMapId}" was not found in the manifest`,
4610
+ location: { path: opfPath }
4611
+ });
4612
+ }
4613
+ }
4492
4614
  /**
4493
4615
  * Validate fallback chains
4494
4616
  */
@@ -7212,6 +7334,9 @@ var ContentValidator = class {
7212
7334
  const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
7213
7335
  const opfDir = context.opfPath?.includes("/") ? context.opfPath.substring(0, context.opfPath.lastIndexOf("/")) : "";
7214
7336
  const tocAnchors = tocNav.find(".//html:a[@href]", HTML_NS);
7337
+ if (context.contentFeatures) {
7338
+ context.contentFeatures.tocLinkCount = (context.contentFeatures.tocLinkCount ?? 0) + tocAnchors.length;
7339
+ }
7215
7340
  const tocLinks = [];
7216
7341
  for (const anchor of tocAnchors) {
7217
7342
  const href = this.getAttribute(anchor, "href")?.trim();
@@ -8334,6 +8459,10 @@ var ContentValidator = class {
8334
8459
  if (!features.hasRDFa && root.get(".//*[@property]")) {
8335
8460
  features.hasRDFa = true;
8336
8461
  }
8462
+ if (context.options.profile === "edupub") {
8463
+ const sections = root.find(".//html:body//html:section", XHTML_NS);
8464
+ features.sectionCount = (features.sectionCount ?? 0) + sections.length;
8465
+ }
8337
8466
  }
8338
8467
  validateImages(context, path, root) {
8339
8468
  const packageDoc = context.packageDocument;
@@ -10269,6 +10398,13 @@ var NCXValidator = class {
10269
10398
  });
10270
10399
  return;
10271
10400
  }
10401
+ if (uidContent !== uidContent.trim()) {
10402
+ pushMessage(context.messages, {
10403
+ id: MessageId.NCX_004,
10404
+ message: "NCX dtb:uid meta content has leading or trailing whitespace.",
10405
+ location: { path, line: uidElement.line }
10406
+ });
10407
+ }
10272
10408
  context.ncxUid = uidContent.trim();
10273
10409
  }
10274
10410
  checkNavMap(context, root, path) {
@@ -10898,8 +11034,8 @@ var OCFValidator = class {
10898
11034
  zip = ZipReader.open(context.data);
10899
11035
  } catch (error) {
10900
11036
  pushMessage(context.messages, {
10901
- id: MessageId.PKG_001,
10902
- message: `Failed to open EPUB file: ${error instanceof Error ? error.message : "Unknown error"}`
11037
+ id: MessageId.PKG_004,
11038
+ message: `Failed to open EPUB ZIP: ${error instanceof Error ? error.message : "Unknown error"}`
10903
11039
  });
10904
11040
  return;
10905
11041
  }
@@ -10933,8 +11069,8 @@ var OCFValidator = class {
10933
11069
  const compressionInfo = zip.getMimetypeCompressionInfo();
10934
11070
  if (compressionInfo === null) {
10935
11071
  pushMessage(messages, {
10936
- id: MessageId.PKG_006,
10937
- message: "Could not read ZIP header",
11072
+ id: MessageId.PKG_003,
11073
+ message: "Unable to read EPUB file header, likely corrupted",
10938
11074
  location: { path: "mimetype" }
10939
11075
  });
10940
11076
  return;
@@ -12199,7 +12335,7 @@ var EpubCheck = class _EpubCheck {
12199
12335
  await this.runPipeline(context);
12200
12336
  } catch (error) {
12201
12337
  pushMessage(context.messages, {
12202
- id: MessageId.PKG_025,
12338
+ id: MessageId.PKG_008,
12203
12339
  message: error instanceof Error ? error.message : "Unknown validation error"
12204
12340
  });
12205
12341
  } finally {
@@ -12241,7 +12377,7 @@ var EpubCheck = class _EpubCheck {
12241
12377
  await this.runPipeline(context);
12242
12378
  } catch (error) {
12243
12379
  pushMessage(context.messages, {
12244
- id: MessageId.PKG_025,
12380
+ id: MessageId.PKG_008,
12245
12381
  message: error instanceof Error ? error.message : "Unknown validation error"
12246
12382
  });
12247
12383
  } finally {
@@ -12308,7 +12444,7 @@ var EpubCheck = class _EpubCheck {
12308
12444
  }
12309
12445
  } catch (error) {
12310
12446
  pushMessage(context.messages, {
12311
- id: MessageId.PKG_025,
12447
+ id: MessageId.PKG_008,
12312
12448
  message: error instanceof Error ? error.message : "Unknown validation error"
12313
12449
  });
12314
12450
  } finally {
@@ -12358,6 +12494,15 @@ var EpubCheck = class _EpubCheck {
12358
12494
  const profile = context.options.profile;
12359
12495
  const opfPath = context.opfPath ?? "";
12360
12496
  if (profile === "edupub") {
12497
+ const sectionCount = features.sectionCount ?? 0;
12498
+ const tocLinkCount = features.tocLinkCount ?? 0;
12499
+ if (sectionCount > 0 && sectionCount !== tocLinkCount) {
12500
+ pushMessage(context.messages, {
12501
+ id: MessageId.NAV_004,
12502
+ message: "The Navigation Document should contain the full hierarchy of headings in the document for EDUPUB.",
12503
+ location: { path: opfPath }
12504
+ });
12505
+ }
12361
12506
  if (features.hasPageBreak && !features.hasPageList) {
12362
12507
  pushMessage(context.messages, {
12363
12508
  id: MessageId.NAV_003,
@@ -12698,7 +12843,14 @@ var EpubCheck = class _EpubCheck {
12698
12843
  message: "For maximum compatibility, use only lowercase characters for the EPUB file extension.",
12699
12844
  location: { path: filename }
12700
12845
  });
12846
+ return;
12701
12847
  }
12848
+ const isEpub2 = context.version.startsWith("2");
12849
+ pushMessage(context.messages, {
12850
+ id: isEpub2 ? MessageId.PKG_017 : MessageId.PKG_024,
12851
+ message: `EPUB file has an uncommon extension "${extension}".`,
12852
+ location: { path: filename }
12853
+ });
12702
12854
  }
12703
12855
  /**
12704
12856
  * Build a filtered report from validation context