@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/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" && registry) {
1759
+ } else if (item.mediaType === "image/svg+xml") {
1760
1760
  const fullPath = opfDir ? `${opfDir}/${item.href}` : item.href;
1761
- this.extractSVGIDs(context, fullPath, registry);
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, doc, root) {
2021
- const navElements = root.find(".//html:nav", { html: "http://www.w3.org/1999/xhtml" });
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
- let tocNav;
2031
- let tocEpubTypeValue = "";
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
- if (epubTypeAttr?.value.includes("toc")) {
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 = epubTypeAttr.value;
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", { html: "http://www.w3.org/1999/xhtml" });
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.trim() === "") {
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 srcBase = src.split("#")[0] ?? src;
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.OPF_054,
4339
- message: "EPUB 3 metadata must include a dcterms:modified meta element",
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: `Invalid dcterms:modified date format "${modifiedMeta.value}"; must be W3C date format (ISO 8601)`,
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 hasNav = this.packageDoc.manifest.some((item) => item.properties?.includes("nav"));
4507
- if (!hasNav) {
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 (!validRoles.has(collection.role)) {
5315
+ if (collection.role === "manifest") {
4662
5316
  pushMessage(context.messages, {
4663
- id: MessageId.OPF_071,
4664
- message: `Unknown collection role: "${collection.role}"`,
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
- if (/[\s<>]/.test(url)) return true;
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.version === "2.0" && context.packageDocument) {
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.0)
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(