@likecoin/epubcheck-ts 0.3.5 → 0.3.7

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
@@ -439,6 +439,11 @@ var MessageDefs = {
439
439
  severity: "warning",
440
440
  description: "Property is deprecated"
441
441
  },
442
+ OPF_086b: {
443
+ id: "OPF-086b",
444
+ severity: "usage",
445
+ description: "epub:type value is deprecated"
446
+ },
442
447
  OPF_087: {
443
448
  id: "OPF-087",
444
449
  severity: "usage",
@@ -870,7 +875,7 @@ var MessageDefs = {
870
875
  NAV_001: {
871
876
  id: "NAV-001",
872
877
  severity: "error",
873
- description: "Nav file not supported for EPUB v2"
878
+ description: 'Navigation Document must have a nav element with epub:type="toc"'
874
879
  },
875
880
  NAV_002: { id: "NAV-002", severity: "suppressed", description: "Missing toc nav element" },
876
881
  NAV_003: {
@@ -1362,13 +1367,6 @@ var CSSValidator = class {
1362
1367
  location
1363
1368
  });
1364
1369
  }
1365
- if (value === "absolute") {
1366
- pushMessage(context.messages, {
1367
- id: MessageId.CSS_019,
1368
- message: 'CSS property "position: absolute" should be used with caution in EPUB',
1369
- location
1370
- });
1371
- }
1372
1370
  }
1373
1371
  /**
1374
1372
  * Extract the value from a Declaration node
@@ -1634,18 +1632,97 @@ var CSSValidator = class {
1634
1632
  }
1635
1633
  };
1636
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
+
1637
1710
  // src/references/types.ts
1711
+ var PUBLICATION_RESOURCE_TYPES = /* @__PURE__ */ new Set([
1712
+ "generic" /* GENERIC */,
1713
+ "stylesheet" /* STYLESHEET */,
1714
+ "font" /* FONT */,
1715
+ "image" /* IMAGE */,
1716
+ "audio" /* AUDIO */,
1717
+ "video" /* VIDEO */,
1718
+ "track" /* TRACK */,
1719
+ "media-overlay" /* MEDIA_OVERLAY */,
1720
+ "svg-symbol" /* SVG_SYMBOL */,
1721
+ "svg-paint" /* SVG_PAINT */,
1722
+ "svg-clip-path" /* SVG_CLIP_PATH */
1723
+ ]);
1638
1724
  function isPublicationResourceReference(type) {
1639
- return [
1640
- "generic" /* GENERIC */,
1641
- "stylesheet" /* STYLESHEET */,
1642
- "font" /* FONT */,
1643
- "image" /* IMAGE */,
1644
- "audio" /* AUDIO */,
1645
- "video" /* VIDEO */,
1646
- "track" /* TRACK */,
1647
- "media-overlay" /* MEDIA_OVERLAY */
1648
- ].includes(type);
1725
+ return PUBLICATION_RESOURCE_TYPES.has(type);
1649
1726
  }
1650
1727
 
1651
1728
  // src/references/url.ts
@@ -1702,6 +1779,17 @@ function isHTTP(url) {
1702
1779
  function isRemoteURL(url) {
1703
1780
  return isHTTP(url) || isHTTPS(url);
1704
1781
  }
1782
+ function checkUrlLeaking(href) {
1783
+ const TEST_BASE_A = "https://a.example.org/A/";
1784
+ const TEST_BASE_B = "https://b.example.org/B/";
1785
+ try {
1786
+ const urlA = new URL(href, TEST_BASE_A).toString();
1787
+ const urlB = new URL(href, TEST_BASE_B).toString();
1788
+ return !urlA.startsWith(TEST_BASE_A) || !urlB.startsWith(TEST_BASE_B);
1789
+ } catch {
1790
+ return false;
1791
+ }
1792
+ }
1705
1793
  function resolveManifestHref(opfDir, href) {
1706
1794
  if (isRemoteURL(href)) return href;
1707
1795
  try {
@@ -1715,7 +1803,184 @@ function resolveManifestHref(opfDir, href) {
1715
1803
  }
1716
1804
 
1717
1805
  // src/content/validator.ts
1718
- var DISCOURAGED_ELEMENTS = /* @__PURE__ */ new Set(["base", "embed"]);
1806
+ var DISCOURAGED_ELEMENTS = /* @__PURE__ */ new Set(["base", "embed", "rp"]);
1807
+ var ABSOLUTE_URI_RE = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
1808
+ var IMAGE_MAGIC = [
1809
+ { mime: "image/jpeg", bytes: [255, 216], extensions: [".jpg", ".jpeg", ".jpe"] },
1810
+ { mime: "image/gif", bytes: [71, 73, 70, 56], extensions: [".gif"] },
1811
+ { mime: "image/png", bytes: [137, 80, 78, 71], extensions: [".png"] },
1812
+ { mime: "image/webp", bytes: [82, 73, 70, 70], extensions: [".webp"] }
1813
+ ];
1814
+ function stripMimeParams(t) {
1815
+ const idx = t.indexOf(";");
1816
+ return (idx >= 0 ? t.substring(0, idx) : t).trim();
1817
+ }
1818
+ var EPUB_SSV_DEPRECATED = /* @__PURE__ */ new Set([
1819
+ "annoref",
1820
+ "annotation",
1821
+ "biblioentry",
1822
+ "bridgehead",
1823
+ "endnote",
1824
+ "help",
1825
+ "marginalia",
1826
+ "note",
1827
+ "rearnote",
1828
+ "rearnotes",
1829
+ "sidebar",
1830
+ "subchapter",
1831
+ "warning"
1832
+ ]);
1833
+ var EPUB_SSV_DISALLOWED_ON_CONTENT = /* @__PURE__ */ new Set([
1834
+ "aside",
1835
+ "figure",
1836
+ "list",
1837
+ "list-item",
1838
+ "table",
1839
+ "table-cell",
1840
+ "table-row"
1841
+ ]);
1842
+ var EPUB_SSV_ALL = /* @__PURE__ */ new Set([
1843
+ ...EPUB_SSV_DEPRECATED,
1844
+ ...EPUB_SSV_DISALLOWED_ON_CONTENT,
1845
+ "abstract",
1846
+ "acknowledgments",
1847
+ "afterword",
1848
+ "appendix",
1849
+ "assessment",
1850
+ "assessments",
1851
+ "backlink",
1852
+ "backmatter",
1853
+ "balloon",
1854
+ "bibliography",
1855
+ "biblioref",
1856
+ "bodymatter",
1857
+ "case-study",
1858
+ "chapter",
1859
+ "colophon",
1860
+ "concluding-sentence",
1861
+ "conclusion",
1862
+ "contributors",
1863
+ "copyright-page",
1864
+ "cover",
1865
+ "covertitle",
1866
+ "credit",
1867
+ "credits",
1868
+ "dedication",
1869
+ "division",
1870
+ "endnotes",
1871
+ "epigraph",
1872
+ "epilogue",
1873
+ "errata",
1874
+ "fill-in-the-blank-problem",
1875
+ "footnote",
1876
+ "footnotes",
1877
+ "foreword",
1878
+ "frontmatter",
1879
+ "fulltitle",
1880
+ "general-problem",
1881
+ "glossary",
1882
+ "glossdef",
1883
+ "glossref",
1884
+ "glossterm",
1885
+ "halftitle",
1886
+ "halftitlepage",
1887
+ "imprimatur",
1888
+ "imprint",
1889
+ "index",
1890
+ "index-editor-note",
1891
+ "index-entry",
1892
+ "index-entry-list",
1893
+ "index-group",
1894
+ "index-headnotes",
1895
+ "index-legend",
1896
+ "index-locator",
1897
+ "index-locator-list",
1898
+ "index-locator-range",
1899
+ "index-term",
1900
+ "index-term-categories",
1901
+ "index-term-category",
1902
+ "index-xref-preferred",
1903
+ "index-xref-related",
1904
+ "introduction",
1905
+ "keyword",
1906
+ "keywords",
1907
+ "label",
1908
+ "landmarks",
1909
+ "learning-objective",
1910
+ "learning-objectives",
1911
+ "learning-outcome",
1912
+ "learning-outcomes",
1913
+ "learning-resource",
1914
+ "learning-resources",
1915
+ "learning-standard",
1916
+ "learning-standards",
1917
+ "loa",
1918
+ "loi",
1919
+ "lot",
1920
+ "lov",
1921
+ "match-problem",
1922
+ "multiple-choice-problem",
1923
+ "noteref",
1924
+ "notice",
1925
+ "ordinal",
1926
+ "other-credits",
1927
+ "page-list",
1928
+ "pagebreak",
1929
+ "panel",
1930
+ "panel-group",
1931
+ "part",
1932
+ "practice",
1933
+ "practices",
1934
+ "preamble",
1935
+ "preface",
1936
+ "prologue",
1937
+ "pullquote",
1938
+ "qna",
1939
+ "question",
1940
+ "referrer",
1941
+ "revision-history",
1942
+ "seriespage",
1943
+ "sound-area",
1944
+ "subtitle",
1945
+ "tip",
1946
+ "title",
1947
+ "titlepage",
1948
+ "toc",
1949
+ "toc-brief",
1950
+ "topic-sentence",
1951
+ "true-false-problem",
1952
+ "volume"
1953
+ ]);
1954
+ var TIME_RE = /^(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d{1,3})?)?$/;
1955
+ var TZ_RE = /(?:Z|[+-](?:[01]\d|2[0-3]):?[0-5]\d)$/;
1956
+ var DATE_RE = /^\d{4,}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/;
1957
+ var ISO_DURATION_RE = /^P(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d{1,3})?S)?)?$/;
1958
+ var INFORMAL_DURATION_RE = /^\s*(?:\d+(?:\.\d{1,3})?[WDHMS]\s*)+$/;
1959
+ function isValidDatetime(value) {
1960
+ const trimmed = value.trim();
1961
+ if (trimmed === "") return false;
1962
+ if (trimmed.startsWith("P")) {
1963
+ if (!ISO_DURATION_RE.test(trimmed)) return false;
1964
+ if (trimmed === "P" || trimmed === "PT") return false;
1965
+ if (trimmed.endsWith("T")) return false;
1966
+ return true;
1967
+ }
1968
+ if (INFORMAL_DURATION_RE.test(value)) return true;
1969
+ if (/^\d{4,}$/.test(trimmed)) return true;
1970
+ if (/^\d{4,}-(?:0[1-9]|1[0-2])$/.test(trimmed)) return true;
1971
+ if (/^-?-?(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/.test(trimmed)) return true;
1972
+ if (DATE_RE.test(trimmed)) return true;
1973
+ if (/^\d{4,}-W(?:0[1-9]|[1-4]\d|5[0-3])$/.test(trimmed)) return true;
1974
+ if (TIME_RE.test(trimmed)) return true;
1975
+ const dtMatch = /^(\d{4,}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01]))[T ]([\s\S]+)$/.exec(trimmed);
1976
+ if (dtMatch?.[2]) {
1977
+ let timePart = dtMatch[2];
1978
+ const tzMatch = TZ_RE.exec(timePart);
1979
+ if (tzMatch) timePart = timePart.substring(0, timePart.length - tzMatch[0].length);
1980
+ return TIME_RE.test(timePart);
1981
+ }
1982
+ return false;
1983
+ }
1719
1984
  var HTML_ENTITIES = /* @__PURE__ */ new Set([
1720
1985
  "nbsp",
1721
1986
  "iexcl",
@@ -1815,6 +2080,7 @@ var HTML_ENTITIES = /* @__PURE__ */ new Set([
1815
2080
  "yuml"
1816
2081
  ]);
1817
2082
  var ContentValidator = class {
2083
+ cssWithRemoteResources = /* @__PURE__ */ new Set();
1818
2084
  validate(context, registry, refValidator) {
1819
2085
  const packageDoc = context.packageDocument;
1820
2086
  if (!packageDoc) {
@@ -1822,13 +2088,18 @@ var ContentValidator = class {
1822
2088
  }
1823
2089
  const opfPath = context.opfPath ?? "";
1824
2090
  const opfDir = opfPath.includes("/") ? opfPath.substring(0, opfPath.lastIndexOf("/")) : "";
2091
+ if (refValidator) {
2092
+ for (const item of packageDoc.manifest) {
2093
+ if (item.mediaType === "text/css") {
2094
+ const fullPath = resolveManifestHref(opfDir, item.href);
2095
+ this.validateCSSDocument(context, fullPath, opfDir, refValidator);
2096
+ }
2097
+ }
2098
+ }
1825
2099
  for (const item of packageDoc.manifest) {
1826
2100
  if (item.mediaType === "application/xhtml+xml") {
1827
2101
  const fullPath = resolveManifestHref(opfDir, item.href);
1828
2102
  this.validateXHTMLDocument(context, fullPath, item.id, opfDir, registry, refValidator);
1829
- } else if (item.mediaType === "text/css" && refValidator) {
1830
- const fullPath = resolveManifestHref(opfDir, item.href);
1831
- this.validateCSSDocument(context, fullPath, opfDir, refValidator);
1832
2103
  } else if (item.mediaType === "image/svg+xml") {
1833
2104
  const fullPath = resolveManifestHref(opfDir, item.href);
1834
2105
  if (registry) {
@@ -1837,7 +2108,51 @@ var ContentValidator = class {
1837
2108
  if (context.version.startsWith("3")) {
1838
2109
  this.validateSVGDocument(context, fullPath, item);
1839
2110
  }
2111
+ if (refValidator) {
2112
+ this.extractSVGReferences(context, fullPath, opfDir, refValidator);
2113
+ }
1840
2114
  }
2115
+ this.validateMediaFile(context, item, opfDir);
2116
+ }
2117
+ }
2118
+ validateMediaFile(context, item, opfDir) {
2119
+ const declaredType = item.mediaType;
2120
+ const magicEntry = IMAGE_MAGIC.find((m) => m.mime === declaredType);
2121
+ if (!magicEntry) return;
2122
+ const fullPath = resolveManifestHref(opfDir, item.href);
2123
+ const fileData = context.files.get(fullPath);
2124
+ if (!fileData) return;
2125
+ const bytes = typeof fileData === "string" ? new TextEncoder().encode(fileData) : fileData;
2126
+ if (bytes.length < 4) {
2127
+ pushMessage(context.messages, {
2128
+ id: MessageId.MED_004,
2129
+ message: "Image file header may be corrupted",
2130
+ location: { path: fullPath }
2131
+ });
2132
+ pushMessage(context.messages, {
2133
+ id: MessageId.PKG_021,
2134
+ message: "Corrupted image file encountered",
2135
+ location: { path: fullPath }
2136
+ });
2137
+ return;
2138
+ }
2139
+ const headerMatches = magicEntry.bytes.every((b, i) => bytes[i] === b);
2140
+ if (!headerMatches) {
2141
+ const actualType = IMAGE_MAGIC.find((m) => m.bytes.every((b, i) => bytes[i] === b));
2142
+ pushMessage(context.messages, {
2143
+ id: MessageId.OPF_029,
2144
+ message: `File does not match declared media type "${declaredType}"${actualType ? ` (appears to be ${actualType.mime})` : ""}`,
2145
+ location: { path: fullPath }
2146
+ });
2147
+ return;
2148
+ }
2149
+ const ext = item.href.includes(".") ? item.href.substring(item.href.lastIndexOf(".")).toLowerCase() : "";
2150
+ if (ext && !magicEntry.extensions.includes(ext)) {
2151
+ pushMessage(context.messages, {
2152
+ id: MessageId.PKG_022,
2153
+ message: `Wrong file extension "${ext}" for declared media type "${declaredType}"`,
2154
+ location: { path: fullPath }
2155
+ });
1841
2156
  }
1842
2157
  }
1843
2158
  extractSVGIDs(context, path, registry) {
@@ -1880,9 +2195,154 @@ var ContentValidator = class {
1880
2195
  location: { path }
1881
2196
  });
1882
2197
  }
2198
+ this.checkDuplicateIDs(context, path, root);
2199
+ this.checkSVGInvalidIDs(context, path, root);
2200
+ this.validateSvgEpubType(context, path, root);
2201
+ this.checkUnknownEpubAttributes(context, path, root);
2202
+ } finally {
2203
+ doc.dispose();
2204
+ }
2205
+ }
2206
+ /**
2207
+ * Extract references from SVG documents: font-face-uri, xml-stylesheet PI, @import in style
2208
+ */
2209
+ extractSVGReferences(context, path, opfDir, refValidator) {
2210
+ const svgData = context.files.get(path);
2211
+ if (!svgData) return;
2212
+ const svgContent = new TextDecoder().decode(svgData);
2213
+ let doc;
2214
+ try {
2215
+ doc = libxml2Wasm.XmlDocument.fromString(svgContent);
2216
+ } catch {
2217
+ return;
2218
+ }
2219
+ const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
2220
+ try {
2221
+ const root = doc.root;
2222
+ try {
2223
+ const fontFaceUris = root.find(".//svg:font-face-uri", {
2224
+ svg: "http://www.w3.org/2000/svg"
2225
+ });
2226
+ for (const uri of fontFaceUris) {
2227
+ const href = this.getAttribute(uri, "xlink:href") ?? this.getAttribute(uri, "href");
2228
+ if (!href) continue;
2229
+ if (href.startsWith("http://") || href.startsWith("https://")) {
2230
+ refValidator.addReference({
2231
+ url: href,
2232
+ targetResource: href,
2233
+ type: "font" /* FONT */,
2234
+ location: { path, line: uri.line }
2235
+ });
2236
+ } else {
2237
+ const resolvedPath = this.resolveRelativePath(docDir, href, opfDir);
2238
+ refValidator.addReference({
2239
+ url: href,
2240
+ targetResource: resolvedPath,
2241
+ type: "font" /* FONT */,
2242
+ location: { path, line: uri.line }
2243
+ });
2244
+ }
2245
+ }
2246
+ } catch {
2247
+ }
2248
+ try {
2249
+ const styles = root.find(".//svg:style", { svg: "http://www.w3.org/2000/svg" });
2250
+ for (const style of styles) {
2251
+ const cssContent = style.content;
2252
+ if (cssContent) {
2253
+ this.extractCSSImports(path, cssContent, opfDir, refValidator);
2254
+ }
2255
+ }
2256
+ } catch {
2257
+ }
2258
+ this.extractSVGUseReferences(context, path, root, docDir, opfDir, refValidator);
1883
2259
  } finally {
1884
2260
  doc.dispose();
1885
2261
  }
2262
+ this.extractXmlStylesheetPIs(svgContent, path, docDir, opfDir, refValidator);
2263
+ }
2264
+ /**
2265
+ * Extract href from <?xml-stylesheet?> processing instructions
2266
+ */
2267
+ extractXmlStylesheetPIs(content, path, docDir, opfDir, refValidator) {
2268
+ const piRegex = /<\?xml-stylesheet\s+([^?]*)\?>/g;
2269
+ let match;
2270
+ while ((match = piRegex.exec(content)) !== null) {
2271
+ const attrs = match[1];
2272
+ if (!attrs) continue;
2273
+ const hrefMatch = /href\s*=\s*["']([^"']*)["']/.exec(attrs);
2274
+ if (!hrefMatch?.[1]) continue;
2275
+ const href = hrefMatch[1];
2276
+ const beforeMatch = content.substring(0, match.index);
2277
+ const line = beforeMatch.split("\n").length;
2278
+ if (href.startsWith("http://") || href.startsWith("https://")) {
2279
+ refValidator.addReference({
2280
+ url: href,
2281
+ targetResource: href,
2282
+ type: "stylesheet" /* STYLESHEET */,
2283
+ location: { path, line }
2284
+ });
2285
+ } else {
2286
+ const resolvedPath = this.resolveRelativePath(docDir, href, opfDir);
2287
+ refValidator.addReference({
2288
+ url: href,
2289
+ targetResource: resolvedPath,
2290
+ type: "stylesheet" /* STYLESHEET */,
2291
+ location: { path, line }
2292
+ });
2293
+ }
2294
+ }
2295
+ }
2296
+ extractSVGUseReferences(context, path, root, docDir, opfDir, refValidator) {
2297
+ try {
2298
+ const svgUseXlink = root.find(".//svg:use[@xlink:href]", {
2299
+ svg: "http://www.w3.org/2000/svg",
2300
+ xlink: "http://www.w3.org/1999/xlink"
2301
+ });
2302
+ const svgUseHref = root.find(".//svg:use[@href]", {
2303
+ svg: "http://www.w3.org/2000/svg"
2304
+ });
2305
+ for (const useNode of [...svgUseXlink, ...svgUseHref]) {
2306
+ const useElem = useNode;
2307
+ const href = this.getAttribute(useElem, "xlink:href") ?? this.getAttribute(useElem, "href");
2308
+ if (href === null) continue;
2309
+ if (href.startsWith("http://") || href.startsWith("https://")) continue;
2310
+ const line = useNode.line;
2311
+ if (href === "" || !href.includes("#")) {
2312
+ pushMessage(context.messages, {
2313
+ id: MessageId.RSC_015,
2314
+ message: `SVG "use" element requires a fragment identifier, but found "${href}"`,
2315
+ location: { path, line }
2316
+ });
2317
+ continue;
2318
+ }
2319
+ if (href.startsWith("#")) {
2320
+ refValidator.addReference({
2321
+ url: href,
2322
+ targetResource: path,
2323
+ fragment: href.slice(1),
2324
+ type: "svg-symbol" /* SVG_SYMBOL */,
2325
+ location: { path, line }
2326
+ });
2327
+ continue;
2328
+ }
2329
+ const resolvedPath = this.resolveRelativePath(docDir, href, opfDir);
2330
+ const hashIndex = resolvedPath.indexOf("#");
2331
+ const targetResource = hashIndex >= 0 ? resolvedPath.slice(0, hashIndex) : path;
2332
+ const fragment = hashIndex >= 0 ? resolvedPath.slice(hashIndex + 1) : void 0;
2333
+ const useRef = {
2334
+ url: href,
2335
+ targetResource,
2336
+ type: "svg-symbol" /* SVG_SYMBOL */,
2337
+ location: { path, line }
2338
+ };
2339
+ if (fragment) {
2340
+ useRef.fragment = fragment;
2341
+ }
2342
+ refValidator.addReference(useRef);
2343
+ }
2344
+ } catch {
2345
+ }
1886
2346
  }
1887
2347
  detectSVGRemoteResources(root) {
1888
2348
  try {
@@ -1931,6 +2391,7 @@ var ContentValidator = class {
1931
2391
  (ref) => ref.url.startsWith("http://") || ref.url.startsWith("https://")
1932
2392
  );
1933
2393
  if (hasRemoteResources) {
2394
+ this.cssWithRemoteResources.add(path);
1934
2395
  const packageDoc = context.packageDocument;
1935
2396
  if (packageDoc) {
1936
2397
  const manifestItem = packageDoc.manifest.find(
@@ -1949,9 +2410,11 @@ var ContentValidator = class {
1949
2410
  for (const ref of result.references) {
1950
2411
  if (ref.type === "font") {
1951
2412
  if (ref.url.startsWith("http://") || ref.url.startsWith("https://")) {
2413
+ const hashIndex = ref.url.indexOf("#");
2414
+ const targetResource = hashIndex >= 0 ? ref.url.slice(0, hashIndex) : ref.url;
1952
2415
  refValidator.addReference({
1953
2416
  url: ref.url,
1954
- targetResource: ref.url,
2417
+ targetResource,
1955
2418
  type: "font" /* FONT */,
1956
2419
  location: { path }
1957
2420
  });
@@ -1968,9 +2431,11 @@ var ContentValidator = class {
1968
2431
  }
1969
2432
  } else if (ref.type === "image") {
1970
2433
  if (ref.url.startsWith("http://") || ref.url.startsWith("https://")) {
2434
+ const hashIndex = ref.url.indexOf("#");
2435
+ const targetResource = hashIndex >= 0 ? ref.url.slice(0, hashIndex) : ref.url;
1971
2436
  refValidator.addReference({
1972
2437
  url: ref.url,
1973
- targetResource: ref.url,
2438
+ targetResource,
1974
2439
  type: "image" /* IMAGE */,
1975
2440
  location: { path }
1976
2441
  });
@@ -1985,9 +2450,27 @@ var ContentValidator = class {
1985
2450
  location: { path }
1986
2451
  });
1987
2452
  }
2453
+ } else if (ref.type === "import") {
2454
+ const location = { path };
2455
+ if (ref.line !== void 0) location.line = ref.line;
2456
+ if (ref.url.startsWith("http://") || ref.url.startsWith("https://")) {
2457
+ refValidator.addReference({
2458
+ url: ref.url,
2459
+ targetResource: ref.url,
2460
+ type: "stylesheet" /* STYLESHEET */,
2461
+ location
2462
+ });
2463
+ } else {
2464
+ const resolvedPath = this.resolveRelativePath(cssDir, ref.url, opfDir);
2465
+ refValidator.addReference({
2466
+ url: ref.url,
2467
+ targetResource: resolvedPath,
2468
+ type: "stylesheet" /* STYLESHEET */,
2469
+ location
2470
+ });
2471
+ }
1988
2472
  }
1989
2473
  }
1990
- this.extractCSSImports(path, cssContent, opfDir, refValidator);
1991
2474
  }
1992
2475
  validateXHTMLDocument(context, path, itemId, opfDir, registry, refValidator) {
1993
2476
  const data = context.files.get(path);
@@ -2000,6 +2483,43 @@ var ContentValidator = class {
2000
2483
  return;
2001
2484
  }
2002
2485
  this.checkUnescapedAmpersands(context, path, content);
2486
+ if (context.version !== "2.0") {
2487
+ const doctypeMatch = /<!DOCTYPE\s+html\b([\s\S]*?)>/i.exec(content);
2488
+ if (doctypeMatch) {
2489
+ const inner = doctypeMatch[1] ?? "";
2490
+ const hasPublic = /\bPUBLIC\b/i.test(inner);
2491
+ const hasSystem = /\bSYSTEM\b/i.test(inner);
2492
+ const isLegacyCompat = /['"]about:legacy-compat['"]/.test(inner);
2493
+ if (hasPublic || hasSystem && !isLegacyCompat) {
2494
+ pushMessage(context.messages, {
2495
+ id: MessageId.HTM_004,
2496
+ message: 'Irregular DOCTYPE found; expected "<!DOCTYPE html>"',
2497
+ location: { path }
2498
+ });
2499
+ }
2500
+ }
2501
+ }
2502
+ if (context.version !== "2.0") {
2503
+ const entityRe = /<!ENTITY\s+\w+\s+(?:SYSTEM|PUBLIC)\s/gi;
2504
+ let entityMatch = entityRe.exec(content);
2505
+ while (entityMatch) {
2506
+ pushMessage(context.messages, {
2507
+ id: MessageId.HTM_003,
2508
+ message: "External entities are not allowed in EPUB 3 content documents",
2509
+ location: { path }
2510
+ });
2511
+ entityMatch = entityRe.exec(content);
2512
+ }
2513
+ }
2514
+ const xmlVersionMatch = /<\?xml\s[^?]*version\s*=\s*["']([^"']+)["']/.exec(content);
2515
+ if (xmlVersionMatch?.[1] && xmlVersionMatch[1] !== "1.0") {
2516
+ pushMessage(context.messages, {
2517
+ id: MessageId.HTM_001,
2518
+ message: `XML version "${xmlVersionMatch[1]}" is not allowed; must be "1.0"`,
2519
+ location: { path }
2520
+ });
2521
+ return;
2522
+ }
2003
2523
  let doc = null;
2004
2524
  try {
2005
2525
  doc = libxml2Wasm.XmlDocument.fromString(content);
@@ -2019,8 +2539,9 @@ var ContentValidator = class {
2019
2539
  if (column !== void 0) {
2020
2540
  location.column = column;
2021
2541
  }
2542
+ const isEntityError = error.message.includes("Entity '") || error.message.includes("EntityRef:");
2022
2543
  pushMessage(context.messages, {
2023
- id: MessageId.HTM_004,
2544
+ id: isEntityError ? MessageId.RSC_016 : MessageId.HTM_004,
2024
2545
  message,
2025
2546
  location
2026
2547
  });
@@ -2050,10 +2571,19 @@ var ContentValidator = class {
2050
2571
  const title = root.get(".//html:title", { html: "http://www.w3.org/1999/xhtml" });
2051
2572
  if (!title) {
2052
2573
  pushMessage(context.messages, {
2053
- id: MessageId.HTM_003,
2054
- message: "XHTML document must have a title element",
2574
+ id: MessageId.RSC_017,
2575
+ message: 'The "head" element should have a "title" child element',
2055
2576
  location: { path }
2056
2577
  });
2578
+ } else {
2579
+ const titleText = title.content.trim();
2580
+ if (titleText === "") {
2581
+ pushMessage(context.messages, {
2582
+ id: MessageId.RSC_005,
2583
+ message: 'The "title" element must not be empty',
2584
+ location: { path, line: title.line }
2585
+ });
2586
+ }
2057
2587
  }
2058
2588
  const body = root.get(".//html:body", { html: "http://www.w3.org/1999/xhtml" });
2059
2589
  if (!body) {
@@ -2094,6 +2624,13 @@ var ContentValidator = class {
2094
2624
  location: { path }
2095
2625
  });
2096
2626
  }
2627
+ if (!hasMathML && manifestItem?.properties?.includes("mathml")) {
2628
+ pushMessage(context.messages, {
2629
+ id: MessageId.OPF_015,
2630
+ message: 'The property "mathml" should not be declared in the OPF file',
2631
+ location: { path }
2632
+ });
2633
+ }
2097
2634
  const hasSVG = this.detectSVG(context, path, root);
2098
2635
  if (hasSVG && !manifestItem?.properties?.includes("svg")) {
2099
2636
  pushMessage(context.messages, {
@@ -2117,7 +2654,14 @@ var ContentValidator = class {
2117
2654
  location: { path }
2118
2655
  });
2119
2656
  }
2120
- const hasRemoteResources = this.detectRemoteResources(context, path, root);
2657
+ if (!hasSwitch && manifestItem?.properties?.includes("switch")) {
2658
+ pushMessage(context.messages, {
2659
+ id: MessageId.OPF_015,
2660
+ message: 'The property "switch" should not be declared in the OPF file',
2661
+ location: { path }
2662
+ });
2663
+ }
2664
+ const hasRemoteResources = this.detectRemoteResources(context, path, root, opfDir);
2121
2665
  if (hasRemoteResources && !manifestItem?.properties?.includes("remote-resources")) {
2122
2666
  pushMessage(context.messages, {
2123
2667
  id: MessageId.OPF_014,
@@ -2134,24 +2678,49 @@ var ContentValidator = class {
2134
2678
  }
2135
2679
  }
2136
2680
  this.checkDiscouragedElements(context, path, root);
2681
+ this.checkSSMLPh(context, path, root, content);
2682
+ this.checkObsoleteHTML(context, path, root);
2683
+ this.checkDuplicateIDs(context, path, root);
2684
+ this.checkImgSrcEmpty(context, path, root);
2685
+ this.checkStyleInBody(context, path, root);
2686
+ this.validateInlineStyles(context, path, root);
2687
+ this.checkHttpEquivCharset(context, path, root);
2688
+ this.checkLangMismatch(context, path, root);
2689
+ this.checkDpubAriaDeprecated(context, path, root);
2690
+ this.checkTableBorder(context, path, root);
2691
+ this.checkTimeElement(context, path, root);
2692
+ this.checkMathMLAnnotations(context, path, root);
2693
+ this.checkReservedNamespace(context, path, content);
2694
+ this.checkDataAttributes(context, path, root);
2137
2695
  this.checkAccessibility(context, path, root);
2138
2696
  this.validateImages(context, path, root);
2139
2697
  if (context.version.startsWith("3")) {
2140
2698
  this.validateEpubTypes(context, path, root);
2141
2699
  }
2700
+ this.validateEpubSwitch(context, path, root);
2701
+ this.validateEpubTrigger(context, path, root);
2702
+ this.validateStyleAttributes(context, path, root);
2142
2703
  this.validateStylesheetLinks(context, path, root);
2143
2704
  this.validateViewportMeta(context, path, root, manifestItem);
2144
2705
  if (registry) {
2145
2706
  this.extractAndRegisterIDs(path, root, registry);
2146
2707
  }
2147
2708
  if (refValidator && opfDir !== void 0) {
2148
- this.extractAndRegisterHyperlinks(context, path, root, opfDir, refValidator);
2709
+ this.extractAndRegisterHyperlinks(context, path, root, opfDir, refValidator, !!isNavItem);
2149
2710
  this.extractAndRegisterStylesheets(path, root, opfDir, refValidator);
2150
- this.extractAndRegisterImages(path, root, opfDir, refValidator);
2711
+ this.extractAndRegisterImages(context, path, root, opfDir, refValidator, registry);
2151
2712
  this.extractAndRegisterMathMLAltimg(path, root, opfDir, refValidator);
2152
2713
  this.extractAndRegisterScripts(path, root, opfDir, refValidator);
2153
2714
  this.extractAndRegisterCiteAttributes(path, root, opfDir, refValidator);
2154
- this.extractAndRegisterMediaElements(path, root, opfDir, refValidator);
2715
+ this.extractAndRegisterMediaElements(context, path, root, opfDir, refValidator, registry);
2716
+ this.extractAndRegisterEmbeddedElements(
2717
+ context,
2718
+ path,
2719
+ root,
2720
+ opfDir,
2721
+ refValidator,
2722
+ registry
2723
+ );
2155
2724
  }
2156
2725
  } finally {
2157
2726
  doc.dispose();
@@ -2207,14 +2776,12 @@ var ContentValidator = class {
2207
2776
  return epubTypeAttr ? epubTypeAttr.value.trim().split(/\s+/) : [];
2208
2777
  };
2209
2778
  let tocNav;
2210
- let tocEpubTypeValue = "";
2211
2779
  let pageListCount = 0;
2212
2780
  let landmarksCount = 0;
2213
2781
  for (const nav of navElements) {
2214
2782
  const types = getNavTypes(nav);
2215
2783
  if (types.includes("toc") && !tocNav) {
2216
2784
  tocNav = nav;
2217
- tocEpubTypeValue = types.join(" ");
2218
2785
  }
2219
2786
  if (types.includes("page-list")) pageListCount++;
2220
2787
  if (types.includes("landmarks")) landmarksCount++;
@@ -2265,7 +2832,7 @@ var ContentValidator = class {
2265
2832
  }
2266
2833
  this.checkNavHeadingContent(context, path, root);
2267
2834
  this.checkNavHiddenAttribute(context, path, root);
2268
- this.checkNavRemoteLinks(context, path, root, tocEpubTypeValue);
2835
+ this.checkNavRemoteLinks(context, path, root);
2269
2836
  this.collectTocLinks(context, path, tocNav);
2270
2837
  }
2271
2838
  checkNavFirstChildHeading(context, path, navElem) {
@@ -2448,27 +3015,33 @@ var ContentValidator = class {
2448
3015
  }
2449
3016
  }
2450
3017
  }
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
- });
2469
- }
2470
- }
2471
- }
3018
+ checkNavRemoteLinks(context, path, root) {
3019
+ const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
3020
+ const navElements = root.find(".//html:nav", HTML_NS);
3021
+ for (const nav of navElements) {
3022
+ const navElem = nav;
3023
+ const epubTypeAttr = "attrs" in navElem ? navElem.attrs.find(
3024
+ (attr) => attr.name === "type" && attr.prefix === "epub" && attr.namespaceUri === "http://www.idpf.org/2007/ops"
3025
+ ) : void 0;
3026
+ const types = epubTypeAttr ? epubTypeAttr.value.trim().split(/\s+/) : [];
3027
+ const isToc = types.includes("toc");
3028
+ const isLandmarks = types.includes("landmarks");
3029
+ const isPageList = types.includes("page-list");
3030
+ if (!isToc && !isLandmarks && !isPageList) continue;
3031
+ const navType = isToc ? "toc" : isLandmarks ? "landmarks" : "page-list";
3032
+ const links = navElem.find(".//html:a[@href]", HTML_NS);
3033
+ for (const link of links) {
3034
+ const href = this.getAttribute(link, "href");
3035
+ if (href && (href.startsWith("http://") || href.startsWith("https://"))) {
3036
+ pushMessage(context.messages, {
3037
+ id: MessageId.NAV_010,
3038
+ message: `"${navType}" nav must not link to remote resources; found link to "${href}"`,
3039
+ location: { path }
3040
+ });
3041
+ }
3042
+ }
3043
+ }
3044
+ }
2472
3045
  collectTocLinks(context, path, tocNav) {
2473
3046
  const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
2474
3047
  const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
@@ -2574,7 +3147,7 @@ var ContentValidator = class {
2574
3147
  * - Remote scripts do NOT require the property (scripted property is used instead)
2575
3148
  * - Remote stylesheets DO require the property
2576
3149
  */
2577
- detectRemoteResources(_context, _path, root) {
3150
+ detectRemoteResources(_context, path, root, opfDir) {
2578
3151
  const images = root.find(".//html:img[@src]", { html: "http://www.w3.org/1999/xhtml" });
2579
3152
  for (const img of images) {
2580
3153
  const src = this.getAttribute(img, "src");
@@ -2603,9 +3176,24 @@ var ContentValidator = class {
2603
3176
  return true;
2604
3177
  }
2605
3178
  }
3179
+ const objects = root.find(".//html:object[@data]", { html: "http://www.w3.org/1999/xhtml" });
3180
+ for (const obj of objects) {
3181
+ const data = this.getAttribute(obj, "data");
3182
+ if (data && (data.startsWith("http://") || data.startsWith("https://"))) {
3183
+ return true;
3184
+ }
3185
+ }
3186
+ const embeds = root.find(".//html:embed[@src]", { html: "http://www.w3.org/1999/xhtml" });
3187
+ for (const embed of embeds) {
3188
+ const src = this.getAttribute(embed, "src");
3189
+ if (src && (src.startsWith("http://") || src.startsWith("https://"))) {
3190
+ return true;
3191
+ }
3192
+ }
2606
3193
  const linkElements = root.find(".//html:link[@rel and @href]", {
2607
3194
  html: "http://www.w3.org/1999/xhtml"
2608
3195
  });
3196
+ const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
2609
3197
  for (const linkElem of linkElements) {
2610
3198
  const rel = this.getAttribute(linkElem, "rel");
2611
3199
  const href = this.getAttribute(linkElem, "href");
@@ -2613,6 +3201,10 @@ var ContentValidator = class {
2613
3201
  if (href.startsWith("http://") || href.startsWith("https://")) {
2614
3202
  return true;
2615
3203
  }
3204
+ const resolvedCss = this.resolveRelativePath(docDir, href, opfDir ?? "");
3205
+ if (this.cssWithRemoteResources.has(resolvedCss)) {
3206
+ return true;
3207
+ }
2616
3208
  }
2617
3209
  }
2618
3210
  const styleElements = root.find(".//html:style", { html: "http://www.w3.org/1999/xhtml" });
@@ -2640,6 +3232,654 @@ var ContentValidator = class {
2640
3232
  }
2641
3233
  }
2642
3234
  }
3235
+ checkSSMLPh(context, path, root, content) {
3236
+ const ssmlPhPattern = /\bssml:ph\s*=\s*"([^"]*)"/g;
3237
+ let match;
3238
+ while ((match = ssmlPhPattern.exec(content)) !== null) {
3239
+ if (match[1]?.trim() === "") {
3240
+ const line = content.substring(0, match.index).split("\n").length;
3241
+ pushMessage(context.messages, {
3242
+ id: MessageId.HTM_007,
3243
+ message: "The ssml:ph attribute value should not be empty",
3244
+ location: { path, line }
3245
+ });
3246
+ }
3247
+ }
3248
+ }
3249
+ checkObsoleteHTML(context, path, root) {
3250
+ const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
3251
+ const obsoleteGlobalAttrs = ["contextmenu", "dropzone"];
3252
+ for (const attr of obsoleteGlobalAttrs) {
3253
+ try {
3254
+ const elements = root.find(`.//*[@${attr}]`);
3255
+ for (const el of elements) {
3256
+ pushMessage(context.messages, {
3257
+ id: MessageId.RSC_005,
3258
+ message: `The "${attr}" attribute is obsolete`,
3259
+ location: { path, line: el.line }
3260
+ });
3261
+ }
3262
+ } catch {
3263
+ }
3264
+ }
3265
+ const obsoleteElementAttrs = [
3266
+ ["typemustmatch", ".//html:object[@typemustmatch]"],
3267
+ ["pubdate", ".//html:time[@pubdate]"],
3268
+ ["seamless", ".//html:iframe[@seamless]"]
3269
+ ];
3270
+ for (const [attr, xpath] of obsoleteElementAttrs) {
3271
+ try {
3272
+ const elements = root.find(xpath, HTML_NS);
3273
+ for (const el of elements) {
3274
+ pushMessage(context.messages, {
3275
+ id: MessageId.RSC_005,
3276
+ message: `The "${attr}" attribute is obsolete`,
3277
+ location: { path, line: el.line }
3278
+ });
3279
+ }
3280
+ } catch {
3281
+ }
3282
+ }
3283
+ try {
3284
+ const keygens = root.find(".//html:keygen", HTML_NS);
3285
+ for (const keygen of keygens) {
3286
+ pushMessage(context.messages, {
3287
+ id: MessageId.RSC_005,
3288
+ message: 'The "keygen" element is obsolete',
3289
+ location: { path, line: keygen.line }
3290
+ });
3291
+ }
3292
+ } catch {
3293
+ }
3294
+ try {
3295
+ const menuTypes = root.find(".//html:menu[@type]", HTML_NS);
3296
+ for (const menuType of menuTypes) {
3297
+ pushMessage(context.messages, {
3298
+ id: MessageId.RSC_005,
3299
+ message: 'The "type" attribute on the "menu" element is obsolete',
3300
+ location: { path, line: menuType.line }
3301
+ });
3302
+ }
3303
+ } catch {
3304
+ }
3305
+ try {
3306
+ const commands = root.find(".//html:command", HTML_NS);
3307
+ for (const command of commands) {
3308
+ pushMessage(context.messages, {
3309
+ id: MessageId.RSC_005,
3310
+ message: 'The "command" element is obsolete',
3311
+ location: { path, line: command.line }
3312
+ });
3313
+ }
3314
+ } catch {
3315
+ }
3316
+ }
3317
+ checkDuplicateIDs(context, path, root) {
3318
+ const seen = /* @__PURE__ */ new Map();
3319
+ const elements = root.find(".//*[@id]");
3320
+ for (const elem of elements) {
3321
+ const id = this.getAttribute(elem, "id");
3322
+ if (id) {
3323
+ if (seen.has(id)) {
3324
+ pushMessage(context.messages, {
3325
+ id: MessageId.RSC_005,
3326
+ message: `Duplicate ID "${id}"`,
3327
+ location: { path, line: elem.line }
3328
+ });
3329
+ } else {
3330
+ seen.set(id, elem.line);
3331
+ }
3332
+ }
3333
+ }
3334
+ }
3335
+ checkImgSrcEmpty(context, path, root) {
3336
+ const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
3337
+ try {
3338
+ const imgs = root.find(".//html:img[@src]", HTML_NS);
3339
+ for (const img of imgs) {
3340
+ const src = this.getAttribute(img, "src");
3341
+ if (src !== null && src.trim() === "") {
3342
+ pushMessage(context.messages, {
3343
+ id: MessageId.RSC_005,
3344
+ message: 'The "src" attribute must not be empty',
3345
+ location: { path, line: img.line }
3346
+ });
3347
+ }
3348
+ }
3349
+ } catch {
3350
+ }
3351
+ }
3352
+ checkStyleInBody(context, path, root) {
3353
+ const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
3354
+ try {
3355
+ const bodyStyles = root.find(".//html:body//html:style", HTML_NS);
3356
+ for (const style of bodyStyles) {
3357
+ pushMessage(context.messages, {
3358
+ id: MessageId.RSC_005,
3359
+ message: 'The "style" element must not appear in the document body',
3360
+ location: { path, line: style.line }
3361
+ });
3362
+ }
3363
+ } catch {
3364
+ }
3365
+ }
3366
+ checkHttpEquivCharset(context, path, root) {
3367
+ const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
3368
+ try {
3369
+ const metas = root.find(".//html:head/html:meta", HTML_NS);
3370
+ let hasCharsetMeta = false;
3371
+ let hasHttpEquivContentType = false;
3372
+ for (const meta of metas) {
3373
+ const el = meta;
3374
+ const charset = this.getAttribute(el, "charset");
3375
+ if (charset !== null) {
3376
+ hasCharsetMeta = true;
3377
+ }
3378
+ const httpEquiv = this.getAttribute(el, "http-equiv");
3379
+ if (httpEquiv?.toLowerCase() === "content-type") {
3380
+ hasHttpEquivContentType = true;
3381
+ const contentAttr = (this.getAttribute(el, "content") ?? "").trim();
3382
+ if (!/^text\/html;\s*charset=utf-8$/i.test(contentAttr)) {
3383
+ pushMessage(context.messages, {
3384
+ id: MessageId.RSC_005,
3385
+ message: `The meta element in encoding declaration state must have the value "text/html; charset=utf-8"`,
3386
+ location: { path, line: el.line }
3387
+ });
3388
+ }
3389
+ }
3390
+ }
3391
+ if (hasCharsetMeta && hasHttpEquivContentType) {
3392
+ pushMessage(context.messages, {
3393
+ id: MessageId.RSC_005,
3394
+ message: "The document must not contain both a meta charset declaration and a meta http-equiv Content-Type declaration",
3395
+ location: { path }
3396
+ });
3397
+ }
3398
+ } catch {
3399
+ }
3400
+ }
3401
+ checkSVGInvalidIDs(context, path, root) {
3402
+ const XML_NAME_START_RE = /^[a-zA-Z_:\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF]/;
3403
+ const elements = root.find(".//*[@id]");
3404
+ for (const elem of elements) {
3405
+ const id = this.getAttribute(elem, "id");
3406
+ if (id && !XML_NAME_START_RE.test(id)) {
3407
+ pushMessage(context.messages, {
3408
+ id: MessageId.RSC_005,
3409
+ message: `Invalid ID value "${id}"`,
3410
+ location: { path, line: elem.line }
3411
+ });
3412
+ }
3413
+ }
3414
+ const rootId = this.getAttribute(root, "id");
3415
+ if (rootId && !XML_NAME_START_RE.test(rootId)) {
3416
+ pushMessage(context.messages, {
3417
+ id: MessageId.RSC_005,
3418
+ message: `Invalid ID value "${rootId}"`,
3419
+ location: { path, line: root.line }
3420
+ });
3421
+ }
3422
+ }
3423
+ validateInlineStyles(context, path, root) {
3424
+ const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
3425
+ try {
3426
+ const styles = root.find(".//html:style", HTML_NS);
3427
+ for (const style of styles) {
3428
+ const cssContent = style.content;
3429
+ if (cssContent) {
3430
+ const cssValidator = new CSSValidator();
3431
+ cssValidator.validate(context, cssContent, path);
3432
+ }
3433
+ }
3434
+ } catch {
3435
+ }
3436
+ }
3437
+ checkLangMismatch(context, path, root) {
3438
+ const lang = root.attr("lang")?.value ?? null;
3439
+ const xmlLang = root.attr("lang", "xml")?.value ?? null;
3440
+ if (lang !== null && xmlLang !== null && lang.toLowerCase() !== xmlLang.toLowerCase()) {
3441
+ pushMessage(context.messages, {
3442
+ id: MessageId.RSC_005,
3443
+ message: "The lang and xml:lang attributes must have the same value",
3444
+ location: { path, line: root.line }
3445
+ });
3446
+ }
3447
+ }
3448
+ checkDpubAriaDeprecated(context, path, root) {
3449
+ const DEPRECATED_ROLES = ["doc-endnote", "doc-biblioentry"];
3450
+ try {
3451
+ const elements = root.find(".//*[@role]");
3452
+ for (const elem of elements) {
3453
+ const roleAttr = this.getAttribute(elem, "role");
3454
+ if (!roleAttr) continue;
3455
+ const roles = roleAttr.split(/\s+/);
3456
+ for (const role of DEPRECATED_ROLES) {
3457
+ if (roles.includes(role)) {
3458
+ pushMessage(context.messages, {
3459
+ id: MessageId.RSC_017,
3460
+ message: `The "${role}" role is deprecated and should not be used`,
3461
+ location: { path, line: elem.line }
3462
+ });
3463
+ }
3464
+ }
3465
+ }
3466
+ } catch {
3467
+ }
3468
+ }
3469
+ validateEpubSwitch(context, path, root) {
3470
+ const EPUB_NS = { epub: "http://www.idpf.org/2007/ops" };
3471
+ try {
3472
+ const switches = root.find(".//epub:switch", EPUB_NS);
3473
+ for (const sw of switches) {
3474
+ pushMessage(context.messages, {
3475
+ id: MessageId.RSC_017,
3476
+ message: 'The "epub:switch" element is deprecated',
3477
+ location: { path, line: sw.line }
3478
+ });
3479
+ const swElem = sw;
3480
+ const cases = [];
3481
+ const defaults = [];
3482
+ let defaultBeforeCase = false;
3483
+ try {
3484
+ const childCases = swElem.find("./epub:case", EPUB_NS);
3485
+ const childDefaults = swElem.find("./epub:default", EPUB_NS);
3486
+ cases.push(...childCases);
3487
+ defaults.push(...childDefaults);
3488
+ const firstDefault = childDefaults[0];
3489
+ const lastCase = childCases[childCases.length - 1];
3490
+ if (firstDefault && lastCase && firstDefault.line < lastCase.line) {
3491
+ defaultBeforeCase = true;
3492
+ }
3493
+ } catch {
3494
+ }
3495
+ if (cases.length === 0) {
3496
+ pushMessage(context.messages, {
3497
+ id: MessageId.RSC_005,
3498
+ message: 'The "epub:switch" element must contain at least one "epub:case" child element',
3499
+ location: { path, line: sw.line }
3500
+ });
3501
+ }
3502
+ if (defaults.length === 0) {
3503
+ pushMessage(context.messages, {
3504
+ id: MessageId.RSC_005,
3505
+ message: 'The "epub:switch" element must contain an "epub:default" child element',
3506
+ location: { path, line: sw.line }
3507
+ });
3508
+ }
3509
+ const secondDefault = defaults[1];
3510
+ if (secondDefault) {
3511
+ pushMessage(context.messages, {
3512
+ id: MessageId.RSC_005,
3513
+ message: 'The "epub:switch" element must not contain more than one "epub:default" child element',
3514
+ location: { path, line: secondDefault.line }
3515
+ });
3516
+ }
3517
+ const firstDefaultElem = defaults[0];
3518
+ if (defaultBeforeCase && firstDefaultElem) {
3519
+ pushMessage(context.messages, {
3520
+ id: MessageId.RSC_005,
3521
+ message: 'The "epub:default" element must appear after all "epub:case" elements',
3522
+ location: { path, line: firstDefaultElem.line }
3523
+ });
3524
+ }
3525
+ for (const c of cases) {
3526
+ const caseElem = c;
3527
+ const reqNs = caseElem.attr("required-namespace");
3528
+ if (!reqNs) {
3529
+ pushMessage(context.messages, {
3530
+ id: MessageId.RSC_005,
3531
+ message: 'The "epub:case" element must have a "required-namespace" attribute',
3532
+ location: { path, line: c.line }
3533
+ });
3534
+ }
3535
+ }
3536
+ try {
3537
+ const MATH_NS = { m: "http://www.w3.org/1998/Math/MathML" };
3538
+ const nestedMath = swElem.find(".//m:math//m:math", MATH_NS);
3539
+ for (const nested of nestedMath) {
3540
+ pushMessage(context.messages, {
3541
+ id: MessageId.RSC_005,
3542
+ message: 'The "math" element must not be nested inside another "math" element',
3543
+ location: { path, line: nested.line }
3544
+ });
3545
+ }
3546
+ } catch {
3547
+ }
3548
+ }
3549
+ } catch {
3550
+ }
3551
+ }
3552
+ validateEpubTrigger(context, path, root) {
3553
+ const EPUB_NS = { epub: "http://www.idpf.org/2007/ops" };
3554
+ try {
3555
+ const triggers = root.find(".//epub:trigger", EPUB_NS);
3556
+ if (triggers.length === 0) return;
3557
+ const allIds = /* @__PURE__ */ new Set();
3558
+ try {
3559
+ const idElements = root.find(".//*[@id]");
3560
+ for (const el of idElements) {
3561
+ const idAttr = this.getAttribute(el, "id");
3562
+ if (idAttr) allIds.add(idAttr);
3563
+ }
3564
+ } catch {
3565
+ }
3566
+ for (const trigger of triggers) {
3567
+ pushMessage(context.messages, {
3568
+ id: MessageId.RSC_017,
3569
+ message: 'The "epub:trigger" element is deprecated',
3570
+ location: { path, line: trigger.line }
3571
+ });
3572
+ const triggerElem = trigger;
3573
+ const ref = triggerElem.attr("ref");
3574
+ if (ref?.value && !allIds.has(ref.value)) {
3575
+ pushMessage(context.messages, {
3576
+ id: MessageId.RSC_005,
3577
+ message: `The "ref" attribute value "${ref.value}" does not reference a valid ID in the document`,
3578
+ location: { path, line: trigger.line }
3579
+ });
3580
+ }
3581
+ const observer = triggerElem.attr("observer", "ev") ?? triggerElem.attr("ev:observer");
3582
+ if (observer?.value && !allIds.has(observer.value)) {
3583
+ pushMessage(context.messages, {
3584
+ id: MessageId.RSC_005,
3585
+ message: `The "ev:observer" attribute value "${observer.value}" does not reference a valid ID in the document`,
3586
+ location: { path, line: trigger.line }
3587
+ });
3588
+ }
3589
+ }
3590
+ } catch {
3591
+ }
3592
+ }
3593
+ validateStyleAttributes(context, path, root) {
3594
+ try {
3595
+ const elements = root.find(".//*[@style]");
3596
+ for (const elem of elements) {
3597
+ const style = this.getAttribute(elem, "style");
3598
+ if (!style) continue;
3599
+ const wrappedCss = `* { ${style} }`;
3600
+ const cssValidator = new CSSValidator();
3601
+ cssValidator.validate(context, wrappedCss, path);
3602
+ }
3603
+ } catch {
3604
+ }
3605
+ }
3606
+ validateSvgEpubType(context, path, root) {
3607
+ const ALLOWED_ELEMENTS = /* @__PURE__ */ new Set([
3608
+ "svg",
3609
+ "a",
3610
+ "audio",
3611
+ "canvas",
3612
+ "circle",
3613
+ "ellipse",
3614
+ "g",
3615
+ "iframe",
3616
+ "image",
3617
+ "line",
3618
+ "path",
3619
+ "polygon",
3620
+ "polyline",
3621
+ "rect",
3622
+ "switch",
3623
+ "symbol",
3624
+ "text",
3625
+ "textPath",
3626
+ "tspan",
3627
+ "unknown",
3628
+ "use",
3629
+ "video"
3630
+ ]);
3631
+ const EPUB_NS = { epub: "http://www.idpf.org/2007/ops" };
3632
+ try {
3633
+ const elements = root.find(".//*[@epub:type]", EPUB_NS);
3634
+ for (const elem of elements) {
3635
+ const elemTyped = elem;
3636
+ const localName = elemTyped.name;
3637
+ if (!ALLOWED_ELEMENTS.has(localName)) {
3638
+ pushMessage(context.messages, {
3639
+ id: MessageId.RSC_005,
3640
+ message: `Attribute "epub:type" not allowed on SVG element "${localName}"`,
3641
+ location: { path, line: elem.line }
3642
+ });
3643
+ }
3644
+ }
3645
+ const rootEpubType = root.attr("type", "epub");
3646
+ if (rootEpubType && !ALLOWED_ELEMENTS.has(root.name)) {
3647
+ pushMessage(context.messages, {
3648
+ id: MessageId.RSC_005,
3649
+ message: `Attribute "epub:type" not allowed on SVG element "${root.name}"`,
3650
+ location: { path, line: root.line }
3651
+ });
3652
+ }
3653
+ } catch {
3654
+ }
3655
+ }
3656
+ checkUnknownEpubAttributes(context, path, root) {
3657
+ const KNOWN_EPUB_ATTRS = /* @__PURE__ */ new Set(["type"]);
3658
+ const checkElement = (elem) => {
3659
+ if (!("attrs" in elem)) return;
3660
+ for (const attr of elem.attrs) {
3661
+ if (attr.prefix === "epub" && !KNOWN_EPUB_ATTRS.has(attr.name)) {
3662
+ pushMessage(context.messages, {
3663
+ id: MessageId.RSC_005,
3664
+ message: `Attribute "epub:${attr.name}" not allowed`,
3665
+ location: { path, line: elem.line }
3666
+ });
3667
+ }
3668
+ }
3669
+ };
3670
+ checkElement(root);
3671
+ try {
3672
+ const allElements = root.find(".//*");
3673
+ for (const elem of allElements) {
3674
+ checkElement(elem);
3675
+ }
3676
+ } catch {
3677
+ }
3678
+ }
3679
+ checkTableBorder(context, path, root) {
3680
+ const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
3681
+ try {
3682
+ const tables = root.find(".//html:table[@border]", HTML_NS);
3683
+ for (const table of tables) {
3684
+ const border = this.getAttribute(table, "border");
3685
+ if (border !== null && border !== "" && border !== "1") {
3686
+ pushMessage(context.messages, {
3687
+ id: MessageId.RSC_005,
3688
+ message: `The value of the "border" attribute on the "table" element must be either "1" or the empty string`,
3689
+ location: { path, line: table.line }
3690
+ });
3691
+ }
3692
+ }
3693
+ } catch {
3694
+ }
3695
+ }
3696
+ checkTimeElement(context, path, root) {
3697
+ const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
3698
+ try {
3699
+ const nestedTimes = root.find(".//html:time//html:time", HTML_NS);
3700
+ for (const nested of nestedTimes) {
3701
+ pushMessage(context.messages, {
3702
+ id: MessageId.RSC_005,
3703
+ message: 'The element "time" must not appear as a descendant of the "time" element',
3704
+ location: { path, line: nested.line }
3705
+ });
3706
+ }
3707
+ } catch {
3708
+ }
3709
+ try {
3710
+ const times = root.find(".//html:time[@datetime]", HTML_NS);
3711
+ for (const time of times) {
3712
+ const datetime = this.getAttribute(time, "datetime");
3713
+ if (datetime !== null && !isValidDatetime(datetime)) {
3714
+ pushMessage(context.messages, {
3715
+ id: MessageId.RSC_005,
3716
+ message: `The "datetime" attribute value "${datetime}" is not a valid date, time, or duration`,
3717
+ location: { path, line: time.line }
3718
+ });
3719
+ }
3720
+ }
3721
+ } catch {
3722
+ }
3723
+ }
3724
+ checkMathMLAnnotations(context, path, root) {
3725
+ const MATH_NS = { math: "http://www.w3.org/1998/Math/MathML" };
3726
+ const CONTENT_MATHML_ENCODINGS = /* @__PURE__ */ new Set(["mathml-content", "application/mathml-content+xml"]);
3727
+ const CONTENT_MATHML_ELEMENTS = /* @__PURE__ */ new Set([
3728
+ "apply",
3729
+ "bind",
3730
+ "ci",
3731
+ "cn",
3732
+ "cs",
3733
+ "csymbol",
3734
+ "cbytes",
3735
+ "cerror",
3736
+ "share",
3737
+ "piecewise",
3738
+ "lambda",
3739
+ "set",
3740
+ "list",
3741
+ "vector",
3742
+ "matrix",
3743
+ "matrixrow",
3744
+ "interval"
3745
+ ]);
3746
+ const contentMathMLNames = [...CONTENT_MATHML_ELEMENTS];
3747
+ try {
3748
+ const annotations = root.find(".//math:annotation-xml", MATH_NS);
3749
+ for (const anno of annotations) {
3750
+ const el = anno;
3751
+ const encoding = this.getAttribute(el, "encoding");
3752
+ const name = this.getAttribute(el, "name");
3753
+ if (encoding) {
3754
+ const encodingLower = encoding.toLowerCase();
3755
+ if (CONTENT_MATHML_ENCODINGS.has(encodingLower)) {
3756
+ if (!name) {
3757
+ pushMessage(context.messages, {
3758
+ id: MessageId.RSC_005,
3759
+ message: 'The "annotation-xml" element with Content MathML encoding must have a "name" attribute with value "contentequiv"',
3760
+ location: { path, line: el.line }
3761
+ });
3762
+ } else if (name !== "contentequiv") {
3763
+ pushMessage(context.messages, {
3764
+ id: MessageId.RSC_005,
3765
+ message: `The "name" attribute on "annotation-xml" with Content MathML encoding must be "contentequiv", but found "${name}"`,
3766
+ location: { path, line: el.line }
3767
+ });
3768
+ }
3769
+ } else {
3770
+ for (const cElemName of contentMathMLNames) {
3771
+ try {
3772
+ const found = el.get(`./math:${cElemName}`, MATH_NS);
3773
+ if (found) {
3774
+ pushMessage(context.messages, {
3775
+ id: MessageId.RSC_005,
3776
+ message: `Content MathML element "${cElemName}" found in annotation-xml with encoding "${encoding}"`,
3777
+ location: { path, line: found.line }
3778
+ });
3779
+ break;
3780
+ }
3781
+ } catch {
3782
+ }
3783
+ }
3784
+ }
3785
+ if (encodingLower === "application/xml+xhtml") {
3786
+ pushMessage(context.messages, {
3787
+ id: MessageId.RSC_005,
3788
+ message: 'The encoding "application/xml+xhtml" is not valid; use "application/xhtml+xml" instead',
3789
+ location: { path, line: el.line }
3790
+ });
3791
+ }
3792
+ }
3793
+ }
3794
+ } catch {
3795
+ }
3796
+ for (const elemName of contentMathMLNames) {
3797
+ try {
3798
+ const found = root.get(`.//math:math/math:${elemName}`, MATH_NS);
3799
+ if (found) {
3800
+ pushMessage(context.messages, {
3801
+ id: MessageId.RSC_005,
3802
+ message: `Content MathML element "${elemName}" must not appear as a direct child of "math"; use "semantics" with "annotation-xml" instead`,
3803
+ location: { path, line: found.line }
3804
+ });
3805
+ break;
3806
+ }
3807
+ } catch {
3808
+ }
3809
+ }
3810
+ }
3811
+ checkReservedNamespace(context, path, content) {
3812
+ const nsPattern = /xmlns:(\w+)="([^"]+)"/g;
3813
+ const STANDARD_PREFIXES = /* @__PURE__ */ new Set([
3814
+ "xml",
3815
+ "xmlns",
3816
+ "xlink",
3817
+ "epub",
3818
+ "ops",
3819
+ "dc",
3820
+ "dcterms",
3821
+ "svg",
3822
+ "math",
3823
+ "ssml",
3824
+ "ev",
3825
+ "xsi"
3826
+ ]);
3827
+ const STANDARD_NAMESPACES = /* @__PURE__ */ new Set([
3828
+ "http://www.w3.org/XML/1998/namespace",
3829
+ "http://www.w3.org/2000/xmlns/",
3830
+ "http://www.w3.org/1999/xhtml",
3831
+ "http://www.w3.org/1999/xlink",
3832
+ "http://www.w3.org/2000/svg",
3833
+ "http://www.w3.org/1998/Math/MathML",
3834
+ "http://www.idpf.org/2007/ops",
3835
+ "http://purl.org/dc/elements/1.1/",
3836
+ "http://purl.org/dc/terms/",
3837
+ "http://www.w3.org/2001/10/synthesis",
3838
+ "http://www.w3.org/2001/xml-events",
3839
+ "http://www.w3.org/2001/XMLSchema-instance"
3840
+ ]);
3841
+ let match;
3842
+ while ((match = nsPattern.exec(content)) !== null) {
3843
+ const prefix = match[1] ?? "";
3844
+ const uri = match[2] ?? "";
3845
+ if (STANDARD_PREFIXES.has(prefix) || STANDARD_NAMESPACES.has(uri)) continue;
3846
+ try {
3847
+ const url = new URL(uri);
3848
+ const host = url.hostname.toLowerCase();
3849
+ for (const reserved of ["w3.org", "idpf.org"]) {
3850
+ if (host.includes(reserved)) {
3851
+ const line = content.substring(0, match.index).split("\n").length;
3852
+ pushMessage(context.messages, {
3853
+ id: MessageId.HTM_054,
3854
+ message: `Custom attribute namespace ("${uri}") must not include the string "${reserved}" in its domain`,
3855
+ location: { path, line }
3856
+ });
3857
+ }
3858
+ }
3859
+ } catch {
3860
+ }
3861
+ }
3862
+ }
3863
+ checkDataAttributes(context, path, root) {
3864
+ const elements = root.find(".//*");
3865
+ const XML_NCNAME_RE = /^[a-z_\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF][a-z0-9._\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF-]*$/;
3866
+ for (const elem of elements) {
3867
+ const el = elem;
3868
+ if (!("attrs" in el)) continue;
3869
+ const attrs = el.attrs;
3870
+ for (const attr of attrs) {
3871
+ if (!attr.name.startsWith("data-")) continue;
3872
+ const suffix = attr.name.substring(5);
3873
+ if (suffix.length === 0 || !XML_NCNAME_RE.test(suffix) || /[A-Z]/.test(attr.name)) {
3874
+ pushMessage(context.messages, {
3875
+ id: MessageId.HTM_061,
3876
+ message: `"${attr.name}" is not a valid custom data attribute (it must have at least one character after the hyphen, be XML-compatible, and not contain ASCII uppercase letters)`,
3877
+ location: { path, line: el.line }
3878
+ });
3879
+ }
3880
+ }
3881
+ }
3882
+ }
2643
3883
  checkAccessibility(context, path, root) {
2644
3884
  const links = root.find(".//html:a", { html: "http://www.w3.org/1999/xhtml" });
2645
3885
  for (const link of links) {
@@ -2752,24 +3992,32 @@ var ContentValidator = class {
2752
3992
  const epubTypeElements = root.find(".//*[@epub:type]", {
2753
3993
  epub: "http://www.idpf.org/2007/ops"
2754
3994
  });
2755
- const knownPrefixes = /* @__PURE__ */ new Set([
2756
- "",
2757
- "http://idpf.org/epub/structure/v1/",
2758
- "http://idpf.org/epub/vocab/structure/",
2759
- "http://www.idpf.org/2007/ops"
2760
- ]);
2761
3995
  for (const elem of epubTypeElements) {
2762
3996
  const elemTyped = elem;
2763
- const epubTypeAttr = elemTyped.attr("epub:type");
3997
+ const epubTypeAttr = elemTyped.attr("type", "epub");
2764
3998
  if (!epubTypeAttr?.value) continue;
2765
- const epubTypeValue = epubTypeAttr.value;
2766
- for (const part of epubTypeValue.split(/\s+/)) {
2767
- const prefix = part.includes(":") ? part.substring(0, part.indexOf(":")) : "";
2768
- if (!knownPrefixes.has(prefix) && !prefix.startsWith("http://") && !prefix.startsWith("https://")) {
3999
+ for (const part of epubTypeAttr.value.split(/\s+/)) {
4000
+ if (!part) continue;
4001
+ const hasPrefix = part.includes(":");
4002
+ const localName = hasPrefix ? part.substring(part.indexOf(":") + 1) : part;
4003
+ if (hasPrefix) continue;
4004
+ if (EPUB_SSV_DEPRECATED.has(localName)) {
4005
+ pushMessage(context.messages, {
4006
+ id: MessageId.OPF_086b,
4007
+ message: `epub:type value "${localName}" is deprecated`,
4008
+ location: { path, line: elem.line }
4009
+ });
4010
+ } else if (EPUB_SSV_DISALLOWED_ON_CONTENT.has(localName)) {
4011
+ pushMessage(context.messages, {
4012
+ id: MessageId.OPF_087,
4013
+ message: `epub:type value "${localName}" is not allowed on documents of type "application/xhtml+xml"`,
4014
+ location: { path, line: elem.line }
4015
+ });
4016
+ } else if (!EPUB_SSV_ALL.has(localName)) {
2769
4017
  pushMessage(context.messages, {
2770
4018
  id: MessageId.OPF_088,
2771
- message: `Unknown epub:type prefix "${prefix}": ${epubTypeValue}`,
2772
- location: { path }
4019
+ message: `Unrecognized epub:type value "${localName}"`,
4020
+ location: { path, line: elem.line }
2773
4021
  });
2774
4022
  }
2775
4023
  }
@@ -2881,14 +4129,39 @@ var ContentValidator = class {
2881
4129
  extractAndRegisterIDs(path, root, registry) {
2882
4130
  const elementsWithId = root.find(".//*[@id]");
2883
4131
  for (const elem of elementsWithId) {
2884
- const id = this.getAttribute(elem, "id");
4132
+ const xmlElem = elem;
4133
+ const id = this.getAttribute(xmlElem, "id");
2885
4134
  if (id) {
2886
4135
  registry.registerID(path, id);
4136
+ const localName = xmlElem.name.includes(":") ? xmlElem.name.split(":").pop() : xmlElem.name;
4137
+ if (localName === "symbol") {
4138
+ registry.registerSVGSymbolID(path, id);
4139
+ }
2887
4140
  }
2888
4141
  }
2889
4142
  }
2890
- extractAndRegisterHyperlinks(context, path, root, opfDir, refValidator) {
4143
+ extractAndRegisterHyperlinks(context, path, root, opfDir, refValidator, isNavDocument = false) {
2891
4144
  const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
4145
+ const navAnchorTypes = /* @__PURE__ */ new Map();
4146
+ if (isNavDocument) {
4147
+ const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
4148
+ const navElements = root.find(".//html:nav", HTML_NS);
4149
+ for (const nav of navElements) {
4150
+ const navElem = nav;
4151
+ const epubTypeAttr = "attrs" in navElem ? navElem.attrs.find(
4152
+ (attr) => attr.name === "type" && attr.prefix === "epub" && attr.namespaceUri === "http://www.idpf.org/2007/ops"
4153
+ ) : void 0;
4154
+ const types = epubTypeAttr ? epubTypeAttr.value.trim().split(/\s+/) : [];
4155
+ let refType = "hyperlink" /* HYPERLINK */;
4156
+ if (types.includes("toc")) refType = "nav-toc-link" /* NAV_TOC_LINK */;
4157
+ else if (types.includes("page-list")) refType = "nav-pagelist-link" /* NAV_PAGELIST_LINK */;
4158
+ const navAnchors = navElem.find(".//html:a[@href]", HTML_NS);
4159
+ for (const a of navAnchors) {
4160
+ const anchorHref = this.getAttribute(a, "href") ?? "";
4161
+ navAnchorTypes.set(`${String(a.line)}:${anchorHref}`, refType);
4162
+ }
4163
+ }
4164
+ }
2892
4165
  const links = root.find(".//html:a[@href]", { html: "http://www.w3.org/1999/xhtml" });
2893
4166
  for (const link of links) {
2894
4167
  const href = this.getAttribute(link, "href")?.trim() ?? null;
@@ -2902,10 +4175,8 @@ var ContentValidator = class {
2902
4175
  continue;
2903
4176
  }
2904
4177
  const line = link.line;
2905
- if (href.startsWith("http://") || href.startsWith("https://")) {
2906
- continue;
2907
- }
2908
- if (href.startsWith("mailto:") || href.startsWith("tel:")) {
4178
+ const refType = isNavDocument ? navAnchorTypes.get(`${String(line)}:${href}`) ?? "hyperlink" /* HYPERLINK */ : "hyperlink" /* HYPERLINK */;
4179
+ if (ABSOLUTE_URI_RE.test(href)) {
2909
4180
  continue;
2910
4181
  }
2911
4182
  if (href.includes("#epubcfi(")) {
@@ -2918,7 +4189,7 @@ var ContentValidator = class {
2918
4189
  url: href,
2919
4190
  targetResource: targetResource2,
2920
4191
  fragment,
2921
- type: "hyperlink" /* HYPERLINK */,
4192
+ type: refType,
2922
4193
  location: { path, line }
2923
4194
  });
2924
4195
  continue;
@@ -2930,7 +4201,7 @@ var ContentValidator = class {
2930
4201
  const ref = {
2931
4202
  url: href,
2932
4203
  targetResource,
2933
- type: "hyperlink" /* HYPERLINK */,
4204
+ type: refType,
2934
4205
  location: { path, line }
2935
4206
  };
2936
4207
  if (fragmentPart) {
@@ -2938,6 +4209,38 @@ var ContentValidator = class {
2938
4209
  }
2939
4210
  refValidator.addReference(ref);
2940
4211
  }
4212
+ const areaLinks = root.find(".//html:area[@href]", { html: "http://www.w3.org/1999/xhtml" });
4213
+ for (const area of areaLinks) {
4214
+ const href = this.getAttribute(area, "href")?.trim();
4215
+ if (!href) continue;
4216
+ const line = area.line;
4217
+ if (ABSOLUTE_URI_RE.test(href)) continue;
4218
+ if (href.includes("#epubcfi(")) continue;
4219
+ if (href.startsWith("#")) {
4220
+ refValidator.addReference({
4221
+ url: href,
4222
+ targetResource: path,
4223
+ fragment: href.slice(1),
4224
+ type: "hyperlink" /* HYPERLINK */,
4225
+ location: { path, line }
4226
+ });
4227
+ continue;
4228
+ }
4229
+ const resolvedAreaPath = this.resolveRelativePath(docDir, href, opfDir);
4230
+ const areaHashIndex = resolvedAreaPath.indexOf("#");
4231
+ const areaTarget = areaHashIndex >= 0 ? resolvedAreaPath.slice(0, areaHashIndex) : resolvedAreaPath;
4232
+ const areaFragment = areaHashIndex >= 0 ? resolvedAreaPath.slice(areaHashIndex + 1) : void 0;
4233
+ const areaRef = {
4234
+ url: href,
4235
+ targetResource: areaTarget,
4236
+ type: "hyperlink" /* HYPERLINK */,
4237
+ location: { path, line }
4238
+ };
4239
+ if (areaFragment) {
4240
+ areaRef.fragment = areaFragment;
4241
+ }
4242
+ refValidator.addReference(areaRef);
4243
+ }
2941
4244
  const svgLinks = root.find(".//svg:a", {
2942
4245
  svg: "http://www.w3.org/2000/svg",
2943
4246
  xlink: "http://www.w3.org/1999/xlink"
@@ -2985,9 +4288,9 @@ var ContentValidator = class {
2985
4288
  const href = this.getAttribute(linkElem, "href");
2986
4289
  const rel = this.getAttribute(linkElem, "rel");
2987
4290
  if (!href) continue;
4291
+ if (!rel?.toLowerCase().includes("stylesheet")) continue;
2988
4292
  const line = linkElem.line;
2989
- const isStylesheet = rel?.toLowerCase().includes("stylesheet");
2990
- const type = isStylesheet ? "stylesheet" /* STYLESHEET */ : "link" /* LINK */;
4293
+ const type = "stylesheet" /* STYLESHEET */;
2991
4294
  if (href.startsWith("http://") || href.startsWith("https://")) {
2992
4295
  refValidator.addReference({
2993
4296
  url: href,
@@ -3014,10 +4317,10 @@ var ContentValidator = class {
3014
4317
  extractCSSImports(cssPath, cssContent, opfDir, refValidator) {
3015
4318
  const cssDir = cssPath.includes("/") ? cssPath.substring(0, cssPath.lastIndexOf("/")) : "";
3016
4319
  const cleanedCSS = cssContent.replace(/\/\*[\s\S]*?\*\//g, "");
3017
- const importRegex = /@import\s+(?:url\s*\(\s*)?["']([^"']+)["']\s*\)?[^;]*;/gi;
4320
+ const importRegex = /@import\s+(?:url\s*\(\s*["']?([^"')]+?)["']?\s*\)|["']([^"']+)["'])[^;]*;/gi;
3018
4321
  let match;
3019
4322
  while ((match = importRegex.exec(cleanedCSS)) !== null) {
3020
- const importUrl = match[1];
4323
+ const importUrl = match[1] ?? match[2];
3021
4324
  if (!importUrl) continue;
3022
4325
  const beforeMatch = cleanedCSS.substring(0, match.index);
3023
4326
  const line = beforeMatch.split("\n").length;
@@ -3039,21 +4342,56 @@ var ContentValidator = class {
3039
4342
  });
3040
4343
  }
3041
4344
  }
3042
- extractAndRegisterImages(path, root, opfDir, refValidator) {
4345
+ extractAndRegisterImages(context, path, root, opfDir, refValidator, registry) {
3043
4346
  const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
3044
- const images = root.find(".//html:img[@src]", { html: "http://www.w3.org/1999/xhtml" });
4347
+ const ns = { html: "http://www.w3.org/1999/xhtml" };
4348
+ const pictureHasCMTSource = /* @__PURE__ */ new Set();
4349
+ if (registry) {
4350
+ const pictures = root.find(".//html:picture", ns);
4351
+ for (const pic of pictures) {
4352
+ const picElem = pic;
4353
+ const sources = picElem.find("html:source[@src]", ns);
4354
+ const sourcesWithSrcset = picElem.find("html:source[@srcset]", ns);
4355
+ for (const source of [...sources, ...sourcesWithSrcset]) {
4356
+ const srcAttr = this.getAttribute(source, "src");
4357
+ const srcsetAttr = this.getAttribute(source, "srcset");
4358
+ const sourceUrl = srcAttr ?? srcsetAttr?.split(",")[0]?.trim().split(/\s+/)[0];
4359
+ if (!sourceUrl || sourceUrl.startsWith("http://") || sourceUrl.startsWith("https://"))
4360
+ continue;
4361
+ const resolvedSource = this.resolveRelativePath(docDir, sourceUrl, opfDir);
4362
+ const resource = registry.getResource(resolvedSource);
4363
+ if (resource && isCoreMediaType(resource.mimeType)) {
4364
+ pictureHasCMTSource.add(pic.line);
4365
+ break;
4366
+ }
4367
+ }
4368
+ }
4369
+ }
4370
+ const images = root.find(".//html:img[@src]", ns);
3045
4371
  for (const img of images) {
3046
4372
  const imgElem = img;
3047
4373
  const src = this.getAttribute(imgElem, "src");
3048
4374
  if (!src) continue;
3049
4375
  const line = img.line;
4376
+ let hasIntrinsicFallback;
4377
+ if (pictureHasCMTSource.size > 0) {
4378
+ try {
4379
+ const pictureParent = imgElem.get("ancestor::html:picture", ns);
4380
+ if (pictureParent && pictureHasCMTSource.has(pictureParent.line)) {
4381
+ hasIntrinsicFallback = true;
4382
+ }
4383
+ } catch {
4384
+ }
4385
+ }
3050
4386
  if (src.startsWith("http://") || src.startsWith("https://")) {
3051
- refValidator.addReference({
4387
+ const ref = {
3052
4388
  url: src,
3053
4389
  targetResource: src,
3054
4390
  type: "image" /* IMAGE */,
3055
4391
  location: { path, line }
3056
- });
4392
+ };
4393
+ if (hasIntrinsicFallback) ref.hasIntrinsicFallback = true;
4394
+ refValidator.addReference(ref);
3057
4395
  } else {
3058
4396
  const resolvedPath = this.resolveRelativePath(docDir, src, opfDir);
3059
4397
  const hashIndex = resolvedPath.indexOf("#");
@@ -3065,6 +4403,7 @@ var ContentValidator = class {
3065
4403
  type: "image" /* IMAGE */,
3066
4404
  location: { path, line }
3067
4405
  };
4406
+ if (hasIntrinsicFallback) ref.hasIntrinsicFallback = true;
3068
4407
  if (fragment) {
3069
4408
  ref.fragment = fragment;
3070
4409
  }
@@ -3117,6 +4456,7 @@ var ContentValidator = class {
3117
4456
  }
3118
4457
  refValidator.addReference(svgImgRef);
3119
4458
  }
4459
+ this.extractSVGUseReferences(context, path, root, docDir, opfDir, refValidator);
3120
4460
  const videos = root.find(".//html:video[@poster]", { html: "http://www.w3.org/1999/xhtml" });
3121
4461
  for (const video of videos) {
3122
4462
  const poster = this.getAttribute(video, "poster");
@@ -3213,110 +4553,88 @@ var ContentValidator = class {
3213
4553
  }
3214
4554
  if (cite.startsWith("#")) {
3215
4555
  const targetResource2 = path;
3216
- const fragment2 = cite.slice(1);
3217
- refValidator.addReference({
3218
- url: cite,
3219
- targetResource: targetResource2,
3220
- fragment: fragment2,
3221
- type: "hyperlink" /* HYPERLINK */,
3222
- location: { path, line }
3223
- });
3224
- continue;
3225
- }
3226
- const resolvedPath = this.resolveRelativePath(docDir, cite, opfDir);
3227
- const hashIndex = resolvedPath.indexOf("#");
3228
- const targetResource = hashIndex >= 0 ? resolvedPath.slice(0, hashIndex) : resolvedPath;
3229
- const fragment = hashIndex >= 0 ? resolvedPath.slice(hashIndex + 1) : void 0;
3230
- const ref = {
3231
- url: cite,
3232
- targetResource,
3233
- type: "hyperlink" /* HYPERLINK */,
3234
- location: { path, line }
3235
- };
3236
- if (fragment) {
3237
- ref.fragment = fragment;
3238
- }
3239
- 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);
4556
+ const fragment2 = cite.slice(1);
3284
4557
  refValidator.addReference({
3285
- url: src,
3286
- targetResource: resolvedPath,
3287
- type: "video" /* VIDEO */,
3288
- location: line !== void 0 ? { path, line } : { path }
4558
+ url: cite,
4559
+ targetResource: targetResource2,
4560
+ fragment: fragment2,
4561
+ type: "cite" /* CITE */,
4562
+ location: { path, line }
3289
4563
  });
4564
+ continue;
4565
+ }
4566
+ const resolvedPath = this.resolveRelativePath(docDir, cite, opfDir);
4567
+ const hashIndex = resolvedPath.indexOf("#");
4568
+ const targetResource = hashIndex >= 0 ? resolvedPath.slice(0, hashIndex) : resolvedPath;
4569
+ const fragment = hashIndex >= 0 ? resolvedPath.slice(hashIndex + 1) : void 0;
4570
+ const ref = {
4571
+ url: cite,
4572
+ targetResource,
4573
+ type: "cite" /* CITE */,
4574
+ location: { path, line }
4575
+ };
4576
+ if (fragment) {
4577
+ ref.fragment = fragment;
3290
4578
  }
4579
+ refValidator.addReference(ref);
3291
4580
  }
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
- });
4581
+ }
4582
+ extractAndRegisterMediaElements(context, path, root, opfDir, refValidator, registry) {
4583
+ const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
4584
+ const ns = { html: "http://www.w3.org/1999/xhtml" };
4585
+ for (const tagName of ["audio", "video"]) {
4586
+ const isAudio = tagName === "audio";
4587
+ const refType = isAudio ? "audio" /* AUDIO */ : "video" /* VIDEO */;
4588
+ const elements = root.find(`.//html:${tagName}`, ns);
4589
+ for (const elem of elements) {
4590
+ const mediaElem = elem;
4591
+ const pendingRefs = [];
4592
+ const src = this.getAttribute(mediaElem, "src");
4593
+ if (src) {
4594
+ const line = elem.line;
4595
+ if (src.startsWith("http://") || src.startsWith("https://")) {
4596
+ pendingRefs.push({ url: src, targetResource: src, type: refType, line });
4597
+ } else {
4598
+ const resolvedPath = this.resolveRelativePath(docDir, src, opfDir);
4599
+ pendingRefs.push({ url: src, targetResource: resolvedPath, type: refType, line });
4600
+ }
4601
+ }
4602
+ const sources = mediaElem.find("html:source[@src]", ns);
4603
+ for (const source of sources) {
4604
+ const sourceElem = source;
4605
+ const sourceSrc = this.getAttribute(sourceElem, "src");
4606
+ if (!sourceSrc) continue;
4607
+ const line = source.line;
4608
+ if (sourceSrc.startsWith("http://") || sourceSrc.startsWith("https://")) {
4609
+ pendingRefs.push({ url: sourceSrc, targetResource: sourceSrc, type: refType, line });
4610
+ } else {
4611
+ const resolvedPath = this.resolveRelativePath(docDir, sourceSrc, opfDir);
4612
+ pendingRefs.push({ url: sourceSrc, targetResource: resolvedPath, type: refType, line });
4613
+ }
4614
+ if (registry) {
4615
+ this.checkMimeTypeMatch(context, path, docDir, opfDir, sourceElem, "src", registry);
4616
+ }
4617
+ }
4618
+ let hasIntrinsicFallback = false;
4619
+ if (registry && pendingRefs.length > 1) {
4620
+ hasIntrinsicFallback = pendingRefs.some((ref) => {
4621
+ const resource = registry.getResource(ref.targetResource);
4622
+ return resource && isCoreMediaType(resource.mimeType);
4623
+ });
4624
+ }
4625
+ for (const ref of pendingRefs) {
4626
+ const reference = {
4627
+ url: ref.url,
4628
+ targetResource: ref.targetResource,
4629
+ type: ref.type,
4630
+ location: ref.line !== void 0 ? { path, line: ref.line } : { path }
4631
+ };
4632
+ if (hasIntrinsicFallback) reference.hasIntrinsicFallback = true;
4633
+ refValidator.addReference(reference);
4634
+ }
3318
4635
  }
3319
4636
  }
4637
+ this.extractAndRegisterPictureElements(context, path, root, opfDir, refValidator, registry);
3320
4638
  const iframeElements = root.find(".//html:iframe[@src]", {
3321
4639
  html: "http://www.w3.org/1999/xhtml"
3322
4640
  });
@@ -3366,6 +4684,180 @@ var ContentValidator = class {
3366
4684
  }
3367
4685
  }
3368
4686
  }
4687
+ extractAndRegisterEmbeddedElements(context, path, root, opfDir, refValidator, registry) {
4688
+ const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
4689
+ const ns = { html: "http://www.w3.org/1999/xhtml" };
4690
+ const addRef = (src, type, line, hasIntrinsicFallback) => {
4691
+ const location = line !== void 0 ? { path, line } : { path };
4692
+ if (src.startsWith("http://") || src.startsWith("https://")) {
4693
+ const ref = {
4694
+ url: src,
4695
+ targetResource: src,
4696
+ type,
4697
+ location
4698
+ };
4699
+ if (hasIntrinsicFallback) ref.hasIntrinsicFallback = true;
4700
+ refValidator.addReference(ref);
4701
+ } else {
4702
+ const resolvedPath = this.resolveRelativePath(docDir, src, opfDir);
4703
+ const hashIndex = resolvedPath.indexOf("#");
4704
+ const targetResource = hashIndex >= 0 ? resolvedPath.slice(0, hashIndex) : resolvedPath;
4705
+ const ref = {
4706
+ url: src,
4707
+ targetResource,
4708
+ type,
4709
+ location
4710
+ };
4711
+ if (hashIndex >= 0) ref.fragment = resolvedPath.slice(hashIndex + 1);
4712
+ if (hasIntrinsicFallback) ref.hasIntrinsicFallback = true;
4713
+ refValidator.addReference(ref);
4714
+ }
4715
+ };
4716
+ for (const elem of root.find(".//html:embed[@src]", ns)) {
4717
+ const embedElem = elem;
4718
+ const src = this.getAttribute(embedElem, "src");
4719
+ if (src) addRef(src, "generic" /* GENERIC */, elem.line);
4720
+ if (registry) {
4721
+ this.checkMimeTypeMatch(context, path, docDir, opfDir, embedElem, "src", registry);
4722
+ }
4723
+ }
4724
+ for (const elem of root.find(".//html:input[@src]", ns)) {
4725
+ const type = this.getAttribute(elem, "type");
4726
+ if (type?.toLowerCase() === "image") {
4727
+ const src = this.getAttribute(elem, "src");
4728
+ if (src) addRef(src, "image" /* IMAGE */, elem.line);
4729
+ }
4730
+ }
4731
+ for (const elem of root.find(".//html:object[@data]", ns)) {
4732
+ const objElem = elem;
4733
+ const data = this.getAttribute(objElem, "data");
4734
+ if (!data) continue;
4735
+ const allChildren = objElem.find("html:*", ns);
4736
+ const hasFallbackContent = allChildren.some((child) => {
4737
+ const c = child;
4738
+ return c.name !== "param" && this.getAttribute(c, "hidden") === null;
4739
+ });
4740
+ addRef(data, "generic" /* GENERIC */, elem.line, hasFallbackContent || void 0);
4741
+ if (registry) {
4742
+ this.checkMimeTypeMatch(context, path, docDir, opfDir, objElem, "data", registry);
4743
+ }
4744
+ }
4745
+ }
4746
+ /**
4747
+ * Check if an element's type attribute matches the manifest MIME type (OPF-013)
4748
+ */
4749
+ checkMimeTypeMatch(context, path, docDir, opfDir, element, srcAttr, registry) {
4750
+ const typeAttr = this.getAttribute(element, "type");
4751
+ if (!typeAttr) return;
4752
+ const src = this.getAttribute(element, srcAttr);
4753
+ if (!src || src.startsWith("http://") || src.startsWith("https://")) return;
4754
+ const resolvedPath = this.resolveRelativePath(docDir, src, opfDir);
4755
+ const hashIndex = resolvedPath.indexOf("#");
4756
+ const targetResource = hashIndex >= 0 ? resolvedPath.slice(0, hashIndex) : resolvedPath;
4757
+ const resource = registry.getResource(targetResource);
4758
+ if (!resource) return;
4759
+ const declaredType = stripMimeParams(typeAttr);
4760
+ const manifestType = stripMimeParams(resource.mimeType);
4761
+ if (declaredType && declaredType !== manifestType) {
4762
+ pushMessage(context.messages, {
4763
+ id: MessageId.OPF_013,
4764
+ message: `Resource "${targetResource}" is declared with MIME type "${declaredType}" in content, but has MIME type "${manifestType}" in the package document`,
4765
+ location: { path, line: element.line }
4766
+ });
4767
+ }
4768
+ }
4769
+ /**
4770
+ * Extract and validate picture elements (MED-003, MED-007, OPF-013)
4771
+ */
4772
+ extractAndRegisterPictureElements(context, path, root, opfDir, refValidator, registry) {
4773
+ const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
4774
+ const ns = { html: "http://www.w3.org/1999/xhtml" };
4775
+ const BLESSED_IMAGE_TYPES = /* @__PURE__ */ new Set([
4776
+ "image/gif",
4777
+ "image/jpeg",
4778
+ "image/png",
4779
+ "image/svg+xml",
4780
+ "image/webp"
4781
+ ]);
4782
+ const pictures = root.find(".//html:picture", ns);
4783
+ for (const pic of pictures) {
4784
+ const picElem = pic;
4785
+ const imgs = picElem.find("html:img[@src]", ns);
4786
+ for (const img of imgs) {
4787
+ const imgElem = img;
4788
+ const src = this.getAttribute(imgElem, "src");
4789
+ if (!src || src.startsWith("http://") || src.startsWith("https://")) continue;
4790
+ if (registry) {
4791
+ const resolvedPath = this.resolveRelativePath(docDir, src, opfDir);
4792
+ const resource = registry.getResource(resolvedPath);
4793
+ if (resource && !BLESSED_IMAGE_TYPES.has(resource.mimeType)) {
4794
+ pushMessage(context.messages, {
4795
+ id: MessageId.MED_003,
4796
+ message: `Image in "picture" element must be a core image type, but found "${resource.mimeType}"`,
4797
+ location: { path, line: img.line }
4798
+ });
4799
+ }
4800
+ }
4801
+ const srcset = this.getAttribute(imgElem, "srcset");
4802
+ if (srcset && registry) {
4803
+ const entries = srcset.split(",");
4804
+ for (const entry of entries) {
4805
+ const url = entry.trim().split(/\s+/)[0];
4806
+ if (!url || url.startsWith("http://") || url.startsWith("https://")) continue;
4807
+ const resolvedPath = this.resolveRelativePath(docDir, url, opfDir);
4808
+ const resource = registry.getResource(resolvedPath);
4809
+ if (resource && !BLESSED_IMAGE_TYPES.has(resource.mimeType)) {
4810
+ pushMessage(context.messages, {
4811
+ id: MessageId.MED_003,
4812
+ message: `Image in "picture" element must be a core image type, but found "${resource.mimeType}"`,
4813
+ location: { path, line: img.line }
4814
+ });
4815
+ }
4816
+ }
4817
+ }
4818
+ }
4819
+ const sourcesWithSrc = picElem.find("html:source[@src]", ns);
4820
+ const sourcesWithSrcset = picElem.find("html:source[@srcset]", ns);
4821
+ const allSources = /* @__PURE__ */ new Set([...sourcesWithSrc, ...sourcesWithSrcset]);
4822
+ for (const source of allSources) {
4823
+ const sourceElem = source;
4824
+ const typeAttr = this.getAttribute(sourceElem, "type");
4825
+ const src = this.getAttribute(sourceElem, "src");
4826
+ const srcset = this.getAttribute(sourceElem, "srcset");
4827
+ const sourceUrl = src ?? srcset?.split(",")[0]?.trim().split(/\s+/)[0];
4828
+ if (!sourceUrl || sourceUrl.startsWith("http://") || sourceUrl.startsWith("https://"))
4829
+ continue;
4830
+ if (registry) {
4831
+ if (src) {
4832
+ this.checkMimeTypeMatch(context, path, docDir, opfDir, sourceElem, "src", registry);
4833
+ } else if (srcset && typeAttr) {
4834
+ const resolvedPath2 = this.resolveRelativePath(docDir, sourceUrl, opfDir);
4835
+ const resource2 = registry.getResource(resolvedPath2);
4836
+ if (resource2) {
4837
+ const declaredType = stripMimeParams(typeAttr);
4838
+ const manifestType = stripMimeParams(resource2.mimeType);
4839
+ if (declaredType && declaredType !== manifestType) {
4840
+ pushMessage(context.messages, {
4841
+ id: MessageId.OPF_013,
4842
+ message: `Resource "${resolvedPath2}" is declared with MIME type "${declaredType}" in content, but has MIME type "${manifestType}" in the package document`,
4843
+ location: { path, line: source.line }
4844
+ });
4845
+ }
4846
+ }
4847
+ }
4848
+ const resolvedPath = this.resolveRelativePath(docDir, sourceUrl, opfDir);
4849
+ const resource = registry.getResource(resolvedPath);
4850
+ if (resource && !BLESSED_IMAGE_TYPES.has(resource.mimeType) && !typeAttr) {
4851
+ pushMessage(context.messages, {
4852
+ id: MessageId.MED_007,
4853
+ message: `Source element in "picture" with foreign resource type "${resource.mimeType}" must declare a "type" attribute`,
4854
+ location: { path, line: source.line }
4855
+ });
4856
+ }
4857
+ }
4858
+ }
4859
+ }
4860
+ }
3369
4861
  parseSrcset(srcset, docDir, opfDir, path, line, refValidator) {
3370
4862
  const entries = srcset.split(",");
3371
4863
  for (const entry of entries) {
@@ -4489,6 +5981,9 @@ function parseSpine(spineXml, spineAttrs) {
4489
5981
  idref,
4490
5982
  linear: attrs.linear !== "no"
4491
5983
  };
5984
+ if (attrs.id) {
5985
+ itemref.id = attrs.id.trim();
5986
+ }
4492
5987
  if (attrs.properties) {
4493
5988
  itemref.properties = attrs.properties.split(/\s+/);
4494
5989
  }
@@ -4527,9 +6022,14 @@ function parseAttributes(attrsStr) {
4527
6022
  const name = match[1];
4528
6023
  const value = match[2];
4529
6024
  if (name !== void 0 && value !== void 0) {
6025
+ attrs[name] = value;
4530
6026
  const colonIdx = name.indexOf(":");
4531
- const localName = colonIdx >= 0 ? name.slice(colonIdx + 1) : name;
4532
- attrs[localName] = value;
6027
+ if (colonIdx >= 0) {
6028
+ const localName = name.slice(colonIdx + 1);
6029
+ if (!(localName in attrs)) {
6030
+ attrs[localName] = value;
6031
+ }
6032
+ }
4533
6033
  }
4534
6034
  }
4535
6035
  return attrs;
@@ -4574,70 +6074,324 @@ function parseCollections(xml) {
4574
6074
  return collections;
4575
6075
  }
4576
6076
 
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"
6077
+ // src/opf/validator.ts
6078
+ var VALID_VERSIONS = /* @__PURE__ */ new Set(["2.0", "3.0", "3.1", "3.2", "3.3"]);
6079
+ var DEPRECATED_MEDIA_TYPES = /* @__PURE__ */ new Set([
6080
+ "text/x-oeb1-document",
6081
+ "text/x-oeb1-css",
6082
+ "application/x-oeb1-package",
6083
+ "text/x-oeb1-html"
4613
6084
  ]);
4614
- var ITEM_PROPERTIES = /* @__PURE__ */ new Set([
4615
- "cover-image",
4616
- "mathml",
4617
- "nav",
4618
- "remote-resources",
4619
- "scripted",
4620
- "svg",
4621
- "switch"
6085
+ var VALID_RELATOR_CODES = /* @__PURE__ */ new Set([
6086
+ "abr",
6087
+ "acp",
6088
+ "act",
6089
+ "adi",
6090
+ "adp",
6091
+ "aft",
6092
+ "anl",
6093
+ "anm",
6094
+ "ann",
6095
+ "ant",
6096
+ "ape",
6097
+ "apl",
6098
+ "app",
6099
+ "aqt",
6100
+ "arc",
6101
+ "ard",
6102
+ "arr",
6103
+ "art",
6104
+ "asg",
6105
+ "asn",
6106
+ "ato",
6107
+ "att",
6108
+ "auc",
6109
+ "aud",
6110
+ "aui",
6111
+ "aus",
6112
+ "aut",
6113
+ "bdd",
6114
+ "bjd",
6115
+ "bkd",
6116
+ "bkp",
6117
+ "blw",
6118
+ "bnd",
6119
+ "bpd",
6120
+ "brd",
6121
+ "brl",
6122
+ "bsl",
6123
+ "cas",
6124
+ "ccp",
6125
+ "chr",
6126
+ "clb",
6127
+ "cli",
6128
+ "cll",
6129
+ "clr",
6130
+ "clt",
6131
+ "cmm",
6132
+ "cmp",
6133
+ "cmt",
6134
+ "cnd",
6135
+ "cng",
6136
+ "cns",
6137
+ "coe",
6138
+ "col",
6139
+ "com",
6140
+ "con",
6141
+ "cor",
6142
+ "cos",
6143
+ "cot",
6144
+ "cou",
6145
+ "cov",
6146
+ "cpc",
6147
+ "cpe",
6148
+ "cph",
6149
+ "cpl",
6150
+ "cpt",
6151
+ "cre",
6152
+ "crp",
6153
+ "crr",
6154
+ "crt",
6155
+ "csl",
6156
+ "csp",
6157
+ "cst",
6158
+ "ctb",
6159
+ "cte",
6160
+ "ctg",
6161
+ "ctr",
6162
+ "cts",
6163
+ "ctt",
6164
+ "cur",
6165
+ "cwt",
6166
+ "dbp",
6167
+ "dfd",
6168
+ "dfe",
6169
+ "dft",
6170
+ "dgc",
6171
+ "dgg",
6172
+ "dgs",
6173
+ "dis",
6174
+ "dln",
6175
+ "dnc",
6176
+ "dnr",
6177
+ "dpc",
6178
+ "dpt",
6179
+ "drm",
6180
+ "drt",
6181
+ "dsr",
6182
+ "dst",
6183
+ "dtc",
6184
+ "dte",
6185
+ "dtm",
6186
+ "dto",
6187
+ "dub",
6188
+ "edc",
6189
+ "edm",
6190
+ "edt",
6191
+ "egr",
6192
+ "elg",
6193
+ "elt",
6194
+ "eng",
6195
+ "enj",
6196
+ "etr",
6197
+ "evp",
6198
+ "exp",
6199
+ "fac",
6200
+ "fds",
6201
+ "fld",
6202
+ "flm",
6203
+ "fmd",
6204
+ "fmk",
6205
+ "fmo",
6206
+ "fmp",
6207
+ "fnd",
6208
+ "fpy",
6209
+ "frg",
6210
+ "gis",
6211
+ "grt",
6212
+ "his",
6213
+ "hnr",
6214
+ "hst",
6215
+ "ill",
6216
+ "ilu",
6217
+ "ins",
6218
+ "inv",
6219
+ "isb",
6220
+ "itr",
6221
+ "ive",
6222
+ "ivr",
6223
+ "jud",
6224
+ "jug",
6225
+ "lbr",
6226
+ "lbt",
6227
+ "ldr",
6228
+ "led",
6229
+ "lee",
6230
+ "lel",
6231
+ "len",
6232
+ "let",
6233
+ "lgd",
6234
+ "lie",
6235
+ "lil",
6236
+ "lit",
6237
+ "lsa",
6238
+ "lse",
6239
+ "lso",
6240
+ "ltg",
6241
+ "lyr",
6242
+ "mcp",
6243
+ "mdc",
6244
+ "med",
6245
+ "mfp",
6246
+ "mfr",
6247
+ "mod",
6248
+ "mon",
6249
+ "mrb",
6250
+ "mrk",
6251
+ "msd",
6252
+ "mte",
6253
+ "mtk",
6254
+ "mus",
6255
+ "nrt",
6256
+ "opn",
6257
+ "org",
6258
+ "orm",
6259
+ "osp",
6260
+ "oth",
6261
+ "own",
6262
+ "pad",
6263
+ "pan",
6264
+ "pat",
6265
+ "pbd",
6266
+ "pbl",
6267
+ "pdr",
6268
+ "pfr",
6269
+ "pht",
6270
+ "plt",
6271
+ "pma",
6272
+ "pmn",
6273
+ "pop",
6274
+ "ppm",
6275
+ "ppt",
6276
+ "pra",
6277
+ "prc",
6278
+ "prd",
6279
+ "pre",
6280
+ "prf",
6281
+ "prg",
6282
+ "prm",
6283
+ "prn",
6284
+ "pro",
6285
+ "prp",
6286
+ "prs",
6287
+ "prt",
6288
+ "prv",
6289
+ "pta",
6290
+ "pte",
6291
+ "ptf",
6292
+ "pth",
6293
+ "ptt",
6294
+ "pup",
6295
+ "rbr",
6296
+ "rcd",
6297
+ "rce",
6298
+ "rcp",
6299
+ "rdd",
6300
+ "red",
6301
+ "ren",
6302
+ "res",
6303
+ "rev",
6304
+ "rpc",
6305
+ "rps",
6306
+ "rpt",
6307
+ "rpy",
6308
+ "rse",
6309
+ "rsg",
6310
+ "rsp",
6311
+ "rsr",
6312
+ "rst",
6313
+ "rth",
6314
+ "rtm",
6315
+ "sad",
6316
+ "sce",
6317
+ "scl",
6318
+ "scr",
6319
+ "sds",
6320
+ "sec",
6321
+ "sgd",
6322
+ "sgn",
6323
+ "sht",
6324
+ "sll",
6325
+ "sng",
6326
+ "spk",
6327
+ "spn",
6328
+ "spy",
6329
+ "srv",
6330
+ "std",
6331
+ "stg",
6332
+ "stl",
6333
+ "stm",
6334
+ "stn",
6335
+ "str",
6336
+ "tcd",
6337
+ "tch",
6338
+ "ths",
6339
+ "tld",
6340
+ "tlp",
6341
+ "trc",
6342
+ "trl",
6343
+ "tyd",
6344
+ "tyg",
6345
+ "uvp",
6346
+ "vac",
6347
+ "vdg",
6348
+ "voc",
6349
+ "wac",
6350
+ "wal",
6351
+ "wam",
6352
+ "wat",
6353
+ "wdc",
6354
+ "wde",
6355
+ "win",
6356
+ "wit",
6357
+ "wpr",
6358
+ "wst"
4622
6359
  ]);
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"
6360
+ var DEPRECATED_LINK_REL = /* @__PURE__ */ new Set([
6361
+ "marc21xml-record",
6362
+ "mods-record",
6363
+ "onix-record",
6364
+ "xmp-record",
6365
+ "xml-signature"
6366
+ ]);
6367
+ var GRANDFATHERED_LANG_TAGS = /* @__PURE__ */ new Set([
6368
+ "en-GB-oed",
6369
+ "i-ami",
6370
+ "i-bnn",
6371
+ "i-default",
6372
+ "i-enochian",
6373
+ "i-hak",
6374
+ "i-klingon",
6375
+ "i-lux",
6376
+ "i-mingo",
6377
+ "i-navajo",
6378
+ "i-pwn",
6379
+ "i-tao",
6380
+ "i-tay",
6381
+ "i-tsu",
6382
+ "sgn-BE-FR",
6383
+ "sgn-BE-NL",
6384
+ "sgn-CH-DE",
6385
+ "art-lojban",
6386
+ "cel-gaulish",
6387
+ "no-bok",
6388
+ "no-nyn",
6389
+ "zh-guoyu",
6390
+ "zh-hakka",
6391
+ "zh-min",
6392
+ "zh-min-nan",
6393
+ "zh-xiang"
4638
6394
  ]);
4639
-
4640
- // src/opf/validator.ts
4641
6395
  var OPFValidator = class {
4642
6396
  packageDoc = null;
4643
6397
  manifestById = /* @__PURE__ */ new Map();
@@ -4699,13 +6453,7 @@ var OPFValidator = class {
4699
6453
  if (this.packageDoc.xmlLangs) {
4700
6454
  for (const lang of this.packageDoc.xmlLangs) {
4701
6455
  if (lang === "") continue;
4702
- if (lang !== lang.trim()) {
4703
- pushMessage(context.messages, {
4704
- id: MessageId.OPF_092,
4705
- message: `Language tag "${lang}" is not well-formed`,
4706
- location: { path: opfPath }
4707
- });
4708
- } else if (!isValidLanguageTag(lang)) {
6456
+ if (lang !== lang.trim() || !isValidLanguageTag(lang)) {
4709
6457
  pushMessage(context.messages, {
4710
6458
  id: MessageId.OPF_092,
4711
6459
  message: `Language tag "${lang}" is not well-formed`,
@@ -4731,11 +6479,10 @@ var OPFValidator = class {
4731
6479
  */
4732
6480
  validatePackageAttributes(context, opfPath) {
4733
6481
  if (!this.packageDoc) return;
4734
- const validVersions = /* @__PURE__ */ new Set(["2.0", "3.0", "3.1", "3.2", "3.3"]);
4735
- if (!validVersions.has(this.packageDoc.version)) {
6482
+ if (!VALID_VERSIONS.has(this.packageDoc.version)) {
4736
6483
  pushMessage(context.messages, {
4737
6484
  id: MessageId.OPF_001,
4738
- message: `Invalid package version "${this.packageDoc.version}"; must be one of: ${Array.from(validVersions).join(", ")}`,
6485
+ message: `Invalid package version "${this.packageDoc.version}"; must be one of: ${Array.from(VALID_VERSIONS).join(", ")}`,
4739
6486
  location: { path: opfPath }
4740
6487
  });
4741
6488
  }
@@ -4835,111 +6582,13 @@ var OPFValidator = class {
4835
6582
  }
4836
6583
  }
4837
6584
  if (dc.name === "creator" && dc.attributes) {
4838
- const opfRole = dc.attributes["opf:role"];
4839
- if (opfRole?.startsWith("marc:")) {
4840
- const relatorCode = opfRole.substring(5);
4841
- const validRelatorCodes = /* @__PURE__ */ new Set([
4842
- "arr",
4843
- "aut",
4844
- "aut",
4845
- "ccp",
4846
- "com",
4847
- "ctb",
4848
- "csl",
4849
- "edt",
4850
- "ill",
4851
- "itr",
4852
- "pbl",
4853
- "pdr",
4854
- "prt",
4855
- "trl",
4856
- "cre",
4857
- "art",
4858
- "ctb",
4859
- "edt",
4860
- "pfr",
4861
- "red",
4862
- "rev",
4863
- "spn",
4864
- "dsx",
4865
- "pmc",
4866
- "dte",
4867
- "ove",
4868
- "trc",
4869
- "ldr",
4870
- "led",
4871
- "prg",
4872
- "rap",
4873
- "rce",
4874
- "rpc",
4875
- "rtr",
4876
- "sad",
4877
- "sgn",
4878
- "tce",
4879
- "aac",
4880
- "acq",
4881
- "ant",
4882
- "arr",
4883
- "art",
4884
- "ard",
4885
- "asg",
4886
- "aus",
4887
- "aft",
4888
- "bdd",
4889
- "bdd",
4890
- "clb",
4891
- "clc",
4892
- "drd",
4893
- "edt",
4894
- "edt",
4895
- "fmd",
4896
- "flm",
4897
- "fmo",
4898
- "fpy",
4899
- "hnr",
4900
- "ill",
4901
- "ilt",
4902
- "img",
4903
- "itr",
4904
- "lrg",
4905
- "lsa",
4906
- "led",
4907
- "lee",
4908
- "lel",
4909
- "lgd",
4910
- "lse",
4911
- "mfr",
4912
- "mod",
4913
- "mon",
4914
- "mus",
4915
- "nrt",
4916
- "ogt",
4917
- "org",
4918
- "oth",
4919
- "pnt",
4920
- "ppa",
4921
- "prv",
4922
- "pup",
4923
- "red",
4924
- "rev",
4925
- "rsg",
4926
- "srv",
4927
- "stn",
4928
- "stl",
4929
- "trc",
4930
- "typ",
4931
- "vdg",
4932
- "voc",
4933
- "wac",
4934
- "wdc"
4935
- ]);
4936
- if (!validRelatorCodes.has(relatorCode)) {
4937
- pushMessage(context.messages, {
4938
- id: MessageId.OPF_052,
4939
- message: `Unknown MARC relator code "${relatorCode}" in dc:creator`,
4940
- location: { path: opfPath }
4941
- });
4942
- }
6585
+ const role = dc.attributes["opf:role"];
6586
+ if (role && !VALID_RELATOR_CODES.has(role) && !role.startsWith("oth.")) {
6587
+ pushMessage(context.messages, {
6588
+ id: MessageId.OPF_052,
6589
+ message: `Invalid role value "${role}" in dc:creator`,
6590
+ location: { path: opfPath }
6591
+ });
4943
6592
  }
4944
6593
  }
4945
6594
  }
@@ -5009,6 +6658,9 @@ var OPFValidator = class {
5009
6658
  for (const item of this.packageDoc.manifest) {
5010
6659
  allIdSources.push({ id: item.id, normalized: item.id.trim() });
5011
6660
  }
6661
+ for (const itemref of this.packageDoc.spine) {
6662
+ if (itemref.id) allIdSources.push({ id: itemref.id, normalized: itemref.id.trim() });
6663
+ }
5012
6664
  for (const src of allIdSources) {
5013
6665
  if (seenGlobalIds.has(src.normalized)) {
5014
6666
  pushMessage(context.messages, {
@@ -5031,11 +6683,14 @@ var OPFValidator = class {
5031
6683
  continue;
5032
6684
  }
5033
6685
  if (!refines.startsWith("#")) {
5034
- pushMessage(context.messages, {
5035
- id: MessageId.RSC_017,
5036
- message: `@refines should instead refer to "${refines}" using a fragment identifier pointing to its manifest item`,
5037
- location: { path: opfPath }
5038
- });
6686
+ const isManifestHref = this.packageDoc.manifest.some((item) => item.href === refines);
6687
+ if (isManifestHref) {
6688
+ pushMessage(context.messages, {
6689
+ id: MessageId.RSC_017,
6690
+ message: `@refines should instead refer to "${refines}" using a fragment identifier pointing to its manifest item`,
6691
+ location: { path: opfPath }
6692
+ });
6693
+ }
5039
6694
  continue;
5040
6695
  }
5041
6696
  const targetId = refines.substring(1);
@@ -5050,35 +6705,293 @@ var OPFValidator = class {
5050
6705
  this.detectRefinesCycles(context, opfPath);
5051
6706
  }
5052
6707
  if (this.packageDoc.version !== "2.0") {
5053
- const modifiedMeta = this.packageDoc.metaElements.find(
6708
+ this.validateMetaPropertiesVocab(context, opfPath, dcElements);
6709
+ }
6710
+ if (this.packageDoc.version !== "2.0") {
6711
+ const modifiedMetas = this.packageDoc.metaElements.filter(
5054
6712
  (meta) => meta.property === "dcterms:modified"
5055
6713
  );
6714
+ const modifiedMeta = modifiedMetas[0];
6715
+ if (modifiedMetas.length > 1) {
6716
+ pushMessage(context.messages, {
6717
+ id: MessageId.RSC_005,
6718
+ message: "package dcterms:modified meta element must occur exactly once",
6719
+ location: { path: opfPath }
6720
+ });
6721
+ }
5056
6722
  if (!modifiedMeta) {
5057
6723
  pushMessage(context.messages, {
5058
6724
  id: MessageId.RSC_005,
5059
- message: "package dcterms:modified meta element must occur exactly once",
6725
+ message: "package dcterms:modified meta element must occur exactly once",
6726
+ location: { path: opfPath }
6727
+ });
6728
+ pushMessage(context.messages, {
6729
+ id: MessageId.OPF_054,
6730
+ message: "EPUB 3 metadata must include a dcterms:modified meta element",
6731
+ location: { path: opfPath }
6732
+ });
6733
+ } else if (modifiedMeta.value) {
6734
+ const strictModifiedPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
6735
+ if (!strictModifiedPattern.test(modifiedMeta.value.trim())) {
6736
+ pushMessage(context.messages, {
6737
+ id: MessageId.RSC_005,
6738
+ message: `dcterms:modified illegal syntax (expecting: "CCYY-MM-DDThh:mm:ssZ")`,
6739
+ location: { path: opfPath }
6740
+ });
6741
+ }
6742
+ if (!isValidW3CDateFormat(modifiedMeta.value)) {
6743
+ pushMessage(context.messages, {
6744
+ id: MessageId.OPF_054,
6745
+ message: `Invalid dcterms:modified date format "${modifiedMeta.value}"; must be W3C date format (ISO 8601)`,
6746
+ location: { path: opfPath }
6747
+ });
6748
+ }
6749
+ }
6750
+ }
6751
+ }
6752
+ /**
6753
+ * Validate EPUB 3 meta element vocabulary (D-vocabularies: meta-properties)
6754
+ * Ports package-30.sch Schematron patterns for authority, term, belongs-to-collection,
6755
+ * collection-type, display-seq, file-as, group-position, identifier-type, meta-auth,
6756
+ * role, source-of, and title-type.
6757
+ */
6758
+ validateMetaPropertiesVocab(context, opfPath, dcElements) {
6759
+ if (!this.packageDoc) return;
6760
+ const metaElements = this.packageDoc.metaElements;
6761
+ const metaIdToProp = /* @__PURE__ */ new Map();
6762
+ for (const meta of metaElements) {
6763
+ if (meta.id) metaIdToProp.set(meta.id.trim(), meta.property.trim());
6764
+ }
6765
+ for (const dc of dcElements) {
6766
+ if (dc.name !== "subject" || !dc.id) continue;
6767
+ const subjectId = dc.id.trim();
6768
+ const authorityCount = metaElements.filter(
6769
+ (m) => m.property.trim() === "authority" && m.refines?.trim().substring(1) === subjectId
6770
+ ).length;
6771
+ const termCount = metaElements.filter(
6772
+ (m) => m.property.trim() === "term" && m.refines?.trim().substring(1) === subjectId
6773
+ ).length;
6774
+ if (authorityCount > 1 || termCount > 1) {
6775
+ pushMessage(context.messages, {
6776
+ id: MessageId.RSC_005,
6777
+ message: "Only one pair of authority and term properties can be associated with a dc:subject",
6778
+ location: { path: opfPath }
6779
+ });
6780
+ } else if (authorityCount === 1 && termCount === 0) {
6781
+ pushMessage(context.messages, {
6782
+ id: MessageId.RSC_005,
6783
+ message: "A term property must be associated with a dc:subject when an authority is specified",
5060
6784
  location: { path: opfPath }
5061
6785
  });
6786
+ } else if (authorityCount === 0 && termCount === 1) {
5062
6787
  pushMessage(context.messages, {
5063
- id: MessageId.OPF_054,
5064
- message: "EPUB 3 metadata must include a dcterms:modified meta element",
6788
+ id: MessageId.RSC_005,
6789
+ message: "An authority property must be associated with a dc:subject when a term is specified",
5065
6790
  location: { path: opfPath }
5066
6791
  });
5067
- } else if (modifiedMeta.value) {
5068
- const strictModifiedPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
5069
- if (!strictModifiedPattern.test(modifiedMeta.value.trim())) {
5070
- pushMessage(context.messages, {
5071
- id: MessageId.RSC_005,
5072
- message: `dcterms:modified illegal syntax (expecting: "CCYY-MM-DDThh:mm:ssZ")`,
5073
- location: { path: opfPath }
5074
- });
6792
+ }
6793
+ }
6794
+ const seenPropertyRefines = /* @__PURE__ */ new Set();
6795
+ for (const meta of metaElements) {
6796
+ const prop = meta.property.trim();
6797
+ const refines = meta.refines?.trim();
6798
+ switch (prop) {
6799
+ case "authority": {
6800
+ const ok = dcElements.some(
6801
+ (dc) => dc.name === "subject" && dc.id && "#" + dc.id.trim() === refines
6802
+ );
6803
+ if (!ok) {
6804
+ pushMessage(context.messages, {
6805
+ id: MessageId.RSC_005,
6806
+ message: 'Property "authority" must refine a "subject" property.',
6807
+ location: { path: opfPath }
6808
+ });
6809
+ }
6810
+ break;
5075
6811
  }
5076
- if (!isValidW3CDateFormat(modifiedMeta.value)) {
6812
+ case "term": {
6813
+ const ok = dcElements.some(
6814
+ (dc) => dc.name === "subject" && dc.id && "#" + dc.id.trim() === refines
6815
+ );
6816
+ if (!ok) {
6817
+ pushMessage(context.messages, {
6818
+ id: MessageId.RSC_005,
6819
+ message: 'Property "term" must refine a "subject" property.',
6820
+ location: { path: opfPath }
6821
+ });
6822
+ }
6823
+ break;
6824
+ }
6825
+ case "belongs-to-collection": {
6826
+ if (refines) {
6827
+ const targetId = refines.substring(1);
6828
+ if (metaIdToProp.get(targetId) !== "belongs-to-collection") {
6829
+ pushMessage(context.messages, {
6830
+ id: MessageId.RSC_005,
6831
+ message: 'Property "belongs-to-collection" can only refine other "belongs-to-collection" properties.',
6832
+ location: { path: opfPath }
6833
+ });
6834
+ }
6835
+ }
6836
+ break;
6837
+ }
6838
+ case "collection-type": {
6839
+ if (!refines) {
6840
+ pushMessage(context.messages, {
6841
+ id: MessageId.RSC_005,
6842
+ message: 'Property "collection-type" must refine a "belongs-to-collection" property.',
6843
+ location: { path: opfPath }
6844
+ });
6845
+ } else {
6846
+ const targetId = refines.substring(1);
6847
+ if (metaIdToProp.get(targetId) !== "belongs-to-collection") {
6848
+ pushMessage(context.messages, {
6849
+ id: MessageId.RSC_005,
6850
+ message: 'Property "collection-type" must refine a "belongs-to-collection" property.',
6851
+ location: { path: opfPath }
6852
+ });
6853
+ }
6854
+ }
6855
+ const ctKey = `${prop}:${refines ?? ""}`;
6856
+ if (seenPropertyRefines.has(ctKey)) {
6857
+ pushMessage(context.messages, {
6858
+ id: MessageId.RSC_005,
6859
+ message: '"collection-type" cannot be declared more than once to refine the same "belongs-to-collection" expression.',
6860
+ location: { path: opfPath }
6861
+ });
6862
+ }
6863
+ seenPropertyRefines.add(ctKey);
6864
+ break;
6865
+ }
6866
+ case "display-seq": {
6867
+ const key = `${prop}:${refines ?? ""}`;
6868
+ if (seenPropertyRefines.has(key)) {
6869
+ pushMessage(context.messages, {
6870
+ id: MessageId.RSC_005,
6871
+ message: '"display-seq" cannot be declared more than once to refine the same expression.',
6872
+ location: { path: opfPath }
6873
+ });
6874
+ }
6875
+ seenPropertyRefines.add(key);
6876
+ break;
6877
+ }
6878
+ case "file-as": {
6879
+ const key = `${prop}:${refines ?? ""}`;
6880
+ if (seenPropertyRefines.has(key)) {
6881
+ pushMessage(context.messages, {
6882
+ id: MessageId.RSC_005,
6883
+ message: '"file-as" cannot be declared more than once to refine the same expression.',
6884
+ location: { path: opfPath }
6885
+ });
6886
+ }
6887
+ seenPropertyRefines.add(key);
6888
+ break;
6889
+ }
6890
+ case "group-position": {
6891
+ const key = `${prop}:${refines ?? ""}`;
6892
+ if (seenPropertyRefines.has(key)) {
6893
+ pushMessage(context.messages, {
6894
+ id: MessageId.RSC_005,
6895
+ message: '"group-position" cannot be declared more than once to refine the same expression.',
6896
+ location: { path: opfPath }
6897
+ });
6898
+ }
6899
+ seenPropertyRefines.add(key);
6900
+ break;
6901
+ }
6902
+ case "identifier-type": {
6903
+ const ok = dcElements.some(
6904
+ (dc) => (dc.name === "identifier" || dc.name === "source") && dc.id && "#" + dc.id.trim() === refines
6905
+ );
6906
+ if (!ok) {
6907
+ pushMessage(context.messages, {
6908
+ id: MessageId.RSC_005,
6909
+ message: 'Property "identifier-type" must refine an "identifier" or "source" property.',
6910
+ location: { path: opfPath }
6911
+ });
6912
+ }
6913
+ const itKey = `${prop}:${refines ?? ""}`;
6914
+ if (seenPropertyRefines.has(itKey)) {
6915
+ pushMessage(context.messages, {
6916
+ id: MessageId.RSC_005,
6917
+ message: '"identifier-type" cannot be declared more than once to refine the same expression.',
6918
+ location: { path: opfPath }
6919
+ });
6920
+ }
6921
+ seenPropertyRefines.add(itKey);
6922
+ break;
6923
+ }
6924
+ case "meta-auth": {
5077
6925
  pushMessage(context.messages, {
5078
- id: MessageId.OPF_054,
5079
- message: `Invalid dcterms:modified date format "${modifiedMeta.value}"; must be W3C date format (ISO 8601)`,
6926
+ id: MessageId.RSC_017,
6927
+ message: "Use of the meta-auth property is deprecated",
5080
6928
  location: { path: opfPath }
5081
6929
  });
6930
+ break;
6931
+ }
6932
+ case "role": {
6933
+ const ok = dcElements.some(
6934
+ (dc) => (dc.name === "creator" || dc.name === "contributor" || dc.name === "publisher") && dc.id && "#" + dc.id.trim() === refines
6935
+ );
6936
+ if (!ok) {
6937
+ pushMessage(context.messages, {
6938
+ id: MessageId.RSC_005,
6939
+ message: 'Property "role" must refine a "creator", "contributor", or "publisher" property.',
6940
+ location: { path: opfPath }
6941
+ });
6942
+ }
6943
+ break;
6944
+ }
6945
+ case "source-of": {
6946
+ if (meta.value.trim() !== "pagination") {
6947
+ pushMessage(context.messages, {
6948
+ id: MessageId.RSC_005,
6949
+ message: 'The "source-of" property must have the value "pagination"',
6950
+ location: { path: opfPath }
6951
+ });
6952
+ }
6953
+ const hasSourceRefines = dcElements.some(
6954
+ (dc) => dc.name === "source" && dc.id && refines?.substring(1) === dc.id.trim()
6955
+ );
6956
+ if (!hasSourceRefines) {
6957
+ pushMessage(context.messages, {
6958
+ id: MessageId.RSC_005,
6959
+ message: 'The "source-of" property must refine a "source" property.',
6960
+ location: { path: opfPath }
6961
+ });
6962
+ }
6963
+ const soKey = `${prop}:${refines ?? ""}`;
6964
+ if (seenPropertyRefines.has(soKey)) {
6965
+ pushMessage(context.messages, {
6966
+ id: MessageId.RSC_005,
6967
+ message: '"source-of" cannot be declared more than once to refine the same "source" expression.',
6968
+ location: { path: opfPath }
6969
+ });
6970
+ }
6971
+ seenPropertyRefines.add(soKey);
6972
+ break;
6973
+ }
6974
+ case "title-type": {
6975
+ const ok = dcElements.some(
6976
+ (dc) => dc.name === "title" && dc.id && "#" + dc.id.trim() === refines
6977
+ );
6978
+ if (!ok) {
6979
+ pushMessage(context.messages, {
6980
+ id: MessageId.RSC_005,
6981
+ message: 'Property "title-type" must refine a "title" property.',
6982
+ location: { path: opfPath }
6983
+ });
6984
+ }
6985
+ const ttKey = `${prop}:${refines ?? ""}`;
6986
+ if (seenPropertyRefines.has(ttKey)) {
6987
+ pushMessage(context.messages, {
6988
+ id: MessageId.RSC_005,
6989
+ message: '"title-type" cannot be declared more than once to refine the same "title" expression.',
6990
+ location: { path: opfPath }
6991
+ });
6992
+ }
6993
+ seenPropertyRefines.add(ttKey);
6994
+ break;
5082
6995
  }
5083
6996
  }
5084
6997
  }
@@ -5088,7 +7001,6 @@ var OPFValidator = class {
5088
7001
  */
5089
7002
  validateLinkElements(context, opfPath) {
5090
7003
  if (!this.packageDoc) return;
5091
- const opfDir = opfPath.includes("/") ? opfPath.substring(0, opfPath.lastIndexOf("/")) : "";
5092
7004
  for (const link of this.packageDoc.linkElements) {
5093
7005
  if (link.hreflang !== void 0 && link.hreflang !== "") {
5094
7006
  const lang = link.hreflang;
@@ -5117,6 +7029,40 @@ var OPFValidator = class {
5117
7029
  }
5118
7030
  }
5119
7031
  }
7032
+ const relKeywords = link.rel ? link.rel.trim().split(/\s+/).filter(Boolean) : [];
7033
+ const hasRecord = relKeywords.includes("record");
7034
+ const hasVoicing = relKeywords.includes("voicing");
7035
+ const hasAlternate = relKeywords.includes("alternate");
7036
+ if (hasAlternate && relKeywords.length > 1) {
7037
+ pushMessage(context.messages, {
7038
+ id: MessageId.OPF_089,
7039
+ message: `The "alternate" keyword must not be combined with other keywords in the "rel" attribute`,
7040
+ location: { path: opfPath }
7041
+ });
7042
+ }
7043
+ for (const kw of relKeywords) {
7044
+ if (DEPRECATED_LINK_REL.has(kw)) {
7045
+ pushMessage(context.messages, {
7046
+ id: MessageId.OPF_086,
7047
+ message: `The rel keyword "${kw}" is deprecated`,
7048
+ location: { path: opfPath }
7049
+ });
7050
+ }
7051
+ }
7052
+ if (hasRecord && link.refines) {
7053
+ pushMessage(context.messages, {
7054
+ id: MessageId.RSC_005,
7055
+ message: '"record" links only applies to the Publication (must not have a "refines" attribute).',
7056
+ location: { path: opfPath }
7057
+ });
7058
+ }
7059
+ if (hasVoicing && !link.refines) {
7060
+ pushMessage(context.messages, {
7061
+ id: MessageId.RSC_005,
7062
+ message: '"voicing" links must have a "refines" attribute.',
7063
+ location: { path: opfPath }
7064
+ });
7065
+ }
5120
7066
  const href = link.href;
5121
7067
  const decodedHref = tryDecodeUriComponent(href);
5122
7068
  const basePath = href.includes("#") ? href.substring(0, href.indexOf("#")) : href;
@@ -5130,6 +7076,20 @@ var OPFValidator = class {
5130
7076
  continue;
5131
7077
  }
5132
7078
  const isRemote = /^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(href);
7079
+ if (hasVoicing && link.mediaType && !link.mediaType.startsWith("audio/")) {
7080
+ pushMessage(context.messages, {
7081
+ id: MessageId.OPF_095,
7082
+ message: `The "voicing" link media type must be an audio type, but found "${link.mediaType}"`,
7083
+ location: { path: opfPath }
7084
+ });
7085
+ }
7086
+ if (isRemote && !link.mediaType && (hasRecord || hasVoicing)) {
7087
+ pushMessage(context.messages, {
7088
+ id: MessageId.OPF_094,
7089
+ message: `The "media-type" attribute is required for "record" and "voicing" links`,
7090
+ location: { path: opfPath }
7091
+ });
7092
+ }
5133
7093
  if (isRemote) {
5134
7094
  continue;
5135
7095
  }
@@ -5140,8 +7100,8 @@ var OPFValidator = class {
5140
7100
  location: { path: opfPath }
5141
7101
  });
5142
7102
  }
5143
- const resolvedPath = resolvePath(opfDir, basePath);
5144
- const resolvedPathDecoded = basePathDecoded !== basePath ? resolvePath(opfDir, basePathDecoded) : resolvedPath;
7103
+ const resolvedPath = resolvePath(opfPath, basePath);
7104
+ const resolvedPathDecoded = basePathDecoded !== basePath ? resolvePath(opfPath, basePathDecoded) : resolvedPath;
5145
7105
  const fileExists = context.files.has(resolvedPath) || context.files.has(resolvedPathDecoded);
5146
7106
  const inManifest = this.manifestByHref.has(basePath) || this.manifestByHref.has(basePathDecoded);
5147
7107
  if (!fileExists && !inManifest) {
@@ -5218,13 +7178,7 @@ var OPFValidator = class {
5218
7178
  location: { path: opfPath }
5219
7179
  });
5220
7180
  }
5221
- const deprecatedTypes = /* @__PURE__ */ new Set([
5222
- "text/x-oeb1-document",
5223
- "text/x-oeb1-css",
5224
- "application/x-oeb1-package",
5225
- "text/x-oeb1-html"
5226
- ]);
5227
- if (deprecatedTypes.has(item.mediaType)) {
7181
+ if (DEPRECATED_MEDIA_TYPES.has(item.mediaType)) {
5228
7182
  pushMessage(context.messages, {
5229
7183
  id: MessageId.OPF_037,
5230
7184
  message: `Found deprecated media-type "${item.mediaType}"`,
@@ -5280,20 +7234,23 @@ var OPFValidator = class {
5280
7234
  });
5281
7235
  }
5282
7236
  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
- }
7237
+ 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/font-woff2" || item.mediaType === "application/vnd.ms-opentype";
5290
7238
  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
- });
7239
+ if (inSpine) {
7240
+ if (!isAllowedRemoteType) {
7241
+ pushMessage(context.messages, {
7242
+ id: MessageId.RSC_006,
7243
+ message: `Remote resource reference is not allowed in this context; resource "${item.href}" must be located in the EPUB container`,
7244
+ location: { path: opfPath }
7245
+ });
7246
+ }
7247
+ if (!item.properties?.includes("remote-resources")) {
7248
+ pushMessage(context.messages, {
7249
+ id: MessageId.RSC_006,
7250
+ message: `Manifest item "${item.id}" references remote resource but is missing "remote-resources" property`,
7251
+ location: { path: opfPath }
7252
+ });
7253
+ }
5297
7254
  }
5298
7255
  }
5299
7256
  }
@@ -5398,12 +7355,20 @@ var OPFValidator = class {
5398
7355
  });
5399
7356
  }
5400
7357
  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
- });
7358
+ if (!isSpineMediaType(item.mediaType)) {
7359
+ if (!item.fallback) {
7360
+ pushMessage(context.messages, {
7361
+ id: MessageId.OPF_043,
7362
+ message: `Spine item "${item.id}" has non-standard media type "${item.mediaType}" without fallback`,
7363
+ location: { path: opfPath }
7364
+ });
7365
+ } else if (!this.fallbackChainResolvesToContentDocument(item.id)) {
7366
+ pushMessage(context.messages, {
7367
+ id: MessageId.OPF_044,
7368
+ message: `Spine item "${item.id}" has non-standard media type "${item.mediaType}" and its fallback chain does not resolve to a content document`,
7369
+ location: { path: opfPath }
7370
+ });
7371
+ }
5407
7372
  }
5408
7373
  if (this.packageDoc.version !== "2.0" && itemref.properties) {
5409
7374
  for (const prop of itemref.properties) {
@@ -5601,14 +7566,29 @@ var OPFValidator = class {
5601
7566
  }
5602
7567
  }
5603
7568
  }
7569
+ fallbackChainResolvesToContentDocument(itemId) {
7570
+ const visited = /* @__PURE__ */ new Set();
7571
+ let currentId = itemId;
7572
+ while (currentId) {
7573
+ if (visited.has(currentId)) return false;
7574
+ visited.add(currentId);
7575
+ const item = this.manifestById.get(currentId);
7576
+ if (!item) return false;
7577
+ if (isSpineMediaType(item.mediaType)) return true;
7578
+ currentId = item.fallback;
7579
+ }
7580
+ return false;
7581
+ }
5604
7582
  };
5605
7583
  function isSpineMediaType(mediaType) {
5606
7584
  return mediaType === "application/xhtml+xml" || mediaType === "image/svg+xml" || // EPUB 2 also allows these in spine
5607
7585
  mediaType === "application/x-dtbook+xml";
5608
7586
  }
5609
7587
  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);
7588
+ 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})+)?$/;
7589
+ if (pattern.test(tag)) return true;
7590
+ if (/^x(-[a-zA-Z\d]{1,8})+$/.test(tag)) return true;
7591
+ return GRANDFATHERED_LANG_TAGS.has(tag);
5612
7592
  }
5613
7593
  function resolvePath(basePath, relativePath) {
5614
7594
  if (relativePath.startsWith("/")) {
@@ -5636,17 +7616,6 @@ function tryDecodeUriComponent(encoded) {
5636
7616
  return encoded;
5637
7617
  }
5638
7618
  }
5639
- function checkUrlLeaking(href) {
5640
- const TEST_BASE_A = "https://a.example.org/A/";
5641
- const TEST_BASE_B = "https://b.example.org/B/";
5642
- try {
5643
- const urlA = new URL(href, TEST_BASE_A).toString();
5644
- const urlB = new URL(href, TEST_BASE_B).toString();
5645
- return !urlA.startsWith(TEST_BASE_A) || !urlB.startsWith(TEST_BASE_B);
5646
- } catch {
5647
- return false;
5648
- }
5649
- }
5650
7619
  function isValidMimeType(mediaType) {
5651
7620
  const mimeTypePattern = /^[a-zA-Z][a-zA-Z0-9!#$&\-^_.]*\/[a-zA-Z0-9][a-zA-Z0-9!#$&\-^_.+]*(?:\s*;\s*[a-zA-Z0-9-]+=[^;]+)?$/;
5652
7621
  if (!mimeTypePattern.test(mediaType)) {
@@ -5714,9 +7683,11 @@ function isValidW3CDateFormat(dateStr) {
5714
7683
  var ResourceRegistry = class {
5715
7684
  resources;
5716
7685
  ids;
7686
+ svgSymbolIds;
5717
7687
  constructor() {
5718
7688
  this.resources = /* @__PURE__ */ new Map();
5719
7689
  this.ids = /* @__PURE__ */ new Map();
7690
+ this.svgSymbolIds = /* @__PURE__ */ new Map();
5720
7691
  }
5721
7692
  /**
5722
7693
  * Register a resource from manifest
@@ -5778,6 +7749,21 @@ var ResourceRegistry = class {
5778
7749
  }
5779
7750
  return -1;
5780
7751
  }
7752
+ /**
7753
+ * Register an ID as belonging to an SVG symbol element
7754
+ */
7755
+ registerSVGSymbolID(resourceURL, id) {
7756
+ if (!this.svgSymbolIds.has(resourceURL)) {
7757
+ this.svgSymbolIds.set(resourceURL, /* @__PURE__ */ new Set());
7758
+ }
7759
+ this.svgSymbolIds.get(resourceURL)?.add(id);
7760
+ }
7761
+ /**
7762
+ * Check if an ID in a resource belongs to an SVG symbol element
7763
+ */
7764
+ isSVGSymbolID(resourceURL, id) {
7765
+ return this.svgSymbolIds.get(resourceURL)?.has(id) ?? false;
7766
+ }
5781
7767
  /**
5782
7768
  * Get all resources
5783
7769
  */
@@ -5827,6 +7813,7 @@ var ReferenceValidator = class {
5827
7813
  for (const reference of this.references) {
5828
7814
  this.validateReference(context, reference);
5829
7815
  }
7816
+ this.checkRemoteResources(context);
5830
7817
  this.checkUndeclaredResources(context);
5831
7818
  this.checkReadingOrder(context);
5832
7819
  this.checkNonLinearReachability(context);
@@ -5836,14 +7823,6 @@ var ReferenceValidator = class {
5836
7823
  */
5837
7824
  validateReference(context, reference) {
5838
7825
  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
7826
  if (isDataURL(url)) {
5848
7827
  if (this.version.startsWith("3.")) {
5849
7828
  const forbiddenDataUrlTypes = [
@@ -5858,10 +7837,35 @@ var ReferenceValidator = class {
5858
7837
  message: "Data URLs are not allowed in this context",
5859
7838
  location: reference.location
5860
7839
  });
7840
+ } else {
7841
+ const fallbackCheckedTypes = [
7842
+ "image" /* IMAGE */,
7843
+ "audio" /* AUDIO */,
7844
+ "video" /* VIDEO */,
7845
+ "generic" /* GENERIC */
7846
+ ];
7847
+ if (fallbackCheckedTypes.includes(reference.type) && !reference.hasIntrinsicFallback) {
7848
+ const dataUrlMimeType = this.extractDataURLMimeType(url);
7849
+ if (dataUrlMimeType && !isCoreMediaType(dataUrlMimeType)) {
7850
+ pushMessage(context.messages, {
7851
+ id: MessageId.RSC_032,
7852
+ message: `Fallback must be provided for foreign resources, but found none for data URL of type "${dataUrlMimeType}"`,
7853
+ location: reference.location
7854
+ });
7855
+ }
7856
+ }
5861
7857
  }
5862
7858
  }
5863
7859
  return;
5864
7860
  }
7861
+ if (isMalformedURL(url)) {
7862
+ pushMessage(context.messages, {
7863
+ id: MessageId.RSC_020,
7864
+ message: `Malformed URL: ${url}`,
7865
+ location: reference.location
7866
+ });
7867
+ return;
7868
+ }
5865
7869
  if (isFileURL(url)) {
5866
7870
  pushMessage(context.messages, {
5867
7871
  id: MessageId.RSC_030,
@@ -5873,6 +7877,13 @@ var ReferenceValidator = class {
5873
7877
  const resourcePath = reference.targetResource || parseURL(url).resource;
5874
7878
  const fragment = reference.fragment ?? parseURL(url).fragment;
5875
7879
  const hasFragment = fragment !== void 0 && fragment !== "";
7880
+ if (!isRemoteURL(url) && url.includes("?")) {
7881
+ pushMessage(context.messages, {
7882
+ id: MessageId.RSC_033,
7883
+ message: `Relative URL strings must not have a query component: "${url}"`,
7884
+ location: reference.location
7885
+ });
7886
+ }
5876
7887
  if (!isRemoteURL(url)) {
5877
7888
  this.validateLocalReference(context, reference, resourcePath);
5878
7889
  } else {
@@ -5888,7 +7899,7 @@ var ReferenceValidator = class {
5888
7899
  validateLocalReference(context, reference, resourcePath) {
5889
7900
  if (hasAbsolutePath(resourcePath)) {
5890
7901
  pushMessage(context.messages, {
5891
- id: MessageId.RSC_027,
7902
+ id: MessageId.RSC_026,
5892
7903
  message: "Absolute paths are not allowed in EPUB",
5893
7904
  location: reference.location
5894
7905
  });
@@ -5900,10 +7911,16 @@ var ReferenceValidator = class {
5900
7911
  ];
5901
7912
  if (hasParentDirectoryReference(reference.url) && forbiddenParentDirTypes.includes(reference.type)) {
5902
7913
  pushMessage(context.messages, {
5903
- id: MessageId.RSC_028,
7914
+ id: MessageId.RSC_026,
5904
7915
  message: "Parent directory references (..) are not allowed",
5905
7916
  location: reference.location
5906
7917
  });
7918
+ } else if (!hasAbsolutePath(resourcePath) && !hasParentDirectoryReference(reference.url) && checkUrlLeaking(reference.url)) {
7919
+ pushMessage(context.messages, {
7920
+ id: MessageId.RSC_026,
7921
+ message: `URL "${reference.url}" leaks outside the container`,
7922
+ location: reference.location
7923
+ });
5907
7924
  }
5908
7925
  if (!this.registry.hasResource(resourcePath)) {
5909
7926
  const fileExistsInContainer = context.files.has(resourcePath);
@@ -5928,14 +7945,15 @@ var ReferenceValidator = class {
5928
7945
  return;
5929
7946
  }
5930
7947
  const resource = this.registry.getResource(resourcePath);
5931
- if (reference.type === "hyperlink" /* HYPERLINK */ && !resource?.inSpine) {
7948
+ const isHyperlinkLike = reference.type === "hyperlink" /* HYPERLINK */ || reference.type === "nav-toc-link" /* NAV_TOC_LINK */ || reference.type === "nav-pagelist-link" /* NAV_PAGELIST_LINK */;
7949
+ if (this.version.startsWith("3") && isHyperlinkLike && !resource?.inSpine) {
5932
7950
  pushMessage(context.messages, {
5933
7951
  id: MessageId.RSC_011,
5934
7952
  message: "Hyperlinks must reference spine items",
5935
7953
  location: reference.location
5936
7954
  });
5937
7955
  }
5938
- if (reference.type === "hyperlink" /* HYPERLINK */ || reference.type === "overlay-text-link" /* OVERLAY_TEXT_LINK */) {
7956
+ if (isHyperlinkLike || reference.type === "overlay-text-link" /* OVERLAY_TEXT_LINK */) {
5939
7957
  const targetMimeType = resource?.mimeType;
5940
7958
  if (targetMimeType && !this.isBlessedItemType(targetMimeType, context.version) && !this.isDeprecatedBlessedItemType(targetMimeType) && !resource.hasCoreMediaTypeFallback) {
5941
7959
  pushMessage(context.messages, {
@@ -5945,7 +7963,13 @@ var ReferenceValidator = class {
5945
7963
  });
5946
7964
  }
5947
7965
  }
5948
- if (resource && isPublicationResourceReference(reference.type) && !CORE_MEDIA_TYPES.has(resource.mimeType) && !resource.hasCoreMediaTypeFallback && !reference.hasIntrinsicFallback) {
7966
+ const fallbackCheckedTypes = [
7967
+ "image" /* IMAGE */,
7968
+ "audio" /* AUDIO */,
7969
+ "video" /* VIDEO */,
7970
+ "generic" /* GENERIC */
7971
+ ];
7972
+ if (resource && fallbackCheckedTypes.includes(reference.type) && !isCoreMediaType(resource.mimeType) && !resource.hasCoreMediaTypeFallback && !reference.hasIntrinsicFallback) {
5949
7973
  pushMessage(context.messages, {
5950
7974
  id: MessageId.RSC_032,
5951
7975
  message: `Fallback must be provided for foreign resources, but found none for resource "${resourcePath}" of type "${resource.mimeType}"`,
@@ -5958,20 +7982,24 @@ var ReferenceValidator = class {
5958
7982
  */
5959
7983
  validateRemoteReference(context, reference) {
5960
7984
  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
7985
  if (isPublicationResourceReference(reference.type)) {
5969
- const allowedRemoteTypes = /* @__PURE__ */ new Set([
7986
+ if (isHTTP(url) && !isHTTPS(url)) {
7987
+ pushMessage(context.messages, {
7988
+ id: MessageId.RSC_031,
7989
+ message: "Remote resources must use HTTPS",
7990
+ location: reference.location
7991
+ });
7992
+ }
7993
+ const allowedRemoteRefTypes = /* @__PURE__ */ new Set([
5970
7994
  "audio" /* AUDIO */,
5971
7995
  "video" /* VIDEO */,
5972
7996
  "font" /* FONT */
5973
7997
  ]);
5974
- if (!allowedRemoteTypes.has(reference.type)) {
7998
+ const targetResource = reference.targetResource || url;
7999
+ const resource = this.registry.getResource(targetResource);
8000
+ const isAllowedByRefType = allowedRemoteRefTypes.has(reference.type);
8001
+ const isAllowedByMimeType = resource && this.isRemoteResourceType(resource.mimeType);
8002
+ if (!isAllowedByRefType && !isAllowedByMimeType) {
5975
8003
  pushMessage(context.messages, {
5976
8004
  id: MessageId.RSC_006,
5977
8005
  message: "Remote resources are only allowed for audio, video, and fonts",
@@ -5979,8 +8007,7 @@ var ReferenceValidator = class {
5979
8007
  });
5980
8008
  return;
5981
8009
  }
5982
- const targetResource = reference.targetResource || url;
5983
- if (!this.registry.hasResource(targetResource)) {
8010
+ if (!resource) {
5984
8011
  pushMessage(context.messages, {
5985
8012
  id: MessageId.RSC_008,
5986
8013
  message: `Referenced resource "${targetResource}" is not declared in the OPF manifest`,
@@ -6013,9 +8040,9 @@ var ReferenceValidator = class {
6013
8040
  });
6014
8041
  return;
6015
8042
  }
6016
- if (resource?.mimeType === "image/svg+xml") {
8043
+ if (resource?.mimeType === "image/svg+xml" && reference.type === "hyperlink" /* HYPERLINK */) {
6017
8044
  const hasSVGView = fragment.includes("svgView(") || fragment.includes("viewBox(");
6018
- if (hasSVGView && reference.type === "hyperlink" /* HYPERLINK */) {
8045
+ if (hasSVGView) {
6019
8046
  pushMessage(context.messages, {
6020
8047
  id: MessageId.RSC_014,
6021
8048
  message: "SVG view fragments can only be referenced from SVG documents",
@@ -6023,11 +8050,51 @@ var ReferenceValidator = class {
6023
8050
  });
6024
8051
  }
6025
8052
  }
6026
- if (!this.registry.hasID(resourcePath, fragment)) {
8053
+ if (reference.type === "hyperlink" /* HYPERLINK */) {
8054
+ if (this.registry.isSVGSymbolID(resourcePath, fragment)) {
8055
+ pushMessage(context.messages, {
8056
+ id: MessageId.RSC_014,
8057
+ message: `Fragment identifier "${fragment}" defines an incompatible resource type (SVG symbol)`,
8058
+ location: reference.location
8059
+ });
8060
+ }
8061
+ }
8062
+ const parsedMimeTypes = ["application/xhtml+xml", "image/svg+xml"];
8063
+ if (resource && parsedMimeTypes.includes(resource.mimeType) && !isRemoteURL(resourcePath) && !fragment.includes("svgView(") && reference.type !== "svg-symbol" /* SVG_SYMBOL */) {
8064
+ if (!this.registry.hasID(resourcePath, fragment)) {
8065
+ pushMessage(context.messages, {
8066
+ id: MessageId.RSC_012,
8067
+ message: `Fragment identifier not found: #${fragment}`,
8068
+ location: reference.location
8069
+ });
8070
+ }
8071
+ }
8072
+ }
8073
+ /**
8074
+ * Check non-spine remote resources that have non-standard types.
8075
+ * Fires RSC-006 for remote items that aren't audio/video/font types
8076
+ * and aren't referenced as audio/video/font by content documents.
8077
+ * This mirrors Java's checkItemAfterResourceValidation behavior.
8078
+ */
8079
+ checkRemoteResources(context) {
8080
+ if (!this.version.startsWith("3")) return;
8081
+ const referencedAsAllowed = /* @__PURE__ */ new Set();
8082
+ for (const ref of this.references) {
8083
+ if (isRemoteURL(ref.url) || isRemoteURL(ref.targetResource)) {
8084
+ if (ref.type === "font" /* FONT */ || ref.type === "audio" /* AUDIO */ || ref.type === "video" /* VIDEO */) {
8085
+ referencedAsAllowed.add(ref.targetResource);
8086
+ }
8087
+ }
8088
+ }
8089
+ for (const resource of this.registry.getAllResources()) {
8090
+ if (!isRemoteURL(resource.url)) continue;
8091
+ if (resource.inSpine) continue;
8092
+ if (this.isRemoteResourceType(resource.mimeType)) continue;
8093
+ if (referencedAsAllowed.has(resource.url)) continue;
6027
8094
  pushMessage(context.messages, {
6028
- id: MessageId.RSC_012,
6029
- message: `Fragment identifier not found: #${fragment}`,
6030
- location: reference.location
8095
+ id: MessageId.RSC_006,
8096
+ message: `Remote resource reference is not allowed; resource "${resource.url}" must be located in the EPUB container`,
8097
+ location: { path: resource.url }
6031
8098
  });
6032
8099
  }
6033
8100
  }
@@ -6051,9 +8118,9 @@ var ReferenceValidator = class {
6051
8118
  for (const resource of this.registry.getAllResources()) {
6052
8119
  if (resource.inSpine) continue;
6053
8120
  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;
8121
+ if (resource.isNav) continue;
8122
+ if (resource.isNcx) continue;
8123
+ if (resource.isCoverImage) continue;
6057
8124
  pushMessage(context.messages, {
6058
8125
  id: MessageId.OPF_097,
6059
8126
  message: `Resource declared in manifest but not referenced: ${resource.url}`,
@@ -6114,7 +8181,7 @@ var ReferenceValidator = class {
6114
8181
  const opfDir = opfPath.includes("/") ? opfPath.substring(0, opfPath.lastIndexOf("/")) : "";
6115
8182
  const hyperlinkTargets = /* @__PURE__ */ new Set();
6116
8183
  for (const ref of this.references) {
6117
- if (ref.type === "hyperlink" /* HYPERLINK */) {
8184
+ if (ref.type === "hyperlink" /* HYPERLINK */ || ref.type === "nav-toc-link" /* NAV_TOC_LINK */ || ref.type === "nav-pagelist-link" /* NAV_PAGELIST_LINK */) {
6118
8185
  hyperlinkTargets.add(ref.targetResource);
6119
8186
  }
6120
8187
  }
@@ -6151,6 +8218,13 @@ var ReferenceValidator = class {
6151
8218
  isDeprecatedBlessedItemType(mimeType) {
6152
8219
  return mimeType === "text/x-oeb1-document" || mimeType === "text/html";
6153
8220
  }
8221
+ extractDataURLMimeType(url) {
8222
+ const match = /^data:([^;,]+)/.exec(url);
8223
+ return match?.[1]?.trim().toLowerCase() ?? "text/plain";
8224
+ }
8225
+ isRemoteResourceType(mimeType) {
8226
+ 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";
8227
+ }
6154
8228
  };
6155
8229
  var COMPRESSED_SCHEMAS = {
6156
8230
  "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 +8739,15 @@ var EpubCheck = class _EpubCheck {
6665
8739
  const manifestById = new Map(packageDoc.manifest.map((item) => [item.id, item]));
6666
8740
  for (const item of packageDoc.manifest) {
6667
8741
  const fullPath = resolveManifestHref(opfDir, item.href);
8742
+ const properties = item.properties ?? [];
6668
8743
  registry.registerResource({
6669
8744
  url: fullPath,
6670
8745
  mimeType: item.mediaType,
6671
8746
  inSpine: spineIdrefs.has(item.id),
6672
8747
  hasCoreMediaTypeFallback: this.hasCMTFallback(item.id, manifestById),
8748
+ isNav: properties.includes("nav"),
8749
+ isCoverImage: properties.includes("cover-image"),
8750
+ isNcx: item.mediaType === "application/x-dtbncx+xml",
6673
8751
  ids: /* @__PURE__ */ new Set()
6674
8752
  });
6675
8753
  }
@@ -6685,7 +8763,7 @@ var EpubCheck = class _EpubCheck {
6685
8763
  visited.add(currentId);
6686
8764
  const item = manifestById.get(currentId);
6687
8765
  if (!item) return false;
6688
- if (CORE_MEDIA_TYPES.has(item.mediaType)) return true;
8766
+ if (isCoreMediaType(item.mediaType)) return true;
6689
8767
  currentId = item.fallback;
6690
8768
  }
6691
8769
  return false;