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