@likecoin/epubcheck-ts 0.6.0 → 0.6.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
@@ -114,17 +114,22 @@ if (result.valid) {
114
114
  const fs = require('node:fs');
115
115
 
116
116
  async function validate() {
117
- const { EpubCheck } = await import('epubcheck-ts');
118
-
117
+ const { EpubCheck } = require('@likecoin/epubcheck-ts');
118
+
119
119
  const epubData = fs.readFileSync('book.epub');
120
120
  const result = await EpubCheck.validate(epubData);
121
-
121
+
122
122
  console.log(result.valid ? 'Valid!' : 'Invalid');
123
123
  }
124
124
 
125
125
  validate();
126
126
  ```
127
127
 
128
+ > The XML engine (`libxml2-wasm`, ESM-only with top-level await) is lazy-loaded
129
+ > inside `EpubCheck.validate()`, so the package stays `require()`-able from
130
+ > CommonJS and importable without forcing top-level-await support on your
131
+ > bundler.
132
+
128
133
  ### Browser
129
134
 
130
135
  ```typescript
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.2";
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.2';
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
@@ -1,10 +1,25 @@
1
1
  'use strict';
2
2
 
3
- var libxml2Wasm = require('libxml2-wasm');
4
3
  var cssTree = require('css-tree');
5
4
  var fflate = require('fflate');
6
5
 
7
- // src/content/validator.ts
6
+ // src/util/xml-engine.ts
7
+ var engine;
8
+ async function loadXmlEngine() {
9
+ engine ??= await import('libxml2-wasm');
10
+ }
11
+ function getXmlDocument() {
12
+ if (!engine) {
13
+ throw new Error("libxml2-wasm not initialized \u2014 call loadXmlEngine() first");
14
+ }
15
+ return engine.XmlDocument;
16
+ }
17
+ function getXmlElement() {
18
+ if (!engine) {
19
+ throw new Error("libxml2-wasm not initialized \u2014 call loadXmlEngine() first");
20
+ }
21
+ return engine.XmlElement;
22
+ }
8
23
 
9
24
  // src/messages/messages.ts
10
25
  var severityOverrides = /* @__PURE__ */ new Map();
@@ -2693,6 +2708,31 @@ var PROFILE_DC_TYPE = {
2693
2708
  dict: "dictionary",
2694
2709
  preview: "preview"
2695
2710
  };
2711
+ var TYPE_TO_PROFILE = {
2712
+ dictionary: "dict",
2713
+ edupub: "edupub",
2714
+ index: "idx",
2715
+ preview: "preview"
2716
+ };
2717
+ var RESERVED_PREFIX_URIS = {
2718
+ dcterms: "http://purl.org/dc/terms/",
2719
+ marc: "http://id.loc.gov/vocabulary/",
2720
+ media: "http://www.idpf.org/epub/vocab/overlays/#",
2721
+ onix: "http://www.editeur.org/ONIX/book/codelists/current.html#",
2722
+ rendition: "http://www.idpf.org/vocab/rendition/#",
2723
+ schema: "http://schema.org/",
2724
+ xsd: "http://www.w3.org/2001/XMLSchema#",
2725
+ a11y: "http://www.idpf.org/epub/vocab/package/a11y/#"
2726
+ };
2727
+ function isValidURI(uri) {
2728
+ if (!uri) return false;
2729
+ try {
2730
+ new URL(uri);
2731
+ return true;
2732
+ } catch {
2733
+ return false;
2734
+ }
2735
+ }
2696
2736
  var DICTIONARY_TYPE_VALUES = /* @__PURE__ */ new Set([
2697
2737
  "monolingual",
2698
2738
  "bilingual",
@@ -2800,11 +2840,13 @@ var OPFValidator = class {
2800
2840
  this.validatePackageAttributes(context, opfPath);
2801
2841
  this.validateMetadata(context, opfPath);
2802
2842
  if (this.packageDoc.version !== "2.0") {
2843
+ this.validatePrefixDeclarations(context, opfPath, opfXml);
2803
2844
  this.validateMetaPrefixes(context, opfPath, opfXml);
2804
2845
  }
2805
2846
  this.validateLinkElements(context, opfPath);
2806
2847
  this.validateManifest(context, opfPath);
2807
2848
  this.validateSpine(context, opfPath);
2849
+ this.validatePageMap(context, opfPath, opfXml);
2808
2850
  this.validateFallbackChains(context, opfPath);
2809
2851
  this.validateUndeclaredResources(context, opfPath);
2810
2852
  if (this.packageDoc.version === "2.0") {
@@ -2835,6 +2877,7 @@ var OPFValidator = class {
2835
2877
  if (this.packageDoc.version.startsWith("3.")) {
2836
2878
  this.validateAccessibilityMetadata(context, opfPath);
2837
2879
  this.validateProfileDcType(context, opfPath);
2880
+ this.validateDcTypeProfileSwitch(context, opfPath);
2838
2881
  this.validateEdupubMetadata(context, opfPath);
2839
2882
  this.validateDictionaryMetadata(context, opfPath);
2840
2883
  this.validatePreviewMetadata(context, opfPath);
@@ -3038,6 +3081,22 @@ var OPFValidator = class {
3038
3081
  });
3039
3082
  }
3040
3083
  }
3084
+ // Mirrors Java's EPUBProfile.makeTypeCompatible flow.
3085
+ validateDcTypeProfileSwitch(context, opfPath) {
3086
+ if (!this.packageDoc) return;
3087
+ for (const dc of this.packageDoc.dcElements) {
3088
+ if (dc.name !== "type") continue;
3089
+ const inferred = TYPE_TO_PROFILE[dc.value.trim().toLowerCase()];
3090
+ if (inferred && inferred !== context.options.profile) {
3091
+ pushMessage(context.messages, {
3092
+ id: MessageId.OPF_064,
3093
+ message: `OPF declares type "${dc.value.trim().toLowerCase()}"; consider validating using the "${inferred}" profile.`,
3094
+ location: { path: opfPath }
3095
+ });
3096
+ return;
3097
+ }
3098
+ }
3099
+ }
3041
3100
  /**
3042
3101
  * Build lookup maps for manifest items
3043
3102
  */
@@ -3054,6 +3113,13 @@ var OPFValidator = class {
3054
3113
  */
3055
3114
  validatePackageAttributes(context, opfPath) {
3056
3115
  if (!this.packageDoc) return;
3116
+ if (this.packageDoc.isLegacyOebps12) {
3117
+ pushMessage(context.messages, {
3118
+ id: MessageId.OPF_047,
3119
+ message: "OPF file is using OEBPS 1.2 syntax allowing backwards compatibility.",
3120
+ location: { path: opfPath }
3121
+ });
3122
+ }
3057
3123
  if (this.packageDoc.versionDeclared === false) {
3058
3124
  pushMessage(context.messages, {
3059
3125
  id: MessageId.OPF_001,
@@ -3978,14 +4044,24 @@ var OPFValidator = class {
3978
4044
  const resolvedPath = resolvePath(opfPath, basePathNoQuery);
3979
4045
  const resolvedPathDecoded = basePathDecodedNoQuery !== basePathNoQuery ? resolvePath(opfPath, basePathDecodedNoQuery) : resolvedPath;
3980
4046
  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) {
4047
+ const manifestItem = this.manifestByHref.get(basePathNoQuery) ?? this.manifestByHref.get(basePathDecodedNoQuery);
4048
+ if (!fileExists && !manifestItem) {
3983
4049
  pushMessage(context.messages, {
3984
4050
  id: MessageId.RSC_007w,
3985
4051
  message: `Referenced resource "${resolvedPath}" could not be found in the EPUB`,
3986
4052
  location: { path: opfPath }
3987
4053
  });
3988
4054
  }
4055
+ if (manifestItem) {
4056
+ const inSpine = this.packageDoc.spine.some((ref) => ref.idref === manifestItem.id);
4057
+ if (!inSpine) {
4058
+ pushMessage(context.messages, {
4059
+ id: MessageId.OPF_067,
4060
+ message: `Resource "${manifestItem.href}" is referenced as a link but is also declared as a manifest item.`,
4061
+ location: { path: opfPath }
4062
+ });
4063
+ }
4064
+ }
3989
4065
  }
3990
4066
  }
3991
4067
  /**
@@ -4267,6 +4343,56 @@ var OPFValidator = class {
4267
4343
  }
4268
4344
  }
4269
4345
  }
4346
+ // Mirrors Java's PrefixDeclarationParser + VocabUtil.parsePrefixDeclaration,
4347
+ // but emits only the four main IDs (not Java's OPF-004a..f sub-codes).
4348
+ validatePrefixDeclarations(context, opfPath, opfXml) {
4349
+ const stripped = stripXmlComments(opfXml);
4350
+ const match = /<package[^>]*\sprefix\s*=\s*["']([^"']*)["']/.exec(stripped);
4351
+ if (!match) return;
4352
+ const raw = match[1] ?? "";
4353
+ if (raw !== raw.trim()) {
4354
+ pushMessage(context.messages, {
4355
+ id: MessageId.OPF_004,
4356
+ message: "The value of the prefix attribute has leading or trailing whitespace.",
4357
+ location: { path: opfPath }
4358
+ });
4359
+ }
4360
+ const parts = raw.trim().split(/\s+/).filter(Boolean);
4361
+ for (let i = 0; i < parts.length; ) {
4362
+ const token = parts[i] ?? "";
4363
+ if (token.endsWith(":") && token.length > 1) {
4364
+ const prefix = token.slice(0, -1);
4365
+ const uri = parts[i + 1];
4366
+ if (!uri || uri.endsWith(":")) {
4367
+ pushMessage(context.messages, {
4368
+ id: MessageId.OPF_005,
4369
+ message: `The prefix "${prefix}" is declared but no URI is bound to it.`,
4370
+ location: { path: opfPath }
4371
+ });
4372
+ i += 1;
4373
+ continue;
4374
+ }
4375
+ if (!isValidURI(uri)) {
4376
+ pushMessage(context.messages, {
4377
+ id: MessageId.OPF_006,
4378
+ message: `The value "${uri}" bound to prefix "${prefix}" is not a valid URI.`,
4379
+ location: { path: opfPath }
4380
+ });
4381
+ }
4382
+ const reservedUri = RESERVED_PREFIX_URIS[prefix];
4383
+ if (reservedUri !== void 0 && reservedUri !== uri) {
4384
+ pushMessage(context.messages, {
4385
+ id: MessageId.OPF_007,
4386
+ message: `The prefix "${prefix}" is reserved and must not be re-declared.`,
4387
+ location: { path: opfPath }
4388
+ });
4389
+ }
4390
+ i += 2;
4391
+ } else {
4392
+ i += 1;
4393
+ }
4394
+ }
4395
+ }
4270
4396
  /**
4271
4397
  * RSC-005: all id attributes on elements in the OPF document must be unique.
4272
4398
  * Mirrors Java's id-unique.sch / opf.sch opf_idAttrUnique pattern, which
@@ -4275,16 +4401,7 @@ var OPFValidator = class {
4275
4401
  */
4276
4402
  validateMetaPrefixes(context, opfPath, opfXml) {
4277
4403
  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
- ]);
4404
+ const RESERVED = new Set(Object.keys(RESERVED_PREFIX_URIS));
4288
4405
  const declared = new Set(Object.keys(this.packageDoc.prefixes ?? {}));
4289
4406
  const reported = /* @__PURE__ */ new Set();
4290
4407
  const reportIfUndeclared = (prefix) => {
@@ -4491,6 +4608,26 @@ var OPFValidator = class {
4491
4608
  }
4492
4609
  }
4493
4610
  }
4611
+ validatePageMap(context, opfPath, opfXml) {
4612
+ if (!this.packageDoc) return;
4613
+ const stripped = stripXmlComments(opfXml);
4614
+ const m = /<spine\b[^>]*\spage-map\s*=\s*["']([^"']*)["']/.exec(stripped);
4615
+ if (!m) return;
4616
+ const pageMapId = (m[1] ?? "").trim();
4617
+ pushMessage(context.messages, {
4618
+ id: MessageId.OPF_062,
4619
+ message: `Found Adobe page-map attribute on spine element (page-map="${pageMapId}")`,
4620
+ location: { path: opfPath }
4621
+ });
4622
+ if (!pageMapId) return;
4623
+ if (!this.manifestById.has(pageMapId)) {
4624
+ pushMessage(context.messages, {
4625
+ id: MessageId.OPF_063,
4626
+ message: `The Adobe page-map item "${pageMapId}" was not found in the manifest`,
4627
+ location: { path: opfPath }
4628
+ });
4629
+ }
4630
+ }
4494
4631
  /**
4495
4632
  * Validate fallback chains
4496
4633
  */
@@ -4962,7 +5099,7 @@ var SKMValidator = class {
4962
5099
  const content = typeof data === "string" ? data : new TextDecoder().decode(data);
4963
5100
  let doc = null;
4964
5101
  try {
4965
- doc = libxml2Wasm.XmlDocument.fromString(content);
5102
+ doc = getXmlDocument().fromString(content);
4966
5103
  } catch {
4967
5104
  pushMessage(context.messages, {
4968
5105
  id: MessageId.RSC_016,
@@ -5199,7 +5336,7 @@ var SMILValidator = class {
5199
5336
  const content = typeof data === "string" ? data : new TextDecoder().decode(data);
5200
5337
  let doc = null;
5201
5338
  try {
5202
- doc = libxml2Wasm.XmlDocument.fromString(content);
5339
+ doc = getXmlDocument().fromString(content);
5203
5340
  } catch {
5204
5341
  pushMessage(context.messages, {
5205
5342
  id: MessageId.RSC_016,
@@ -6109,7 +6246,7 @@ var ContentValidator = class {
6109
6246
  const svgContent = new TextDecoder().decode(svgData);
6110
6247
  let doc;
6111
6248
  try {
6112
- doc = libxml2Wasm.XmlDocument.fromString(svgContent);
6249
+ doc = getXmlDocument().fromString(svgContent);
6113
6250
  this.extractAndRegisterIDs(path, doc.root, registry);
6114
6251
  } catch (e) {
6115
6252
  pushMessage(context.messages, {
@@ -6127,7 +6264,7 @@ var ContentValidator = class {
6127
6264
  const svgContent = new TextDecoder().decode(svgData);
6128
6265
  let doc;
6129
6266
  try {
6130
- doc = libxml2Wasm.XmlDocument.fromString(svgContent);
6267
+ doc = getXmlDocument().fromString(svgContent);
6131
6268
  } catch {
6132
6269
  return;
6133
6270
  }
@@ -6173,7 +6310,7 @@ var ContentValidator = class {
6173
6310
  const svgContent = new TextDecoder().decode(svgData);
6174
6311
  let doc;
6175
6312
  try {
6176
- doc = libxml2Wasm.XmlDocument.fromString(svgContent);
6313
+ doc = getXmlDocument().fromString(svgContent);
6177
6314
  } catch {
6178
6315
  return;
6179
6316
  }
@@ -6523,7 +6660,7 @@ var ContentValidator = class {
6523
6660
  }
6524
6661
  let doc = null;
6525
6662
  try {
6526
- doc = libxml2Wasm.XmlDocument.fromString(content);
6663
+ doc = getXmlDocument().fromString(content);
6527
6664
  } catch (error) {
6528
6665
  if (error instanceof Error) {
6529
6666
  const { message, line, column } = this.parseLibxmlError(error.message);
@@ -7214,6 +7351,9 @@ var ContentValidator = class {
7214
7351
  const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
7215
7352
  const opfDir = context.opfPath?.includes("/") ? context.opfPath.substring(0, context.opfPath.lastIndexOf("/")) : "";
7216
7353
  const tocAnchors = tocNav.find(".//html:a[@href]", HTML_NS);
7354
+ if (context.contentFeatures) {
7355
+ context.contentFeatures.tocLinkCount = (context.contentFeatures.tocLinkCount ?? 0) + tocAnchors.length;
7356
+ }
7217
7357
  const tocLinks = [];
7218
7358
  for (const anchor of tocAnchors) {
7219
7359
  const href = this.getAttribute(anchor, "href")?.trim();
@@ -8336,6 +8476,10 @@ var ContentValidator = class {
8336
8476
  if (!features.hasRDFa && root.get(".//*[@property]")) {
8337
8477
  features.hasRDFa = true;
8338
8478
  }
8479
+ if (context.options.profile === "edupub") {
8480
+ const sections = root.find(".//html:body//html:section", XHTML_NS);
8481
+ features.sectionCount = (features.sectionCount ?? 0) + sections.length;
8482
+ }
8339
8483
  }
8340
8484
  validateImages(context, path, root) {
8341
8485
  const packageDoc = context.packageDocument;
@@ -8601,11 +8745,12 @@ var ContentValidator = class {
8601
8745
  }
8602
8746
  return Number.parseInt(el.name.substring(1), 10);
8603
8747
  };
8748
+ const XmlElement = getXmlElement();
8604
8749
  const directElementChildren = (parent) => {
8605
8750
  const out = [];
8606
8751
  let n = parent.firstChild;
8607
8752
  while (n) {
8608
- if (n instanceof libxml2Wasm.XmlElement) out.push(n);
8753
+ if (n instanceof XmlElement) out.push(n);
8609
8754
  n = n.next;
8610
8755
  }
8611
8756
  return out;
@@ -8759,7 +8904,7 @@ var ContentValidator = class {
8759
8904
  let prev = p.prev;
8760
8905
  let hasHeadingBefore = false;
8761
8906
  while (prev) {
8762
- if (prev instanceof libxml2Wasm.XmlElement && /^h[1-6]$/.test(prev.name)) {
8907
+ if (prev instanceof XmlElement && /^h[1-6]$/.test(prev.name)) {
8763
8908
  hasHeadingBefore = true;
8764
8909
  break;
8765
8910
  }
@@ -10209,11 +10354,13 @@ function toJSONReport(result) {
10209
10354
  2
10210
10355
  );
10211
10356
  }
10357
+
10358
+ // src/ncx/validator.ts
10212
10359
  var NCXValidator = class {
10213
10360
  validate(context, ncxContent, ncxPath, registry) {
10214
10361
  let doc = null;
10215
10362
  try {
10216
- doc = libxml2Wasm.XmlDocument.fromString(ncxContent);
10363
+ doc = getXmlDocument().fromString(ncxContent);
10217
10364
  } catch (error) {
10218
10365
  if (error instanceof Error) {
10219
10366
  pushMessage(context.messages, {
@@ -10271,6 +10418,13 @@ var NCXValidator = class {
10271
10418
  });
10272
10419
  return;
10273
10420
  }
10421
+ if (uidContent !== uidContent.trim()) {
10422
+ pushMessage(context.messages, {
10423
+ id: MessageId.NCX_004,
10424
+ message: "NCX dtb:uid meta content has leading or trailing whitespace.",
10425
+ location: { path, line: uidElement.line }
10426
+ });
10427
+ }
10274
10428
  context.ncxUid = uidContent.trim();
10275
10429
  }
10276
10430
  checkNavMap(context, root, path) {
@@ -10900,8 +11054,8 @@ var OCFValidator = class {
10900
11054
  zip = ZipReader.open(context.data);
10901
11055
  } catch (error) {
10902
11056
  pushMessage(context.messages, {
10903
- id: MessageId.PKG_001,
10904
- message: `Failed to open EPUB file: ${error instanceof Error ? error.message : "Unknown error"}`
11057
+ id: MessageId.PKG_004,
11058
+ message: `Failed to open EPUB ZIP: ${error instanceof Error ? error.message : "Unknown error"}`
10905
11059
  });
10906
11060
  return;
10907
11061
  }
@@ -10935,8 +11089,8 @@ var OCFValidator = class {
10935
11089
  const compressionInfo = zip.getMimetypeCompressionInfo();
10936
11090
  if (compressionInfo === null) {
10937
11091
  pushMessage(messages, {
10938
- id: MessageId.PKG_006,
10939
- message: "Could not read ZIP header",
11092
+ id: MessageId.PKG_003,
11093
+ message: "Unable to read EPUB file header, likely corrupted",
10940
11094
  location: { path: "mimetype" }
10941
11095
  });
10942
11096
  return;
@@ -11963,11 +12117,11 @@ var RelaxNGValidator = class extends BaseSchemaValidator {
11963
12117
  try {
11964
12118
  const libxml2 = await import('libxml2-wasm');
11965
12119
  const LibRelaxNGValidator = libxml2.RelaxNGValidator;
11966
- const { XmlDocument: XmlDocument5 } = libxml2;
11967
- const doc = XmlDocument5.fromString(xml);
12120
+ const { XmlDocument } = libxml2;
12121
+ const doc = XmlDocument.fromString(xml);
11968
12122
  try {
11969
12123
  const schemaContent = await loadSchema(schemaPath);
11970
- const schemaDoc = XmlDocument5.fromString(schemaContent);
12124
+ const schemaDoc = XmlDocument.fromString(schemaContent);
11971
12125
  try {
11972
12126
  const validator = LibRelaxNGValidator.fromDoc(schemaDoc);
11973
12127
  try {
@@ -12177,6 +12331,7 @@ var EpubCheck = class _EpubCheck {
12177
12331
  */
12178
12332
  async check(data, filename) {
12179
12333
  const startTime = performance.now();
12334
+ await loadXmlEngine();
12180
12335
  const context = {
12181
12336
  data,
12182
12337
  options: this.options,
@@ -12201,7 +12356,7 @@ var EpubCheck = class _EpubCheck {
12201
12356
  await this.runPipeline(context);
12202
12357
  } catch (error) {
12203
12358
  pushMessage(context.messages, {
12204
- id: MessageId.PKG_025,
12359
+ id: MessageId.PKG_008,
12205
12360
  message: error instanceof Error ? error.message : "Unknown validation error"
12206
12361
  });
12207
12362
  } finally {
@@ -12218,6 +12373,7 @@ var EpubCheck = class _EpubCheck {
12218
12373
  */
12219
12374
  async checkExpanded(files) {
12220
12375
  const startTime = performance.now();
12376
+ await loadXmlEngine();
12221
12377
  const context = {
12222
12378
  data: new Uint8Array(0),
12223
12379
  options: this.options,
@@ -12243,7 +12399,7 @@ var EpubCheck = class _EpubCheck {
12243
12399
  await this.runPipeline(context);
12244
12400
  } catch (error) {
12245
12401
  pushMessage(context.messages, {
12246
- id: MessageId.PKG_025,
12402
+ id: MessageId.PKG_008,
12247
12403
  message: error instanceof Error ? error.message : "Unknown validation error"
12248
12404
  });
12249
12405
  } finally {
@@ -12262,6 +12418,7 @@ var EpubCheck = class _EpubCheck {
12262
12418
  async checkSingleFile(data, filename) {
12263
12419
  const startTime = performance.now();
12264
12420
  const mode = this.options.mode;
12421
+ await loadXmlEngine();
12265
12422
  const context = {
12266
12423
  data: new Uint8Array(0),
12267
12424
  options: this.options,
@@ -12310,7 +12467,7 @@ var EpubCheck = class _EpubCheck {
12310
12467
  }
12311
12468
  } catch (error) {
12312
12469
  pushMessage(context.messages, {
12313
- id: MessageId.PKG_025,
12470
+ id: MessageId.PKG_008,
12314
12471
  message: error instanceof Error ? error.message : "Unknown validation error"
12315
12472
  });
12316
12473
  } finally {
@@ -12360,6 +12517,15 @@ var EpubCheck = class _EpubCheck {
12360
12517
  const profile = context.options.profile;
12361
12518
  const opfPath = context.opfPath ?? "";
12362
12519
  if (profile === "edupub") {
12520
+ const sectionCount = features.sectionCount ?? 0;
12521
+ const tocLinkCount = features.tocLinkCount ?? 0;
12522
+ if (sectionCount > 0 && sectionCount !== tocLinkCount) {
12523
+ pushMessage(context.messages, {
12524
+ id: MessageId.NAV_004,
12525
+ message: "The Navigation Document should contain the full hierarchy of headings in the document for EDUPUB.",
12526
+ location: { path: opfPath }
12527
+ });
12528
+ }
12363
12529
  if (features.hasPageBreak && !features.hasPageList) {
12364
12530
  pushMessage(context.messages, {
12365
12531
  id: MessageId.NAV_003,
@@ -12700,7 +12866,14 @@ var EpubCheck = class _EpubCheck {
12700
12866
  message: "For maximum compatibility, use only lowercase characters for the EPUB file extension.",
12701
12867
  location: { path: filename }
12702
12868
  });
12869
+ return;
12703
12870
  }
12871
+ const isEpub2 = context.version.startsWith("2");
12872
+ pushMessage(context.messages, {
12873
+ id: isEpub2 ? MessageId.PKG_017 : MessageId.PKG_024,
12874
+ message: `EPUB file has an uncommon extension "${extension}".`,
12875
+ location: { path: filename }
12876
+ });
12704
12877
  }
12705
12878
  /**
12706
12879
  * Build a filtered report from validation context