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