@likecoin/epubcheck-ts 0.3.6 → 0.3.8

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",
@@ -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
@@ -1706,24 +1704,113 @@ var SPINE_PROPERTIES = /* @__PURE__ */ new Set([
1706
1704
  "rendition:layout-pre-paginated",
1707
1705
  "rendition:orientation-auto",
1708
1706
  "rendition:orientation-landscape",
1709
- "rendition:orientation-portrait"
1707
+ "rendition:orientation-portrait",
1708
+ "rendition:flow-auto",
1709
+ "rendition:flow-paginated",
1710
+ "rendition:flow-scrolled-continuous",
1711
+ "rendition:flow-scrolled-doc",
1712
+ "rendition:align-x-center"
1710
1713
  ]);
1711
1714
 
1712
1715
  // src/references/types.ts
1716
+ var PUBLICATION_RESOURCE_TYPES = /* @__PURE__ */ new Set([
1717
+ "generic" /* GENERIC */,
1718
+ "stylesheet" /* STYLESHEET */,
1719
+ "font" /* FONT */,
1720
+ "image" /* IMAGE */,
1721
+ "audio" /* AUDIO */,
1722
+ "video" /* VIDEO */,
1723
+ "track" /* TRACK */,
1724
+ "media-overlay" /* MEDIA_OVERLAY */,
1725
+ "svg-symbol" /* SVG_SYMBOL */,
1726
+ "svg-paint" /* SVG_PAINT */,
1727
+ "svg-clip-path" /* SVG_CLIP_PATH */
1728
+ ]);
1713
1729
  function isPublicationResourceReference(type) {
1714
- return [
1715
- "generic" /* GENERIC */,
1716
- "stylesheet" /* STYLESHEET */,
1717
- "font" /* FONT */,
1718
- "image" /* IMAGE */,
1719
- "audio" /* AUDIO */,
1720
- "video" /* VIDEO */,
1721
- "track" /* TRACK */,
1722
- "media-overlay" /* MEDIA_OVERLAY */,
1723
- "svg-symbol" /* SVG_SYMBOL */,
1724
- "svg-paint" /* SVG_PAINT */,
1725
- "svg-clip-path" /* SVG_CLIP_PATH */
1726
- ].includes(type);
1730
+ return PUBLICATION_RESOURCE_TYPES.has(type);
1731
+ }
1732
+
1733
+ // src/references/uri-schemes.ts
1734
+ var URI_SCHEMES = /* @__PURE__ */ new Set([
1735
+ "aaa",
1736
+ "aaas",
1737
+ "acap",
1738
+ "afs",
1739
+ "cap",
1740
+ "cid",
1741
+ "crid",
1742
+ "data",
1743
+ "dav",
1744
+ "dict",
1745
+ "dns",
1746
+ "dtn",
1747
+ "fax",
1748
+ "file",
1749
+ "ftp",
1750
+ "go",
1751
+ "gopher",
1752
+ "h323",
1753
+ "http",
1754
+ "https",
1755
+ "iax",
1756
+ "icap",
1757
+ "im",
1758
+ "imap",
1759
+ "info",
1760
+ "ipp",
1761
+ "irc",
1762
+ "iris",
1763
+ "iris.beep",
1764
+ "iris.lwz",
1765
+ "iris.xpc",
1766
+ "iris.xpcs",
1767
+ "javascript",
1768
+ "ldap",
1769
+ "mailto",
1770
+ "mailserver",
1771
+ "mid",
1772
+ "modem",
1773
+ "msrp",
1774
+ "msrps",
1775
+ "mtqp",
1776
+ "mupdate",
1777
+ "news",
1778
+ "nfs",
1779
+ "nntp",
1780
+ "opaquelocktoken",
1781
+ "pack",
1782
+ "pop",
1783
+ "pres",
1784
+ "prospero",
1785
+ "rtsp",
1786
+ "service",
1787
+ "shttp",
1788
+ "sip",
1789
+ "sips",
1790
+ "snews",
1791
+ "snmp",
1792
+ "soap.beep",
1793
+ "soap.beeps",
1794
+ "tag",
1795
+ "tel",
1796
+ "telnet",
1797
+ "tftp",
1798
+ "thismessage",
1799
+ "tip",
1800
+ "tn3270",
1801
+ "tv",
1802
+ "urn",
1803
+ "vemmi",
1804
+ "videotex",
1805
+ "wais",
1806
+ "xmlrpc.beep",
1807
+ "xmlrpc.beeps",
1808
+ "xmpp",
1809
+ "z39.50r",
1810
+ "z39.50s"
1811
+ ]);
1812
+ function isRegisteredScheme(scheme) {
1813
+ return URI_SCHEMES.has(scheme.toLowerCase());
1727
1814
  }
1728
1815
 
1729
1816
  // src/references/url.ts
@@ -1761,15 +1848,9 @@ function hasParentDirectoryReference(url) {
1761
1848
  return url.includes("..");
1762
1849
  }
1763
1850
  function isMalformedURL(url) {
1764
- if (!url) return true;
1765
- try {
1766
- const trimmed = url.trim();
1767
- if (!trimmed) return true;
1768
- if (/[\s<>]/.test(trimmed)) return true;
1769
- return false;
1770
- } catch {
1771
- return true;
1772
- }
1851
+ if (!url.trim()) return true;
1852
+ if (/[\s<>]/.test(url)) return true;
1853
+ return false;
1773
1854
  }
1774
1855
  function isHTTPS(url) {
1775
1856
  return url.startsWith("https://");
@@ -1804,7 +1885,205 @@ function resolveManifestHref(opfDir, href) {
1804
1885
  }
1805
1886
 
1806
1887
  // src/content/validator.ts
1807
- var DISCOURAGED_ELEMENTS = /* @__PURE__ */ new Set(["base", "embed"]);
1888
+ var DISCOURAGED_ELEMENTS = /* @__PURE__ */ new Set(["base", "embed", "rp"]);
1889
+ var ABSOLUTE_URI_RE = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
1890
+ var SPECIAL_URL_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "ftp", "ws", "wss"]);
1891
+ var CSS_CHARSET_RE = /^@charset\s+"([^"]+)"\s*;/;
1892
+ var EPUB_XMLNS_RE = /xmlns:epub\s*=\s*"([^"]*)"/;
1893
+ function validateAbsoluteHyperlinkURL(context, href, path, line) {
1894
+ const location = line != null ? { path, line } : { path };
1895
+ const scheme = href.slice(0, href.indexOf(":")).toLowerCase();
1896
+ if (!isRegisteredScheme(scheme)) {
1897
+ pushMessage(context.messages, {
1898
+ id: MessageId.HTM_025,
1899
+ message: "Hyperlink uses non-registered URI scheme type",
1900
+ location
1901
+ });
1902
+ }
1903
+ if (/[\s<>]/.test(href) || SPECIAL_URL_SCHEMES.has(scheme) && !href.slice(href.indexOf(":")).startsWith("://")) {
1904
+ pushMessage(context.messages, {
1905
+ id: MessageId.RSC_020,
1906
+ message: `URL is not valid: "${href}"`,
1907
+ location
1908
+ });
1909
+ }
1910
+ }
1911
+ var IMAGE_MAGIC = [
1912
+ { mime: "image/jpeg", bytes: [255, 216], extensions: [".jpg", ".jpeg", ".jpe"] },
1913
+ { mime: "image/gif", bytes: [71, 73, 70, 56], extensions: [".gif"] },
1914
+ { mime: "image/png", bytes: [137, 80, 78, 71], extensions: [".png"] },
1915
+ { mime: "image/webp", bytes: [82, 73, 70, 70], extensions: [".webp"] }
1916
+ ];
1917
+ function stripMimeParams(t) {
1918
+ const idx = t.indexOf(";");
1919
+ return (idx >= 0 ? t.substring(0, idx) : t).trim();
1920
+ }
1921
+ var EPUB_SSV_DEPRECATED = /* @__PURE__ */ new Set([
1922
+ "annoref",
1923
+ "annotation",
1924
+ "biblioentry",
1925
+ "bridgehead",
1926
+ "endnote",
1927
+ "help",
1928
+ "marginalia",
1929
+ "note",
1930
+ "rearnote",
1931
+ "rearnotes",
1932
+ "sidebar",
1933
+ "subchapter",
1934
+ "warning"
1935
+ ]);
1936
+ var EPUB_SSV_DISALLOWED_ON_CONTENT = /* @__PURE__ */ new Set([
1937
+ "aside",
1938
+ "figure",
1939
+ "list",
1940
+ "list-item",
1941
+ "table",
1942
+ "table-cell",
1943
+ "table-row"
1944
+ ]);
1945
+ var EPUB_SSV_ALL = /* @__PURE__ */ new Set([
1946
+ ...EPUB_SSV_DEPRECATED,
1947
+ ...EPUB_SSV_DISALLOWED_ON_CONTENT,
1948
+ "abstract",
1949
+ "acknowledgments",
1950
+ "afterword",
1951
+ "appendix",
1952
+ "assessment",
1953
+ "assessments",
1954
+ "backlink",
1955
+ "backmatter",
1956
+ "balloon",
1957
+ "bibliography",
1958
+ "biblioref",
1959
+ "bodymatter",
1960
+ "case-study",
1961
+ "chapter",
1962
+ "colophon",
1963
+ "concluding-sentence",
1964
+ "conclusion",
1965
+ "contributors",
1966
+ "copyright-page",
1967
+ "cover",
1968
+ "covertitle",
1969
+ "credit",
1970
+ "credits",
1971
+ "dedication",
1972
+ "division",
1973
+ "endnotes",
1974
+ "epigraph",
1975
+ "epilogue",
1976
+ "errata",
1977
+ "fill-in-the-blank-problem",
1978
+ "footnote",
1979
+ "footnotes",
1980
+ "foreword",
1981
+ "frontmatter",
1982
+ "fulltitle",
1983
+ "general-problem",
1984
+ "glossary",
1985
+ "glossdef",
1986
+ "glossref",
1987
+ "glossterm",
1988
+ "halftitle",
1989
+ "halftitlepage",
1990
+ "imprimatur",
1991
+ "imprint",
1992
+ "index",
1993
+ "index-editor-note",
1994
+ "index-entry",
1995
+ "index-entry-list",
1996
+ "index-group",
1997
+ "index-headnotes",
1998
+ "index-legend",
1999
+ "index-locator",
2000
+ "index-locator-list",
2001
+ "index-locator-range",
2002
+ "index-term",
2003
+ "index-term-categories",
2004
+ "index-term-category",
2005
+ "index-xref-preferred",
2006
+ "index-xref-related",
2007
+ "introduction",
2008
+ "keyword",
2009
+ "keywords",
2010
+ "label",
2011
+ "landmarks",
2012
+ "learning-objective",
2013
+ "learning-objectives",
2014
+ "learning-outcome",
2015
+ "learning-outcomes",
2016
+ "learning-resource",
2017
+ "learning-resources",
2018
+ "learning-standard",
2019
+ "learning-standards",
2020
+ "loa",
2021
+ "loi",
2022
+ "lot",
2023
+ "lov",
2024
+ "match-problem",
2025
+ "multiple-choice-problem",
2026
+ "noteref",
2027
+ "notice",
2028
+ "ordinal",
2029
+ "other-credits",
2030
+ "page-list",
2031
+ "pagebreak",
2032
+ "panel",
2033
+ "panel-group",
2034
+ "part",
2035
+ "practice",
2036
+ "practices",
2037
+ "preamble",
2038
+ "preface",
2039
+ "prologue",
2040
+ "pullquote",
2041
+ "qna",
2042
+ "question",
2043
+ "referrer",
2044
+ "revision-history",
2045
+ "seriespage",
2046
+ "sound-area",
2047
+ "subtitle",
2048
+ "tip",
2049
+ "title",
2050
+ "titlepage",
2051
+ "toc",
2052
+ "toc-brief",
2053
+ "topic-sentence",
2054
+ "true-false-problem",
2055
+ "volume"
2056
+ ]);
2057
+ var TIME_RE = /^(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d{1,3})?)?$/;
2058
+ var TZ_RE = /(?:Z|[+-](?:[01]\d|2[0-3]):?[0-5]\d)$/;
2059
+ var DATE_RE = /^\d{4,}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/;
2060
+ var ISO_DURATION_RE = /^P(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d{1,3})?S)?)?$/;
2061
+ var INFORMAL_DURATION_RE = /^\s*(?:\d+(?:\.\d{1,3})?[WDHMS]\s*)+$/;
2062
+ function isValidDatetime(value) {
2063
+ const trimmed = value.trim();
2064
+ if (trimmed === "") return false;
2065
+ if (trimmed.startsWith("P")) {
2066
+ if (!ISO_DURATION_RE.test(trimmed)) return false;
2067
+ if (trimmed === "P" || trimmed === "PT") return false;
2068
+ if (trimmed.endsWith("T")) return false;
2069
+ return true;
2070
+ }
2071
+ if (INFORMAL_DURATION_RE.test(value)) return true;
2072
+ if (/^\d{4,}$/.test(trimmed)) return true;
2073
+ if (/^\d{4,}-(?:0[1-9]|1[0-2])$/.test(trimmed)) return true;
2074
+ if (/^-?-?(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/.test(trimmed)) return true;
2075
+ if (DATE_RE.test(trimmed)) return true;
2076
+ if (/^\d{4,}-W(?:0[1-9]|[1-4]\d|5[0-3])$/.test(trimmed)) return true;
2077
+ if (TIME_RE.test(trimmed)) return true;
2078
+ const dtMatch = /^(\d{4,}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01]))[T ]([\s\S]+)$/.exec(trimmed);
2079
+ if (dtMatch?.[2]) {
2080
+ let timePart = dtMatch[2];
2081
+ const tzMatch = TZ_RE.exec(timePart);
2082
+ if (tzMatch) timePart = timePart.substring(0, timePart.length - tzMatch[0].length);
2083
+ return TIME_RE.test(timePart);
2084
+ }
2085
+ return false;
2086
+ }
1808
2087
  var HTML_ENTITIES = /* @__PURE__ */ new Set([
1809
2088
  "nbsp",
1810
2089
  "iexcl",
@@ -1903,7 +2182,18 @@ var HTML_ENTITIES = /* @__PURE__ */ new Set([
1903
2182
  "thorn",
1904
2183
  "yuml"
1905
2184
  ]);
2185
+ function isItemFixedLayout(packageDoc, itemId) {
2186
+ const spineItem = packageDoc.spine.find((s) => s.idref === itemId);
2187
+ if (!spineItem) return false;
2188
+ if (spineItem.properties?.includes("rendition:layout-pre-paginated")) return true;
2189
+ if (spineItem.properties?.includes("rendition:layout-reflowable")) return false;
2190
+ const globalLayout = packageDoc.metaElements.find(
2191
+ (m) => m.property === "rendition:layout" && !m.refines
2192
+ );
2193
+ return globalLayout?.value === "pre-paginated";
2194
+ }
1906
2195
  var ContentValidator = class {
2196
+ cssWithRemoteResources = /* @__PURE__ */ new Set();
1907
2197
  validate(context, registry, refValidator) {
1908
2198
  const packageDoc = context.packageDocument;
1909
2199
  if (!packageDoc) {
@@ -1911,13 +2201,18 @@ var ContentValidator = class {
1911
2201
  }
1912
2202
  const opfPath = context.opfPath ?? "";
1913
2203
  const opfDir = opfPath.includes("/") ? opfPath.substring(0, opfPath.lastIndexOf("/")) : "";
2204
+ if (refValidator) {
2205
+ for (const item of packageDoc.manifest) {
2206
+ if (item.mediaType === "text/css") {
2207
+ const fullPath = resolveManifestHref(opfDir, item.href);
2208
+ this.validateCSSDocument(context, fullPath, opfDir, refValidator);
2209
+ }
2210
+ }
2211
+ }
1914
2212
  for (const item of packageDoc.manifest) {
1915
2213
  if (item.mediaType === "application/xhtml+xml") {
1916
2214
  const fullPath = resolveManifestHref(opfDir, item.href);
1917
2215
  this.validateXHTMLDocument(context, fullPath, item.id, opfDir, registry, refValidator);
1918
- } else if (item.mediaType === "text/css" && refValidator) {
1919
- const fullPath = resolveManifestHref(opfDir, item.href);
1920
- this.validateCSSDocument(context, fullPath, opfDir, refValidator);
1921
2216
  } else if (item.mediaType === "image/svg+xml") {
1922
2217
  const fullPath = resolveManifestHref(opfDir, item.href);
1923
2218
  if (registry) {
@@ -1930,6 +2225,47 @@ var ContentValidator = class {
1930
2225
  this.extractSVGReferences(context, fullPath, opfDir, refValidator);
1931
2226
  }
1932
2227
  }
2228
+ this.validateMediaFile(context, item, opfDir);
2229
+ }
2230
+ }
2231
+ validateMediaFile(context, item, opfDir) {
2232
+ const declaredType = item.mediaType;
2233
+ const magicEntry = IMAGE_MAGIC.find((m) => m.mime === declaredType);
2234
+ if (!magicEntry) return;
2235
+ const fullPath = resolveManifestHref(opfDir, item.href);
2236
+ const fileData = context.files.get(fullPath);
2237
+ if (!fileData) return;
2238
+ const bytes = typeof fileData === "string" ? new TextEncoder().encode(fileData) : fileData;
2239
+ if (bytes.length < 4) {
2240
+ pushMessage(context.messages, {
2241
+ id: MessageId.MED_004,
2242
+ message: "Image file header may be corrupted",
2243
+ location: { path: fullPath }
2244
+ });
2245
+ pushMessage(context.messages, {
2246
+ id: MessageId.PKG_021,
2247
+ message: "Corrupted image file encountered",
2248
+ location: { path: fullPath }
2249
+ });
2250
+ return;
2251
+ }
2252
+ const headerMatches = magicEntry.bytes.every((b, i) => bytes[i] === b);
2253
+ if (!headerMatches) {
2254
+ const actualType = IMAGE_MAGIC.find((m) => m.bytes.every((b, i) => bytes[i] === b));
2255
+ pushMessage(context.messages, {
2256
+ id: MessageId.OPF_029,
2257
+ message: `File does not match declared media type "${declaredType}"${actualType ? ` (appears to be ${actualType.mime})` : ""}`,
2258
+ location: { path: fullPath }
2259
+ });
2260
+ return;
2261
+ }
2262
+ const ext = item.href.includes(".") ? item.href.substring(item.href.lastIndexOf(".")).toLowerCase() : "";
2263
+ if (ext && !magicEntry.extensions.includes(ext)) {
2264
+ pushMessage(context.messages, {
2265
+ id: MessageId.PKG_022,
2266
+ message: `Wrong file extension "${ext}" for declared media type "${declaredType}"`,
2267
+ location: { path: fullPath }
2268
+ });
1933
2269
  }
1934
2270
  }
1935
2271
  extractSVGIDs(context, path, registry) {
@@ -1972,6 +2308,22 @@ var ContentValidator = class {
1972
2308
  location: { path }
1973
2309
  });
1974
2310
  }
2311
+ this.checkDuplicateIDs(context, path, root);
2312
+ this.checkSVGInvalidIDs(context, path, root);
2313
+ this.validateSvgEpubType(context, path, root);
2314
+ this.checkUnknownEpubAttributes(context, path, root);
2315
+ this.checkSVGLinkAccessibility(context, path, root);
2316
+ const packageDoc = context.packageDocument;
2317
+ if (packageDoc && isItemFixedLayout(packageDoc, manifestItem.id)) {
2318
+ const viewBox = this.getAttribute(root, "viewBox");
2319
+ if (!viewBox) {
2320
+ pushMessage(context.messages, {
2321
+ id: MessageId.HTM_048,
2322
+ message: "SVG Fixed-Layout Documents must have a viewBox attribute on the outermost svg element",
2323
+ location: { path }
2324
+ });
2325
+ }
2326
+ }
1975
2327
  } finally {
1976
2328
  doc.dispose();
1977
2329
  }
@@ -2028,44 +2380,7 @@ var ContentValidator = class {
2028
2380
  }
2029
2381
  } catch {
2030
2382
  }
2031
- try {
2032
- const svgUseXlink = root.find(".//svg:use[@xlink:href]", {
2033
- svg: "http://www.w3.org/2000/svg",
2034
- xlink: "http://www.w3.org/1999/xlink"
2035
- });
2036
- const svgUseHref = root.find(".//svg:use[@href]", {
2037
- svg: "http://www.w3.org/2000/svg"
2038
- });
2039
- for (const useNode of [...svgUseXlink, ...svgUseHref]) {
2040
- const useElem = useNode;
2041
- const href = this.getAttribute(useElem, "xlink:href") ?? this.getAttribute(useElem, "href");
2042
- if (!href) continue;
2043
- if (href.startsWith("http://") || href.startsWith("https://")) continue;
2044
- if (!href.includes("#")) {
2045
- pushMessage(context.messages, {
2046
- id: MessageId.RSC_015,
2047
- message: `SVG "use" element requires a fragment identifier, but found "${href}"`,
2048
- location: { path, line: useNode.line }
2049
- });
2050
- continue;
2051
- }
2052
- const resolvedPath = this.resolveRelativePath(docDir, href, opfDir);
2053
- const hashIndex = resolvedPath.indexOf("#");
2054
- const targetResource = hashIndex >= 0 ? resolvedPath.slice(0, hashIndex) : path;
2055
- const fragment = hashIndex >= 0 ? resolvedPath.slice(hashIndex + 1) : void 0;
2056
- const useRef = {
2057
- url: href,
2058
- targetResource,
2059
- type: "svg-symbol" /* SVG_SYMBOL */,
2060
- location: { path, line: useNode.line }
2061
- };
2062
- if (fragment) {
2063
- useRef.fragment = fragment;
2064
- }
2065
- refValidator.addReference(useRef);
2066
- }
2067
- } catch {
2068
- }
2383
+ this.extractSVGUseReferences(context, path, root, docDir, opfDir, refValidator);
2069
2384
  } finally {
2070
2385
  doc.dispose();
2071
2386
  }
@@ -2103,6 +2418,57 @@ var ContentValidator = class {
2103
2418
  }
2104
2419
  }
2105
2420
  }
2421
+ extractSVGUseReferences(context, path, root, docDir, opfDir, refValidator) {
2422
+ try {
2423
+ const svgUseXlink = root.find(".//svg:use[@xlink:href]", {
2424
+ svg: "http://www.w3.org/2000/svg",
2425
+ xlink: "http://www.w3.org/1999/xlink"
2426
+ });
2427
+ const svgUseHref = root.find(".//svg:use[@href]", {
2428
+ svg: "http://www.w3.org/2000/svg"
2429
+ });
2430
+ for (const useNode of [...svgUseXlink, ...svgUseHref]) {
2431
+ const useElem = useNode;
2432
+ const href = this.getAttribute(useElem, "xlink:href") ?? this.getAttribute(useElem, "href");
2433
+ if (href === null) continue;
2434
+ if (href.startsWith("http://") || href.startsWith("https://")) continue;
2435
+ const line = useNode.line;
2436
+ if (href === "" || !href.includes("#")) {
2437
+ pushMessage(context.messages, {
2438
+ id: MessageId.RSC_015,
2439
+ message: `SVG "use" element requires a fragment identifier, but found "${href}"`,
2440
+ location: { path, line }
2441
+ });
2442
+ continue;
2443
+ }
2444
+ if (href.startsWith("#")) {
2445
+ refValidator.addReference({
2446
+ url: href,
2447
+ targetResource: path,
2448
+ fragment: href.slice(1),
2449
+ type: "svg-symbol" /* SVG_SYMBOL */,
2450
+ location: { path, line }
2451
+ });
2452
+ continue;
2453
+ }
2454
+ const resolvedPath = this.resolveRelativePath(docDir, href, opfDir);
2455
+ const hashIndex = resolvedPath.indexOf("#");
2456
+ const targetResource = hashIndex >= 0 ? resolvedPath.slice(0, hashIndex) : path;
2457
+ const fragment = hashIndex >= 0 ? resolvedPath.slice(hashIndex + 1) : void 0;
2458
+ const useRef = {
2459
+ url: href,
2460
+ targetResource,
2461
+ type: "svg-symbol" /* SVG_SYMBOL */,
2462
+ location: { path, line }
2463
+ };
2464
+ if (fragment) {
2465
+ useRef.fragment = fragment;
2466
+ }
2467
+ refValidator.addReference(useRef);
2468
+ }
2469
+ } catch {
2470
+ }
2471
+ }
2106
2472
  detectSVGRemoteResources(root) {
2107
2473
  try {
2108
2474
  const fontFaceUris = root.find(".//svg:font-face-uri", {
@@ -2143,13 +2509,33 @@ var ContentValidator = class {
2143
2509
  if (!cssData) {
2144
2510
  return;
2145
2511
  }
2146
- const cssContent = new TextDecoder().decode(cssData);
2512
+ let cssContent;
2513
+ const utf16Encoding = cssData.length >= 2 && cssData[0] === 254 && cssData[1] === 255 ? "utf-16be" : cssData.length >= 2 && cssData[0] === 255 && cssData[1] === 254 ? "utf-16le" : null;
2514
+ if (utf16Encoding) {
2515
+ pushMessage(context.messages, {
2516
+ id: MessageId.CSS_003,
2517
+ message: "CSS documents should be encoded in UTF-8, but UTF-16 was detected",
2518
+ location: { path }
2519
+ });
2520
+ cssContent = new TextDecoder(utf16Encoding).decode(cssData);
2521
+ } else {
2522
+ cssContent = new TextDecoder().decode(cssData);
2523
+ const charsetMatch = CSS_CHARSET_RE.exec(cssContent);
2524
+ if (charsetMatch?.[1] && charsetMatch[1].toLowerCase() !== "utf-8") {
2525
+ pushMessage(context.messages, {
2526
+ id: MessageId.CSS_004,
2527
+ message: `CSS documents must be encoded in UTF-8, but detected "${charsetMatch[1]}"`,
2528
+ location: { path }
2529
+ });
2530
+ }
2531
+ }
2147
2532
  const cssValidator = new CSSValidator();
2148
2533
  const result = cssValidator.validate(context, cssContent, path);
2149
2534
  const hasRemoteResources = result.references.some(
2150
2535
  (ref) => ref.url.startsWith("http://") || ref.url.startsWith("https://")
2151
2536
  );
2152
2537
  if (hasRemoteResources) {
2538
+ this.cssWithRemoteResources.add(path);
2153
2539
  const packageDoc = context.packageDocument;
2154
2540
  if (packageDoc) {
2155
2541
  const manifestItem = packageDoc.manifest.find(
@@ -2235,12 +2621,66 @@ var ContentValidator = class {
2235
2621
  if (!data) {
2236
2622
  return;
2237
2623
  }
2238
- const content = new TextDecoder().decode(data);
2624
+ if (data.length >= 2 && (data[0] === 254 && data[1] === 255 || data[0] === 255 && data[1] === 254)) {
2625
+ pushMessage(context.messages, {
2626
+ id: MessageId.HTM_058,
2627
+ message: "HTML documents must be encoded in UTF-8, but UTF-16 was detected",
2628
+ location: { path }
2629
+ });
2630
+ return;
2631
+ }
2632
+ let content = new TextDecoder().decode(data);
2239
2633
  const packageDoc = context.packageDocument;
2240
2634
  if (!packageDoc) {
2241
2635
  return;
2242
2636
  }
2637
+ const epubNsMatch = EPUB_XMLNS_RE.exec(content);
2638
+ if (epubNsMatch?.[1] && epubNsMatch[1] !== "http://www.idpf.org/2007/ops") {
2639
+ pushMessage(context.messages, {
2640
+ id: MessageId.HTM_010,
2641
+ message: `Namespace URI "${epubNsMatch[1]}" is unusual for the "epub" prefix`,
2642
+ location: { path }
2643
+ });
2644
+ content = content.replace(epubNsMatch[0], 'xmlns:epub="http://www.idpf.org/2007/ops"');
2645
+ }
2243
2646
  this.checkUnescapedAmpersands(context, path, content);
2647
+ if (context.version !== "2.0") {
2648
+ const doctypeMatch = /<!DOCTYPE\s+html\b([\s\S]*?)>/i.exec(content);
2649
+ if (doctypeMatch) {
2650
+ const inner = doctypeMatch[1] ?? "";
2651
+ const hasPublic = /\bPUBLIC\b/i.test(inner);
2652
+ const hasSystem = /\bSYSTEM\b/i.test(inner);
2653
+ const isLegacyCompat = /['"]about:legacy-compat['"]/.test(inner);
2654
+ if (hasPublic || hasSystem && !isLegacyCompat) {
2655
+ pushMessage(context.messages, {
2656
+ id: MessageId.HTM_004,
2657
+ message: 'Irregular DOCTYPE found; expected "<!DOCTYPE html>"',
2658
+ location: { path }
2659
+ });
2660
+ }
2661
+ }
2662
+ }
2663
+ if (context.version !== "2.0") {
2664
+ const entityRe = /<!ENTITY\s+\w+\s+(?:SYSTEM|PUBLIC)\s/gi;
2665
+ let entityMatch = entityRe.exec(content);
2666
+ while (entityMatch) {
2667
+ pushMessage(context.messages, {
2668
+ id: MessageId.HTM_003,
2669
+ message: "External entities are not allowed in EPUB 3 content documents",
2670
+ location: { path }
2671
+ });
2672
+ entityMatch = entityRe.exec(content);
2673
+ }
2674
+ }
2675
+ const xmlVersionMatch = /<\?xml\s[^?]*version\s*=\s*["']([^"']+)["']/.exec(content);
2676
+ if (xmlVersionMatch?.[1] && xmlVersionMatch[1] !== "1.0") {
2677
+ pushMessage(context.messages, {
2678
+ id: MessageId.HTM_001,
2679
+ message: `XML version "${xmlVersionMatch[1]}" is not allowed; must be "1.0"`,
2680
+ location: { path }
2681
+ });
2682
+ return;
2683
+ }
2244
2684
  let doc = null;
2245
2685
  try {
2246
2686
  doc = libxml2Wasm.XmlDocument.fromString(content);
@@ -2260,8 +2700,9 @@ var ContentValidator = class {
2260
2700
  if (column !== void 0) {
2261
2701
  location.column = column;
2262
2702
  }
2703
+ const isEntityError = error.message.includes("Entity '") || error.message.includes("EntityRef:");
2263
2704
  pushMessage(context.messages, {
2264
- id: MessageId.HTM_004,
2705
+ id: isEntityError ? MessageId.RSC_016 : MessageId.HTM_004,
2265
2706
  message,
2266
2707
  location
2267
2708
  });
@@ -2291,10 +2732,19 @@ var ContentValidator = class {
2291
2732
  const title = root.get(".//html:title", { html: "http://www.w3.org/1999/xhtml" });
2292
2733
  if (!title) {
2293
2734
  pushMessage(context.messages, {
2294
- id: MessageId.HTM_003,
2295
- message: "XHTML document must have a title element",
2735
+ id: MessageId.RSC_017,
2736
+ message: 'The "head" element should have a "title" child element',
2296
2737
  location: { path }
2297
2738
  });
2739
+ } else {
2740
+ const titleText = title.content.trim();
2741
+ if (titleText === "") {
2742
+ pushMessage(context.messages, {
2743
+ id: MessageId.RSC_005,
2744
+ message: 'The "title" element must not be empty',
2745
+ location: { path, line: title.line }
2746
+ });
2747
+ }
2298
2748
  }
2299
2749
  const body = root.get(".//html:body", { html: "http://www.w3.org/1999/xhtml" });
2300
2750
  if (!body) {
@@ -2372,7 +2822,7 @@ var ContentValidator = class {
2372
2822
  location: { path }
2373
2823
  });
2374
2824
  }
2375
- const hasRemoteResources = this.detectRemoteResources(context, path, root);
2825
+ const hasRemoteResources = this.detectRemoteResources(context, path, root, opfDir);
2376
2826
  if (hasRemoteResources && !manifestItem?.properties?.includes("remote-resources")) {
2377
2827
  pushMessage(context.messages, {
2378
2828
  id: MessageId.OPF_014,
@@ -2389,11 +2839,28 @@ var ContentValidator = class {
2389
2839
  }
2390
2840
  }
2391
2841
  this.checkDiscouragedElements(context, path, root);
2842
+ this.checkSSMLPh(context, path, root, content);
2843
+ this.checkObsoleteHTML(context, path, root);
2844
+ this.checkDuplicateIDs(context, path, root);
2845
+ this.checkImgSrcEmpty(context, path, root);
2846
+ this.checkStyleInBody(context, path, root);
2847
+ this.validateInlineStyles(context, path, root);
2848
+ this.checkHttpEquivCharset(context, path, root);
2849
+ this.checkLangMismatch(context, path, root);
2850
+ this.checkDpubAriaDeprecated(context, path, root);
2851
+ this.checkTableBorder(context, path, root);
2852
+ this.checkTimeElement(context, path, root);
2853
+ this.checkMathMLAnnotations(context, path, root);
2854
+ this.checkReservedNamespace(context, path, content);
2855
+ this.checkDataAttributes(context, path, root);
2392
2856
  this.checkAccessibility(context, path, root);
2393
2857
  this.validateImages(context, path, root);
2394
2858
  if (context.version.startsWith("3")) {
2395
2859
  this.validateEpubTypes(context, path, root);
2396
2860
  }
2861
+ this.validateEpubSwitch(context, path, root);
2862
+ this.validateEpubTrigger(context, path, root);
2863
+ this.validateStyleAttributes(context, path, root);
2397
2864
  this.validateStylesheetLinks(context, path, root);
2398
2865
  this.validateViewportMeta(context, path, root, manifestItem);
2399
2866
  if (registry) {
@@ -2401,7 +2868,7 @@ var ContentValidator = class {
2401
2868
  }
2402
2869
  if (refValidator && opfDir !== void 0) {
2403
2870
  this.extractAndRegisterHyperlinks(context, path, root, opfDir, refValidator, !!isNavItem);
2404
- this.extractAndRegisterStylesheets(path, root, opfDir, refValidator);
2871
+ this.extractAndRegisterStylesheets(context, path, root, opfDir, refValidator);
2405
2872
  this.extractAndRegisterImages(context, path, root, opfDir, refValidator, registry);
2406
2873
  this.extractAndRegisterMathMLAltimg(path, root, opfDir, refValidator);
2407
2874
  this.extractAndRegisterScripts(path, root, opfDir, refValidator);
@@ -2841,7 +3308,7 @@ var ContentValidator = class {
2841
3308
  * - Remote scripts do NOT require the property (scripted property is used instead)
2842
3309
  * - Remote stylesheets DO require the property
2843
3310
  */
2844
- detectRemoteResources(_context, _path, root) {
3311
+ detectRemoteResources(_context, path, root, opfDir) {
2845
3312
  const images = root.find(".//html:img[@src]", { html: "http://www.w3.org/1999/xhtml" });
2846
3313
  for (const img of images) {
2847
3314
  const src = this.getAttribute(img, "src");
@@ -2887,6 +3354,7 @@ var ContentValidator = class {
2887
3354
  const linkElements = root.find(".//html:link[@rel and @href]", {
2888
3355
  html: "http://www.w3.org/1999/xhtml"
2889
3356
  });
3357
+ const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
2890
3358
  for (const linkElem of linkElements) {
2891
3359
  const rel = this.getAttribute(linkElem, "rel");
2892
3360
  const href = this.getAttribute(linkElem, "href");
@@ -2894,6 +3362,10 @@ var ContentValidator = class {
2894
3362
  if (href.startsWith("http://") || href.startsWith("https://")) {
2895
3363
  return true;
2896
3364
  }
3365
+ const resolvedCss = this.resolveRelativePath(docDir, href, opfDir ?? "");
3366
+ if (this.cssWithRemoteResources.has(resolvedCss)) {
3367
+ return true;
3368
+ }
2897
3369
  }
2898
3370
  }
2899
3371
  const styleElements = root.find(".//html:style", { html: "http://www.w3.org/1999/xhtml" });
@@ -2921,77 +3393,733 @@ var ContentValidator = class {
2921
3393
  }
2922
3394
  }
2923
3395
  }
2924
- checkAccessibility(context, path, root) {
2925
- const links = root.find(".//html:a", { html: "http://www.w3.org/1999/xhtml" });
2926
- for (const link of links) {
2927
- if (!this.hasAccessibleContent(link)) {
3396
+ checkSSMLPh(context, path, root, content) {
3397
+ const ssmlPhPattern = /\bssml:ph\s*=\s*"([^"]*)"/g;
3398
+ let match;
3399
+ while ((match = ssmlPhPattern.exec(content)) !== null) {
3400
+ if (match[1]?.trim() === "") {
3401
+ const line = content.substring(0, match.index).split("\n").length;
2928
3402
  pushMessage(context.messages, {
2929
- id: MessageId.ACC_004,
2930
- message: "Hyperlink has no accessible text content",
2931
- location: { path }
3403
+ id: MessageId.HTM_007,
3404
+ message: "The ssml:ph attribute value should not be empty",
3405
+ location: { path, line }
2932
3406
  });
2933
3407
  }
2934
3408
  }
2935
- const images = root.find(".//html:img", { html: "http://www.w3.org/1999/xhtml" });
2936
- for (const img of images) {
2937
- const altAttr = this.getAttribute(img, "alt");
2938
- if (altAttr === null) {
3409
+ }
3410
+ checkObsoleteHTML(context, path, root) {
3411
+ const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
3412
+ const obsoleteGlobalAttrs = ["contextmenu", "dropzone"];
3413
+ for (const attr of obsoleteGlobalAttrs) {
3414
+ try {
3415
+ const elements = root.find(`.//*[@${attr}]`);
3416
+ for (const el of elements) {
3417
+ pushMessage(context.messages, {
3418
+ id: MessageId.RSC_005,
3419
+ message: `The "${attr}" attribute is obsolete`,
3420
+ location: { path, line: el.line }
3421
+ });
3422
+ }
3423
+ } catch {
3424
+ }
3425
+ }
3426
+ const obsoleteElementAttrs = [
3427
+ ["typemustmatch", ".//html:object[@typemustmatch]"],
3428
+ ["pubdate", ".//html:time[@pubdate]"],
3429
+ ["seamless", ".//html:iframe[@seamless]"]
3430
+ ];
3431
+ for (const [attr, xpath] of obsoleteElementAttrs) {
3432
+ try {
3433
+ const elements = root.find(xpath, HTML_NS);
3434
+ for (const el of elements) {
3435
+ pushMessage(context.messages, {
3436
+ id: MessageId.RSC_005,
3437
+ message: `The "${attr}" attribute is obsolete`,
3438
+ location: { path, line: el.line }
3439
+ });
3440
+ }
3441
+ } catch {
3442
+ }
3443
+ }
3444
+ try {
3445
+ const keygens = root.find(".//html:keygen", HTML_NS);
3446
+ for (const keygen of keygens) {
2939
3447
  pushMessage(context.messages, {
2940
- id: MessageId.ACC_005,
2941
- message: "Image is missing alt attribute",
2942
- location: { path }
3448
+ id: MessageId.RSC_005,
3449
+ message: 'The "keygen" element is obsolete',
3450
+ location: { path, line: keygen.line }
2943
3451
  });
2944
3452
  }
3453
+ } catch {
2945
3454
  }
2946
- const svgLinks = root.find(".//svg:a", {
2947
- svg: "http://www.w3.org/2000/svg",
2948
- xlink: "http://www.w3.org/1999/xlink"
2949
- });
2950
- for (const svgLink of svgLinks) {
2951
- const svgElem = svgLink;
2952
- const title = svgElem.get("./svg:title", { svg: "http://www.w3.org/2000/svg" });
2953
- const ariaLabel = this.getAttribute(svgElem, "aria-label");
2954
- if (!title && !ariaLabel) {
3455
+ try {
3456
+ const menuTypes = root.find(".//html:menu[@type]", HTML_NS);
3457
+ for (const menuType of menuTypes) {
2955
3458
  pushMessage(context.messages, {
2956
- id: MessageId.ACC_011,
2957
- message: "SVG hyperlink has no accessible name (missing title element or aria-label)",
2958
- location: { path }
3459
+ id: MessageId.RSC_005,
3460
+ message: 'The "type" attribute on the "menu" element is obsolete',
3461
+ location: { path, line: menuType.line }
2959
3462
  });
2960
3463
  }
3464
+ } catch {
2961
3465
  }
2962
- const mathElements = root.find(".//math:math", { math: "http://www.w3.org/1998/Math/MathML" });
2963
- for (const mathElem of mathElements) {
2964
- const elem = mathElem;
2965
- const alttext = elem.attr("alttext");
2966
- const annotation = elem.get('./math:annotation[@encoding="application/x-tex"]', {
2967
- math: "http://www.w3.org/1998/Math/MathML"
2968
- });
2969
- const ariaLabel = this.getAttribute(elem, "aria-label");
2970
- if (!alttext?.value && !annotation && !ariaLabel) {
3466
+ try {
3467
+ const commands = root.find(".//html:command", HTML_NS);
3468
+ for (const command of commands) {
2971
3469
  pushMessage(context.messages, {
2972
- id: MessageId.ACC_009,
2973
- message: "MathML element should have alttext attribute or annotation for accessibility",
2974
- location: { path }
3470
+ id: MessageId.RSC_005,
3471
+ message: 'The "command" element is obsolete',
3472
+ location: { path, line: command.line }
2975
3473
  });
2976
3474
  }
3475
+ } catch {
2977
3476
  }
2978
3477
  }
2979
- validateImages(context, path, root) {
2980
- const packageDoc = context.packageDocument;
2981
- if (!packageDoc) return;
2982
- const images = root.find(".//html:img[@src]", { html: "http://www.w3.org/1999/xhtml" });
2983
- for (const img of images) {
2984
- const imgElem = img;
2985
- const srcAttr = this.getAttribute(imgElem, "src");
2986
- if (!srcAttr) continue;
2987
- const src = srcAttr;
2988
- const opfDir = context.opfPath?.includes("/") ? context.opfPath.substring(0, context.opfPath.lastIndexOf("/")) : "";
2989
- let fullPath = src;
2990
- if (opfDir && !src.startsWith("http://") && !src.startsWith("https://")) {
2991
- if (src.startsWith("/")) {
2992
- fullPath = src.slice(1);
3478
+ checkDuplicateIDs(context, path, root) {
3479
+ const seen = /* @__PURE__ */ new Map();
3480
+ const elements = root.find(".//*[@id]");
3481
+ for (const elem of elements) {
3482
+ const id = this.getAttribute(elem, "id");
3483
+ if (id) {
3484
+ if (seen.has(id)) {
3485
+ pushMessage(context.messages, {
3486
+ id: MessageId.RSC_005,
3487
+ message: `Duplicate ID "${id}"`,
3488
+ location: { path, line: elem.line }
3489
+ });
2993
3490
  } else {
2994
- const parts = opfDir.split("/");
3491
+ seen.set(id, elem.line);
3492
+ }
3493
+ }
3494
+ }
3495
+ }
3496
+ checkImgSrcEmpty(context, path, root) {
3497
+ const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
3498
+ try {
3499
+ const imgs = root.find(".//html:img[@src]", HTML_NS);
3500
+ for (const img of imgs) {
3501
+ const src = this.getAttribute(img, "src");
3502
+ if (src !== null && src.trim() === "") {
3503
+ pushMessage(context.messages, {
3504
+ id: MessageId.RSC_005,
3505
+ message: 'The "src" attribute must not be empty',
3506
+ location: { path, line: img.line }
3507
+ });
3508
+ }
3509
+ }
3510
+ } catch {
3511
+ }
3512
+ }
3513
+ checkStyleInBody(context, path, root) {
3514
+ const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
3515
+ try {
3516
+ const bodyStyles = root.find(".//html:body//html:style", HTML_NS);
3517
+ for (const style of bodyStyles) {
3518
+ pushMessage(context.messages, {
3519
+ id: MessageId.RSC_005,
3520
+ message: 'The "style" element must not appear in the document body',
3521
+ location: { path, line: style.line }
3522
+ });
3523
+ }
3524
+ } catch {
3525
+ }
3526
+ }
3527
+ checkHttpEquivCharset(context, path, root) {
3528
+ const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
3529
+ try {
3530
+ const metas = root.find(".//html:head/html:meta", HTML_NS);
3531
+ let hasCharsetMeta = false;
3532
+ let hasHttpEquivContentType = false;
3533
+ for (const meta of metas) {
3534
+ const el = meta;
3535
+ const charset = this.getAttribute(el, "charset");
3536
+ if (charset !== null) {
3537
+ hasCharsetMeta = true;
3538
+ }
3539
+ const httpEquiv = this.getAttribute(el, "http-equiv");
3540
+ if (httpEquiv?.toLowerCase() === "content-type") {
3541
+ hasHttpEquivContentType = true;
3542
+ const contentAttr = (this.getAttribute(el, "content") ?? "").trim();
3543
+ if (!/^text\/html;\s*charset=utf-8$/i.test(contentAttr)) {
3544
+ pushMessage(context.messages, {
3545
+ id: MessageId.RSC_005,
3546
+ message: `The meta element in encoding declaration state must have the value "text/html; charset=utf-8"`,
3547
+ location: { path, line: el.line }
3548
+ });
3549
+ }
3550
+ }
3551
+ }
3552
+ if (hasCharsetMeta && hasHttpEquivContentType) {
3553
+ pushMessage(context.messages, {
3554
+ id: MessageId.RSC_005,
3555
+ message: "The document must not contain both a meta charset declaration and a meta http-equiv Content-Type declaration",
3556
+ location: { path }
3557
+ });
3558
+ }
3559
+ } catch {
3560
+ }
3561
+ }
3562
+ checkSVGInvalidIDs(context, path, root) {
3563
+ const XML_NAME_START_RE = /^[a-zA-Z_:\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF]/;
3564
+ const elements = root.find(".//*[@id]");
3565
+ for (const elem of elements) {
3566
+ const id = this.getAttribute(elem, "id");
3567
+ if (id && !XML_NAME_START_RE.test(id)) {
3568
+ pushMessage(context.messages, {
3569
+ id: MessageId.RSC_005,
3570
+ message: `Invalid ID value "${id}"`,
3571
+ location: { path, line: elem.line }
3572
+ });
3573
+ }
3574
+ }
3575
+ const rootId = this.getAttribute(root, "id");
3576
+ if (rootId && !XML_NAME_START_RE.test(rootId)) {
3577
+ pushMessage(context.messages, {
3578
+ id: MessageId.RSC_005,
3579
+ message: `Invalid ID value "${rootId}"`,
3580
+ location: { path, line: root.line }
3581
+ });
3582
+ }
3583
+ }
3584
+ validateInlineStyles(context, path, root) {
3585
+ const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
3586
+ try {
3587
+ const styles = root.find(".//html:style", HTML_NS);
3588
+ for (const style of styles) {
3589
+ const cssContent = style.content;
3590
+ if (cssContent) {
3591
+ const cssValidator = new CSSValidator();
3592
+ cssValidator.validate(context, cssContent, path);
3593
+ }
3594
+ }
3595
+ } catch {
3596
+ }
3597
+ }
3598
+ checkLangMismatch(context, path, root) {
3599
+ const lang = root.attr("lang")?.value ?? null;
3600
+ const xmlLang = root.attr("lang", "xml")?.value ?? null;
3601
+ if (lang !== null && xmlLang !== null && lang.toLowerCase() !== xmlLang.toLowerCase()) {
3602
+ pushMessage(context.messages, {
3603
+ id: MessageId.RSC_005,
3604
+ message: "The lang and xml:lang attributes must have the same value",
3605
+ location: { path, line: root.line }
3606
+ });
3607
+ }
3608
+ }
3609
+ checkDpubAriaDeprecated(context, path, root) {
3610
+ const DEPRECATED_ROLES = ["doc-endnote", "doc-biblioentry"];
3611
+ try {
3612
+ const elements = root.find(".//*[@role]");
3613
+ for (const elem of elements) {
3614
+ const roleAttr = this.getAttribute(elem, "role");
3615
+ if (!roleAttr) continue;
3616
+ const roles = roleAttr.split(/\s+/);
3617
+ for (const role of DEPRECATED_ROLES) {
3618
+ if (roles.includes(role)) {
3619
+ pushMessage(context.messages, {
3620
+ id: MessageId.RSC_017,
3621
+ message: `The "${role}" role is deprecated and should not be used`,
3622
+ location: { path, line: elem.line }
3623
+ });
3624
+ }
3625
+ }
3626
+ }
3627
+ } catch {
3628
+ }
3629
+ }
3630
+ validateEpubSwitch(context, path, root) {
3631
+ const EPUB_NS = { epub: "http://www.idpf.org/2007/ops" };
3632
+ try {
3633
+ const switches = root.find(".//epub:switch", EPUB_NS);
3634
+ for (const sw of switches) {
3635
+ pushMessage(context.messages, {
3636
+ id: MessageId.RSC_017,
3637
+ message: 'The "epub:switch" element is deprecated',
3638
+ location: { path, line: sw.line }
3639
+ });
3640
+ const swElem = sw;
3641
+ const cases = [];
3642
+ const defaults = [];
3643
+ let defaultBeforeCase = false;
3644
+ try {
3645
+ const childCases = swElem.find("./epub:case", EPUB_NS);
3646
+ const childDefaults = swElem.find("./epub:default", EPUB_NS);
3647
+ cases.push(...childCases);
3648
+ defaults.push(...childDefaults);
3649
+ const firstDefault = childDefaults[0];
3650
+ const lastCase = childCases[childCases.length - 1];
3651
+ if (firstDefault && lastCase && firstDefault.line < lastCase.line) {
3652
+ defaultBeforeCase = true;
3653
+ }
3654
+ } catch {
3655
+ }
3656
+ if (cases.length === 0) {
3657
+ pushMessage(context.messages, {
3658
+ id: MessageId.RSC_005,
3659
+ message: 'The "epub:switch" element must contain at least one "epub:case" child element',
3660
+ location: { path, line: sw.line }
3661
+ });
3662
+ }
3663
+ if (defaults.length === 0) {
3664
+ pushMessage(context.messages, {
3665
+ id: MessageId.RSC_005,
3666
+ message: 'The "epub:switch" element must contain an "epub:default" child element',
3667
+ location: { path, line: sw.line }
3668
+ });
3669
+ }
3670
+ const secondDefault = defaults[1];
3671
+ if (secondDefault) {
3672
+ pushMessage(context.messages, {
3673
+ id: MessageId.RSC_005,
3674
+ message: 'The "epub:switch" element must not contain more than one "epub:default" child element',
3675
+ location: { path, line: secondDefault.line }
3676
+ });
3677
+ }
3678
+ const firstDefaultElem = defaults[0];
3679
+ if (defaultBeforeCase && firstDefaultElem) {
3680
+ pushMessage(context.messages, {
3681
+ id: MessageId.RSC_005,
3682
+ message: 'The "epub:default" element must appear after all "epub:case" elements',
3683
+ location: { path, line: firstDefaultElem.line }
3684
+ });
3685
+ }
3686
+ for (const c of cases) {
3687
+ const caseElem = c;
3688
+ const reqNs = caseElem.attr("required-namespace");
3689
+ if (!reqNs) {
3690
+ pushMessage(context.messages, {
3691
+ id: MessageId.RSC_005,
3692
+ message: 'The "epub:case" element must have a "required-namespace" attribute',
3693
+ location: { path, line: c.line }
3694
+ });
3695
+ }
3696
+ }
3697
+ try {
3698
+ const MATH_NS = { m: "http://www.w3.org/1998/Math/MathML" };
3699
+ const nestedMath = swElem.find(".//m:math//m:math", MATH_NS);
3700
+ for (const nested of nestedMath) {
3701
+ pushMessage(context.messages, {
3702
+ id: MessageId.RSC_005,
3703
+ message: 'The "math" element must not be nested inside another "math" element',
3704
+ location: { path, line: nested.line }
3705
+ });
3706
+ }
3707
+ } catch {
3708
+ }
3709
+ }
3710
+ } catch {
3711
+ }
3712
+ }
3713
+ validateEpubTrigger(context, path, root) {
3714
+ const EPUB_NS = { epub: "http://www.idpf.org/2007/ops" };
3715
+ try {
3716
+ const triggers = root.find(".//epub:trigger", EPUB_NS);
3717
+ if (triggers.length === 0) return;
3718
+ const allIds = /* @__PURE__ */ new Set();
3719
+ try {
3720
+ const idElements = root.find(".//*[@id]");
3721
+ for (const el of idElements) {
3722
+ const idAttr = this.getAttribute(el, "id");
3723
+ if (idAttr) allIds.add(idAttr);
3724
+ }
3725
+ } catch {
3726
+ }
3727
+ for (const trigger of triggers) {
3728
+ pushMessage(context.messages, {
3729
+ id: MessageId.RSC_017,
3730
+ message: 'The "epub:trigger" element is deprecated',
3731
+ location: { path, line: trigger.line }
3732
+ });
3733
+ const triggerElem = trigger;
3734
+ const ref = triggerElem.attr("ref");
3735
+ if (ref?.value && !allIds.has(ref.value)) {
3736
+ pushMessage(context.messages, {
3737
+ id: MessageId.RSC_005,
3738
+ message: `The "ref" attribute value "${ref.value}" does not reference a valid ID in the document`,
3739
+ location: { path, line: trigger.line }
3740
+ });
3741
+ }
3742
+ const observer = triggerElem.attr("observer", "ev") ?? triggerElem.attr("ev:observer");
3743
+ if (observer?.value && !allIds.has(observer.value)) {
3744
+ pushMessage(context.messages, {
3745
+ id: MessageId.RSC_005,
3746
+ message: `The "ev:observer" attribute value "${observer.value}" does not reference a valid ID in the document`,
3747
+ location: { path, line: trigger.line }
3748
+ });
3749
+ }
3750
+ }
3751
+ } catch {
3752
+ }
3753
+ }
3754
+ validateStyleAttributes(context, path, root) {
3755
+ try {
3756
+ const elements = root.find(".//*[@style]");
3757
+ for (const elem of elements) {
3758
+ const style = this.getAttribute(elem, "style");
3759
+ if (!style) continue;
3760
+ const wrappedCss = `* { ${style} }`;
3761
+ const cssValidator = new CSSValidator();
3762
+ cssValidator.validate(context, wrappedCss, path);
3763
+ }
3764
+ } catch {
3765
+ }
3766
+ }
3767
+ validateSvgEpubType(context, path, root) {
3768
+ const ALLOWED_ELEMENTS = /* @__PURE__ */ new Set([
3769
+ "svg",
3770
+ "a",
3771
+ "audio",
3772
+ "canvas",
3773
+ "circle",
3774
+ "ellipse",
3775
+ "g",
3776
+ "iframe",
3777
+ "image",
3778
+ "line",
3779
+ "path",
3780
+ "polygon",
3781
+ "polyline",
3782
+ "rect",
3783
+ "switch",
3784
+ "symbol",
3785
+ "text",
3786
+ "textPath",
3787
+ "tspan",
3788
+ "unknown",
3789
+ "use",
3790
+ "video"
3791
+ ]);
3792
+ const EPUB_NS = { epub: "http://www.idpf.org/2007/ops" };
3793
+ try {
3794
+ const elements = root.find(".//*[@epub:type]", EPUB_NS);
3795
+ for (const elem of elements) {
3796
+ const elemTyped = elem;
3797
+ const localName = elemTyped.name;
3798
+ if (!ALLOWED_ELEMENTS.has(localName)) {
3799
+ pushMessage(context.messages, {
3800
+ id: MessageId.RSC_005,
3801
+ message: `Attribute "epub:type" not allowed on SVG element "${localName}"`,
3802
+ location: { path, line: elem.line }
3803
+ });
3804
+ }
3805
+ }
3806
+ const rootEpubType = root.attr("type", "epub");
3807
+ if (rootEpubType && !ALLOWED_ELEMENTS.has(root.name)) {
3808
+ pushMessage(context.messages, {
3809
+ id: MessageId.RSC_005,
3810
+ message: `Attribute "epub:type" not allowed on SVG element "${root.name}"`,
3811
+ location: { path, line: root.line }
3812
+ });
3813
+ }
3814
+ } catch {
3815
+ }
3816
+ }
3817
+ checkUnknownEpubAttributes(context, path, root) {
3818
+ const KNOWN_EPUB_ATTRS = /* @__PURE__ */ new Set(["type"]);
3819
+ const checkElement = (elem) => {
3820
+ if (!("attrs" in elem)) return;
3821
+ for (const attr of elem.attrs) {
3822
+ if (attr.prefix === "epub" && !KNOWN_EPUB_ATTRS.has(attr.name)) {
3823
+ pushMessage(context.messages, {
3824
+ id: MessageId.RSC_005,
3825
+ message: `Attribute "epub:${attr.name}" not allowed`,
3826
+ location: { path, line: elem.line }
3827
+ });
3828
+ }
3829
+ }
3830
+ };
3831
+ checkElement(root);
3832
+ try {
3833
+ const allElements = root.find(".//*");
3834
+ for (const elem of allElements) {
3835
+ checkElement(elem);
3836
+ }
3837
+ } catch {
3838
+ }
3839
+ }
3840
+ checkTableBorder(context, path, root) {
3841
+ const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
3842
+ try {
3843
+ const tables = root.find(".//html:table[@border]", HTML_NS);
3844
+ for (const table of tables) {
3845
+ const border = this.getAttribute(table, "border");
3846
+ if (border !== null && border !== "" && border !== "1") {
3847
+ pushMessage(context.messages, {
3848
+ id: MessageId.RSC_005,
3849
+ message: `The value of the "border" attribute on the "table" element must be either "1" or the empty string`,
3850
+ location: { path, line: table.line }
3851
+ });
3852
+ }
3853
+ }
3854
+ } catch {
3855
+ }
3856
+ }
3857
+ checkTimeElement(context, path, root) {
3858
+ const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
3859
+ try {
3860
+ const nestedTimes = root.find(".//html:time//html:time", HTML_NS);
3861
+ for (const nested of nestedTimes) {
3862
+ pushMessage(context.messages, {
3863
+ id: MessageId.RSC_005,
3864
+ message: 'The element "time" must not appear as a descendant of the "time" element',
3865
+ location: { path, line: nested.line }
3866
+ });
3867
+ }
3868
+ } catch {
3869
+ }
3870
+ try {
3871
+ const times = root.find(".//html:time[@datetime]", HTML_NS);
3872
+ for (const time of times) {
3873
+ const datetime = this.getAttribute(time, "datetime");
3874
+ if (datetime !== null && !isValidDatetime(datetime)) {
3875
+ pushMessage(context.messages, {
3876
+ id: MessageId.RSC_005,
3877
+ message: `The "datetime" attribute value "${datetime}" is not a valid date, time, or duration`,
3878
+ location: { path, line: time.line }
3879
+ });
3880
+ }
3881
+ }
3882
+ } catch {
3883
+ }
3884
+ }
3885
+ checkMathMLAnnotations(context, path, root) {
3886
+ const MATH_NS = { math: "http://www.w3.org/1998/Math/MathML" };
3887
+ const CONTENT_MATHML_ENCODINGS = /* @__PURE__ */ new Set(["mathml-content", "application/mathml-content+xml"]);
3888
+ const CONTENT_MATHML_ELEMENTS = /* @__PURE__ */ new Set([
3889
+ "apply",
3890
+ "bind",
3891
+ "ci",
3892
+ "cn",
3893
+ "cs",
3894
+ "csymbol",
3895
+ "cbytes",
3896
+ "cerror",
3897
+ "share",
3898
+ "piecewise",
3899
+ "lambda",
3900
+ "set",
3901
+ "list",
3902
+ "vector",
3903
+ "matrix",
3904
+ "matrixrow",
3905
+ "interval"
3906
+ ]);
3907
+ const contentMathMLNames = [...CONTENT_MATHML_ELEMENTS];
3908
+ try {
3909
+ const annotations = root.find(".//math:annotation-xml", MATH_NS);
3910
+ for (const anno of annotations) {
3911
+ const el = anno;
3912
+ const encoding = this.getAttribute(el, "encoding");
3913
+ const name = this.getAttribute(el, "name");
3914
+ if (encoding) {
3915
+ const encodingLower = encoding.toLowerCase();
3916
+ if (CONTENT_MATHML_ENCODINGS.has(encodingLower)) {
3917
+ if (!name) {
3918
+ pushMessage(context.messages, {
3919
+ id: MessageId.RSC_005,
3920
+ message: 'The "annotation-xml" element with Content MathML encoding must have a "name" attribute with value "contentequiv"',
3921
+ location: { path, line: el.line }
3922
+ });
3923
+ } else if (name !== "contentequiv") {
3924
+ pushMessage(context.messages, {
3925
+ id: MessageId.RSC_005,
3926
+ message: `The "name" attribute on "annotation-xml" with Content MathML encoding must be "contentequiv", but found "${name}"`,
3927
+ location: { path, line: el.line }
3928
+ });
3929
+ }
3930
+ } else {
3931
+ for (const cElemName of contentMathMLNames) {
3932
+ try {
3933
+ const found = el.get(`./math:${cElemName}`, MATH_NS);
3934
+ if (found) {
3935
+ pushMessage(context.messages, {
3936
+ id: MessageId.RSC_005,
3937
+ message: `Content MathML element "${cElemName}" found in annotation-xml with encoding "${encoding}"`,
3938
+ location: { path, line: found.line }
3939
+ });
3940
+ break;
3941
+ }
3942
+ } catch {
3943
+ }
3944
+ }
3945
+ }
3946
+ if (encodingLower === "application/xml+xhtml") {
3947
+ pushMessage(context.messages, {
3948
+ id: MessageId.RSC_005,
3949
+ message: 'The encoding "application/xml+xhtml" is not valid; use "application/xhtml+xml" instead',
3950
+ location: { path, line: el.line }
3951
+ });
3952
+ }
3953
+ }
3954
+ }
3955
+ } catch {
3956
+ }
3957
+ for (const elemName of contentMathMLNames) {
3958
+ try {
3959
+ const found = root.get(`.//math:math/math:${elemName}`, MATH_NS);
3960
+ if (found) {
3961
+ pushMessage(context.messages, {
3962
+ id: MessageId.RSC_005,
3963
+ message: `Content MathML element "${elemName}" must not appear as a direct child of "math"; use "semantics" with "annotation-xml" instead`,
3964
+ location: { path, line: found.line }
3965
+ });
3966
+ break;
3967
+ }
3968
+ } catch {
3969
+ }
3970
+ }
3971
+ }
3972
+ checkReservedNamespace(context, path, content) {
3973
+ const nsPattern = /xmlns:(\w+)="([^"]+)"/g;
3974
+ const STANDARD_PREFIXES = /* @__PURE__ */ new Set([
3975
+ "xml",
3976
+ "xmlns",
3977
+ "xlink",
3978
+ "epub",
3979
+ "ops",
3980
+ "dc",
3981
+ "dcterms",
3982
+ "svg",
3983
+ "math",
3984
+ "ssml",
3985
+ "ev",
3986
+ "xsi"
3987
+ ]);
3988
+ const STANDARD_NAMESPACES = /* @__PURE__ */ new Set([
3989
+ "http://www.w3.org/XML/1998/namespace",
3990
+ "http://www.w3.org/2000/xmlns/",
3991
+ "http://www.w3.org/1999/xhtml",
3992
+ "http://www.w3.org/1999/xlink",
3993
+ "http://www.w3.org/2000/svg",
3994
+ "http://www.w3.org/1998/Math/MathML",
3995
+ "http://www.idpf.org/2007/ops",
3996
+ "http://purl.org/dc/elements/1.1/",
3997
+ "http://purl.org/dc/terms/",
3998
+ "http://www.w3.org/2001/10/synthesis",
3999
+ "http://www.w3.org/2001/xml-events",
4000
+ "http://www.w3.org/2001/XMLSchema-instance"
4001
+ ]);
4002
+ let match;
4003
+ while ((match = nsPattern.exec(content)) !== null) {
4004
+ const prefix = match[1] ?? "";
4005
+ const uri = match[2] ?? "";
4006
+ if (STANDARD_PREFIXES.has(prefix) || STANDARD_NAMESPACES.has(uri)) continue;
4007
+ try {
4008
+ const url = new URL(uri);
4009
+ const host = url.hostname.toLowerCase();
4010
+ for (const reserved of ["w3.org", "idpf.org"]) {
4011
+ if (host.includes(reserved)) {
4012
+ const line = content.substring(0, match.index).split("\n").length;
4013
+ pushMessage(context.messages, {
4014
+ id: MessageId.HTM_054,
4015
+ message: `Custom attribute namespace ("${uri}") must not include the string "${reserved}" in its domain`,
4016
+ location: { path, line }
4017
+ });
4018
+ }
4019
+ }
4020
+ } catch {
4021
+ }
4022
+ }
4023
+ }
4024
+ checkDataAttributes(context, path, root) {
4025
+ const elements = root.find(".//*");
4026
+ const XML_NCNAME_RE = /^[a-z_\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF][a-z0-9._\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF-]*$/;
4027
+ for (const elem of elements) {
4028
+ const el = elem;
4029
+ if (!("attrs" in el)) continue;
4030
+ const attrs = el.attrs;
4031
+ for (const attr of attrs) {
4032
+ if (!attr.name.startsWith("data-")) continue;
4033
+ const suffix = attr.name.substring(5);
4034
+ if (suffix.length === 0 || !XML_NCNAME_RE.test(suffix) || /[A-Z]/.test(attr.name)) {
4035
+ pushMessage(context.messages, {
4036
+ id: MessageId.HTM_061,
4037
+ 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)`,
4038
+ location: { path, line: el.line }
4039
+ });
4040
+ }
4041
+ }
4042
+ }
4043
+ }
4044
+ checkAccessibility(context, path, root) {
4045
+ const links = root.find(".//html:a", { html: "http://www.w3.org/1999/xhtml" });
4046
+ for (const link of links) {
4047
+ if (!this.hasAccessibleContent(link)) {
4048
+ pushMessage(context.messages, {
4049
+ id: MessageId.ACC_004,
4050
+ message: "Hyperlink has no accessible text content",
4051
+ location: { path }
4052
+ });
4053
+ }
4054
+ }
4055
+ const images = root.find(".//html:img", { html: "http://www.w3.org/1999/xhtml" });
4056
+ for (const img of images) {
4057
+ const altAttr = this.getAttribute(img, "alt");
4058
+ if (altAttr === null) {
4059
+ pushMessage(context.messages, {
4060
+ id: MessageId.ACC_005,
4061
+ message: "Image is missing alt attribute",
4062
+ location: { path }
4063
+ });
4064
+ }
4065
+ }
4066
+ this.checkSVGLinkAccessibility(context, path, root);
4067
+ const mathElements = root.find(".//math:math", { math: "http://www.w3.org/1998/Math/MathML" });
4068
+ for (const mathElem of mathElements) {
4069
+ const elem = mathElem;
4070
+ const alttext = elem.attr("alttext");
4071
+ const annotation = elem.get('./math:annotation[@encoding="application/x-tex"]', {
4072
+ math: "http://www.w3.org/1998/Math/MathML"
4073
+ });
4074
+ const ariaLabel = this.getAttribute(elem, "aria-label");
4075
+ if (!alttext?.value && !annotation && !ariaLabel) {
4076
+ pushMessage(context.messages, {
4077
+ id: MessageId.ACC_009,
4078
+ message: "MathML element should have alttext attribute or annotation for accessibility",
4079
+ location: { path }
4080
+ });
4081
+ }
4082
+ }
4083
+ }
4084
+ hasSVGLinkAccessibleName(svgElem) {
4085
+ const ns = { svg: "http://www.w3.org/2000/svg" };
4086
+ if (svgElem.get(".//svg:title", ns)) return true;
4087
+ if (svgElem.get(".//svg:text", ns)) return true;
4088
+ if (this.getAttribute(svgElem, "aria-label")) return true;
4089
+ if (this.getAttribute(svgElem, "xlink:title")) return true;
4090
+ return false;
4091
+ }
4092
+ checkSVGLinkAccessibility(context, path, root) {
4093
+ const svgLinks = root.find(".//svg:a", {
4094
+ svg: "http://www.w3.org/2000/svg",
4095
+ xlink: "http://www.w3.org/1999/xlink"
4096
+ });
4097
+ for (const svgLink of svgLinks) {
4098
+ if (!this.hasSVGLinkAccessibleName(svgLink)) {
4099
+ pushMessage(context.messages, {
4100
+ id: MessageId.ACC_011,
4101
+ message: "SVG hyperlink has no accessible name (missing title element or aria-label)",
4102
+ location: { path }
4103
+ });
4104
+ }
4105
+ }
4106
+ }
4107
+ validateImages(context, path, root) {
4108
+ const packageDoc = context.packageDocument;
4109
+ if (!packageDoc) return;
4110
+ const images = root.find(".//html:img[@src]", { html: "http://www.w3.org/1999/xhtml" });
4111
+ for (const img of images) {
4112
+ const imgElem = img;
4113
+ const srcAttr = this.getAttribute(imgElem, "src");
4114
+ if (!srcAttr) continue;
4115
+ const src = srcAttr;
4116
+ const opfDir = context.opfPath?.includes("/") ? context.opfPath.substring(0, context.opfPath.lastIndexOf("/")) : "";
4117
+ let fullPath = src;
4118
+ if (opfDir && !src.startsWith("http://") && !src.startsWith("https://")) {
4119
+ if (src.startsWith("/")) {
4120
+ fullPath = src.slice(1);
4121
+ } else {
4122
+ const parts = opfDir.split("/");
2995
4123
  const relParts = src.split("/");
2996
4124
  for (const part of relParts) {
2997
4125
  if (part === "..") {
@@ -3033,24 +4161,32 @@ var ContentValidator = class {
3033
4161
  const epubTypeElements = root.find(".//*[@epub:type]", {
3034
4162
  epub: "http://www.idpf.org/2007/ops"
3035
4163
  });
3036
- const knownPrefixes = /* @__PURE__ */ new Set([
3037
- "",
3038
- "http://idpf.org/epub/structure/v1/",
3039
- "http://idpf.org/epub/vocab/structure/",
3040
- "http://www.idpf.org/2007/ops"
3041
- ]);
3042
4164
  for (const elem of epubTypeElements) {
3043
4165
  const elemTyped = elem;
3044
- const epubTypeAttr = elemTyped.attr("epub:type");
4166
+ const epubTypeAttr = elemTyped.attr("type", "epub");
3045
4167
  if (!epubTypeAttr?.value) continue;
3046
- const epubTypeValue = epubTypeAttr.value;
3047
- for (const part of epubTypeValue.split(/\s+/)) {
3048
- const prefix = part.includes(":") ? part.substring(0, part.indexOf(":")) : "";
3049
- if (!knownPrefixes.has(prefix) && !prefix.startsWith("http://") && !prefix.startsWith("https://")) {
4168
+ for (const part of epubTypeAttr.value.split(/\s+/)) {
4169
+ if (!part) continue;
4170
+ const hasPrefix = part.includes(":");
4171
+ const localName = hasPrefix ? part.substring(part.indexOf(":") + 1) : part;
4172
+ if (hasPrefix) continue;
4173
+ if (EPUB_SSV_DEPRECATED.has(localName)) {
4174
+ pushMessage(context.messages, {
4175
+ id: MessageId.OPF_086b,
4176
+ message: `epub:type value "${localName}" is deprecated`,
4177
+ location: { path, line: elem.line }
4178
+ });
4179
+ } else if (EPUB_SSV_DISALLOWED_ON_CONTENT.has(localName)) {
4180
+ pushMessage(context.messages, {
4181
+ id: MessageId.OPF_087,
4182
+ message: `epub:type value "${localName}" is not allowed on documents of type "application/xhtml+xml"`,
4183
+ location: { path, line: elem.line }
4184
+ });
4185
+ } else if (!EPUB_SSV_ALL.has(localName)) {
3050
4186
  pushMessage(context.messages, {
3051
4187
  id: MessageId.OPF_088,
3052
- message: `Unknown epub:type prefix "${prefix}": ${epubTypeValue}`,
3053
- location: { path }
4188
+ message: `Unrecognized epub:type value "${localName}"`,
4189
+ location: { path, line: elem.line }
3054
4190
  });
3055
4191
  }
3056
4192
  }
@@ -3125,40 +4261,119 @@ var ContentValidator = class {
3125
4261
  return attr?.value ?? null;
3126
4262
  }
3127
4263
  validateViewportMeta(context, path, root, manifestItem) {
3128
- const isFixedLayout = manifestItem?.properties?.includes("fixed-layout");
3129
- const metaTags = root.find(".//html:meta[@name]", { html: "http://www.w3.org/1999/xhtml" });
3130
- let hasViewportMeta = false;
3131
- for (const meta of metaTags) {
4264
+ const packageDoc = context.packageDocument;
4265
+ const isFixedLayout = manifestItem && packageDoc ? isItemFixedLayout(packageDoc, manifestItem.id) : false;
4266
+ const headMetas = root.find(".//html:head/html:meta[@name]", {
4267
+ html: "http://www.w3.org/1999/xhtml"
4268
+ });
4269
+ let viewportCount = 0;
4270
+ for (const meta of headMetas) {
3132
4271
  const nameAttr = this.getAttribute(meta, "name");
3133
- if (nameAttr === "viewport") {
3134
- hasViewportMeta = true;
3135
- const contentAttr = this.getAttribute(meta, "content");
3136
- if (isFixedLayout) {
3137
- if (!contentAttr) {
3138
- pushMessage(context.messages, {
3139
- id: MessageId.HTM_046,
3140
- message: "Viewport meta element should have a content attribute in fixed-layout documents",
3141
- location: { path }
3142
- });
3143
- continue;
3144
- }
3145
- } else {
3146
- pushMessage(context.messages, {
3147
- id: MessageId.HTM_060b,
3148
- message: `EPUB reading systems must ignore viewport meta elements in reflowable documents; viewport declaration "${contentAttr ?? ""}" will be ignored`,
3149
- location: { path }
3150
- });
3151
- }
4272
+ if (nameAttr !== "viewport") continue;
4273
+ viewportCount++;
4274
+ const contentAttr = this.getAttribute(meta, "content");
4275
+ if (!isFixedLayout) {
4276
+ pushMessage(context.messages, {
4277
+ id: MessageId.HTM_060b,
4278
+ message: `EPUB reading systems must ignore viewport meta elements in reflowable documents; viewport declaration "${contentAttr ?? ""}" will be ignored`,
4279
+ location: { path, line: meta.line }
4280
+ });
4281
+ continue;
3152
4282
  }
4283
+ if (viewportCount > 1) {
4284
+ pushMessage(context.messages, {
4285
+ id: MessageId.HTM_060a,
4286
+ message: `EPUB reading systems must ignore secondary viewport meta elements in fixed-layout documents; viewport declaration "${contentAttr ?? ""}" will be ignored`,
4287
+ location: { path, line: meta.line }
4288
+ });
4289
+ continue;
4290
+ }
4291
+ if (!contentAttr?.trim()) {
4292
+ pushMessage(context.messages, {
4293
+ id: MessageId.HTM_047,
4294
+ message: `Viewport metadata "${contentAttr ?? ""}" has a syntax error`,
4295
+ location: { path, line: meta.line }
4296
+ });
4297
+ continue;
4298
+ }
4299
+ this.parseViewportContent(context, path, contentAttr, meta.line);
3153
4300
  }
3154
- if (isFixedLayout && !hasViewportMeta) {
4301
+ if (isFixedLayout && viewportCount === 0) {
3155
4302
  pushMessage(context.messages, {
3156
- id: MessageId.HTM_049,
3157
- message: "Fixed-layout document should include a viewport meta element",
4303
+ id: MessageId.HTM_046,
4304
+ message: "Fixed layout document has no viewport meta element",
3158
4305
  location: { path }
3159
4306
  });
3160
4307
  }
3161
4308
  }
4309
+ parseViewportContent(context, path, content, line) {
4310
+ const location = line != null ? { path, line } : { path };
4311
+ const parts = content.split(/[,;]/);
4312
+ const seenKeys = /* @__PURE__ */ new Set();
4313
+ let hasWidth = false;
4314
+ let hasHeight = false;
4315
+ let hasSyntaxError = false;
4316
+ for (const part of parts) {
4317
+ const trimmed = part.trim();
4318
+ if (!trimmed) continue;
4319
+ const eqIndex = trimmed.indexOf("=");
4320
+ let key;
4321
+ let value;
4322
+ if (eqIndex < 0) {
4323
+ key = trimmed;
4324
+ value = "";
4325
+ } else {
4326
+ key = trimmed.substring(0, eqIndex).trim();
4327
+ const rawValue = trimmed.substring(eqIndex + 1);
4328
+ if (!rawValue.trim()) {
4329
+ pushMessage(context.messages, {
4330
+ id: MessageId.HTM_047,
4331
+ message: `Viewport metadata "${content}" has a syntax error`,
4332
+ location
4333
+ });
4334
+ hasSyntaxError = true;
4335
+ break;
4336
+ }
4337
+ value = rawValue.trim();
4338
+ }
4339
+ if (key === "width" || key === "height") {
4340
+ if (seenKeys.has(key)) {
4341
+ pushMessage(context.messages, {
4342
+ id: MessageId.HTM_059,
4343
+ message: `Viewport "${key}" property must not be defined more than once`,
4344
+ location
4345
+ });
4346
+ }
4347
+ seenKeys.add(key);
4348
+ if (key === "width") hasWidth = true;
4349
+ if (key === "height") hasHeight = true;
4350
+ const deviceKeyword = key === "width" ? "device-width" : "device-height";
4351
+ if (value === deviceKeyword) ; else if (value === "" || !/^[0-9]*\.?[0-9]+$/.test(value)) {
4352
+ pushMessage(context.messages, {
4353
+ id: MessageId.HTM_057,
4354
+ message: `Viewport "${key}" value must be a positive number or the keyword "${deviceKeyword}"`,
4355
+ location
4356
+ });
4357
+ }
4358
+ }
4359
+ }
4360
+ if (!hasSyntaxError) {
4361
+ if (!hasWidth) {
4362
+ pushMessage(context.messages, {
4363
+ id: MessageId.HTM_056,
4364
+ message: 'Viewport metadata has no "width" dimension (both "width" and "height" properties are required)',
4365
+ location
4366
+ });
4367
+ }
4368
+ if (!hasHeight) {
4369
+ pushMessage(context.messages, {
4370
+ id: MessageId.HTM_056,
4371
+ message: 'Viewport metadata has no "height" dimension (both "width" and "height" properties are required)',
4372
+ location
4373
+ });
4374
+ }
4375
+ }
4376
+ }
3162
4377
  extractAndRegisterIDs(path, root, registry) {
3163
4378
  const elementsWithId = root.find(".//*[@id]");
3164
4379
  for (const elem of elementsWithId) {
@@ -3190,7 +4405,8 @@ var ContentValidator = class {
3190
4405
  else if (types.includes("page-list")) refType = "nav-pagelist-link" /* NAV_PAGELIST_LINK */;
3191
4406
  const navAnchors = navElem.find(".//html:a[@href]", HTML_NS);
3192
4407
  for (const a of navAnchors) {
3193
- navAnchorTypes.set(a.line, refType);
4408
+ const anchorHref = this.getAttribute(a, "href") ?? "";
4409
+ navAnchorTypes.set(`${String(a.line)}:${anchorHref}`, refType);
3194
4410
  }
3195
4411
  }
3196
4412
  }
@@ -3207,11 +4423,18 @@ var ContentValidator = class {
3207
4423
  continue;
3208
4424
  }
3209
4425
  const line = link.line;
3210
- const refType = isNavDocument ? navAnchorTypes.get(line) ?? "hyperlink" /* HYPERLINK */ : "hyperlink" /* HYPERLINK */;
3211
- if (href.startsWith("http://") || href.startsWith("https://")) {
4426
+ const refType = isNavDocument ? navAnchorTypes.get(`${String(line)}:${href}`) ?? "hyperlink" /* HYPERLINK */ : "hyperlink" /* HYPERLINK */;
4427
+ if (href.startsWith("data:") || href.startsWith("file:")) {
4428
+ refValidator.addReference({
4429
+ url: href,
4430
+ targetResource: href,
4431
+ type: refType,
4432
+ location: { path, line }
4433
+ });
3212
4434
  continue;
3213
4435
  }
3214
- if (href.startsWith("mailto:") || href.startsWith("tel:")) {
4436
+ if (ABSOLUTE_URI_RE.test(href)) {
4437
+ validateAbsoluteHyperlinkURL(context, href, path, line);
3215
4438
  continue;
3216
4439
  }
3217
4440
  if (href.includes("#epubcfi(")) {
@@ -3249,8 +4472,19 @@ var ContentValidator = class {
3249
4472
  const href = this.getAttribute(area, "href")?.trim();
3250
4473
  if (!href) continue;
3251
4474
  const line = area.line;
3252
- if (href.startsWith("http://") || href.startsWith("https://")) continue;
3253
- if (href.startsWith("mailto:") || href.startsWith("tel:")) continue;
4475
+ if (href.startsWith("data:") || href.startsWith("file:")) {
4476
+ refValidator.addReference({
4477
+ url: href,
4478
+ targetResource: href,
4479
+ type: "hyperlink" /* HYPERLINK */,
4480
+ location: { path, line }
4481
+ });
4482
+ continue;
4483
+ }
4484
+ if (ABSOLUTE_URI_RE.test(href)) {
4485
+ validateAbsoluteHyperlinkURL(context, href, path, line);
4486
+ continue;
4487
+ }
3254
4488
  if (href.includes("#epubcfi(")) continue;
3255
4489
  if (href.startsWith("#")) {
3256
4490
  refValidator.addReference({
@@ -3286,7 +4520,17 @@ var ContentValidator = class {
3286
4520
  const href = this.getAttribute(elem, "xlink:href") ?? this.getAttribute(elem, "href");
3287
4521
  if (!href) continue;
3288
4522
  const line = link.line;
3289
- if (href.startsWith("http://") || href.startsWith("https://")) {
4523
+ if (href.startsWith("data:") || href.startsWith("file:")) {
4524
+ refValidator.addReference({
4525
+ url: href,
4526
+ targetResource: href,
4527
+ type: "hyperlink" /* HYPERLINK */,
4528
+ location: { path, line }
4529
+ });
4530
+ continue;
4531
+ }
4532
+ if (ABSOLUTE_URI_RE.test(href)) {
4533
+ validateAbsoluteHyperlinkURL(context, href, path, line);
3290
4534
  continue;
3291
4535
  }
3292
4536
  if (href.startsWith("#")) {
@@ -3317,16 +4561,19 @@ var ContentValidator = class {
3317
4561
  refValidator.addReference(svgRef);
3318
4562
  }
3319
4563
  }
3320
- extractAndRegisterStylesheets(path, root, opfDir, refValidator) {
4564
+ extractAndRegisterStylesheets(context, path, root, opfDir, refValidator) {
3321
4565
  const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
4566
+ const baseElem = root.get(".//html:base[@href]", { html: "http://www.w3.org/1999/xhtml" });
4567
+ const baseHref = baseElem ? this.getAttribute(baseElem, "href") : null;
4568
+ const remoteBaseUrl = baseHref?.startsWith("http://") || baseHref?.startsWith("https://") ? baseHref : null;
3322
4569
  const linkElements = root.find(".//html:link[@href]", { html: "http://www.w3.org/1999/xhtml" });
3323
4570
  for (const linkElem of linkElements) {
3324
4571
  const href = this.getAttribute(linkElem, "href");
3325
4572
  const rel = this.getAttribute(linkElem, "rel");
3326
4573
  if (!href) continue;
4574
+ if (!rel?.toLowerCase().includes("stylesheet")) continue;
3327
4575
  const line = linkElem.line;
3328
- const isStylesheet = rel?.toLowerCase().includes("stylesheet");
3329
- const type = isStylesheet ? "stylesheet" /* STYLESHEET */ : "link" /* LINK */;
4576
+ const type = "stylesheet" /* STYLESHEET */;
3330
4577
  if (href.startsWith("http://") || href.startsWith("https://")) {
3331
4578
  refValidator.addReference({
3332
4579
  url: href,
@@ -3336,6 +4583,15 @@ var ContentValidator = class {
3336
4583
  });
3337
4584
  continue;
3338
4585
  }
4586
+ if (remoteBaseUrl && !ABSOLUTE_URI_RE.test(href)) {
4587
+ const resolvedUrl = new URL(href, remoteBaseUrl).href;
4588
+ pushMessage(context.messages, {
4589
+ id: MessageId.RSC_006,
4590
+ message: `Remote resource reference is not allowed; resource "${resolvedUrl}" must be located in the EPUB container`,
4591
+ location: { path, line }
4592
+ });
4593
+ continue;
4594
+ }
3339
4595
  const resolvedPath = this.resolveRelativePath(docDir, href, opfDir);
3340
4596
  const hashIndex = resolvedPath.indexOf("#");
3341
4597
  const targetResource = hashIndex >= 0 ? resolvedPath.slice(0, hashIndex) : resolvedPath;
@@ -3481,56 +4737,18 @@ var ContentValidator = class {
3481
4737
  const hashIndex = resolvedPath.indexOf("#");
3482
4738
  const targetResource = hashIndex >= 0 ? resolvedPath.slice(0, hashIndex) : resolvedPath;
3483
4739
  const fragment = hashIndex >= 0 ? resolvedPath.slice(hashIndex + 1) : void 0;
3484
- const svgImgRef = {
3485
- url: href,
3486
- targetResource,
3487
- type: "image" /* IMAGE */,
3488
- location: { path, line }
3489
- };
3490
- if (fragment) {
3491
- svgImgRef.fragment = fragment;
3492
- }
3493
- refValidator.addReference(svgImgRef);
3494
- }
3495
- try {
3496
- const svgUseXlink = root.find(".//svg:use[@xlink:href]", {
3497
- svg: "http://www.w3.org/2000/svg",
3498
- xlink: "http://www.w3.org/1999/xlink"
3499
- });
3500
- const svgUseHref = root.find(".//svg:use[@href]", {
3501
- svg: "http://www.w3.org/2000/svg"
3502
- });
3503
- for (const useNode of [...svgUseXlink, ...svgUseHref]) {
3504
- const useElem = useNode;
3505
- const href = this.getAttribute(useElem, "xlink:href") ?? this.getAttribute(useElem, "href");
3506
- if (href === null) continue;
3507
- const line = useNode.line;
3508
- if (href.startsWith("http://") || href.startsWith("https://")) continue;
3509
- if (href === "" || !href.includes("#")) {
3510
- pushMessage(context.messages, {
3511
- id: MessageId.RSC_015,
3512
- message: `SVG "use" element requires a fragment identifier, but found "${href}"`,
3513
- location: { path, line }
3514
- });
3515
- continue;
3516
- }
3517
- const resolvedPath = this.resolveRelativePath(docDir, href, opfDir);
3518
- const hashIndex = resolvedPath.indexOf("#");
3519
- const targetResource = hashIndex >= 0 ? resolvedPath.slice(0, hashIndex) : path;
3520
- const fragment = hashIndex >= 0 ? resolvedPath.slice(hashIndex + 1) : void 0;
3521
- const useRef = {
3522
- url: href,
3523
- targetResource,
3524
- type: "svg-symbol" /* SVG_SYMBOL */,
3525
- location: { path, line }
3526
- };
3527
- if (fragment) {
3528
- useRef.fragment = fragment;
3529
- }
3530
- refValidator.addReference(useRef);
4740
+ const svgImgRef = {
4741
+ url: href,
4742
+ targetResource,
4743
+ type: "image" /* IMAGE */,
4744
+ location: { path, line }
4745
+ };
4746
+ if (fragment) {
4747
+ svgImgRef.fragment = fragment;
3531
4748
  }
3532
- } catch {
4749
+ refValidator.addReference(svgImgRef);
3533
4750
  }
4751
+ this.extractSVGUseReferences(context, path, root, docDir, opfDir, refValidator);
3534
4752
  const videos = root.find(".//html:video[@poster]", { html: "http://www.w3.org/1999/xhtml" });
3535
4753
  for (const video of videos) {
3536
4754
  const poster = this.getAttribute(video, "poster");
@@ -3830,12 +5048,8 @@ var ContentValidator = class {
3830
5048
  const targetResource = hashIndex >= 0 ? resolvedPath.slice(0, hashIndex) : resolvedPath;
3831
5049
  const resource = registry.getResource(targetResource);
3832
5050
  if (!resource) return;
3833
- const stripParams = (t) => {
3834
- const idx = t.indexOf(";");
3835
- return (idx >= 0 ? t.substring(0, idx) : t).trim();
3836
- };
3837
- const declaredType = stripParams(typeAttr);
3838
- const manifestType = stripParams(resource.mimeType);
5051
+ const declaredType = stripMimeParams(typeAttr);
5052
+ const manifestType = stripMimeParams(resource.mimeType);
3839
5053
  if (declaredType && declaredType !== manifestType) {
3840
5054
  pushMessage(context.messages, {
3841
5055
  id: MessageId.OPF_013,
@@ -3912,12 +5126,8 @@ var ContentValidator = class {
3912
5126
  const resolvedPath2 = this.resolveRelativePath(docDir, sourceUrl, opfDir);
3913
5127
  const resource2 = registry.getResource(resolvedPath2);
3914
5128
  if (resource2) {
3915
- const stripParams = (t) => {
3916
- const idx = t.indexOf(";");
3917
- return (idx >= 0 ? t.substring(0, idx) : t).trim();
3918
- };
3919
- const declaredType = stripParams(typeAttr);
3920
- const manifestType = stripParams(resource2.mimeType);
5129
+ const declaredType = stripMimeParams(typeAttr);
5130
+ const manifestType = stripMimeParams(resource2.mimeType);
3921
5131
  if (declaredType && declaredType !== manifestType) {
3922
5132
  pushMessage(context.messages, {
3923
5133
  id: MessageId.OPF_013,
@@ -4779,9 +5989,17 @@ var OCFValidator = class {
4779
5989
  const xml = block[0];
4780
5990
  const algorithmMatch = /Algorithm=["']([^"']+)["']/.exec(xml);
4781
5991
  const uriMatch = /<(?:\w+:)?CipherReference[^>]+URI=["']([^"']+)["']/.exec(xml);
4782
- if (algorithmMatch?.[1] === IDPF_OBFUSCATION && uriMatch?.[1]) {
4783
- obfuscated.add(uriMatch[1]);
5992
+ const algorithm = algorithmMatch?.[1];
5993
+ const uri = uriMatch?.[1];
5994
+ if (!uri) continue;
5995
+ if (algorithm === IDPF_OBFUSCATION) {
5996
+ obfuscated.add(uri);
4784
5997
  }
5998
+ pushMessage(context.messages, {
5999
+ id: MessageId.RSC_004,
6000
+ message: `File "${uri}" is encrypted, its content will not be checked`,
6001
+ location: { path: encryptionPath }
6002
+ });
4785
6003
  }
4786
6004
  if (obfuscated.size > 0) {
4787
6005
  context.obfuscatedResources = obfuscated;
@@ -5063,6 +6281,9 @@ function parseSpine(spineXml, spineAttrs) {
5063
6281
  idref,
5064
6282
  linear: attrs.linear !== "no"
5065
6283
  };
6284
+ if (attrs.id) {
6285
+ itemref.id = attrs.id.trim();
6286
+ }
5066
6287
  if (attrs.properties) {
5067
6288
  itemref.properties = attrs.properties.split(/\s+/);
5068
6289
  }
@@ -5101,9 +6322,14 @@ function parseAttributes(attrsStr) {
5101
6322
  const name = match[1];
5102
6323
  const value = match[2];
5103
6324
  if (name !== void 0 && value !== void 0) {
6325
+ attrs[name] = value;
5104
6326
  const colonIdx = name.indexOf(":");
5105
- const localName = colonIdx >= 0 ? name.slice(colonIdx + 1) : name;
5106
- attrs[localName] = value;
6327
+ if (colonIdx >= 0) {
6328
+ const localName = name.slice(colonIdx + 1);
6329
+ if (!(localName in attrs)) {
6330
+ attrs[localName] = value;
6331
+ }
6332
+ }
5107
6333
  }
5108
6334
  }
5109
6335
  return attrs;
@@ -5149,6 +6375,378 @@ function parseCollections(xml) {
5149
6375
  }
5150
6376
 
5151
6377
  // src/opf/validator.ts
6378
+ var VALID_VERSIONS = /* @__PURE__ */ new Set(["2.0", "3.0", "3.1", "3.2", "3.3"]);
6379
+ var DEPRECATED_MEDIA_TYPES = /* @__PURE__ */ new Set([
6380
+ "text/x-oeb1-document",
6381
+ "text/x-oeb1-css",
6382
+ "application/x-oeb1-package",
6383
+ "text/x-oeb1-html"
6384
+ ]);
6385
+ var VALID_RELATOR_CODES = /* @__PURE__ */ new Set([
6386
+ "abr",
6387
+ "acp",
6388
+ "act",
6389
+ "adi",
6390
+ "adp",
6391
+ "aft",
6392
+ "anl",
6393
+ "anm",
6394
+ "ann",
6395
+ "ant",
6396
+ "ape",
6397
+ "apl",
6398
+ "app",
6399
+ "aqt",
6400
+ "arc",
6401
+ "ard",
6402
+ "arr",
6403
+ "art",
6404
+ "asg",
6405
+ "asn",
6406
+ "ato",
6407
+ "att",
6408
+ "auc",
6409
+ "aud",
6410
+ "aui",
6411
+ "aus",
6412
+ "aut",
6413
+ "bdd",
6414
+ "bjd",
6415
+ "bkd",
6416
+ "bkp",
6417
+ "blw",
6418
+ "bnd",
6419
+ "bpd",
6420
+ "brd",
6421
+ "brl",
6422
+ "bsl",
6423
+ "cas",
6424
+ "ccp",
6425
+ "chr",
6426
+ "clb",
6427
+ "cli",
6428
+ "cll",
6429
+ "clr",
6430
+ "clt",
6431
+ "cmm",
6432
+ "cmp",
6433
+ "cmt",
6434
+ "cnd",
6435
+ "cng",
6436
+ "cns",
6437
+ "coe",
6438
+ "col",
6439
+ "com",
6440
+ "con",
6441
+ "cor",
6442
+ "cos",
6443
+ "cot",
6444
+ "cou",
6445
+ "cov",
6446
+ "cpc",
6447
+ "cpe",
6448
+ "cph",
6449
+ "cpl",
6450
+ "cpt",
6451
+ "cre",
6452
+ "crp",
6453
+ "crr",
6454
+ "crt",
6455
+ "csl",
6456
+ "csp",
6457
+ "cst",
6458
+ "ctb",
6459
+ "cte",
6460
+ "ctg",
6461
+ "ctr",
6462
+ "cts",
6463
+ "ctt",
6464
+ "cur",
6465
+ "cwt",
6466
+ "dbp",
6467
+ "dfd",
6468
+ "dfe",
6469
+ "dft",
6470
+ "dgc",
6471
+ "dgg",
6472
+ "dgs",
6473
+ "dis",
6474
+ "dln",
6475
+ "dnc",
6476
+ "dnr",
6477
+ "dpc",
6478
+ "dpt",
6479
+ "drm",
6480
+ "drt",
6481
+ "dsr",
6482
+ "dst",
6483
+ "dtc",
6484
+ "dte",
6485
+ "dtm",
6486
+ "dto",
6487
+ "dub",
6488
+ "edc",
6489
+ "edm",
6490
+ "edt",
6491
+ "egr",
6492
+ "elg",
6493
+ "elt",
6494
+ "eng",
6495
+ "enj",
6496
+ "etr",
6497
+ "evp",
6498
+ "exp",
6499
+ "fac",
6500
+ "fds",
6501
+ "fld",
6502
+ "flm",
6503
+ "fmd",
6504
+ "fmk",
6505
+ "fmo",
6506
+ "fmp",
6507
+ "fnd",
6508
+ "fpy",
6509
+ "frg",
6510
+ "gis",
6511
+ "grt",
6512
+ "his",
6513
+ "hnr",
6514
+ "hst",
6515
+ "ill",
6516
+ "ilu",
6517
+ "ins",
6518
+ "inv",
6519
+ "isb",
6520
+ "itr",
6521
+ "ive",
6522
+ "ivr",
6523
+ "jud",
6524
+ "jug",
6525
+ "lbr",
6526
+ "lbt",
6527
+ "ldr",
6528
+ "led",
6529
+ "lee",
6530
+ "lel",
6531
+ "len",
6532
+ "let",
6533
+ "lgd",
6534
+ "lie",
6535
+ "lil",
6536
+ "lit",
6537
+ "lsa",
6538
+ "lse",
6539
+ "lso",
6540
+ "ltg",
6541
+ "lyr",
6542
+ "mcp",
6543
+ "mdc",
6544
+ "med",
6545
+ "mfp",
6546
+ "mfr",
6547
+ "mod",
6548
+ "mon",
6549
+ "mrb",
6550
+ "mrk",
6551
+ "msd",
6552
+ "mte",
6553
+ "mtk",
6554
+ "mus",
6555
+ "nrt",
6556
+ "opn",
6557
+ "org",
6558
+ "orm",
6559
+ "osp",
6560
+ "oth",
6561
+ "own",
6562
+ "pad",
6563
+ "pan",
6564
+ "pat",
6565
+ "pbd",
6566
+ "pbl",
6567
+ "pdr",
6568
+ "pfr",
6569
+ "pht",
6570
+ "plt",
6571
+ "pma",
6572
+ "pmn",
6573
+ "pop",
6574
+ "ppm",
6575
+ "ppt",
6576
+ "pra",
6577
+ "prc",
6578
+ "prd",
6579
+ "pre",
6580
+ "prf",
6581
+ "prg",
6582
+ "prm",
6583
+ "prn",
6584
+ "pro",
6585
+ "prp",
6586
+ "prs",
6587
+ "prt",
6588
+ "prv",
6589
+ "pta",
6590
+ "pte",
6591
+ "ptf",
6592
+ "pth",
6593
+ "ptt",
6594
+ "pup",
6595
+ "rbr",
6596
+ "rcd",
6597
+ "rce",
6598
+ "rcp",
6599
+ "rdd",
6600
+ "red",
6601
+ "ren",
6602
+ "res",
6603
+ "rev",
6604
+ "rpc",
6605
+ "rps",
6606
+ "rpt",
6607
+ "rpy",
6608
+ "rse",
6609
+ "rsg",
6610
+ "rsp",
6611
+ "rsr",
6612
+ "rst",
6613
+ "rth",
6614
+ "rtm",
6615
+ "sad",
6616
+ "sce",
6617
+ "scl",
6618
+ "scr",
6619
+ "sds",
6620
+ "sec",
6621
+ "sgd",
6622
+ "sgn",
6623
+ "sht",
6624
+ "sll",
6625
+ "sng",
6626
+ "spk",
6627
+ "spn",
6628
+ "spy",
6629
+ "srv",
6630
+ "std",
6631
+ "stg",
6632
+ "stl",
6633
+ "stm",
6634
+ "stn",
6635
+ "str",
6636
+ "tcd",
6637
+ "tch",
6638
+ "ths",
6639
+ "tld",
6640
+ "tlp",
6641
+ "trc",
6642
+ "trl",
6643
+ "tyd",
6644
+ "tyg",
6645
+ "uvp",
6646
+ "vac",
6647
+ "vdg",
6648
+ "voc",
6649
+ "wac",
6650
+ "wal",
6651
+ "wam",
6652
+ "wat",
6653
+ "wdc",
6654
+ "wde",
6655
+ "win",
6656
+ "wit",
6657
+ "wpr",
6658
+ "wst"
6659
+ ]);
6660
+ var DEPRECATED_LINK_REL = /* @__PURE__ */ new Set([
6661
+ "marc21xml-record",
6662
+ "mods-record",
6663
+ "onix-record",
6664
+ "xmp-record",
6665
+ "xml-signature"
6666
+ ]);
6667
+ var EXCLUSIVE_SPINE_GROUPS = [
6668
+ ["rendition:layout-reflowable", "rendition:layout-pre-paginated"],
6669
+ [
6670
+ "rendition:orientation-auto",
6671
+ "rendition:orientation-landscape",
6672
+ "rendition:orientation-portrait"
6673
+ ],
6674
+ [
6675
+ "rendition:spread-auto",
6676
+ "rendition:spread-both",
6677
+ "rendition:spread-landscape",
6678
+ "rendition:spread-none",
6679
+ "rendition:spread-portrait"
6680
+ ],
6681
+ ["page-spread-left", "page-spread-right", "rendition:page-spread-center"],
6682
+ [
6683
+ "rendition:flow-auto",
6684
+ "rendition:flow-paginated",
6685
+ "rendition:flow-scrolled-continuous",
6686
+ "rendition:flow-scrolled-doc"
6687
+ ]
6688
+ ];
6689
+ var RENDITION_META_RULES = [
6690
+ {
6691
+ property: "rendition:layout",
6692
+ allowedValues: /* @__PURE__ */ new Set(["reflowable", "pre-paginated"]),
6693
+ forbidRefines: true
6694
+ },
6695
+ {
6696
+ property: "rendition:orientation",
6697
+ allowedValues: /* @__PURE__ */ new Set(["landscape", "portrait", "auto"]),
6698
+ forbidRefines: true
6699
+ },
6700
+ {
6701
+ property: "rendition:spread",
6702
+ allowedValues: /* @__PURE__ */ new Set(["none", "landscape", "portrait", "both", "auto"]),
6703
+ forbidRefines: true,
6704
+ deprecatedValues: /* @__PURE__ */ new Set(["portrait"])
6705
+ },
6706
+ {
6707
+ property: "rendition:flow",
6708
+ allowedValues: /* @__PURE__ */ new Set(["paginated", "scrolled-continuous", "scrolled-doc", "auto"]),
6709
+ forbidRefines: true
6710
+ },
6711
+ {
6712
+ property: "rendition:viewport",
6713
+ deprecated: true,
6714
+ allowedValues: /* @__PURE__ */ new Set(),
6715
+ validateSyntax: (v) => /^(width=\d+,\s*height=\d+|height=\d+,\s*width=\d+)$/.test(v)
6716
+ }
6717
+ ];
6718
+ var KNOWN_RENDITION_META_PROPERTIES = new Set(
6719
+ RENDITION_META_RULES.map((r) => r.property.slice("rendition:".length))
6720
+ );
6721
+ var SMIL3_CLOCK_RE = /^([0-9]+:[0-5][0-9]:[0-5][0-9](\.[0-9]+)?|[0-5][0-9]:[0-5][0-9](\.[0-9]+)?|[0-9]+(\.[0-9]+)?(h|min|s|ms)?)$/;
6722
+ var GRANDFATHERED_LANG_TAGS = /* @__PURE__ */ new Set([
6723
+ "en-GB-oed",
6724
+ "i-ami",
6725
+ "i-bnn",
6726
+ "i-default",
6727
+ "i-enochian",
6728
+ "i-hak",
6729
+ "i-klingon",
6730
+ "i-lux",
6731
+ "i-mingo",
6732
+ "i-navajo",
6733
+ "i-pwn",
6734
+ "i-tao",
6735
+ "i-tay",
6736
+ "i-tsu",
6737
+ "sgn-BE-FR",
6738
+ "sgn-BE-NL",
6739
+ "sgn-CH-DE",
6740
+ "art-lojban",
6741
+ "cel-gaulish",
6742
+ "no-bok",
6743
+ "no-nyn",
6744
+ "zh-guoyu",
6745
+ "zh-hakka",
6746
+ "zh-min",
6747
+ "zh-min-nan",
6748
+ "zh-xiang"
6749
+ ]);
5152
6750
  var OPFValidator = class {
5153
6751
  packageDoc = null;
5154
6752
  manifestById = /* @__PURE__ */ new Map();
@@ -5210,13 +6808,7 @@ var OPFValidator = class {
5210
6808
  if (this.packageDoc.xmlLangs) {
5211
6809
  for (const lang of this.packageDoc.xmlLangs) {
5212
6810
  if (lang === "") continue;
5213
- if (lang !== lang.trim()) {
5214
- pushMessage(context.messages, {
5215
- id: MessageId.OPF_092,
5216
- message: `Language tag "${lang}" is not well-formed`,
5217
- location: { path: opfPath }
5218
- });
5219
- } else if (!isValidLanguageTag(lang)) {
6811
+ if (lang !== lang.trim() || !isValidLanguageTag(lang)) {
5220
6812
  pushMessage(context.messages, {
5221
6813
  id: MessageId.OPF_092,
5222
6814
  message: `Language tag "${lang}" is not well-formed`,
@@ -5242,11 +6834,10 @@ var OPFValidator = class {
5242
6834
  */
5243
6835
  validatePackageAttributes(context, opfPath) {
5244
6836
  if (!this.packageDoc) return;
5245
- const validVersions = /* @__PURE__ */ new Set(["2.0", "3.0", "3.1", "3.2", "3.3"]);
5246
- if (!validVersions.has(this.packageDoc.version)) {
6837
+ if (!VALID_VERSIONS.has(this.packageDoc.version)) {
5247
6838
  pushMessage(context.messages, {
5248
6839
  id: MessageId.OPF_001,
5249
- message: `Invalid package version "${this.packageDoc.version}"; must be one of: ${Array.from(validVersions).join(", ")}`,
6840
+ message: `Invalid package version "${this.packageDoc.version}"; must be one of: ${Array.from(VALID_VERSIONS).join(", ")}`,
5250
6841
  location: { path: opfPath }
5251
6842
  });
5252
6843
  }
@@ -5322,280 +6913,539 @@ var OPFValidator = class {
5322
6913
  location: { path: opfPath }
5323
6914
  });
5324
6915
  }
5325
- }
5326
- if (dc.name === "date" && dc.value) {
5327
- if (!isValidW3CDateFormat(dc.value)) {
5328
- pushMessage(context.messages, {
5329
- id: MessageId.OPF_053,
5330
- message: `Invalid date format "${dc.value}"; must be W3C date format (ISO 8601)`,
5331
- location: { path: opfPath }
5332
- });
6916
+ }
6917
+ if (dc.name === "date" && dc.value) {
6918
+ if (!isValidW3CDateFormat(dc.value)) {
6919
+ pushMessage(context.messages, {
6920
+ id: MessageId.OPF_053,
6921
+ message: `Invalid date format "${dc.value}"; must be W3C date format (ISO 8601)`,
6922
+ location: { path: opfPath }
6923
+ });
6924
+ }
6925
+ }
6926
+ if (dc.name === "identifier" && dc.value) {
6927
+ const val = dc.value.trim();
6928
+ if (val.startsWith("urn:uuid:")) {
6929
+ const uuid = val.substring(9);
6930
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(uuid)) {
6931
+ pushMessage(context.messages, {
6932
+ id: MessageId.OPF_085,
6933
+ message: `Invalid UUID value "${uuid}"`,
6934
+ location: { path: opfPath }
6935
+ });
6936
+ }
6937
+ }
6938
+ }
6939
+ if (dc.name === "creator" && dc.attributes) {
6940
+ const role = dc.attributes["opf:role"];
6941
+ if (role && !VALID_RELATOR_CODES.has(role) && !role.startsWith("oth.")) {
6942
+ pushMessage(context.messages, {
6943
+ id: MessageId.OPF_052,
6944
+ message: `Invalid role value "${role}" in dc:creator`,
6945
+ location: { path: opfPath }
6946
+ });
6947
+ }
6948
+ }
6949
+ }
6950
+ if (this.packageDoc.version !== "2.0") {
6951
+ for (const meta of this.packageDoc.metaElements) {
6952
+ if (meta.property && /\s/.test(meta.property.trim())) {
6953
+ pushMessage(context.messages, {
6954
+ id: MessageId.OPF_025,
6955
+ message: `Property value must be a single value, not a list: "${meta.property}"`,
6956
+ location: { path: opfPath }
6957
+ });
6958
+ }
6959
+ if (meta.scheme && /\s/.test(meta.scheme.trim())) {
6960
+ pushMessage(context.messages, {
6961
+ id: MessageId.OPF_025,
6962
+ message: `Scheme value must be a single value, not a list: "${meta.scheme}"`,
6963
+ location: { path: opfPath }
6964
+ });
6965
+ }
6966
+ if (meta.property && !/\s/.test(meta.property.trim())) {
6967
+ const prop = meta.property.trim();
6968
+ if (prop.includes(":") && /:\s*$/.test(prop)) {
6969
+ pushMessage(context.messages, {
6970
+ id: MessageId.OPF_026,
6971
+ message: `Malformed property name: "${prop}"`,
6972
+ location: { path: opfPath }
6973
+ });
6974
+ }
6975
+ }
6976
+ if (meta.scheme) {
6977
+ const scheme = meta.scheme.trim();
6978
+ if (scheme && !scheme.includes(":")) {
6979
+ pushMessage(context.messages, {
6980
+ id: MessageId.OPF_027,
6981
+ message: `Undefined property: "${scheme}"`,
6982
+ location: { path: opfPath }
6983
+ });
6984
+ }
6985
+ }
6986
+ }
6987
+ }
6988
+ if (this.packageDoc.version !== "2.0") {
6989
+ const allIds = /* @__PURE__ */ new Set();
6990
+ for (const dc of dcElements) {
6991
+ if (dc.id) allIds.add(dc.id);
6992
+ }
6993
+ for (const meta of this.packageDoc.metaElements) {
6994
+ if (meta.id) allIds.add(meta.id);
6995
+ }
6996
+ for (const link of this.packageDoc.linkElements) {
6997
+ if (link.id) allIds.add(link.id);
6998
+ }
6999
+ for (const item of this.packageDoc.manifest) {
7000
+ allIds.add(item.id);
7001
+ }
7002
+ const seenGlobalIds = /* @__PURE__ */ new Set();
7003
+ const allIdSources = [];
7004
+ for (const dc of dcElements) {
7005
+ if (dc.id) allIdSources.push({ id: dc.id, normalized: dc.id.trim() });
7006
+ }
7007
+ for (const meta of this.packageDoc.metaElements) {
7008
+ if (meta.id) allIdSources.push({ id: meta.id, normalized: meta.id.trim() });
7009
+ }
7010
+ for (const link of this.packageDoc.linkElements) {
7011
+ if (link.id) allIdSources.push({ id: link.id, normalized: link.id.trim() });
7012
+ }
7013
+ for (const item of this.packageDoc.manifest) {
7014
+ allIdSources.push({ id: item.id, normalized: item.id.trim() });
7015
+ }
7016
+ for (const itemref of this.packageDoc.spine) {
7017
+ if (itemref.id) allIdSources.push({ id: itemref.id, normalized: itemref.id.trim() });
7018
+ }
7019
+ for (const src of allIdSources) {
7020
+ if (seenGlobalIds.has(src.normalized)) {
7021
+ pushMessage(context.messages, {
7022
+ id: MessageId.RSC_005,
7023
+ message: `Duplicate "${src.normalized}"`,
7024
+ location: { path: opfPath }
7025
+ });
7026
+ }
7027
+ seenGlobalIds.add(src.normalized);
7028
+ }
7029
+ for (const meta of this.packageDoc.metaElements) {
7030
+ if (!meta.refines) continue;
7031
+ const refines = meta.refines;
7032
+ if (/^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(refines)) {
7033
+ pushMessage(context.messages, {
7034
+ id: MessageId.RSC_005,
7035
+ message: "@refines must be a relative URL",
7036
+ location: { path: opfPath }
7037
+ });
7038
+ continue;
7039
+ }
7040
+ if (!refines.startsWith("#")) {
7041
+ const isManifestHref = this.packageDoc.manifest.some((item) => item.href === refines);
7042
+ if (isManifestHref) {
7043
+ pushMessage(context.messages, {
7044
+ id: MessageId.RSC_017,
7045
+ message: `@refines should instead refer to "${refines}" using a fragment identifier pointing to its manifest item`,
7046
+ location: { path: opfPath }
7047
+ });
7048
+ }
7049
+ continue;
7050
+ }
7051
+ const targetId = refines.substring(1);
7052
+ if (!allIds.has(targetId)) {
7053
+ pushMessage(context.messages, {
7054
+ id: MessageId.RSC_005,
7055
+ message: `@refines missing target id: "${targetId}"`,
7056
+ location: { path: opfPath }
7057
+ });
7058
+ }
7059
+ }
7060
+ this.detectRefinesCycles(context, opfPath);
7061
+ }
7062
+ if (this.packageDoc.version !== "2.0") {
7063
+ this.validateMetaPropertiesVocab(context, opfPath, dcElements);
7064
+ this.validateRenditionVocab(context, opfPath);
7065
+ this.validateMediaOverlaysVocab(context, opfPath);
7066
+ }
7067
+ if (this.packageDoc.version !== "2.0") {
7068
+ const modifiedMetas = this.packageDoc.metaElements.filter(
7069
+ (meta) => meta.property === "dcterms:modified"
7070
+ );
7071
+ const modifiedMeta = modifiedMetas[0];
7072
+ if (modifiedMetas.length > 1) {
7073
+ pushMessage(context.messages, {
7074
+ id: MessageId.RSC_005,
7075
+ message: "package dcterms:modified meta element must occur exactly once",
7076
+ location: { path: opfPath }
7077
+ });
7078
+ }
7079
+ if (!modifiedMeta) {
7080
+ pushMessage(context.messages, {
7081
+ id: MessageId.RSC_005,
7082
+ message: "package dcterms:modified meta element must occur exactly once",
7083
+ location: { path: opfPath }
7084
+ });
7085
+ pushMessage(context.messages, {
7086
+ id: MessageId.OPF_054,
7087
+ message: "EPUB 3 metadata must include a dcterms:modified meta element",
7088
+ location: { path: opfPath }
7089
+ });
7090
+ } else if (modifiedMeta.value) {
7091
+ const strictModifiedPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
7092
+ if (!strictModifiedPattern.test(modifiedMeta.value.trim())) {
7093
+ pushMessage(context.messages, {
7094
+ id: MessageId.RSC_005,
7095
+ message: `dcterms:modified illegal syntax (expecting: "CCYY-MM-DDThh:mm:ssZ")`,
7096
+ location: { path: opfPath }
7097
+ });
7098
+ }
7099
+ if (!isValidW3CDateFormat(modifiedMeta.value)) {
7100
+ pushMessage(context.messages, {
7101
+ id: MessageId.OPF_054,
7102
+ message: `Invalid dcterms:modified date format "${modifiedMeta.value}"; must be W3C date format (ISO 8601)`,
7103
+ location: { path: opfPath }
7104
+ });
7105
+ }
7106
+ }
7107
+ }
7108
+ }
7109
+ /**
7110
+ * Validate EPUB 3 meta element vocabulary (D-vocabularies: meta-properties)
7111
+ * Ports package-30.sch Schematron patterns for authority, term, belongs-to-collection,
7112
+ * collection-type, display-seq, file-as, group-position, identifier-type, meta-auth,
7113
+ * role, source-of, and title-type.
7114
+ */
7115
+ validateMetaPropertiesVocab(context, opfPath, dcElements) {
7116
+ if (!this.packageDoc) return;
7117
+ const metaElements = this.packageDoc.metaElements;
7118
+ const metaIdToProp = /* @__PURE__ */ new Map();
7119
+ for (const meta of metaElements) {
7120
+ if (meta.id) metaIdToProp.set(meta.id.trim(), meta.property.trim());
7121
+ }
7122
+ for (const dc of dcElements) {
7123
+ if (dc.name !== "subject" || !dc.id) continue;
7124
+ const subjectId = dc.id.trim();
7125
+ const authorityCount = metaElements.filter(
7126
+ (m) => m.property.trim() === "authority" && m.refines?.trim().substring(1) === subjectId
7127
+ ).length;
7128
+ const termCount = metaElements.filter(
7129
+ (m) => m.property.trim() === "term" && m.refines?.trim().substring(1) === subjectId
7130
+ ).length;
7131
+ if (authorityCount > 1 || termCount > 1) {
7132
+ pushMessage(context.messages, {
7133
+ id: MessageId.RSC_005,
7134
+ message: "Only one pair of authority and term properties can be associated with a dc:subject",
7135
+ location: { path: opfPath }
7136
+ });
7137
+ } else if (authorityCount === 1 && termCount === 0) {
7138
+ pushMessage(context.messages, {
7139
+ id: MessageId.RSC_005,
7140
+ message: "A term property must be associated with a dc:subject when an authority is specified",
7141
+ location: { path: opfPath }
7142
+ });
7143
+ } else if (authorityCount === 0 && termCount === 1) {
7144
+ pushMessage(context.messages, {
7145
+ id: MessageId.RSC_005,
7146
+ message: "An authority property must be associated with a dc:subject when a term is specified",
7147
+ location: { path: opfPath }
7148
+ });
7149
+ }
7150
+ }
7151
+ const seenPropertyRefines = /* @__PURE__ */ new Set();
7152
+ for (const meta of metaElements) {
7153
+ const prop = meta.property.trim();
7154
+ const refines = meta.refines?.trim();
7155
+ switch (prop) {
7156
+ case "authority": {
7157
+ const ok = dcElements.some(
7158
+ (dc) => dc.name === "subject" && dc.id && "#" + dc.id.trim() === refines
7159
+ );
7160
+ if (!ok) {
7161
+ pushMessage(context.messages, {
7162
+ id: MessageId.RSC_005,
7163
+ message: 'Property "authority" must refine a "subject" property.',
7164
+ location: { path: opfPath }
7165
+ });
7166
+ }
7167
+ break;
7168
+ }
7169
+ case "term": {
7170
+ const ok = dcElements.some(
7171
+ (dc) => dc.name === "subject" && dc.id && "#" + dc.id.trim() === refines
7172
+ );
7173
+ if (!ok) {
7174
+ pushMessage(context.messages, {
7175
+ id: MessageId.RSC_005,
7176
+ message: 'Property "term" must refine a "subject" property.',
7177
+ location: { path: opfPath }
7178
+ });
7179
+ }
7180
+ break;
7181
+ }
7182
+ case "belongs-to-collection": {
7183
+ if (refines) {
7184
+ const targetId = refines.substring(1);
7185
+ if (metaIdToProp.get(targetId) !== "belongs-to-collection") {
7186
+ pushMessage(context.messages, {
7187
+ id: MessageId.RSC_005,
7188
+ message: 'Property "belongs-to-collection" can only refine other "belongs-to-collection" properties.',
7189
+ location: { path: opfPath }
7190
+ });
7191
+ }
7192
+ }
7193
+ break;
7194
+ }
7195
+ case "collection-type": {
7196
+ if (!refines) {
7197
+ pushMessage(context.messages, {
7198
+ id: MessageId.RSC_005,
7199
+ message: 'Property "collection-type" must refine a "belongs-to-collection" property.',
7200
+ location: { path: opfPath }
7201
+ });
7202
+ } else {
7203
+ const targetId = refines.substring(1);
7204
+ if (metaIdToProp.get(targetId) !== "belongs-to-collection") {
7205
+ pushMessage(context.messages, {
7206
+ id: MessageId.RSC_005,
7207
+ message: 'Property "collection-type" must refine a "belongs-to-collection" property.',
7208
+ location: { path: opfPath }
7209
+ });
7210
+ }
7211
+ }
7212
+ const ctKey = `${prop}:${refines ?? ""}`;
7213
+ if (seenPropertyRefines.has(ctKey)) {
7214
+ pushMessage(context.messages, {
7215
+ id: MessageId.RSC_005,
7216
+ message: '"collection-type" cannot be declared more than once to refine the same "belongs-to-collection" expression.',
7217
+ location: { path: opfPath }
7218
+ });
7219
+ }
7220
+ seenPropertyRefines.add(ctKey);
7221
+ break;
7222
+ }
7223
+ case "display-seq": {
7224
+ const key = `${prop}:${refines ?? ""}`;
7225
+ if (seenPropertyRefines.has(key)) {
7226
+ pushMessage(context.messages, {
7227
+ id: MessageId.RSC_005,
7228
+ message: '"display-seq" cannot be declared more than once to refine the same expression.',
7229
+ location: { path: opfPath }
7230
+ });
7231
+ }
7232
+ seenPropertyRefines.add(key);
7233
+ break;
5333
7234
  }
5334
- }
5335
- if (dc.name === "identifier" && dc.value) {
5336
- const val = dc.value.trim();
5337
- if (val.startsWith("urn:uuid:")) {
5338
- const uuid = val.substring(9);
5339
- if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(uuid)) {
7235
+ case "file-as": {
7236
+ const key = `${prop}:${refines ?? ""}`;
7237
+ if (seenPropertyRefines.has(key)) {
5340
7238
  pushMessage(context.messages, {
5341
- id: MessageId.OPF_085,
5342
- message: `Invalid UUID value "${uuid}"`,
7239
+ id: MessageId.RSC_005,
7240
+ message: '"file-as" cannot be declared more than once to refine the same expression.',
5343
7241
  location: { path: opfPath }
5344
7242
  });
5345
7243
  }
7244
+ seenPropertyRefines.add(key);
7245
+ break;
5346
7246
  }
5347
- }
5348
- if (dc.name === "creator" && dc.attributes) {
5349
- const opfRole = dc.attributes["opf:role"];
5350
- if (opfRole?.startsWith("marc:")) {
5351
- const relatorCode = opfRole.substring(5);
5352
- const validRelatorCodes = /* @__PURE__ */ new Set([
5353
- "arr",
5354
- "aut",
5355
- "aut",
5356
- "ccp",
5357
- "com",
5358
- "ctb",
5359
- "csl",
5360
- "edt",
5361
- "ill",
5362
- "itr",
5363
- "pbl",
5364
- "pdr",
5365
- "prt",
5366
- "trl",
5367
- "cre",
5368
- "art",
5369
- "ctb",
5370
- "edt",
5371
- "pfr",
5372
- "red",
5373
- "rev",
5374
- "spn",
5375
- "dsx",
5376
- "pmc",
5377
- "dte",
5378
- "ove",
5379
- "trc",
5380
- "ldr",
5381
- "led",
5382
- "prg",
5383
- "rap",
5384
- "rce",
5385
- "rpc",
5386
- "rtr",
5387
- "sad",
5388
- "sgn",
5389
- "tce",
5390
- "aac",
5391
- "acq",
5392
- "ant",
5393
- "arr",
5394
- "art",
5395
- "ard",
5396
- "asg",
5397
- "aus",
5398
- "aft",
5399
- "bdd",
5400
- "bdd",
5401
- "clb",
5402
- "clc",
5403
- "drd",
5404
- "edt",
5405
- "edt",
5406
- "fmd",
5407
- "flm",
5408
- "fmo",
5409
- "fpy",
5410
- "hnr",
5411
- "ill",
5412
- "ilt",
5413
- "img",
5414
- "itr",
5415
- "lrg",
5416
- "lsa",
5417
- "led",
5418
- "lee",
5419
- "lel",
5420
- "lgd",
5421
- "lse",
5422
- "mfr",
5423
- "mod",
5424
- "mon",
5425
- "mus",
5426
- "nrt",
5427
- "ogt",
5428
- "org",
5429
- "oth",
5430
- "pnt",
5431
- "ppa",
5432
- "prv",
5433
- "pup",
5434
- "red",
5435
- "rev",
5436
- "rsg",
5437
- "srv",
5438
- "stn",
5439
- "stl",
5440
- "trc",
5441
- "typ",
5442
- "vdg",
5443
- "voc",
5444
- "wac",
5445
- "wdc"
5446
- ]);
5447
- if (!validRelatorCodes.has(relatorCode)) {
7247
+ case "group-position": {
7248
+ const key = `${prop}:${refines ?? ""}`;
7249
+ if (seenPropertyRefines.has(key)) {
5448
7250
  pushMessage(context.messages, {
5449
- id: MessageId.OPF_052,
5450
- message: `Unknown MARC relator code "${relatorCode}" in dc:creator`,
7251
+ id: MessageId.RSC_005,
7252
+ message: '"group-position" cannot be declared more than once to refine the same expression.',
5451
7253
  location: { path: opfPath }
5452
7254
  });
5453
7255
  }
7256
+ seenPropertyRefines.add(key);
7257
+ break;
5454
7258
  }
5455
- }
5456
- }
5457
- if (this.packageDoc.version !== "2.0") {
5458
- for (const meta of this.packageDoc.metaElements) {
5459
- if (meta.property && /\s/.test(meta.property.trim())) {
5460
- pushMessage(context.messages, {
5461
- id: MessageId.OPF_025,
5462
- message: `Property value must be a single value, not a list: "${meta.property}"`,
5463
- location: { path: opfPath }
5464
- });
7259
+ case "identifier-type": {
7260
+ const ok = dcElements.some(
7261
+ (dc) => (dc.name === "identifier" || dc.name === "source") && dc.id && "#" + dc.id.trim() === refines
7262
+ );
7263
+ if (!ok) {
7264
+ pushMessage(context.messages, {
7265
+ id: MessageId.RSC_005,
7266
+ message: 'Property "identifier-type" must refine an "identifier" or "source" property.',
7267
+ location: { path: opfPath }
7268
+ });
7269
+ }
7270
+ const itKey = `${prop}:${refines ?? ""}`;
7271
+ if (seenPropertyRefines.has(itKey)) {
7272
+ pushMessage(context.messages, {
7273
+ id: MessageId.RSC_005,
7274
+ message: '"identifier-type" cannot be declared more than once to refine the same expression.',
7275
+ location: { path: opfPath }
7276
+ });
7277
+ }
7278
+ seenPropertyRefines.add(itKey);
7279
+ break;
5465
7280
  }
5466
- if (meta.scheme && /\s/.test(meta.scheme.trim())) {
7281
+ case "meta-auth": {
5467
7282
  pushMessage(context.messages, {
5468
- id: MessageId.OPF_025,
5469
- message: `Scheme value must be a single value, not a list: "${meta.scheme}"`,
7283
+ id: MessageId.RSC_017,
7284
+ message: "Use of the meta-auth property is deprecated",
5470
7285
  location: { path: opfPath }
5471
7286
  });
7287
+ break;
5472
7288
  }
5473
- if (meta.property && !/\s/.test(meta.property.trim())) {
5474
- const prop = meta.property.trim();
5475
- if (prop.includes(":") && /:\s*$/.test(prop)) {
7289
+ case "role": {
7290
+ const ok = dcElements.some(
7291
+ (dc) => (dc.name === "creator" || dc.name === "contributor" || dc.name === "publisher") && dc.id && "#" + dc.id.trim() === refines
7292
+ );
7293
+ if (!ok) {
5476
7294
  pushMessage(context.messages, {
5477
- id: MessageId.OPF_026,
5478
- message: `Malformed property name: "${prop}"`,
7295
+ id: MessageId.RSC_005,
7296
+ message: 'Property "role" must refine a "creator", "contributor", or "publisher" property.',
5479
7297
  location: { path: opfPath }
5480
7298
  });
5481
7299
  }
7300
+ break;
5482
7301
  }
5483
- if (meta.scheme) {
5484
- const scheme = meta.scheme.trim();
5485
- if (scheme && !scheme.includes(":")) {
7302
+ case "source-of": {
7303
+ if (meta.value.trim() !== "pagination") {
5486
7304
  pushMessage(context.messages, {
5487
- id: MessageId.OPF_027,
5488
- message: `Undefined property: "${scheme}"`,
7305
+ id: MessageId.RSC_005,
7306
+ message: 'The "source-of" property must have the value "pagination"',
7307
+ location: { path: opfPath }
7308
+ });
7309
+ }
7310
+ const hasSourceRefines = dcElements.some(
7311
+ (dc) => dc.name === "source" && dc.id && refines?.substring(1) === dc.id.trim()
7312
+ );
7313
+ if (!hasSourceRefines) {
7314
+ pushMessage(context.messages, {
7315
+ id: MessageId.RSC_005,
7316
+ message: 'The "source-of" property must refine a "source" property.',
7317
+ location: { path: opfPath }
7318
+ });
7319
+ }
7320
+ const soKey = `${prop}:${refines ?? ""}`;
7321
+ if (seenPropertyRefines.has(soKey)) {
7322
+ pushMessage(context.messages, {
7323
+ id: MessageId.RSC_005,
7324
+ message: '"source-of" cannot be declared more than once to refine the same "source" expression.',
7325
+ location: { path: opfPath }
7326
+ });
7327
+ }
7328
+ seenPropertyRefines.add(soKey);
7329
+ break;
7330
+ }
7331
+ case "title-type": {
7332
+ const ok = dcElements.some(
7333
+ (dc) => dc.name === "title" && dc.id && "#" + dc.id.trim() === refines
7334
+ );
7335
+ if (!ok) {
7336
+ pushMessage(context.messages, {
7337
+ id: MessageId.RSC_005,
7338
+ message: 'Property "title-type" must refine a "title" property.',
7339
+ location: { path: opfPath }
7340
+ });
7341
+ }
7342
+ const ttKey = `${prop}:${refines ?? ""}`;
7343
+ if (seenPropertyRefines.has(ttKey)) {
7344
+ pushMessage(context.messages, {
7345
+ id: MessageId.RSC_005,
7346
+ message: '"title-type" cannot be declared more than once to refine the same "title" expression.',
5489
7347
  location: { path: opfPath }
5490
7348
  });
5491
7349
  }
7350
+ seenPropertyRefines.add(ttKey);
7351
+ break;
5492
7352
  }
5493
7353
  }
5494
7354
  }
5495
- if (this.packageDoc.version !== "2.0") {
5496
- const allIds = /* @__PURE__ */ new Set();
5497
- for (const dc of dcElements) {
5498
- if (dc.id) allIds.add(dc.id);
5499
- }
5500
- for (const meta of this.packageDoc.metaElements) {
5501
- if (meta.id) allIds.add(meta.id);
5502
- }
5503
- for (const link of this.packageDoc.linkElements) {
5504
- if (link.id) allIds.add(link.id);
5505
- }
5506
- for (const item of this.packageDoc.manifest) {
5507
- allIds.add(item.id);
5508
- }
5509
- const seenGlobalIds = /* @__PURE__ */ new Set();
5510
- const allIdSources = [];
5511
- for (const dc of dcElements) {
5512
- if (dc.id) allIdSources.push({ id: dc.id, normalized: dc.id.trim() });
5513
- }
5514
- for (const meta of this.packageDoc.metaElements) {
5515
- if (meta.id) allIdSources.push({ id: meta.id, normalized: meta.id.trim() });
5516
- }
5517
- for (const link of this.packageDoc.linkElements) {
5518
- if (link.id) allIdSources.push({ id: link.id, normalized: link.id.trim() });
5519
- }
5520
- for (const item of this.packageDoc.manifest) {
5521
- allIdSources.push({ id: item.id, normalized: item.id.trim() });
5522
- }
5523
- for (const src of allIdSources) {
5524
- if (seenGlobalIds.has(src.normalized)) {
7355
+ }
7356
+ /**
7357
+ * Validate rendition vocabulary meta properties (rendition:layout, orientation, spread, flow, viewport).
7358
+ * Ports the Schematron rules from package-30.sch for the rendition vocabulary.
7359
+ */
7360
+ validateRenditionVocab(context, opfPath) {
7361
+ if (!this.packageDoc) return;
7362
+ const metas = this.packageDoc.metaElements;
7363
+ for (const rp of RENDITION_META_RULES) {
7364
+ const matching = metas.filter((m) => m.property === rp.property);
7365
+ for (const meta of matching) {
7366
+ if (meta.refines && rp.forbidRefines) {
5525
7367
  pushMessage(context.messages, {
5526
7368
  id: MessageId.RSC_005,
5527
- message: `Duplicate "${src.normalized}"`,
7369
+ message: `The "${rp.property}" property must not refine a publication resource`,
5528
7370
  location: { path: opfPath }
5529
7371
  });
7372
+ continue;
5530
7373
  }
5531
- seenGlobalIds.add(src.normalized);
5532
- }
5533
- for (const meta of this.packageDoc.metaElements) {
5534
- if (!meta.refines) continue;
5535
- const refines = meta.refines;
5536
- if (/^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(refines)) {
7374
+ if (rp.deprecated) {
5537
7375
  pushMessage(context.messages, {
5538
- id: MessageId.RSC_005,
5539
- message: "@refines must be a relative URL",
7376
+ id: MessageId.OPF_086,
7377
+ message: `The "${rp.property}" property is deprecated`,
5540
7378
  location: { path: opfPath }
5541
7379
  });
5542
- continue;
5543
7380
  }
5544
- if (!refines.startsWith("#")) {
7381
+ if (rp.validateSyntax) {
7382
+ if (!rp.validateSyntax(meta.value)) {
7383
+ pushMessage(context.messages, {
7384
+ id: MessageId.RSC_005,
7385
+ message: `The value of the "${rp.property}" property must be of the form "width=x, height=y"`,
7386
+ location: { path: opfPath }
7387
+ });
7388
+ }
7389
+ } else if (!rp.allowedValues.has(meta.value)) {
5545
7390
  pushMessage(context.messages, {
5546
- id: MessageId.RSC_017,
5547
- message: `@refines should instead refer to "${refines}" using a fragment identifier pointing to its manifest item`,
7391
+ id: MessageId.RSC_005,
7392
+ message: `The value of the "${rp.property}" property must be ${[...rp.allowedValues].map((v) => `"${v}"`).join(" or ")}`,
5548
7393
  location: { path: opfPath }
5549
7394
  });
5550
- continue;
5551
7395
  }
5552
- const targetId = refines.substring(1);
5553
- if (!allIds.has(targetId)) {
7396
+ if (rp.deprecatedValues?.has(meta.value)) {
5554
7397
  pushMessage(context.messages, {
5555
- id: MessageId.RSC_005,
5556
- message: `@refines missing target id: "${targetId}"`,
7398
+ id: MessageId.OPF_086,
7399
+ message: `The "${rp.property}" property value "${meta.value}" is deprecated`,
5557
7400
  location: { path: opfPath }
5558
7401
  });
5559
7402
  }
5560
7403
  }
5561
- this.detectRefinesCycles(context, opfPath);
5562
- }
5563
- if (this.packageDoc.version !== "2.0") {
5564
- const modifiedMetas = this.packageDoc.metaElements.filter(
5565
- (meta) => meta.property === "dcterms:modified"
5566
- );
5567
- const modifiedMeta = modifiedMetas[0];
5568
- if (modifiedMetas.length > 1) {
7404
+ const countable = rp.forbidRefines ? matching : matching.filter((m) => !m.refines);
7405
+ if (countable.length > 1) {
5569
7406
  pushMessage(context.messages, {
5570
7407
  id: MessageId.RSC_005,
5571
- message: "package dcterms:modified meta element must occur exactly once",
7408
+ message: `The "${rp.property}" property must not occur more than one time as a global value`,
5572
7409
  location: { path: opfPath }
5573
7410
  });
5574
7411
  }
5575
- if (!modifiedMeta) {
7412
+ }
7413
+ for (const meta of metas) {
7414
+ if (meta.property.startsWith("rendition:")) {
7415
+ const localName = meta.property.slice("rendition:".length);
7416
+ if (!KNOWN_RENDITION_META_PROPERTIES.has(localName)) {
7417
+ pushMessage(context.messages, {
7418
+ id: MessageId.OPF_027,
7419
+ message: `Undefined property: "${meta.property}"`,
7420
+ location: { path: opfPath }
7421
+ });
7422
+ }
7423
+ }
7424
+ }
7425
+ }
7426
+ /**
7427
+ * Validate media overlays vocabulary meta properties (media:active-class, playback-active-class, duration).
7428
+ * Ports the Schematron rules from package-30.sch for the media overlays vocabulary.
7429
+ */
7430
+ validateMediaOverlaysVocab(context, opfPath) {
7431
+ if (!this.packageDoc) return;
7432
+ const metas = this.packageDoc.metaElements;
7433
+ for (const prop of ["media:active-class", "media:playback-active-class"]) {
7434
+ if (metas.filter((m) => m.property === prop).length > 1) {
7435
+ const displayName = prop.slice("media:".length);
5576
7436
  pushMessage(context.messages, {
5577
7437
  id: MessageId.RSC_005,
5578
- message: "package dcterms:modified meta element must occur exactly once",
5579
- location: { path: opfPath }
5580
- });
5581
- pushMessage(context.messages, {
5582
- id: MessageId.OPF_054,
5583
- message: "EPUB 3 metadata must include a dcterms:modified meta element",
7438
+ message: `The '${displayName}' property must not occur more than one time in the package metadata`,
5584
7439
  location: { path: opfPath }
5585
7440
  });
5586
- } else if (modifiedMeta.value) {
5587
- const strictModifiedPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
5588
- if (!strictModifiedPattern.test(modifiedMeta.value.trim())) {
7441
+ }
7442
+ }
7443
+ for (const meta of metas) {
7444
+ if (meta.property === "media:duration") {
7445
+ if (!SMIL3_CLOCK_RE.test(meta.value.trim())) {
5589
7446
  pushMessage(context.messages, {
5590
7447
  id: MessageId.RSC_005,
5591
- message: `dcterms:modified illegal syntax (expecting: "CCYY-MM-DDThh:mm:ssZ")`,
5592
- location: { path: opfPath }
5593
- });
5594
- }
5595
- if (!isValidW3CDateFormat(modifiedMeta.value)) {
5596
- pushMessage(context.messages, {
5597
- id: MessageId.OPF_054,
5598
- message: `Invalid dcterms:modified date format "${modifiedMeta.value}"; must be W3C date format (ISO 8601)`,
7448
+ message: `The value of the "media:duration" property must be a valid SMIL3 clock value`,
5599
7449
  location: { path: opfPath }
5600
7450
  });
5601
7451
  }
@@ -5607,7 +7457,6 @@ var OPFValidator = class {
5607
7457
  */
5608
7458
  validateLinkElements(context, opfPath) {
5609
7459
  if (!this.packageDoc) return;
5610
- const opfDir = opfPath.includes("/") ? opfPath.substring(0, opfPath.lastIndexOf("/")) : "";
5611
7460
  for (const link of this.packageDoc.linkElements) {
5612
7461
  if (link.hreflang !== void 0 && link.hreflang !== "") {
5613
7462
  const lang = link.hreflang;
@@ -5636,6 +7485,40 @@ var OPFValidator = class {
5636
7485
  }
5637
7486
  }
5638
7487
  }
7488
+ const relKeywords = link.rel ? link.rel.trim().split(/\s+/).filter(Boolean) : [];
7489
+ const hasRecord = relKeywords.includes("record");
7490
+ const hasVoicing = relKeywords.includes("voicing");
7491
+ const hasAlternate = relKeywords.includes("alternate");
7492
+ if (hasAlternate && relKeywords.length > 1) {
7493
+ pushMessage(context.messages, {
7494
+ id: MessageId.OPF_089,
7495
+ message: `The "alternate" keyword must not be combined with other keywords in the "rel" attribute`,
7496
+ location: { path: opfPath }
7497
+ });
7498
+ }
7499
+ for (const kw of relKeywords) {
7500
+ if (DEPRECATED_LINK_REL.has(kw)) {
7501
+ pushMessage(context.messages, {
7502
+ id: MessageId.OPF_086,
7503
+ message: `The rel keyword "${kw}" is deprecated`,
7504
+ location: { path: opfPath }
7505
+ });
7506
+ }
7507
+ }
7508
+ if (hasRecord && link.refines) {
7509
+ pushMessage(context.messages, {
7510
+ id: MessageId.RSC_005,
7511
+ message: '"record" links only applies to the Publication (must not have a "refines" attribute).',
7512
+ location: { path: opfPath }
7513
+ });
7514
+ }
7515
+ if (hasVoicing && !link.refines) {
7516
+ pushMessage(context.messages, {
7517
+ id: MessageId.RSC_005,
7518
+ message: '"voicing" links must have a "refines" attribute.',
7519
+ location: { path: opfPath }
7520
+ });
7521
+ }
5639
7522
  const href = link.href;
5640
7523
  const decodedHref = tryDecodeUriComponent(href);
5641
7524
  const basePath = href.includes("#") ? href.substring(0, href.indexOf("#")) : href;
@@ -5648,7 +7531,44 @@ var OPFValidator = class {
5648
7531
  });
5649
7532
  continue;
5650
7533
  }
7534
+ if (isDataURL(href)) {
7535
+ pushMessage(context.messages, {
7536
+ id: MessageId.RSC_029,
7537
+ message: `Data URLs are not allowed in the package document link href`,
7538
+ location: { path: opfPath }
7539
+ });
7540
+ continue;
7541
+ }
7542
+ if (isFileURL(href)) {
7543
+ pushMessage(context.messages, {
7544
+ id: MessageId.RSC_030,
7545
+ message: `File URLs are not allowed in the package document`,
7546
+ location: { path: opfPath }
7547
+ });
7548
+ continue;
7549
+ }
5651
7550
  const isRemote = /^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(href);
7551
+ if (!isRemote && href.includes("?")) {
7552
+ pushMessage(context.messages, {
7553
+ id: MessageId.RSC_033,
7554
+ message: `Relative URL strings must not have a query component: "${href}"`,
7555
+ location: { path: opfPath }
7556
+ });
7557
+ }
7558
+ if (hasVoicing && link.mediaType && !link.mediaType.startsWith("audio/")) {
7559
+ pushMessage(context.messages, {
7560
+ id: MessageId.OPF_095,
7561
+ message: `The "voicing" link media type must be an audio type, but found "${link.mediaType}"`,
7562
+ location: { path: opfPath }
7563
+ });
7564
+ }
7565
+ if (isRemote && !link.mediaType && (hasRecord || hasVoicing)) {
7566
+ pushMessage(context.messages, {
7567
+ id: MessageId.OPF_094,
7568
+ message: `The "media-type" attribute is required for "record" and "voicing" links`,
7569
+ location: { path: opfPath }
7570
+ });
7571
+ }
5652
7572
  if (isRemote) {
5653
7573
  continue;
5654
7574
  }
@@ -5659,10 +7579,12 @@ var OPFValidator = class {
5659
7579
  location: { path: opfPath }
5660
7580
  });
5661
7581
  }
5662
- const resolvedPath = resolvePath(opfDir, basePath);
5663
- const resolvedPathDecoded = basePathDecoded !== basePath ? resolvePath(opfDir, basePathDecoded) : resolvedPath;
7582
+ const basePathNoQuery = basePath.includes("?") ? basePath.substring(0, basePath.indexOf("?")) : basePath;
7583
+ const basePathDecodedNoQuery = basePathDecoded.includes("?") ? basePathDecoded.substring(0, basePathDecoded.indexOf("?")) : basePathDecoded;
7584
+ const resolvedPath = resolvePath(opfPath, basePathNoQuery);
7585
+ const resolvedPathDecoded = basePathDecodedNoQuery !== basePathNoQuery ? resolvePath(opfPath, basePathDecodedNoQuery) : resolvedPath;
5664
7586
  const fileExists = context.files.has(resolvedPath) || context.files.has(resolvedPathDecoded);
5665
- const inManifest = this.manifestByHref.has(basePath) || this.manifestByHref.has(basePathDecoded);
7587
+ const inManifest = this.manifestByHref.has(basePathNoQuery) || this.manifestByHref.has(basePathDecodedNoQuery);
5666
7588
  if (!fileExists && !inManifest) {
5667
7589
  pushMessage(context.messages, {
5668
7590
  id: MessageId.RSC_007w,
@@ -5696,7 +7618,24 @@ var OPFValidator = class {
5696
7618
  });
5697
7619
  }
5698
7620
  seenHrefs.add(item.href);
5699
- const fullPath = resolvePath(opfPath, item.href);
7621
+ if (isDataURL(item.href)) {
7622
+ pushMessage(context.messages, {
7623
+ id: MessageId.RSC_029,
7624
+ message: `Data URLs are not allowed in the manifest item href`,
7625
+ location: { path: opfPath }
7626
+ });
7627
+ continue;
7628
+ }
7629
+ const isRemoteItem = /^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(item.href);
7630
+ if (!isRemoteItem && item.href.includes("?")) {
7631
+ pushMessage(context.messages, {
7632
+ id: MessageId.RSC_033,
7633
+ message: `Relative URL strings must not have a query component: "${item.href}"`,
7634
+ location: { path: opfPath }
7635
+ });
7636
+ }
7637
+ const itemHrefBase = item.href.includes("?") ? item.href.substring(0, item.href.indexOf("?")) : item.href;
7638
+ const fullPath = resolvePath(opfPath, itemHrefBase);
5700
7639
  if (fullPath === opfPath) {
5701
7640
  pushMessage(context.messages, {
5702
7641
  id: MessageId.OPF_099,
@@ -5705,7 +7644,7 @@ var OPFValidator = class {
5705
7644
  });
5706
7645
  }
5707
7646
  if (!item.href.startsWith("http") && !item.href.startsWith("mailto:")) {
5708
- const leaked = checkUrlLeaking2(item.href);
7647
+ const leaked = checkUrlLeaking(item.href);
5709
7648
  if (leaked) {
5710
7649
  pushMessage(context.messages, {
5711
7650
  id: MessageId.RSC_026,
@@ -5714,8 +7653,8 @@ var OPFValidator = class {
5714
7653
  });
5715
7654
  }
5716
7655
  }
5717
- const decodedHref = tryDecodeUriComponent(item.href);
5718
- const fullPathDecoded = decodedHref !== item.href ? resolvePath(opfPath, decodedHref).normalize("NFC") : fullPath;
7656
+ const decodedHref = tryDecodeUriComponent(itemHrefBase);
7657
+ const fullPathDecoded = decodedHref !== itemHrefBase ? resolvePath(opfPath, decodedHref).normalize("NFC") : fullPath;
5719
7658
  if (!context.files.has(fullPath) && !context.files.has(fullPathDecoded) && !item.href.startsWith("http")) {
5720
7659
  pushMessage(context.messages, {
5721
7660
  id: MessageId.RSC_001,
@@ -5737,13 +7676,7 @@ var OPFValidator = class {
5737
7676
  location: { path: opfPath }
5738
7677
  });
5739
7678
  }
5740
- const deprecatedTypes = /* @__PURE__ */ new Set([
5741
- "text/x-oeb1-document",
5742
- "text/x-oeb1-css",
5743
- "application/x-oeb1-package",
5744
- "text/x-oeb1-html"
5745
- ]);
5746
- if (deprecatedTypes.has(item.mediaType)) {
7679
+ if (DEPRECATED_MEDIA_TYPES.has(item.mediaType)) {
5747
7680
  pushMessage(context.messages, {
5748
7681
  id: MessageId.OPF_037,
5749
7682
  message: `Found deprecated media-type "${item.mediaType}"`,
@@ -5799,7 +7732,7 @@ var OPFValidator = class {
5799
7732
  });
5800
7733
  }
5801
7734
  if (this.packageDoc.version !== "2.0" && (item.href.startsWith("http://") || item.href.startsWith("https://"))) {
5802
- const isAllowedRemoteType = item.mediaType.startsWith("audio/") || item.mediaType.startsWith("video/") || item.mediaType.startsWith("font/") || item.mediaType === "application/font-sfnt" || item.mediaType === "application/font-woff" || item.mediaType === "application/vnd.ms-opentype";
7735
+ 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";
5803
7736
  const inSpine = this.packageDoc.spine.some((s) => s.idref === item.id);
5804
7737
  if (inSpine) {
5805
7738
  if (!isAllowedRemoteType) {
@@ -5944,6 +7877,24 @@ var OPFValidator = class {
5944
7877
  location: { path: opfPath }
5945
7878
  });
5946
7879
  }
7880
+ if (prop === "rendition:spread-portrait") {
7881
+ pushMessage(context.messages, {
7882
+ id: MessageId.OPF_086,
7883
+ message: `The "rendition:spread-portrait" property is deprecated`,
7884
+ location: { path: opfPath }
7885
+ });
7886
+ }
7887
+ }
7888
+ const props = new Set(itemref.properties);
7889
+ for (const group of EXCLUSIVE_SPINE_GROUPS) {
7890
+ const found = group.filter((p) => props.has(p));
7891
+ if (found.length > 1) {
7892
+ pushMessage(context.messages, {
7893
+ id: MessageId.RSC_005,
7894
+ message: `Properties "${found.join('", "')}" are mutually exclusive`,
7895
+ location: { path: opfPath }
7896
+ });
7897
+ }
5947
7898
  }
5948
7899
  }
5949
7900
  }
@@ -6153,35 +8104,7 @@ function isValidLanguageTag(tag) {
6153
8104
  const pattern = /^[a-zA-Z]{2,3}(-[a-zA-Z]{4})?(-[a-zA-Z]{2}|-\d{3})?(-([a-zA-Z\d]{5,8}|\d[a-zA-Z\d]{3}))*(-[a-wyzA-WYZ](-[a-zA-Z\d]{2,8})+)*(-x(-[a-zA-Z\d]{1,8})+)?$/;
6154
8105
  if (pattern.test(tag)) return true;
6155
8106
  if (/^x(-[a-zA-Z\d]{1,8})+$/.test(tag)) return true;
6156
- const grandfathered = /* @__PURE__ */ new Set([
6157
- "en-GB-oed",
6158
- "i-ami",
6159
- "i-bnn",
6160
- "i-default",
6161
- "i-enochian",
6162
- "i-hak",
6163
- "i-klingon",
6164
- "i-lux",
6165
- "i-mingo",
6166
- "i-navajo",
6167
- "i-pwn",
6168
- "i-tao",
6169
- "i-tay",
6170
- "i-tsu",
6171
- "sgn-BE-FR",
6172
- "sgn-BE-NL",
6173
- "sgn-CH-DE",
6174
- "art-lojban",
6175
- "cel-gaulish",
6176
- "no-bok",
6177
- "no-nyn",
6178
- "zh-guoyu",
6179
- "zh-hakka",
6180
- "zh-min",
6181
- "zh-min-nan",
6182
- "zh-xiang"
6183
- ]);
6184
- return grandfathered.has(tag);
8107
+ return GRANDFATHERED_LANG_TAGS.has(tag);
6185
8108
  }
6186
8109
  function resolvePath(basePath, relativePath) {
6187
8110
  if (relativePath.startsWith("/")) {
@@ -6209,17 +8132,6 @@ function tryDecodeUriComponent(encoded) {
6209
8132
  return encoded;
6210
8133
  }
6211
8134
  }
6212
- function checkUrlLeaking2(href) {
6213
- const TEST_BASE_A = "https://a.example.org/A/";
6214
- const TEST_BASE_B = "https://b.example.org/B/";
6215
- try {
6216
- const urlA = new URL(href, TEST_BASE_A).toString();
6217
- const urlB = new URL(href, TEST_BASE_B).toString();
6218
- return !urlA.startsWith(TEST_BASE_A) || !urlB.startsWith(TEST_BASE_B);
6219
- } catch {
6220
- return false;
6221
- }
6222
- }
6223
8135
  function isValidMimeType(mediaType) {
6224
8136
  const mimeTypePattern = /^[a-zA-Z][a-zA-Z0-9!#$&\-^_.]*\/[a-zA-Z0-9][a-zA-Z0-9!#$&\-^_.+]*(?:\s*;\s*[a-zA-Z0-9-]+=[^;]+)?$/;
6225
8137
  if (!mimeTypePattern.test(mediaType)) {
@@ -6664,7 +8576,7 @@ var ReferenceValidator = class {
6664
8576
  }
6665
8577
  }
6666
8578
  const parsedMimeTypes = ["application/xhtml+xml", "image/svg+xml"];
6667
- if (resource && parsedMimeTypes.includes(resource.mimeType) && !isRemoteURL(resourcePath)) {
8579
+ if (resource && parsedMimeTypes.includes(resource.mimeType) && !isRemoteURL(resourcePath) && !fragment.includes("svgView(") && reference.type !== "svg-symbol" /* SVG_SYMBOL */) {
6668
8580
  if (!this.registry.hasID(resourcePath, fragment)) {
6669
8581
  pushMessage(context.messages, {
6670
8582
  id: MessageId.RSC_012,