@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 +15 -2
- package/bin/epubcheck.ts +15 -2
- package/dist/index.cjs +171 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +171 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
package/dist/index.d.ts
CHANGED
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
|
|
3980
|
-
if (!fileExists && !
|
|
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 =
|
|
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.
|
|
10902
|
-
message: `Failed to open EPUB
|
|
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.
|
|
10937
|
-
message: "
|
|
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.
|
|
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.
|
|
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.
|
|
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
|