@likecoin/epubcheck-ts 0.3.3 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/bin/epubcheck.js +2 -4
- package/bin/epubcheck.ts +9 -13
- package/dist/index.cjs +703 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +703 -36
- package/dist/index.js.map +1 -1
- package/package.json +7 -2
package/dist/index.js
CHANGED
|
@@ -1756,9 +1756,14 @@ var ContentValidator = class {
|
|
|
1756
1756
|
} else if (item.mediaType === "text/css" && refValidator) {
|
|
1757
1757
|
const fullPath = opfDir ? `${opfDir}/${item.href}` : item.href;
|
|
1758
1758
|
this.validateCSSDocument(context, fullPath, opfDir, refValidator);
|
|
1759
|
-
} else if (item.mediaType === "image/svg+xml"
|
|
1759
|
+
} else if (item.mediaType === "image/svg+xml") {
|
|
1760
1760
|
const fullPath = opfDir ? `${opfDir}/${item.href}` : item.href;
|
|
1761
|
-
|
|
1761
|
+
if (registry) {
|
|
1762
|
+
this.extractSVGIDs(context, fullPath, registry);
|
|
1763
|
+
}
|
|
1764
|
+
if (context.version.startsWith("3")) {
|
|
1765
|
+
this.validateSVGDocument(context, fullPath, item);
|
|
1766
|
+
}
|
|
1762
1767
|
}
|
|
1763
1768
|
}
|
|
1764
1769
|
}
|
|
@@ -1772,11 +1777,75 @@ var ContentValidator = class {
|
|
|
1772
1777
|
try {
|
|
1773
1778
|
doc = XmlDocument.fromString(svgContent);
|
|
1774
1779
|
this.extractAndRegisterIDs(path, doc.root, registry);
|
|
1775
|
-
} catch {
|
|
1780
|
+
} catch (e) {
|
|
1781
|
+
pushMessage(context.messages, {
|
|
1782
|
+
id: MessageId.RSC_016,
|
|
1783
|
+
message: e instanceof Error ? e.message : "SVG parsing failed",
|
|
1784
|
+
location: { path }
|
|
1785
|
+
});
|
|
1776
1786
|
} finally {
|
|
1777
1787
|
doc?.dispose();
|
|
1778
1788
|
}
|
|
1779
1789
|
}
|
|
1790
|
+
validateSVGDocument(context, path, manifestItem) {
|
|
1791
|
+
const svgData = context.files.get(path);
|
|
1792
|
+
if (!svgData) return;
|
|
1793
|
+
const svgContent = new TextDecoder().decode(svgData);
|
|
1794
|
+
let doc;
|
|
1795
|
+
try {
|
|
1796
|
+
doc = XmlDocument.fromString(svgContent);
|
|
1797
|
+
} catch {
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
try {
|
|
1801
|
+
const root = doc.root;
|
|
1802
|
+
const hasRemote = this.detectSVGRemoteResources(root);
|
|
1803
|
+
if (hasRemote && !manifestItem.properties?.includes("remote-resources")) {
|
|
1804
|
+
pushMessage(context.messages, {
|
|
1805
|
+
id: MessageId.OPF_014,
|
|
1806
|
+
message: 'SVG document references remote resources but manifest item is missing "remote-resources" property',
|
|
1807
|
+
location: { path }
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
} finally {
|
|
1811
|
+
doc.dispose();
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
detectSVGRemoteResources(root) {
|
|
1815
|
+
try {
|
|
1816
|
+
const fontFaceUris = root.find(".//svg:font-face-uri", {
|
|
1817
|
+
svg: "http://www.w3.org/2000/svg"
|
|
1818
|
+
});
|
|
1819
|
+
for (const uri of fontFaceUris) {
|
|
1820
|
+
const href = this.getAttribute(uri, "xlink:href") ?? this.getAttribute(uri, "href");
|
|
1821
|
+
if (href && (href.startsWith("http://") || href.startsWith("https://"))) {
|
|
1822
|
+
return true;
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
} catch {
|
|
1826
|
+
}
|
|
1827
|
+
try {
|
|
1828
|
+
const images = root.find(".//svg:image", { svg: "http://www.w3.org/2000/svg" });
|
|
1829
|
+
for (const img of images) {
|
|
1830
|
+
const href = this.getAttribute(img, "xlink:href") ?? this.getAttribute(img, "href");
|
|
1831
|
+
if (href && (href.startsWith("http://") || href.startsWith("https://"))) {
|
|
1832
|
+
return true;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
} catch {
|
|
1836
|
+
}
|
|
1837
|
+
try {
|
|
1838
|
+
const styles = root.find(".//svg:style", { svg: "http://www.w3.org/2000/svg" });
|
|
1839
|
+
for (const style of styles) {
|
|
1840
|
+
const cssContent = style.content;
|
|
1841
|
+
if (this.cssContainsRemoteUrl(cssContent)) {
|
|
1842
|
+
return true;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
} catch {
|
|
1846
|
+
}
|
|
1847
|
+
return false;
|
|
1848
|
+
}
|
|
1780
1849
|
validateCSSDocument(context, path, opfDir, refValidator) {
|
|
1781
1850
|
const cssData = context.files.get(path);
|
|
1782
1851
|
if (!cssData) {
|
|
@@ -1937,6 +2006,13 @@ var ContentValidator = class {
|
|
|
1937
2006
|
location: { path }
|
|
1938
2007
|
});
|
|
1939
2008
|
}
|
|
2009
|
+
if (!hasScripts && manifestItem?.properties?.includes("scripted")) {
|
|
2010
|
+
pushMessage(context.messages, {
|
|
2011
|
+
id: MessageId.OPF_015,
|
|
2012
|
+
message: 'The property "scripted" should not be declared in the OPF file',
|
|
2013
|
+
location: { path }
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
1940
2016
|
const hasMathML = this.detectMathML(context, path, root);
|
|
1941
2017
|
if (hasMathML && !manifestItem?.properties?.includes("mathml")) {
|
|
1942
2018
|
pushMessage(context.messages, {
|
|
@@ -1953,6 +2029,21 @@ var ContentValidator = class {
|
|
|
1953
2029
|
location: { path }
|
|
1954
2030
|
});
|
|
1955
2031
|
}
|
|
2032
|
+
if (!hasSVG && manifestItem?.properties?.includes("svg")) {
|
|
2033
|
+
pushMessage(context.messages, {
|
|
2034
|
+
id: MessageId.OPF_015,
|
|
2035
|
+
message: 'The property "svg" should not be declared in the OPF file',
|
|
2036
|
+
location: { path }
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
2039
|
+
const hasSwitch = this.detectSwitch(root);
|
|
2040
|
+
if (hasSwitch && !manifestItem?.properties?.includes("switch")) {
|
|
2041
|
+
pushMessage(context.messages, {
|
|
2042
|
+
id: MessageId.OPF_014,
|
|
2043
|
+
message: 'Content document contains epub:switch but manifest item is missing "switch" property',
|
|
2044
|
+
location: { path }
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
1956
2047
|
const hasRemoteResources = this.detectRemoteResources(context, path, root);
|
|
1957
2048
|
if (hasRemoteResources && !manifestItem?.properties?.includes("remote-resources")) {
|
|
1958
2049
|
pushMessage(context.messages, {
|
|
@@ -1961,6 +2052,13 @@ var ContentValidator = class {
|
|
|
1961
2052
|
location: { path }
|
|
1962
2053
|
});
|
|
1963
2054
|
}
|
|
2055
|
+
if (!hasRemoteResources && manifestItem?.properties?.includes("remote-resources")) {
|
|
2056
|
+
pushMessage(context.messages, {
|
|
2057
|
+
id: MessageId.OPF_018,
|
|
2058
|
+
message: 'The "remote-resources" property was declared in the Package Document, but no reference to remote resources has been found',
|
|
2059
|
+
location: { path }
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
1964
2062
|
}
|
|
1965
2063
|
this.checkDiscouragedElements(context, path, root);
|
|
1966
2064
|
this.checkAccessibility(context, path, root);
|
|
@@ -2017,8 +2115,9 @@ var ContentValidator = class {
|
|
|
2017
2115
|
});
|
|
2018
2116
|
}
|
|
2019
2117
|
}
|
|
2020
|
-
checkNavDocument(context, path,
|
|
2021
|
-
const
|
|
2118
|
+
checkNavDocument(context, path, _doc, root) {
|
|
2119
|
+
const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
|
|
2120
|
+
const navElements = root.find(".//html:nav", HTML_NS);
|
|
2022
2121
|
if (navElements.length === 0) {
|
|
2023
2122
|
pushMessage(context.messages, {
|
|
2024
2123
|
id: MessageId.NAV_001,
|
|
@@ -2027,18 +2126,25 @@ var ContentValidator = class {
|
|
|
2027
2126
|
});
|
|
2028
2127
|
return;
|
|
2029
2128
|
}
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
for (const nav of navElements) {
|
|
2033
|
-
if (!("attrs" in nav)) continue;
|
|
2129
|
+
const getNavTypes = (nav) => {
|
|
2130
|
+
if (!("attrs" in nav)) return [];
|
|
2034
2131
|
const epubTypeAttr = nav.attrs.find(
|
|
2035
2132
|
(attr) => attr.name === "type" && attr.prefix === "epub" && attr.namespaceUri === "http://www.idpf.org/2007/ops"
|
|
2036
2133
|
);
|
|
2037
|
-
|
|
2134
|
+
return epubTypeAttr ? epubTypeAttr.value.trim().split(/\s+/) : [];
|
|
2135
|
+
};
|
|
2136
|
+
let tocNav;
|
|
2137
|
+
let tocEpubTypeValue = "";
|
|
2138
|
+
let pageListCount = 0;
|
|
2139
|
+
let landmarksCount = 0;
|
|
2140
|
+
for (const nav of navElements) {
|
|
2141
|
+
const types = getNavTypes(nav);
|
|
2142
|
+
if (types.includes("toc") && !tocNav) {
|
|
2038
2143
|
tocNav = nav;
|
|
2039
|
-
tocEpubTypeValue =
|
|
2040
|
-
break;
|
|
2144
|
+
tocEpubTypeValue = types.join(" ");
|
|
2041
2145
|
}
|
|
2146
|
+
if (types.includes("page-list")) pageListCount++;
|
|
2147
|
+
if (types.includes("landmarks")) landmarksCount++;
|
|
2042
2148
|
}
|
|
2043
2149
|
if (!tocNav) {
|
|
2044
2150
|
pushMessage(context.messages, {
|
|
@@ -2048,7 +2154,7 @@ var ContentValidator = class {
|
|
|
2048
2154
|
});
|
|
2049
2155
|
return;
|
|
2050
2156
|
}
|
|
2051
|
-
const ol = tocNav.get(".//html:ol",
|
|
2157
|
+
const ol = tocNav.get(".//html:ol", HTML_NS);
|
|
2052
2158
|
if (!ol) {
|
|
2053
2159
|
pushMessage(context.messages, {
|
|
2054
2160
|
id: MessageId.NAV_002,
|
|
@@ -2056,8 +2162,218 @@ var ContentValidator = class {
|
|
|
2056
2162
|
location: { path }
|
|
2057
2163
|
});
|
|
2058
2164
|
}
|
|
2165
|
+
if (pageListCount > 1) {
|
|
2166
|
+
pushMessage(context.messages, {
|
|
2167
|
+
id: MessageId.RSC_005,
|
|
2168
|
+
message: 'Multiple occurrences of the "page-list" nav element',
|
|
2169
|
+
location: { path }
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2172
|
+
if (landmarksCount > 1) {
|
|
2173
|
+
pushMessage(context.messages, {
|
|
2174
|
+
id: MessageId.RSC_005,
|
|
2175
|
+
message: 'Multiple occurrences of the "landmarks" nav element',
|
|
2176
|
+
location: { path }
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2179
|
+
for (const nav of navElements) {
|
|
2180
|
+
const navElem = nav;
|
|
2181
|
+
const types = getNavTypes(navElem);
|
|
2182
|
+
if (types.length === 0) continue;
|
|
2183
|
+
const isStandard = types.includes("toc") || types.includes("page-list") || types.includes("landmarks");
|
|
2184
|
+
if (!isStandard) {
|
|
2185
|
+
this.checkNavFirstChildHeading(context, path, navElem);
|
|
2186
|
+
}
|
|
2187
|
+
if (types.includes("landmarks")) {
|
|
2188
|
+
this.checkNavLandmarks(context, path, navElem);
|
|
2189
|
+
}
|
|
2190
|
+
this.checkNavLabels(context, path, navElem);
|
|
2191
|
+
this.checkNavContentModel(context, path, navElem);
|
|
2192
|
+
}
|
|
2193
|
+
this.checkNavHeadingContent(context, path, root);
|
|
2194
|
+
this.checkNavHiddenAttribute(context, path, root);
|
|
2059
2195
|
this.checkNavRemoteLinks(context, path, root, tocEpubTypeValue);
|
|
2060
2196
|
}
|
|
2197
|
+
checkNavFirstChildHeading(context, path, navElem) {
|
|
2198
|
+
const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
|
|
2199
|
+
const headingTags = /* @__PURE__ */ new Set(["h1", "h2", "h3", "h4", "h5", "h6"]);
|
|
2200
|
+
const children = navElem.find("./html:*", HTML_NS);
|
|
2201
|
+
if (children.length === 0) return;
|
|
2202
|
+
const firstChild = children[0];
|
|
2203
|
+
const localName = firstChild.name.split(":").pop() ?? firstChild.name;
|
|
2204
|
+
if (!headingTags.has(localName)) {
|
|
2205
|
+
pushMessage(context.messages, {
|
|
2206
|
+
id: MessageId.RSC_005,
|
|
2207
|
+
message: 'nav elements other than "toc", "page-list" and "landmarks" must have a heading as their first child',
|
|
2208
|
+
location: { path }
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
checkNavLandmarks(context, path, navElem) {
|
|
2213
|
+
const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
|
|
2214
|
+
const EPUB_NS = "http://www.idpf.org/2007/ops";
|
|
2215
|
+
const anchors = navElem.find(".//html:ol//html:a", HTML_NS);
|
|
2216
|
+
const seenLandmarks = [];
|
|
2217
|
+
for (const anchor of anchors) {
|
|
2218
|
+
const aElem = anchor;
|
|
2219
|
+
const epubTypeAttr = "attrs" in aElem ? aElem.attrs.find((attr) => attr.name === "type" && attr.namespaceUri === EPUB_NS) : void 0;
|
|
2220
|
+
if (!epubTypeAttr) {
|
|
2221
|
+
pushMessage(context.messages, {
|
|
2222
|
+
id: MessageId.RSC_005,
|
|
2223
|
+
message: 'Missing epub:type attribute on anchor inside "landmarks" nav element',
|
|
2224
|
+
location: { path }
|
|
2225
|
+
});
|
|
2226
|
+
continue;
|
|
2227
|
+
}
|
|
2228
|
+
const href = this.getAttribute(aElem, "href");
|
|
2229
|
+
const typeTokens = epubTypeAttr.value.toLowerCase().trim().split(/\s+/);
|
|
2230
|
+
const normalizedHref = (href ?? "").toLowerCase().trim();
|
|
2231
|
+
for (const typeToken of typeTokens) {
|
|
2232
|
+
const isDuplicate = seenLandmarks.some(
|
|
2233
|
+
(seen) => seen.type === typeToken && seen.href === normalizedHref
|
|
2234
|
+
);
|
|
2235
|
+
if (isDuplicate) {
|
|
2236
|
+
pushMessage(context.messages, {
|
|
2237
|
+
id: MessageId.RSC_005,
|
|
2238
|
+
message: `Another landmark was found with the same epub:type and same reference to "${normalizedHref}"`,
|
|
2239
|
+
location: { path }
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
seenLandmarks.push({ type: typeToken, href: normalizedHref });
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
checkNavLabels(context, path, navElem) {
|
|
2247
|
+
const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
|
|
2248
|
+
const anchors = navElem.find(".//html:ol//html:a", HTML_NS);
|
|
2249
|
+
for (const anchor of anchors) {
|
|
2250
|
+
if (!this.hasNavLabelContent(anchor)) {
|
|
2251
|
+
pushMessage(context.messages, {
|
|
2252
|
+
id: MessageId.RSC_005,
|
|
2253
|
+
message: "Anchors within nav elements must contain text",
|
|
2254
|
+
location: { path }
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
const spans = navElem.find(".//html:ol//html:span", HTML_NS);
|
|
2259
|
+
for (const span of spans) {
|
|
2260
|
+
if (!this.hasNavLabelContent(span)) {
|
|
2261
|
+
pushMessage(context.messages, {
|
|
2262
|
+
id: MessageId.RSC_005,
|
|
2263
|
+
message: "Spans within nav elements must contain text",
|
|
2264
|
+
location: { path }
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
hasNavLabelContent(element) {
|
|
2270
|
+
const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
|
|
2271
|
+
const textContent = element.content;
|
|
2272
|
+
if (textContent && textContent.trim().length > 0) return true;
|
|
2273
|
+
const imgs = element.find("./html:img[@alt]", HTML_NS);
|
|
2274
|
+
for (const img of imgs) {
|
|
2275
|
+
const alt = this.getAttribute(img, "alt");
|
|
2276
|
+
if (alt && alt.trim().length > 0) return true;
|
|
2277
|
+
}
|
|
2278
|
+
const ariaLabel = this.getAttribute(element, "aria-label");
|
|
2279
|
+
if (ariaLabel && ariaLabel.trim().length > 0) return true;
|
|
2280
|
+
const ariaLabelElements = element.find(".//*[@aria-label]");
|
|
2281
|
+
for (const el of ariaLabelElements) {
|
|
2282
|
+
const label = this.getAttribute(el, "aria-label");
|
|
2283
|
+
if (label && label.trim().length > 0) return true;
|
|
2284
|
+
}
|
|
2285
|
+
return false;
|
|
2286
|
+
}
|
|
2287
|
+
checkNavContentModel(context, path, navElem) {
|
|
2288
|
+
const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
|
|
2289
|
+
const headingTags = /* @__PURE__ */ new Set(["h1", "h2", "h3", "h4", "h5", "h6", "hgroup"]);
|
|
2290
|
+
const navChildren = navElem.find("./html:*", HTML_NS);
|
|
2291
|
+
for (const child of navChildren) {
|
|
2292
|
+
const localName = child.name.split(":").pop() ?? child.name;
|
|
2293
|
+
if (!headingTags.has(localName) && localName !== "ol") {
|
|
2294
|
+
pushMessage(context.messages, {
|
|
2295
|
+
id: MessageId.RSC_005,
|
|
2296
|
+
message: `element "${localName}" not allowed here; expected element "h1", "h2", "h3", "h4", "h5", "h6", "hgroup" or "ol"`,
|
|
2297
|
+
location: { path }
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
const olElements = navElem.find(".//html:ol", HTML_NS);
|
|
2302
|
+
for (const ol of olElements) {
|
|
2303
|
+
const liChildren = ol.find("./html:li", HTML_NS);
|
|
2304
|
+
if (liChildren.length === 0) {
|
|
2305
|
+
pushMessage(context.messages, {
|
|
2306
|
+
id: MessageId.RSC_005,
|
|
2307
|
+
message: 'element "ol" incomplete; missing required element "li"',
|
|
2308
|
+
location: { path }
|
|
2309
|
+
});
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
const liElements = navElem.find(".//html:ol//html:li", HTML_NS);
|
|
2313
|
+
for (const li of liElements) {
|
|
2314
|
+
const liElem = li;
|
|
2315
|
+
const hasOl = liElem.get("./html:ol", HTML_NS);
|
|
2316
|
+
const hasAnchor = liElem.get("./html:a", HTML_NS);
|
|
2317
|
+
const hasSpan = liElem.get("./html:span", HTML_NS);
|
|
2318
|
+
if (!hasAnchor && !hasSpan) {
|
|
2319
|
+
if (hasOl) {
|
|
2320
|
+
pushMessage(context.messages, {
|
|
2321
|
+
id: MessageId.RSC_005,
|
|
2322
|
+
message: 'element "ol" not allowed yet; expected element "a" or "span"',
|
|
2323
|
+
location: { path }
|
|
2324
|
+
});
|
|
2325
|
+
} else {
|
|
2326
|
+
pushMessage(context.messages, {
|
|
2327
|
+
id: MessageId.RSC_005,
|
|
2328
|
+
message: 'element "li" incomplete; missing required element "ol"',
|
|
2329
|
+
location: { path }
|
|
2330
|
+
});
|
|
2331
|
+
}
|
|
2332
|
+
} else if (hasSpan && !hasAnchor && !hasOl) {
|
|
2333
|
+
pushMessage(context.messages, {
|
|
2334
|
+
id: MessageId.RSC_005,
|
|
2335
|
+
message: 'element "li" incomplete; missing required element "ol"',
|
|
2336
|
+
location: { path }
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
checkNavHeadingContent(context, path, root) {
|
|
2342
|
+
const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
|
|
2343
|
+
const headingSelectors = [
|
|
2344
|
+
".//html:h1",
|
|
2345
|
+
".//html:h2",
|
|
2346
|
+
".//html:h3",
|
|
2347
|
+
".//html:h4",
|
|
2348
|
+
".//html:h5",
|
|
2349
|
+
".//html:h6"
|
|
2350
|
+
];
|
|
2351
|
+
for (const selector of headingSelectors) {
|
|
2352
|
+
const headings = root.find(selector, HTML_NS);
|
|
2353
|
+
for (const heading of headings) {
|
|
2354
|
+
if (!this.hasNavLabelContent(heading)) {
|
|
2355
|
+
pushMessage(context.messages, {
|
|
2356
|
+
id: MessageId.RSC_005,
|
|
2357
|
+
message: "Heading elements must contain text",
|
|
2358
|
+
location: { path }
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
checkNavHiddenAttribute(context, path, root) {
|
|
2365
|
+
const hiddenElements = root.find(".//*[@hidden]");
|
|
2366
|
+
for (const elem of hiddenElements) {
|
|
2367
|
+
const hiddenValue = this.getAttribute(elem, "hidden");
|
|
2368
|
+
if (hiddenValue !== null && hiddenValue !== "" && hiddenValue !== "hidden" && hiddenValue !== "until-found") {
|
|
2369
|
+
pushMessage(context.messages, {
|
|
2370
|
+
id: MessageId.RSC_005,
|
|
2371
|
+
message: `value of attribute "hidden" is invalid; must be equal to "", "hidden" or "until-found"`,
|
|
2372
|
+
location: { path }
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2061
2377
|
checkNavRemoteLinks(context, path, root, epubTypeValue) {
|
|
2062
2378
|
const navTypes = epubTypeValue.split(/\s+/);
|
|
2063
2379
|
const isToc = navTypes.includes("toc");
|
|
@@ -2129,6 +2445,10 @@ var ContentValidator = class {
|
|
|
2129
2445
|
]);
|
|
2130
2446
|
return jsTypes.has(type.toLowerCase());
|
|
2131
2447
|
}
|
|
2448
|
+
detectSwitch(root) {
|
|
2449
|
+
const switchElem = root.get(".//epub:switch", { epub: "http://www.idpf.org/2007/ops" });
|
|
2450
|
+
return !!switchElem;
|
|
2451
|
+
}
|
|
2132
2452
|
detectMathML(_context, _path, root) {
|
|
2133
2453
|
const mathMLElements = root.find(".//math:*", { math: "http://www.w3.org/1998/Math/MathML" });
|
|
2134
2454
|
return mathMLElements.length > 0;
|
|
@@ -2190,8 +2510,19 @@ var ContentValidator = class {
|
|
|
2190
2510
|
}
|
|
2191
2511
|
}
|
|
2192
2512
|
}
|
|
2513
|
+
const styleElements = root.find(".//html:style", { html: "http://www.w3.org/1999/xhtml" });
|
|
2514
|
+
for (const style of styleElements) {
|
|
2515
|
+
const cssContent = style.content;
|
|
2516
|
+
if (this.cssContainsRemoteUrl(cssContent)) {
|
|
2517
|
+
return true;
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2193
2520
|
return false;
|
|
2194
2521
|
}
|
|
2522
|
+
cssContainsRemoteUrl(css) {
|
|
2523
|
+
const urlRegex = /url\s*\(\s*["']?(https?:\/\/[^"')]+)["']?\s*\)/gi;
|
|
2524
|
+
return urlRegex.test(css);
|
|
2525
|
+
}
|
|
2195
2526
|
checkDiscouragedElements(context, path, root) {
|
|
2196
2527
|
for (const elemName of DISCOURAGED_ELEMENTS) {
|
|
2197
2528
|
const element = root.get(`.//html:${elemName}`, { html: "http://www.w3.org/1999/xhtml" });
|
|
@@ -2455,9 +2786,9 @@ var ContentValidator = class {
|
|
|
2455
2786
|
const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
|
|
2456
2787
|
const links = root.find(".//html:a[@href]", { html: "http://www.w3.org/1999/xhtml" });
|
|
2457
2788
|
for (const link of links) {
|
|
2458
|
-
const href = this.getAttribute(link, "href");
|
|
2789
|
+
const href = this.getAttribute(link, "href")?.trim() ?? null;
|
|
2459
2790
|
if (href === null) continue;
|
|
2460
|
-
if (href
|
|
2791
|
+
if (href === "") {
|
|
2461
2792
|
pushMessage(context.messages, {
|
|
2462
2793
|
id: MessageId.HTM_045,
|
|
2463
2794
|
message: "Encountered empty href",
|
|
@@ -3056,7 +3387,7 @@ function toJSONReport(result) {
|
|
|
3056
3387
|
);
|
|
3057
3388
|
}
|
|
3058
3389
|
var NCXValidator = class {
|
|
3059
|
-
validate(context, ncxContent, ncxPath) {
|
|
3390
|
+
validate(context, ncxContent, ncxPath, registry) {
|
|
3060
3391
|
let doc = null;
|
|
3061
3392
|
try {
|
|
3062
3393
|
doc = XmlDocument.fromString(ncxContent);
|
|
@@ -3090,7 +3421,7 @@ var NCXValidator = class {
|
|
|
3090
3421
|
}
|
|
3091
3422
|
this.checkUid(context, root, ncxPath);
|
|
3092
3423
|
this.checkNavMap(context, root, ncxPath);
|
|
3093
|
-
this.checkContentSrc(context, root, ncxPath);
|
|
3424
|
+
this.checkContentSrc(context, root, ncxPath, registry);
|
|
3094
3425
|
} finally {
|
|
3095
3426
|
doc.dispose();
|
|
3096
3427
|
}
|
|
@@ -3126,7 +3457,7 @@ var NCXValidator = class {
|
|
|
3126
3457
|
});
|
|
3127
3458
|
}
|
|
3128
3459
|
}
|
|
3129
|
-
checkContentSrc(context, root, ncxPath) {
|
|
3460
|
+
checkContentSrc(context, root, ncxPath, registry) {
|
|
3130
3461
|
const contentElements = root.find(".//ncx:content[@src]", {
|
|
3131
3462
|
ncx: "http://www.daisy.org/z3986/2005/ncx/"
|
|
3132
3463
|
});
|
|
@@ -3135,7 +3466,9 @@ var NCXValidator = class {
|
|
|
3135
3466
|
const srcAttr = contentElem.attr("src");
|
|
3136
3467
|
const src = srcAttr?.value;
|
|
3137
3468
|
if (!src) continue;
|
|
3138
|
-
const
|
|
3469
|
+
const hashIdx = src.indexOf("#");
|
|
3470
|
+
const srcBase = hashIdx >= 0 ? src.substring(0, hashIdx) : src;
|
|
3471
|
+
const fragment = hashIdx >= 0 ? src.substring(hashIdx + 1) : "";
|
|
3139
3472
|
let fullPath = srcBase;
|
|
3140
3473
|
if (ncxDir) {
|
|
3141
3474
|
if (srcBase.startsWith("/")) {
|
|
@@ -3160,6 +3493,15 @@ var NCXValidator = class {
|
|
|
3160
3493
|
message: `NCX content src references missing file: ${src}`,
|
|
3161
3494
|
location: { path: ncxPath, line }
|
|
3162
3495
|
});
|
|
3496
|
+
} else if (fragment && registry) {
|
|
3497
|
+
if (!registry.hasID(fullPath, fragment)) {
|
|
3498
|
+
const line = contentElem.line;
|
|
3499
|
+
pushMessage(context.messages, {
|
|
3500
|
+
id: MessageId.RSC_012,
|
|
3501
|
+
message: `Fragment identifier is not defined`,
|
|
3502
|
+
location: { path: ncxPath, line }
|
|
3503
|
+
});
|
|
3504
|
+
}
|
|
3163
3505
|
}
|
|
3164
3506
|
}
|
|
3165
3507
|
}
|
|
@@ -3757,6 +4099,15 @@ function parseOPF(xml) {
|
|
|
3757
4099
|
const guideSection = extractSection(xml, "guide");
|
|
3758
4100
|
const guide = parseGuide(guideSection);
|
|
3759
4101
|
const collections = parseCollections(xml);
|
|
4102
|
+
const hasBindings = /<bindings[\s>]/.test(xml);
|
|
4103
|
+
const xmlLangs = [];
|
|
4104
|
+
const xmlLangRegex = /xml:lang=["']([^"']*?)["']/g;
|
|
4105
|
+
let langMatch;
|
|
4106
|
+
while ((langMatch = xmlLangRegex.exec(xml)) !== null) {
|
|
4107
|
+
if (langMatch[1] !== void 0) {
|
|
4108
|
+
xmlLangs.push(langMatch[1]);
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
3760
4111
|
const result = {
|
|
3761
4112
|
version,
|
|
3762
4113
|
uniqueIdentifier,
|
|
@@ -3780,6 +4131,12 @@ function parseOPF(xml) {
|
|
|
3780
4131
|
if (spineResult.pageProgressionDirection) {
|
|
3781
4132
|
result.pageProgressionDirection = spineResult.pageProgressionDirection;
|
|
3782
4133
|
}
|
|
4134
|
+
if (hasBindings) {
|
|
4135
|
+
result.hasBindings = true;
|
|
4136
|
+
}
|
|
4137
|
+
if (xmlLangs.length > 0) {
|
|
4138
|
+
result.xmlLangs = xmlLangs;
|
|
4139
|
+
}
|
|
3783
4140
|
return result;
|
|
3784
4141
|
}
|
|
3785
4142
|
function normalizeVersion(versionStr) {
|
|
@@ -3904,6 +4261,9 @@ function parseLinkElements(metadataXml) {
|
|
|
3904
4261
|
if (attrs.id) {
|
|
3905
4262
|
element.id = attrs.id;
|
|
3906
4263
|
}
|
|
4264
|
+
if (attrs.hreflang !== void 0) {
|
|
4265
|
+
element.hreflang = attrs.hreflang;
|
|
4266
|
+
}
|
|
3907
4267
|
elements.push(element);
|
|
3908
4268
|
}
|
|
3909
4269
|
}
|
|
@@ -4050,6 +4410,7 @@ var ITEM_PROPERTIES = /* @__PURE__ */ new Set([
|
|
|
4050
4410
|
"svg",
|
|
4051
4411
|
"switch"
|
|
4052
4412
|
]);
|
|
4413
|
+
var LINK_PROPERTIES = /* @__PURE__ */ new Set(["onix", "marc21xml-record", "mods-record", "xmp-record"]);
|
|
4053
4414
|
var SPINE_PROPERTIES = /* @__PURE__ */ new Set([
|
|
4054
4415
|
"page-spread-left",
|
|
4055
4416
|
"page-spread-right",
|
|
@@ -4117,6 +4478,31 @@ var OPFValidator = class {
|
|
|
4117
4478
|
}
|
|
4118
4479
|
if (this.packageDoc.version.startsWith("3.")) {
|
|
4119
4480
|
this.validateCollections(context, opfPath);
|
|
4481
|
+
if (this.packageDoc.hasBindings) {
|
|
4482
|
+
pushMessage(context.messages, {
|
|
4483
|
+
id: MessageId.RSC_017,
|
|
4484
|
+
message: "Use of the bindings element is deprecated",
|
|
4485
|
+
location: { path: opfPath }
|
|
4486
|
+
});
|
|
4487
|
+
}
|
|
4488
|
+
}
|
|
4489
|
+
if (this.packageDoc.xmlLangs) {
|
|
4490
|
+
for (const lang of this.packageDoc.xmlLangs) {
|
|
4491
|
+
if (lang === "") continue;
|
|
4492
|
+
if (lang !== lang.trim()) {
|
|
4493
|
+
pushMessage(context.messages, {
|
|
4494
|
+
id: MessageId.OPF_092,
|
|
4495
|
+
message: `Language tag "${lang}" is not well-formed`,
|
|
4496
|
+
location: { path: opfPath }
|
|
4497
|
+
});
|
|
4498
|
+
} else if (!isValidLanguageTag(lang)) {
|
|
4499
|
+
pushMessage(context.messages, {
|
|
4500
|
+
id: MessageId.OPF_092,
|
|
4501
|
+
message: `Language tag "${lang}" is not well-formed`,
|
|
4502
|
+
location: { path: opfPath }
|
|
4503
|
+
});
|
|
4504
|
+
}
|
|
4505
|
+
}
|
|
4120
4506
|
}
|
|
4121
4507
|
}
|
|
4122
4508
|
/**
|
|
@@ -4155,6 +4541,11 @@ var OPFValidator = class {
|
|
|
4155
4541
|
(dc) => dc.name === "identifier" && dc.id === refId
|
|
4156
4542
|
);
|
|
4157
4543
|
if (!matchingDc) {
|
|
4544
|
+
pushMessage(context.messages, {
|
|
4545
|
+
id: MessageId.RSC_005,
|
|
4546
|
+
message: `package element unique-identifier attribute does not resolve to a dc:identifier element (given reference was "${refId}")`,
|
|
4547
|
+
location: { path: opfPath }
|
|
4548
|
+
});
|
|
4158
4549
|
pushMessage(context.messages, {
|
|
4159
4550
|
id: MessageId.OPF_030,
|
|
4160
4551
|
message: `unique-identifier "${refId}" does not reference an existing dc:identifier`,
|
|
@@ -4220,6 +4611,19 @@ var OPFValidator = class {
|
|
|
4220
4611
|
});
|
|
4221
4612
|
}
|
|
4222
4613
|
}
|
|
4614
|
+
if (dc.name === "identifier" && dc.value) {
|
|
4615
|
+
const val = dc.value.trim();
|
|
4616
|
+
if (val.startsWith("urn:uuid:")) {
|
|
4617
|
+
const uuid = val.substring(9);
|
|
4618
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(uuid)) {
|
|
4619
|
+
pushMessage(context.messages, {
|
|
4620
|
+
id: MessageId.OPF_085,
|
|
4621
|
+
message: `Invalid UUID value "${uuid}"`,
|
|
4622
|
+
location: { path: opfPath }
|
|
4623
|
+
});
|
|
4624
|
+
}
|
|
4625
|
+
}
|
|
4626
|
+
}
|
|
4223
4627
|
if (dc.name === "creator" && dc.attributes) {
|
|
4224
4628
|
const opfRole = dc.attributes["opf:role"];
|
|
4225
4629
|
if (opfRole?.startsWith("marc:")) {
|
|
@@ -4329,22 +4733,143 @@ var OPFValidator = class {
|
|
|
4329
4733
|
}
|
|
4330
4734
|
}
|
|
4331
4735
|
}
|
|
4736
|
+
if (this.packageDoc.version !== "2.0") {
|
|
4737
|
+
for (const meta of this.packageDoc.metaElements) {
|
|
4738
|
+
if (meta.property && /\s/.test(meta.property.trim())) {
|
|
4739
|
+
pushMessage(context.messages, {
|
|
4740
|
+
id: MessageId.OPF_025,
|
|
4741
|
+
message: `Property value must be a single value, not a list: "${meta.property}"`,
|
|
4742
|
+
location: { path: opfPath }
|
|
4743
|
+
});
|
|
4744
|
+
}
|
|
4745
|
+
if (meta.scheme && /\s/.test(meta.scheme.trim())) {
|
|
4746
|
+
pushMessage(context.messages, {
|
|
4747
|
+
id: MessageId.OPF_025,
|
|
4748
|
+
message: `Scheme value must be a single value, not a list: "${meta.scheme}"`,
|
|
4749
|
+
location: { path: opfPath }
|
|
4750
|
+
});
|
|
4751
|
+
}
|
|
4752
|
+
if (meta.property && !/\s/.test(meta.property.trim())) {
|
|
4753
|
+
const prop = meta.property.trim();
|
|
4754
|
+
if (prop.includes(":") && /:\s*$/.test(prop)) {
|
|
4755
|
+
pushMessage(context.messages, {
|
|
4756
|
+
id: MessageId.OPF_026,
|
|
4757
|
+
message: `Malformed property name: "${prop}"`,
|
|
4758
|
+
location: { path: opfPath }
|
|
4759
|
+
});
|
|
4760
|
+
}
|
|
4761
|
+
}
|
|
4762
|
+
if (meta.scheme) {
|
|
4763
|
+
const scheme = meta.scheme.trim();
|
|
4764
|
+
if (scheme && !scheme.includes(":")) {
|
|
4765
|
+
pushMessage(context.messages, {
|
|
4766
|
+
id: MessageId.OPF_027,
|
|
4767
|
+
message: `Undefined property: "${scheme}"`,
|
|
4768
|
+
location: { path: opfPath }
|
|
4769
|
+
});
|
|
4770
|
+
}
|
|
4771
|
+
}
|
|
4772
|
+
}
|
|
4773
|
+
}
|
|
4774
|
+
if (this.packageDoc.version !== "2.0") {
|
|
4775
|
+
const allIds = /* @__PURE__ */ new Set();
|
|
4776
|
+
for (const dc of dcElements) {
|
|
4777
|
+
if (dc.id) allIds.add(dc.id);
|
|
4778
|
+
}
|
|
4779
|
+
for (const meta of this.packageDoc.metaElements) {
|
|
4780
|
+
if (meta.id) allIds.add(meta.id);
|
|
4781
|
+
}
|
|
4782
|
+
for (const link of this.packageDoc.linkElements) {
|
|
4783
|
+
if (link.id) allIds.add(link.id);
|
|
4784
|
+
}
|
|
4785
|
+
for (const item of this.packageDoc.manifest) {
|
|
4786
|
+
allIds.add(item.id);
|
|
4787
|
+
}
|
|
4788
|
+
const seenGlobalIds = /* @__PURE__ */ new Set();
|
|
4789
|
+
const allIdSources = [];
|
|
4790
|
+
for (const dc of dcElements) {
|
|
4791
|
+
if (dc.id) allIdSources.push({ id: dc.id, normalized: dc.id.trim() });
|
|
4792
|
+
}
|
|
4793
|
+
for (const meta of this.packageDoc.metaElements) {
|
|
4794
|
+
if (meta.id) allIdSources.push({ id: meta.id, normalized: meta.id.trim() });
|
|
4795
|
+
}
|
|
4796
|
+
for (const link of this.packageDoc.linkElements) {
|
|
4797
|
+
if (link.id) allIdSources.push({ id: link.id, normalized: link.id.trim() });
|
|
4798
|
+
}
|
|
4799
|
+
for (const item of this.packageDoc.manifest) {
|
|
4800
|
+
allIdSources.push({ id: item.id, normalized: item.id.trim() });
|
|
4801
|
+
}
|
|
4802
|
+
for (const src of allIdSources) {
|
|
4803
|
+
if (seenGlobalIds.has(src.normalized)) {
|
|
4804
|
+
pushMessage(context.messages, {
|
|
4805
|
+
id: MessageId.RSC_005,
|
|
4806
|
+
message: `Duplicate "${src.normalized}"`,
|
|
4807
|
+
location: { path: opfPath }
|
|
4808
|
+
});
|
|
4809
|
+
}
|
|
4810
|
+
seenGlobalIds.add(src.normalized);
|
|
4811
|
+
}
|
|
4812
|
+
for (const meta of this.packageDoc.metaElements) {
|
|
4813
|
+
if (!meta.refines) continue;
|
|
4814
|
+
const refines = meta.refines;
|
|
4815
|
+
if (/^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(refines)) {
|
|
4816
|
+
pushMessage(context.messages, {
|
|
4817
|
+
id: MessageId.RSC_005,
|
|
4818
|
+
message: "@refines must be a relative URL",
|
|
4819
|
+
location: { path: opfPath }
|
|
4820
|
+
});
|
|
4821
|
+
continue;
|
|
4822
|
+
}
|
|
4823
|
+
if (!refines.startsWith("#")) {
|
|
4824
|
+
pushMessage(context.messages, {
|
|
4825
|
+
id: MessageId.RSC_017,
|
|
4826
|
+
message: `@refines should instead refer to "${refines}" using a fragment identifier pointing to its manifest item`,
|
|
4827
|
+
location: { path: opfPath }
|
|
4828
|
+
});
|
|
4829
|
+
continue;
|
|
4830
|
+
}
|
|
4831
|
+
const targetId = refines.substring(1);
|
|
4832
|
+
if (!allIds.has(targetId)) {
|
|
4833
|
+
pushMessage(context.messages, {
|
|
4834
|
+
id: MessageId.RSC_005,
|
|
4835
|
+
message: `@refines missing target id: "${targetId}"`,
|
|
4836
|
+
location: { path: opfPath }
|
|
4837
|
+
});
|
|
4838
|
+
}
|
|
4839
|
+
}
|
|
4840
|
+
this.detectRefinesCycles(context, opfPath);
|
|
4841
|
+
}
|
|
4332
4842
|
if (this.packageDoc.version !== "2.0") {
|
|
4333
4843
|
const modifiedMeta = this.packageDoc.metaElements.find(
|
|
4334
4844
|
(meta) => meta.property === "dcterms:modified"
|
|
4335
4845
|
);
|
|
4336
4846
|
if (!modifiedMeta) {
|
|
4337
4847
|
pushMessage(context.messages, {
|
|
4338
|
-
id: MessageId.
|
|
4339
|
-
message: "
|
|
4848
|
+
id: MessageId.RSC_005,
|
|
4849
|
+
message: "package dcterms:modified meta element must occur exactly once",
|
|
4340
4850
|
location: { path: opfPath }
|
|
4341
4851
|
});
|
|
4342
|
-
} else if (modifiedMeta.value && !isValidW3CDateFormat(modifiedMeta.value)) {
|
|
4343
4852
|
pushMessage(context.messages, {
|
|
4344
4853
|
id: MessageId.OPF_054,
|
|
4345
|
-
message:
|
|
4854
|
+
message: "EPUB 3 metadata must include a dcterms:modified meta element",
|
|
4346
4855
|
location: { path: opfPath }
|
|
4347
4856
|
});
|
|
4857
|
+
} else if (modifiedMeta.value) {
|
|
4858
|
+
const strictModifiedPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
|
|
4859
|
+
if (!strictModifiedPattern.test(modifiedMeta.value.trim())) {
|
|
4860
|
+
pushMessage(context.messages, {
|
|
4861
|
+
id: MessageId.RSC_005,
|
|
4862
|
+
message: `dcterms:modified illegal syntax (expecting: "CCYY-MM-DDThh:mm:ssZ")`,
|
|
4863
|
+
location: { path: opfPath }
|
|
4864
|
+
});
|
|
4865
|
+
}
|
|
4866
|
+
if (!isValidW3CDateFormat(modifiedMeta.value)) {
|
|
4867
|
+
pushMessage(context.messages, {
|
|
4868
|
+
id: MessageId.OPF_054,
|
|
4869
|
+
message: `Invalid dcterms:modified date format "${modifiedMeta.value}"; must be W3C date format (ISO 8601)`,
|
|
4870
|
+
location: { path: opfPath }
|
|
4871
|
+
});
|
|
4872
|
+
}
|
|
4348
4873
|
}
|
|
4349
4874
|
}
|
|
4350
4875
|
}
|
|
@@ -4355,11 +4880,43 @@ var OPFValidator = class {
|
|
|
4355
4880
|
if (!this.packageDoc) return;
|
|
4356
4881
|
const opfDir = opfPath.includes("/") ? opfPath.substring(0, opfPath.lastIndexOf("/")) : "";
|
|
4357
4882
|
for (const link of this.packageDoc.linkElements) {
|
|
4883
|
+
if (link.hreflang !== void 0 && link.hreflang !== "") {
|
|
4884
|
+
const lang = link.hreflang;
|
|
4885
|
+
if (lang !== lang.trim()) {
|
|
4886
|
+
pushMessage(context.messages, {
|
|
4887
|
+
id: MessageId.OPF_092,
|
|
4888
|
+
message: `Language tag must not have leading or trailing whitespace: "${lang}"`,
|
|
4889
|
+
location: { path: opfPath }
|
|
4890
|
+
});
|
|
4891
|
+
} else if (!isValidLanguageTag(lang)) {
|
|
4892
|
+
pushMessage(context.messages, {
|
|
4893
|
+
id: MessageId.OPF_092,
|
|
4894
|
+
message: `Invalid language tag: "${lang}"`,
|
|
4895
|
+
location: { path: opfPath }
|
|
4896
|
+
});
|
|
4897
|
+
}
|
|
4898
|
+
}
|
|
4899
|
+
if (link.properties) {
|
|
4900
|
+
for (const prop of link.properties) {
|
|
4901
|
+
if (!LINK_PROPERTIES.has(prop) && !prop.includes(":")) {
|
|
4902
|
+
pushMessage(context.messages, {
|
|
4903
|
+
id: MessageId.OPF_027,
|
|
4904
|
+
message: `Undefined property: "${prop}"`,
|
|
4905
|
+
location: { path: opfPath }
|
|
4906
|
+
});
|
|
4907
|
+
}
|
|
4908
|
+
}
|
|
4909
|
+
}
|
|
4358
4910
|
const href = link.href;
|
|
4359
4911
|
const decodedHref = tryDecodeUriComponent(href);
|
|
4360
4912
|
const basePath = href.includes("#") ? href.substring(0, href.indexOf("#")) : href;
|
|
4361
4913
|
const basePathDecoded = decodedHref.includes("#") ? decodedHref.substring(0, decodedHref.indexOf("#")) : decodedHref;
|
|
4362
4914
|
if (href.startsWith("#")) {
|
|
4915
|
+
pushMessage(context.messages, {
|
|
4916
|
+
id: MessageId.OPF_098,
|
|
4917
|
+
message: `The "href" attribute must reference resources, not elements in the package document, but found URL "${href}".`,
|
|
4918
|
+
location: { path: opfPath }
|
|
4919
|
+
});
|
|
4363
4920
|
continue;
|
|
4364
4921
|
}
|
|
4365
4922
|
const isRemote = /^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(href);
|
|
@@ -4476,6 +5033,11 @@ var OPFValidator = class {
|
|
|
4476
5033
|
}
|
|
4477
5034
|
if (item.properties.includes("nav")) {
|
|
4478
5035
|
if (item.mediaType !== "application/xhtml+xml") {
|
|
5036
|
+
pushMessage(context.messages, {
|
|
5037
|
+
id: MessageId.RSC_005,
|
|
5038
|
+
message: `The manifest item representing the Navigation Document must be of the "application/xhtml+xml" type (given type was "${item.mediaType}")`,
|
|
5039
|
+
location: { path: opfPath }
|
|
5040
|
+
});
|
|
4479
5041
|
pushMessage(context.messages, {
|
|
4480
5042
|
id: MessageId.OPF_012,
|
|
4481
5043
|
message: `Item with "nav" property must be XHTML, found: ${item.mediaType}`,
|
|
@@ -4483,6 +5045,22 @@ var OPFValidator = class {
|
|
|
4483
5045
|
});
|
|
4484
5046
|
}
|
|
4485
5047
|
}
|
|
5048
|
+
if (item.properties.includes("cover-image")) {
|
|
5049
|
+
if (!item.mediaType.startsWith("image/")) {
|
|
5050
|
+
pushMessage(context.messages, {
|
|
5051
|
+
id: MessageId.OPF_012,
|
|
5052
|
+
message: `Item with "cover-image" property must be an image, found: ${item.mediaType}`,
|
|
5053
|
+
location: { path: opfPath }
|
|
5054
|
+
});
|
|
5055
|
+
}
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
5058
|
+
if (item.href.includes(" ")) {
|
|
5059
|
+
pushMessage(context.messages, {
|
|
5060
|
+
id: MessageId.RSC_020,
|
|
5061
|
+
message: `"${item.href}" is not a valid URL (Illegal character in path segment: space is not allowed)`,
|
|
5062
|
+
location: { path: opfPath }
|
|
5063
|
+
});
|
|
4486
5064
|
}
|
|
4487
5065
|
if (this.packageDoc.version !== "2.0" && item.href.includes("#")) {
|
|
4488
5066
|
pushMessage(context.messages, {
|
|
@@ -4492,6 +5070,13 @@ var OPFValidator = class {
|
|
|
4492
5070
|
});
|
|
4493
5071
|
}
|
|
4494
5072
|
if (this.packageDoc.version !== "2.0" && (item.href.startsWith("http://") || item.href.startsWith("https://"))) {
|
|
5073
|
+
if (!item.mediaType.startsWith("audio/") && !item.mediaType.startsWith("video/") && !item.mediaType.startsWith("font/") && item.mediaType !== "application/font-sfnt" && item.mediaType !== "application/font-woff" && item.mediaType !== "application/vnd.ms-opentype") {
|
|
5074
|
+
pushMessage(context.messages, {
|
|
5075
|
+
id: MessageId.RSC_006,
|
|
5076
|
+
message: `Remote resource reference is not allowed in this context; resource "${item.href}" must be located in the EPUB container`,
|
|
5077
|
+
location: { path: opfPath }
|
|
5078
|
+
});
|
|
5079
|
+
}
|
|
4495
5080
|
const inSpine = this.packageDoc.spine.some((s) => s.idref === item.id);
|
|
4496
5081
|
if (inSpine && !item.properties?.includes("remote-resources")) {
|
|
4497
5082
|
pushMessage(context.messages, {
|
|
@@ -4503,13 +5088,29 @@ var OPFValidator = class {
|
|
|
4503
5088
|
}
|
|
4504
5089
|
}
|
|
4505
5090
|
if (this.packageDoc.version !== "2.0") {
|
|
4506
|
-
const
|
|
4507
|
-
if (
|
|
5091
|
+
const navItems = this.packageDoc.manifest.filter((item) => item.properties?.includes("nav"));
|
|
5092
|
+
if (navItems.length === 0) {
|
|
4508
5093
|
pushMessage(context.messages, {
|
|
4509
5094
|
id: MessageId.RSC_005,
|
|
4510
5095
|
message: 'Exactly one manifest item must declare the "nav" property',
|
|
4511
5096
|
location: { path: opfPath }
|
|
4512
5097
|
});
|
|
5098
|
+
} else if (navItems.length > 1) {
|
|
5099
|
+
pushMessage(context.messages, {
|
|
5100
|
+
id: MessageId.RSC_005,
|
|
5101
|
+
message: `Exactly one manifest item must declare the "nav" property (number of "nav" items: ${String(navItems.length)}).`,
|
|
5102
|
+
location: { path: opfPath }
|
|
5103
|
+
});
|
|
5104
|
+
}
|
|
5105
|
+
const coverItems = this.packageDoc.manifest.filter(
|
|
5106
|
+
(item) => item.properties?.includes("cover-image")
|
|
5107
|
+
);
|
|
5108
|
+
if (coverItems.length > 1) {
|
|
5109
|
+
pushMessage(context.messages, {
|
|
5110
|
+
id: MessageId.RSC_005,
|
|
5111
|
+
message: `Multiple occurrences of the "cover-image" property (number of "cover-image" items: ${String(coverItems.length)}).`,
|
|
5112
|
+
location: { path: opfPath }
|
|
5113
|
+
});
|
|
4513
5114
|
}
|
|
4514
5115
|
}
|
|
4515
5116
|
}
|
|
@@ -4541,6 +5142,17 @@ var OPFValidator = class {
|
|
|
4541
5142
|
message: "EPUB 2 spine should have a toc attribute referencing the NCX",
|
|
4542
5143
|
location: { path: opfPath }
|
|
4543
5144
|
});
|
|
5145
|
+
} else if (!ncxId && this.packageDoc.version !== "2.0") {
|
|
5146
|
+
const hasNcxInManifest = this.packageDoc.manifest.some(
|
|
5147
|
+
(item) => item.mediaType === "application/x-dtbncx+xml"
|
|
5148
|
+
);
|
|
5149
|
+
if (hasNcxInManifest) {
|
|
5150
|
+
pushMessage(context.messages, {
|
|
5151
|
+
id: MessageId.RSC_005,
|
|
5152
|
+
message: "spine element toc attribute must be set when an NCX is included in the publication",
|
|
5153
|
+
location: { path: opfPath }
|
|
5154
|
+
});
|
|
5155
|
+
}
|
|
4544
5156
|
} else if (ncxId) {
|
|
4545
5157
|
const ncxItem = this.manifestById.get(ncxId);
|
|
4546
5158
|
if (!ncxItem) {
|
|
@@ -4650,21 +5262,74 @@ var OPFValidator = class {
|
|
|
4650
5262
|
}
|
|
4651
5263
|
}
|
|
4652
5264
|
}
|
|
5265
|
+
detectRefinesCycles(context, opfPath) {
|
|
5266
|
+
if (!this.packageDoc) return;
|
|
5267
|
+
const refinesGraph = /* @__PURE__ */ new Map();
|
|
5268
|
+
for (const meta of this.packageDoc.metaElements) {
|
|
5269
|
+
if (!meta.refines || !meta.id) continue;
|
|
5270
|
+
if (!meta.refines.startsWith("#")) continue;
|
|
5271
|
+
const targetId = meta.refines.substring(1);
|
|
5272
|
+
const existing = refinesGraph.get(meta.id);
|
|
5273
|
+
if (existing) {
|
|
5274
|
+
existing.push(targetId);
|
|
5275
|
+
} else {
|
|
5276
|
+
refinesGraph.set(meta.id, [targetId]);
|
|
5277
|
+
}
|
|
5278
|
+
}
|
|
5279
|
+
const visited = /* @__PURE__ */ new Set();
|
|
5280
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
5281
|
+
const reportedCycles = /* @__PURE__ */ new Set();
|
|
5282
|
+
const dfs = (node) => {
|
|
5283
|
+
if (inStack.has(node)) return true;
|
|
5284
|
+
if (visited.has(node)) return false;
|
|
5285
|
+
visited.add(node);
|
|
5286
|
+
inStack.add(node);
|
|
5287
|
+
const neighbors = refinesGraph.get(node) ?? [];
|
|
5288
|
+
for (const neighbor of neighbors) {
|
|
5289
|
+
if (dfs(neighbor)) {
|
|
5290
|
+
if (!reportedCycles.has(node)) {
|
|
5291
|
+
reportedCycles.add(node);
|
|
5292
|
+
pushMessage(context.messages, {
|
|
5293
|
+
id: MessageId.OPF_065,
|
|
5294
|
+
message: `Invalid metadata declaration, probably due to a cycle in "refines" metadata.`,
|
|
5295
|
+
location: { path: opfPath }
|
|
5296
|
+
});
|
|
5297
|
+
}
|
|
5298
|
+
return true;
|
|
5299
|
+
}
|
|
5300
|
+
}
|
|
5301
|
+
inStack.delete(node);
|
|
5302
|
+
return false;
|
|
5303
|
+
};
|
|
5304
|
+
for (const node of refinesGraph.keys()) {
|
|
5305
|
+
dfs(node);
|
|
5306
|
+
}
|
|
5307
|
+
}
|
|
4653
5308
|
validateCollections(context, opfPath) {
|
|
4654
5309
|
if (!this.packageDoc) return;
|
|
4655
5310
|
const collections = this.packageDoc.collections;
|
|
4656
5311
|
if (collections.length === 0) {
|
|
4657
5312
|
return;
|
|
4658
5313
|
}
|
|
4659
|
-
const validRoles = /* @__PURE__ */ new Set(["dictionary", "index", "preview", "recordings"]);
|
|
4660
5314
|
for (const collection of collections) {
|
|
4661
|
-
if (
|
|
5315
|
+
if (collection.role === "manifest") {
|
|
4662
5316
|
pushMessage(context.messages, {
|
|
4663
|
-
id: MessageId.
|
|
4664
|
-
message:
|
|
5317
|
+
id: MessageId.RSC_005,
|
|
5318
|
+
message: 'Collection with role "manifest" must be a child of another collection',
|
|
4665
5319
|
location: { path: opfPath }
|
|
4666
5320
|
});
|
|
4667
5321
|
}
|
|
5322
|
+
if (collection.role.includes(":")) {
|
|
5323
|
+
try {
|
|
5324
|
+
new URL(collection.role);
|
|
5325
|
+
} catch {
|
|
5326
|
+
pushMessage(context.messages, {
|
|
5327
|
+
id: MessageId.OPF_070,
|
|
5328
|
+
message: `Invalid collection role URL: "${collection.role}"`,
|
|
5329
|
+
location: { path: opfPath }
|
|
5330
|
+
});
|
|
5331
|
+
}
|
|
5332
|
+
}
|
|
4668
5333
|
if (collection.role === "dictionary") {
|
|
4669
5334
|
if (!collection.name || collection.name.trim() === "") {
|
|
4670
5335
|
pushMessage(context.messages, {
|
|
@@ -4952,7 +5617,9 @@ function hasParentDirectoryReference(url) {
|
|
|
4952
5617
|
function isMalformedURL(url) {
|
|
4953
5618
|
if (!url) return true;
|
|
4954
5619
|
try {
|
|
4955
|
-
|
|
5620
|
+
const trimmed = url.trim();
|
|
5621
|
+
if (!trimmed) return true;
|
|
5622
|
+
if (/[\s<>]/.test(trimmed)) return true;
|
|
4956
5623
|
return false;
|
|
4957
5624
|
} catch {
|
|
4958
5625
|
return true;
|
|
@@ -4996,7 +5663,7 @@ var ReferenceValidator = class {
|
|
|
4996
5663
|
* Validate a single reference
|
|
4997
5664
|
*/
|
|
4998
5665
|
validateReference(context, reference) {
|
|
4999
|
-
const url = reference.url;
|
|
5666
|
+
const url = reference.url.trim();
|
|
5000
5667
|
if (isMalformedURL(url)) {
|
|
5001
5668
|
pushMessage(context.messages, {
|
|
5002
5669
|
id: MessageId.RSC_020,
|
|
@@ -5604,8 +6271,8 @@ var EpubCheck = class _EpubCheck {
|
|
|
5604
6271
|
}
|
|
5605
6272
|
const contentValidator = new ContentValidator();
|
|
5606
6273
|
contentValidator.validate(context, registry, refValidator);
|
|
5607
|
-
if (context.
|
|
5608
|
-
this.validateNCX(context);
|
|
6274
|
+
if (context.packageDocument) {
|
|
6275
|
+
this.validateNCX(context, registry);
|
|
5609
6276
|
}
|
|
5610
6277
|
refValidator.validate(context);
|
|
5611
6278
|
const schemaValidator = new SchemaValidator(context);
|
|
@@ -5646,9 +6313,9 @@ var EpubCheck = class _EpubCheck {
|
|
|
5646
6313
|
return this.options.version;
|
|
5647
6314
|
}
|
|
5648
6315
|
/**
|
|
5649
|
-
* Validate NCX navigation document (EPUB 2
|
|
6316
|
+
* Validate NCX navigation document (EPUB 2 always, EPUB 3 when NCX present)
|
|
5650
6317
|
*/
|
|
5651
|
-
validateNCX(context) {
|
|
6318
|
+
validateNCX(context, registry) {
|
|
5652
6319
|
if (!context.packageDocument) {
|
|
5653
6320
|
return;
|
|
5654
6321
|
}
|
|
@@ -5669,7 +6336,7 @@ var EpubCheck = class _EpubCheck {
|
|
|
5669
6336
|
}
|
|
5670
6337
|
const ncxContent = new TextDecoder().decode(ncxData);
|
|
5671
6338
|
const ncxValidator = new NCXValidator();
|
|
5672
|
-
ncxValidator.validate(context, ncxContent, ncxPath);
|
|
6339
|
+
ncxValidator.validate(context, ncxContent, ncxPath, registry);
|
|
5673
6340
|
if (context.ncxUid && context.packageDocument.uniqueIdentifier) {
|
|
5674
6341
|
const uniqueIdRef = context.packageDocument.uniqueIdentifier;
|
|
5675
6342
|
const matchingIdentifier = context.packageDocument.dcElements.find(
|