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