@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 +8 -3
- package/bin/epubcheck.js +15 -2
- package/bin/epubcheck.ts +15 -2
- package/dist/index.cjs +206 -33
- 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 +204 -31
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
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 } =
|
|
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.
|
|
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.
|
|
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/
|
|
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
|
|
3982
|
-
if (!fileExists && !
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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.
|
|
10904
|
-
message: `Failed to open EPUB
|
|
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.
|
|
10939
|
-
message: "
|
|
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
|
|
11967
|
-
const doc =
|
|
12120
|
+
const { XmlDocument } = libxml2;
|
|
12121
|
+
const doc = XmlDocument.fromString(xml);
|
|
11968
12122
|
try {
|
|
11969
12123
|
const schemaContent = await loadSchema(schemaPath);
|
|
11970
|
-
const schemaDoc =
|
|
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.
|
|
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.
|
|
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.
|
|
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
|