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