@likecoin/epubcheck-ts 0.5.0 → 0.5.1

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
@@ -1806,379 +1806,6 @@ var EPUB_SSV_ALL = /* @__PURE__ */ new Set([
1806
1806
  "volume"
1807
1807
  ]);
1808
1808
 
1809
- // src/smil/clock.ts
1810
- var FULL_CLOCK_RE = /^(\d+):([0-5]\d):([0-5]\d)(\.\d+)?$/;
1811
- var PARTIAL_CLOCK_RE = /^([0-5]\d):([0-5]\d)(\.\d+)?$/;
1812
- var TIMECOUNT_RE = /^(\d+(\.\d+)?)(h|min|s|ms)?$/;
1813
- function parseSmilClock(value) {
1814
- const trimmed = value.trim();
1815
- const full = FULL_CLOCK_RE.exec(trimmed);
1816
- if (full) {
1817
- const hours = Number.parseInt(full[1] ?? "0", 10);
1818
- const minutes = Number.parseInt(full[2] ?? "0", 10);
1819
- const seconds = Number.parseInt(full[3] ?? "0", 10);
1820
- const frac = full[4] ? Number.parseFloat(full[4]) : 0;
1821
- return hours * 3600 + minutes * 60 + seconds + frac;
1822
- }
1823
- const partial = PARTIAL_CLOCK_RE.exec(trimmed);
1824
- if (partial) {
1825
- const minutes = Number.parseInt(partial[1] ?? "0", 10);
1826
- const seconds = Number.parseInt(partial[2] ?? "0", 10);
1827
- const frac = partial[3] ? Number.parseFloat(partial[3]) : 0;
1828
- return minutes * 60 + seconds + frac;
1829
- }
1830
- const timecount = TIMECOUNT_RE.exec(trimmed);
1831
- if (timecount) {
1832
- const num = Number.parseFloat(timecount[1] ?? "0");
1833
- const unit = timecount[3] ?? "s";
1834
- switch (unit) {
1835
- case "h":
1836
- return num * 3600;
1837
- case "min":
1838
- return num * 60;
1839
- case "s":
1840
- return num;
1841
- case "ms":
1842
- return num / 1e3;
1843
- default:
1844
- return NaN;
1845
- }
1846
- }
1847
- return NaN;
1848
- }
1849
- function isValidSmilClock(value) {
1850
- return !Number.isNaN(parseSmilClock(value));
1851
- }
1852
-
1853
- // src/smil/validator.ts
1854
- var SMIL_NS = { smil: "http://www.w3.org/ns/SMIL" };
1855
- var BLESSED_AUDIO_TYPES = /* @__PURE__ */ new Set(["audio/mpeg", "audio/mp4"]);
1856
- var BLESSED_AUDIO_PATTERN = /^audio\/ogg\s*;\s*codecs=opus$/i;
1857
- function isBlessedAudioType(mimeType) {
1858
- return BLESSED_AUDIO_TYPES.has(mimeType) || BLESSED_AUDIO_PATTERN.test(mimeType);
1859
- }
1860
- var SMILValidator = class {
1861
- getAttribute(element, name) {
1862
- return element.attr(name)?.value ?? null;
1863
- }
1864
- getEpubAttribute(element, localName) {
1865
- return element.attr(localName, "epub")?.value ?? null;
1866
- }
1867
- validate(context, path, manifestByPath) {
1868
- const result = {
1869
- textReferences: [],
1870
- referencedDocuments: /* @__PURE__ */ new Set(),
1871
- hasRemoteResources: false
1872
- };
1873
- const data = context.files.get(path);
1874
- if (!data) return result;
1875
- const content = typeof data === "string" ? data : new TextDecoder().decode(data);
1876
- let doc = null;
1877
- try {
1878
- doc = libxml2Wasm.XmlDocument.fromString(content);
1879
- } catch {
1880
- pushMessage(context.messages, {
1881
- id: MessageId.RSC_016,
1882
- message: "Media Overlay document is not well-formed XML",
1883
- location: { path }
1884
- });
1885
- return result;
1886
- }
1887
- try {
1888
- const root = doc.root;
1889
- this.validateStructure(context, path, root);
1890
- this.validateAudioElements(context, path, root, manifestByPath, result);
1891
- this.validateEpubTypes(context, path, root);
1892
- this.extractTextReferences(path, root, result);
1893
- } finally {
1894
- doc.dispose();
1895
- }
1896
- return result;
1897
- }
1898
- validateStructure(context, path, root) {
1899
- try {
1900
- for (const text of root.find(".//smil:seq/smil:text", SMIL_NS)) {
1901
- pushMessage(context.messages, {
1902
- id: MessageId.RSC_005,
1903
- message: "element 'text' not allowed here; expected 'seq' or 'par'",
1904
- location: { path, line: text.line }
1905
- });
1906
- }
1907
- for (const audio of root.find(".//smil:seq/smil:audio", SMIL_NS)) {
1908
- pushMessage(context.messages, {
1909
- id: MessageId.RSC_005,
1910
- message: "element 'audio' not allowed here; expected 'seq' or 'par'",
1911
- location: { path, line: audio.line }
1912
- });
1913
- }
1914
- } catch {
1915
- }
1916
- try {
1917
- for (const seq of root.find(".//smil:par/smil:seq", SMIL_NS)) {
1918
- pushMessage(context.messages, {
1919
- id: MessageId.RSC_005,
1920
- message: "element 'seq' not allowed here; expected 'text' or 'audio'",
1921
- location: { path, line: seq.line }
1922
- });
1923
- }
1924
- const parElements = root.find(".//smil:par", SMIL_NS);
1925
- for (const par of parElements) {
1926
- const textChildren = par.find("./smil:text", SMIL_NS);
1927
- for (let i = 1; i < textChildren.length; i++) {
1928
- const extra = textChildren[i];
1929
- if (!extra) continue;
1930
- pushMessage(context.messages, {
1931
- id: MessageId.RSC_005,
1932
- message: "element 'text' not allowed here; only one 'text' element is allowed in 'par'",
1933
- location: { path, line: extra.line }
1934
- });
1935
- }
1936
- }
1937
- } catch {
1938
- }
1939
- try {
1940
- const headMetaElements = root.find(".//smil:head/smil:meta", SMIL_NS);
1941
- for (const meta of headMetaElements) {
1942
- pushMessage(context.messages, {
1943
- id: MessageId.RSC_005,
1944
- message: "element 'meta' not allowed here; expected 'metadata'",
1945
- location: { path, line: meta.line }
1946
- });
1947
- }
1948
- } catch {
1949
- }
1950
- }
1951
- validateAudioElements(context, path, root, manifestByPath, result) {
1952
- try {
1953
- const audioElements = root.find(".//smil:audio", SMIL_NS);
1954
- for (const audio of audioElements) {
1955
- const elem = audio;
1956
- const src = this.getAttribute(elem, "src");
1957
- if (src) {
1958
- if (/^https?:\/\//i.test(src)) {
1959
- result.hasRemoteResources = true;
1960
- }
1961
- if (src.includes("#")) {
1962
- pushMessage(context.messages, {
1963
- id: MessageId.MED_014,
1964
- message: `Media overlay audio file URLs must not have a fragment: "${src}"`,
1965
- location: { path, line: audio.line }
1966
- });
1967
- }
1968
- if (manifestByPath) {
1969
- const audioPath = this.resolveRelativePath(path, src.split("#")[0] ?? src);
1970
- const audioItem = manifestByPath.get(audioPath);
1971
- if (audioItem && !isBlessedAudioType(audioItem.mediaType)) {
1972
- pushMessage(context.messages, {
1973
- id: MessageId.MED_005,
1974
- message: `Media Overlay audio reference "${src}" to non-standard audio type "${audioItem.mediaType}"`,
1975
- location: { path, line: audio.line }
1976
- });
1977
- }
1978
- }
1979
- }
1980
- const clipBegin = this.getAttribute(elem, "clipBegin");
1981
- const clipEnd = this.getAttribute(elem, "clipEnd");
1982
- this.checkClipTiming(context, path, audio.line, clipBegin, clipEnd);
1983
- }
1984
- } catch {
1985
- }
1986
- }
1987
- checkClipTiming(context, path, line, clipBegin, clipEnd) {
1988
- if (clipEnd === null) return;
1989
- const beginStr = clipBegin ?? "0";
1990
- const start = parseSmilClock(beginStr);
1991
- const end = parseSmilClock(clipEnd);
1992
- const location = line != null ? { path, line } : { path };
1993
- if (clipBegin !== null && Number.isNaN(start)) {
1994
- pushMessage(context.messages, {
1995
- id: MessageId.RSC_005,
1996
- message: `Invalid SMIL clock value "${clipBegin}" in clipBegin attribute`,
1997
- location
1998
- });
1999
- }
2000
- if (Number.isNaN(end)) {
2001
- pushMessage(context.messages, {
2002
- id: MessageId.RSC_005,
2003
- message: `Invalid SMIL clock value "${clipEnd}" in clipEnd attribute`,
2004
- location
2005
- });
2006
- }
2007
- if (Number.isNaN(start) || Number.isNaN(end)) return;
2008
- if (start > end) {
2009
- pushMessage(context.messages, {
2010
- id: MessageId.MED_008,
2011
- message: "The time specified in the clipBegin attribute must not be after clipEnd",
2012
- location
2013
- });
2014
- } else if (start === end) {
2015
- pushMessage(context.messages, {
2016
- id: MessageId.MED_009,
2017
- message: "The time specified in the clipBegin attribute must not be the same as clipEnd",
2018
- location
2019
- });
2020
- }
2021
- }
2022
- /**
2023
- * Validate epub:type attribute values against the EPUB SSV vocabulary.
2024
- * Only emits OPF-088 (usage) for unknown local names. Prefixed values
2025
- * from declared vocabularies are allowed.
2026
- */
2027
- validateEpubTypes(context, path, root) {
2028
- try {
2029
- const epubTypeElements = root.find(".//*[@epub:type]", {
2030
- epub: "http://www.idpf.org/2007/ops"
2031
- });
2032
- for (const elem of epubTypeElements) {
2033
- const elemTyped = elem;
2034
- const epubTypeAttr = elemTyped.attr("type", "epub");
2035
- if (!epubTypeAttr?.value) continue;
2036
- for (const part of epubTypeAttr.value.split(/\s+/)) {
2037
- if (!part || part.includes(":")) continue;
2038
- if (!EPUB_SSV_ALL.has(part)) {
2039
- pushMessage(context.messages, {
2040
- id: MessageId.OPF_088,
2041
- message: `Unrecognized epub:type value "${part}"`,
2042
- location: { path, line: elem.line }
2043
- });
2044
- }
2045
- }
2046
- }
2047
- } catch {
2048
- }
2049
- }
2050
- extractTextReferences(path, root, result) {
2051
- try {
2052
- const textElements = root.find(".//smil:text", SMIL_NS);
2053
- for (const text of textElements) {
2054
- const src = this.getAttribute(text, "src");
2055
- if (!src) continue;
2056
- const hashIndex = src.indexOf("#");
2057
- const docRef = hashIndex >= 0 ? src.substring(0, hashIndex) : src;
2058
- const fragment = hashIndex >= 0 ? src.substring(hashIndex + 1) : void 0;
2059
- const docPath = this.resolveRelativePath(path, docRef);
2060
- result.textReferences.push({ docPath, fragment, line: text.line });
2061
- result.referencedDocuments.add(docPath);
2062
- }
2063
- const bodyElements = root.find(".//smil:body", SMIL_NS);
2064
- const seqElements = root.find(".//smil:seq", SMIL_NS);
2065
- for (const elem of [...bodyElements, ...seqElements]) {
2066
- const textref = this.getEpubAttribute(elem, "textref");
2067
- if (!textref) continue;
2068
- const hashIndex = textref.indexOf("#");
2069
- const docRef = hashIndex >= 0 ? textref.substring(0, hashIndex) : textref;
2070
- const fragment = hashIndex >= 0 ? textref.substring(hashIndex + 1) : void 0;
2071
- const docPath = this.resolveRelativePath(path, docRef);
2072
- result.textReferences.push({ docPath, fragment, line: elem.line });
2073
- result.referencedDocuments.add(docPath);
2074
- }
2075
- } catch {
2076
- }
2077
- }
2078
- resolveRelativePath(basePath, relativePath) {
2079
- if (relativePath.startsWith("/") || /^[a-zA-Z]+:/.test(relativePath)) {
2080
- return relativePath;
2081
- }
2082
- const baseDir = basePath.includes("/") ? basePath.substring(0, basePath.lastIndexOf("/")) : "";
2083
- if (!baseDir) return relativePath;
2084
- const segments = `${baseDir}/${relativePath}`.split("/");
2085
- const resolved = [];
2086
- for (const seg of segments) {
2087
- if (seg === "..") {
2088
- resolved.pop();
2089
- } else if (seg !== ".") {
2090
- resolved.push(seg);
2091
- }
2092
- }
2093
- return resolved.join("/");
2094
- }
2095
- };
2096
-
2097
- // src/opf/types.ts
2098
- var CORE_MEDIA_TYPES = /* @__PURE__ */ new Set([
2099
- // Image types
2100
- "image/gif",
2101
- "image/jpeg",
2102
- "image/png",
2103
- "image/svg+xml",
2104
- "image/webp",
2105
- // Audio types
2106
- "audio/mpeg",
2107
- "audio/mp4",
2108
- "audio/ogg",
2109
- // CSS
2110
- "text/css",
2111
- // Fonts
2112
- "font/otf",
2113
- "font/ttf",
2114
- "font/woff",
2115
- "font/woff2",
2116
- "application/font-sfnt",
2117
- // deprecated alias for font/otf, font/ttf
2118
- "application/font-woff",
2119
- // deprecated alias for font/woff
2120
- "application/vnd.ms-opentype",
2121
- // deprecated alias
2122
- // Content documents
2123
- "application/xhtml+xml",
2124
- "application/x-dtbncx+xml",
2125
- // NCX
2126
- // JavaScript (EPUB 3)
2127
- "text/javascript",
2128
- "application/javascript",
2129
- // Media overlays
2130
- "application/smil+xml",
2131
- // PLS (Pronunciation Lexicon)
2132
- "application/pls+xml"
2133
- ]);
2134
- function isCoreMediaType(mimeType) {
2135
- if (CORE_MEDIA_TYPES.has(mimeType)) return true;
2136
- if (mimeType.startsWith("video/")) return true;
2137
- if (/^audio\/ogg\s*;\s*codecs=opus$/i.test(mimeType)) return true;
2138
- const semicolonIndex = mimeType.indexOf(";");
2139
- if (semicolonIndex >= 0) {
2140
- const baseType = mimeType.substring(0, semicolonIndex).trim();
2141
- if (CORE_MEDIA_TYPES.has(baseType)) return true;
2142
- if (baseType.startsWith("video/")) return true;
2143
- }
2144
- return false;
2145
- }
2146
- var ITEM_PROPERTIES = /* @__PURE__ */ new Set([
2147
- "cover-image",
2148
- "data-nav",
2149
- "dictionary",
2150
- "glossary",
2151
- "index",
2152
- "mathml",
2153
- "nav",
2154
- "remote-resources",
2155
- "scripted",
2156
- "search-key-map",
2157
- "svg",
2158
- "switch"
2159
- ]);
2160
- var LINK_PROPERTIES = /* @__PURE__ */ new Set(["onix", "marc21xml-record", "mods-record", "xmp-record"]);
2161
- var SPINE_PROPERTIES = /* @__PURE__ */ new Set([
2162
- "page-spread-left",
2163
- "page-spread-right",
2164
- "rendition:spread-none",
2165
- "rendition:spread-landscape",
2166
- "rendition:spread-portrait",
2167
- "rendition:spread-both",
2168
- "rendition:spread-auto",
2169
- "rendition:page-spread-center",
2170
- "rendition:layout-reflowable",
2171
- "rendition:layout-pre-paginated",
2172
- "rendition:orientation-auto",
2173
- "rendition:orientation-landscape",
2174
- "rendition:orientation-portrait",
2175
- "rendition:flow-auto",
2176
- "rendition:flow-paginated",
2177
- "rendition:flow-scrolled-continuous",
2178
- "rendition:flow-scrolled-doc",
2179
- "rendition:align-x-center"
2180
- ]);
2181
-
2182
1809
  // src/references/url.ts
2183
1810
  function parseURL(urlString) {
2184
1811
  const hashIndex = urlString.indexOf("#");
@@ -2250,6 +1877,50 @@ function resolveManifestHref(opfDir, href) {
2250
1877
  }
2251
1878
  }
2252
1879
 
1880
+ // src/smil/clock.ts
1881
+ var FULL_CLOCK_RE = /^(\d+):([0-5]\d):([0-5]\d)(\.\d+)?$/;
1882
+ var PARTIAL_CLOCK_RE = /^([0-5]\d):([0-5]\d)(\.\d+)?$/;
1883
+ var TIMECOUNT_RE = /^(\d+(\.\d+)?)(h|min|s|ms)?$/;
1884
+ function parseSmilClock(value) {
1885
+ const trimmed = value.trim();
1886
+ const full = FULL_CLOCK_RE.exec(trimmed);
1887
+ if (full) {
1888
+ const hours = Number.parseInt(full[1] ?? "0", 10);
1889
+ const minutes = Number.parseInt(full[2] ?? "0", 10);
1890
+ const seconds = Number.parseInt(full[3] ?? "0", 10);
1891
+ const frac = full[4] ? Number.parseFloat(full[4]) : 0;
1892
+ return hours * 3600 + minutes * 60 + seconds + frac;
1893
+ }
1894
+ const partial = PARTIAL_CLOCK_RE.exec(trimmed);
1895
+ if (partial) {
1896
+ const minutes = Number.parseInt(partial[1] ?? "0", 10);
1897
+ const seconds = Number.parseInt(partial[2] ?? "0", 10);
1898
+ const frac = partial[3] ? Number.parseFloat(partial[3]) : 0;
1899
+ return minutes * 60 + seconds + frac;
1900
+ }
1901
+ const timecount = TIMECOUNT_RE.exec(trimmed);
1902
+ if (timecount) {
1903
+ const num = Number.parseFloat(timecount[1] ?? "0");
1904
+ const unit = timecount[3] ?? "s";
1905
+ switch (unit) {
1906
+ case "h":
1907
+ return num * 3600;
1908
+ case "min":
1909
+ return num * 60;
1910
+ case "s":
1911
+ return num;
1912
+ case "ms":
1913
+ return num / 1e3;
1914
+ default:
1915
+ return NaN;
1916
+ }
1917
+ }
1918
+ return NaN;
1919
+ }
1920
+ function isValidSmilClock(value) {
1921
+ return !Number.isNaN(parseSmilClock(value));
1922
+ }
1923
+
2253
1924
  // src/types.ts
2254
1925
  var EPUB_VERSIONS = ["2.0", "3.0", "3.1", "3.2", "3.3"];
2255
1926
 
@@ -2698,6 +2369,91 @@ function parseCollections(xml) {
2698
2369
  return roots;
2699
2370
  }
2700
2371
 
2372
+ // src/opf/types.ts
2373
+ var CORE_MEDIA_TYPES = /* @__PURE__ */ new Set([
2374
+ // Image types
2375
+ "image/gif",
2376
+ "image/jpeg",
2377
+ "image/png",
2378
+ "image/svg+xml",
2379
+ "image/webp",
2380
+ // Audio types
2381
+ "audio/mpeg",
2382
+ "audio/mp4",
2383
+ "audio/ogg",
2384
+ // CSS
2385
+ "text/css",
2386
+ // Fonts
2387
+ "font/otf",
2388
+ "font/ttf",
2389
+ "font/woff",
2390
+ "font/woff2",
2391
+ "application/font-sfnt",
2392
+ // deprecated alias for font/otf, font/ttf
2393
+ "application/font-woff",
2394
+ // deprecated alias for font/woff
2395
+ "application/vnd.ms-opentype",
2396
+ // deprecated alias
2397
+ // Content documents
2398
+ "application/xhtml+xml",
2399
+ "application/x-dtbncx+xml",
2400
+ // NCX
2401
+ // JavaScript (EPUB 3)
2402
+ "text/javascript",
2403
+ "application/javascript",
2404
+ // Media overlays
2405
+ "application/smil+xml",
2406
+ // PLS (Pronunciation Lexicon)
2407
+ "application/pls+xml"
2408
+ ]);
2409
+ function isCoreMediaType(mimeType) {
2410
+ if (CORE_MEDIA_TYPES.has(mimeType)) return true;
2411
+ if (mimeType.startsWith("video/")) return true;
2412
+ if (/^audio\/ogg\s*;\s*codecs=opus$/i.test(mimeType)) return true;
2413
+ const semicolonIndex = mimeType.indexOf(";");
2414
+ if (semicolonIndex >= 0) {
2415
+ const baseType = mimeType.substring(0, semicolonIndex).trim();
2416
+ if (CORE_MEDIA_TYPES.has(baseType)) return true;
2417
+ if (baseType.startsWith("video/")) return true;
2418
+ }
2419
+ return false;
2420
+ }
2421
+ var ITEM_PROPERTIES = /* @__PURE__ */ new Set([
2422
+ "cover-image",
2423
+ "data-nav",
2424
+ "dictionary",
2425
+ "glossary",
2426
+ "index",
2427
+ "mathml",
2428
+ "nav",
2429
+ "remote-resources",
2430
+ "scripted",
2431
+ "search-key-map",
2432
+ "svg",
2433
+ "switch"
2434
+ ]);
2435
+ var LINK_PROPERTIES = /* @__PURE__ */ new Set(["onix", "marc21xml-record", "mods-record", "xmp-record"]);
2436
+ var SPINE_PROPERTIES = /* @__PURE__ */ new Set([
2437
+ "page-spread-left",
2438
+ "page-spread-right",
2439
+ "rendition:spread-none",
2440
+ "rendition:spread-landscape",
2441
+ "rendition:spread-portrait",
2442
+ "rendition:spread-both",
2443
+ "rendition:spread-auto",
2444
+ "rendition:page-spread-center",
2445
+ "rendition:layout-reflowable",
2446
+ "rendition:layout-pre-paginated",
2447
+ "rendition:orientation-auto",
2448
+ "rendition:orientation-landscape",
2449
+ "rendition:orientation-portrait",
2450
+ "rendition:flow-auto",
2451
+ "rendition:flow-paginated",
2452
+ "rendition:flow-scrolled-continuous",
2453
+ "rendition:flow-scrolled-doc",
2454
+ "rendition:align-x-center"
2455
+ ]);
2456
+
2701
2457
  // src/opf/validator.ts
2702
2458
  var DEPRECATED_MEDIA_TYPES = /* @__PURE__ */ new Set([
2703
2459
  "text/x-oeb1-document",
@@ -5259,6 +5015,251 @@ function isValidW3CDateFormat(dateStr) {
5259
5015
  return false;
5260
5016
  }
5261
5017
 
5018
+ // src/smil/validator.ts
5019
+ var SMIL_NS = { smil: "http://www.w3.org/ns/SMIL" };
5020
+ var BLESSED_AUDIO_TYPES = /* @__PURE__ */ new Set(["audio/mpeg", "audio/mp4"]);
5021
+ var BLESSED_AUDIO_PATTERN = /^audio\/ogg\s*;\s*codecs=opus$/i;
5022
+ function isBlessedAudioType(mimeType) {
5023
+ return BLESSED_AUDIO_TYPES.has(mimeType) || BLESSED_AUDIO_PATTERN.test(mimeType);
5024
+ }
5025
+ var SMILValidator = class {
5026
+ getAttribute(element, name) {
5027
+ return element.attr(name)?.value ?? null;
5028
+ }
5029
+ getEpubAttribute(element, localName) {
5030
+ return element.attr(localName, "epub")?.value ?? null;
5031
+ }
5032
+ validate(context, path, manifestByPath) {
5033
+ const result = {
5034
+ textReferences: [],
5035
+ referencedDocuments: /* @__PURE__ */ new Set(),
5036
+ hasRemoteResources: false
5037
+ };
5038
+ const data = context.files.get(path);
5039
+ if (!data) return result;
5040
+ const content = typeof data === "string" ? data : new TextDecoder().decode(data);
5041
+ let doc = null;
5042
+ try {
5043
+ doc = libxml2Wasm.XmlDocument.fromString(content);
5044
+ } catch {
5045
+ pushMessage(context.messages, {
5046
+ id: MessageId.RSC_016,
5047
+ message: "Media Overlay document is not well-formed XML",
5048
+ location: { path }
5049
+ });
5050
+ return result;
5051
+ }
5052
+ try {
5053
+ const root = doc.root;
5054
+ this.validateStructure(context, path, root);
5055
+ this.validateAudioElements(context, path, root, manifestByPath, result);
5056
+ this.validateEpubTypes(context, path, root);
5057
+ this.extractTextReferences(path, root, result);
5058
+ } finally {
5059
+ doc.dispose();
5060
+ }
5061
+ return result;
5062
+ }
5063
+ validateStructure(context, path, root) {
5064
+ try {
5065
+ for (const text of root.find(".//smil:seq/smil:text", SMIL_NS)) {
5066
+ pushMessage(context.messages, {
5067
+ id: MessageId.RSC_005,
5068
+ message: "element 'text' not allowed here; expected 'seq' or 'par'",
5069
+ location: { path, line: text.line }
5070
+ });
5071
+ }
5072
+ for (const audio of root.find(".//smil:seq/smil:audio", SMIL_NS)) {
5073
+ pushMessage(context.messages, {
5074
+ id: MessageId.RSC_005,
5075
+ message: "element 'audio' not allowed here; expected 'seq' or 'par'",
5076
+ location: { path, line: audio.line }
5077
+ });
5078
+ }
5079
+ } catch {
5080
+ }
5081
+ try {
5082
+ for (const seq of root.find(".//smil:par/smil:seq", SMIL_NS)) {
5083
+ pushMessage(context.messages, {
5084
+ id: MessageId.RSC_005,
5085
+ message: "element 'seq' not allowed here; expected 'text' or 'audio'",
5086
+ location: { path, line: seq.line }
5087
+ });
5088
+ }
5089
+ const parElements = root.find(".//smil:par", SMIL_NS);
5090
+ for (const par of parElements) {
5091
+ const textChildren = par.find("./smil:text", SMIL_NS);
5092
+ for (let i = 1; i < textChildren.length; i++) {
5093
+ const extra = textChildren[i];
5094
+ if (!extra) continue;
5095
+ pushMessage(context.messages, {
5096
+ id: MessageId.RSC_005,
5097
+ message: "element 'text' not allowed here; only one 'text' element is allowed in 'par'",
5098
+ location: { path, line: extra.line }
5099
+ });
5100
+ }
5101
+ }
5102
+ } catch {
5103
+ }
5104
+ try {
5105
+ const headMetaElements = root.find(".//smil:head/smil:meta", SMIL_NS);
5106
+ for (const meta of headMetaElements) {
5107
+ pushMessage(context.messages, {
5108
+ id: MessageId.RSC_005,
5109
+ message: "element 'meta' not allowed here; expected 'metadata'",
5110
+ location: { path, line: meta.line }
5111
+ });
5112
+ }
5113
+ } catch {
5114
+ }
5115
+ }
5116
+ validateAudioElements(context, path, root, manifestByPath, result) {
5117
+ try {
5118
+ const audioElements = root.find(".//smil:audio", SMIL_NS);
5119
+ for (const audio of audioElements) {
5120
+ const elem = audio;
5121
+ const src = this.getAttribute(elem, "src");
5122
+ if (src) {
5123
+ if (isRemoteURL(src)) {
5124
+ result.hasRemoteResources = true;
5125
+ }
5126
+ if (src.includes("#")) {
5127
+ pushMessage(context.messages, {
5128
+ id: MessageId.MED_014,
5129
+ message: `Media overlay audio file URLs must not have a fragment: "${src}"`,
5130
+ location: { path, line: audio.line }
5131
+ });
5132
+ }
5133
+ if (manifestByPath) {
5134
+ const audioPath = this.resolveRelativePath(path, src.split("#")[0] ?? src);
5135
+ const audioItem = manifestByPath.get(audioPath);
5136
+ if (audioItem && !isBlessedAudioType(audioItem.mediaType)) {
5137
+ pushMessage(context.messages, {
5138
+ id: MessageId.MED_005,
5139
+ message: `Media Overlay audio reference "${src}" to non-standard audio type "${audioItem.mediaType}"`,
5140
+ location: { path, line: audio.line }
5141
+ });
5142
+ }
5143
+ }
5144
+ }
5145
+ const clipBegin = this.getAttribute(elem, "clipBegin");
5146
+ const clipEnd = this.getAttribute(elem, "clipEnd");
5147
+ this.checkClipTiming(context, path, audio.line, clipBegin, clipEnd);
5148
+ }
5149
+ } catch {
5150
+ }
5151
+ }
5152
+ checkClipTiming(context, path, line, clipBegin, clipEnd) {
5153
+ if (clipEnd === null) return;
5154
+ const beginStr = clipBegin ?? "0";
5155
+ const start = parseSmilClock(beginStr);
5156
+ const end = parseSmilClock(clipEnd);
5157
+ const location = line != null ? { path, line } : { path };
5158
+ if (clipBegin !== null && Number.isNaN(start)) {
5159
+ pushMessage(context.messages, {
5160
+ id: MessageId.RSC_005,
5161
+ message: `Invalid SMIL clock value "${clipBegin}" in clipBegin attribute`,
5162
+ location
5163
+ });
5164
+ }
5165
+ if (Number.isNaN(end)) {
5166
+ pushMessage(context.messages, {
5167
+ id: MessageId.RSC_005,
5168
+ message: `Invalid SMIL clock value "${clipEnd}" in clipEnd attribute`,
5169
+ location
5170
+ });
5171
+ }
5172
+ if (Number.isNaN(start) || Number.isNaN(end)) return;
5173
+ if (start > end) {
5174
+ pushMessage(context.messages, {
5175
+ id: MessageId.MED_008,
5176
+ message: "The time specified in the clipBegin attribute must not be after clipEnd",
5177
+ location
5178
+ });
5179
+ } else if (start === end) {
5180
+ pushMessage(context.messages, {
5181
+ id: MessageId.MED_009,
5182
+ message: "The time specified in the clipBegin attribute must not be the same as clipEnd",
5183
+ location
5184
+ });
5185
+ }
5186
+ }
5187
+ /**
5188
+ * Validate epub:type attribute values against the EPUB SSV vocabulary.
5189
+ * Only emits OPF-088 (usage) for unknown local names. Prefixed values
5190
+ * from declared vocabularies are allowed.
5191
+ */
5192
+ validateEpubTypes(context, path, root) {
5193
+ try {
5194
+ const epubTypeElements = root.find(".//*[@epub:type]", {
5195
+ epub: "http://www.idpf.org/2007/ops"
5196
+ });
5197
+ for (const elem of epubTypeElements) {
5198
+ const elemTyped = elem;
5199
+ const epubTypeAttr = elemTyped.attr("type", "epub");
5200
+ if (!epubTypeAttr?.value) continue;
5201
+ for (const part of epubTypeAttr.value.split(/\s+/)) {
5202
+ if (!part || part.includes(":")) continue;
5203
+ if (!EPUB_SSV_ALL.has(part)) {
5204
+ pushMessage(context.messages, {
5205
+ id: MessageId.OPF_088,
5206
+ message: `Unrecognized epub:type value "${part}"`,
5207
+ location: { path, line: elem.line }
5208
+ });
5209
+ }
5210
+ }
5211
+ }
5212
+ } catch {
5213
+ }
5214
+ }
5215
+ extractTextReferences(path, root, result) {
5216
+ try {
5217
+ const textElements = root.find(".//smil:text", SMIL_NS);
5218
+ for (const text of textElements) {
5219
+ const src = this.getAttribute(text, "src");
5220
+ if (!src) continue;
5221
+ const hashIndex = src.indexOf("#");
5222
+ const docRef = hashIndex >= 0 ? src.substring(0, hashIndex) : src;
5223
+ const fragment = hashIndex >= 0 ? src.substring(hashIndex + 1) : void 0;
5224
+ const docPath = this.resolveRelativePath(path, docRef);
5225
+ result.textReferences.push({ docPath, fragment, line: text.line });
5226
+ result.referencedDocuments.add(docPath);
5227
+ }
5228
+ const bodyElements = root.find(".//smil:body", SMIL_NS);
5229
+ const seqElements = root.find(".//smil:seq", SMIL_NS);
5230
+ for (const elem of [...bodyElements, ...seqElements]) {
5231
+ const textref = this.getEpubAttribute(elem, "textref");
5232
+ if (!textref) continue;
5233
+ const hashIndex = textref.indexOf("#");
5234
+ const docRef = hashIndex >= 0 ? textref.substring(0, hashIndex) : textref;
5235
+ const fragment = hashIndex >= 0 ? textref.substring(hashIndex + 1) : void 0;
5236
+ const docPath = this.resolveRelativePath(path, docRef);
5237
+ result.textReferences.push({ docPath, fragment, line: elem.line });
5238
+ result.referencedDocuments.add(docPath);
5239
+ }
5240
+ } catch {
5241
+ }
5242
+ }
5243
+ resolveRelativePath(basePath, relativePath) {
5244
+ const decoded = tryDecodeUriComponent(relativePath);
5245
+ if (decoded.startsWith("/") || /^[a-zA-Z]+:/.test(decoded)) {
5246
+ return decoded.normalize("NFC");
5247
+ }
5248
+ const baseDir = basePath.includes("/") ? basePath.substring(0, basePath.lastIndexOf("/")) : "";
5249
+ if (!baseDir) return decoded.normalize("NFC");
5250
+ const segments = `${baseDir}/${decoded}`.split("/");
5251
+ const resolved = [];
5252
+ for (const seg of segments) {
5253
+ if (seg === "..") {
5254
+ resolved.pop();
5255
+ } else if (seg !== ".") {
5256
+ resolved.push(seg);
5257
+ }
5258
+ }
5259
+ return resolved.join("/").normalize("NFC");
5260
+ }
5261
+ };
5262
+
5262
5263
  // src/references/types.ts
5263
5264
  var PUBLICATION_RESOURCE_TYPES = /* @__PURE__ */ new Set([
5264
5265
  "generic" /* GENERIC */,
@@ -7726,6 +7727,7 @@ var ContentValidator = class {
7726
7727
  }
7727
7728
  }
7728
7729
  checkUsemapAttribute(context, path, root) {
7730
+ if (context.version === "2.0") return;
7729
7731
  try {
7730
7732
  const elements = root.find(".//html:*[@usemap]", XHTML_NS);
7731
7733
  for (const elem of elements) {
@@ -9840,8 +9842,8 @@ var NCXValidator = class {
9840
9842
  const hashIdx = src.indexOf("#");
9841
9843
  const srcBase = hashIdx >= 0 ? src.substring(0, hashIdx) : src;
9842
9844
  const fragment = hashIdx >= 0 ? src.substring(hashIdx + 1) : "";
9843
- const isRemote = srcBase.startsWith("http://") || srcBase.startsWith("https://");
9844
- const fullPath = isRemote ? srcBase : resolvePath(ncxPath, srcBase);
9845
+ const isRemote = isRemoteURL(srcBase);
9846
+ const fullPath = isRemote ? srcBase : resolvePath(ncxPath, tryDecodeUriComponent(srcBase)).normalize("NFC");
9845
9847
  if (!context.files.has(fullPath) && !isRemote) {
9846
9848
  const line = contentElem.line;
9847
9849
  pushMessage(context.messages, {