@likecoin/epubcheck-ts 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/bin/epubcheck.js +15 -2
- package/bin/epubcheck.ts +15 -2
- package/dist/index.cjs +918 -244
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +918 -244
- 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] ?? "";
|
|
@@ -2823,6 +2693,31 @@ var PROFILE_DC_TYPE = {
|
|
|
2823
2693
|
dict: "dictionary",
|
|
2824
2694
|
preview: "preview"
|
|
2825
2695
|
};
|
|
2696
|
+
var TYPE_TO_PROFILE = {
|
|
2697
|
+
dictionary: "dict",
|
|
2698
|
+
edupub: "edupub",
|
|
2699
|
+
index: "idx",
|
|
2700
|
+
preview: "preview"
|
|
2701
|
+
};
|
|
2702
|
+
var RESERVED_PREFIX_URIS = {
|
|
2703
|
+
dcterms: "http://purl.org/dc/terms/",
|
|
2704
|
+
marc: "http://id.loc.gov/vocabulary/",
|
|
2705
|
+
media: "http://www.idpf.org/epub/vocab/overlays/#",
|
|
2706
|
+
onix: "http://www.editeur.org/ONIX/book/codelists/current.html#",
|
|
2707
|
+
rendition: "http://www.idpf.org/vocab/rendition/#",
|
|
2708
|
+
schema: "http://schema.org/",
|
|
2709
|
+
xsd: "http://www.w3.org/2001/XMLSchema#",
|
|
2710
|
+
a11y: "http://www.idpf.org/epub/vocab/package/a11y/#"
|
|
2711
|
+
};
|
|
2712
|
+
function isValidURI(uri) {
|
|
2713
|
+
if (!uri) return false;
|
|
2714
|
+
try {
|
|
2715
|
+
new URL(uri);
|
|
2716
|
+
return true;
|
|
2717
|
+
} catch {
|
|
2718
|
+
return false;
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2826
2721
|
var DICTIONARY_TYPE_VALUES = /* @__PURE__ */ new Set([
|
|
2827
2722
|
"monolingual",
|
|
2828
2723
|
"bilingual",
|
|
@@ -2929,9 +2824,14 @@ var OPFValidator = class {
|
|
|
2929
2824
|
}
|
|
2930
2825
|
this.validatePackageAttributes(context, opfPath);
|
|
2931
2826
|
this.validateMetadata(context, opfPath);
|
|
2827
|
+
if (this.packageDoc.version !== "2.0") {
|
|
2828
|
+
this.validatePrefixDeclarations(context, opfPath, opfXml);
|
|
2829
|
+
this.validateMetaPrefixes(context, opfPath, opfXml);
|
|
2830
|
+
}
|
|
2932
2831
|
this.validateLinkElements(context, opfPath);
|
|
2933
2832
|
this.validateManifest(context, opfPath);
|
|
2934
2833
|
this.validateSpine(context, opfPath);
|
|
2834
|
+
this.validatePageMap(context, opfPath, opfXml);
|
|
2935
2835
|
this.validateFallbackChains(context, opfPath);
|
|
2936
2836
|
this.validateUndeclaredResources(context, opfPath);
|
|
2937
2837
|
if (this.packageDoc.version === "2.0") {
|
|
@@ -2962,6 +2862,7 @@ var OPFValidator = class {
|
|
|
2962
2862
|
if (this.packageDoc.version.startsWith("3.")) {
|
|
2963
2863
|
this.validateAccessibilityMetadata(context, opfPath);
|
|
2964
2864
|
this.validateProfileDcType(context, opfPath);
|
|
2865
|
+
this.validateDcTypeProfileSwitch(context, opfPath);
|
|
2965
2866
|
this.validateEdupubMetadata(context, opfPath);
|
|
2966
2867
|
this.validateDictionaryMetadata(context, opfPath);
|
|
2967
2868
|
this.validatePreviewMetadata(context, opfPath);
|
|
@@ -3165,6 +3066,22 @@ var OPFValidator = class {
|
|
|
3165
3066
|
});
|
|
3166
3067
|
}
|
|
3167
3068
|
}
|
|
3069
|
+
// Mirrors Java's EPUBProfile.makeTypeCompatible flow.
|
|
3070
|
+
validateDcTypeProfileSwitch(context, opfPath) {
|
|
3071
|
+
if (!this.packageDoc) return;
|
|
3072
|
+
for (const dc of this.packageDoc.dcElements) {
|
|
3073
|
+
if (dc.name !== "type") continue;
|
|
3074
|
+
const inferred = TYPE_TO_PROFILE[dc.value.trim().toLowerCase()];
|
|
3075
|
+
if (inferred && inferred !== context.options.profile) {
|
|
3076
|
+
pushMessage(context.messages, {
|
|
3077
|
+
id: MessageId.OPF_064,
|
|
3078
|
+
message: `OPF declares type "${dc.value.trim().toLowerCase()}"; consider validating using the "${inferred}" profile.`,
|
|
3079
|
+
location: { path: opfPath }
|
|
3080
|
+
});
|
|
3081
|
+
return;
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3168
3085
|
/**
|
|
3169
3086
|
* Build lookup maps for manifest items
|
|
3170
3087
|
*/
|
|
@@ -3181,6 +3098,13 @@ var OPFValidator = class {
|
|
|
3181
3098
|
*/
|
|
3182
3099
|
validatePackageAttributes(context, opfPath) {
|
|
3183
3100
|
if (!this.packageDoc) return;
|
|
3101
|
+
if (this.packageDoc.isLegacyOebps12) {
|
|
3102
|
+
pushMessage(context.messages, {
|
|
3103
|
+
id: MessageId.OPF_047,
|
|
3104
|
+
message: "OPF file is using OEBPS 1.2 syntax allowing backwards compatibility.",
|
|
3105
|
+
location: { path: opfPath }
|
|
3106
|
+
});
|
|
3107
|
+
}
|
|
3184
3108
|
if (this.packageDoc.versionDeclared === false) {
|
|
3185
3109
|
pushMessage(context.messages, {
|
|
3186
3110
|
id: MessageId.OPF_001,
|
|
@@ -4105,14 +4029,24 @@ var OPFValidator = class {
|
|
|
4105
4029
|
const resolvedPath = resolvePath(opfPath, basePathNoQuery);
|
|
4106
4030
|
const resolvedPathDecoded = basePathDecodedNoQuery !== basePathNoQuery ? resolvePath(opfPath, basePathDecodedNoQuery) : resolvedPath;
|
|
4107
4031
|
const fileExists = context.files.has(resolvedPath) || context.files.has(resolvedPathDecoded);
|
|
4108
|
-
const
|
|
4109
|
-
if (!fileExists && !
|
|
4032
|
+
const manifestItem = this.manifestByHref.get(basePathNoQuery) ?? this.manifestByHref.get(basePathDecodedNoQuery);
|
|
4033
|
+
if (!fileExists && !manifestItem) {
|
|
4110
4034
|
pushMessage(context.messages, {
|
|
4111
4035
|
id: MessageId.RSC_007w,
|
|
4112
4036
|
message: `Referenced resource "${resolvedPath}" could not be found in the EPUB`,
|
|
4113
4037
|
location: { path: opfPath }
|
|
4114
4038
|
});
|
|
4115
4039
|
}
|
|
4040
|
+
if (manifestItem) {
|
|
4041
|
+
const inSpine = this.packageDoc.spine.some((ref) => ref.idref === manifestItem.id);
|
|
4042
|
+
if (!inSpine) {
|
|
4043
|
+
pushMessage(context.messages, {
|
|
4044
|
+
id: MessageId.OPF_067,
|
|
4045
|
+
message: `Resource "${manifestItem.href}" is referenced as a link but is also declared as a manifest item.`,
|
|
4046
|
+
location: { path: opfPath }
|
|
4047
|
+
});
|
|
4048
|
+
}
|
|
4049
|
+
}
|
|
4116
4050
|
}
|
|
4117
4051
|
}
|
|
4118
4052
|
/**
|
|
@@ -4122,6 +4056,7 @@ var OPFValidator = class {
|
|
|
4122
4056
|
if (!this.packageDoc) return;
|
|
4123
4057
|
const seenIds = /* @__PURE__ */ new Set();
|
|
4124
4058
|
const seenHrefs = /* @__PURE__ */ new Set();
|
|
4059
|
+
const declaredPrefixes = this.packageDoc.prefixes ?? {};
|
|
4125
4060
|
for (const item of this.packageDoc.manifest) {
|
|
4126
4061
|
if (seenIds.has(item.id)) {
|
|
4127
4062
|
pushMessage(context.messages, {
|
|
@@ -4165,7 +4100,7 @@ var OPFValidator = class {
|
|
|
4165
4100
|
});
|
|
4166
4101
|
}
|
|
4167
4102
|
if (!item.href.startsWith("http") && !item.href.startsWith("mailto:")) {
|
|
4168
|
-
const leaked = checkUrlLeaking(item.href);
|
|
4103
|
+
const leaked = checkUrlLeaking(item.href, opfPath);
|
|
4169
4104
|
if (leaked) {
|
|
4170
4105
|
pushMessage(context.messages, {
|
|
4171
4106
|
id: MessageId.RSC_026,
|
|
@@ -4200,11 +4135,11 @@ var OPFValidator = class {
|
|
|
4200
4135
|
if (DEPRECATED_MEDIA_TYPES.has(item.mediaType) || item.mediaType === "text/html") {
|
|
4201
4136
|
if (this.packageDoc.version === "2.0" && item.mediaType === "text/html") {
|
|
4202
4137
|
pushMessage(context.messages, {
|
|
4203
|
-
id: MessageId.OPF_035,
|
|
4138
|
+
id: this.packageDoc.isLegacyOebps12 ? MessageId.OPF_038 : MessageId.OPF_035,
|
|
4204
4139
|
message: `XHTML Content Document "${item.id}" is declared as "text/html"`,
|
|
4205
4140
|
location: { path: opfPath }
|
|
4206
4141
|
});
|
|
4207
|
-
} else if (this.packageDoc.version === "2.0") {
|
|
4142
|
+
} else if (this.packageDoc.version === "2.0" && !this.packageDoc.isLegacyOebps12) {
|
|
4208
4143
|
pushMessage(context.messages, {
|
|
4209
4144
|
id: MessageId.OPF_037,
|
|
4210
4145
|
message: `Found deprecated media-type "${item.mediaType}"`,
|
|
@@ -4212,6 +4147,13 @@ var OPFValidator = class {
|
|
|
4212
4147
|
});
|
|
4213
4148
|
}
|
|
4214
4149
|
}
|
|
4150
|
+
if (this.packageDoc.version === "2.0" && this.packageDoc.isLegacyOebps12 && item.mediaType === "text/css" && !item.fallback) {
|
|
4151
|
+
pushMessage(context.messages, {
|
|
4152
|
+
id: MessageId.OPF_039,
|
|
4153
|
+
message: `Media type "${item.mediaType}" requires a fallback in legacy OEBPS 1.2 context`,
|
|
4154
|
+
location: { path: opfPath }
|
|
4155
|
+
});
|
|
4156
|
+
}
|
|
4215
4157
|
const preferred = getPreferredMediaType(item.mediaType, fullPath);
|
|
4216
4158
|
if (preferred !== null) {
|
|
4217
4159
|
pushMessage(context.messages, {
|
|
@@ -4222,13 +4164,14 @@ var OPFValidator = class {
|
|
|
4222
4164
|
}
|
|
4223
4165
|
if (this.packageDoc.version !== "2.0" && item.properties) {
|
|
4224
4166
|
for (const prop of item.properties) {
|
|
4225
|
-
if (
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
}
|
|
4231
|
-
|
|
4167
|
+
if (ITEM_PROPERTIES.has(prop)) continue;
|
|
4168
|
+
const colon = prop.indexOf(":");
|
|
4169
|
+
if (colon > 0 && declaredPrefixes[prop.slice(0, colon)] !== void 0) continue;
|
|
4170
|
+
pushMessage(context.messages, {
|
|
4171
|
+
id: MessageId.OPF_027,
|
|
4172
|
+
message: `Undefined property: "${prop}"`,
|
|
4173
|
+
location: { path: opfPath }
|
|
4174
|
+
});
|
|
4232
4175
|
}
|
|
4233
4176
|
if (item.properties.includes("nav")) {
|
|
4234
4177
|
if (item.mediaType !== "application/xhtml+xml") {
|
|
@@ -4385,12 +4328,89 @@ var OPFValidator = class {
|
|
|
4385
4328
|
}
|
|
4386
4329
|
}
|
|
4387
4330
|
}
|
|
4331
|
+
// Mirrors Java's PrefixDeclarationParser + VocabUtil.parsePrefixDeclaration,
|
|
4332
|
+
// but emits only the four main IDs (not Java's OPF-004a..f sub-codes).
|
|
4333
|
+
validatePrefixDeclarations(context, opfPath, opfXml) {
|
|
4334
|
+
const stripped = stripXmlComments(opfXml);
|
|
4335
|
+
const match = /<package[^>]*\sprefix\s*=\s*["']([^"']*)["']/.exec(stripped);
|
|
4336
|
+
if (!match) return;
|
|
4337
|
+
const raw = match[1] ?? "";
|
|
4338
|
+
if (raw !== raw.trim()) {
|
|
4339
|
+
pushMessage(context.messages, {
|
|
4340
|
+
id: MessageId.OPF_004,
|
|
4341
|
+
message: "The value of the prefix attribute has leading or trailing whitespace.",
|
|
4342
|
+
location: { path: opfPath }
|
|
4343
|
+
});
|
|
4344
|
+
}
|
|
4345
|
+
const parts = raw.trim().split(/\s+/).filter(Boolean);
|
|
4346
|
+
for (let i = 0; i < parts.length; ) {
|
|
4347
|
+
const token = parts[i] ?? "";
|
|
4348
|
+
if (token.endsWith(":") && token.length > 1) {
|
|
4349
|
+
const prefix = token.slice(0, -1);
|
|
4350
|
+
const uri = parts[i + 1];
|
|
4351
|
+
if (!uri || uri.endsWith(":")) {
|
|
4352
|
+
pushMessage(context.messages, {
|
|
4353
|
+
id: MessageId.OPF_005,
|
|
4354
|
+
message: `The prefix "${prefix}" is declared but no URI is bound to it.`,
|
|
4355
|
+
location: { path: opfPath }
|
|
4356
|
+
});
|
|
4357
|
+
i += 1;
|
|
4358
|
+
continue;
|
|
4359
|
+
}
|
|
4360
|
+
if (!isValidURI(uri)) {
|
|
4361
|
+
pushMessage(context.messages, {
|
|
4362
|
+
id: MessageId.OPF_006,
|
|
4363
|
+
message: `The value "${uri}" bound to prefix "${prefix}" is not a valid URI.`,
|
|
4364
|
+
location: { path: opfPath }
|
|
4365
|
+
});
|
|
4366
|
+
}
|
|
4367
|
+
const reservedUri = RESERVED_PREFIX_URIS[prefix];
|
|
4368
|
+
if (reservedUri !== void 0 && reservedUri !== uri) {
|
|
4369
|
+
pushMessage(context.messages, {
|
|
4370
|
+
id: MessageId.OPF_007,
|
|
4371
|
+
message: `The prefix "${prefix}" is reserved and must not be re-declared.`,
|
|
4372
|
+
location: { path: opfPath }
|
|
4373
|
+
});
|
|
4374
|
+
}
|
|
4375
|
+
i += 2;
|
|
4376
|
+
} else {
|
|
4377
|
+
i += 1;
|
|
4378
|
+
}
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4388
4381
|
/**
|
|
4389
4382
|
* RSC-005: all id attributes on elements in the OPF document must be unique.
|
|
4390
4383
|
* Mirrors Java's id-unique.sch / opf.sch opf_idAttrUnique pattern, which
|
|
4391
4384
|
* emits one assertion failure per offending element (so two duplicate ids
|
|
4392
4385
|
* produce two RSC-005 messages).
|
|
4393
4386
|
*/
|
|
4387
|
+
validateMetaPrefixes(context, opfPath, opfXml) {
|
|
4388
|
+
if (!this.packageDoc) return;
|
|
4389
|
+
const RESERVED = new Set(Object.keys(RESERVED_PREFIX_URIS));
|
|
4390
|
+
const declared = new Set(Object.keys(this.packageDoc.prefixes ?? {}));
|
|
4391
|
+
const reported = /* @__PURE__ */ new Set();
|
|
4392
|
+
const reportIfUndeclared = (prefix) => {
|
|
4393
|
+
if (!prefix || reported.has(prefix)) return;
|
|
4394
|
+
if (RESERVED.has(prefix) || declared.has(prefix)) return;
|
|
4395
|
+
reported.add(prefix);
|
|
4396
|
+
pushMessage(context.messages, {
|
|
4397
|
+
id: MessageId.OPF_028,
|
|
4398
|
+
message: `Undeclared prefix: "${prefix}"`,
|
|
4399
|
+
location: { path: opfPath }
|
|
4400
|
+
});
|
|
4401
|
+
};
|
|
4402
|
+
const stripped = stripXmlComments(opfXml);
|
|
4403
|
+
const attrRegex = /\b(?:property|scheme|rel)\s*=\s*["']([^"']+)["']/g;
|
|
4404
|
+
for (const match of stripped.matchAll(attrRegex)) {
|
|
4405
|
+
const value = match[1]?.trim();
|
|
4406
|
+
if (!value) continue;
|
|
4407
|
+
for (const token of value.split(/\s+/)) {
|
|
4408
|
+
const colon = token.indexOf(":");
|
|
4409
|
+
if (colon <= 0) continue;
|
|
4410
|
+
reportIfUndeclared(token.slice(0, colon));
|
|
4411
|
+
}
|
|
4412
|
+
}
|
|
4413
|
+
}
|
|
4394
4414
|
validateOpfIdUniqueness(context, opfPath, opfXml) {
|
|
4395
4415
|
const stripped = stripXmlComments(opfXml);
|
|
4396
4416
|
const counts = /* @__PURE__ */ new Map();
|
|
@@ -4422,11 +4442,12 @@ var OPFValidator = class {
|
|
|
4422
4442
|
for (const item of this.packageDoc.manifest) {
|
|
4423
4443
|
const hrefBase = item.href.split("?")[0] ?? item.href;
|
|
4424
4444
|
if (/^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(hrefBase)) continue;
|
|
4425
|
-
manifestPaths.add(resolvePath(opfPath, hrefBase).normalize("NFC"));
|
|
4445
|
+
manifestPaths.add(resolvePath(opfPath, tryDecodeUriComponent(hrefBase)).normalize("NFC"));
|
|
4426
4446
|
}
|
|
4427
4447
|
const rootfilePaths = new Set(context.rootfiles.map((r) => r.path.normalize("NFC")));
|
|
4428
4448
|
for (const path of context.files.keys()) {
|
|
4429
4449
|
if (path === "mimetype") continue;
|
|
4450
|
+
if (path.endsWith("/")) continue;
|
|
4430
4451
|
if (path.startsWith("META-INF/")) continue;
|
|
4431
4452
|
if (rootfilePaths.has(path)) continue;
|
|
4432
4453
|
if (manifestPaths.has(path)) continue;
|
|
@@ -4572,6 +4593,26 @@ var OPFValidator = class {
|
|
|
4572
4593
|
}
|
|
4573
4594
|
}
|
|
4574
4595
|
}
|
|
4596
|
+
validatePageMap(context, opfPath, opfXml) {
|
|
4597
|
+
if (!this.packageDoc) return;
|
|
4598
|
+
const stripped = stripXmlComments(opfXml);
|
|
4599
|
+
const m = /<spine\b[^>]*\spage-map\s*=\s*["']([^"']*)["']/.exec(stripped);
|
|
4600
|
+
if (!m) return;
|
|
4601
|
+
const pageMapId = (m[1] ?? "").trim();
|
|
4602
|
+
pushMessage(context.messages, {
|
|
4603
|
+
id: MessageId.OPF_062,
|
|
4604
|
+
message: `Found Adobe page-map attribute on spine element (page-map="${pageMapId}")`,
|
|
4605
|
+
location: { path: opfPath }
|
|
4606
|
+
});
|
|
4607
|
+
if (!pageMapId) return;
|
|
4608
|
+
if (!this.manifestById.has(pageMapId)) {
|
|
4609
|
+
pushMessage(context.messages, {
|
|
4610
|
+
id: MessageId.OPF_063,
|
|
4611
|
+
message: `The Adobe page-map item "${pageMapId}" was not found in the manifest`,
|
|
4612
|
+
location: { path: opfPath }
|
|
4613
|
+
});
|
|
4614
|
+
}
|
|
4615
|
+
}
|
|
4575
4616
|
/**
|
|
4576
4617
|
* Validate fallback chains
|
|
4577
4618
|
*/
|
|
@@ -5015,6 +5056,246 @@ function isValidW3CDateFormat(dateStr) {
|
|
|
5015
5056
|
return false;
|
|
5016
5057
|
}
|
|
5017
5058
|
|
|
5059
|
+
// src/references/types.ts
|
|
5060
|
+
var PUBLICATION_RESOURCE_TYPES = /* @__PURE__ */ new Set([
|
|
5061
|
+
"generic" /* GENERIC */,
|
|
5062
|
+
"stylesheet" /* STYLESHEET */,
|
|
5063
|
+
"font" /* FONT */,
|
|
5064
|
+
"image" /* IMAGE */,
|
|
5065
|
+
"audio" /* AUDIO */,
|
|
5066
|
+
"video" /* VIDEO */,
|
|
5067
|
+
"track" /* TRACK */,
|
|
5068
|
+
"media-overlay" /* MEDIA_OVERLAY */,
|
|
5069
|
+
"svg-symbol" /* SVG_SYMBOL */,
|
|
5070
|
+
"svg-paint" /* SVG_PAINT */,
|
|
5071
|
+
"svg-clip-path" /* SVG_CLIP_PATH */
|
|
5072
|
+
]);
|
|
5073
|
+
function isPublicationResourceReference(type) {
|
|
5074
|
+
return PUBLICATION_RESOURCE_TYPES.has(type);
|
|
5075
|
+
}
|
|
5076
|
+
|
|
5077
|
+
// src/skm/validator.ts
|
|
5078
|
+
var OPS_NS_URI = "http://www.idpf.org/2007/ops";
|
|
5079
|
+
var SKM_NS = { ops: OPS_NS_URI };
|
|
5080
|
+
var SKMValidator = class {
|
|
5081
|
+
validate(context, path, refValidator) {
|
|
5082
|
+
const data = context.files.get(path);
|
|
5083
|
+
if (!data) return;
|
|
5084
|
+
const content = typeof data === "string" ? data : new TextDecoder().decode(data);
|
|
5085
|
+
let doc = null;
|
|
5086
|
+
try {
|
|
5087
|
+
doc = libxml2Wasm.XmlDocument.fromString(content);
|
|
5088
|
+
} catch {
|
|
5089
|
+
pushMessage(context.messages, {
|
|
5090
|
+
id: MessageId.RSC_016,
|
|
5091
|
+
message: "Search Key Map document is not well-formed XML",
|
|
5092
|
+
location: { path }
|
|
5093
|
+
});
|
|
5094
|
+
return;
|
|
5095
|
+
}
|
|
5096
|
+
try {
|
|
5097
|
+
const root = doc.root;
|
|
5098
|
+
if (root.namespaceUri !== OPS_NS_URI || root.name !== "search-key-map") {
|
|
5099
|
+
pushMessage(context.messages, {
|
|
5100
|
+
id: MessageId.RSC_005,
|
|
5101
|
+
message: `Root element must be "search-key-map" in the OPS namespace`,
|
|
5102
|
+
location: { path, line: root.line }
|
|
5103
|
+
});
|
|
5104
|
+
return;
|
|
5105
|
+
}
|
|
5106
|
+
const groups = root.find("./ops:search-key-group", SKM_NS);
|
|
5107
|
+
if (groups.length === 0) {
|
|
5108
|
+
pushMessage(context.messages, {
|
|
5109
|
+
id: MessageId.RSC_005,
|
|
5110
|
+
message: 'A "search-key-map" must contain at least one "search-key-group"',
|
|
5111
|
+
location: { path, line: root.line }
|
|
5112
|
+
});
|
|
5113
|
+
}
|
|
5114
|
+
for (const group of groups) {
|
|
5115
|
+
const groupEl = group;
|
|
5116
|
+
const href = groupEl.attr("href")?.value;
|
|
5117
|
+
if (!href) {
|
|
5118
|
+
pushMessage(context.messages, {
|
|
5119
|
+
id: MessageId.RSC_005,
|
|
5120
|
+
message: 'The "href" attribute is required on "search-key-group"',
|
|
5121
|
+
location: { path, line: groupEl.line }
|
|
5122
|
+
});
|
|
5123
|
+
} else if (refValidator) {
|
|
5124
|
+
registerSkmRef(refValidator, path, href, groupEl.line);
|
|
5125
|
+
}
|
|
5126
|
+
const matches = groupEl.find("./ops:match", SKM_NS);
|
|
5127
|
+
if (matches.length === 0) {
|
|
5128
|
+
pushMessage(context.messages, {
|
|
5129
|
+
id: MessageId.RSC_005,
|
|
5130
|
+
message: 'A "search-key-group" must contain at least one "match"',
|
|
5131
|
+
location: { path, line: groupEl.line }
|
|
5132
|
+
});
|
|
5133
|
+
}
|
|
5134
|
+
for (const match of matches) {
|
|
5135
|
+
const matchEl = match;
|
|
5136
|
+
const matchHref = matchEl.attr("href")?.value;
|
|
5137
|
+
if (matchHref && refValidator) {
|
|
5138
|
+
registerSkmRef(refValidator, path, matchHref, matchEl.line);
|
|
5139
|
+
}
|
|
5140
|
+
}
|
|
5141
|
+
}
|
|
5142
|
+
} finally {
|
|
5143
|
+
doc.dispose();
|
|
5144
|
+
}
|
|
5145
|
+
}
|
|
5146
|
+
};
|
|
5147
|
+
function registerSkmRef(refValidator, path, href, line) {
|
|
5148
|
+
const parsed = parseURL(href);
|
|
5149
|
+
const targetResource = resolvePath(path, tryDecodeUriComponent(parsed.resource)).normalize("NFC");
|
|
5150
|
+
const location = line != null ? { path, line } : { path };
|
|
5151
|
+
const ref = {
|
|
5152
|
+
url: parsed.hasFragment ? `${targetResource}#${parsed.fragment ?? ""}` : targetResource,
|
|
5153
|
+
targetResource,
|
|
5154
|
+
type: "search-key" /* SEARCH_KEY */,
|
|
5155
|
+
location
|
|
5156
|
+
};
|
|
5157
|
+
if (parsed.fragment !== void 0) ref.fragment = parsed.fragment;
|
|
5158
|
+
refValidator.addReference(ref);
|
|
5159
|
+
}
|
|
5160
|
+
|
|
5161
|
+
// src/vocab/epub-ssv.ts
|
|
5162
|
+
var EPUB_SSV_DEPRECATED = /* @__PURE__ */ new Set([
|
|
5163
|
+
"annoref",
|
|
5164
|
+
"annotation",
|
|
5165
|
+
"biblioentry",
|
|
5166
|
+
"bridgehead",
|
|
5167
|
+
"endnote",
|
|
5168
|
+
"help",
|
|
5169
|
+
"marginalia",
|
|
5170
|
+
"note",
|
|
5171
|
+
"rearnote",
|
|
5172
|
+
"rearnotes",
|
|
5173
|
+
"sidebar",
|
|
5174
|
+
"subchapter",
|
|
5175
|
+
"warning"
|
|
5176
|
+
]);
|
|
5177
|
+
var EPUB_SSV_DISALLOWED_ON_CONTENT = /* @__PURE__ */ new Set([
|
|
5178
|
+
"aside",
|
|
5179
|
+
"figure",
|
|
5180
|
+
"list",
|
|
5181
|
+
"list-item",
|
|
5182
|
+
"table",
|
|
5183
|
+
"table-cell",
|
|
5184
|
+
"table-row"
|
|
5185
|
+
]);
|
|
5186
|
+
var EPUB_SSV_ALL = /* @__PURE__ */ new Set([
|
|
5187
|
+
...EPUB_SSV_DEPRECATED,
|
|
5188
|
+
...EPUB_SSV_DISALLOWED_ON_CONTENT,
|
|
5189
|
+
"abstract",
|
|
5190
|
+
"acknowledgments",
|
|
5191
|
+
"afterword",
|
|
5192
|
+
"appendix",
|
|
5193
|
+
"assessment",
|
|
5194
|
+
"assessments",
|
|
5195
|
+
"backlink",
|
|
5196
|
+
"backmatter",
|
|
5197
|
+
"balloon",
|
|
5198
|
+
"bibliography",
|
|
5199
|
+
"biblioref",
|
|
5200
|
+
"bodymatter",
|
|
5201
|
+
"case-study",
|
|
5202
|
+
"chapter",
|
|
5203
|
+
"colophon",
|
|
5204
|
+
"concluding-sentence",
|
|
5205
|
+
"conclusion",
|
|
5206
|
+
"contributors",
|
|
5207
|
+
"copyright-page",
|
|
5208
|
+
"cover",
|
|
5209
|
+
"covertitle",
|
|
5210
|
+
"credit",
|
|
5211
|
+
"credits",
|
|
5212
|
+
"dedication",
|
|
5213
|
+
"division",
|
|
5214
|
+
"endnotes",
|
|
5215
|
+
"epigraph",
|
|
5216
|
+
"epilogue",
|
|
5217
|
+
"errata",
|
|
5218
|
+
"fill-in-the-blank-problem",
|
|
5219
|
+
"footnote",
|
|
5220
|
+
"footnotes",
|
|
5221
|
+
"foreword",
|
|
5222
|
+
"frontmatter",
|
|
5223
|
+
"fulltitle",
|
|
5224
|
+
"general-problem",
|
|
5225
|
+
"glossary",
|
|
5226
|
+
"glossdef",
|
|
5227
|
+
"glossref",
|
|
5228
|
+
"glossterm",
|
|
5229
|
+
"halftitle",
|
|
5230
|
+
"halftitlepage",
|
|
5231
|
+
"imprimatur",
|
|
5232
|
+
"imprint",
|
|
5233
|
+
"index",
|
|
5234
|
+
"index-editor-note",
|
|
5235
|
+
"index-entry",
|
|
5236
|
+
"index-entry-list",
|
|
5237
|
+
"index-group",
|
|
5238
|
+
"index-headnotes",
|
|
5239
|
+
"index-legend",
|
|
5240
|
+
"index-locator",
|
|
5241
|
+
"index-locator-list",
|
|
5242
|
+
"index-locator-range",
|
|
5243
|
+
"index-term",
|
|
5244
|
+
"index-term-categories",
|
|
5245
|
+
"index-term-category",
|
|
5246
|
+
"index-xref-preferred",
|
|
5247
|
+
"index-xref-related",
|
|
5248
|
+
"introduction",
|
|
5249
|
+
"keyword",
|
|
5250
|
+
"keywords",
|
|
5251
|
+
"label",
|
|
5252
|
+
"landmarks",
|
|
5253
|
+
"learning-objective",
|
|
5254
|
+
"learning-objectives",
|
|
5255
|
+
"learning-outcome",
|
|
5256
|
+
"learning-outcomes",
|
|
5257
|
+
"learning-resource",
|
|
5258
|
+
"learning-resources",
|
|
5259
|
+
"learning-standard",
|
|
5260
|
+
"learning-standards",
|
|
5261
|
+
"loa",
|
|
5262
|
+
"loi",
|
|
5263
|
+
"lot",
|
|
5264
|
+
"lov",
|
|
5265
|
+
"match-problem",
|
|
5266
|
+
"multiple-choice-problem",
|
|
5267
|
+
"noteref",
|
|
5268
|
+
"notice",
|
|
5269
|
+
"ordinal",
|
|
5270
|
+
"other-credits",
|
|
5271
|
+
"page-list",
|
|
5272
|
+
"pagebreak",
|
|
5273
|
+
"panel",
|
|
5274
|
+
"panel-group",
|
|
5275
|
+
"part",
|
|
5276
|
+
"practice",
|
|
5277
|
+
"practices",
|
|
5278
|
+
"preamble",
|
|
5279
|
+
"preface",
|
|
5280
|
+
"prologue",
|
|
5281
|
+
"pullquote",
|
|
5282
|
+
"qna",
|
|
5283
|
+
"question",
|
|
5284
|
+
"referrer",
|
|
5285
|
+
"revision-history",
|
|
5286
|
+
"seriespage",
|
|
5287
|
+
"sound-area",
|
|
5288
|
+
"subtitle",
|
|
5289
|
+
"tip",
|
|
5290
|
+
"title",
|
|
5291
|
+
"titlepage",
|
|
5292
|
+
"toc",
|
|
5293
|
+
"toc-brief",
|
|
5294
|
+
"topic-sentence",
|
|
5295
|
+
"true-false-problem",
|
|
5296
|
+
"volume"
|
|
5297
|
+
]);
|
|
5298
|
+
|
|
5018
5299
|
// src/smil/validator.ts
|
|
5019
5300
|
var SMIL_NS = { smil: "http://www.w3.org/ns/SMIL" };
|
|
5020
5301
|
var BLESSED_AUDIO_TYPES = /* @__PURE__ */ new Set(["audio/mpeg", "audio/mp4"]);
|
|
@@ -5249,34 +5530,16 @@ var SMILValidator = class {
|
|
|
5249
5530
|
if (!baseDir) return decoded.normalize("NFC");
|
|
5250
5531
|
const segments = `${baseDir}/${decoded}`.split("/");
|
|
5251
5532
|
const resolved = [];
|
|
5252
|
-
for (const seg of segments) {
|
|
5253
|
-
if (seg === "..") {
|
|
5254
|
-
resolved.pop();
|
|
5255
|
-
} else if (seg !== ".") {
|
|
5256
|
-
resolved.push(seg);
|
|
5257
|
-
}
|
|
5258
|
-
}
|
|
5259
|
-
return resolved.join("/").normalize("NFC");
|
|
5260
|
-
}
|
|
5261
|
-
};
|
|
5262
|
-
|
|
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
|
-
}
|
|
5533
|
+
for (const seg of segments) {
|
|
5534
|
+
if (seg === "..") {
|
|
5535
|
+
resolved.pop();
|
|
5536
|
+
} else if (seg !== ".") {
|
|
5537
|
+
resolved.push(seg);
|
|
5538
|
+
}
|
|
5539
|
+
}
|
|
5540
|
+
return resolved.join("/").normalize("NFC");
|
|
5541
|
+
}
|
|
5542
|
+
};
|
|
5280
5543
|
|
|
5281
5544
|
// src/references/uri-schemes.ts
|
|
5282
5545
|
var URI_SCHEMES = /* @__PURE__ */ new Set([
|
|
@@ -5367,7 +5630,9 @@ var ABSOLUTE_URI_RE = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
|
|
|
5367
5630
|
var SPECIAL_URL_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "ftp", "ws", "wss"]);
|
|
5368
5631
|
var CSS_CHARSET_RE = /^@charset\s+"([^"]+)"\s*;/;
|
|
5369
5632
|
var EPUB_XMLNS_RE = /xmlns:epub\s*=\s*"([^"]*)"/;
|
|
5370
|
-
var
|
|
5633
|
+
var XHTML_NS_URI = "http://www.w3.org/1999/xhtml";
|
|
5634
|
+
var XML_NS_URI = "http://www.w3.org/XML/1998/namespace";
|
|
5635
|
+
var XHTML_NS = { html: XHTML_NS_URI };
|
|
5371
5636
|
var EPUB_OPS_NS = { epub: "http://www.idpf.org/2007/ops" };
|
|
5372
5637
|
var EPUB_TYPE_FORBIDDEN_ELEMENTS = /* @__PURE__ */ new Set([
|
|
5373
5638
|
"head",
|
|
@@ -5651,6 +5916,112 @@ var HTML5_ELEMENTS = /* @__PURE__ */ new Set([
|
|
|
5651
5916
|
"video",
|
|
5652
5917
|
"wbr"
|
|
5653
5918
|
]);
|
|
5919
|
+
var XHTML11_ELEMENTS = /* @__PURE__ */ new Set([
|
|
5920
|
+
// struct
|
|
5921
|
+
"html",
|
|
5922
|
+
"head",
|
|
5923
|
+
"title",
|
|
5924
|
+
"body",
|
|
5925
|
+
"meta",
|
|
5926
|
+
"link",
|
|
5927
|
+
"base",
|
|
5928
|
+
"style",
|
|
5929
|
+
"script",
|
|
5930
|
+
"noscript",
|
|
5931
|
+
// text
|
|
5932
|
+
"br",
|
|
5933
|
+
"span",
|
|
5934
|
+
"abbr",
|
|
5935
|
+
"acronym",
|
|
5936
|
+
"cite",
|
|
5937
|
+
"code",
|
|
5938
|
+
"dfn",
|
|
5939
|
+
"em",
|
|
5940
|
+
"kbd",
|
|
5941
|
+
"q",
|
|
5942
|
+
"samp",
|
|
5943
|
+
"strong",
|
|
5944
|
+
"var",
|
|
5945
|
+
"div",
|
|
5946
|
+
"p",
|
|
5947
|
+
"address",
|
|
5948
|
+
"blockquote",
|
|
5949
|
+
"pre",
|
|
5950
|
+
"h1",
|
|
5951
|
+
"h2",
|
|
5952
|
+
"h3",
|
|
5953
|
+
"h4",
|
|
5954
|
+
"h5",
|
|
5955
|
+
"h6",
|
|
5956
|
+
// pres / legacy
|
|
5957
|
+
"hr",
|
|
5958
|
+
"b",
|
|
5959
|
+
"big",
|
|
5960
|
+
"i",
|
|
5961
|
+
"small",
|
|
5962
|
+
"sub",
|
|
5963
|
+
"sup",
|
|
5964
|
+
"tt",
|
|
5965
|
+
"basefont",
|
|
5966
|
+
"center",
|
|
5967
|
+
"font",
|
|
5968
|
+
"s",
|
|
5969
|
+
"strike",
|
|
5970
|
+
"u",
|
|
5971
|
+
"dir",
|
|
5972
|
+
"menu",
|
|
5973
|
+
"isindex",
|
|
5974
|
+
// list
|
|
5975
|
+
"dl",
|
|
5976
|
+
"dt",
|
|
5977
|
+
"dd",
|
|
5978
|
+
"ol",
|
|
5979
|
+
"ul",
|
|
5980
|
+
"li",
|
|
5981
|
+
// table
|
|
5982
|
+
"table",
|
|
5983
|
+
"caption",
|
|
5984
|
+
"tr",
|
|
5985
|
+
"th",
|
|
5986
|
+
"td",
|
|
5987
|
+
"col",
|
|
5988
|
+
"colgroup",
|
|
5989
|
+
"tbody",
|
|
5990
|
+
"thead",
|
|
5991
|
+
"tfoot",
|
|
5992
|
+
// hypertext / image / object / form / edit / ruby / map / iframe / applet / bdo / param
|
|
5993
|
+
"a",
|
|
5994
|
+
"img",
|
|
5995
|
+
"object",
|
|
5996
|
+
"param",
|
|
5997
|
+
"form",
|
|
5998
|
+
"label",
|
|
5999
|
+
"input",
|
|
6000
|
+
"select",
|
|
6001
|
+
"option",
|
|
6002
|
+
"optgroup",
|
|
6003
|
+
"fieldset",
|
|
6004
|
+
"button",
|
|
6005
|
+
"legend",
|
|
6006
|
+
"textarea",
|
|
6007
|
+
"ins",
|
|
6008
|
+
"del",
|
|
6009
|
+
"ruby",
|
|
6010
|
+
"rbc",
|
|
6011
|
+
"rtc",
|
|
6012
|
+
"rb",
|
|
6013
|
+
"rt",
|
|
6014
|
+
"rp",
|
|
6015
|
+
"map",
|
|
6016
|
+
"area",
|
|
6017
|
+
"iframe",
|
|
6018
|
+
"applet",
|
|
6019
|
+
"bdo",
|
|
6020
|
+
// frames
|
|
6021
|
+
"frameset",
|
|
6022
|
+
"frame",
|
|
6023
|
+
"noframes"
|
|
6024
|
+
]);
|
|
5654
6025
|
function isItemFixedLayout(packageDoc, itemId) {
|
|
5655
6026
|
const spineItem = packageDoc.spine.find((s) => s.idref === itemId);
|
|
5656
6027
|
if (!spineItem) return false;
|
|
@@ -5717,6 +6088,10 @@ var ContentValidator = class {
|
|
|
5717
6088
|
if (refValidator) {
|
|
5718
6089
|
this.extractSVGReferences(context, fullPath, opfDir, refValidator);
|
|
5719
6090
|
}
|
|
6091
|
+
} else if (item.mediaType === "application/vnd.epub.search-key-map+xml") {
|
|
6092
|
+
const fullPath = resolveManifestHref(opfDir, item.href);
|
|
6093
|
+
const skmValidator = new SKMValidator();
|
|
6094
|
+
skmValidator.validate(context, fullPath, refValidator);
|
|
5720
6095
|
} else if (item.mediaType === "application/smil+xml") {
|
|
5721
6096
|
const fullPath = resolveManifestHref(opfDir, item.href);
|
|
5722
6097
|
const smilValidator = new SMILValidator();
|
|
@@ -6479,6 +6854,9 @@ var ContentValidator = class {
|
|
|
6479
6854
|
}
|
|
6480
6855
|
}
|
|
6481
6856
|
}
|
|
6857
|
+
if (context.version === "2.0") {
|
|
6858
|
+
this.checkEpub2XhtmlStrict(context, path, root);
|
|
6859
|
+
}
|
|
6482
6860
|
this.checkDiscouragedElements(context, path, root);
|
|
6483
6861
|
this.checkSSMLPh(context, path, root, content);
|
|
6484
6862
|
this.checkObsoleteHTML(context, path, root);
|
|
@@ -6509,6 +6887,9 @@ var ContentValidator = class {
|
|
|
6509
6887
|
this.validateEpubTypes(context, path, root);
|
|
6510
6888
|
this.validateRegionBasedNav(context, path, root, manifestItem);
|
|
6511
6889
|
}
|
|
6890
|
+
if (context.version.startsWith("3") && context.options.profile === "dict") {
|
|
6891
|
+
this.validateDictionaryContent(context, path, root);
|
|
6892
|
+
}
|
|
6512
6893
|
if (context.version.startsWith("3") && context.options.profile === "edupub") {
|
|
6513
6894
|
const isFxl = manifestItem && packageDoc ? isItemFixedLayout(packageDoc, manifestItem.id) : false;
|
|
6514
6895
|
const isNonLinear = manifestItem && packageDoc ? packageDoc.spine.find((ref) => ref.idref === manifestItem.id)?.linear === false : false;
|
|
@@ -6517,7 +6898,7 @@ var ContentValidator = class {
|
|
|
6517
6898
|
}
|
|
6518
6899
|
}
|
|
6519
6900
|
if (context.version.startsWith("3")) {
|
|
6520
|
-
this.collectFeatures(context, root);
|
|
6901
|
+
this.collectFeatures(context, path, root);
|
|
6521
6902
|
}
|
|
6522
6903
|
this.validateEpubSwitch(context, path, root);
|
|
6523
6904
|
this.validateEpubTrigger(context, path, root);
|
|
@@ -6955,6 +7336,9 @@ var ContentValidator = class {
|
|
|
6955
7336
|
const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
|
|
6956
7337
|
const opfDir = context.opfPath?.includes("/") ? context.opfPath.substring(0, context.opfPath.lastIndexOf("/")) : "";
|
|
6957
7338
|
const tocAnchors = tocNav.find(".//html:a[@href]", HTML_NS);
|
|
7339
|
+
if (context.contentFeatures) {
|
|
7340
|
+
context.contentFeatures.tocLinkCount = (context.contentFeatures.tocLinkCount ?? 0) + tocAnchors.length;
|
|
7341
|
+
}
|
|
6958
7342
|
const tocLinks = [];
|
|
6959
7343
|
for (const anchor of tocAnchors) {
|
|
6960
7344
|
const href = this.getAttribute(anchor, "href")?.trim();
|
|
@@ -8040,7 +8424,7 @@ var ContentValidator = class {
|
|
|
8040
8424
|
}
|
|
8041
8425
|
}
|
|
8042
8426
|
}
|
|
8043
|
-
collectFeatures(context, root) {
|
|
8427
|
+
collectFeatures(context, path, root) {
|
|
8044
8428
|
const features = context.contentFeatures;
|
|
8045
8429
|
if (!features) return;
|
|
8046
8430
|
if (!features.hasTable && root.get(".//html:table", XHTML_NS)) {
|
|
@@ -8055,22 +8439,20 @@ var ContentValidator = class {
|
|
|
8055
8439
|
if (!features.hasVideo && root.get(".//html:video", XHTML_NS)) {
|
|
8056
8440
|
features.hasVideo = true;
|
|
8057
8441
|
}
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
}
|
|
8073
|
-
if (features.hasPageBreak && features.hasDictionary && features.hasIndex) break;
|
|
8442
|
+
const epubTypeElements = root.find(".//*[@epub:type]", EPUB_OPS_NS);
|
|
8443
|
+
for (const el of epubTypeElements) {
|
|
8444
|
+
const attr = el.attr("type", "epub");
|
|
8445
|
+
if (!attr?.value) continue;
|
|
8446
|
+
const tokens = attr.value.trim().split(/\s+/);
|
|
8447
|
+
if (!features.hasPageBreak && tokens.includes("pagebreak")) {
|
|
8448
|
+
features.hasPageBreak = true;
|
|
8449
|
+
}
|
|
8450
|
+
if (tokens.includes("dictionary")) {
|
|
8451
|
+
features.hasDictionary = true;
|
|
8452
|
+
(features.dictionaryContentPaths ??= /* @__PURE__ */ new Set()).add(path);
|
|
8453
|
+
}
|
|
8454
|
+
if (!features.hasIndex && tokens.includes("index")) {
|
|
8455
|
+
features.hasIndex = true;
|
|
8074
8456
|
}
|
|
8075
8457
|
}
|
|
8076
8458
|
if (!features.hasMicrodata && root.get(".//*[@itemscope]")) {
|
|
@@ -8079,6 +8461,10 @@ var ContentValidator = class {
|
|
|
8079
8461
|
if (!features.hasRDFa && root.get(".//*[@property]")) {
|
|
8080
8462
|
features.hasRDFa = true;
|
|
8081
8463
|
}
|
|
8464
|
+
if (context.options.profile === "edupub") {
|
|
8465
|
+
const sections = root.find(".//html:body//html:section", XHTML_NS);
|
|
8466
|
+
features.sectionCount = (features.sectionCount ?? 0) + sections.length;
|
|
8467
|
+
}
|
|
8082
8468
|
}
|
|
8083
8469
|
validateImages(context, path, root) {
|
|
8084
8470
|
const packageDoc = context.packageDocument;
|
|
@@ -8171,6 +8557,158 @@ var ContentValidator = class {
|
|
|
8171
8557
|
});
|
|
8172
8558
|
}
|
|
8173
8559
|
}
|
|
8560
|
+
this.validateRegionBasedNavRules(context, path, root);
|
|
8561
|
+
}
|
|
8562
|
+
}
|
|
8563
|
+
validateRegionBasedNavRules(context, path, root) {
|
|
8564
|
+
const XHTML_NS2 = { html: "http://www.w3.org/1999/xhtml" };
|
|
8565
|
+
let regionNavs;
|
|
8566
|
+
try {
|
|
8567
|
+
regionNavs = root.find(".//html:nav", XHTML_NS2);
|
|
8568
|
+
} catch {
|
|
8569
|
+
return;
|
|
8570
|
+
}
|
|
8571
|
+
const packageDoc = context.packageDocument;
|
|
8572
|
+
const opfDir = context.opfPath?.includes("/") ? context.opfPath.substring(0, context.opfPath.lastIndexOf("/")) : "";
|
|
8573
|
+
const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
|
|
8574
|
+
const manifestByPath = packageDoc ? new Map(packageDoc.manifest.map((m) => [resolveManifestHref(opfDir, m.href), m])) : void 0;
|
|
8575
|
+
for (const nav of regionNavs) {
|
|
8576
|
+
const epubType = nav.attr("type", "epub")?.value ?? "";
|
|
8577
|
+
if (!epubType.split(/\s+/).includes("region-based")) continue;
|
|
8578
|
+
const childEls = nav.find("./html:*", XHTML_NS2);
|
|
8579
|
+
if (childEls.length !== 1 || childEls[0]?.name !== "ol") {
|
|
8580
|
+
pushMessage(context.messages, {
|
|
8581
|
+
id: MessageId.RSC_017,
|
|
8582
|
+
message: "A region-based nav element must contain exactly one child ol element.",
|
|
8583
|
+
location: { path, line: nav.line }
|
|
8584
|
+
});
|
|
8585
|
+
}
|
|
8586
|
+
const liElements = nav.find(".//html:li", XHTML_NS2);
|
|
8587
|
+
for (const li of liElements) {
|
|
8588
|
+
const liChildren = li.find("./html:*", XHTML_NS2);
|
|
8589
|
+
const first = liChildren[0];
|
|
8590
|
+
if (!first || first.name !== "a" && first.name !== "span") {
|
|
8591
|
+
pushMessage(context.messages, {
|
|
8592
|
+
id: MessageId.RSC_017,
|
|
8593
|
+
message: "The first child of a region-based nav list item must be either an 'a' or 'span' element.",
|
|
8594
|
+
location: { path, line: li.line }
|
|
8595
|
+
});
|
|
8596
|
+
}
|
|
8597
|
+
if (liChildren.length > 1 && (liChildren.length !== 2 || liChildren[1]?.name !== "ol")) {
|
|
8598
|
+
pushMessage(context.messages, {
|
|
8599
|
+
id: MessageId.RSC_017,
|
|
8600
|
+
message: "The first child of a region-based nav list item can only be followed by a single 'ol' element.",
|
|
8601
|
+
location: { path, line: li.line }
|
|
8602
|
+
});
|
|
8603
|
+
}
|
|
8604
|
+
}
|
|
8605
|
+
const spans = nav.find(".//html:span", XHTML_NS2);
|
|
8606
|
+
for (const span of spans) {
|
|
8607
|
+
const spanChildren = span.find("./html:*", XHTML_NS2);
|
|
8608
|
+
const aChildren = spanChildren.filter((c) => c.name === "a");
|
|
8609
|
+
if (spanChildren.length !== 2 || aChildren.length !== 2) {
|
|
8610
|
+
pushMessage(context.messages, {
|
|
8611
|
+
id: MessageId.RSC_017,
|
|
8612
|
+
message: "'span' elements in region-based navs must contain exactly two 'a' elements.",
|
|
8613
|
+
location: { path, line: span.line }
|
|
8614
|
+
});
|
|
8615
|
+
}
|
|
8616
|
+
}
|
|
8617
|
+
const anchors = nav.find(".//html:a", XHTML_NS2);
|
|
8618
|
+
for (const a of anchors) {
|
|
8619
|
+
if (a.content.trim() !== "") {
|
|
8620
|
+
pushMessage(context.messages, {
|
|
8621
|
+
id: MessageId.RSC_017,
|
|
8622
|
+
message: "'a' elements in region-based navs should not contain text labels.",
|
|
8623
|
+
location: { path, line: a.line }
|
|
8624
|
+
});
|
|
8625
|
+
}
|
|
8626
|
+
}
|
|
8627
|
+
if (!packageDoc || !manifestByPath) continue;
|
|
8628
|
+
for (const a of anchors) {
|
|
8629
|
+
const href = a.attr("href")?.value;
|
|
8630
|
+
if (!href || !isRelativeURL(href)) continue;
|
|
8631
|
+
const resolved = this.resolveRelativePath(docDir, href, opfDir);
|
|
8632
|
+
const targetPath = parseURL(resolved).resource;
|
|
8633
|
+
if (!targetPath) continue;
|
|
8634
|
+
const item = manifestByPath.get(targetPath);
|
|
8635
|
+
if (!item) continue;
|
|
8636
|
+
if (!isItemFixedLayout(packageDoc, item.id)) {
|
|
8637
|
+
pushMessage(context.messages, {
|
|
8638
|
+
id: MessageId.NAV_009,
|
|
8639
|
+
message: "Region-based navigation links must point to Fixed-Layout Documents.",
|
|
8640
|
+
location: { path, line: a.line }
|
|
8641
|
+
});
|
|
8642
|
+
}
|
|
8643
|
+
}
|
|
8644
|
+
}
|
|
8645
|
+
}
|
|
8646
|
+
/**
|
|
8647
|
+
* EPUB Dictionaries content document rules.
|
|
8648
|
+
*
|
|
8649
|
+
* Mirrors ../epubcheck/src/main/resources/com/adobe/epubcheck/schema/30/dict/dict-xhtml.sch
|
|
8650
|
+
* (minimum set: `dictionary` must be on body/section with article children; each article or
|
|
8651
|
+
* `dictentry` must have a `dfn` descendant outside of optional `condensed-entry`).
|
|
8652
|
+
*/
|
|
8653
|
+
validateDictionaryContent(context, path, root) {
|
|
8654
|
+
let typedElements;
|
|
8655
|
+
try {
|
|
8656
|
+
typedElements = root.find(".//*[@epub:type]", EPUB_OPS_NS);
|
|
8657
|
+
} catch {
|
|
8658
|
+
return;
|
|
8659
|
+
}
|
|
8660
|
+
for (const el of typedElements) {
|
|
8661
|
+
const tokens = el.attr("type", "epub")?.value.split(/\s+/) ?? [];
|
|
8662
|
+
if (tokens.includes("dictionary")) {
|
|
8663
|
+
if (el.name !== "body" && el.name !== "section") {
|
|
8664
|
+
pushMessage(context.messages, {
|
|
8665
|
+
id: MessageId.RSC_005,
|
|
8666
|
+
message: 'The "dictionary" type is only allowed on "body" or "section" elements.',
|
|
8667
|
+
location: { path, line: el.line }
|
|
8668
|
+
});
|
|
8669
|
+
}
|
|
8670
|
+
const articles = el.find("./html:article", XHTML_NS);
|
|
8671
|
+
if (articles.length === 0) {
|
|
8672
|
+
pushMessage(context.messages, {
|
|
8673
|
+
id: MessageId.RSC_005,
|
|
8674
|
+
message: 'A "dictionary" must have at least one article child.',
|
|
8675
|
+
location: { path, line: el.line }
|
|
8676
|
+
});
|
|
8677
|
+
}
|
|
8678
|
+
for (const article of articles) {
|
|
8679
|
+
this.checkDictionaryEntry(context, path, article);
|
|
8680
|
+
}
|
|
8681
|
+
}
|
|
8682
|
+
if (tokens.includes("dictentry")) {
|
|
8683
|
+
if (el.name !== "article") {
|
|
8684
|
+
pushMessage(context.messages, {
|
|
8685
|
+
id: MessageId.RSC_005,
|
|
8686
|
+
message: 'The "dictentry" type is only allowed on "article" elements.',
|
|
8687
|
+
location: { path, line: el.line }
|
|
8688
|
+
});
|
|
8689
|
+
} else {
|
|
8690
|
+
this.checkDictionaryEntry(context, path, el);
|
|
8691
|
+
}
|
|
8692
|
+
}
|
|
8693
|
+
}
|
|
8694
|
+
}
|
|
8695
|
+
checkDictionaryEntry(context, path, article) {
|
|
8696
|
+
const dfns = article.find(".//html:dfn", XHTML_NS);
|
|
8697
|
+
const hasDfnOutsideCondensed = dfns.some((dfn) => {
|
|
8698
|
+
let parent = dfn.parent;
|
|
8699
|
+
while (parent) {
|
|
8700
|
+
const type = parent.attr("type", "epub")?.value;
|
|
8701
|
+
if (type?.split(/\s+/).includes("condensed-entry")) return false;
|
|
8702
|
+
parent = parent.parent;
|
|
8703
|
+
}
|
|
8704
|
+
return true;
|
|
8705
|
+
});
|
|
8706
|
+
if (!hasDfnOutsideCondensed) {
|
|
8707
|
+
pushMessage(context.messages, {
|
|
8708
|
+
id: MessageId.RSC_005,
|
|
8709
|
+
message: 'A dictionary entry must have at least one "dfn" descendant (outside of the optional condensed entry "aside").',
|
|
8710
|
+
location: { path, line: article.line }
|
|
8711
|
+
});
|
|
8174
8712
|
}
|
|
8175
8713
|
}
|
|
8176
8714
|
/**
|
|
@@ -9567,13 +10105,11 @@ var ContentValidator = class {
|
|
|
9567
10105
|
}
|
|
9568
10106
|
}
|
|
9569
10107
|
checkUnknownElements(context, path, root) {
|
|
9570
|
-
const XHTML_NS2 = "http://www.w3.org/1999/xhtml";
|
|
9571
10108
|
try {
|
|
9572
10109
|
const allElements = root.find(".//*");
|
|
9573
10110
|
for (const el of allElements) {
|
|
9574
10111
|
const xmlEl = el;
|
|
9575
|
-
|
|
9576
|
-
if (ns !== XHTML_NS2) continue;
|
|
10112
|
+
if (xmlEl.namespaceUri !== XHTML_NS_URI) continue;
|
|
9577
10113
|
const localName = xmlEl.name.includes(":") ? xmlEl.name.substring(xmlEl.name.indexOf(":") + 1) : xmlEl.name;
|
|
9578
10114
|
if (localName.includes("-")) continue;
|
|
9579
10115
|
if (!HTML5_ELEMENTS.has(localName)) {
|
|
@@ -9587,6 +10123,51 @@ var ContentValidator = class {
|
|
|
9587
10123
|
} catch {
|
|
9588
10124
|
}
|
|
9589
10125
|
}
|
|
10126
|
+
checkEpub2XhtmlStrict(context, path, root) {
|
|
10127
|
+
if (!root.namespaceUri) {
|
|
10128
|
+
pushMessage(context.messages, {
|
|
10129
|
+
id: MessageId.RSC_005,
|
|
10130
|
+
message: `element "${root.name}" from namespace "" is not allowed`,
|
|
10131
|
+
location: { path, line: root.line }
|
|
10132
|
+
});
|
|
10133
|
+
return;
|
|
10134
|
+
}
|
|
10135
|
+
const checkElement = (xmlEl) => {
|
|
10136
|
+
if (xmlEl.namespaceUri !== XHTML_NS_URI) return;
|
|
10137
|
+
const localName = xmlEl.name.includes(":") ? xmlEl.name.substring(xmlEl.name.indexOf(":") + 1) : xmlEl.name;
|
|
10138
|
+
if (!XHTML11_ELEMENTS.has(localName)) {
|
|
10139
|
+
pushMessage(context.messages, {
|
|
10140
|
+
id: MessageId.RSC_005,
|
|
10141
|
+
message: `element "${localName}" not allowed here`,
|
|
10142
|
+
location: { path, line: xmlEl.line }
|
|
10143
|
+
});
|
|
10144
|
+
}
|
|
10145
|
+
for (const attr of xmlEl.attrs) {
|
|
10146
|
+
const ns = attr.namespaceUri;
|
|
10147
|
+
if (!ns || ns === XHTML_NS_URI || ns === XML_NS_URI) continue;
|
|
10148
|
+
const qname = attr.prefix ? `${attr.prefix}:${attr.name}` : attr.name;
|
|
10149
|
+
pushMessage(context.messages, {
|
|
10150
|
+
id: MessageId.RSC_005,
|
|
10151
|
+
message: `attribute "${qname}" not allowed here`,
|
|
10152
|
+
location: { path, line: xmlEl.line }
|
|
10153
|
+
});
|
|
10154
|
+
}
|
|
10155
|
+
};
|
|
10156
|
+
checkElement(root);
|
|
10157
|
+
try {
|
|
10158
|
+
for (const el of root.find(".//*")) {
|
|
10159
|
+
checkElement(el);
|
|
10160
|
+
}
|
|
10161
|
+
for (const a of root.find(".//html:a//html:a", XHTML_NS)) {
|
|
10162
|
+
pushMessage(context.messages, {
|
|
10163
|
+
id: MessageId.RSC_005,
|
|
10164
|
+
message: 'The "a" element cannot contain any nested "a" elements',
|
|
10165
|
+
location: { path, line: a.line }
|
|
10166
|
+
});
|
|
10167
|
+
}
|
|
10168
|
+
} catch {
|
|
10169
|
+
}
|
|
10170
|
+
}
|
|
9590
10171
|
checkForeignObjectContent(context, path, root, isSVGDoc) {
|
|
9591
10172
|
const SVG_NS = { svg: "http://www.w3.org/2000/svg" };
|
|
9592
10173
|
const XHTML_URI = "http://www.w3.org/1999/xhtml";
|
|
@@ -9795,6 +10376,7 @@ var NCXValidator = class {
|
|
|
9795
10376
|
this.checkContentSrc(context, root, ncxPath, registry);
|
|
9796
10377
|
this.checkEmptyLabels(context, root, ncxPath);
|
|
9797
10378
|
this.checkPageTargets(context, root, ncxPath);
|
|
10379
|
+
this.checkIdSyntax(context, root, ncxPath);
|
|
9798
10380
|
} finally {
|
|
9799
10381
|
doc.dispose();
|
|
9800
10382
|
}
|
|
@@ -9818,6 +10400,13 @@ var NCXValidator = class {
|
|
|
9818
10400
|
});
|
|
9819
10401
|
return;
|
|
9820
10402
|
}
|
|
10403
|
+
if (uidContent !== uidContent.trim()) {
|
|
10404
|
+
pushMessage(context.messages, {
|
|
10405
|
+
id: MessageId.NCX_004,
|
|
10406
|
+
message: "NCX dtb:uid meta content has leading or trailing whitespace.",
|
|
10407
|
+
location: { path, line: uidElement.line }
|
|
10408
|
+
});
|
|
10409
|
+
}
|
|
9821
10410
|
context.ncxUid = uidContent.trim();
|
|
9822
10411
|
}
|
|
9823
10412
|
checkNavMap(context, root, path) {
|
|
@@ -9898,10 +10487,21 @@ var NCXValidator = class {
|
|
|
9898
10487
|
}
|
|
9899
10488
|
}
|
|
9900
10489
|
}
|
|
9901
|
-
|
|
9902
|
-
|
|
9903
|
-
|
|
9904
|
-
|
|
10490
|
+
checkIdSyntax(context, root, ncxPath) {
|
|
10491
|
+
const nodes = root.find(".//*[@id]", {
|
|
10492
|
+
ncx: "http://www.daisy.org/z3986/2005/ncx/"
|
|
10493
|
+
});
|
|
10494
|
+
for (const node of nodes) {
|
|
10495
|
+
const idValue = node.attr("id")?.value;
|
|
10496
|
+
if (idValue && !NC_NAME_REGEX.test(idValue)) {
|
|
10497
|
+
pushMessage(context.messages, {
|
|
10498
|
+
id: MessageId.RSC_005,
|
|
10499
|
+
message: `Invalid id "${idValue}"; must match xs:ID syntax (NCName)`,
|
|
10500
|
+
location: { path: ncxPath, line: node.line }
|
|
10501
|
+
});
|
|
10502
|
+
}
|
|
10503
|
+
}
|
|
10504
|
+
}
|
|
9905
10505
|
checkPageTargets(context, root, ncxPath) {
|
|
9906
10506
|
const pageTargets = root.find(".//ncx:pageTarget[@type]", {
|
|
9907
10507
|
ncx: "http://www.daisy.org/z3986/2005/ncx/"
|
|
@@ -9919,6 +10519,7 @@ var NCXValidator = class {
|
|
|
9919
10519
|
}
|
|
9920
10520
|
}
|
|
9921
10521
|
};
|
|
10522
|
+
var NC_NAME_REGEX = /^[A-Za-z_][A-Za-z0-9._-]*$/;
|
|
9922
10523
|
var PAGE_TARGET_TYPES = /* @__PURE__ */ new Set(["front", "normal", "special"]);
|
|
9923
10524
|
var OPS_MEDIA_TYPES = /* @__PURE__ */ new Set([
|
|
9924
10525
|
"application/xhtml+xml",
|
|
@@ -10048,15 +10649,6 @@ function parseContainerContent(content, context, fileExists, getFileContent) {
|
|
|
10048
10649
|
}
|
|
10049
10650
|
}
|
|
10050
10651
|
}
|
|
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
10652
|
}
|
|
10061
10653
|
function validateMappingDocumentContent(xml, mappingPath, context) {
|
|
10062
10654
|
const stripped = stripXmlComments(xml);
|
|
@@ -10444,8 +11036,8 @@ var OCFValidator = class {
|
|
|
10444
11036
|
zip = ZipReader.open(context.data);
|
|
10445
11037
|
} catch (error) {
|
|
10446
11038
|
pushMessage(context.messages, {
|
|
10447
|
-
id: MessageId.
|
|
10448
|
-
message: `Failed to open EPUB
|
|
11039
|
+
id: MessageId.PKG_004,
|
|
11040
|
+
message: `Failed to open EPUB ZIP: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
10449
11041
|
});
|
|
10450
11042
|
return;
|
|
10451
11043
|
}
|
|
@@ -10479,8 +11071,8 @@ var OCFValidator = class {
|
|
|
10479
11071
|
const compressionInfo = zip.getMimetypeCompressionInfo();
|
|
10480
11072
|
if (compressionInfo === null) {
|
|
10481
11073
|
pushMessage(messages, {
|
|
10482
|
-
id: MessageId.
|
|
10483
|
-
message: "
|
|
11074
|
+
id: MessageId.PKG_003,
|
|
11075
|
+
message: "Unable to read EPUB file header, likely corrupted",
|
|
10484
11076
|
location: { path: "mimetype" }
|
|
10485
11077
|
});
|
|
10486
11078
|
return;
|
|
@@ -10975,19 +11567,7 @@ var ReferenceValidator = class {
|
|
|
10975
11567
|
message: "Absolute paths are not allowed in EPUB",
|
|
10976
11568
|
location: reference.location
|
|
10977
11569
|
});
|
|
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)) {
|
|
11570
|
+
} else if (checkUrlLeaking(reference.url, reference.location.path)) {
|
|
10991
11571
|
pushMessage(context.messages, {
|
|
10992
11572
|
id: MessageId.RSC_026,
|
|
10993
11573
|
message: `URL "${reference.url}" leaks outside the container`,
|
|
@@ -11025,6 +11605,16 @@ var ReferenceValidator = class {
|
|
|
11025
11605
|
location: reference.location
|
|
11026
11606
|
});
|
|
11027
11607
|
}
|
|
11608
|
+
if (reference.type === "search-key" /* SEARCH_KEY */ && !resource?.inSpine) {
|
|
11609
|
+
const isEpubCfi = reference.fragment?.startsWith("epubcfi(") ?? false;
|
|
11610
|
+
if (!isEpubCfi) {
|
|
11611
|
+
pushMessage(context.messages, {
|
|
11612
|
+
id: MessageId.RSC_021,
|
|
11613
|
+
message: `Search Key Map target "${resourcePath}" must be a Content Document in the spine`,
|
|
11614
|
+
location: reference.location
|
|
11615
|
+
});
|
|
11616
|
+
}
|
|
11617
|
+
}
|
|
11028
11618
|
if (isHyperlinkLike || reference.type === "overlay-text-link" /* OVERLAY_TEXT_LINK */) {
|
|
11029
11619
|
const targetMimeType = resource?.mimeType;
|
|
11030
11620
|
if (targetMimeType && !this.isBlessedItemType(targetMimeType, context.version) && !this.isDeprecatedBlessedItemType(targetMimeType) && !resource.hasCoreMediaTypeFallback) {
|
|
@@ -11509,11 +12099,11 @@ var RelaxNGValidator = class extends BaseSchemaValidator {
|
|
|
11509
12099
|
try {
|
|
11510
12100
|
const libxml2 = await import('libxml2-wasm');
|
|
11511
12101
|
const LibRelaxNGValidator = libxml2.RelaxNGValidator;
|
|
11512
|
-
const { XmlDocument:
|
|
11513
|
-
const doc =
|
|
12102
|
+
const { XmlDocument: XmlDocument5 } = libxml2;
|
|
12103
|
+
const doc = XmlDocument5.fromString(xml);
|
|
11514
12104
|
try {
|
|
11515
12105
|
const schemaContent = await loadSchema(schemaPath);
|
|
11516
|
-
const schemaDoc =
|
|
12106
|
+
const schemaDoc = XmlDocument5.fromString(schemaContent);
|
|
11517
12107
|
try {
|
|
11518
12108
|
const validator = LibRelaxNGValidator.fromDoc(schemaDoc);
|
|
11519
12109
|
try {
|
|
@@ -11747,7 +12337,7 @@ var EpubCheck = class _EpubCheck {
|
|
|
11747
12337
|
await this.runPipeline(context);
|
|
11748
12338
|
} catch (error) {
|
|
11749
12339
|
pushMessage(context.messages, {
|
|
11750
|
-
id: MessageId.
|
|
12340
|
+
id: MessageId.PKG_008,
|
|
11751
12341
|
message: error instanceof Error ? error.message : "Unknown validation error"
|
|
11752
12342
|
});
|
|
11753
12343
|
} finally {
|
|
@@ -11789,7 +12379,7 @@ var EpubCheck = class _EpubCheck {
|
|
|
11789
12379
|
await this.runPipeline(context);
|
|
11790
12380
|
} catch (error) {
|
|
11791
12381
|
pushMessage(context.messages, {
|
|
11792
|
-
id: MessageId.
|
|
12382
|
+
id: MessageId.PKG_008,
|
|
11793
12383
|
message: error instanceof Error ? error.message : "Unknown validation error"
|
|
11794
12384
|
});
|
|
11795
12385
|
} finally {
|
|
@@ -11856,7 +12446,7 @@ var EpubCheck = class _EpubCheck {
|
|
|
11856
12446
|
}
|
|
11857
12447
|
} catch (error) {
|
|
11858
12448
|
pushMessage(context.messages, {
|
|
11859
|
-
id: MessageId.
|
|
12449
|
+
id: MessageId.PKG_008,
|
|
11860
12450
|
message: error instanceof Error ? error.message : "Unknown validation error"
|
|
11861
12451
|
});
|
|
11862
12452
|
} finally {
|
|
@@ -11906,6 +12496,15 @@ var EpubCheck = class _EpubCheck {
|
|
|
11906
12496
|
const profile = context.options.profile;
|
|
11907
12497
|
const opfPath = context.opfPath ?? "";
|
|
11908
12498
|
if (profile === "edupub") {
|
|
12499
|
+
const sectionCount = features.sectionCount ?? 0;
|
|
12500
|
+
const tocLinkCount = features.tocLinkCount ?? 0;
|
|
12501
|
+
if (sectionCount > 0 && sectionCount !== tocLinkCount) {
|
|
12502
|
+
pushMessage(context.messages, {
|
|
12503
|
+
id: MessageId.NAV_004,
|
|
12504
|
+
message: "The Navigation Document should contain the full hierarchy of headings in the document for EDUPUB.",
|
|
12505
|
+
location: { path: opfPath }
|
|
12506
|
+
});
|
|
12507
|
+
}
|
|
11909
12508
|
if (features.hasPageBreak && !features.hasPageList) {
|
|
11910
12509
|
pushMessage(context.messages, {
|
|
11911
12510
|
id: MessageId.NAV_003,
|
|
@@ -11977,6 +12576,73 @@ var EpubCheck = class _EpubCheck {
|
|
|
11977
12576
|
location: { path: opfPath }
|
|
11978
12577
|
});
|
|
11979
12578
|
}
|
|
12579
|
+
if (profile === "dict" && context.packageDocument) {
|
|
12580
|
+
const opfDir = opfPath.includes("/") ? opfPath.substring(0, opfPath.lastIndexOf("/")) : "";
|
|
12581
|
+
const manifestByPath = /* @__PURE__ */ new Map();
|
|
12582
|
+
for (const item of context.packageDocument.manifest) {
|
|
12583
|
+
manifestByPath.set(resolveManifestHref(opfDir, item.href), item);
|
|
12584
|
+
}
|
|
12585
|
+
const dictPaths = features.dictionaryContentPaths ?? /* @__PURE__ */ new Set();
|
|
12586
|
+
for (const coll of context.packageDocument.collections) {
|
|
12587
|
+
if (coll.role !== "dictionary") continue;
|
|
12588
|
+
const hasDictContent = coll.links.some((href) => {
|
|
12589
|
+
const full = resolveManifestHref(opfDir, href);
|
|
12590
|
+
const item = manifestByPath.get(full);
|
|
12591
|
+
return item?.mediaType === "application/xhtml+xml" && dictPaths.has(full);
|
|
12592
|
+
});
|
|
12593
|
+
if (!hasDictContent) {
|
|
12594
|
+
pushMessage(context.messages, {
|
|
12595
|
+
id: MessageId.OPF_078,
|
|
12596
|
+
message: 'An EPUB Dictionary must contain at least one Content Document with dictionary content (epub:type "dictionary")',
|
|
12597
|
+
location: { path: opfPath }
|
|
12598
|
+
});
|
|
12599
|
+
}
|
|
12600
|
+
}
|
|
12601
|
+
}
|
|
12602
|
+
}
|
|
12603
|
+
/**
|
|
12604
|
+
* Mirrors ../epubcheck/src/main/resources/com/adobe/epubcheck/schema/30/edupub/
|
|
12605
|
+
* edu-ocf-metadata.sch (META-INF/metadata.xml) and edu-opf.sch (each OPF).
|
|
12606
|
+
*
|
|
12607
|
+
* Non-primary OPFs are otherwise unreached: runPipeline only runs OPFValidator
|
|
12608
|
+
* on context.opfPath.
|
|
12609
|
+
*/
|
|
12610
|
+
validateEdupubMultiRendition(context) {
|
|
12611
|
+
if (context.options.profile !== "edupub") return;
|
|
12612
|
+
if (context.rootfiles.length <= 1) return;
|
|
12613
|
+
const metadataPath = "META-INF/metadata.xml";
|
|
12614
|
+
const metadataData = context.files.get(metadataPath);
|
|
12615
|
+
if (metadataData) {
|
|
12616
|
+
const content = typeof metadataData === "string" ? metadataData : new TextDecoder().decode(metadataData);
|
|
12617
|
+
const stripped = stripXmlComments(content);
|
|
12618
|
+
if (!/<dc:type\b[^>]*>\s*edupub\s*<\/dc:type>/i.test(stripped)) {
|
|
12619
|
+
pushMessage(context.messages, {
|
|
12620
|
+
id: MessageId.RSC_005,
|
|
12621
|
+
message: 'A dc:type element with the value "edupub" is required.',
|
|
12622
|
+
location: { path: metadataPath }
|
|
12623
|
+
});
|
|
12624
|
+
}
|
|
12625
|
+
}
|
|
12626
|
+
const primary = context.opfPath;
|
|
12627
|
+
for (const rootfile of context.rootfiles) {
|
|
12628
|
+
if (rootfile.path === primary) continue;
|
|
12629
|
+
if (rootfile.mediaType !== "application/oebps-package+xml") continue;
|
|
12630
|
+
const path = rootfile.path.normalize("NFC");
|
|
12631
|
+
const opfData = context.files.get(path);
|
|
12632
|
+
if (!opfData) continue;
|
|
12633
|
+
const xml = typeof opfData === "string" ? opfData : new TextDecoder().decode(opfData);
|
|
12634
|
+
const pkg = parseOPF(xml);
|
|
12635
|
+
const hasType = pkg.dcElements.some(
|
|
12636
|
+
(dc) => dc.name === "type" && dc.value.trim() === "edupub"
|
|
12637
|
+
);
|
|
12638
|
+
if (!hasType) {
|
|
12639
|
+
pushMessage(context.messages, {
|
|
12640
|
+
id: MessageId.RSC_005,
|
|
12641
|
+
message: 'The dc:type identifier "edupub" is required.',
|
|
12642
|
+
location: { path }
|
|
12643
|
+
});
|
|
12644
|
+
}
|
|
12645
|
+
}
|
|
11980
12646
|
}
|
|
11981
12647
|
/**
|
|
11982
12648
|
* Validate NCX navigation document (EPUB 2 always, EPUB 3 when NCX present)
|
|
@@ -12122,6 +12788,7 @@ var EpubCheck = class _EpubCheck {
|
|
|
12122
12788
|
const contentValidator = new ContentValidator();
|
|
12123
12789
|
contentValidator.validate(context, registry, refValidator);
|
|
12124
12790
|
this.validateCrossDocumentFeatures(context);
|
|
12791
|
+
this.validateEdupubMultiRendition(context);
|
|
12125
12792
|
if (context.packageDocument) {
|
|
12126
12793
|
this.validateNCX(context, registry);
|
|
12127
12794
|
}
|
|
@@ -12178,7 +12845,14 @@ var EpubCheck = class _EpubCheck {
|
|
|
12178
12845
|
message: "For maximum compatibility, use only lowercase characters for the EPUB file extension.",
|
|
12179
12846
|
location: { path: filename }
|
|
12180
12847
|
});
|
|
12848
|
+
return;
|
|
12181
12849
|
}
|
|
12850
|
+
const isEpub2 = context.version.startsWith("2");
|
|
12851
|
+
pushMessage(context.messages, {
|
|
12852
|
+
id: isEpub2 ? MessageId.PKG_017 : MessageId.PKG_024,
|
|
12853
|
+
message: `EPUB file has an uncommon extension "${extension}".`,
|
|
12854
|
+
location: { path: filename }
|
|
12855
|
+
});
|
|
12182
12856
|
}
|
|
12183
12857
|
/**
|
|
12184
12858
|
* Build a filtered report from validation context
|