@likecoin/epubcheck-ts 0.3.5 → 0.3.6

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.cjs CHANGED
@@ -870,7 +870,7 @@ var MessageDefs = {
870
870
  NAV_001: {
871
871
  id: "NAV-001",
872
872
  severity: "error",
873
- description: "Nav file not supported for EPUB v2"
873
+ description: 'Navigation Document must have a nav element with epub:type="toc"'
874
874
  },
875
875
  NAV_002: { id: "NAV-002", severity: "suppressed", description: "Missing toc nav element" },
876
876
  NAV_003: {
@@ -1634,6 +1634,81 @@ var CSSValidator = class {
1634
1634
  }
1635
1635
  };
1636
1636
 
1637
+ // src/opf/types.ts
1638
+ var CORE_MEDIA_TYPES = /* @__PURE__ */ new Set([
1639
+ // Image types
1640
+ "image/gif",
1641
+ "image/jpeg",
1642
+ "image/png",
1643
+ "image/svg+xml",
1644
+ "image/webp",
1645
+ // Audio types
1646
+ "audio/mpeg",
1647
+ "audio/mp4",
1648
+ "audio/ogg",
1649
+ // CSS
1650
+ "text/css",
1651
+ // Fonts
1652
+ "font/otf",
1653
+ "font/ttf",
1654
+ "font/woff",
1655
+ "font/woff2",
1656
+ "application/font-sfnt",
1657
+ // deprecated alias for font/otf, font/ttf
1658
+ "application/font-woff",
1659
+ // deprecated alias for font/woff
1660
+ "application/vnd.ms-opentype",
1661
+ // deprecated alias
1662
+ // Content documents
1663
+ "application/xhtml+xml",
1664
+ "application/x-dtbncx+xml",
1665
+ // NCX
1666
+ // JavaScript (EPUB 3)
1667
+ "text/javascript",
1668
+ "application/javascript",
1669
+ // Media overlays
1670
+ "application/smil+xml",
1671
+ // PLS (Pronunciation Lexicon)
1672
+ "application/pls+xml"
1673
+ ]);
1674
+ function isCoreMediaType(mimeType) {
1675
+ if (CORE_MEDIA_TYPES.has(mimeType)) return true;
1676
+ if (mimeType.startsWith("video/")) return true;
1677
+ if (/^audio\/ogg\s*;\s*codecs=opus$/i.test(mimeType)) return true;
1678
+ const semicolonIndex = mimeType.indexOf(";");
1679
+ if (semicolonIndex >= 0) {
1680
+ const baseType = mimeType.substring(0, semicolonIndex).trim();
1681
+ if (CORE_MEDIA_TYPES.has(baseType)) return true;
1682
+ if (baseType.startsWith("video/")) return true;
1683
+ }
1684
+ return false;
1685
+ }
1686
+ var ITEM_PROPERTIES = /* @__PURE__ */ new Set([
1687
+ "cover-image",
1688
+ "mathml",
1689
+ "nav",
1690
+ "remote-resources",
1691
+ "scripted",
1692
+ "svg",
1693
+ "switch"
1694
+ ]);
1695
+ var LINK_PROPERTIES = /* @__PURE__ */ new Set(["onix", "marc21xml-record", "mods-record", "xmp-record"]);
1696
+ var SPINE_PROPERTIES = /* @__PURE__ */ new Set([
1697
+ "page-spread-left",
1698
+ "page-spread-right",
1699
+ "rendition:spread-none",
1700
+ "rendition:spread-landscape",
1701
+ "rendition:spread-portrait",
1702
+ "rendition:spread-both",
1703
+ "rendition:spread-auto",
1704
+ "rendition:page-spread-center",
1705
+ "rendition:layout-reflowable",
1706
+ "rendition:layout-pre-paginated",
1707
+ "rendition:orientation-auto",
1708
+ "rendition:orientation-landscape",
1709
+ "rendition:orientation-portrait"
1710
+ ]);
1711
+
1637
1712
  // src/references/types.ts
1638
1713
  function isPublicationResourceReference(type) {
1639
1714
  return [
@@ -1644,7 +1719,10 @@ function isPublicationResourceReference(type) {
1644
1719
  "audio" /* AUDIO */,
1645
1720
  "video" /* VIDEO */,
1646
1721
  "track" /* TRACK */,
1647
- "media-overlay" /* MEDIA_OVERLAY */
1722
+ "media-overlay" /* MEDIA_OVERLAY */,
1723
+ "svg-symbol" /* SVG_SYMBOL */,
1724
+ "svg-paint" /* SVG_PAINT */,
1725
+ "svg-clip-path" /* SVG_CLIP_PATH */
1648
1726
  ].includes(type);
1649
1727
  }
1650
1728
 
@@ -1702,6 +1780,17 @@ function isHTTP(url) {
1702
1780
  function isRemoteURL(url) {
1703
1781
  return isHTTP(url) || isHTTPS(url);
1704
1782
  }
1783
+ function checkUrlLeaking(href) {
1784
+ const TEST_BASE_A = "https://a.example.org/A/";
1785
+ const TEST_BASE_B = "https://b.example.org/B/";
1786
+ try {
1787
+ const urlA = new URL(href, TEST_BASE_A).toString();
1788
+ const urlB = new URL(href, TEST_BASE_B).toString();
1789
+ return !urlA.startsWith(TEST_BASE_A) || !urlB.startsWith(TEST_BASE_B);
1790
+ } catch {
1791
+ return false;
1792
+ }
1793
+ }
1705
1794
  function resolveManifestHref(opfDir, href) {
1706
1795
  if (isRemoteURL(href)) return href;
1707
1796
  try {
@@ -1837,6 +1926,9 @@ var ContentValidator = class {
1837
1926
  if (context.version.startsWith("3")) {
1838
1927
  this.validateSVGDocument(context, fullPath, item);
1839
1928
  }
1929
+ if (refValidator) {
1930
+ this.extractSVGReferences(context, fullPath, opfDir, refValidator);
1931
+ }
1840
1932
  }
1841
1933
  }
1842
1934
  }
@@ -1884,6 +1976,133 @@ var ContentValidator = class {
1884
1976
  doc.dispose();
1885
1977
  }
1886
1978
  }
1979
+ /**
1980
+ * Extract references from SVG documents: font-face-uri, xml-stylesheet PI, @import in style
1981
+ */
1982
+ extractSVGReferences(context, path, opfDir, refValidator) {
1983
+ const svgData = context.files.get(path);
1984
+ if (!svgData) return;
1985
+ const svgContent = new TextDecoder().decode(svgData);
1986
+ let doc;
1987
+ try {
1988
+ doc = libxml2Wasm.XmlDocument.fromString(svgContent);
1989
+ } catch {
1990
+ return;
1991
+ }
1992
+ const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
1993
+ try {
1994
+ const root = doc.root;
1995
+ try {
1996
+ const fontFaceUris = root.find(".//svg:font-face-uri", {
1997
+ svg: "http://www.w3.org/2000/svg"
1998
+ });
1999
+ for (const uri of fontFaceUris) {
2000
+ const href = this.getAttribute(uri, "xlink:href") ?? this.getAttribute(uri, "href");
2001
+ if (!href) continue;
2002
+ if (href.startsWith("http://") || href.startsWith("https://")) {
2003
+ refValidator.addReference({
2004
+ url: href,
2005
+ targetResource: href,
2006
+ type: "font" /* FONT */,
2007
+ location: { path, line: uri.line }
2008
+ });
2009
+ } else {
2010
+ const resolvedPath = this.resolveRelativePath(docDir, href, opfDir);
2011
+ refValidator.addReference({
2012
+ url: href,
2013
+ targetResource: resolvedPath,
2014
+ type: "font" /* FONT */,
2015
+ location: { path, line: uri.line }
2016
+ });
2017
+ }
2018
+ }
2019
+ } catch {
2020
+ }
2021
+ try {
2022
+ const styles = root.find(".//svg:style", { svg: "http://www.w3.org/2000/svg" });
2023
+ for (const style of styles) {
2024
+ const cssContent = style.content;
2025
+ if (cssContent) {
2026
+ this.extractCSSImports(path, cssContent, opfDir, refValidator);
2027
+ }
2028
+ }
2029
+ } catch {
2030
+ }
2031
+ try {
2032
+ const svgUseXlink = root.find(".//svg:use[@xlink:href]", {
2033
+ svg: "http://www.w3.org/2000/svg",
2034
+ xlink: "http://www.w3.org/1999/xlink"
2035
+ });
2036
+ const svgUseHref = root.find(".//svg:use[@href]", {
2037
+ svg: "http://www.w3.org/2000/svg"
2038
+ });
2039
+ for (const useNode of [...svgUseXlink, ...svgUseHref]) {
2040
+ const useElem = useNode;
2041
+ const href = this.getAttribute(useElem, "xlink:href") ?? this.getAttribute(useElem, "href");
2042
+ if (!href) continue;
2043
+ if (href.startsWith("http://") || href.startsWith("https://")) continue;
2044
+ if (!href.includes("#")) {
2045
+ pushMessage(context.messages, {
2046
+ id: MessageId.RSC_015,
2047
+ message: `SVG "use" element requires a fragment identifier, but found "${href}"`,
2048
+ location: { path, line: useNode.line }
2049
+ });
2050
+ continue;
2051
+ }
2052
+ const resolvedPath = this.resolveRelativePath(docDir, href, opfDir);
2053
+ const hashIndex = resolvedPath.indexOf("#");
2054
+ const targetResource = hashIndex >= 0 ? resolvedPath.slice(0, hashIndex) : path;
2055
+ const fragment = hashIndex >= 0 ? resolvedPath.slice(hashIndex + 1) : void 0;
2056
+ const useRef = {
2057
+ url: href,
2058
+ targetResource,
2059
+ type: "svg-symbol" /* SVG_SYMBOL */,
2060
+ location: { path, line: useNode.line }
2061
+ };
2062
+ if (fragment) {
2063
+ useRef.fragment = fragment;
2064
+ }
2065
+ refValidator.addReference(useRef);
2066
+ }
2067
+ } catch {
2068
+ }
2069
+ } finally {
2070
+ doc.dispose();
2071
+ }
2072
+ this.extractXmlStylesheetPIs(svgContent, path, docDir, opfDir, refValidator);
2073
+ }
2074
+ /**
2075
+ * Extract href from <?xml-stylesheet?> processing instructions
2076
+ */
2077
+ extractXmlStylesheetPIs(content, path, docDir, opfDir, refValidator) {
2078
+ const piRegex = /<\?xml-stylesheet\s+([^?]*)\?>/g;
2079
+ let match;
2080
+ while ((match = piRegex.exec(content)) !== null) {
2081
+ const attrs = match[1];
2082
+ if (!attrs) continue;
2083
+ const hrefMatch = /href\s*=\s*["']([^"']*)["']/.exec(attrs);
2084
+ if (!hrefMatch?.[1]) continue;
2085
+ const href = hrefMatch[1];
2086
+ const beforeMatch = content.substring(0, match.index);
2087
+ const line = beforeMatch.split("\n").length;
2088
+ if (href.startsWith("http://") || href.startsWith("https://")) {
2089
+ refValidator.addReference({
2090
+ url: href,
2091
+ targetResource: href,
2092
+ type: "stylesheet" /* STYLESHEET */,
2093
+ location: { path, line }
2094
+ });
2095
+ } else {
2096
+ const resolvedPath = this.resolveRelativePath(docDir, href, opfDir);
2097
+ refValidator.addReference({
2098
+ url: href,
2099
+ targetResource: resolvedPath,
2100
+ type: "stylesheet" /* STYLESHEET */,
2101
+ location: { path, line }
2102
+ });
2103
+ }
2104
+ }
2105
+ }
1887
2106
  detectSVGRemoteResources(root) {
1888
2107
  try {
1889
2108
  const fontFaceUris = root.find(".//svg:font-face-uri", {
@@ -1949,9 +2168,11 @@ var ContentValidator = class {
1949
2168
  for (const ref of result.references) {
1950
2169
  if (ref.type === "font") {
1951
2170
  if (ref.url.startsWith("http://") || ref.url.startsWith("https://")) {
2171
+ const hashIndex = ref.url.indexOf("#");
2172
+ const targetResource = hashIndex >= 0 ? ref.url.slice(0, hashIndex) : ref.url;
1952
2173
  refValidator.addReference({
1953
2174
  url: ref.url,
1954
- targetResource: ref.url,
2175
+ targetResource,
1955
2176
  type: "font" /* FONT */,
1956
2177
  location: { path }
1957
2178
  });
@@ -1968,9 +2189,11 @@ var ContentValidator = class {
1968
2189
  }
1969
2190
  } else if (ref.type === "image") {
1970
2191
  if (ref.url.startsWith("http://") || ref.url.startsWith("https://")) {
2192
+ const hashIndex = ref.url.indexOf("#");
2193
+ const targetResource = hashIndex >= 0 ? ref.url.slice(0, hashIndex) : ref.url;
1971
2194
  refValidator.addReference({
1972
2195
  url: ref.url,
1973
- targetResource: ref.url,
2196
+ targetResource,
1974
2197
  type: "image" /* IMAGE */,
1975
2198
  location: { path }
1976
2199
  });
@@ -1985,9 +2208,27 @@ var ContentValidator = class {
1985
2208
  location: { path }
1986
2209
  });
1987
2210
  }
2211
+ } else if (ref.type === "import") {
2212
+ const location = { path };
2213
+ if (ref.line !== void 0) location.line = ref.line;
2214
+ if (ref.url.startsWith("http://") || ref.url.startsWith("https://")) {
2215
+ refValidator.addReference({
2216
+ url: ref.url,
2217
+ targetResource: ref.url,
2218
+ type: "stylesheet" /* STYLESHEET */,
2219
+ location
2220
+ });
2221
+ } else {
2222
+ const resolvedPath = this.resolveRelativePath(cssDir, ref.url, opfDir);
2223
+ refValidator.addReference({
2224
+ url: ref.url,
2225
+ targetResource: resolvedPath,
2226
+ type: "stylesheet" /* STYLESHEET */,
2227
+ location
2228
+ });
2229
+ }
1988
2230
  }
1989
2231
  }
1990
- this.extractCSSImports(path, cssContent, opfDir, refValidator);
1991
2232
  }
1992
2233
  validateXHTMLDocument(context, path, itemId, opfDir, registry, refValidator) {
1993
2234
  const data = context.files.get(path);
@@ -2094,6 +2335,13 @@ var ContentValidator = class {
2094
2335
  location: { path }
2095
2336
  });
2096
2337
  }
2338
+ if (!hasMathML && manifestItem?.properties?.includes("mathml")) {
2339
+ pushMessage(context.messages, {
2340
+ id: MessageId.OPF_015,
2341
+ message: 'The property "mathml" should not be declared in the OPF file',
2342
+ location: { path }
2343
+ });
2344
+ }
2097
2345
  const hasSVG = this.detectSVG(context, path, root);
2098
2346
  if (hasSVG && !manifestItem?.properties?.includes("svg")) {
2099
2347
  pushMessage(context.messages, {
@@ -2117,6 +2365,13 @@ var ContentValidator = class {
2117
2365
  location: { path }
2118
2366
  });
2119
2367
  }
2368
+ if (!hasSwitch && manifestItem?.properties?.includes("switch")) {
2369
+ pushMessage(context.messages, {
2370
+ id: MessageId.OPF_015,
2371
+ message: 'The property "switch" should not be declared in the OPF file',
2372
+ location: { path }
2373
+ });
2374
+ }
2120
2375
  const hasRemoteResources = this.detectRemoteResources(context, path, root);
2121
2376
  if (hasRemoteResources && !manifestItem?.properties?.includes("remote-resources")) {
2122
2377
  pushMessage(context.messages, {
@@ -2145,13 +2400,21 @@ var ContentValidator = class {
2145
2400
  this.extractAndRegisterIDs(path, root, registry);
2146
2401
  }
2147
2402
  if (refValidator && opfDir !== void 0) {
2148
- this.extractAndRegisterHyperlinks(context, path, root, opfDir, refValidator);
2403
+ this.extractAndRegisterHyperlinks(context, path, root, opfDir, refValidator, !!isNavItem);
2149
2404
  this.extractAndRegisterStylesheets(path, root, opfDir, refValidator);
2150
- this.extractAndRegisterImages(path, root, opfDir, refValidator);
2405
+ this.extractAndRegisterImages(context, path, root, opfDir, refValidator, registry);
2151
2406
  this.extractAndRegisterMathMLAltimg(path, root, opfDir, refValidator);
2152
2407
  this.extractAndRegisterScripts(path, root, opfDir, refValidator);
2153
2408
  this.extractAndRegisterCiteAttributes(path, root, opfDir, refValidator);
2154
- this.extractAndRegisterMediaElements(path, root, opfDir, refValidator);
2409
+ this.extractAndRegisterMediaElements(context, path, root, opfDir, refValidator, registry);
2410
+ this.extractAndRegisterEmbeddedElements(
2411
+ context,
2412
+ path,
2413
+ root,
2414
+ opfDir,
2415
+ refValidator,
2416
+ registry
2417
+ );
2155
2418
  }
2156
2419
  } finally {
2157
2420
  doc.dispose();
@@ -2207,14 +2470,12 @@ var ContentValidator = class {
2207
2470
  return epubTypeAttr ? epubTypeAttr.value.trim().split(/\s+/) : [];
2208
2471
  };
2209
2472
  let tocNav;
2210
- let tocEpubTypeValue = "";
2211
2473
  let pageListCount = 0;
2212
2474
  let landmarksCount = 0;
2213
2475
  for (const nav of navElements) {
2214
2476
  const types = getNavTypes(nav);
2215
2477
  if (types.includes("toc") && !tocNav) {
2216
2478
  tocNav = nav;
2217
- tocEpubTypeValue = types.join(" ");
2218
2479
  }
2219
2480
  if (types.includes("page-list")) pageListCount++;
2220
2481
  if (types.includes("landmarks")) landmarksCount++;
@@ -2265,7 +2526,7 @@ var ContentValidator = class {
2265
2526
  }
2266
2527
  this.checkNavHeadingContent(context, path, root);
2267
2528
  this.checkNavHiddenAttribute(context, path, root);
2268
- this.checkNavRemoteLinks(context, path, root, tocEpubTypeValue);
2529
+ this.checkNavRemoteLinks(context, path, root);
2269
2530
  this.collectTocLinks(context, path, tocNav);
2270
2531
  }
2271
2532
  checkNavFirstChildHeading(context, path, navElem) {
@@ -2448,24 +2709,30 @@ var ContentValidator = class {
2448
2709
  }
2449
2710
  }
2450
2711
  }
2451
- checkNavRemoteLinks(context, path, root, epubTypeValue) {
2452
- const navTypes = epubTypeValue.split(/\s+/);
2453
- const isToc = navTypes.includes("toc");
2454
- const isLandmarks = navTypes.includes("landmarks");
2455
- const isPageList = navTypes.includes("page-list");
2456
- if (!isToc && !isLandmarks && !isPageList) {
2457
- return;
2458
- }
2459
- const links = root.find(".//html:a[@href]", { html: "http://www.w3.org/1999/xhtml" });
2460
- for (const link of links) {
2461
- const href = this.getAttribute(link, "href");
2462
- if (href && (href.startsWith("http://") || href.startsWith("https://"))) {
2463
- const navType = isToc ? "toc" : isLandmarks ? "landmarks" : "page-list";
2464
- pushMessage(context.messages, {
2465
- id: MessageId.NAV_010,
2466
- message: `"${navType}" nav must not link to remote resources; found link to "${href}"`,
2467
- location: { path }
2468
- });
2712
+ checkNavRemoteLinks(context, path, root) {
2713
+ const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
2714
+ const navElements = root.find(".//html:nav", HTML_NS);
2715
+ for (const nav of navElements) {
2716
+ const navElem = nav;
2717
+ const epubTypeAttr = "attrs" in navElem ? navElem.attrs.find(
2718
+ (attr) => attr.name === "type" && attr.prefix === "epub" && attr.namespaceUri === "http://www.idpf.org/2007/ops"
2719
+ ) : void 0;
2720
+ const types = epubTypeAttr ? epubTypeAttr.value.trim().split(/\s+/) : [];
2721
+ const isToc = types.includes("toc");
2722
+ const isLandmarks = types.includes("landmarks");
2723
+ const isPageList = types.includes("page-list");
2724
+ if (!isToc && !isLandmarks && !isPageList) continue;
2725
+ const navType = isToc ? "toc" : isLandmarks ? "landmarks" : "page-list";
2726
+ const links = navElem.find(".//html:a[@href]", HTML_NS);
2727
+ for (const link of links) {
2728
+ const href = this.getAttribute(link, "href");
2729
+ if (href && (href.startsWith("http://") || href.startsWith("https://"))) {
2730
+ pushMessage(context.messages, {
2731
+ id: MessageId.NAV_010,
2732
+ message: `"${navType}" nav must not link to remote resources; found link to "${href}"`,
2733
+ location: { path }
2734
+ });
2735
+ }
2469
2736
  }
2470
2737
  }
2471
2738
  }
@@ -2603,6 +2870,20 @@ var ContentValidator = class {
2603
2870
  return true;
2604
2871
  }
2605
2872
  }
2873
+ const objects = root.find(".//html:object[@data]", { html: "http://www.w3.org/1999/xhtml" });
2874
+ for (const obj of objects) {
2875
+ const data = this.getAttribute(obj, "data");
2876
+ if (data && (data.startsWith("http://") || data.startsWith("https://"))) {
2877
+ return true;
2878
+ }
2879
+ }
2880
+ const embeds = root.find(".//html:embed[@src]", { html: "http://www.w3.org/1999/xhtml" });
2881
+ for (const embed of embeds) {
2882
+ const src = this.getAttribute(embed, "src");
2883
+ if (src && (src.startsWith("http://") || src.startsWith("https://"))) {
2884
+ return true;
2885
+ }
2886
+ }
2606
2887
  const linkElements = root.find(".//html:link[@rel and @href]", {
2607
2888
  html: "http://www.w3.org/1999/xhtml"
2608
2889
  });
@@ -2881,14 +3162,38 @@ var ContentValidator = class {
2881
3162
  extractAndRegisterIDs(path, root, registry) {
2882
3163
  const elementsWithId = root.find(".//*[@id]");
2883
3164
  for (const elem of elementsWithId) {
2884
- const id = this.getAttribute(elem, "id");
3165
+ const xmlElem = elem;
3166
+ const id = this.getAttribute(xmlElem, "id");
2885
3167
  if (id) {
2886
3168
  registry.registerID(path, id);
3169
+ const localName = xmlElem.name.includes(":") ? xmlElem.name.split(":").pop() : xmlElem.name;
3170
+ if (localName === "symbol") {
3171
+ registry.registerSVGSymbolID(path, id);
3172
+ }
2887
3173
  }
2888
3174
  }
2889
3175
  }
2890
- extractAndRegisterHyperlinks(context, path, root, opfDir, refValidator) {
3176
+ extractAndRegisterHyperlinks(context, path, root, opfDir, refValidator, isNavDocument = false) {
2891
3177
  const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
3178
+ const navAnchorTypes = /* @__PURE__ */ new Map();
3179
+ if (isNavDocument) {
3180
+ const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
3181
+ const navElements = root.find(".//html:nav", HTML_NS);
3182
+ for (const nav of navElements) {
3183
+ const navElem = nav;
3184
+ const epubTypeAttr = "attrs" in navElem ? navElem.attrs.find(
3185
+ (attr) => attr.name === "type" && attr.prefix === "epub" && attr.namespaceUri === "http://www.idpf.org/2007/ops"
3186
+ ) : void 0;
3187
+ const types = epubTypeAttr ? epubTypeAttr.value.trim().split(/\s+/) : [];
3188
+ let refType = "hyperlink" /* HYPERLINK */;
3189
+ if (types.includes("toc")) refType = "nav-toc-link" /* NAV_TOC_LINK */;
3190
+ else if (types.includes("page-list")) refType = "nav-pagelist-link" /* NAV_PAGELIST_LINK */;
3191
+ const navAnchors = navElem.find(".//html:a[@href]", HTML_NS);
3192
+ for (const a of navAnchors) {
3193
+ navAnchorTypes.set(a.line, refType);
3194
+ }
3195
+ }
3196
+ }
2892
3197
  const links = root.find(".//html:a[@href]", { html: "http://www.w3.org/1999/xhtml" });
2893
3198
  for (const link of links) {
2894
3199
  const href = this.getAttribute(link, "href")?.trim() ?? null;
@@ -2902,6 +3207,7 @@ var ContentValidator = class {
2902
3207
  continue;
2903
3208
  }
2904
3209
  const line = link.line;
3210
+ const refType = isNavDocument ? navAnchorTypes.get(line) ?? "hyperlink" /* HYPERLINK */ : "hyperlink" /* HYPERLINK */;
2905
3211
  if (href.startsWith("http://") || href.startsWith("https://")) {
2906
3212
  continue;
2907
3213
  }
@@ -2918,7 +3224,7 @@ var ContentValidator = class {
2918
3224
  url: href,
2919
3225
  targetResource: targetResource2,
2920
3226
  fragment,
2921
- type: "hyperlink" /* HYPERLINK */,
3227
+ type: refType,
2922
3228
  location: { path, line }
2923
3229
  });
2924
3230
  continue;
@@ -2930,7 +3236,7 @@ var ContentValidator = class {
2930
3236
  const ref = {
2931
3237
  url: href,
2932
3238
  targetResource,
2933
- type: "hyperlink" /* HYPERLINK */,
3239
+ type: refType,
2934
3240
  location: { path, line }
2935
3241
  };
2936
3242
  if (fragmentPart) {
@@ -2938,6 +3244,39 @@ var ContentValidator = class {
2938
3244
  }
2939
3245
  refValidator.addReference(ref);
2940
3246
  }
3247
+ const areaLinks = root.find(".//html:area[@href]", { html: "http://www.w3.org/1999/xhtml" });
3248
+ for (const area of areaLinks) {
3249
+ const href = this.getAttribute(area, "href")?.trim();
3250
+ if (!href) continue;
3251
+ const line = area.line;
3252
+ if (href.startsWith("http://") || href.startsWith("https://")) continue;
3253
+ if (href.startsWith("mailto:") || href.startsWith("tel:")) continue;
3254
+ if (href.includes("#epubcfi(")) continue;
3255
+ if (href.startsWith("#")) {
3256
+ refValidator.addReference({
3257
+ url: href,
3258
+ targetResource: path,
3259
+ fragment: href.slice(1),
3260
+ type: "hyperlink" /* HYPERLINK */,
3261
+ location: { path, line }
3262
+ });
3263
+ continue;
3264
+ }
3265
+ const resolvedAreaPath = this.resolveRelativePath(docDir, href, opfDir);
3266
+ const areaHashIndex = resolvedAreaPath.indexOf("#");
3267
+ const areaTarget = areaHashIndex >= 0 ? resolvedAreaPath.slice(0, areaHashIndex) : resolvedAreaPath;
3268
+ const areaFragment = areaHashIndex >= 0 ? resolvedAreaPath.slice(areaHashIndex + 1) : void 0;
3269
+ const areaRef = {
3270
+ url: href,
3271
+ targetResource: areaTarget,
3272
+ type: "hyperlink" /* HYPERLINK */,
3273
+ location: { path, line }
3274
+ };
3275
+ if (areaFragment) {
3276
+ areaRef.fragment = areaFragment;
3277
+ }
3278
+ refValidator.addReference(areaRef);
3279
+ }
2941
3280
  const svgLinks = root.find(".//svg:a", {
2942
3281
  svg: "http://www.w3.org/2000/svg",
2943
3282
  xlink: "http://www.w3.org/1999/xlink"
@@ -3014,10 +3353,10 @@ var ContentValidator = class {
3014
3353
  extractCSSImports(cssPath, cssContent, opfDir, refValidator) {
3015
3354
  const cssDir = cssPath.includes("/") ? cssPath.substring(0, cssPath.lastIndexOf("/")) : "";
3016
3355
  const cleanedCSS = cssContent.replace(/\/\*[\s\S]*?\*\//g, "");
3017
- const importRegex = /@import\s+(?:url\s*\(\s*)?["']([^"']+)["']\s*\)?[^;]*;/gi;
3356
+ const importRegex = /@import\s+(?:url\s*\(\s*["']?([^"')]+?)["']?\s*\)|["']([^"']+)["'])[^;]*;/gi;
3018
3357
  let match;
3019
3358
  while ((match = importRegex.exec(cleanedCSS)) !== null) {
3020
- const importUrl = match[1];
3359
+ const importUrl = match[1] ?? match[2];
3021
3360
  if (!importUrl) continue;
3022
3361
  const beforeMatch = cleanedCSS.substring(0, match.index);
3023
3362
  const line = beforeMatch.split("\n").length;
@@ -3039,21 +3378,56 @@ var ContentValidator = class {
3039
3378
  });
3040
3379
  }
3041
3380
  }
3042
- extractAndRegisterImages(path, root, opfDir, refValidator) {
3381
+ extractAndRegisterImages(context, path, root, opfDir, refValidator, registry) {
3043
3382
  const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
3044
- const images = root.find(".//html:img[@src]", { html: "http://www.w3.org/1999/xhtml" });
3383
+ const ns = { html: "http://www.w3.org/1999/xhtml" };
3384
+ const pictureHasCMTSource = /* @__PURE__ */ new Set();
3385
+ if (registry) {
3386
+ const pictures = root.find(".//html:picture", ns);
3387
+ for (const pic of pictures) {
3388
+ const picElem = pic;
3389
+ const sources = picElem.find("html:source[@src]", ns);
3390
+ const sourcesWithSrcset = picElem.find("html:source[@srcset]", ns);
3391
+ for (const source of [...sources, ...sourcesWithSrcset]) {
3392
+ const srcAttr = this.getAttribute(source, "src");
3393
+ const srcsetAttr = this.getAttribute(source, "srcset");
3394
+ const sourceUrl = srcAttr ?? srcsetAttr?.split(",")[0]?.trim().split(/\s+/)[0];
3395
+ if (!sourceUrl || sourceUrl.startsWith("http://") || sourceUrl.startsWith("https://"))
3396
+ continue;
3397
+ const resolvedSource = this.resolveRelativePath(docDir, sourceUrl, opfDir);
3398
+ const resource = registry.getResource(resolvedSource);
3399
+ if (resource && isCoreMediaType(resource.mimeType)) {
3400
+ pictureHasCMTSource.add(pic.line);
3401
+ break;
3402
+ }
3403
+ }
3404
+ }
3405
+ }
3406
+ const images = root.find(".//html:img[@src]", ns);
3045
3407
  for (const img of images) {
3046
3408
  const imgElem = img;
3047
3409
  const src = this.getAttribute(imgElem, "src");
3048
3410
  if (!src) continue;
3049
3411
  const line = img.line;
3412
+ let hasIntrinsicFallback;
3413
+ if (pictureHasCMTSource.size > 0) {
3414
+ try {
3415
+ const pictureParent = imgElem.get("ancestor::html:picture", ns);
3416
+ if (pictureParent && pictureHasCMTSource.has(pictureParent.line)) {
3417
+ hasIntrinsicFallback = true;
3418
+ }
3419
+ } catch {
3420
+ }
3421
+ }
3050
3422
  if (src.startsWith("http://") || src.startsWith("https://")) {
3051
- refValidator.addReference({
3423
+ const ref = {
3052
3424
  url: src,
3053
3425
  targetResource: src,
3054
3426
  type: "image" /* IMAGE */,
3055
3427
  location: { path, line }
3056
- });
3428
+ };
3429
+ if (hasIntrinsicFallback) ref.hasIntrinsicFallback = true;
3430
+ refValidator.addReference(ref);
3057
3431
  } else {
3058
3432
  const resolvedPath = this.resolveRelativePath(docDir, src, opfDir);
3059
3433
  const hashIndex = resolvedPath.indexOf("#");
@@ -3065,6 +3439,7 @@ var ContentValidator = class {
3065
3439
  type: "image" /* IMAGE */,
3066
3440
  location: { path, line }
3067
3441
  };
3442
+ if (hasIntrinsicFallback) ref.hasIntrinsicFallback = true;
3068
3443
  if (fragment) {
3069
3444
  ref.fragment = fragment;
3070
3445
  }
@@ -3117,6 +3492,45 @@ var ContentValidator = class {
3117
3492
  }
3118
3493
  refValidator.addReference(svgImgRef);
3119
3494
  }
3495
+ try {
3496
+ const svgUseXlink = root.find(".//svg:use[@xlink:href]", {
3497
+ svg: "http://www.w3.org/2000/svg",
3498
+ xlink: "http://www.w3.org/1999/xlink"
3499
+ });
3500
+ const svgUseHref = root.find(".//svg:use[@href]", {
3501
+ svg: "http://www.w3.org/2000/svg"
3502
+ });
3503
+ for (const useNode of [...svgUseXlink, ...svgUseHref]) {
3504
+ const useElem = useNode;
3505
+ const href = this.getAttribute(useElem, "xlink:href") ?? this.getAttribute(useElem, "href");
3506
+ if (href === null) continue;
3507
+ const line = useNode.line;
3508
+ if (href.startsWith("http://") || href.startsWith("https://")) continue;
3509
+ if (href === "" || !href.includes("#")) {
3510
+ pushMessage(context.messages, {
3511
+ id: MessageId.RSC_015,
3512
+ message: `SVG "use" element requires a fragment identifier, but found "${href}"`,
3513
+ location: { path, line }
3514
+ });
3515
+ continue;
3516
+ }
3517
+ const resolvedPath = this.resolveRelativePath(docDir, href, opfDir);
3518
+ const hashIndex = resolvedPath.indexOf("#");
3519
+ const targetResource = hashIndex >= 0 ? resolvedPath.slice(0, hashIndex) : path;
3520
+ const fragment = hashIndex >= 0 ? resolvedPath.slice(hashIndex + 1) : void 0;
3521
+ const useRef = {
3522
+ url: href,
3523
+ targetResource,
3524
+ type: "svg-symbol" /* SVG_SYMBOL */,
3525
+ location: { path, line }
3526
+ };
3527
+ if (fragment) {
3528
+ useRef.fragment = fragment;
3529
+ }
3530
+ refValidator.addReference(useRef);
3531
+ }
3532
+ } catch {
3533
+ }
3120
3534
  const videos = root.find(".//html:video[@poster]", { html: "http://www.w3.org/1999/xhtml" });
3121
3535
  for (const video of videos) {
3122
3536
  const poster = this.getAttribute(video, "poster");
@@ -3218,7 +3632,7 @@ var ContentValidator = class {
3218
3632
  url: cite,
3219
3633
  targetResource: targetResource2,
3220
3634
  fragment: fragment2,
3221
- type: "hyperlink" /* HYPERLINK */,
3635
+ type: "cite" /* CITE */,
3222
3636
  location: { path, line }
3223
3637
  });
3224
3638
  continue;
@@ -3230,93 +3644,71 @@ var ContentValidator = class {
3230
3644
  const ref = {
3231
3645
  url: cite,
3232
3646
  targetResource,
3233
- type: "hyperlink" /* HYPERLINK */,
3647
+ type: "cite" /* CITE */,
3234
3648
  location: { path, line }
3235
3649
  };
3236
3650
  if (fragment) {
3237
3651
  ref.fragment = fragment;
3238
3652
  }
3239
3653
  refValidator.addReference(ref);
3240
- }
3241
- }
3242
- extractAndRegisterMediaElements(path, root, opfDir, refValidator) {
3243
- const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
3244
- const audioElements = root.find(".//html:audio[@src]", {
3245
- html: "http://www.w3.org/1999/xhtml"
3246
- });
3247
- for (const audio of audioElements) {
3248
- const src = this.getAttribute(audio, "src");
3249
- if (!src) continue;
3250
- const line = audio.line;
3251
- if (src.startsWith("http://") || src.startsWith("https://")) {
3252
- refValidator.addReference({
3253
- url: src,
3254
- targetResource: src,
3255
- type: "audio" /* AUDIO */,
3256
- location: line !== void 0 ? { path, line } : { path }
3257
- });
3258
- } else {
3259
- const resolvedPath = this.resolveRelativePath(docDir, src, opfDir);
3260
- refValidator.addReference({
3261
- url: src,
3262
- targetResource: resolvedPath,
3263
- type: "audio" /* AUDIO */,
3264
- location: line !== void 0 ? { path, line } : { path }
3265
- });
3266
- }
3267
- }
3268
- const videoElements = root.find(".//html:video[@src]", {
3269
- html: "http://www.w3.org/1999/xhtml"
3270
- });
3271
- for (const video of videoElements) {
3272
- const src = this.getAttribute(video, "src");
3273
- if (!src) continue;
3274
- const line = video.line;
3275
- if (src.startsWith("http://") || src.startsWith("https://")) {
3276
- refValidator.addReference({
3277
- url: src,
3278
- targetResource: src,
3279
- type: "video" /* VIDEO */,
3280
- location: line !== void 0 ? { path, line } : { path }
3281
- });
3282
- } else {
3283
- const resolvedPath = this.resolveRelativePath(docDir, src, opfDir);
3284
- refValidator.addReference({
3285
- url: src,
3286
- targetResource: resolvedPath,
3287
- type: "video" /* VIDEO */,
3288
- location: line !== void 0 ? { path, line } : { path }
3289
- });
3290
- }
3291
- }
3292
- const sourceElements = root.find(".//html:source[@src]", {
3293
- html: "http://www.w3.org/1999/xhtml"
3294
- });
3295
- for (const source of sourceElements) {
3296
- const src = this.getAttribute(source, "src");
3297
- if (!src) continue;
3298
- const parent = source.parent;
3299
- const parentName = parent?.name ?? "";
3300
- const isAudioChild = parentName === "audio";
3301
- const type = isAudioChild ? "audio" /* AUDIO */ : "video" /* VIDEO */;
3302
- const line = source.line;
3303
- if (src.startsWith("http://") || src.startsWith("https://")) {
3304
- refValidator.addReference({
3305
- url: src,
3306
- targetResource: src,
3307
- type,
3308
- location: line !== void 0 ? { path, line } : { path }
3309
- });
3310
- } else {
3311
- const resolvedPath = this.resolveRelativePath(docDir, src, opfDir);
3312
- refValidator.addReference({
3313
- url: src,
3314
- targetResource: resolvedPath,
3315
- type,
3316
- location: line !== void 0 ? { path, line } : { path }
3317
- });
3654
+ }
3655
+ }
3656
+ extractAndRegisterMediaElements(context, path, root, opfDir, refValidator, registry) {
3657
+ const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
3658
+ const ns = { html: "http://www.w3.org/1999/xhtml" };
3659
+ for (const tagName of ["audio", "video"]) {
3660
+ const isAudio = tagName === "audio";
3661
+ const refType = isAudio ? "audio" /* AUDIO */ : "video" /* VIDEO */;
3662
+ const elements = root.find(`.//html:${tagName}`, ns);
3663
+ for (const elem of elements) {
3664
+ const mediaElem = elem;
3665
+ const pendingRefs = [];
3666
+ const src = this.getAttribute(mediaElem, "src");
3667
+ if (src) {
3668
+ const line = elem.line;
3669
+ if (src.startsWith("http://") || src.startsWith("https://")) {
3670
+ pendingRefs.push({ url: src, targetResource: src, type: refType, line });
3671
+ } else {
3672
+ const resolvedPath = this.resolveRelativePath(docDir, src, opfDir);
3673
+ pendingRefs.push({ url: src, targetResource: resolvedPath, type: refType, line });
3674
+ }
3675
+ }
3676
+ const sources = mediaElem.find("html:source[@src]", ns);
3677
+ for (const source of sources) {
3678
+ const sourceElem = source;
3679
+ const sourceSrc = this.getAttribute(sourceElem, "src");
3680
+ if (!sourceSrc) continue;
3681
+ const line = source.line;
3682
+ if (sourceSrc.startsWith("http://") || sourceSrc.startsWith("https://")) {
3683
+ pendingRefs.push({ url: sourceSrc, targetResource: sourceSrc, type: refType, line });
3684
+ } else {
3685
+ const resolvedPath = this.resolveRelativePath(docDir, sourceSrc, opfDir);
3686
+ pendingRefs.push({ url: sourceSrc, targetResource: resolvedPath, type: refType, line });
3687
+ }
3688
+ if (registry) {
3689
+ this.checkMimeTypeMatch(context, path, docDir, opfDir, sourceElem, "src", registry);
3690
+ }
3691
+ }
3692
+ let hasIntrinsicFallback = false;
3693
+ if (registry && pendingRefs.length > 1) {
3694
+ hasIntrinsicFallback = pendingRefs.some((ref) => {
3695
+ const resource = registry.getResource(ref.targetResource);
3696
+ return resource && isCoreMediaType(resource.mimeType);
3697
+ });
3698
+ }
3699
+ for (const ref of pendingRefs) {
3700
+ const reference = {
3701
+ url: ref.url,
3702
+ targetResource: ref.targetResource,
3703
+ type: ref.type,
3704
+ location: ref.line !== void 0 ? { path, line: ref.line } : { path }
3705
+ };
3706
+ if (hasIntrinsicFallback) reference.hasIntrinsicFallback = true;
3707
+ refValidator.addReference(reference);
3708
+ }
3318
3709
  }
3319
3710
  }
3711
+ this.extractAndRegisterPictureElements(context, path, root, opfDir, refValidator, registry);
3320
3712
  const iframeElements = root.find(".//html:iframe[@src]", {
3321
3713
  html: "http://www.w3.org/1999/xhtml"
3322
3714
  });
@@ -3366,6 +3758,188 @@ var ContentValidator = class {
3366
3758
  }
3367
3759
  }
3368
3760
  }
3761
+ extractAndRegisterEmbeddedElements(context, path, root, opfDir, refValidator, registry) {
3762
+ const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
3763
+ const ns = { html: "http://www.w3.org/1999/xhtml" };
3764
+ const addRef = (src, type, line, hasIntrinsicFallback) => {
3765
+ const location = line !== void 0 ? { path, line } : { path };
3766
+ if (src.startsWith("http://") || src.startsWith("https://")) {
3767
+ const ref = {
3768
+ url: src,
3769
+ targetResource: src,
3770
+ type,
3771
+ location
3772
+ };
3773
+ if (hasIntrinsicFallback) ref.hasIntrinsicFallback = true;
3774
+ refValidator.addReference(ref);
3775
+ } else {
3776
+ const resolvedPath = this.resolveRelativePath(docDir, src, opfDir);
3777
+ const hashIndex = resolvedPath.indexOf("#");
3778
+ const targetResource = hashIndex >= 0 ? resolvedPath.slice(0, hashIndex) : resolvedPath;
3779
+ const ref = {
3780
+ url: src,
3781
+ targetResource,
3782
+ type,
3783
+ location
3784
+ };
3785
+ if (hashIndex >= 0) ref.fragment = resolvedPath.slice(hashIndex + 1);
3786
+ if (hasIntrinsicFallback) ref.hasIntrinsicFallback = true;
3787
+ refValidator.addReference(ref);
3788
+ }
3789
+ };
3790
+ for (const elem of root.find(".//html:embed[@src]", ns)) {
3791
+ const embedElem = elem;
3792
+ const src = this.getAttribute(embedElem, "src");
3793
+ if (src) addRef(src, "generic" /* GENERIC */, elem.line);
3794
+ if (registry) {
3795
+ this.checkMimeTypeMatch(context, path, docDir, opfDir, embedElem, "src", registry);
3796
+ }
3797
+ }
3798
+ for (const elem of root.find(".//html:input[@src]", ns)) {
3799
+ const type = this.getAttribute(elem, "type");
3800
+ if (type?.toLowerCase() === "image") {
3801
+ const src = this.getAttribute(elem, "src");
3802
+ if (src) addRef(src, "image" /* IMAGE */, elem.line);
3803
+ }
3804
+ }
3805
+ for (const elem of root.find(".//html:object[@data]", ns)) {
3806
+ const objElem = elem;
3807
+ const data = this.getAttribute(objElem, "data");
3808
+ if (!data) continue;
3809
+ const allChildren = objElem.find("html:*", ns);
3810
+ const hasFallbackContent = allChildren.some((child) => {
3811
+ const c = child;
3812
+ return c.name !== "param" && this.getAttribute(c, "hidden") === null;
3813
+ });
3814
+ addRef(data, "generic" /* GENERIC */, elem.line, hasFallbackContent || void 0);
3815
+ if (registry) {
3816
+ this.checkMimeTypeMatch(context, path, docDir, opfDir, objElem, "data", registry);
3817
+ }
3818
+ }
3819
+ }
3820
+ /**
3821
+ * Check if an element's type attribute matches the manifest MIME type (OPF-013)
3822
+ */
3823
+ checkMimeTypeMatch(context, path, docDir, opfDir, element, srcAttr, registry) {
3824
+ const typeAttr = this.getAttribute(element, "type");
3825
+ if (!typeAttr) return;
3826
+ const src = this.getAttribute(element, srcAttr);
3827
+ if (!src || src.startsWith("http://") || src.startsWith("https://")) return;
3828
+ const resolvedPath = this.resolveRelativePath(docDir, src, opfDir);
3829
+ const hashIndex = resolvedPath.indexOf("#");
3830
+ const targetResource = hashIndex >= 0 ? resolvedPath.slice(0, hashIndex) : resolvedPath;
3831
+ const resource = registry.getResource(targetResource);
3832
+ if (!resource) return;
3833
+ const stripParams = (t) => {
3834
+ const idx = t.indexOf(";");
3835
+ return (idx >= 0 ? t.substring(0, idx) : t).trim();
3836
+ };
3837
+ const declaredType = stripParams(typeAttr);
3838
+ const manifestType = stripParams(resource.mimeType);
3839
+ if (declaredType && declaredType !== manifestType) {
3840
+ pushMessage(context.messages, {
3841
+ id: MessageId.OPF_013,
3842
+ message: `Resource "${targetResource}" is declared with MIME type "${declaredType}" in content, but has MIME type "${manifestType}" in the package document`,
3843
+ location: { path, line: element.line }
3844
+ });
3845
+ }
3846
+ }
3847
+ /**
3848
+ * Extract and validate picture elements (MED-003, MED-007, OPF-013)
3849
+ */
3850
+ extractAndRegisterPictureElements(context, path, root, opfDir, refValidator, registry) {
3851
+ const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
3852
+ const ns = { html: "http://www.w3.org/1999/xhtml" };
3853
+ const BLESSED_IMAGE_TYPES = /* @__PURE__ */ new Set([
3854
+ "image/gif",
3855
+ "image/jpeg",
3856
+ "image/png",
3857
+ "image/svg+xml",
3858
+ "image/webp"
3859
+ ]);
3860
+ const pictures = root.find(".//html:picture", ns);
3861
+ for (const pic of pictures) {
3862
+ const picElem = pic;
3863
+ const imgs = picElem.find("html:img[@src]", ns);
3864
+ for (const img of imgs) {
3865
+ const imgElem = img;
3866
+ const src = this.getAttribute(imgElem, "src");
3867
+ if (!src || src.startsWith("http://") || src.startsWith("https://")) continue;
3868
+ if (registry) {
3869
+ const resolvedPath = this.resolveRelativePath(docDir, src, opfDir);
3870
+ const resource = registry.getResource(resolvedPath);
3871
+ if (resource && !BLESSED_IMAGE_TYPES.has(resource.mimeType)) {
3872
+ pushMessage(context.messages, {
3873
+ id: MessageId.MED_003,
3874
+ message: `Image in "picture" element must be a core image type, but found "${resource.mimeType}"`,
3875
+ location: { path, line: img.line }
3876
+ });
3877
+ }
3878
+ }
3879
+ const srcset = this.getAttribute(imgElem, "srcset");
3880
+ if (srcset && registry) {
3881
+ const entries = srcset.split(",");
3882
+ for (const entry of entries) {
3883
+ const url = entry.trim().split(/\s+/)[0];
3884
+ if (!url || url.startsWith("http://") || url.startsWith("https://")) continue;
3885
+ const resolvedPath = this.resolveRelativePath(docDir, url, opfDir);
3886
+ const resource = registry.getResource(resolvedPath);
3887
+ if (resource && !BLESSED_IMAGE_TYPES.has(resource.mimeType)) {
3888
+ pushMessage(context.messages, {
3889
+ id: MessageId.MED_003,
3890
+ message: `Image in "picture" element must be a core image type, but found "${resource.mimeType}"`,
3891
+ location: { path, line: img.line }
3892
+ });
3893
+ }
3894
+ }
3895
+ }
3896
+ }
3897
+ const sourcesWithSrc = picElem.find("html:source[@src]", ns);
3898
+ const sourcesWithSrcset = picElem.find("html:source[@srcset]", ns);
3899
+ const allSources = /* @__PURE__ */ new Set([...sourcesWithSrc, ...sourcesWithSrcset]);
3900
+ for (const source of allSources) {
3901
+ const sourceElem = source;
3902
+ const typeAttr = this.getAttribute(sourceElem, "type");
3903
+ const src = this.getAttribute(sourceElem, "src");
3904
+ const srcset = this.getAttribute(sourceElem, "srcset");
3905
+ const sourceUrl = src ?? srcset?.split(",")[0]?.trim().split(/\s+/)[0];
3906
+ if (!sourceUrl || sourceUrl.startsWith("http://") || sourceUrl.startsWith("https://"))
3907
+ continue;
3908
+ if (registry) {
3909
+ if (src) {
3910
+ this.checkMimeTypeMatch(context, path, docDir, opfDir, sourceElem, "src", registry);
3911
+ } else if (srcset && typeAttr) {
3912
+ const resolvedPath2 = this.resolveRelativePath(docDir, sourceUrl, opfDir);
3913
+ const resource2 = registry.getResource(resolvedPath2);
3914
+ if (resource2) {
3915
+ const stripParams = (t) => {
3916
+ const idx = t.indexOf(";");
3917
+ return (idx >= 0 ? t.substring(0, idx) : t).trim();
3918
+ };
3919
+ const declaredType = stripParams(typeAttr);
3920
+ const manifestType = stripParams(resource2.mimeType);
3921
+ if (declaredType && declaredType !== manifestType) {
3922
+ pushMessage(context.messages, {
3923
+ id: MessageId.OPF_013,
3924
+ message: `Resource "${resolvedPath2}" is declared with MIME type "${declaredType}" in content, but has MIME type "${manifestType}" in the package document`,
3925
+ location: { path, line: source.line }
3926
+ });
3927
+ }
3928
+ }
3929
+ }
3930
+ const resolvedPath = this.resolveRelativePath(docDir, sourceUrl, opfDir);
3931
+ const resource = registry.getResource(resolvedPath);
3932
+ if (resource && !BLESSED_IMAGE_TYPES.has(resource.mimeType) && !typeAttr) {
3933
+ pushMessage(context.messages, {
3934
+ id: MessageId.MED_007,
3935
+ message: `Source element in "picture" with foreign resource type "${resource.mimeType}" must declare a "type" attribute`,
3936
+ location: { path, line: source.line }
3937
+ });
3938
+ }
3939
+ }
3940
+ }
3941
+ }
3942
+ }
3369
3943
  parseSrcset(srcset, docDir, opfDir, path, line, refValidator) {
3370
3944
  const entries = srcset.split(",");
3371
3945
  for (const entry of entries) {
@@ -4574,69 +5148,6 @@ function parseCollections(xml) {
4574
5148
  return collections;
4575
5149
  }
4576
5150
 
4577
- // src/opf/types.ts
4578
- var CORE_MEDIA_TYPES = /* @__PURE__ */ new Set([
4579
- // Image types
4580
- "image/gif",
4581
- "image/jpeg",
4582
- "image/png",
4583
- "image/svg+xml",
4584
- "image/webp",
4585
- // Audio types
4586
- "audio/mpeg",
4587
- "audio/mp4",
4588
- "audio/ogg",
4589
- // CSS
4590
- "text/css",
4591
- // Fonts
4592
- "font/otf",
4593
- "font/ttf",
4594
- "font/woff",
4595
- "font/woff2",
4596
- "application/font-sfnt",
4597
- // deprecated alias for font/otf, font/ttf
4598
- "application/font-woff",
4599
- // deprecated alias for font/woff
4600
- "application/vnd.ms-opentype",
4601
- // deprecated alias
4602
- // Content documents
4603
- "application/xhtml+xml",
4604
- "application/x-dtbncx+xml",
4605
- // NCX
4606
- // JavaScript (EPUB 3)
4607
- "text/javascript",
4608
- "application/javascript",
4609
- // Media overlays
4610
- "application/smil+xml",
4611
- // PLS (Pronunciation Lexicon)
4612
- "application/pls+xml"
4613
- ]);
4614
- var ITEM_PROPERTIES = /* @__PURE__ */ new Set([
4615
- "cover-image",
4616
- "mathml",
4617
- "nav",
4618
- "remote-resources",
4619
- "scripted",
4620
- "svg",
4621
- "switch"
4622
- ]);
4623
- var LINK_PROPERTIES = /* @__PURE__ */ new Set(["onix", "marc21xml-record", "mods-record", "xmp-record"]);
4624
- var SPINE_PROPERTIES = /* @__PURE__ */ new Set([
4625
- "page-spread-left",
4626
- "page-spread-right",
4627
- "rendition:spread-none",
4628
- "rendition:spread-landscape",
4629
- "rendition:spread-portrait",
4630
- "rendition:spread-both",
4631
- "rendition:spread-auto",
4632
- "rendition:page-spread-center",
4633
- "rendition:layout-reflowable",
4634
- "rendition:layout-pre-paginated",
4635
- "rendition:orientation-auto",
4636
- "rendition:orientation-landscape",
4637
- "rendition:orientation-portrait"
4638
- ]);
4639
-
4640
5151
  // src/opf/validator.ts
4641
5152
  var OPFValidator = class {
4642
5153
  packageDoc = null;
@@ -5050,9 +5561,17 @@ var OPFValidator = class {
5050
5561
  this.detectRefinesCycles(context, opfPath);
5051
5562
  }
5052
5563
  if (this.packageDoc.version !== "2.0") {
5053
- const modifiedMeta = this.packageDoc.metaElements.find(
5564
+ const modifiedMetas = this.packageDoc.metaElements.filter(
5054
5565
  (meta) => meta.property === "dcterms:modified"
5055
5566
  );
5567
+ const modifiedMeta = modifiedMetas[0];
5568
+ if (modifiedMetas.length > 1) {
5569
+ pushMessage(context.messages, {
5570
+ id: MessageId.RSC_005,
5571
+ message: "package dcterms:modified meta element must occur exactly once",
5572
+ location: { path: opfPath }
5573
+ });
5574
+ }
5056
5575
  if (!modifiedMeta) {
5057
5576
  pushMessage(context.messages, {
5058
5577
  id: MessageId.RSC_005,
@@ -5186,7 +5705,7 @@ var OPFValidator = class {
5186
5705
  });
5187
5706
  }
5188
5707
  if (!item.href.startsWith("http") && !item.href.startsWith("mailto:")) {
5189
- const leaked = checkUrlLeaking(item.href);
5708
+ const leaked = checkUrlLeaking2(item.href);
5190
5709
  if (leaked) {
5191
5710
  pushMessage(context.messages, {
5192
5711
  id: MessageId.RSC_026,
@@ -5280,20 +5799,23 @@ var OPFValidator = class {
5280
5799
  });
5281
5800
  }
5282
5801
  if (this.packageDoc.version !== "2.0" && (item.href.startsWith("http://") || item.href.startsWith("https://"))) {
5283
- 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") {
5284
- pushMessage(context.messages, {
5285
- id: MessageId.RSC_006,
5286
- message: `Remote resource reference is not allowed in this context; resource "${item.href}" must be located in the EPUB container`,
5287
- location: { path: opfPath }
5288
- });
5289
- }
5802
+ const isAllowedRemoteType = 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";
5290
5803
  const inSpine = this.packageDoc.spine.some((s) => s.idref === item.id);
5291
- if (inSpine && !item.properties?.includes("remote-resources")) {
5292
- pushMessage(context.messages, {
5293
- id: MessageId.RSC_006,
5294
- message: `Manifest item "${item.id}" references remote resource but is missing "remote-resources" property`,
5295
- location: { path: opfPath }
5296
- });
5804
+ if (inSpine) {
5805
+ if (!isAllowedRemoteType) {
5806
+ pushMessage(context.messages, {
5807
+ id: MessageId.RSC_006,
5808
+ message: `Remote resource reference is not allowed in this context; resource "${item.href}" must be located in the EPUB container`,
5809
+ location: { path: opfPath }
5810
+ });
5811
+ }
5812
+ if (!item.properties?.includes("remote-resources")) {
5813
+ pushMessage(context.messages, {
5814
+ id: MessageId.RSC_006,
5815
+ message: `Manifest item "${item.id}" references remote resource but is missing "remote-resources" property`,
5816
+ location: { path: opfPath }
5817
+ });
5818
+ }
5297
5819
  }
5298
5820
  }
5299
5821
  }
@@ -5398,12 +5920,20 @@ var OPFValidator = class {
5398
5920
  });
5399
5921
  }
5400
5922
  seenIdrefs.add(itemref.idref);
5401
- if (!isSpineMediaType(item.mediaType) && !item.fallback) {
5402
- pushMessage(context.messages, {
5403
- id: MessageId.OPF_043,
5404
- message: `Spine item "${item.id}" has non-standard media type "${item.mediaType}" without fallback`,
5405
- location: { path: opfPath }
5406
- });
5923
+ if (!isSpineMediaType(item.mediaType)) {
5924
+ if (!item.fallback) {
5925
+ pushMessage(context.messages, {
5926
+ id: MessageId.OPF_043,
5927
+ message: `Spine item "${item.id}" has non-standard media type "${item.mediaType}" without fallback`,
5928
+ location: { path: opfPath }
5929
+ });
5930
+ } else if (!this.fallbackChainResolvesToContentDocument(item.id)) {
5931
+ pushMessage(context.messages, {
5932
+ id: MessageId.OPF_044,
5933
+ message: `Spine item "${item.id}" has non-standard media type "${item.mediaType}" and its fallback chain does not resolve to a content document`,
5934
+ location: { path: opfPath }
5935
+ });
5936
+ }
5407
5937
  }
5408
5938
  if (this.packageDoc.version !== "2.0" && itemref.properties) {
5409
5939
  for (const prop of itemref.properties) {
@@ -5601,14 +6131,57 @@ var OPFValidator = class {
5601
6131
  }
5602
6132
  }
5603
6133
  }
6134
+ fallbackChainResolvesToContentDocument(itemId) {
6135
+ const visited = /* @__PURE__ */ new Set();
6136
+ let currentId = itemId;
6137
+ while (currentId) {
6138
+ if (visited.has(currentId)) return false;
6139
+ visited.add(currentId);
6140
+ const item = this.manifestById.get(currentId);
6141
+ if (!item) return false;
6142
+ if (isSpineMediaType(item.mediaType)) return true;
6143
+ currentId = item.fallback;
6144
+ }
6145
+ return false;
6146
+ }
5604
6147
  };
5605
6148
  function isSpineMediaType(mediaType) {
5606
6149
  return mediaType === "application/xhtml+xml" || mediaType === "image/svg+xml" || // EPUB 2 also allows these in spine
5607
6150
  mediaType === "application/x-dtbook+xml";
5608
6151
  }
5609
6152
  function isValidLanguageTag(tag) {
5610
- const pattern = /^[a-zA-Z]{2,3}(-[a-zA-Z]{4})?(-[a-zA-Z]{2}|-\d{3})?(-([a-zA-Z\d]{5,8}|\d[a-zA-Z\d]{3}))*$/;
5611
- return pattern.test(tag);
6153
+ const pattern = /^[a-zA-Z]{2,3}(-[a-zA-Z]{4})?(-[a-zA-Z]{2}|-\d{3})?(-([a-zA-Z\d]{5,8}|\d[a-zA-Z\d]{3}))*(-[a-wyzA-WYZ](-[a-zA-Z\d]{2,8})+)*(-x(-[a-zA-Z\d]{1,8})+)?$/;
6154
+ if (pattern.test(tag)) return true;
6155
+ if (/^x(-[a-zA-Z\d]{1,8})+$/.test(tag)) return true;
6156
+ const grandfathered = /* @__PURE__ */ new Set([
6157
+ "en-GB-oed",
6158
+ "i-ami",
6159
+ "i-bnn",
6160
+ "i-default",
6161
+ "i-enochian",
6162
+ "i-hak",
6163
+ "i-klingon",
6164
+ "i-lux",
6165
+ "i-mingo",
6166
+ "i-navajo",
6167
+ "i-pwn",
6168
+ "i-tao",
6169
+ "i-tay",
6170
+ "i-tsu",
6171
+ "sgn-BE-FR",
6172
+ "sgn-BE-NL",
6173
+ "sgn-CH-DE",
6174
+ "art-lojban",
6175
+ "cel-gaulish",
6176
+ "no-bok",
6177
+ "no-nyn",
6178
+ "zh-guoyu",
6179
+ "zh-hakka",
6180
+ "zh-min",
6181
+ "zh-min-nan",
6182
+ "zh-xiang"
6183
+ ]);
6184
+ return grandfathered.has(tag);
5612
6185
  }
5613
6186
  function resolvePath(basePath, relativePath) {
5614
6187
  if (relativePath.startsWith("/")) {
@@ -5636,7 +6209,7 @@ function tryDecodeUriComponent(encoded) {
5636
6209
  return encoded;
5637
6210
  }
5638
6211
  }
5639
- function checkUrlLeaking(href) {
6212
+ function checkUrlLeaking2(href) {
5640
6213
  const TEST_BASE_A = "https://a.example.org/A/";
5641
6214
  const TEST_BASE_B = "https://b.example.org/B/";
5642
6215
  try {
@@ -5714,9 +6287,11 @@ function isValidW3CDateFormat(dateStr) {
5714
6287
  var ResourceRegistry = class {
5715
6288
  resources;
5716
6289
  ids;
6290
+ svgSymbolIds;
5717
6291
  constructor() {
5718
6292
  this.resources = /* @__PURE__ */ new Map();
5719
6293
  this.ids = /* @__PURE__ */ new Map();
6294
+ this.svgSymbolIds = /* @__PURE__ */ new Map();
5720
6295
  }
5721
6296
  /**
5722
6297
  * Register a resource from manifest
@@ -5778,6 +6353,21 @@ var ResourceRegistry = class {
5778
6353
  }
5779
6354
  return -1;
5780
6355
  }
6356
+ /**
6357
+ * Register an ID as belonging to an SVG symbol element
6358
+ */
6359
+ registerSVGSymbolID(resourceURL, id) {
6360
+ if (!this.svgSymbolIds.has(resourceURL)) {
6361
+ this.svgSymbolIds.set(resourceURL, /* @__PURE__ */ new Set());
6362
+ }
6363
+ this.svgSymbolIds.get(resourceURL)?.add(id);
6364
+ }
6365
+ /**
6366
+ * Check if an ID in a resource belongs to an SVG symbol element
6367
+ */
6368
+ isSVGSymbolID(resourceURL, id) {
6369
+ return this.svgSymbolIds.get(resourceURL)?.has(id) ?? false;
6370
+ }
5781
6371
  /**
5782
6372
  * Get all resources
5783
6373
  */
@@ -5827,6 +6417,7 @@ var ReferenceValidator = class {
5827
6417
  for (const reference of this.references) {
5828
6418
  this.validateReference(context, reference);
5829
6419
  }
6420
+ this.checkRemoteResources(context);
5830
6421
  this.checkUndeclaredResources(context);
5831
6422
  this.checkReadingOrder(context);
5832
6423
  this.checkNonLinearReachability(context);
@@ -5836,14 +6427,6 @@ var ReferenceValidator = class {
5836
6427
  */
5837
6428
  validateReference(context, reference) {
5838
6429
  const url = reference.url.trim();
5839
- if (isMalformedURL(url)) {
5840
- pushMessage(context.messages, {
5841
- id: MessageId.RSC_020,
5842
- message: `Malformed URL: ${url}`,
5843
- location: reference.location
5844
- });
5845
- return;
5846
- }
5847
6430
  if (isDataURL(url)) {
5848
6431
  if (this.version.startsWith("3.")) {
5849
6432
  const forbiddenDataUrlTypes = [
@@ -5858,10 +6441,35 @@ var ReferenceValidator = class {
5858
6441
  message: "Data URLs are not allowed in this context",
5859
6442
  location: reference.location
5860
6443
  });
6444
+ } else {
6445
+ const fallbackCheckedTypes = [
6446
+ "image" /* IMAGE */,
6447
+ "audio" /* AUDIO */,
6448
+ "video" /* VIDEO */,
6449
+ "generic" /* GENERIC */
6450
+ ];
6451
+ if (fallbackCheckedTypes.includes(reference.type) && !reference.hasIntrinsicFallback) {
6452
+ const dataUrlMimeType = this.extractDataURLMimeType(url);
6453
+ if (dataUrlMimeType && !isCoreMediaType(dataUrlMimeType)) {
6454
+ pushMessage(context.messages, {
6455
+ id: MessageId.RSC_032,
6456
+ message: `Fallback must be provided for foreign resources, but found none for data URL of type "${dataUrlMimeType}"`,
6457
+ location: reference.location
6458
+ });
6459
+ }
6460
+ }
5861
6461
  }
5862
6462
  }
5863
6463
  return;
5864
6464
  }
6465
+ if (isMalformedURL(url)) {
6466
+ pushMessage(context.messages, {
6467
+ id: MessageId.RSC_020,
6468
+ message: `Malformed URL: ${url}`,
6469
+ location: reference.location
6470
+ });
6471
+ return;
6472
+ }
5865
6473
  if (isFileURL(url)) {
5866
6474
  pushMessage(context.messages, {
5867
6475
  id: MessageId.RSC_030,
@@ -5873,6 +6481,13 @@ var ReferenceValidator = class {
5873
6481
  const resourcePath = reference.targetResource || parseURL(url).resource;
5874
6482
  const fragment = reference.fragment ?? parseURL(url).fragment;
5875
6483
  const hasFragment = fragment !== void 0 && fragment !== "";
6484
+ if (!isRemoteURL(url) && url.includes("?")) {
6485
+ pushMessage(context.messages, {
6486
+ id: MessageId.RSC_033,
6487
+ message: `Relative URL strings must not have a query component: "${url}"`,
6488
+ location: reference.location
6489
+ });
6490
+ }
5876
6491
  if (!isRemoteURL(url)) {
5877
6492
  this.validateLocalReference(context, reference, resourcePath);
5878
6493
  } else {
@@ -5888,7 +6503,7 @@ var ReferenceValidator = class {
5888
6503
  validateLocalReference(context, reference, resourcePath) {
5889
6504
  if (hasAbsolutePath(resourcePath)) {
5890
6505
  pushMessage(context.messages, {
5891
- id: MessageId.RSC_027,
6506
+ id: MessageId.RSC_026,
5892
6507
  message: "Absolute paths are not allowed in EPUB",
5893
6508
  location: reference.location
5894
6509
  });
@@ -5900,10 +6515,16 @@ var ReferenceValidator = class {
5900
6515
  ];
5901
6516
  if (hasParentDirectoryReference(reference.url) && forbiddenParentDirTypes.includes(reference.type)) {
5902
6517
  pushMessage(context.messages, {
5903
- id: MessageId.RSC_028,
6518
+ id: MessageId.RSC_026,
5904
6519
  message: "Parent directory references (..) are not allowed",
5905
6520
  location: reference.location
5906
6521
  });
6522
+ } else if (!hasAbsolutePath(resourcePath) && !hasParentDirectoryReference(reference.url) && checkUrlLeaking(reference.url)) {
6523
+ pushMessage(context.messages, {
6524
+ id: MessageId.RSC_026,
6525
+ message: `URL "${reference.url}" leaks outside the container`,
6526
+ location: reference.location
6527
+ });
5907
6528
  }
5908
6529
  if (!this.registry.hasResource(resourcePath)) {
5909
6530
  const fileExistsInContainer = context.files.has(resourcePath);
@@ -5928,14 +6549,15 @@ var ReferenceValidator = class {
5928
6549
  return;
5929
6550
  }
5930
6551
  const resource = this.registry.getResource(resourcePath);
5931
- if (reference.type === "hyperlink" /* HYPERLINK */ && !resource?.inSpine) {
6552
+ const isHyperlinkLike = reference.type === "hyperlink" /* HYPERLINK */ || reference.type === "nav-toc-link" /* NAV_TOC_LINK */ || reference.type === "nav-pagelist-link" /* NAV_PAGELIST_LINK */;
6553
+ if (this.version.startsWith("3") && isHyperlinkLike && !resource?.inSpine) {
5932
6554
  pushMessage(context.messages, {
5933
6555
  id: MessageId.RSC_011,
5934
6556
  message: "Hyperlinks must reference spine items",
5935
6557
  location: reference.location
5936
6558
  });
5937
6559
  }
5938
- if (reference.type === "hyperlink" /* HYPERLINK */ || reference.type === "overlay-text-link" /* OVERLAY_TEXT_LINK */) {
6560
+ if (isHyperlinkLike || reference.type === "overlay-text-link" /* OVERLAY_TEXT_LINK */) {
5939
6561
  const targetMimeType = resource?.mimeType;
5940
6562
  if (targetMimeType && !this.isBlessedItemType(targetMimeType, context.version) && !this.isDeprecatedBlessedItemType(targetMimeType) && !resource.hasCoreMediaTypeFallback) {
5941
6563
  pushMessage(context.messages, {
@@ -5945,7 +6567,13 @@ var ReferenceValidator = class {
5945
6567
  });
5946
6568
  }
5947
6569
  }
5948
- if (resource && isPublicationResourceReference(reference.type) && !CORE_MEDIA_TYPES.has(resource.mimeType) && !resource.hasCoreMediaTypeFallback && !reference.hasIntrinsicFallback) {
6570
+ const fallbackCheckedTypes = [
6571
+ "image" /* IMAGE */,
6572
+ "audio" /* AUDIO */,
6573
+ "video" /* VIDEO */,
6574
+ "generic" /* GENERIC */
6575
+ ];
6576
+ if (resource && fallbackCheckedTypes.includes(reference.type) && !isCoreMediaType(resource.mimeType) && !resource.hasCoreMediaTypeFallback && !reference.hasIntrinsicFallback) {
5949
6577
  pushMessage(context.messages, {
5950
6578
  id: MessageId.RSC_032,
5951
6579
  message: `Fallback must be provided for foreign resources, but found none for resource "${resourcePath}" of type "${resource.mimeType}"`,
@@ -5958,20 +6586,24 @@ var ReferenceValidator = class {
5958
6586
  */
5959
6587
  validateRemoteReference(context, reference) {
5960
6588
  const url = reference.url;
5961
- if (isHTTP(url) && !isHTTPS(url)) {
5962
- pushMessage(context.messages, {
5963
- id: MessageId.RSC_031,
5964
- message: "Remote resources must use HTTPS",
5965
- location: reference.location
5966
- });
5967
- }
5968
6589
  if (isPublicationResourceReference(reference.type)) {
5969
- const allowedRemoteTypes = /* @__PURE__ */ new Set([
6590
+ if (isHTTP(url) && !isHTTPS(url)) {
6591
+ pushMessage(context.messages, {
6592
+ id: MessageId.RSC_031,
6593
+ message: "Remote resources must use HTTPS",
6594
+ location: reference.location
6595
+ });
6596
+ }
6597
+ const allowedRemoteRefTypes = /* @__PURE__ */ new Set([
5970
6598
  "audio" /* AUDIO */,
5971
6599
  "video" /* VIDEO */,
5972
6600
  "font" /* FONT */
5973
6601
  ]);
5974
- if (!allowedRemoteTypes.has(reference.type)) {
6602
+ const targetResource = reference.targetResource || url;
6603
+ const resource = this.registry.getResource(targetResource);
6604
+ const isAllowedByRefType = allowedRemoteRefTypes.has(reference.type);
6605
+ const isAllowedByMimeType = resource && this.isRemoteResourceType(resource.mimeType);
6606
+ if (!isAllowedByRefType && !isAllowedByMimeType) {
5975
6607
  pushMessage(context.messages, {
5976
6608
  id: MessageId.RSC_006,
5977
6609
  message: "Remote resources are only allowed for audio, video, and fonts",
@@ -5979,8 +6611,7 @@ var ReferenceValidator = class {
5979
6611
  });
5980
6612
  return;
5981
6613
  }
5982
- const targetResource = reference.targetResource || url;
5983
- if (!this.registry.hasResource(targetResource)) {
6614
+ if (!resource) {
5984
6615
  pushMessage(context.messages, {
5985
6616
  id: MessageId.RSC_008,
5986
6617
  message: `Referenced resource "${targetResource}" is not declared in the OPF manifest`,
@@ -6013,9 +6644,9 @@ var ReferenceValidator = class {
6013
6644
  });
6014
6645
  return;
6015
6646
  }
6016
- if (resource?.mimeType === "image/svg+xml") {
6647
+ if (resource?.mimeType === "image/svg+xml" && reference.type === "hyperlink" /* HYPERLINK */) {
6017
6648
  const hasSVGView = fragment.includes("svgView(") || fragment.includes("viewBox(");
6018
- if (hasSVGView && reference.type === "hyperlink" /* HYPERLINK */) {
6649
+ if (hasSVGView) {
6019
6650
  pushMessage(context.messages, {
6020
6651
  id: MessageId.RSC_014,
6021
6652
  message: "SVG view fragments can only be referenced from SVG documents",
@@ -6023,11 +6654,51 @@ var ReferenceValidator = class {
6023
6654
  });
6024
6655
  }
6025
6656
  }
6026
- if (!this.registry.hasID(resourcePath, fragment)) {
6657
+ if (reference.type === "hyperlink" /* HYPERLINK */) {
6658
+ if (this.registry.isSVGSymbolID(resourcePath, fragment)) {
6659
+ pushMessage(context.messages, {
6660
+ id: MessageId.RSC_014,
6661
+ message: `Fragment identifier "${fragment}" defines an incompatible resource type (SVG symbol)`,
6662
+ location: reference.location
6663
+ });
6664
+ }
6665
+ }
6666
+ const parsedMimeTypes = ["application/xhtml+xml", "image/svg+xml"];
6667
+ if (resource && parsedMimeTypes.includes(resource.mimeType) && !isRemoteURL(resourcePath)) {
6668
+ if (!this.registry.hasID(resourcePath, fragment)) {
6669
+ pushMessage(context.messages, {
6670
+ id: MessageId.RSC_012,
6671
+ message: `Fragment identifier not found: #${fragment}`,
6672
+ location: reference.location
6673
+ });
6674
+ }
6675
+ }
6676
+ }
6677
+ /**
6678
+ * Check non-spine remote resources that have non-standard types.
6679
+ * Fires RSC-006 for remote items that aren't audio/video/font types
6680
+ * and aren't referenced as audio/video/font by content documents.
6681
+ * This mirrors Java's checkItemAfterResourceValidation behavior.
6682
+ */
6683
+ checkRemoteResources(context) {
6684
+ if (!this.version.startsWith("3")) return;
6685
+ const referencedAsAllowed = /* @__PURE__ */ new Set();
6686
+ for (const ref of this.references) {
6687
+ if (isRemoteURL(ref.url) || isRemoteURL(ref.targetResource)) {
6688
+ if (ref.type === "font" /* FONT */ || ref.type === "audio" /* AUDIO */ || ref.type === "video" /* VIDEO */) {
6689
+ referencedAsAllowed.add(ref.targetResource);
6690
+ }
6691
+ }
6692
+ }
6693
+ for (const resource of this.registry.getAllResources()) {
6694
+ if (!isRemoteURL(resource.url)) continue;
6695
+ if (resource.inSpine) continue;
6696
+ if (this.isRemoteResourceType(resource.mimeType)) continue;
6697
+ if (referencedAsAllowed.has(resource.url)) continue;
6027
6698
  pushMessage(context.messages, {
6028
- id: MessageId.RSC_012,
6029
- message: `Fragment identifier not found: #${fragment}`,
6030
- location: reference.location
6699
+ id: MessageId.RSC_006,
6700
+ message: `Remote resource reference is not allowed; resource "${resource.url}" must be located in the EPUB container`,
6701
+ location: { path: resource.url }
6031
6702
  });
6032
6703
  }
6033
6704
  }
@@ -6051,9 +6722,9 @@ var ReferenceValidator = class {
6051
6722
  for (const resource of this.registry.getAllResources()) {
6052
6723
  if (resource.inSpine) continue;
6053
6724
  if (referencedResources.has(resource.url)) continue;
6054
- if (resource.url.includes("nav")) continue;
6055
- if (resource.url.includes("toc.ncx") || resource.url.includes(".ncx")) continue;
6056
- if (resource.url.includes("cover-image")) continue;
6725
+ if (resource.isNav) continue;
6726
+ if (resource.isNcx) continue;
6727
+ if (resource.isCoverImage) continue;
6057
6728
  pushMessage(context.messages, {
6058
6729
  id: MessageId.OPF_097,
6059
6730
  message: `Resource declared in manifest but not referenced: ${resource.url}`,
@@ -6114,7 +6785,7 @@ var ReferenceValidator = class {
6114
6785
  const opfDir = opfPath.includes("/") ? opfPath.substring(0, opfPath.lastIndexOf("/")) : "";
6115
6786
  const hyperlinkTargets = /* @__PURE__ */ new Set();
6116
6787
  for (const ref of this.references) {
6117
- if (ref.type === "hyperlink" /* HYPERLINK */) {
6788
+ if (ref.type === "hyperlink" /* HYPERLINK */ || ref.type === "nav-toc-link" /* NAV_TOC_LINK */ || ref.type === "nav-pagelist-link" /* NAV_PAGELIST_LINK */) {
6118
6789
  hyperlinkTargets.add(ref.targetResource);
6119
6790
  }
6120
6791
  }
@@ -6151,6 +6822,13 @@ var ReferenceValidator = class {
6151
6822
  isDeprecatedBlessedItemType(mimeType) {
6152
6823
  return mimeType === "text/x-oeb1-document" || mimeType === "text/html";
6153
6824
  }
6825
+ extractDataURLMimeType(url) {
6826
+ const match = /^data:([^;,]+)/.exec(url);
6827
+ return match?.[1]?.trim().toLowerCase() ?? "text/plain";
6828
+ }
6829
+ isRemoteResourceType(mimeType) {
6830
+ return mimeType.startsWith("audio/") || mimeType.startsWith("video/") || mimeType.startsWith("font/") || mimeType === "application/font-sfnt" || mimeType === "application/font-woff" || mimeType === "application/font-woff2" || mimeType === "application/vnd.ms-opentype";
6831
+ }
6154
6832
  };
6155
6833
  var COMPRESSED_SCHEMAS = {
6156
6834
  "applications.rng": "H4sIAJC0cWkCA81aX2/bNhB/76fgXKBPtb1u6zC4ToqgSdMC7TqsGbZXWqIlohQpUFQS99PvSOq/RVmSZTdBHhLy/vF49+PxqPXbx4iheyITKvjF7NXi5xki3BM+5cHF7J+79/M/Zm8vn60DiaMISwTUPFnhi1moVLxaLiVh+JEHCyGDJU+WnohirOiGMqp2S8y5UPCvgBkj2XC7eBMlU0+lkhjay2cIrX+az9HzKX7QfG4E+mRLOUEcR+RiBsZGgi+wUjJZUK6IxJ6aIRjeANHFzAwxgu+JsQbY8coXXhoRbld1if6++XT1H/rzFn31QhJhtBUSfbj7/Am9XqF/yQZdxTGjniFG7wnWq0u0pPLn+XrZlGp1Tb32FvOvfJ+a3UFKoHfGG+gKvEE3qSKJy7DSLXYAhkSs5zHLB2BIkm2bmz0B3PALivGGkdmykLFsChkg1Zc4CCaUF1LfJ3wiYbGIBSSYW9p6WXfpemnD9EDEChWC1K5wdRhUhLqxqKe25sY5Quomm4dwMvQrsK/G6IoqrcXEXaG8TR8QeqGgXhF6MHCPWUouATxAtv27ORczrNf8qOaCs52LaotZ4hJR2buqflhMbvYAV5bR6nDidU6AbhjRwytU8PT1X1PJOM+1+WQKF2QJ5lj/BzNbLt5S9115TbZ72bnQ9oWnXFE234qU++cIiRwjHA75y06XHsno+7qkLt7tE5wq4fJIhHmKmWs2hAQ7h6M0HkBpMuiUvmVigxkq1CUI22NZZxgcecR6NUEPVIUoR5wcuE5yDFIepyoS/lQHVhITxqAE8b5NJFBHgodjcAWj36eyUuO5/EZ2OlieznGYh9SQ07DcPyeEcYXIY0w8OMKQjxVGaheTFSo4++ZuU5U7e7ngTiDXp597zpnWqXROQblLnZMcliWp55r2iUcjN5gkBEsvPMGxU0kTx759LSggJBDmPspvPvngCpVS+u7hnt7pTuOToGwj+13XhyqVGerrj1b5bp+I7dYZaB0xr+1xBZiGdk/fcdoJHoT0nZNeiHX5DHfmE4RoDSMdjr8DJEFii240LQIFCk4x+L2nUsEZjYB9IzCsYAV3+VJc391pMcG9N4bYmecduxM4Kw3egVWxJPdUpM69aYWO6r43y7mRexZLEcBdHvYLigdn2QZEQIM+cl83AYRcoRdMvcl5XwTqjWtPspqkoa3cjf3qo3bAFeZRzqtXz3YiE3nVA7kpfb3M7OnpFCvQXSi1n8QVCzprirqahdnO8RVKQ1qEH4fKamZGR1UlKV7QSBefVC1yzRtc26ED7FIw4mKtB/MxpVWrjx14UZ1sM15XP4stE1gtAK7nnAQQ6VCWZYYPz7lypxwWlVMH7IlFAsXfMbbYBHNVmGZ2EYcSJ+0lpu7yvTN9UoBq6B56BOlSLeue6H5ihBWCypZ0tTB1dpZayhrYRsO+dXX0OlD4+hQzEXQC3bUhQRvx+BIKXKh2PAC7lwiaoQ+U++LB4p4V1B/1LH1vzMvs7EK8jGQqvKuJOxXaVZUsRDy8LTkWnjL3D0QmzIhU+6zTIdO+PxwgUJnrKC2B6EQVdS0euwECwOih5/3TpLqhP5zm1cw9IB2CPe1M8TshGJw2NpM1df881tS9s9gY0pXDhmCqDK4ImzZ/xyYdo0mlNdIv5XwqDeDuhjIGUqTxUKYxFmoeOByGspnAGcEzoqCS2KdwJo7wh24WjnCJsvk0mE2SSq3bt1gk+sJrEvQ0mFxJWncafSdSfJGfhXQkPqMVpLL691kOSamiZOJJGqt5ksaxkGWLr1304K7jECAu0fUQDOdO6EZhRBWJLBQz2h+IGe0Pw2BIJwrD/GQgXMj6oRhcIJV27hjYOYbPNAdHQqTmN/g1lNn6bATgjUGtMd45A3IVcT5BhYb5PbZ3KkvvSGK47wGd+Vbkeges1EO3Esch9aBhp3PaCuqf15a+d25ndjYX5qKbKs2r7inuqpOvp37XPtOaTnsDrCpZhIQGYcfr2SBhD9RX4VQvhpC1kzzqtazW9dJfnXV2erTiACLjqN5Ti9scRlUnz2JTJY+7MUxJDL0ZLCGqh+BZI69Gt7qOLaL24HWAigG9sVaYOtQiIwoeQ7sfA/KP7eB95gufX8PzKTwuftQ9P+jwwXjWI7OSBjTJLEP/LllmamebLKOZrE9Wk3eyRllVy1k7ZW33Rt3Y1VRz+AgAdho+AojSRKEN9HfhhRfRLdIWmo9SUFbmvLFvzGizQzhJoI1mPs3Mv94cfamfsPW27+Gn2nurRflejiepftLf1e6b5+7NVSHjgPiauc6i0mzqMqM11WWmI//AySJMRtAfYTKG3giTW9uFMDnNVAhTl/c0Wnn2XX7olWefa7r8re/MAC+5SutuQA5f7TV0htWX4S/HCvj1WAG/HSvg9bECfj9WgDkQDghxx9B6mX0Adfnsf1i38T4sMgAA",
@@ -6665,11 +7343,15 @@ var EpubCheck = class _EpubCheck {
6665
7343
  const manifestById = new Map(packageDoc.manifest.map((item) => [item.id, item]));
6666
7344
  for (const item of packageDoc.manifest) {
6667
7345
  const fullPath = resolveManifestHref(opfDir, item.href);
7346
+ const properties = item.properties ?? [];
6668
7347
  registry.registerResource({
6669
7348
  url: fullPath,
6670
7349
  mimeType: item.mediaType,
6671
7350
  inSpine: spineIdrefs.has(item.id),
6672
7351
  hasCoreMediaTypeFallback: this.hasCMTFallback(item.id, manifestById),
7352
+ isNav: properties.includes("nav"),
7353
+ isCoverImage: properties.includes("cover-image"),
7354
+ isNcx: item.mediaType === "application/x-dtbncx+xml",
6673
7355
  ids: /* @__PURE__ */ new Set()
6674
7356
  });
6675
7357
  }
@@ -6685,7 +7367,7 @@ var EpubCheck = class _EpubCheck {
6685
7367
  visited.add(currentId);
6686
7368
  const item = manifestById.get(currentId);
6687
7369
  if (!item) return false;
6688
- if (CORE_MEDIA_TYPES.has(item.mediaType)) return true;
7370
+ if (isCoreMediaType(item.mediaType)) return true;
6689
7371
  currentId = item.fallback;
6690
7372
  }
6691
7373
  return false;