@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/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
@@ -1,8 +1,23 @@
1
- import { XmlDocument, XmlElement } from 'libxml2-wasm';
2
1
  import { parse, walk } from 'css-tree';
3
2
  import { unzipSync, strFromU8, gunzipSync } from 'fflate';
4
3
 
5
- // src/content/validator.ts
4
+ // src/util/xml-engine.ts
5
+ var engine;
6
+ async function loadXmlEngine() {
7
+ engine ??= await import('libxml2-wasm');
8
+ }
9
+ function getXmlDocument() {
10
+ if (!engine) {
11
+ throw new Error("libxml2-wasm not initialized \u2014 call loadXmlEngine() first");
12
+ }
13
+ return engine.XmlDocument;
14
+ }
15
+ function getXmlElement() {
16
+ if (!engine) {
17
+ throw new Error("libxml2-wasm not initialized \u2014 call loadXmlEngine() first");
18
+ }
19
+ return engine.XmlElement;
20
+ }
6
21
 
7
22
  // src/messages/messages.ts
8
23
  var severityOverrides = /* @__PURE__ */ new Map();
@@ -2691,6 +2706,31 @@ var PROFILE_DC_TYPE = {
2691
2706
  dict: "dictionary",
2692
2707
  preview: "preview"
2693
2708
  };
2709
+ var TYPE_TO_PROFILE = {
2710
+ dictionary: "dict",
2711
+ edupub: "edupub",
2712
+ index: "idx",
2713
+ preview: "preview"
2714
+ };
2715
+ var RESERVED_PREFIX_URIS = {
2716
+ dcterms: "http://purl.org/dc/terms/",
2717
+ marc: "http://id.loc.gov/vocabulary/",
2718
+ media: "http://www.idpf.org/epub/vocab/overlays/#",
2719
+ onix: "http://www.editeur.org/ONIX/book/codelists/current.html#",
2720
+ rendition: "http://www.idpf.org/vocab/rendition/#",
2721
+ schema: "http://schema.org/",
2722
+ xsd: "http://www.w3.org/2001/XMLSchema#",
2723
+ a11y: "http://www.idpf.org/epub/vocab/package/a11y/#"
2724
+ };
2725
+ function isValidURI(uri) {
2726
+ if (!uri) return false;
2727
+ try {
2728
+ new URL(uri);
2729
+ return true;
2730
+ } catch {
2731
+ return false;
2732
+ }
2733
+ }
2694
2734
  var DICTIONARY_TYPE_VALUES = /* @__PURE__ */ new Set([
2695
2735
  "monolingual",
2696
2736
  "bilingual",
@@ -2798,11 +2838,13 @@ var OPFValidator = class {
2798
2838
  this.validatePackageAttributes(context, opfPath);
2799
2839
  this.validateMetadata(context, opfPath);
2800
2840
  if (this.packageDoc.version !== "2.0") {
2841
+ this.validatePrefixDeclarations(context, opfPath, opfXml);
2801
2842
  this.validateMetaPrefixes(context, opfPath, opfXml);
2802
2843
  }
2803
2844
  this.validateLinkElements(context, opfPath);
2804
2845
  this.validateManifest(context, opfPath);
2805
2846
  this.validateSpine(context, opfPath);
2847
+ this.validatePageMap(context, opfPath, opfXml);
2806
2848
  this.validateFallbackChains(context, opfPath);
2807
2849
  this.validateUndeclaredResources(context, opfPath);
2808
2850
  if (this.packageDoc.version === "2.0") {
@@ -2833,6 +2875,7 @@ var OPFValidator = class {
2833
2875
  if (this.packageDoc.version.startsWith("3.")) {
2834
2876
  this.validateAccessibilityMetadata(context, opfPath);
2835
2877
  this.validateProfileDcType(context, opfPath);
2878
+ this.validateDcTypeProfileSwitch(context, opfPath);
2836
2879
  this.validateEdupubMetadata(context, opfPath);
2837
2880
  this.validateDictionaryMetadata(context, opfPath);
2838
2881
  this.validatePreviewMetadata(context, opfPath);
@@ -3036,6 +3079,22 @@ var OPFValidator = class {
3036
3079
  });
3037
3080
  }
3038
3081
  }
3082
+ // Mirrors Java's EPUBProfile.makeTypeCompatible flow.
3083
+ validateDcTypeProfileSwitch(context, opfPath) {
3084
+ if (!this.packageDoc) return;
3085
+ for (const dc of this.packageDoc.dcElements) {
3086
+ if (dc.name !== "type") continue;
3087
+ const inferred = TYPE_TO_PROFILE[dc.value.trim().toLowerCase()];
3088
+ if (inferred && inferred !== context.options.profile) {
3089
+ pushMessage(context.messages, {
3090
+ id: MessageId.OPF_064,
3091
+ message: `OPF declares type "${dc.value.trim().toLowerCase()}"; consider validating using the "${inferred}" profile.`,
3092
+ location: { path: opfPath }
3093
+ });
3094
+ return;
3095
+ }
3096
+ }
3097
+ }
3039
3098
  /**
3040
3099
  * Build lookup maps for manifest items
3041
3100
  */
@@ -3052,6 +3111,13 @@ var OPFValidator = class {
3052
3111
  */
3053
3112
  validatePackageAttributes(context, opfPath) {
3054
3113
  if (!this.packageDoc) return;
3114
+ if (this.packageDoc.isLegacyOebps12) {
3115
+ pushMessage(context.messages, {
3116
+ id: MessageId.OPF_047,
3117
+ message: "OPF file is using OEBPS 1.2 syntax allowing backwards compatibility.",
3118
+ location: { path: opfPath }
3119
+ });
3120
+ }
3055
3121
  if (this.packageDoc.versionDeclared === false) {
3056
3122
  pushMessage(context.messages, {
3057
3123
  id: MessageId.OPF_001,
@@ -3976,14 +4042,24 @@ var OPFValidator = class {
3976
4042
  const resolvedPath = resolvePath(opfPath, basePathNoQuery);
3977
4043
  const resolvedPathDecoded = basePathDecodedNoQuery !== basePathNoQuery ? resolvePath(opfPath, basePathDecodedNoQuery) : resolvedPath;
3978
4044
  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) {
4045
+ const manifestItem = this.manifestByHref.get(basePathNoQuery) ?? this.manifestByHref.get(basePathDecodedNoQuery);
4046
+ if (!fileExists && !manifestItem) {
3981
4047
  pushMessage(context.messages, {
3982
4048
  id: MessageId.RSC_007w,
3983
4049
  message: `Referenced resource "${resolvedPath}" could not be found in the EPUB`,
3984
4050
  location: { path: opfPath }
3985
4051
  });
3986
4052
  }
4053
+ if (manifestItem) {
4054
+ const inSpine = this.packageDoc.spine.some((ref) => ref.idref === manifestItem.id);
4055
+ if (!inSpine) {
4056
+ pushMessage(context.messages, {
4057
+ id: MessageId.OPF_067,
4058
+ message: `Resource "${manifestItem.href}" is referenced as a link but is also declared as a manifest item.`,
4059
+ location: { path: opfPath }
4060
+ });
4061
+ }
4062
+ }
3987
4063
  }
3988
4064
  }
3989
4065
  /**
@@ -4265,6 +4341,56 @@ var OPFValidator = class {
4265
4341
  }
4266
4342
  }
4267
4343
  }
4344
+ // Mirrors Java's PrefixDeclarationParser + VocabUtil.parsePrefixDeclaration,
4345
+ // but emits only the four main IDs (not Java's OPF-004a..f sub-codes).
4346
+ validatePrefixDeclarations(context, opfPath, opfXml) {
4347
+ const stripped = stripXmlComments(opfXml);
4348
+ const match = /<package[^>]*\sprefix\s*=\s*["']([^"']*)["']/.exec(stripped);
4349
+ if (!match) return;
4350
+ const raw = match[1] ?? "";
4351
+ if (raw !== raw.trim()) {
4352
+ pushMessage(context.messages, {
4353
+ id: MessageId.OPF_004,
4354
+ message: "The value of the prefix attribute has leading or trailing whitespace.",
4355
+ location: { path: opfPath }
4356
+ });
4357
+ }
4358
+ const parts = raw.trim().split(/\s+/).filter(Boolean);
4359
+ for (let i = 0; i < parts.length; ) {
4360
+ const token = parts[i] ?? "";
4361
+ if (token.endsWith(":") && token.length > 1) {
4362
+ const prefix = token.slice(0, -1);
4363
+ const uri = parts[i + 1];
4364
+ if (!uri || uri.endsWith(":")) {
4365
+ pushMessage(context.messages, {
4366
+ id: MessageId.OPF_005,
4367
+ message: `The prefix "${prefix}" is declared but no URI is bound to it.`,
4368
+ location: { path: opfPath }
4369
+ });
4370
+ i += 1;
4371
+ continue;
4372
+ }
4373
+ if (!isValidURI(uri)) {
4374
+ pushMessage(context.messages, {
4375
+ id: MessageId.OPF_006,
4376
+ message: `The value "${uri}" bound to prefix "${prefix}" is not a valid URI.`,
4377
+ location: { path: opfPath }
4378
+ });
4379
+ }
4380
+ const reservedUri = RESERVED_PREFIX_URIS[prefix];
4381
+ if (reservedUri !== void 0 && reservedUri !== uri) {
4382
+ pushMessage(context.messages, {
4383
+ id: MessageId.OPF_007,
4384
+ message: `The prefix "${prefix}" is reserved and must not be re-declared.`,
4385
+ location: { path: opfPath }
4386
+ });
4387
+ }
4388
+ i += 2;
4389
+ } else {
4390
+ i += 1;
4391
+ }
4392
+ }
4393
+ }
4268
4394
  /**
4269
4395
  * RSC-005: all id attributes on elements in the OPF document must be unique.
4270
4396
  * Mirrors Java's id-unique.sch / opf.sch opf_idAttrUnique pattern, which
@@ -4273,16 +4399,7 @@ var OPFValidator = class {
4273
4399
  */
4274
4400
  validateMetaPrefixes(context, opfPath, opfXml) {
4275
4401
  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
- ]);
4402
+ const RESERVED = new Set(Object.keys(RESERVED_PREFIX_URIS));
4286
4403
  const declared = new Set(Object.keys(this.packageDoc.prefixes ?? {}));
4287
4404
  const reported = /* @__PURE__ */ new Set();
4288
4405
  const reportIfUndeclared = (prefix) => {
@@ -4489,6 +4606,26 @@ var OPFValidator = class {
4489
4606
  }
4490
4607
  }
4491
4608
  }
4609
+ validatePageMap(context, opfPath, opfXml) {
4610
+ if (!this.packageDoc) return;
4611
+ const stripped = stripXmlComments(opfXml);
4612
+ const m = /<spine\b[^>]*\spage-map\s*=\s*["']([^"']*)["']/.exec(stripped);
4613
+ if (!m) return;
4614
+ const pageMapId = (m[1] ?? "").trim();
4615
+ pushMessage(context.messages, {
4616
+ id: MessageId.OPF_062,
4617
+ message: `Found Adobe page-map attribute on spine element (page-map="${pageMapId}")`,
4618
+ location: { path: opfPath }
4619
+ });
4620
+ if (!pageMapId) return;
4621
+ if (!this.manifestById.has(pageMapId)) {
4622
+ pushMessage(context.messages, {
4623
+ id: MessageId.OPF_063,
4624
+ message: `The Adobe page-map item "${pageMapId}" was not found in the manifest`,
4625
+ location: { path: opfPath }
4626
+ });
4627
+ }
4628
+ }
4492
4629
  /**
4493
4630
  * Validate fallback chains
4494
4631
  */
@@ -4960,7 +5097,7 @@ var SKMValidator = class {
4960
5097
  const content = typeof data === "string" ? data : new TextDecoder().decode(data);
4961
5098
  let doc = null;
4962
5099
  try {
4963
- doc = XmlDocument.fromString(content);
5100
+ doc = getXmlDocument().fromString(content);
4964
5101
  } catch {
4965
5102
  pushMessage(context.messages, {
4966
5103
  id: MessageId.RSC_016,
@@ -5197,7 +5334,7 @@ var SMILValidator = class {
5197
5334
  const content = typeof data === "string" ? data : new TextDecoder().decode(data);
5198
5335
  let doc = null;
5199
5336
  try {
5200
- doc = XmlDocument.fromString(content);
5337
+ doc = getXmlDocument().fromString(content);
5201
5338
  } catch {
5202
5339
  pushMessage(context.messages, {
5203
5340
  id: MessageId.RSC_016,
@@ -6107,7 +6244,7 @@ var ContentValidator = class {
6107
6244
  const svgContent = new TextDecoder().decode(svgData);
6108
6245
  let doc;
6109
6246
  try {
6110
- doc = XmlDocument.fromString(svgContent);
6247
+ doc = getXmlDocument().fromString(svgContent);
6111
6248
  this.extractAndRegisterIDs(path, doc.root, registry);
6112
6249
  } catch (e) {
6113
6250
  pushMessage(context.messages, {
@@ -6125,7 +6262,7 @@ var ContentValidator = class {
6125
6262
  const svgContent = new TextDecoder().decode(svgData);
6126
6263
  let doc;
6127
6264
  try {
6128
- doc = XmlDocument.fromString(svgContent);
6265
+ doc = getXmlDocument().fromString(svgContent);
6129
6266
  } catch {
6130
6267
  return;
6131
6268
  }
@@ -6171,7 +6308,7 @@ var ContentValidator = class {
6171
6308
  const svgContent = new TextDecoder().decode(svgData);
6172
6309
  let doc;
6173
6310
  try {
6174
- doc = XmlDocument.fromString(svgContent);
6311
+ doc = getXmlDocument().fromString(svgContent);
6175
6312
  } catch {
6176
6313
  return;
6177
6314
  }
@@ -6521,7 +6658,7 @@ var ContentValidator = class {
6521
6658
  }
6522
6659
  let doc = null;
6523
6660
  try {
6524
- doc = XmlDocument.fromString(content);
6661
+ doc = getXmlDocument().fromString(content);
6525
6662
  } catch (error) {
6526
6663
  if (error instanceof Error) {
6527
6664
  const { message, line, column } = this.parseLibxmlError(error.message);
@@ -7212,6 +7349,9 @@ var ContentValidator = class {
7212
7349
  const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
7213
7350
  const opfDir = context.opfPath?.includes("/") ? context.opfPath.substring(0, context.opfPath.lastIndexOf("/")) : "";
7214
7351
  const tocAnchors = tocNav.find(".//html:a[@href]", HTML_NS);
7352
+ if (context.contentFeatures) {
7353
+ context.contentFeatures.tocLinkCount = (context.contentFeatures.tocLinkCount ?? 0) + tocAnchors.length;
7354
+ }
7215
7355
  const tocLinks = [];
7216
7356
  for (const anchor of tocAnchors) {
7217
7357
  const href = this.getAttribute(anchor, "href")?.trim();
@@ -8334,6 +8474,10 @@ var ContentValidator = class {
8334
8474
  if (!features.hasRDFa && root.get(".//*[@property]")) {
8335
8475
  features.hasRDFa = true;
8336
8476
  }
8477
+ if (context.options.profile === "edupub") {
8478
+ const sections = root.find(".//html:body//html:section", XHTML_NS);
8479
+ features.sectionCount = (features.sectionCount ?? 0) + sections.length;
8480
+ }
8337
8481
  }
8338
8482
  validateImages(context, path, root) {
8339
8483
  const packageDoc = context.packageDocument;
@@ -8599,6 +8743,7 @@ var ContentValidator = class {
8599
8743
  }
8600
8744
  return Number.parseInt(el.name.substring(1), 10);
8601
8745
  };
8746
+ const XmlElement = getXmlElement();
8602
8747
  const directElementChildren = (parent) => {
8603
8748
  const out = [];
8604
8749
  let n = parent.firstChild;
@@ -10207,11 +10352,13 @@ function toJSONReport(result) {
10207
10352
  2
10208
10353
  );
10209
10354
  }
10355
+
10356
+ // src/ncx/validator.ts
10210
10357
  var NCXValidator = class {
10211
10358
  validate(context, ncxContent, ncxPath, registry) {
10212
10359
  let doc = null;
10213
10360
  try {
10214
- doc = XmlDocument.fromString(ncxContent);
10361
+ doc = getXmlDocument().fromString(ncxContent);
10215
10362
  } catch (error) {
10216
10363
  if (error instanceof Error) {
10217
10364
  pushMessage(context.messages, {
@@ -10269,6 +10416,13 @@ var NCXValidator = class {
10269
10416
  });
10270
10417
  return;
10271
10418
  }
10419
+ if (uidContent !== uidContent.trim()) {
10420
+ pushMessage(context.messages, {
10421
+ id: MessageId.NCX_004,
10422
+ message: "NCX dtb:uid meta content has leading or trailing whitespace.",
10423
+ location: { path, line: uidElement.line }
10424
+ });
10425
+ }
10272
10426
  context.ncxUid = uidContent.trim();
10273
10427
  }
10274
10428
  checkNavMap(context, root, path) {
@@ -10898,8 +11052,8 @@ var OCFValidator = class {
10898
11052
  zip = ZipReader.open(context.data);
10899
11053
  } catch (error) {
10900
11054
  pushMessage(context.messages, {
10901
- id: MessageId.PKG_001,
10902
- message: `Failed to open EPUB file: ${error instanceof Error ? error.message : "Unknown error"}`
11055
+ id: MessageId.PKG_004,
11056
+ message: `Failed to open EPUB ZIP: ${error instanceof Error ? error.message : "Unknown error"}`
10903
11057
  });
10904
11058
  return;
10905
11059
  }
@@ -10933,8 +11087,8 @@ var OCFValidator = class {
10933
11087
  const compressionInfo = zip.getMimetypeCompressionInfo();
10934
11088
  if (compressionInfo === null) {
10935
11089
  pushMessage(messages, {
10936
- id: MessageId.PKG_006,
10937
- message: "Could not read ZIP header",
11090
+ id: MessageId.PKG_003,
11091
+ message: "Unable to read EPUB file header, likely corrupted",
10938
11092
  location: { path: "mimetype" }
10939
11093
  });
10940
11094
  return;
@@ -11961,11 +12115,11 @@ var RelaxNGValidator = class extends BaseSchemaValidator {
11961
12115
  try {
11962
12116
  const libxml2 = await import('libxml2-wasm');
11963
12117
  const LibRelaxNGValidator = libxml2.RelaxNGValidator;
11964
- const { XmlDocument: XmlDocument5 } = libxml2;
11965
- const doc = XmlDocument5.fromString(xml);
12118
+ const { XmlDocument } = libxml2;
12119
+ const doc = XmlDocument.fromString(xml);
11966
12120
  try {
11967
12121
  const schemaContent = await loadSchema(schemaPath);
11968
- const schemaDoc = XmlDocument5.fromString(schemaContent);
12122
+ const schemaDoc = XmlDocument.fromString(schemaContent);
11969
12123
  try {
11970
12124
  const validator = LibRelaxNGValidator.fromDoc(schemaDoc);
11971
12125
  try {
@@ -12175,6 +12329,7 @@ var EpubCheck = class _EpubCheck {
12175
12329
  */
12176
12330
  async check(data, filename) {
12177
12331
  const startTime = performance.now();
12332
+ await loadXmlEngine();
12178
12333
  const context = {
12179
12334
  data,
12180
12335
  options: this.options,
@@ -12199,7 +12354,7 @@ var EpubCheck = class _EpubCheck {
12199
12354
  await this.runPipeline(context);
12200
12355
  } catch (error) {
12201
12356
  pushMessage(context.messages, {
12202
- id: MessageId.PKG_025,
12357
+ id: MessageId.PKG_008,
12203
12358
  message: error instanceof Error ? error.message : "Unknown validation error"
12204
12359
  });
12205
12360
  } finally {
@@ -12216,6 +12371,7 @@ var EpubCheck = class _EpubCheck {
12216
12371
  */
12217
12372
  async checkExpanded(files) {
12218
12373
  const startTime = performance.now();
12374
+ await loadXmlEngine();
12219
12375
  const context = {
12220
12376
  data: new Uint8Array(0),
12221
12377
  options: this.options,
@@ -12241,7 +12397,7 @@ var EpubCheck = class _EpubCheck {
12241
12397
  await this.runPipeline(context);
12242
12398
  } catch (error) {
12243
12399
  pushMessage(context.messages, {
12244
- id: MessageId.PKG_025,
12400
+ id: MessageId.PKG_008,
12245
12401
  message: error instanceof Error ? error.message : "Unknown validation error"
12246
12402
  });
12247
12403
  } finally {
@@ -12260,6 +12416,7 @@ var EpubCheck = class _EpubCheck {
12260
12416
  async checkSingleFile(data, filename) {
12261
12417
  const startTime = performance.now();
12262
12418
  const mode = this.options.mode;
12419
+ await loadXmlEngine();
12263
12420
  const context = {
12264
12421
  data: new Uint8Array(0),
12265
12422
  options: this.options,
@@ -12308,7 +12465,7 @@ var EpubCheck = class _EpubCheck {
12308
12465
  }
12309
12466
  } catch (error) {
12310
12467
  pushMessage(context.messages, {
12311
- id: MessageId.PKG_025,
12468
+ id: MessageId.PKG_008,
12312
12469
  message: error instanceof Error ? error.message : "Unknown validation error"
12313
12470
  });
12314
12471
  } finally {
@@ -12358,6 +12515,15 @@ var EpubCheck = class _EpubCheck {
12358
12515
  const profile = context.options.profile;
12359
12516
  const opfPath = context.opfPath ?? "";
12360
12517
  if (profile === "edupub") {
12518
+ const sectionCount = features.sectionCount ?? 0;
12519
+ const tocLinkCount = features.tocLinkCount ?? 0;
12520
+ if (sectionCount > 0 && sectionCount !== tocLinkCount) {
12521
+ pushMessage(context.messages, {
12522
+ id: MessageId.NAV_004,
12523
+ message: "The Navigation Document should contain the full hierarchy of headings in the document for EDUPUB.",
12524
+ location: { path: opfPath }
12525
+ });
12526
+ }
12361
12527
  if (features.hasPageBreak && !features.hasPageList) {
12362
12528
  pushMessage(context.messages, {
12363
12529
  id: MessageId.NAV_003,
@@ -12698,7 +12864,14 @@ var EpubCheck = class _EpubCheck {
12698
12864
  message: "For maximum compatibility, use only lowercase characters for the EPUB file extension.",
12699
12865
  location: { path: filename }
12700
12866
  });
12867
+ return;
12701
12868
  }
12869
+ const isEpub2 = context.version.startsWith("2");
12870
+ pushMessage(context.messages, {
12871
+ id: isEpub2 ? MessageId.PKG_017 : MessageId.PKG_024,
12872
+ message: `EPUB file has an uncommon extension "${extension}".`,
12873
+ location: { path: filename }
12874
+ });
12702
12875
  }
12703
12876
  /**
12704
12877
  * Build a filtered report from validation context