@likecoin/epubcheck-ts 0.5.1 → 0.6.0
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 +3 -3
- package/bin/epubcheck.js +1 -1
- package/bin/epubcheck.ts +1 -1
- package/dist/index.cjs +747 -225
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +747 -225
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1668,144 +1668,6 @@ var CSSValidator = class {
|
|
|
1668
1668
|
}
|
|
1669
1669
|
};
|
|
1670
1670
|
|
|
1671
|
-
// src/vocab/epub-ssv.ts
|
|
1672
|
-
var EPUB_SSV_DEPRECATED = /* @__PURE__ */ new Set([
|
|
1673
|
-
"annoref",
|
|
1674
|
-
"annotation",
|
|
1675
|
-
"biblioentry",
|
|
1676
|
-
"bridgehead",
|
|
1677
|
-
"endnote",
|
|
1678
|
-
"help",
|
|
1679
|
-
"marginalia",
|
|
1680
|
-
"note",
|
|
1681
|
-
"rearnote",
|
|
1682
|
-
"rearnotes",
|
|
1683
|
-
"sidebar",
|
|
1684
|
-
"subchapter",
|
|
1685
|
-
"warning"
|
|
1686
|
-
]);
|
|
1687
|
-
var EPUB_SSV_DISALLOWED_ON_CONTENT = /* @__PURE__ */ new Set([
|
|
1688
|
-
"aside",
|
|
1689
|
-
"figure",
|
|
1690
|
-
"list",
|
|
1691
|
-
"list-item",
|
|
1692
|
-
"table",
|
|
1693
|
-
"table-cell",
|
|
1694
|
-
"table-row"
|
|
1695
|
-
]);
|
|
1696
|
-
var EPUB_SSV_ALL = /* @__PURE__ */ new Set([
|
|
1697
|
-
...EPUB_SSV_DEPRECATED,
|
|
1698
|
-
...EPUB_SSV_DISALLOWED_ON_CONTENT,
|
|
1699
|
-
"abstract",
|
|
1700
|
-
"acknowledgments",
|
|
1701
|
-
"afterword",
|
|
1702
|
-
"appendix",
|
|
1703
|
-
"assessment",
|
|
1704
|
-
"assessments",
|
|
1705
|
-
"backlink",
|
|
1706
|
-
"backmatter",
|
|
1707
|
-
"balloon",
|
|
1708
|
-
"bibliography",
|
|
1709
|
-
"biblioref",
|
|
1710
|
-
"bodymatter",
|
|
1711
|
-
"case-study",
|
|
1712
|
-
"chapter",
|
|
1713
|
-
"colophon",
|
|
1714
|
-
"concluding-sentence",
|
|
1715
|
-
"conclusion",
|
|
1716
|
-
"contributors",
|
|
1717
|
-
"copyright-page",
|
|
1718
|
-
"cover",
|
|
1719
|
-
"covertitle",
|
|
1720
|
-
"credit",
|
|
1721
|
-
"credits",
|
|
1722
|
-
"dedication",
|
|
1723
|
-
"division",
|
|
1724
|
-
"endnotes",
|
|
1725
|
-
"epigraph",
|
|
1726
|
-
"epilogue",
|
|
1727
|
-
"errata",
|
|
1728
|
-
"fill-in-the-blank-problem",
|
|
1729
|
-
"footnote",
|
|
1730
|
-
"footnotes",
|
|
1731
|
-
"foreword",
|
|
1732
|
-
"frontmatter",
|
|
1733
|
-
"fulltitle",
|
|
1734
|
-
"general-problem",
|
|
1735
|
-
"glossary",
|
|
1736
|
-
"glossdef",
|
|
1737
|
-
"glossref",
|
|
1738
|
-
"glossterm",
|
|
1739
|
-
"halftitle",
|
|
1740
|
-
"halftitlepage",
|
|
1741
|
-
"imprimatur",
|
|
1742
|
-
"imprint",
|
|
1743
|
-
"index",
|
|
1744
|
-
"index-editor-note",
|
|
1745
|
-
"index-entry",
|
|
1746
|
-
"index-entry-list",
|
|
1747
|
-
"index-group",
|
|
1748
|
-
"index-headnotes",
|
|
1749
|
-
"index-legend",
|
|
1750
|
-
"index-locator",
|
|
1751
|
-
"index-locator-list",
|
|
1752
|
-
"index-locator-range",
|
|
1753
|
-
"index-term",
|
|
1754
|
-
"index-term-categories",
|
|
1755
|
-
"index-term-category",
|
|
1756
|
-
"index-xref-preferred",
|
|
1757
|
-
"index-xref-related",
|
|
1758
|
-
"introduction",
|
|
1759
|
-
"keyword",
|
|
1760
|
-
"keywords",
|
|
1761
|
-
"label",
|
|
1762
|
-
"landmarks",
|
|
1763
|
-
"learning-objective",
|
|
1764
|
-
"learning-objectives",
|
|
1765
|
-
"learning-outcome",
|
|
1766
|
-
"learning-outcomes",
|
|
1767
|
-
"learning-resource",
|
|
1768
|
-
"learning-resources",
|
|
1769
|
-
"learning-standard",
|
|
1770
|
-
"learning-standards",
|
|
1771
|
-
"loa",
|
|
1772
|
-
"loi",
|
|
1773
|
-
"lot",
|
|
1774
|
-
"lov",
|
|
1775
|
-
"match-problem",
|
|
1776
|
-
"multiple-choice-problem",
|
|
1777
|
-
"noteref",
|
|
1778
|
-
"notice",
|
|
1779
|
-
"ordinal",
|
|
1780
|
-
"other-credits",
|
|
1781
|
-
"page-list",
|
|
1782
|
-
"pagebreak",
|
|
1783
|
-
"panel",
|
|
1784
|
-
"panel-group",
|
|
1785
|
-
"part",
|
|
1786
|
-
"practice",
|
|
1787
|
-
"practices",
|
|
1788
|
-
"preamble",
|
|
1789
|
-
"preface",
|
|
1790
|
-
"prologue",
|
|
1791
|
-
"pullquote",
|
|
1792
|
-
"qna",
|
|
1793
|
-
"question",
|
|
1794
|
-
"referrer",
|
|
1795
|
-
"revision-history",
|
|
1796
|
-
"seriespage",
|
|
1797
|
-
"sound-area",
|
|
1798
|
-
"subtitle",
|
|
1799
|
-
"tip",
|
|
1800
|
-
"title",
|
|
1801
|
-
"titlepage",
|
|
1802
|
-
"toc",
|
|
1803
|
-
"toc-brief",
|
|
1804
|
-
"topic-sentence",
|
|
1805
|
-
"true-false-problem",
|
|
1806
|
-
"volume"
|
|
1807
|
-
]);
|
|
1808
|
-
|
|
1809
1671
|
// src/references/url.ts
|
|
1810
1672
|
function parseURL(urlString) {
|
|
1811
1673
|
const hashIndex = urlString.indexOf("#");
|
|
@@ -1834,12 +1696,13 @@ function isDataURL(url) {
|
|
|
1834
1696
|
function isFileURL(url) {
|
|
1835
1697
|
return url.startsWith("file:");
|
|
1836
1698
|
}
|
|
1699
|
+
function isRelativeURL(url) {
|
|
1700
|
+
const regex = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
|
|
1701
|
+
return regex.exec(url) === null;
|
|
1702
|
+
}
|
|
1837
1703
|
function hasAbsolutePath(url) {
|
|
1838
1704
|
return url.startsWith("/");
|
|
1839
1705
|
}
|
|
1840
|
-
function hasParentDirectoryReference(url) {
|
|
1841
|
-
return url.includes("..");
|
|
1842
|
-
}
|
|
1843
1706
|
function isMalformedURL(url) {
|
|
1844
1707
|
if (!url.trim()) return true;
|
|
1845
1708
|
if (/[\s<>]/.test(url)) return true;
|
|
@@ -1854,12 +1717,14 @@ function isHTTP(url) {
|
|
|
1854
1717
|
function isRemoteURL(url) {
|
|
1855
1718
|
return isHTTP(url) || isHTTPS(url);
|
|
1856
1719
|
}
|
|
1857
|
-
function checkUrlLeaking(href) {
|
|
1720
|
+
function checkUrlLeaking(href, resourcePath) {
|
|
1858
1721
|
const TEST_BASE_A = "https://a.example.org/A/";
|
|
1859
1722
|
const TEST_BASE_B = "https://b.example.org/B/";
|
|
1860
1723
|
try {
|
|
1861
|
-
const
|
|
1862
|
-
const
|
|
1724
|
+
const baseA = resourcePath ? new URL(resourcePath, TEST_BASE_A).toString() : TEST_BASE_A;
|
|
1725
|
+
const baseB = resourcePath ? new URL(resourcePath, TEST_BASE_B).toString() : TEST_BASE_B;
|
|
1726
|
+
const urlA = new URL(href, baseA).toString();
|
|
1727
|
+
const urlB = new URL(href, baseB).toString();
|
|
1863
1728
|
return !urlA.startsWith(TEST_BASE_A) || !urlB.startsWith(TEST_BASE_B);
|
|
1864
1729
|
} catch {
|
|
1865
1730
|
return false;
|
|
@@ -2031,6 +1896,8 @@ function parseOPF(xml) {
|
|
|
2031
1896
|
}
|
|
2032
1897
|
}
|
|
2033
1898
|
}
|
|
1899
|
+
const nsRegex = /<package[^>]*\sxmlns=["']([^"']+)["']/;
|
|
1900
|
+
const isLegacyOebps12 = nsRegex.exec(xml)?.[1] === "http://openebook.org/namespaces/oeb-package/1.0/";
|
|
2034
1901
|
const prefixes = parsePrefixes(xml);
|
|
2035
1902
|
const dirRegex = /<package[^>]*\sdir=["']([^"']+)["']/;
|
|
2036
1903
|
const dirMatch = dirRegex.exec(xml);
|
|
@@ -2069,6 +1936,9 @@ function parseOPF(xml) {
|
|
|
2069
1936
|
if (!versionDeclared) {
|
|
2070
1937
|
result.versionDeclared = false;
|
|
2071
1938
|
}
|
|
1939
|
+
if (isLegacyOebps12) {
|
|
1940
|
+
result.isLegacyOebps12 = true;
|
|
1941
|
+
}
|
|
2072
1942
|
if (Object.keys(prefixes).length > 0) {
|
|
2073
1943
|
result.prefixes = prefixes;
|
|
2074
1944
|
}
|
|
@@ -2159,7 +2029,7 @@ function parseDCElements(metadataXml) {
|
|
|
2159
2029
|
}
|
|
2160
2030
|
function parseMetaElements(metadataXml) {
|
|
2161
2031
|
const elements = [];
|
|
2162
|
-
const metaRegex = /<meta([^>]*property=["'][^"']+["'][^>]*)>([^<]*)<\/meta>/g;
|
|
2032
|
+
const metaRegex = /<(?:[A-Za-z_][\w.-]*:)?meta([^>]*property=["'][^"']+["'][^>]*)>([^<]*)<\/(?:[A-Za-z_][\w.-]*:)?meta>/g;
|
|
2163
2033
|
let match;
|
|
2164
2034
|
while ((match = metaRegex.exec(metadataXml)) !== null) {
|
|
2165
2035
|
const attrsStr = match[1] ?? "";
|
|
@@ -2929,6 +2799,9 @@ var OPFValidator = class {
|
|
|
2929
2799
|
}
|
|
2930
2800
|
this.validatePackageAttributes(context, opfPath);
|
|
2931
2801
|
this.validateMetadata(context, opfPath);
|
|
2802
|
+
if (this.packageDoc.version !== "2.0") {
|
|
2803
|
+
this.validateMetaPrefixes(context, opfPath, opfXml);
|
|
2804
|
+
}
|
|
2932
2805
|
this.validateLinkElements(context, opfPath);
|
|
2933
2806
|
this.validateManifest(context, opfPath);
|
|
2934
2807
|
this.validateSpine(context, opfPath);
|
|
@@ -4122,6 +3995,7 @@ var OPFValidator = class {
|
|
|
4122
3995
|
if (!this.packageDoc) return;
|
|
4123
3996
|
const seenIds = /* @__PURE__ */ new Set();
|
|
4124
3997
|
const seenHrefs = /* @__PURE__ */ new Set();
|
|
3998
|
+
const declaredPrefixes = this.packageDoc.prefixes ?? {};
|
|
4125
3999
|
for (const item of this.packageDoc.manifest) {
|
|
4126
4000
|
if (seenIds.has(item.id)) {
|
|
4127
4001
|
pushMessage(context.messages, {
|
|
@@ -4165,7 +4039,7 @@ var OPFValidator = class {
|
|
|
4165
4039
|
});
|
|
4166
4040
|
}
|
|
4167
4041
|
if (!item.href.startsWith("http") && !item.href.startsWith("mailto:")) {
|
|
4168
|
-
const leaked = checkUrlLeaking(item.href);
|
|
4042
|
+
const leaked = checkUrlLeaking(item.href, opfPath);
|
|
4169
4043
|
if (leaked) {
|
|
4170
4044
|
pushMessage(context.messages, {
|
|
4171
4045
|
id: MessageId.RSC_026,
|
|
@@ -4200,11 +4074,11 @@ var OPFValidator = class {
|
|
|
4200
4074
|
if (DEPRECATED_MEDIA_TYPES.has(item.mediaType) || item.mediaType === "text/html") {
|
|
4201
4075
|
if (this.packageDoc.version === "2.0" && item.mediaType === "text/html") {
|
|
4202
4076
|
pushMessage(context.messages, {
|
|
4203
|
-
id: MessageId.OPF_035,
|
|
4077
|
+
id: this.packageDoc.isLegacyOebps12 ? MessageId.OPF_038 : MessageId.OPF_035,
|
|
4204
4078
|
message: `XHTML Content Document "${item.id}" is declared as "text/html"`,
|
|
4205
4079
|
location: { path: opfPath }
|
|
4206
4080
|
});
|
|
4207
|
-
} else if (this.packageDoc.version === "2.0") {
|
|
4081
|
+
} else if (this.packageDoc.version === "2.0" && !this.packageDoc.isLegacyOebps12) {
|
|
4208
4082
|
pushMessage(context.messages, {
|
|
4209
4083
|
id: MessageId.OPF_037,
|
|
4210
4084
|
message: `Found deprecated media-type "${item.mediaType}"`,
|
|
@@ -4212,6 +4086,13 @@ var OPFValidator = class {
|
|
|
4212
4086
|
});
|
|
4213
4087
|
}
|
|
4214
4088
|
}
|
|
4089
|
+
if (this.packageDoc.version === "2.0" && this.packageDoc.isLegacyOebps12 && item.mediaType === "text/css" && !item.fallback) {
|
|
4090
|
+
pushMessage(context.messages, {
|
|
4091
|
+
id: MessageId.OPF_039,
|
|
4092
|
+
message: `Media type "${item.mediaType}" requires a fallback in legacy OEBPS 1.2 context`,
|
|
4093
|
+
location: { path: opfPath }
|
|
4094
|
+
});
|
|
4095
|
+
}
|
|
4215
4096
|
const preferred = getPreferredMediaType(item.mediaType, fullPath);
|
|
4216
4097
|
if (preferred !== null) {
|
|
4217
4098
|
pushMessage(context.messages, {
|
|
@@ -4222,13 +4103,14 @@ var OPFValidator = class {
|
|
|
4222
4103
|
}
|
|
4223
4104
|
if (this.packageDoc.version !== "2.0" && item.properties) {
|
|
4224
4105
|
for (const prop of item.properties) {
|
|
4225
|
-
if (
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
}
|
|
4231
|
-
|
|
4106
|
+
if (ITEM_PROPERTIES.has(prop)) continue;
|
|
4107
|
+
const colon = prop.indexOf(":");
|
|
4108
|
+
if (colon > 0 && declaredPrefixes[prop.slice(0, colon)] !== void 0) continue;
|
|
4109
|
+
pushMessage(context.messages, {
|
|
4110
|
+
id: MessageId.OPF_027,
|
|
4111
|
+
message: `Undefined property: "${prop}"`,
|
|
4112
|
+
location: { path: opfPath }
|
|
4113
|
+
});
|
|
4232
4114
|
}
|
|
4233
4115
|
if (item.properties.includes("nav")) {
|
|
4234
4116
|
if (item.mediaType !== "application/xhtml+xml") {
|
|
@@ -4391,6 +4273,42 @@ var OPFValidator = class {
|
|
|
4391
4273
|
* emits one assertion failure per offending element (so two duplicate ids
|
|
4392
4274
|
* produce two RSC-005 messages).
|
|
4393
4275
|
*/
|
|
4276
|
+
validateMetaPrefixes(context, opfPath, opfXml) {
|
|
4277
|
+
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
|
+
]);
|
|
4288
|
+
const declared = new Set(Object.keys(this.packageDoc.prefixes ?? {}));
|
|
4289
|
+
const reported = /* @__PURE__ */ new Set();
|
|
4290
|
+
const reportIfUndeclared = (prefix) => {
|
|
4291
|
+
if (!prefix || reported.has(prefix)) return;
|
|
4292
|
+
if (RESERVED.has(prefix) || declared.has(prefix)) return;
|
|
4293
|
+
reported.add(prefix);
|
|
4294
|
+
pushMessage(context.messages, {
|
|
4295
|
+
id: MessageId.OPF_028,
|
|
4296
|
+
message: `Undeclared prefix: "${prefix}"`,
|
|
4297
|
+
location: { path: opfPath }
|
|
4298
|
+
});
|
|
4299
|
+
};
|
|
4300
|
+
const stripped = stripXmlComments(opfXml);
|
|
4301
|
+
const attrRegex = /\b(?:property|scheme|rel)\s*=\s*["']([^"']+)["']/g;
|
|
4302
|
+
for (const match of stripped.matchAll(attrRegex)) {
|
|
4303
|
+
const value = match[1]?.trim();
|
|
4304
|
+
if (!value) continue;
|
|
4305
|
+
for (const token of value.split(/\s+/)) {
|
|
4306
|
+
const colon = token.indexOf(":");
|
|
4307
|
+
if (colon <= 0) continue;
|
|
4308
|
+
reportIfUndeclared(token.slice(0, colon));
|
|
4309
|
+
}
|
|
4310
|
+
}
|
|
4311
|
+
}
|
|
4394
4312
|
validateOpfIdUniqueness(context, opfPath, opfXml) {
|
|
4395
4313
|
const stripped = stripXmlComments(opfXml);
|
|
4396
4314
|
const counts = /* @__PURE__ */ new Map();
|
|
@@ -4422,11 +4340,12 @@ var OPFValidator = class {
|
|
|
4422
4340
|
for (const item of this.packageDoc.manifest) {
|
|
4423
4341
|
const hrefBase = item.href.split("?")[0] ?? item.href;
|
|
4424
4342
|
if (/^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(hrefBase)) continue;
|
|
4425
|
-
manifestPaths.add(resolvePath(opfPath, hrefBase).normalize("NFC"));
|
|
4343
|
+
manifestPaths.add(resolvePath(opfPath, tryDecodeUriComponent(hrefBase)).normalize("NFC"));
|
|
4426
4344
|
}
|
|
4427
4345
|
const rootfilePaths = new Set(context.rootfiles.map((r) => r.path.normalize("NFC")));
|
|
4428
4346
|
for (const path of context.files.keys()) {
|
|
4429
4347
|
if (path === "mimetype") continue;
|
|
4348
|
+
if (path.endsWith("/")) continue;
|
|
4430
4349
|
if (path.startsWith("META-INF/")) continue;
|
|
4431
4350
|
if (rootfilePaths.has(path)) continue;
|
|
4432
4351
|
if (manifestPaths.has(path)) continue;
|
|
@@ -5015,6 +4934,246 @@ function isValidW3CDateFormat(dateStr) {
|
|
|
5015
4934
|
return false;
|
|
5016
4935
|
}
|
|
5017
4936
|
|
|
4937
|
+
// src/references/types.ts
|
|
4938
|
+
var PUBLICATION_RESOURCE_TYPES = /* @__PURE__ */ new Set([
|
|
4939
|
+
"generic" /* GENERIC */,
|
|
4940
|
+
"stylesheet" /* STYLESHEET */,
|
|
4941
|
+
"font" /* FONT */,
|
|
4942
|
+
"image" /* IMAGE */,
|
|
4943
|
+
"audio" /* AUDIO */,
|
|
4944
|
+
"video" /* VIDEO */,
|
|
4945
|
+
"track" /* TRACK */,
|
|
4946
|
+
"media-overlay" /* MEDIA_OVERLAY */,
|
|
4947
|
+
"svg-symbol" /* SVG_SYMBOL */,
|
|
4948
|
+
"svg-paint" /* SVG_PAINT */,
|
|
4949
|
+
"svg-clip-path" /* SVG_CLIP_PATH */
|
|
4950
|
+
]);
|
|
4951
|
+
function isPublicationResourceReference(type) {
|
|
4952
|
+
return PUBLICATION_RESOURCE_TYPES.has(type);
|
|
4953
|
+
}
|
|
4954
|
+
|
|
4955
|
+
// src/skm/validator.ts
|
|
4956
|
+
var OPS_NS_URI = "http://www.idpf.org/2007/ops";
|
|
4957
|
+
var SKM_NS = { ops: OPS_NS_URI };
|
|
4958
|
+
var SKMValidator = class {
|
|
4959
|
+
validate(context, path, refValidator) {
|
|
4960
|
+
const data = context.files.get(path);
|
|
4961
|
+
if (!data) return;
|
|
4962
|
+
const content = typeof data === "string" ? data : new TextDecoder().decode(data);
|
|
4963
|
+
let doc = null;
|
|
4964
|
+
try {
|
|
4965
|
+
doc = libxml2Wasm.XmlDocument.fromString(content);
|
|
4966
|
+
} catch {
|
|
4967
|
+
pushMessage(context.messages, {
|
|
4968
|
+
id: MessageId.RSC_016,
|
|
4969
|
+
message: "Search Key Map document is not well-formed XML",
|
|
4970
|
+
location: { path }
|
|
4971
|
+
});
|
|
4972
|
+
return;
|
|
4973
|
+
}
|
|
4974
|
+
try {
|
|
4975
|
+
const root = doc.root;
|
|
4976
|
+
if (root.namespaceUri !== OPS_NS_URI || root.name !== "search-key-map") {
|
|
4977
|
+
pushMessage(context.messages, {
|
|
4978
|
+
id: MessageId.RSC_005,
|
|
4979
|
+
message: `Root element must be "search-key-map" in the OPS namespace`,
|
|
4980
|
+
location: { path, line: root.line }
|
|
4981
|
+
});
|
|
4982
|
+
return;
|
|
4983
|
+
}
|
|
4984
|
+
const groups = root.find("./ops:search-key-group", SKM_NS);
|
|
4985
|
+
if (groups.length === 0) {
|
|
4986
|
+
pushMessage(context.messages, {
|
|
4987
|
+
id: MessageId.RSC_005,
|
|
4988
|
+
message: 'A "search-key-map" must contain at least one "search-key-group"',
|
|
4989
|
+
location: { path, line: root.line }
|
|
4990
|
+
});
|
|
4991
|
+
}
|
|
4992
|
+
for (const group of groups) {
|
|
4993
|
+
const groupEl = group;
|
|
4994
|
+
const href = groupEl.attr("href")?.value;
|
|
4995
|
+
if (!href) {
|
|
4996
|
+
pushMessage(context.messages, {
|
|
4997
|
+
id: MessageId.RSC_005,
|
|
4998
|
+
message: 'The "href" attribute is required on "search-key-group"',
|
|
4999
|
+
location: { path, line: groupEl.line }
|
|
5000
|
+
});
|
|
5001
|
+
} else if (refValidator) {
|
|
5002
|
+
registerSkmRef(refValidator, path, href, groupEl.line);
|
|
5003
|
+
}
|
|
5004
|
+
const matches = groupEl.find("./ops:match", SKM_NS);
|
|
5005
|
+
if (matches.length === 0) {
|
|
5006
|
+
pushMessage(context.messages, {
|
|
5007
|
+
id: MessageId.RSC_005,
|
|
5008
|
+
message: 'A "search-key-group" must contain at least one "match"',
|
|
5009
|
+
location: { path, line: groupEl.line }
|
|
5010
|
+
});
|
|
5011
|
+
}
|
|
5012
|
+
for (const match of matches) {
|
|
5013
|
+
const matchEl = match;
|
|
5014
|
+
const matchHref = matchEl.attr("href")?.value;
|
|
5015
|
+
if (matchHref && refValidator) {
|
|
5016
|
+
registerSkmRef(refValidator, path, matchHref, matchEl.line);
|
|
5017
|
+
}
|
|
5018
|
+
}
|
|
5019
|
+
}
|
|
5020
|
+
} finally {
|
|
5021
|
+
doc.dispose();
|
|
5022
|
+
}
|
|
5023
|
+
}
|
|
5024
|
+
};
|
|
5025
|
+
function registerSkmRef(refValidator, path, href, line) {
|
|
5026
|
+
const parsed = parseURL(href);
|
|
5027
|
+
const targetResource = resolvePath(path, tryDecodeUriComponent(parsed.resource)).normalize("NFC");
|
|
5028
|
+
const location = line != null ? { path, line } : { path };
|
|
5029
|
+
const ref = {
|
|
5030
|
+
url: parsed.hasFragment ? `${targetResource}#${parsed.fragment ?? ""}` : targetResource,
|
|
5031
|
+
targetResource,
|
|
5032
|
+
type: "search-key" /* SEARCH_KEY */,
|
|
5033
|
+
location
|
|
5034
|
+
};
|
|
5035
|
+
if (parsed.fragment !== void 0) ref.fragment = parsed.fragment;
|
|
5036
|
+
refValidator.addReference(ref);
|
|
5037
|
+
}
|
|
5038
|
+
|
|
5039
|
+
// src/vocab/epub-ssv.ts
|
|
5040
|
+
var EPUB_SSV_DEPRECATED = /* @__PURE__ */ new Set([
|
|
5041
|
+
"annoref",
|
|
5042
|
+
"annotation",
|
|
5043
|
+
"biblioentry",
|
|
5044
|
+
"bridgehead",
|
|
5045
|
+
"endnote",
|
|
5046
|
+
"help",
|
|
5047
|
+
"marginalia",
|
|
5048
|
+
"note",
|
|
5049
|
+
"rearnote",
|
|
5050
|
+
"rearnotes",
|
|
5051
|
+
"sidebar",
|
|
5052
|
+
"subchapter",
|
|
5053
|
+
"warning"
|
|
5054
|
+
]);
|
|
5055
|
+
var EPUB_SSV_DISALLOWED_ON_CONTENT = /* @__PURE__ */ new Set([
|
|
5056
|
+
"aside",
|
|
5057
|
+
"figure",
|
|
5058
|
+
"list",
|
|
5059
|
+
"list-item",
|
|
5060
|
+
"table",
|
|
5061
|
+
"table-cell",
|
|
5062
|
+
"table-row"
|
|
5063
|
+
]);
|
|
5064
|
+
var EPUB_SSV_ALL = /* @__PURE__ */ new Set([
|
|
5065
|
+
...EPUB_SSV_DEPRECATED,
|
|
5066
|
+
...EPUB_SSV_DISALLOWED_ON_CONTENT,
|
|
5067
|
+
"abstract",
|
|
5068
|
+
"acknowledgments",
|
|
5069
|
+
"afterword",
|
|
5070
|
+
"appendix",
|
|
5071
|
+
"assessment",
|
|
5072
|
+
"assessments",
|
|
5073
|
+
"backlink",
|
|
5074
|
+
"backmatter",
|
|
5075
|
+
"balloon",
|
|
5076
|
+
"bibliography",
|
|
5077
|
+
"biblioref",
|
|
5078
|
+
"bodymatter",
|
|
5079
|
+
"case-study",
|
|
5080
|
+
"chapter",
|
|
5081
|
+
"colophon",
|
|
5082
|
+
"concluding-sentence",
|
|
5083
|
+
"conclusion",
|
|
5084
|
+
"contributors",
|
|
5085
|
+
"copyright-page",
|
|
5086
|
+
"cover",
|
|
5087
|
+
"covertitle",
|
|
5088
|
+
"credit",
|
|
5089
|
+
"credits",
|
|
5090
|
+
"dedication",
|
|
5091
|
+
"division",
|
|
5092
|
+
"endnotes",
|
|
5093
|
+
"epigraph",
|
|
5094
|
+
"epilogue",
|
|
5095
|
+
"errata",
|
|
5096
|
+
"fill-in-the-blank-problem",
|
|
5097
|
+
"footnote",
|
|
5098
|
+
"footnotes",
|
|
5099
|
+
"foreword",
|
|
5100
|
+
"frontmatter",
|
|
5101
|
+
"fulltitle",
|
|
5102
|
+
"general-problem",
|
|
5103
|
+
"glossary",
|
|
5104
|
+
"glossdef",
|
|
5105
|
+
"glossref",
|
|
5106
|
+
"glossterm",
|
|
5107
|
+
"halftitle",
|
|
5108
|
+
"halftitlepage",
|
|
5109
|
+
"imprimatur",
|
|
5110
|
+
"imprint",
|
|
5111
|
+
"index",
|
|
5112
|
+
"index-editor-note",
|
|
5113
|
+
"index-entry",
|
|
5114
|
+
"index-entry-list",
|
|
5115
|
+
"index-group",
|
|
5116
|
+
"index-headnotes",
|
|
5117
|
+
"index-legend",
|
|
5118
|
+
"index-locator",
|
|
5119
|
+
"index-locator-list",
|
|
5120
|
+
"index-locator-range",
|
|
5121
|
+
"index-term",
|
|
5122
|
+
"index-term-categories",
|
|
5123
|
+
"index-term-category",
|
|
5124
|
+
"index-xref-preferred",
|
|
5125
|
+
"index-xref-related",
|
|
5126
|
+
"introduction",
|
|
5127
|
+
"keyword",
|
|
5128
|
+
"keywords",
|
|
5129
|
+
"label",
|
|
5130
|
+
"landmarks",
|
|
5131
|
+
"learning-objective",
|
|
5132
|
+
"learning-objectives",
|
|
5133
|
+
"learning-outcome",
|
|
5134
|
+
"learning-outcomes",
|
|
5135
|
+
"learning-resource",
|
|
5136
|
+
"learning-resources",
|
|
5137
|
+
"learning-standard",
|
|
5138
|
+
"learning-standards",
|
|
5139
|
+
"loa",
|
|
5140
|
+
"loi",
|
|
5141
|
+
"lot",
|
|
5142
|
+
"lov",
|
|
5143
|
+
"match-problem",
|
|
5144
|
+
"multiple-choice-problem",
|
|
5145
|
+
"noteref",
|
|
5146
|
+
"notice",
|
|
5147
|
+
"ordinal",
|
|
5148
|
+
"other-credits",
|
|
5149
|
+
"page-list",
|
|
5150
|
+
"pagebreak",
|
|
5151
|
+
"panel",
|
|
5152
|
+
"panel-group",
|
|
5153
|
+
"part",
|
|
5154
|
+
"practice",
|
|
5155
|
+
"practices",
|
|
5156
|
+
"preamble",
|
|
5157
|
+
"preface",
|
|
5158
|
+
"prologue",
|
|
5159
|
+
"pullquote",
|
|
5160
|
+
"qna",
|
|
5161
|
+
"question",
|
|
5162
|
+
"referrer",
|
|
5163
|
+
"revision-history",
|
|
5164
|
+
"seriespage",
|
|
5165
|
+
"sound-area",
|
|
5166
|
+
"subtitle",
|
|
5167
|
+
"tip",
|
|
5168
|
+
"title",
|
|
5169
|
+
"titlepage",
|
|
5170
|
+
"toc",
|
|
5171
|
+
"toc-brief",
|
|
5172
|
+
"topic-sentence",
|
|
5173
|
+
"true-false-problem",
|
|
5174
|
+
"volume"
|
|
5175
|
+
]);
|
|
5176
|
+
|
|
5018
5177
|
// src/smil/validator.ts
|
|
5019
5178
|
var SMIL_NS = { smil: "http://www.w3.org/ns/SMIL" };
|
|
5020
5179
|
var BLESSED_AUDIO_TYPES = /* @__PURE__ */ new Set(["audio/mpeg", "audio/mp4"]);
|
|
@@ -5260,24 +5419,6 @@ var SMILValidator = class {
|
|
|
5260
5419
|
}
|
|
5261
5420
|
};
|
|
5262
5421
|
|
|
5263
|
-
// src/references/types.ts
|
|
5264
|
-
var PUBLICATION_RESOURCE_TYPES = /* @__PURE__ */ new Set([
|
|
5265
|
-
"generic" /* GENERIC */,
|
|
5266
|
-
"stylesheet" /* STYLESHEET */,
|
|
5267
|
-
"font" /* FONT */,
|
|
5268
|
-
"image" /* IMAGE */,
|
|
5269
|
-
"audio" /* AUDIO */,
|
|
5270
|
-
"video" /* VIDEO */,
|
|
5271
|
-
"track" /* TRACK */,
|
|
5272
|
-
"media-overlay" /* MEDIA_OVERLAY */,
|
|
5273
|
-
"svg-symbol" /* SVG_SYMBOL */,
|
|
5274
|
-
"svg-paint" /* SVG_PAINT */,
|
|
5275
|
-
"svg-clip-path" /* SVG_CLIP_PATH */
|
|
5276
|
-
]);
|
|
5277
|
-
function isPublicationResourceReference(type) {
|
|
5278
|
-
return PUBLICATION_RESOURCE_TYPES.has(type);
|
|
5279
|
-
}
|
|
5280
|
-
|
|
5281
5422
|
// src/references/uri-schemes.ts
|
|
5282
5423
|
var URI_SCHEMES = /* @__PURE__ */ new Set([
|
|
5283
5424
|
"aaa",
|
|
@@ -5367,7 +5508,9 @@ var ABSOLUTE_URI_RE = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
|
|
|
5367
5508
|
var SPECIAL_URL_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "ftp", "ws", "wss"]);
|
|
5368
5509
|
var CSS_CHARSET_RE = /^@charset\s+"([^"]+)"\s*;/;
|
|
5369
5510
|
var EPUB_XMLNS_RE = /xmlns:epub\s*=\s*"([^"]*)"/;
|
|
5370
|
-
var
|
|
5511
|
+
var XHTML_NS_URI = "http://www.w3.org/1999/xhtml";
|
|
5512
|
+
var XML_NS_URI = "http://www.w3.org/XML/1998/namespace";
|
|
5513
|
+
var XHTML_NS = { html: XHTML_NS_URI };
|
|
5371
5514
|
var EPUB_OPS_NS = { epub: "http://www.idpf.org/2007/ops" };
|
|
5372
5515
|
var EPUB_TYPE_FORBIDDEN_ELEMENTS = /* @__PURE__ */ new Set([
|
|
5373
5516
|
"head",
|
|
@@ -5651,6 +5794,112 @@ var HTML5_ELEMENTS = /* @__PURE__ */ new Set([
|
|
|
5651
5794
|
"video",
|
|
5652
5795
|
"wbr"
|
|
5653
5796
|
]);
|
|
5797
|
+
var XHTML11_ELEMENTS = /* @__PURE__ */ new Set([
|
|
5798
|
+
// struct
|
|
5799
|
+
"html",
|
|
5800
|
+
"head",
|
|
5801
|
+
"title",
|
|
5802
|
+
"body",
|
|
5803
|
+
"meta",
|
|
5804
|
+
"link",
|
|
5805
|
+
"base",
|
|
5806
|
+
"style",
|
|
5807
|
+
"script",
|
|
5808
|
+
"noscript",
|
|
5809
|
+
// text
|
|
5810
|
+
"br",
|
|
5811
|
+
"span",
|
|
5812
|
+
"abbr",
|
|
5813
|
+
"acronym",
|
|
5814
|
+
"cite",
|
|
5815
|
+
"code",
|
|
5816
|
+
"dfn",
|
|
5817
|
+
"em",
|
|
5818
|
+
"kbd",
|
|
5819
|
+
"q",
|
|
5820
|
+
"samp",
|
|
5821
|
+
"strong",
|
|
5822
|
+
"var",
|
|
5823
|
+
"div",
|
|
5824
|
+
"p",
|
|
5825
|
+
"address",
|
|
5826
|
+
"blockquote",
|
|
5827
|
+
"pre",
|
|
5828
|
+
"h1",
|
|
5829
|
+
"h2",
|
|
5830
|
+
"h3",
|
|
5831
|
+
"h4",
|
|
5832
|
+
"h5",
|
|
5833
|
+
"h6",
|
|
5834
|
+
// pres / legacy
|
|
5835
|
+
"hr",
|
|
5836
|
+
"b",
|
|
5837
|
+
"big",
|
|
5838
|
+
"i",
|
|
5839
|
+
"small",
|
|
5840
|
+
"sub",
|
|
5841
|
+
"sup",
|
|
5842
|
+
"tt",
|
|
5843
|
+
"basefont",
|
|
5844
|
+
"center",
|
|
5845
|
+
"font",
|
|
5846
|
+
"s",
|
|
5847
|
+
"strike",
|
|
5848
|
+
"u",
|
|
5849
|
+
"dir",
|
|
5850
|
+
"menu",
|
|
5851
|
+
"isindex",
|
|
5852
|
+
// list
|
|
5853
|
+
"dl",
|
|
5854
|
+
"dt",
|
|
5855
|
+
"dd",
|
|
5856
|
+
"ol",
|
|
5857
|
+
"ul",
|
|
5858
|
+
"li",
|
|
5859
|
+
// table
|
|
5860
|
+
"table",
|
|
5861
|
+
"caption",
|
|
5862
|
+
"tr",
|
|
5863
|
+
"th",
|
|
5864
|
+
"td",
|
|
5865
|
+
"col",
|
|
5866
|
+
"colgroup",
|
|
5867
|
+
"tbody",
|
|
5868
|
+
"thead",
|
|
5869
|
+
"tfoot",
|
|
5870
|
+
// hypertext / image / object / form / edit / ruby / map / iframe / applet / bdo / param
|
|
5871
|
+
"a",
|
|
5872
|
+
"img",
|
|
5873
|
+
"object",
|
|
5874
|
+
"param",
|
|
5875
|
+
"form",
|
|
5876
|
+
"label",
|
|
5877
|
+
"input",
|
|
5878
|
+
"select",
|
|
5879
|
+
"option",
|
|
5880
|
+
"optgroup",
|
|
5881
|
+
"fieldset",
|
|
5882
|
+
"button",
|
|
5883
|
+
"legend",
|
|
5884
|
+
"textarea",
|
|
5885
|
+
"ins",
|
|
5886
|
+
"del",
|
|
5887
|
+
"ruby",
|
|
5888
|
+
"rbc",
|
|
5889
|
+
"rtc",
|
|
5890
|
+
"rb",
|
|
5891
|
+
"rt",
|
|
5892
|
+
"rp",
|
|
5893
|
+
"map",
|
|
5894
|
+
"area",
|
|
5895
|
+
"iframe",
|
|
5896
|
+
"applet",
|
|
5897
|
+
"bdo",
|
|
5898
|
+
// frames
|
|
5899
|
+
"frameset",
|
|
5900
|
+
"frame",
|
|
5901
|
+
"noframes"
|
|
5902
|
+
]);
|
|
5654
5903
|
function isItemFixedLayout(packageDoc, itemId) {
|
|
5655
5904
|
const spineItem = packageDoc.spine.find((s) => s.idref === itemId);
|
|
5656
5905
|
if (!spineItem) return false;
|
|
@@ -5717,6 +5966,10 @@ var ContentValidator = class {
|
|
|
5717
5966
|
if (refValidator) {
|
|
5718
5967
|
this.extractSVGReferences(context, fullPath, opfDir, refValidator);
|
|
5719
5968
|
}
|
|
5969
|
+
} else if (item.mediaType === "application/vnd.epub.search-key-map+xml") {
|
|
5970
|
+
const fullPath = resolveManifestHref(opfDir, item.href);
|
|
5971
|
+
const skmValidator = new SKMValidator();
|
|
5972
|
+
skmValidator.validate(context, fullPath, refValidator);
|
|
5720
5973
|
} else if (item.mediaType === "application/smil+xml") {
|
|
5721
5974
|
const fullPath = resolveManifestHref(opfDir, item.href);
|
|
5722
5975
|
const smilValidator = new SMILValidator();
|
|
@@ -6479,6 +6732,9 @@ var ContentValidator = class {
|
|
|
6479
6732
|
}
|
|
6480
6733
|
}
|
|
6481
6734
|
}
|
|
6735
|
+
if (context.version === "2.0") {
|
|
6736
|
+
this.checkEpub2XhtmlStrict(context, path, root);
|
|
6737
|
+
}
|
|
6482
6738
|
this.checkDiscouragedElements(context, path, root);
|
|
6483
6739
|
this.checkSSMLPh(context, path, root, content);
|
|
6484
6740
|
this.checkObsoleteHTML(context, path, root);
|
|
@@ -6509,6 +6765,9 @@ var ContentValidator = class {
|
|
|
6509
6765
|
this.validateEpubTypes(context, path, root);
|
|
6510
6766
|
this.validateRegionBasedNav(context, path, root, manifestItem);
|
|
6511
6767
|
}
|
|
6768
|
+
if (context.version.startsWith("3") && context.options.profile === "dict") {
|
|
6769
|
+
this.validateDictionaryContent(context, path, root);
|
|
6770
|
+
}
|
|
6512
6771
|
if (context.version.startsWith("3") && context.options.profile === "edupub") {
|
|
6513
6772
|
const isFxl = manifestItem && packageDoc ? isItemFixedLayout(packageDoc, manifestItem.id) : false;
|
|
6514
6773
|
const isNonLinear = manifestItem && packageDoc ? packageDoc.spine.find((ref) => ref.idref === manifestItem.id)?.linear === false : false;
|
|
@@ -6517,7 +6776,7 @@ var ContentValidator = class {
|
|
|
6517
6776
|
}
|
|
6518
6777
|
}
|
|
6519
6778
|
if (context.version.startsWith("3")) {
|
|
6520
|
-
this.collectFeatures(context, root);
|
|
6779
|
+
this.collectFeatures(context, path, root);
|
|
6521
6780
|
}
|
|
6522
6781
|
this.validateEpubSwitch(context, path, root);
|
|
6523
6782
|
this.validateEpubTrigger(context, path, root);
|
|
@@ -8040,7 +8299,7 @@ var ContentValidator = class {
|
|
|
8040
8299
|
}
|
|
8041
8300
|
}
|
|
8042
8301
|
}
|
|
8043
|
-
collectFeatures(context, root) {
|
|
8302
|
+
collectFeatures(context, path, root) {
|
|
8044
8303
|
const features = context.contentFeatures;
|
|
8045
8304
|
if (!features) return;
|
|
8046
8305
|
if (!features.hasTable && root.get(".//html:table", XHTML_NS)) {
|
|
@@ -8055,22 +8314,20 @@ var ContentValidator = class {
|
|
|
8055
8314
|
if (!features.hasVideo && root.get(".//html:video", XHTML_NS)) {
|
|
8056
8315
|
features.hasVideo = true;
|
|
8057
8316
|
}
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
}
|
|
8073
|
-
if (features.hasPageBreak && features.hasDictionary && features.hasIndex) break;
|
|
8317
|
+
const epubTypeElements = root.find(".//*[@epub:type]", EPUB_OPS_NS);
|
|
8318
|
+
for (const el of epubTypeElements) {
|
|
8319
|
+
const attr = el.attr("type", "epub");
|
|
8320
|
+
if (!attr?.value) continue;
|
|
8321
|
+
const tokens = attr.value.trim().split(/\s+/);
|
|
8322
|
+
if (!features.hasPageBreak && tokens.includes("pagebreak")) {
|
|
8323
|
+
features.hasPageBreak = true;
|
|
8324
|
+
}
|
|
8325
|
+
if (tokens.includes("dictionary")) {
|
|
8326
|
+
features.hasDictionary = true;
|
|
8327
|
+
(features.dictionaryContentPaths ??= /* @__PURE__ */ new Set()).add(path);
|
|
8328
|
+
}
|
|
8329
|
+
if (!features.hasIndex && tokens.includes("index")) {
|
|
8330
|
+
features.hasIndex = true;
|
|
8074
8331
|
}
|
|
8075
8332
|
}
|
|
8076
8333
|
if (!features.hasMicrodata && root.get(".//*[@itemscope]")) {
|
|
@@ -8171,6 +8428,158 @@ var ContentValidator = class {
|
|
|
8171
8428
|
});
|
|
8172
8429
|
}
|
|
8173
8430
|
}
|
|
8431
|
+
this.validateRegionBasedNavRules(context, path, root);
|
|
8432
|
+
}
|
|
8433
|
+
}
|
|
8434
|
+
validateRegionBasedNavRules(context, path, root) {
|
|
8435
|
+
const XHTML_NS2 = { html: "http://www.w3.org/1999/xhtml" };
|
|
8436
|
+
let regionNavs;
|
|
8437
|
+
try {
|
|
8438
|
+
regionNavs = root.find(".//html:nav", XHTML_NS2);
|
|
8439
|
+
} catch {
|
|
8440
|
+
return;
|
|
8441
|
+
}
|
|
8442
|
+
const packageDoc = context.packageDocument;
|
|
8443
|
+
const opfDir = context.opfPath?.includes("/") ? context.opfPath.substring(0, context.opfPath.lastIndexOf("/")) : "";
|
|
8444
|
+
const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
|
|
8445
|
+
const manifestByPath = packageDoc ? new Map(packageDoc.manifest.map((m) => [resolveManifestHref(opfDir, m.href), m])) : void 0;
|
|
8446
|
+
for (const nav of regionNavs) {
|
|
8447
|
+
const epubType = nav.attr("type", "epub")?.value ?? "";
|
|
8448
|
+
if (!epubType.split(/\s+/).includes("region-based")) continue;
|
|
8449
|
+
const childEls = nav.find("./html:*", XHTML_NS2);
|
|
8450
|
+
if (childEls.length !== 1 || childEls[0]?.name !== "ol") {
|
|
8451
|
+
pushMessage(context.messages, {
|
|
8452
|
+
id: MessageId.RSC_017,
|
|
8453
|
+
message: "A region-based nav element must contain exactly one child ol element.",
|
|
8454
|
+
location: { path, line: nav.line }
|
|
8455
|
+
});
|
|
8456
|
+
}
|
|
8457
|
+
const liElements = nav.find(".//html:li", XHTML_NS2);
|
|
8458
|
+
for (const li of liElements) {
|
|
8459
|
+
const liChildren = li.find("./html:*", XHTML_NS2);
|
|
8460
|
+
const first = liChildren[0];
|
|
8461
|
+
if (!first || first.name !== "a" && first.name !== "span") {
|
|
8462
|
+
pushMessage(context.messages, {
|
|
8463
|
+
id: MessageId.RSC_017,
|
|
8464
|
+
message: "The first child of a region-based nav list item must be either an 'a' or 'span' element.",
|
|
8465
|
+
location: { path, line: li.line }
|
|
8466
|
+
});
|
|
8467
|
+
}
|
|
8468
|
+
if (liChildren.length > 1 && (liChildren.length !== 2 || liChildren[1]?.name !== "ol")) {
|
|
8469
|
+
pushMessage(context.messages, {
|
|
8470
|
+
id: MessageId.RSC_017,
|
|
8471
|
+
message: "The first child of a region-based nav list item can only be followed by a single 'ol' element.",
|
|
8472
|
+
location: { path, line: li.line }
|
|
8473
|
+
});
|
|
8474
|
+
}
|
|
8475
|
+
}
|
|
8476
|
+
const spans = nav.find(".//html:span", XHTML_NS2);
|
|
8477
|
+
for (const span of spans) {
|
|
8478
|
+
const spanChildren = span.find("./html:*", XHTML_NS2);
|
|
8479
|
+
const aChildren = spanChildren.filter((c) => c.name === "a");
|
|
8480
|
+
if (spanChildren.length !== 2 || aChildren.length !== 2) {
|
|
8481
|
+
pushMessage(context.messages, {
|
|
8482
|
+
id: MessageId.RSC_017,
|
|
8483
|
+
message: "'span' elements in region-based navs must contain exactly two 'a' elements.",
|
|
8484
|
+
location: { path, line: span.line }
|
|
8485
|
+
});
|
|
8486
|
+
}
|
|
8487
|
+
}
|
|
8488
|
+
const anchors = nav.find(".//html:a", XHTML_NS2);
|
|
8489
|
+
for (const a of anchors) {
|
|
8490
|
+
if (a.content.trim() !== "") {
|
|
8491
|
+
pushMessage(context.messages, {
|
|
8492
|
+
id: MessageId.RSC_017,
|
|
8493
|
+
message: "'a' elements in region-based navs should not contain text labels.",
|
|
8494
|
+
location: { path, line: a.line }
|
|
8495
|
+
});
|
|
8496
|
+
}
|
|
8497
|
+
}
|
|
8498
|
+
if (!packageDoc || !manifestByPath) continue;
|
|
8499
|
+
for (const a of anchors) {
|
|
8500
|
+
const href = a.attr("href")?.value;
|
|
8501
|
+
if (!href || !isRelativeURL(href)) continue;
|
|
8502
|
+
const resolved = this.resolveRelativePath(docDir, href, opfDir);
|
|
8503
|
+
const targetPath = parseURL(resolved).resource;
|
|
8504
|
+
if (!targetPath) continue;
|
|
8505
|
+
const item = manifestByPath.get(targetPath);
|
|
8506
|
+
if (!item) continue;
|
|
8507
|
+
if (!isItemFixedLayout(packageDoc, item.id)) {
|
|
8508
|
+
pushMessage(context.messages, {
|
|
8509
|
+
id: MessageId.NAV_009,
|
|
8510
|
+
message: "Region-based navigation links must point to Fixed-Layout Documents.",
|
|
8511
|
+
location: { path, line: a.line }
|
|
8512
|
+
});
|
|
8513
|
+
}
|
|
8514
|
+
}
|
|
8515
|
+
}
|
|
8516
|
+
}
|
|
8517
|
+
/**
|
|
8518
|
+
* EPUB Dictionaries content document rules.
|
|
8519
|
+
*
|
|
8520
|
+
* Mirrors ../epubcheck/src/main/resources/com/adobe/epubcheck/schema/30/dict/dict-xhtml.sch
|
|
8521
|
+
* (minimum set: `dictionary` must be on body/section with article children; each article or
|
|
8522
|
+
* `dictentry` must have a `dfn` descendant outside of optional `condensed-entry`).
|
|
8523
|
+
*/
|
|
8524
|
+
validateDictionaryContent(context, path, root) {
|
|
8525
|
+
let typedElements;
|
|
8526
|
+
try {
|
|
8527
|
+
typedElements = root.find(".//*[@epub:type]", EPUB_OPS_NS);
|
|
8528
|
+
} catch {
|
|
8529
|
+
return;
|
|
8530
|
+
}
|
|
8531
|
+
for (const el of typedElements) {
|
|
8532
|
+
const tokens = el.attr("type", "epub")?.value.split(/\s+/) ?? [];
|
|
8533
|
+
if (tokens.includes("dictionary")) {
|
|
8534
|
+
if (el.name !== "body" && el.name !== "section") {
|
|
8535
|
+
pushMessage(context.messages, {
|
|
8536
|
+
id: MessageId.RSC_005,
|
|
8537
|
+
message: 'The "dictionary" type is only allowed on "body" or "section" elements.',
|
|
8538
|
+
location: { path, line: el.line }
|
|
8539
|
+
});
|
|
8540
|
+
}
|
|
8541
|
+
const articles = el.find("./html:article", XHTML_NS);
|
|
8542
|
+
if (articles.length === 0) {
|
|
8543
|
+
pushMessage(context.messages, {
|
|
8544
|
+
id: MessageId.RSC_005,
|
|
8545
|
+
message: 'A "dictionary" must have at least one article child.',
|
|
8546
|
+
location: { path, line: el.line }
|
|
8547
|
+
});
|
|
8548
|
+
}
|
|
8549
|
+
for (const article of articles) {
|
|
8550
|
+
this.checkDictionaryEntry(context, path, article);
|
|
8551
|
+
}
|
|
8552
|
+
}
|
|
8553
|
+
if (tokens.includes("dictentry")) {
|
|
8554
|
+
if (el.name !== "article") {
|
|
8555
|
+
pushMessage(context.messages, {
|
|
8556
|
+
id: MessageId.RSC_005,
|
|
8557
|
+
message: 'The "dictentry" type is only allowed on "article" elements.',
|
|
8558
|
+
location: { path, line: el.line }
|
|
8559
|
+
});
|
|
8560
|
+
} else {
|
|
8561
|
+
this.checkDictionaryEntry(context, path, el);
|
|
8562
|
+
}
|
|
8563
|
+
}
|
|
8564
|
+
}
|
|
8565
|
+
}
|
|
8566
|
+
checkDictionaryEntry(context, path, article) {
|
|
8567
|
+
const dfns = article.find(".//html:dfn", XHTML_NS);
|
|
8568
|
+
const hasDfnOutsideCondensed = dfns.some((dfn) => {
|
|
8569
|
+
let parent = dfn.parent;
|
|
8570
|
+
while (parent) {
|
|
8571
|
+
const type = parent.attr("type", "epub")?.value;
|
|
8572
|
+
if (type?.split(/\s+/).includes("condensed-entry")) return false;
|
|
8573
|
+
parent = parent.parent;
|
|
8574
|
+
}
|
|
8575
|
+
return true;
|
|
8576
|
+
});
|
|
8577
|
+
if (!hasDfnOutsideCondensed) {
|
|
8578
|
+
pushMessage(context.messages, {
|
|
8579
|
+
id: MessageId.RSC_005,
|
|
8580
|
+
message: 'A dictionary entry must have at least one "dfn" descendant (outside of the optional condensed entry "aside").',
|
|
8581
|
+
location: { path, line: article.line }
|
|
8582
|
+
});
|
|
8174
8583
|
}
|
|
8175
8584
|
}
|
|
8176
8585
|
/**
|
|
@@ -9567,13 +9976,11 @@ var ContentValidator = class {
|
|
|
9567
9976
|
}
|
|
9568
9977
|
}
|
|
9569
9978
|
checkUnknownElements(context, path, root) {
|
|
9570
|
-
const XHTML_NS2 = "http://www.w3.org/1999/xhtml";
|
|
9571
9979
|
try {
|
|
9572
9980
|
const allElements = root.find(".//*");
|
|
9573
9981
|
for (const el of allElements) {
|
|
9574
9982
|
const xmlEl = el;
|
|
9575
|
-
|
|
9576
|
-
if (ns !== XHTML_NS2) continue;
|
|
9983
|
+
if (xmlEl.namespaceUri !== XHTML_NS_URI) continue;
|
|
9577
9984
|
const localName = xmlEl.name.includes(":") ? xmlEl.name.substring(xmlEl.name.indexOf(":") + 1) : xmlEl.name;
|
|
9578
9985
|
if (localName.includes("-")) continue;
|
|
9579
9986
|
if (!HTML5_ELEMENTS.has(localName)) {
|
|
@@ -9587,6 +9994,51 @@ var ContentValidator = class {
|
|
|
9587
9994
|
} catch {
|
|
9588
9995
|
}
|
|
9589
9996
|
}
|
|
9997
|
+
checkEpub2XhtmlStrict(context, path, root) {
|
|
9998
|
+
if (!root.namespaceUri) {
|
|
9999
|
+
pushMessage(context.messages, {
|
|
10000
|
+
id: MessageId.RSC_005,
|
|
10001
|
+
message: `element "${root.name}" from namespace "" is not allowed`,
|
|
10002
|
+
location: { path, line: root.line }
|
|
10003
|
+
});
|
|
10004
|
+
return;
|
|
10005
|
+
}
|
|
10006
|
+
const checkElement = (xmlEl) => {
|
|
10007
|
+
if (xmlEl.namespaceUri !== XHTML_NS_URI) return;
|
|
10008
|
+
const localName = xmlEl.name.includes(":") ? xmlEl.name.substring(xmlEl.name.indexOf(":") + 1) : xmlEl.name;
|
|
10009
|
+
if (!XHTML11_ELEMENTS.has(localName)) {
|
|
10010
|
+
pushMessage(context.messages, {
|
|
10011
|
+
id: MessageId.RSC_005,
|
|
10012
|
+
message: `element "${localName}" not allowed here`,
|
|
10013
|
+
location: { path, line: xmlEl.line }
|
|
10014
|
+
});
|
|
10015
|
+
}
|
|
10016
|
+
for (const attr of xmlEl.attrs) {
|
|
10017
|
+
const ns = attr.namespaceUri;
|
|
10018
|
+
if (!ns || ns === XHTML_NS_URI || ns === XML_NS_URI) continue;
|
|
10019
|
+
const qname = attr.prefix ? `${attr.prefix}:${attr.name}` : attr.name;
|
|
10020
|
+
pushMessage(context.messages, {
|
|
10021
|
+
id: MessageId.RSC_005,
|
|
10022
|
+
message: `attribute "${qname}" not allowed here`,
|
|
10023
|
+
location: { path, line: xmlEl.line }
|
|
10024
|
+
});
|
|
10025
|
+
}
|
|
10026
|
+
};
|
|
10027
|
+
checkElement(root);
|
|
10028
|
+
try {
|
|
10029
|
+
for (const el of root.find(".//*")) {
|
|
10030
|
+
checkElement(el);
|
|
10031
|
+
}
|
|
10032
|
+
for (const a of root.find(".//html:a//html:a", XHTML_NS)) {
|
|
10033
|
+
pushMessage(context.messages, {
|
|
10034
|
+
id: MessageId.RSC_005,
|
|
10035
|
+
message: 'The "a" element cannot contain any nested "a" elements',
|
|
10036
|
+
location: { path, line: a.line }
|
|
10037
|
+
});
|
|
10038
|
+
}
|
|
10039
|
+
} catch {
|
|
10040
|
+
}
|
|
10041
|
+
}
|
|
9590
10042
|
checkForeignObjectContent(context, path, root, isSVGDoc) {
|
|
9591
10043
|
const SVG_NS = { svg: "http://www.w3.org/2000/svg" };
|
|
9592
10044
|
const XHTML_URI = "http://www.w3.org/1999/xhtml";
|
|
@@ -9795,6 +10247,7 @@ var NCXValidator = class {
|
|
|
9795
10247
|
this.checkContentSrc(context, root, ncxPath, registry);
|
|
9796
10248
|
this.checkEmptyLabels(context, root, ncxPath);
|
|
9797
10249
|
this.checkPageTargets(context, root, ncxPath);
|
|
10250
|
+
this.checkIdSyntax(context, root, ncxPath);
|
|
9798
10251
|
} finally {
|
|
9799
10252
|
doc.dispose();
|
|
9800
10253
|
}
|
|
@@ -9898,10 +10351,21 @@ var NCXValidator = class {
|
|
|
9898
10351
|
}
|
|
9899
10352
|
}
|
|
9900
10353
|
}
|
|
9901
|
-
|
|
9902
|
-
|
|
9903
|
-
|
|
9904
|
-
|
|
10354
|
+
checkIdSyntax(context, root, ncxPath) {
|
|
10355
|
+
const nodes = root.find(".//*[@id]", {
|
|
10356
|
+
ncx: "http://www.daisy.org/z3986/2005/ncx/"
|
|
10357
|
+
});
|
|
10358
|
+
for (const node of nodes) {
|
|
10359
|
+
const idValue = node.attr("id")?.value;
|
|
10360
|
+
if (idValue && !NC_NAME_REGEX.test(idValue)) {
|
|
10361
|
+
pushMessage(context.messages, {
|
|
10362
|
+
id: MessageId.RSC_005,
|
|
10363
|
+
message: `Invalid id "${idValue}"; must match xs:ID syntax (NCName)`,
|
|
10364
|
+
location: { path: ncxPath, line: node.line }
|
|
10365
|
+
});
|
|
10366
|
+
}
|
|
10367
|
+
}
|
|
10368
|
+
}
|
|
9905
10369
|
checkPageTargets(context, root, ncxPath) {
|
|
9906
10370
|
const pageTargets = root.find(".//ncx:pageTarget[@type]", {
|
|
9907
10371
|
ncx: "http://www.daisy.org/z3986/2005/ncx/"
|
|
@@ -9919,6 +10383,7 @@ var NCXValidator = class {
|
|
|
9919
10383
|
}
|
|
9920
10384
|
}
|
|
9921
10385
|
};
|
|
10386
|
+
var NC_NAME_REGEX = /^[A-Za-z_][A-Za-z0-9._-]*$/;
|
|
9922
10387
|
var PAGE_TARGET_TYPES = /* @__PURE__ */ new Set(["front", "normal", "special"]);
|
|
9923
10388
|
var OPS_MEDIA_TYPES = /* @__PURE__ */ new Set([
|
|
9924
10389
|
"application/xhtml+xml",
|
|
@@ -10048,15 +10513,6 @@ function parseContainerContent(content, context, fileExists, getFileContent) {
|
|
|
10048
10513
|
}
|
|
10049
10514
|
}
|
|
10050
10515
|
}
|
|
10051
|
-
for (const rootfile of context.rootfiles) {
|
|
10052
|
-
if (!fileExists(rootfile.path)) {
|
|
10053
|
-
pushMessage(context.messages, {
|
|
10054
|
-
id: MessageId.PKG_010,
|
|
10055
|
-
message: `Rootfile "${rootfile.path}" not found in EPUB`,
|
|
10056
|
-
location: { path: containerPath }
|
|
10057
|
-
});
|
|
10058
|
-
}
|
|
10059
|
-
}
|
|
10060
10516
|
}
|
|
10061
10517
|
function validateMappingDocumentContent(xml, mappingPath, context) {
|
|
10062
10518
|
const stripped = stripXmlComments(xml);
|
|
@@ -10975,19 +11431,7 @@ var ReferenceValidator = class {
|
|
|
10975
11431
|
message: "Absolute paths are not allowed in EPUB",
|
|
10976
11432
|
location: reference.location
|
|
10977
11433
|
});
|
|
10978
|
-
}
|
|
10979
|
-
const forbiddenParentDirTypes = [
|
|
10980
|
-
"hyperlink" /* HYPERLINK */,
|
|
10981
|
-
"nav-toc-link" /* NAV_TOC_LINK */,
|
|
10982
|
-
"nav-pagelist-link" /* NAV_PAGELIST_LINK */
|
|
10983
|
-
];
|
|
10984
|
-
if (hasParentDirectoryReference(reference.url) && forbiddenParentDirTypes.includes(reference.type)) {
|
|
10985
|
-
pushMessage(context.messages, {
|
|
10986
|
-
id: MessageId.RSC_026,
|
|
10987
|
-
message: "Parent directory references (..) are not allowed",
|
|
10988
|
-
location: reference.location
|
|
10989
|
-
});
|
|
10990
|
-
} else if (!hasAbsolutePath(resourcePath) && !hasParentDirectoryReference(reference.url) && checkUrlLeaking(reference.url)) {
|
|
11434
|
+
} else if (checkUrlLeaking(reference.url, reference.location.path)) {
|
|
10991
11435
|
pushMessage(context.messages, {
|
|
10992
11436
|
id: MessageId.RSC_026,
|
|
10993
11437
|
message: `URL "${reference.url}" leaks outside the container`,
|
|
@@ -11025,6 +11469,16 @@ var ReferenceValidator = class {
|
|
|
11025
11469
|
location: reference.location
|
|
11026
11470
|
});
|
|
11027
11471
|
}
|
|
11472
|
+
if (reference.type === "search-key" /* SEARCH_KEY */ && !resource?.inSpine) {
|
|
11473
|
+
const isEpubCfi = reference.fragment?.startsWith("epubcfi(") ?? false;
|
|
11474
|
+
if (!isEpubCfi) {
|
|
11475
|
+
pushMessage(context.messages, {
|
|
11476
|
+
id: MessageId.RSC_021,
|
|
11477
|
+
message: `Search Key Map target "${resourcePath}" must be a Content Document in the spine`,
|
|
11478
|
+
location: reference.location
|
|
11479
|
+
});
|
|
11480
|
+
}
|
|
11481
|
+
}
|
|
11028
11482
|
if (isHyperlinkLike || reference.type === "overlay-text-link" /* OVERLAY_TEXT_LINK */) {
|
|
11029
11483
|
const targetMimeType = resource?.mimeType;
|
|
11030
11484
|
if (targetMimeType && !this.isBlessedItemType(targetMimeType, context.version) && !this.isDeprecatedBlessedItemType(targetMimeType) && !resource.hasCoreMediaTypeFallback) {
|
|
@@ -11509,11 +11963,11 @@ var RelaxNGValidator = class extends BaseSchemaValidator {
|
|
|
11509
11963
|
try {
|
|
11510
11964
|
const libxml2 = await import('libxml2-wasm');
|
|
11511
11965
|
const LibRelaxNGValidator = libxml2.RelaxNGValidator;
|
|
11512
|
-
const { XmlDocument:
|
|
11513
|
-
const doc =
|
|
11966
|
+
const { XmlDocument: XmlDocument5 } = libxml2;
|
|
11967
|
+
const doc = XmlDocument5.fromString(xml);
|
|
11514
11968
|
try {
|
|
11515
11969
|
const schemaContent = await loadSchema(schemaPath);
|
|
11516
|
-
const schemaDoc =
|
|
11970
|
+
const schemaDoc = XmlDocument5.fromString(schemaContent);
|
|
11517
11971
|
try {
|
|
11518
11972
|
const validator = LibRelaxNGValidator.fromDoc(schemaDoc);
|
|
11519
11973
|
try {
|
|
@@ -11977,6 +12431,73 @@ var EpubCheck = class _EpubCheck {
|
|
|
11977
12431
|
location: { path: opfPath }
|
|
11978
12432
|
});
|
|
11979
12433
|
}
|
|
12434
|
+
if (profile === "dict" && context.packageDocument) {
|
|
12435
|
+
const opfDir = opfPath.includes("/") ? opfPath.substring(0, opfPath.lastIndexOf("/")) : "";
|
|
12436
|
+
const manifestByPath = /* @__PURE__ */ new Map();
|
|
12437
|
+
for (const item of context.packageDocument.manifest) {
|
|
12438
|
+
manifestByPath.set(resolveManifestHref(opfDir, item.href), item);
|
|
12439
|
+
}
|
|
12440
|
+
const dictPaths = features.dictionaryContentPaths ?? /* @__PURE__ */ new Set();
|
|
12441
|
+
for (const coll of context.packageDocument.collections) {
|
|
12442
|
+
if (coll.role !== "dictionary") continue;
|
|
12443
|
+
const hasDictContent = coll.links.some((href) => {
|
|
12444
|
+
const full = resolveManifestHref(opfDir, href);
|
|
12445
|
+
const item = manifestByPath.get(full);
|
|
12446
|
+
return item?.mediaType === "application/xhtml+xml" && dictPaths.has(full);
|
|
12447
|
+
});
|
|
12448
|
+
if (!hasDictContent) {
|
|
12449
|
+
pushMessage(context.messages, {
|
|
12450
|
+
id: MessageId.OPF_078,
|
|
12451
|
+
message: 'An EPUB Dictionary must contain at least one Content Document with dictionary content (epub:type "dictionary")',
|
|
12452
|
+
location: { path: opfPath }
|
|
12453
|
+
});
|
|
12454
|
+
}
|
|
12455
|
+
}
|
|
12456
|
+
}
|
|
12457
|
+
}
|
|
12458
|
+
/**
|
|
12459
|
+
* Mirrors ../epubcheck/src/main/resources/com/adobe/epubcheck/schema/30/edupub/
|
|
12460
|
+
* edu-ocf-metadata.sch (META-INF/metadata.xml) and edu-opf.sch (each OPF).
|
|
12461
|
+
*
|
|
12462
|
+
* Non-primary OPFs are otherwise unreached: runPipeline only runs OPFValidator
|
|
12463
|
+
* on context.opfPath.
|
|
12464
|
+
*/
|
|
12465
|
+
validateEdupubMultiRendition(context) {
|
|
12466
|
+
if (context.options.profile !== "edupub") return;
|
|
12467
|
+
if (context.rootfiles.length <= 1) return;
|
|
12468
|
+
const metadataPath = "META-INF/metadata.xml";
|
|
12469
|
+
const metadataData = context.files.get(metadataPath);
|
|
12470
|
+
if (metadataData) {
|
|
12471
|
+
const content = typeof metadataData === "string" ? metadataData : new TextDecoder().decode(metadataData);
|
|
12472
|
+
const stripped = stripXmlComments(content);
|
|
12473
|
+
if (!/<dc:type\b[^>]*>\s*edupub\s*<\/dc:type>/i.test(stripped)) {
|
|
12474
|
+
pushMessage(context.messages, {
|
|
12475
|
+
id: MessageId.RSC_005,
|
|
12476
|
+
message: 'A dc:type element with the value "edupub" is required.',
|
|
12477
|
+
location: { path: metadataPath }
|
|
12478
|
+
});
|
|
12479
|
+
}
|
|
12480
|
+
}
|
|
12481
|
+
const primary = context.opfPath;
|
|
12482
|
+
for (const rootfile of context.rootfiles) {
|
|
12483
|
+
if (rootfile.path === primary) continue;
|
|
12484
|
+
if (rootfile.mediaType !== "application/oebps-package+xml") continue;
|
|
12485
|
+
const path = rootfile.path.normalize("NFC");
|
|
12486
|
+
const opfData = context.files.get(path);
|
|
12487
|
+
if (!opfData) continue;
|
|
12488
|
+
const xml = typeof opfData === "string" ? opfData : new TextDecoder().decode(opfData);
|
|
12489
|
+
const pkg = parseOPF(xml);
|
|
12490
|
+
const hasType = pkg.dcElements.some(
|
|
12491
|
+
(dc) => dc.name === "type" && dc.value.trim() === "edupub"
|
|
12492
|
+
);
|
|
12493
|
+
if (!hasType) {
|
|
12494
|
+
pushMessage(context.messages, {
|
|
12495
|
+
id: MessageId.RSC_005,
|
|
12496
|
+
message: 'The dc:type identifier "edupub" is required.',
|
|
12497
|
+
location: { path }
|
|
12498
|
+
});
|
|
12499
|
+
}
|
|
12500
|
+
}
|
|
11980
12501
|
}
|
|
11981
12502
|
/**
|
|
11982
12503
|
* Validate NCX navigation document (EPUB 2 always, EPUB 3 when NCX present)
|
|
@@ -12122,6 +12643,7 @@ var EpubCheck = class _EpubCheck {
|
|
|
12122
12643
|
const contentValidator = new ContentValidator();
|
|
12123
12644
|
contentValidator.validate(context, registry, refValidator);
|
|
12124
12645
|
this.validateCrossDocumentFeatures(context);
|
|
12646
|
+
this.validateEdupubMultiRendition(context);
|
|
12125
12647
|
if (context.packageDocument) {
|
|
12126
12648
|
this.validateNCX(context, registry);
|
|
12127
12649
|
}
|