@likecoin/epubcheck-ts 0.5.0 → 0.6.0
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/README.md +4 -4
- package/bin/epubcheck.js +1 -1
- package/bin/epubcheck.ts +1 -1
- package/dist/index.cjs +1253 -729
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1253 -729
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1668,143 +1668,79 @@ var CSSValidator = class {
|
|
|
1668
1668
|
}
|
|
1669
1669
|
};
|
|
1670
1670
|
|
|
1671
|
-
// src/
|
|
1672
|
-
|
|
1673
|
-
"
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
"
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
"
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
"
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
"
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
"
|
|
1722
|
-
"
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
"index-editor-note",
|
|
1745
|
-
"index-entry",
|
|
1746
|
-
"index-entry-list",
|
|
1747
|
-
"index-group",
|
|
1748
|
-
"index-headnotes",
|
|
1749
|
-
"index-legend",
|
|
1750
|
-
"index-locator",
|
|
1751
|
-
"index-locator-list",
|
|
1752
|
-
"index-locator-range",
|
|
1753
|
-
"index-term",
|
|
1754
|
-
"index-term-categories",
|
|
1755
|
-
"index-term-category",
|
|
1756
|
-
"index-xref-preferred",
|
|
1757
|
-
"index-xref-related",
|
|
1758
|
-
"introduction",
|
|
1759
|
-
"keyword",
|
|
1760
|
-
"keywords",
|
|
1761
|
-
"label",
|
|
1762
|
-
"landmarks",
|
|
1763
|
-
"learning-objective",
|
|
1764
|
-
"learning-objectives",
|
|
1765
|
-
"learning-outcome",
|
|
1766
|
-
"learning-outcomes",
|
|
1767
|
-
"learning-resource",
|
|
1768
|
-
"learning-resources",
|
|
1769
|
-
"learning-standard",
|
|
1770
|
-
"learning-standards",
|
|
1771
|
-
"loa",
|
|
1772
|
-
"loi",
|
|
1773
|
-
"lot",
|
|
1774
|
-
"lov",
|
|
1775
|
-
"match-problem",
|
|
1776
|
-
"multiple-choice-problem",
|
|
1777
|
-
"noteref",
|
|
1778
|
-
"notice",
|
|
1779
|
-
"ordinal",
|
|
1780
|
-
"other-credits",
|
|
1781
|
-
"page-list",
|
|
1782
|
-
"pagebreak",
|
|
1783
|
-
"panel",
|
|
1784
|
-
"panel-group",
|
|
1785
|
-
"part",
|
|
1786
|
-
"practice",
|
|
1787
|
-
"practices",
|
|
1788
|
-
"preamble",
|
|
1789
|
-
"preface",
|
|
1790
|
-
"prologue",
|
|
1791
|
-
"pullquote",
|
|
1792
|
-
"qna",
|
|
1793
|
-
"question",
|
|
1794
|
-
"referrer",
|
|
1795
|
-
"revision-history",
|
|
1796
|
-
"seriespage",
|
|
1797
|
-
"sound-area",
|
|
1798
|
-
"subtitle",
|
|
1799
|
-
"tip",
|
|
1800
|
-
"title",
|
|
1801
|
-
"titlepage",
|
|
1802
|
-
"toc",
|
|
1803
|
-
"toc-brief",
|
|
1804
|
-
"topic-sentence",
|
|
1805
|
-
"true-false-problem",
|
|
1806
|
-
"volume"
|
|
1807
|
-
]);
|
|
1671
|
+
// src/references/url.ts
|
|
1672
|
+
function parseURL(urlString) {
|
|
1673
|
+
const hashIndex = urlString.indexOf("#");
|
|
1674
|
+
if (hashIndex === -1) {
|
|
1675
|
+
return {
|
|
1676
|
+
url: urlString,
|
|
1677
|
+
resource: urlString,
|
|
1678
|
+
hasFragment: false
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
const resource = urlString.substring(0, hashIndex);
|
|
1682
|
+
const fragment = urlString.substring(hashIndex + 1);
|
|
1683
|
+
const result = {
|
|
1684
|
+
url: urlString,
|
|
1685
|
+
resource,
|
|
1686
|
+
hasFragment: true
|
|
1687
|
+
};
|
|
1688
|
+
if (fragment) {
|
|
1689
|
+
result.fragment = fragment;
|
|
1690
|
+
}
|
|
1691
|
+
return result;
|
|
1692
|
+
}
|
|
1693
|
+
function isDataURL(url) {
|
|
1694
|
+
return url.startsWith("data:");
|
|
1695
|
+
}
|
|
1696
|
+
function isFileURL(url) {
|
|
1697
|
+
return url.startsWith("file:");
|
|
1698
|
+
}
|
|
1699
|
+
function isRelativeURL(url) {
|
|
1700
|
+
const regex = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
|
|
1701
|
+
return regex.exec(url) === null;
|
|
1702
|
+
}
|
|
1703
|
+
function hasAbsolutePath(url) {
|
|
1704
|
+
return url.startsWith("/");
|
|
1705
|
+
}
|
|
1706
|
+
function isMalformedURL(url) {
|
|
1707
|
+
if (!url.trim()) return true;
|
|
1708
|
+
if (/[\s<>]/.test(url)) return true;
|
|
1709
|
+
return false;
|
|
1710
|
+
}
|
|
1711
|
+
function isHTTPS(url) {
|
|
1712
|
+
return url.startsWith("https://");
|
|
1713
|
+
}
|
|
1714
|
+
function isHTTP(url) {
|
|
1715
|
+
return url.startsWith("http://");
|
|
1716
|
+
}
|
|
1717
|
+
function isRemoteURL(url) {
|
|
1718
|
+
return isHTTP(url) || isHTTPS(url);
|
|
1719
|
+
}
|
|
1720
|
+
function checkUrlLeaking(href, resourcePath) {
|
|
1721
|
+
const TEST_BASE_A = "https://a.example.org/A/";
|
|
1722
|
+
const TEST_BASE_B = "https://b.example.org/B/";
|
|
1723
|
+
try {
|
|
1724
|
+
const baseA = resourcePath ? new URL(resourcePath, TEST_BASE_A).toString() : TEST_BASE_A;
|
|
1725
|
+
const baseB = resourcePath ? new URL(resourcePath, TEST_BASE_B).toString() : TEST_BASE_B;
|
|
1726
|
+
const urlA = new URL(href, baseA).toString();
|
|
1727
|
+
const urlB = new URL(href, baseB).toString();
|
|
1728
|
+
return !urlA.startsWith(TEST_BASE_A) || !urlB.startsWith(TEST_BASE_B);
|
|
1729
|
+
} catch {
|
|
1730
|
+
return false;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
function resolveManifestHref(opfDir, href) {
|
|
1734
|
+
if (isRemoteURL(href)) return href;
|
|
1735
|
+
try {
|
|
1736
|
+
const decoded = decodeURIComponent(href);
|
|
1737
|
+
const path = opfDir ? `${opfDir}/${decoded}` : decoded;
|
|
1738
|
+
return path.normalize("NFC");
|
|
1739
|
+
} catch {
|
|
1740
|
+
const path = opfDir ? `${opfDir}/${href}` : href;
|
|
1741
|
+
return path.normalize("NFC");
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1808
1744
|
|
|
1809
1745
|
// src/smil/clock.ts
|
|
1810
1746
|
var FULL_CLOCK_RE = /^(\d+):([0-5]\d):([0-5]\d)(\.\d+)?$/;
|
|
@@ -1850,491 +1786,91 @@ function isValidSmilClock(value) {
|
|
|
1850
1786
|
return !Number.isNaN(parseSmilClock(value));
|
|
1851
1787
|
}
|
|
1852
1788
|
|
|
1853
|
-
// src/
|
|
1854
|
-
var
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
var
|
|
1861
|
-
|
|
1862
|
-
|
|
1789
|
+
// src/types.ts
|
|
1790
|
+
var EPUB_VERSIONS = ["2.0", "3.0", "3.1", "3.2", "3.3"];
|
|
1791
|
+
|
|
1792
|
+
// src/util/doctype.ts
|
|
1793
|
+
var DOCTYPE_RE = /<!DOCTYPE\s+(\w+)\s*([^>]*)>/i;
|
|
1794
|
+
var PUBLIC_ID_RE = /\bPUBLIC\s+"([^"]*)"\s+"([^"]*)"/i;
|
|
1795
|
+
var SYSTEM_ID_RE = /\bSYSTEM\s+"([^"]*)"/i;
|
|
1796
|
+
var DEFAULT_MAX_BYTES = 2048;
|
|
1797
|
+
function parseDoctype(content, options = {}) {
|
|
1798
|
+
const { expectedRoot, maxBytes = DEFAULT_MAX_BYTES } = options;
|
|
1799
|
+
const scanned = content.length > maxBytes ? content.slice(0, maxBytes) : content;
|
|
1800
|
+
const doctypeMatch = DOCTYPE_RE.exec(scanned);
|
|
1801
|
+
if (!doctypeMatch) return null;
|
|
1802
|
+
const root = doctypeMatch[1] ?? "";
|
|
1803
|
+
if (expectedRoot && root.toLowerCase() !== expectedRoot.toLowerCase()) return null;
|
|
1804
|
+
const inner = doctypeMatch[2] ?? "";
|
|
1805
|
+
const publicMatch = PUBLIC_ID_RE.exec(inner);
|
|
1806
|
+
if (publicMatch) {
|
|
1807
|
+
return { root, publicId: publicMatch[1] ?? "", systemId: publicMatch[2] ?? "" };
|
|
1863
1808
|
}
|
|
1864
|
-
|
|
1865
|
-
|
|
1809
|
+
const systemMatch = SYSTEM_ID_RE.exec(inner);
|
|
1810
|
+
if (systemMatch) {
|
|
1811
|
+
return { root, publicId: "", systemId: systemMatch[1] ?? "" };
|
|
1866
1812
|
}
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
if (
|
|
1875
|
-
|
|
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();
|
|
1813
|
+
return { root, publicId: "", systemId: "" };
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
// src/util/encoding.ts
|
|
1817
|
+
function sniffXmlEncoding(data) {
|
|
1818
|
+
if (data.length < 2) return null;
|
|
1819
|
+
if (data.length >= 4) {
|
|
1820
|
+
if (data[0] === 0 && data[1] === 0 && data[2] === 254 && data[3] === 255) {
|
|
1821
|
+
return "UCS-4";
|
|
1895
1822
|
}
|
|
1896
|
-
|
|
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 {
|
|
1823
|
+
if (data[0] === 255 && data[1] === 254 && data[2] === 0 && data[3] === 0) {
|
|
1824
|
+
return "UCS-4";
|
|
1915
1825
|
}
|
|
1916
|
-
|
|
1917
|
-
|
|
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 {
|
|
1826
|
+
if (data[0] === 0 && data[1] === 0 && data[2] === 255 && data[3] === 254) {
|
|
1827
|
+
return "UCS-4";
|
|
1938
1828
|
}
|
|
1939
|
-
|
|
1940
|
-
|
|
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 {
|
|
1829
|
+
if (data[0] === 254 && data[1] === 255 && data[2] === 0 && data[3] === 0) {
|
|
1830
|
+
return "UCS-4";
|
|
1949
1831
|
}
|
|
1950
|
-
|
|
1951
|
-
|
|
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 {
|
|
1832
|
+
if (data[0] === 0 && data[1] === 0 && data[2] === 0 && data[3] === 60) {
|
|
1833
|
+
return "UCS-4";
|
|
1985
1834
|
}
|
|
1986
|
-
|
|
1987
|
-
|
|
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
|
-
});
|
|
1835
|
+
if (data[0] === 60 && data[1] === 0 && data[2] === 0 && data[3] === 0) {
|
|
1836
|
+
return "UCS-4";
|
|
1999
1837
|
}
|
|
2000
|
-
if (
|
|
2001
|
-
|
|
2002
|
-
id: MessageId.RSC_005,
|
|
2003
|
-
message: `Invalid SMIL clock value "${clipEnd}" in clipEnd attribute`,
|
|
2004
|
-
location
|
|
2005
|
-
});
|
|
1838
|
+
if (data[0] === 0 && data[1] === 0 && data[2] === 60 && data[3] === 0) {
|
|
1839
|
+
return "UCS-4";
|
|
2006
1840
|
}
|
|
2007
|
-
if (
|
|
2008
|
-
|
|
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
|
-
});
|
|
1841
|
+
if (data[0] === 0 && data[1] === 60 && data[2] === 0 && data[3] === 0) {
|
|
1842
|
+
return "UCS-4";
|
|
2020
1843
|
}
|
|
2021
1844
|
}
|
|
2022
|
-
|
|
2023
|
-
|
|
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
|
-
}
|
|
1845
|
+
if (data[0] === 254 && data[1] === 255) {
|
|
1846
|
+
return "UTF-16";
|
|
2049
1847
|
}
|
|
2050
|
-
|
|
2051
|
-
|
|
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
|
-
}
|
|
1848
|
+
if (data[0] === 255 && data[1] === 254) {
|
|
1849
|
+
return "UTF-16";
|
|
2077
1850
|
}
|
|
2078
|
-
|
|
2079
|
-
if (
|
|
2080
|
-
return
|
|
1851
|
+
if (data.length >= 4) {
|
|
1852
|
+
if (data[0] === 0 && data[1] === 60 && data[2] === 0 && data[3] === 63) {
|
|
1853
|
+
return "UTF-16";
|
|
2081
1854
|
}
|
|
2082
|
-
|
|
2083
|
-
|
|
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
|
-
}
|
|
1855
|
+
if (data[0] === 60 && data[1] === 0 && data[2] === 63 && data[3] === 0) {
|
|
1856
|
+
return "UTF-16";
|
|
2092
1857
|
}
|
|
2093
|
-
return resolved.join("/");
|
|
2094
1858
|
}
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
"
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
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
|
-
// src/references/url.ts
|
|
2183
|
-
function parseURL(urlString) {
|
|
2184
|
-
const hashIndex = urlString.indexOf("#");
|
|
2185
|
-
if (hashIndex === -1) {
|
|
2186
|
-
return {
|
|
2187
|
-
url: urlString,
|
|
2188
|
-
resource: urlString,
|
|
2189
|
-
hasFragment: false
|
|
2190
|
-
};
|
|
2191
|
-
}
|
|
2192
|
-
const resource = urlString.substring(0, hashIndex);
|
|
2193
|
-
const fragment = urlString.substring(hashIndex + 1);
|
|
2194
|
-
const result = {
|
|
2195
|
-
url: urlString,
|
|
2196
|
-
resource,
|
|
2197
|
-
hasFragment: true
|
|
2198
|
-
};
|
|
2199
|
-
if (fragment) {
|
|
2200
|
-
result.fragment = fragment;
|
|
2201
|
-
}
|
|
2202
|
-
return result;
|
|
2203
|
-
}
|
|
2204
|
-
function isDataURL(url) {
|
|
2205
|
-
return url.startsWith("data:");
|
|
2206
|
-
}
|
|
2207
|
-
function isFileURL(url) {
|
|
2208
|
-
return url.startsWith("file:");
|
|
2209
|
-
}
|
|
2210
|
-
function hasAbsolutePath(url) {
|
|
2211
|
-
return url.startsWith("/");
|
|
2212
|
-
}
|
|
2213
|
-
function hasParentDirectoryReference(url) {
|
|
2214
|
-
return url.includes("..");
|
|
2215
|
-
}
|
|
2216
|
-
function isMalformedURL(url) {
|
|
2217
|
-
if (!url.trim()) return true;
|
|
2218
|
-
if (/[\s<>]/.test(url)) return true;
|
|
2219
|
-
return false;
|
|
2220
|
-
}
|
|
2221
|
-
function isHTTPS(url) {
|
|
2222
|
-
return url.startsWith("https://");
|
|
2223
|
-
}
|
|
2224
|
-
function isHTTP(url) {
|
|
2225
|
-
return url.startsWith("http://");
|
|
2226
|
-
}
|
|
2227
|
-
function isRemoteURL(url) {
|
|
2228
|
-
return isHTTP(url) || isHTTPS(url);
|
|
2229
|
-
}
|
|
2230
|
-
function checkUrlLeaking(href) {
|
|
2231
|
-
const TEST_BASE_A = "https://a.example.org/A/";
|
|
2232
|
-
const TEST_BASE_B = "https://b.example.org/B/";
|
|
2233
|
-
try {
|
|
2234
|
-
const urlA = new URL(href, TEST_BASE_A).toString();
|
|
2235
|
-
const urlB = new URL(href, TEST_BASE_B).toString();
|
|
2236
|
-
return !urlA.startsWith(TEST_BASE_A) || !urlB.startsWith(TEST_BASE_B);
|
|
2237
|
-
} catch {
|
|
2238
|
-
return false;
|
|
2239
|
-
}
|
|
2240
|
-
}
|
|
2241
|
-
function resolveManifestHref(opfDir, href) {
|
|
2242
|
-
if (isRemoteURL(href)) return href;
|
|
2243
|
-
try {
|
|
2244
|
-
const decoded = decodeURIComponent(href);
|
|
2245
|
-
const path = opfDir ? `${opfDir}/${decoded}` : decoded;
|
|
2246
|
-
return path.normalize("NFC");
|
|
2247
|
-
} catch {
|
|
2248
|
-
const path = opfDir ? `${opfDir}/${href}` : href;
|
|
2249
|
-
return path.normalize("NFC");
|
|
2250
|
-
}
|
|
2251
|
-
}
|
|
2252
|
-
|
|
2253
|
-
// src/types.ts
|
|
2254
|
-
var EPUB_VERSIONS = ["2.0", "3.0", "3.1", "3.2", "3.3"];
|
|
2255
|
-
|
|
2256
|
-
// src/util/doctype.ts
|
|
2257
|
-
var DOCTYPE_RE = /<!DOCTYPE\s+(\w+)\s*([^>]*)>/i;
|
|
2258
|
-
var PUBLIC_ID_RE = /\bPUBLIC\s+"([^"]*)"\s+"([^"]*)"/i;
|
|
2259
|
-
var SYSTEM_ID_RE = /\bSYSTEM\s+"([^"]*)"/i;
|
|
2260
|
-
var DEFAULT_MAX_BYTES = 2048;
|
|
2261
|
-
function parseDoctype(content, options = {}) {
|
|
2262
|
-
const { expectedRoot, maxBytes = DEFAULT_MAX_BYTES } = options;
|
|
2263
|
-
const scanned = content.length > maxBytes ? content.slice(0, maxBytes) : content;
|
|
2264
|
-
const doctypeMatch = DOCTYPE_RE.exec(scanned);
|
|
2265
|
-
if (!doctypeMatch) return null;
|
|
2266
|
-
const root = doctypeMatch[1] ?? "";
|
|
2267
|
-
if (expectedRoot && root.toLowerCase() !== expectedRoot.toLowerCase()) return null;
|
|
2268
|
-
const inner = doctypeMatch[2] ?? "";
|
|
2269
|
-
const publicMatch = PUBLIC_ID_RE.exec(inner);
|
|
2270
|
-
if (publicMatch) {
|
|
2271
|
-
return { root, publicId: publicMatch[1] ?? "", systemId: publicMatch[2] ?? "" };
|
|
2272
|
-
}
|
|
2273
|
-
const systemMatch = SYSTEM_ID_RE.exec(inner);
|
|
2274
|
-
if (systemMatch) {
|
|
2275
|
-
return { root, publicId: "", systemId: systemMatch[1] ?? "" };
|
|
2276
|
-
}
|
|
2277
|
-
return { root, publicId: "", systemId: "" };
|
|
2278
|
-
}
|
|
2279
|
-
|
|
2280
|
-
// src/util/encoding.ts
|
|
2281
|
-
function sniffXmlEncoding(data) {
|
|
2282
|
-
if (data.length < 2) return null;
|
|
2283
|
-
if (data.length >= 4) {
|
|
2284
|
-
if (data[0] === 0 && data[1] === 0 && data[2] === 254 && data[3] === 255) {
|
|
2285
|
-
return "UCS-4";
|
|
2286
|
-
}
|
|
2287
|
-
if (data[0] === 255 && data[1] === 254 && data[2] === 0 && data[3] === 0) {
|
|
2288
|
-
return "UCS-4";
|
|
2289
|
-
}
|
|
2290
|
-
if (data[0] === 0 && data[1] === 0 && data[2] === 255 && data[3] === 254) {
|
|
2291
|
-
return "UCS-4";
|
|
2292
|
-
}
|
|
2293
|
-
if (data[0] === 254 && data[1] === 255 && data[2] === 0 && data[3] === 0) {
|
|
2294
|
-
return "UCS-4";
|
|
2295
|
-
}
|
|
2296
|
-
if (data[0] === 0 && data[1] === 0 && data[2] === 0 && data[3] === 60) {
|
|
2297
|
-
return "UCS-4";
|
|
2298
|
-
}
|
|
2299
|
-
if (data[0] === 60 && data[1] === 0 && data[2] === 0 && data[3] === 0) {
|
|
2300
|
-
return "UCS-4";
|
|
2301
|
-
}
|
|
2302
|
-
if (data[0] === 0 && data[1] === 0 && data[2] === 60 && data[3] === 0) {
|
|
2303
|
-
return "UCS-4";
|
|
2304
|
-
}
|
|
2305
|
-
if (data[0] === 0 && data[1] === 60 && data[2] === 0 && data[3] === 0) {
|
|
2306
|
-
return "UCS-4";
|
|
2307
|
-
}
|
|
2308
|
-
}
|
|
2309
|
-
if (data[0] === 254 && data[1] === 255) {
|
|
2310
|
-
return "UTF-16";
|
|
2311
|
-
}
|
|
2312
|
-
if (data[0] === 255 && data[1] === 254) {
|
|
2313
|
-
return "UTF-16";
|
|
2314
|
-
}
|
|
2315
|
-
if (data.length >= 4) {
|
|
2316
|
-
if (data[0] === 0 && data[1] === 60 && data[2] === 0 && data[3] === 63) {
|
|
2317
|
-
return "UTF-16";
|
|
2318
|
-
}
|
|
2319
|
-
if (data[0] === 60 && data[1] === 0 && data[2] === 63 && data[3] === 0) {
|
|
2320
|
-
return "UTF-16";
|
|
2321
|
-
}
|
|
2322
|
-
}
|
|
2323
|
-
if (data.length >= 3 && data[0] === 239 && data[1] === 187 && data[2] === 191) {
|
|
2324
|
-
return null;
|
|
2325
|
-
}
|
|
2326
|
-
if (data.length >= 4 && data[0] === 76 && data[1] === 111 && data[2] === 167 && data[3] === 148) {
|
|
2327
|
-
return "EBCDIC";
|
|
2328
|
-
}
|
|
2329
|
-
const prefix = String.fromCharCode(...data.slice(0, Math.min(256, data.length)));
|
|
2330
|
-
const match = /^<\?xml[^?]*\bencoding\s*=\s*["']([^"']+)["']/.exec(prefix);
|
|
2331
|
-
if (match) {
|
|
2332
|
-
const declared = (match[1] ?? "").toUpperCase();
|
|
2333
|
-
if (declared === "UTF-8") return null;
|
|
2334
|
-
return declared;
|
|
2335
|
-
}
|
|
2336
|
-
return null;
|
|
2337
|
-
}
|
|
1859
|
+
if (data.length >= 3 && data[0] === 239 && data[1] === 187 && data[2] === 191) {
|
|
1860
|
+
return null;
|
|
1861
|
+
}
|
|
1862
|
+
if (data.length >= 4 && data[0] === 76 && data[1] === 111 && data[2] === 167 && data[3] === 148) {
|
|
1863
|
+
return "EBCDIC";
|
|
1864
|
+
}
|
|
1865
|
+
const prefix = String.fromCharCode(...data.slice(0, Math.min(256, data.length)));
|
|
1866
|
+
const match = /^<\?xml[^?]*\bencoding\s*=\s*["']([^"']+)["']/.exec(prefix);
|
|
1867
|
+
if (match) {
|
|
1868
|
+
const declared = (match[1] ?? "").toUpperCase();
|
|
1869
|
+
if (declared === "UTF-8") return null;
|
|
1870
|
+
return declared;
|
|
1871
|
+
}
|
|
1872
|
+
return null;
|
|
1873
|
+
}
|
|
2338
1874
|
|
|
2339
1875
|
// src/opf/parser.ts
|
|
2340
1876
|
function parseOPF(xml) {
|
|
@@ -2360,6 +1896,8 @@ function parseOPF(xml) {
|
|
|
2360
1896
|
}
|
|
2361
1897
|
}
|
|
2362
1898
|
}
|
|
1899
|
+
const nsRegex = /<package[^>]*\sxmlns=["']([^"']+)["']/;
|
|
1900
|
+
const isLegacyOebps12 = nsRegex.exec(xml)?.[1] === "http://openebook.org/namespaces/oeb-package/1.0/";
|
|
2363
1901
|
const prefixes = parsePrefixes(xml);
|
|
2364
1902
|
const dirRegex = /<package[^>]*\sdir=["']([^"']+)["']/;
|
|
2365
1903
|
const dirMatch = dirRegex.exec(xml);
|
|
@@ -2398,6 +1936,9 @@ function parseOPF(xml) {
|
|
|
2398
1936
|
if (!versionDeclared) {
|
|
2399
1937
|
result.versionDeclared = false;
|
|
2400
1938
|
}
|
|
1939
|
+
if (isLegacyOebps12) {
|
|
1940
|
+
result.isLegacyOebps12 = true;
|
|
1941
|
+
}
|
|
2401
1942
|
if (Object.keys(prefixes).length > 0) {
|
|
2402
1943
|
result.prefixes = prefixes;
|
|
2403
1944
|
}
|
|
@@ -2488,7 +2029,7 @@ function parseDCElements(metadataXml) {
|
|
|
2488
2029
|
}
|
|
2489
2030
|
function parseMetaElements(metadataXml) {
|
|
2490
2031
|
const elements = [];
|
|
2491
|
-
const metaRegex = /<meta([^>]*property=["'][^"']+["'][^>]*)>([^<]*)<\/meta>/g;
|
|
2032
|
+
const metaRegex = /<(?:[A-Za-z_][\w.-]*:)?meta([^>]*property=["'][^"']+["'][^>]*)>([^<]*)<\/(?:[A-Za-z_][\w.-]*:)?meta>/g;
|
|
2492
2033
|
let match;
|
|
2493
2034
|
while ((match = metaRegex.exec(metadataXml)) !== null) {
|
|
2494
2035
|
const attrsStr = match[1] ?? "";
|
|
@@ -2698,44 +2239,129 @@ function parseCollections(xml) {
|
|
|
2698
2239
|
return roots;
|
|
2699
2240
|
}
|
|
2700
2241
|
|
|
2701
|
-
// src/opf/
|
|
2702
|
-
var
|
|
2703
|
-
|
|
2704
|
-
"
|
|
2705
|
-
"
|
|
2706
|
-
"
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
"
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
"
|
|
2732
|
-
"
|
|
2733
|
-
|
|
2734
|
-
"
|
|
2735
|
-
|
|
2736
|
-
"
|
|
2737
|
-
|
|
2738
|
-
|
|
2242
|
+
// src/opf/types.ts
|
|
2243
|
+
var CORE_MEDIA_TYPES = /* @__PURE__ */ new Set([
|
|
2244
|
+
// Image types
|
|
2245
|
+
"image/gif",
|
|
2246
|
+
"image/jpeg",
|
|
2247
|
+
"image/png",
|
|
2248
|
+
"image/svg+xml",
|
|
2249
|
+
"image/webp",
|
|
2250
|
+
// Audio types
|
|
2251
|
+
"audio/mpeg",
|
|
2252
|
+
"audio/mp4",
|
|
2253
|
+
"audio/ogg",
|
|
2254
|
+
// CSS
|
|
2255
|
+
"text/css",
|
|
2256
|
+
// Fonts
|
|
2257
|
+
"font/otf",
|
|
2258
|
+
"font/ttf",
|
|
2259
|
+
"font/woff",
|
|
2260
|
+
"font/woff2",
|
|
2261
|
+
"application/font-sfnt",
|
|
2262
|
+
// deprecated alias for font/otf, font/ttf
|
|
2263
|
+
"application/font-woff",
|
|
2264
|
+
// deprecated alias for font/woff
|
|
2265
|
+
"application/vnd.ms-opentype",
|
|
2266
|
+
// deprecated alias
|
|
2267
|
+
// Content documents
|
|
2268
|
+
"application/xhtml+xml",
|
|
2269
|
+
"application/x-dtbncx+xml",
|
|
2270
|
+
// NCX
|
|
2271
|
+
// JavaScript (EPUB 3)
|
|
2272
|
+
"text/javascript",
|
|
2273
|
+
"application/javascript",
|
|
2274
|
+
// Media overlays
|
|
2275
|
+
"application/smil+xml",
|
|
2276
|
+
// PLS (Pronunciation Lexicon)
|
|
2277
|
+
"application/pls+xml"
|
|
2278
|
+
]);
|
|
2279
|
+
function isCoreMediaType(mimeType) {
|
|
2280
|
+
if (CORE_MEDIA_TYPES.has(mimeType)) return true;
|
|
2281
|
+
if (mimeType.startsWith("video/")) return true;
|
|
2282
|
+
if (/^audio\/ogg\s*;\s*codecs=opus$/i.test(mimeType)) return true;
|
|
2283
|
+
const semicolonIndex = mimeType.indexOf(";");
|
|
2284
|
+
if (semicolonIndex >= 0) {
|
|
2285
|
+
const baseType = mimeType.substring(0, semicolonIndex).trim();
|
|
2286
|
+
if (CORE_MEDIA_TYPES.has(baseType)) return true;
|
|
2287
|
+
if (baseType.startsWith("video/")) return true;
|
|
2288
|
+
}
|
|
2289
|
+
return false;
|
|
2290
|
+
}
|
|
2291
|
+
var ITEM_PROPERTIES = /* @__PURE__ */ new Set([
|
|
2292
|
+
"cover-image",
|
|
2293
|
+
"data-nav",
|
|
2294
|
+
"dictionary",
|
|
2295
|
+
"glossary",
|
|
2296
|
+
"index",
|
|
2297
|
+
"mathml",
|
|
2298
|
+
"nav",
|
|
2299
|
+
"remote-resources",
|
|
2300
|
+
"scripted",
|
|
2301
|
+
"search-key-map",
|
|
2302
|
+
"svg",
|
|
2303
|
+
"switch"
|
|
2304
|
+
]);
|
|
2305
|
+
var LINK_PROPERTIES = /* @__PURE__ */ new Set(["onix", "marc21xml-record", "mods-record", "xmp-record"]);
|
|
2306
|
+
var SPINE_PROPERTIES = /* @__PURE__ */ new Set([
|
|
2307
|
+
"page-spread-left",
|
|
2308
|
+
"page-spread-right",
|
|
2309
|
+
"rendition:spread-none",
|
|
2310
|
+
"rendition:spread-landscape",
|
|
2311
|
+
"rendition:spread-portrait",
|
|
2312
|
+
"rendition:spread-both",
|
|
2313
|
+
"rendition:spread-auto",
|
|
2314
|
+
"rendition:page-spread-center",
|
|
2315
|
+
"rendition:layout-reflowable",
|
|
2316
|
+
"rendition:layout-pre-paginated",
|
|
2317
|
+
"rendition:orientation-auto",
|
|
2318
|
+
"rendition:orientation-landscape",
|
|
2319
|
+
"rendition:orientation-portrait",
|
|
2320
|
+
"rendition:flow-auto",
|
|
2321
|
+
"rendition:flow-paginated",
|
|
2322
|
+
"rendition:flow-scrolled-continuous",
|
|
2323
|
+
"rendition:flow-scrolled-doc",
|
|
2324
|
+
"rendition:align-x-center"
|
|
2325
|
+
]);
|
|
2326
|
+
|
|
2327
|
+
// src/opf/validator.ts
|
|
2328
|
+
var DEPRECATED_MEDIA_TYPES = /* @__PURE__ */ new Set([
|
|
2329
|
+
"text/x-oeb1-document",
|
|
2330
|
+
"text/x-oeb1-css",
|
|
2331
|
+
"application/x-oeb1-package",
|
|
2332
|
+
"text/x-oeb1-html"
|
|
2333
|
+
]);
|
|
2334
|
+
function getPreferredMediaType(mimeType, path) {
|
|
2335
|
+
switch (mimeType) {
|
|
2336
|
+
case "application/font-sfnt":
|
|
2337
|
+
if (path.endsWith(".ttf")) return "font/ttf";
|
|
2338
|
+
if (path.endsWith(".otf")) return "font/otf";
|
|
2339
|
+
return "font/(ttf|otf)";
|
|
2340
|
+
case "application/vnd.ms-opentype":
|
|
2341
|
+
return "font/otf";
|
|
2342
|
+
case "application/font-woff":
|
|
2343
|
+
return "font/woff";
|
|
2344
|
+
case "application/x-font-ttf":
|
|
2345
|
+
return "font/ttf";
|
|
2346
|
+
case "text/javascript":
|
|
2347
|
+
case "application/ecmascript":
|
|
2348
|
+
return "application/javascript";
|
|
2349
|
+
default:
|
|
2350
|
+
return null;
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
var VALID_RELATOR_CODES = /* @__PURE__ */ new Set([
|
|
2354
|
+
"abr",
|
|
2355
|
+
"acp",
|
|
2356
|
+
"act",
|
|
2357
|
+
"adi",
|
|
2358
|
+
"adp",
|
|
2359
|
+
"aft",
|
|
2360
|
+
"anl",
|
|
2361
|
+
"anm",
|
|
2362
|
+
"ann",
|
|
2363
|
+
"ant",
|
|
2364
|
+
"ape",
|
|
2739
2365
|
"apl",
|
|
2740
2366
|
"app",
|
|
2741
2367
|
"aqt",
|
|
@@ -3173,6 +2799,9 @@ var OPFValidator = class {
|
|
|
3173
2799
|
}
|
|
3174
2800
|
this.validatePackageAttributes(context, opfPath);
|
|
3175
2801
|
this.validateMetadata(context, opfPath);
|
|
2802
|
+
if (this.packageDoc.version !== "2.0") {
|
|
2803
|
+
this.validateMetaPrefixes(context, opfPath, opfXml);
|
|
2804
|
+
}
|
|
3176
2805
|
this.validateLinkElements(context, opfPath);
|
|
3177
2806
|
this.validateManifest(context, opfPath);
|
|
3178
2807
|
this.validateSpine(context, opfPath);
|
|
@@ -4366,6 +3995,7 @@ var OPFValidator = class {
|
|
|
4366
3995
|
if (!this.packageDoc) return;
|
|
4367
3996
|
const seenIds = /* @__PURE__ */ new Set();
|
|
4368
3997
|
const seenHrefs = /* @__PURE__ */ new Set();
|
|
3998
|
+
const declaredPrefixes = this.packageDoc.prefixes ?? {};
|
|
4369
3999
|
for (const item of this.packageDoc.manifest) {
|
|
4370
4000
|
if (seenIds.has(item.id)) {
|
|
4371
4001
|
pushMessage(context.messages, {
|
|
@@ -4409,7 +4039,7 @@ var OPFValidator = class {
|
|
|
4409
4039
|
});
|
|
4410
4040
|
}
|
|
4411
4041
|
if (!item.href.startsWith("http") && !item.href.startsWith("mailto:")) {
|
|
4412
|
-
const leaked = checkUrlLeaking(item.href);
|
|
4042
|
+
const leaked = checkUrlLeaking(item.href, opfPath);
|
|
4413
4043
|
if (leaked) {
|
|
4414
4044
|
pushMessage(context.messages, {
|
|
4415
4045
|
id: MessageId.RSC_026,
|
|
@@ -4444,11 +4074,11 @@ var OPFValidator = class {
|
|
|
4444
4074
|
if (DEPRECATED_MEDIA_TYPES.has(item.mediaType) || item.mediaType === "text/html") {
|
|
4445
4075
|
if (this.packageDoc.version === "2.0" && item.mediaType === "text/html") {
|
|
4446
4076
|
pushMessage(context.messages, {
|
|
4447
|
-
id: MessageId.OPF_035,
|
|
4077
|
+
id: this.packageDoc.isLegacyOebps12 ? MessageId.OPF_038 : MessageId.OPF_035,
|
|
4448
4078
|
message: `XHTML Content Document "${item.id}" is declared as "text/html"`,
|
|
4449
4079
|
location: { path: opfPath }
|
|
4450
4080
|
});
|
|
4451
|
-
} else if (this.packageDoc.version === "2.0") {
|
|
4081
|
+
} else if (this.packageDoc.version === "2.0" && !this.packageDoc.isLegacyOebps12) {
|
|
4452
4082
|
pushMessage(context.messages, {
|
|
4453
4083
|
id: MessageId.OPF_037,
|
|
4454
4084
|
message: `Found deprecated media-type "${item.mediaType}"`,
|
|
@@ -4456,6 +4086,13 @@ var OPFValidator = class {
|
|
|
4456
4086
|
});
|
|
4457
4087
|
}
|
|
4458
4088
|
}
|
|
4089
|
+
if (this.packageDoc.version === "2.0" && this.packageDoc.isLegacyOebps12 && item.mediaType === "text/css" && !item.fallback) {
|
|
4090
|
+
pushMessage(context.messages, {
|
|
4091
|
+
id: MessageId.OPF_039,
|
|
4092
|
+
message: `Media type "${item.mediaType}" requires a fallback in legacy OEBPS 1.2 context`,
|
|
4093
|
+
location: { path: opfPath }
|
|
4094
|
+
});
|
|
4095
|
+
}
|
|
4459
4096
|
const preferred = getPreferredMediaType(item.mediaType, fullPath);
|
|
4460
4097
|
if (preferred !== null) {
|
|
4461
4098
|
pushMessage(context.messages, {
|
|
@@ -4466,13 +4103,14 @@ var OPFValidator = class {
|
|
|
4466
4103
|
}
|
|
4467
4104
|
if (this.packageDoc.version !== "2.0" && item.properties) {
|
|
4468
4105
|
for (const prop of item.properties) {
|
|
4469
|
-
if (
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
}
|
|
4475
|
-
|
|
4106
|
+
if (ITEM_PROPERTIES.has(prop)) continue;
|
|
4107
|
+
const colon = prop.indexOf(":");
|
|
4108
|
+
if (colon > 0 && declaredPrefixes[prop.slice(0, colon)] !== void 0) continue;
|
|
4109
|
+
pushMessage(context.messages, {
|
|
4110
|
+
id: MessageId.OPF_027,
|
|
4111
|
+
message: `Undefined property: "${prop}"`,
|
|
4112
|
+
location: { path: opfPath }
|
|
4113
|
+
});
|
|
4476
4114
|
}
|
|
4477
4115
|
if (item.properties.includes("nav")) {
|
|
4478
4116
|
if (item.mediaType !== "application/xhtml+xml") {
|
|
@@ -4635,6 +4273,42 @@ var OPFValidator = class {
|
|
|
4635
4273
|
* emits one assertion failure per offending element (so two duplicate ids
|
|
4636
4274
|
* produce two RSC-005 messages).
|
|
4637
4275
|
*/
|
|
4276
|
+
validateMetaPrefixes(context, opfPath, opfXml) {
|
|
4277
|
+
if (!this.packageDoc) return;
|
|
4278
|
+
const RESERVED = /* @__PURE__ */ new Set([
|
|
4279
|
+
"dcterms",
|
|
4280
|
+
"marc",
|
|
4281
|
+
"onix",
|
|
4282
|
+
"schema",
|
|
4283
|
+
"xsd",
|
|
4284
|
+
"a11y",
|
|
4285
|
+
"media",
|
|
4286
|
+
"rendition"
|
|
4287
|
+
]);
|
|
4288
|
+
const declared = new Set(Object.keys(this.packageDoc.prefixes ?? {}));
|
|
4289
|
+
const reported = /* @__PURE__ */ new Set();
|
|
4290
|
+
const reportIfUndeclared = (prefix) => {
|
|
4291
|
+
if (!prefix || reported.has(prefix)) return;
|
|
4292
|
+
if (RESERVED.has(prefix) || declared.has(prefix)) return;
|
|
4293
|
+
reported.add(prefix);
|
|
4294
|
+
pushMessage(context.messages, {
|
|
4295
|
+
id: MessageId.OPF_028,
|
|
4296
|
+
message: `Undeclared prefix: "${prefix}"`,
|
|
4297
|
+
location: { path: opfPath }
|
|
4298
|
+
});
|
|
4299
|
+
};
|
|
4300
|
+
const stripped = stripXmlComments(opfXml);
|
|
4301
|
+
const attrRegex = /\b(?:property|scheme|rel)\s*=\s*["']([^"']+)["']/g;
|
|
4302
|
+
for (const match of stripped.matchAll(attrRegex)) {
|
|
4303
|
+
const value = match[1]?.trim();
|
|
4304
|
+
if (!value) continue;
|
|
4305
|
+
for (const token of value.split(/\s+/)) {
|
|
4306
|
+
const colon = token.indexOf(":");
|
|
4307
|
+
if (colon <= 0) continue;
|
|
4308
|
+
reportIfUndeclared(token.slice(0, colon));
|
|
4309
|
+
}
|
|
4310
|
+
}
|
|
4311
|
+
}
|
|
4638
4312
|
validateOpfIdUniqueness(context, opfPath, opfXml) {
|
|
4639
4313
|
const stripped = stripXmlComments(opfXml);
|
|
4640
4314
|
const counts = /* @__PURE__ */ new Map();
|
|
@@ -4666,11 +4340,12 @@ var OPFValidator = class {
|
|
|
4666
4340
|
for (const item of this.packageDoc.manifest) {
|
|
4667
4341
|
const hrefBase = item.href.split("?")[0] ?? item.href;
|
|
4668
4342
|
if (/^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(hrefBase)) continue;
|
|
4669
|
-
manifestPaths.add(resolvePath(opfPath, hrefBase).normalize("NFC"));
|
|
4343
|
+
manifestPaths.add(resolvePath(opfPath, tryDecodeUriComponent(hrefBase)).normalize("NFC"));
|
|
4670
4344
|
}
|
|
4671
4345
|
const rootfilePaths = new Set(context.rootfiles.map((r) => r.path.normalize("NFC")));
|
|
4672
4346
|
for (const path of context.files.keys()) {
|
|
4673
4347
|
if (path === "mimetype") continue;
|
|
4348
|
+
if (path.endsWith("/")) continue;
|
|
4674
4349
|
if (path.startsWith("META-INF/")) continue;
|
|
4675
4350
|
if (rootfilePaths.has(path)) continue;
|
|
4676
4351
|
if (manifestPaths.has(path)) continue;
|
|
@@ -5256,26 +4931,493 @@ function isValidW3CDateFormat(dateStr) {
|
|
|
5256
4931
|
if (seconds < 0 || seconds > 59) return false;
|
|
5257
4932
|
return true;
|
|
5258
4933
|
}
|
|
5259
|
-
return false;
|
|
5260
|
-
}
|
|
5261
|
-
|
|
5262
|
-
// src/references/types.ts
|
|
5263
|
-
var PUBLICATION_RESOURCE_TYPES = /* @__PURE__ */ new Set([
|
|
5264
|
-
"generic" /* GENERIC */,
|
|
5265
|
-
"stylesheet" /* STYLESHEET */,
|
|
5266
|
-
"font" /* FONT */,
|
|
5267
|
-
"image" /* IMAGE */,
|
|
5268
|
-
"audio" /* AUDIO */,
|
|
5269
|
-
"video" /* VIDEO */,
|
|
5270
|
-
"track" /* TRACK */,
|
|
5271
|
-
"media-overlay" /* MEDIA_OVERLAY */,
|
|
5272
|
-
"svg-symbol" /* SVG_SYMBOL */,
|
|
5273
|
-
"svg-paint" /* SVG_PAINT */,
|
|
5274
|
-
"svg-clip-path" /* SVG_CLIP_PATH */
|
|
5275
|
-
]);
|
|
5276
|
-
function isPublicationResourceReference(type) {
|
|
5277
|
-
return PUBLICATION_RESOURCE_TYPES.has(type);
|
|
5278
|
-
}
|
|
4934
|
+
return false;
|
|
4935
|
+
}
|
|
4936
|
+
|
|
4937
|
+
// src/references/types.ts
|
|
4938
|
+
var PUBLICATION_RESOURCE_TYPES = /* @__PURE__ */ new Set([
|
|
4939
|
+
"generic" /* GENERIC */,
|
|
4940
|
+
"stylesheet" /* STYLESHEET */,
|
|
4941
|
+
"font" /* FONT */,
|
|
4942
|
+
"image" /* IMAGE */,
|
|
4943
|
+
"audio" /* AUDIO */,
|
|
4944
|
+
"video" /* VIDEO */,
|
|
4945
|
+
"track" /* TRACK */,
|
|
4946
|
+
"media-overlay" /* MEDIA_OVERLAY */,
|
|
4947
|
+
"svg-symbol" /* SVG_SYMBOL */,
|
|
4948
|
+
"svg-paint" /* SVG_PAINT */,
|
|
4949
|
+
"svg-clip-path" /* SVG_CLIP_PATH */
|
|
4950
|
+
]);
|
|
4951
|
+
function isPublicationResourceReference(type) {
|
|
4952
|
+
return PUBLICATION_RESOURCE_TYPES.has(type);
|
|
4953
|
+
}
|
|
4954
|
+
|
|
4955
|
+
// src/skm/validator.ts
|
|
4956
|
+
var OPS_NS_URI = "http://www.idpf.org/2007/ops";
|
|
4957
|
+
var SKM_NS = { ops: OPS_NS_URI };
|
|
4958
|
+
var SKMValidator = class {
|
|
4959
|
+
validate(context, path, refValidator) {
|
|
4960
|
+
const data = context.files.get(path);
|
|
4961
|
+
if (!data) return;
|
|
4962
|
+
const content = typeof data === "string" ? data : new TextDecoder().decode(data);
|
|
4963
|
+
let doc = null;
|
|
4964
|
+
try {
|
|
4965
|
+
doc = libxml2Wasm.XmlDocument.fromString(content);
|
|
4966
|
+
} catch {
|
|
4967
|
+
pushMessage(context.messages, {
|
|
4968
|
+
id: MessageId.RSC_016,
|
|
4969
|
+
message: "Search Key Map document is not well-formed XML",
|
|
4970
|
+
location: { path }
|
|
4971
|
+
});
|
|
4972
|
+
return;
|
|
4973
|
+
}
|
|
4974
|
+
try {
|
|
4975
|
+
const root = doc.root;
|
|
4976
|
+
if (root.namespaceUri !== OPS_NS_URI || root.name !== "search-key-map") {
|
|
4977
|
+
pushMessage(context.messages, {
|
|
4978
|
+
id: MessageId.RSC_005,
|
|
4979
|
+
message: `Root element must be "search-key-map" in the OPS namespace`,
|
|
4980
|
+
location: { path, line: root.line }
|
|
4981
|
+
});
|
|
4982
|
+
return;
|
|
4983
|
+
}
|
|
4984
|
+
const groups = root.find("./ops:search-key-group", SKM_NS);
|
|
4985
|
+
if (groups.length === 0) {
|
|
4986
|
+
pushMessage(context.messages, {
|
|
4987
|
+
id: MessageId.RSC_005,
|
|
4988
|
+
message: 'A "search-key-map" must contain at least one "search-key-group"',
|
|
4989
|
+
location: { path, line: root.line }
|
|
4990
|
+
});
|
|
4991
|
+
}
|
|
4992
|
+
for (const group of groups) {
|
|
4993
|
+
const groupEl = group;
|
|
4994
|
+
const href = groupEl.attr("href")?.value;
|
|
4995
|
+
if (!href) {
|
|
4996
|
+
pushMessage(context.messages, {
|
|
4997
|
+
id: MessageId.RSC_005,
|
|
4998
|
+
message: 'The "href" attribute is required on "search-key-group"',
|
|
4999
|
+
location: { path, line: groupEl.line }
|
|
5000
|
+
});
|
|
5001
|
+
} else if (refValidator) {
|
|
5002
|
+
registerSkmRef(refValidator, path, href, groupEl.line);
|
|
5003
|
+
}
|
|
5004
|
+
const matches = groupEl.find("./ops:match", SKM_NS);
|
|
5005
|
+
if (matches.length === 0) {
|
|
5006
|
+
pushMessage(context.messages, {
|
|
5007
|
+
id: MessageId.RSC_005,
|
|
5008
|
+
message: 'A "search-key-group" must contain at least one "match"',
|
|
5009
|
+
location: { path, line: groupEl.line }
|
|
5010
|
+
});
|
|
5011
|
+
}
|
|
5012
|
+
for (const match of matches) {
|
|
5013
|
+
const matchEl = match;
|
|
5014
|
+
const matchHref = matchEl.attr("href")?.value;
|
|
5015
|
+
if (matchHref && refValidator) {
|
|
5016
|
+
registerSkmRef(refValidator, path, matchHref, matchEl.line);
|
|
5017
|
+
}
|
|
5018
|
+
}
|
|
5019
|
+
}
|
|
5020
|
+
} finally {
|
|
5021
|
+
doc.dispose();
|
|
5022
|
+
}
|
|
5023
|
+
}
|
|
5024
|
+
};
|
|
5025
|
+
function registerSkmRef(refValidator, path, href, line) {
|
|
5026
|
+
const parsed = parseURL(href);
|
|
5027
|
+
const targetResource = resolvePath(path, tryDecodeUriComponent(parsed.resource)).normalize("NFC");
|
|
5028
|
+
const location = line != null ? { path, line } : { path };
|
|
5029
|
+
const ref = {
|
|
5030
|
+
url: parsed.hasFragment ? `${targetResource}#${parsed.fragment ?? ""}` : targetResource,
|
|
5031
|
+
targetResource,
|
|
5032
|
+
type: "search-key" /* SEARCH_KEY */,
|
|
5033
|
+
location
|
|
5034
|
+
};
|
|
5035
|
+
if (parsed.fragment !== void 0) ref.fragment = parsed.fragment;
|
|
5036
|
+
refValidator.addReference(ref);
|
|
5037
|
+
}
|
|
5038
|
+
|
|
5039
|
+
// src/vocab/epub-ssv.ts
|
|
5040
|
+
var EPUB_SSV_DEPRECATED = /* @__PURE__ */ new Set([
|
|
5041
|
+
"annoref",
|
|
5042
|
+
"annotation",
|
|
5043
|
+
"biblioentry",
|
|
5044
|
+
"bridgehead",
|
|
5045
|
+
"endnote",
|
|
5046
|
+
"help",
|
|
5047
|
+
"marginalia",
|
|
5048
|
+
"note",
|
|
5049
|
+
"rearnote",
|
|
5050
|
+
"rearnotes",
|
|
5051
|
+
"sidebar",
|
|
5052
|
+
"subchapter",
|
|
5053
|
+
"warning"
|
|
5054
|
+
]);
|
|
5055
|
+
var EPUB_SSV_DISALLOWED_ON_CONTENT = /* @__PURE__ */ new Set([
|
|
5056
|
+
"aside",
|
|
5057
|
+
"figure",
|
|
5058
|
+
"list",
|
|
5059
|
+
"list-item",
|
|
5060
|
+
"table",
|
|
5061
|
+
"table-cell",
|
|
5062
|
+
"table-row"
|
|
5063
|
+
]);
|
|
5064
|
+
var EPUB_SSV_ALL = /* @__PURE__ */ new Set([
|
|
5065
|
+
...EPUB_SSV_DEPRECATED,
|
|
5066
|
+
...EPUB_SSV_DISALLOWED_ON_CONTENT,
|
|
5067
|
+
"abstract",
|
|
5068
|
+
"acknowledgments",
|
|
5069
|
+
"afterword",
|
|
5070
|
+
"appendix",
|
|
5071
|
+
"assessment",
|
|
5072
|
+
"assessments",
|
|
5073
|
+
"backlink",
|
|
5074
|
+
"backmatter",
|
|
5075
|
+
"balloon",
|
|
5076
|
+
"bibliography",
|
|
5077
|
+
"biblioref",
|
|
5078
|
+
"bodymatter",
|
|
5079
|
+
"case-study",
|
|
5080
|
+
"chapter",
|
|
5081
|
+
"colophon",
|
|
5082
|
+
"concluding-sentence",
|
|
5083
|
+
"conclusion",
|
|
5084
|
+
"contributors",
|
|
5085
|
+
"copyright-page",
|
|
5086
|
+
"cover",
|
|
5087
|
+
"covertitle",
|
|
5088
|
+
"credit",
|
|
5089
|
+
"credits",
|
|
5090
|
+
"dedication",
|
|
5091
|
+
"division",
|
|
5092
|
+
"endnotes",
|
|
5093
|
+
"epigraph",
|
|
5094
|
+
"epilogue",
|
|
5095
|
+
"errata",
|
|
5096
|
+
"fill-in-the-blank-problem",
|
|
5097
|
+
"footnote",
|
|
5098
|
+
"footnotes",
|
|
5099
|
+
"foreword",
|
|
5100
|
+
"frontmatter",
|
|
5101
|
+
"fulltitle",
|
|
5102
|
+
"general-problem",
|
|
5103
|
+
"glossary",
|
|
5104
|
+
"glossdef",
|
|
5105
|
+
"glossref",
|
|
5106
|
+
"glossterm",
|
|
5107
|
+
"halftitle",
|
|
5108
|
+
"halftitlepage",
|
|
5109
|
+
"imprimatur",
|
|
5110
|
+
"imprint",
|
|
5111
|
+
"index",
|
|
5112
|
+
"index-editor-note",
|
|
5113
|
+
"index-entry",
|
|
5114
|
+
"index-entry-list",
|
|
5115
|
+
"index-group",
|
|
5116
|
+
"index-headnotes",
|
|
5117
|
+
"index-legend",
|
|
5118
|
+
"index-locator",
|
|
5119
|
+
"index-locator-list",
|
|
5120
|
+
"index-locator-range",
|
|
5121
|
+
"index-term",
|
|
5122
|
+
"index-term-categories",
|
|
5123
|
+
"index-term-category",
|
|
5124
|
+
"index-xref-preferred",
|
|
5125
|
+
"index-xref-related",
|
|
5126
|
+
"introduction",
|
|
5127
|
+
"keyword",
|
|
5128
|
+
"keywords",
|
|
5129
|
+
"label",
|
|
5130
|
+
"landmarks",
|
|
5131
|
+
"learning-objective",
|
|
5132
|
+
"learning-objectives",
|
|
5133
|
+
"learning-outcome",
|
|
5134
|
+
"learning-outcomes",
|
|
5135
|
+
"learning-resource",
|
|
5136
|
+
"learning-resources",
|
|
5137
|
+
"learning-standard",
|
|
5138
|
+
"learning-standards",
|
|
5139
|
+
"loa",
|
|
5140
|
+
"loi",
|
|
5141
|
+
"lot",
|
|
5142
|
+
"lov",
|
|
5143
|
+
"match-problem",
|
|
5144
|
+
"multiple-choice-problem",
|
|
5145
|
+
"noteref",
|
|
5146
|
+
"notice",
|
|
5147
|
+
"ordinal",
|
|
5148
|
+
"other-credits",
|
|
5149
|
+
"page-list",
|
|
5150
|
+
"pagebreak",
|
|
5151
|
+
"panel",
|
|
5152
|
+
"panel-group",
|
|
5153
|
+
"part",
|
|
5154
|
+
"practice",
|
|
5155
|
+
"practices",
|
|
5156
|
+
"preamble",
|
|
5157
|
+
"preface",
|
|
5158
|
+
"prologue",
|
|
5159
|
+
"pullquote",
|
|
5160
|
+
"qna",
|
|
5161
|
+
"question",
|
|
5162
|
+
"referrer",
|
|
5163
|
+
"revision-history",
|
|
5164
|
+
"seriespage",
|
|
5165
|
+
"sound-area",
|
|
5166
|
+
"subtitle",
|
|
5167
|
+
"tip",
|
|
5168
|
+
"title",
|
|
5169
|
+
"titlepage",
|
|
5170
|
+
"toc",
|
|
5171
|
+
"toc-brief",
|
|
5172
|
+
"topic-sentence",
|
|
5173
|
+
"true-false-problem",
|
|
5174
|
+
"volume"
|
|
5175
|
+
]);
|
|
5176
|
+
|
|
5177
|
+
// src/smil/validator.ts
|
|
5178
|
+
var SMIL_NS = { smil: "http://www.w3.org/ns/SMIL" };
|
|
5179
|
+
var BLESSED_AUDIO_TYPES = /* @__PURE__ */ new Set(["audio/mpeg", "audio/mp4"]);
|
|
5180
|
+
var BLESSED_AUDIO_PATTERN = /^audio\/ogg\s*;\s*codecs=opus$/i;
|
|
5181
|
+
function isBlessedAudioType(mimeType) {
|
|
5182
|
+
return BLESSED_AUDIO_TYPES.has(mimeType) || BLESSED_AUDIO_PATTERN.test(mimeType);
|
|
5183
|
+
}
|
|
5184
|
+
var SMILValidator = class {
|
|
5185
|
+
getAttribute(element, name) {
|
|
5186
|
+
return element.attr(name)?.value ?? null;
|
|
5187
|
+
}
|
|
5188
|
+
getEpubAttribute(element, localName) {
|
|
5189
|
+
return element.attr(localName, "epub")?.value ?? null;
|
|
5190
|
+
}
|
|
5191
|
+
validate(context, path, manifestByPath) {
|
|
5192
|
+
const result = {
|
|
5193
|
+
textReferences: [],
|
|
5194
|
+
referencedDocuments: /* @__PURE__ */ new Set(),
|
|
5195
|
+
hasRemoteResources: false
|
|
5196
|
+
};
|
|
5197
|
+
const data = context.files.get(path);
|
|
5198
|
+
if (!data) return result;
|
|
5199
|
+
const content = typeof data === "string" ? data : new TextDecoder().decode(data);
|
|
5200
|
+
let doc = null;
|
|
5201
|
+
try {
|
|
5202
|
+
doc = libxml2Wasm.XmlDocument.fromString(content);
|
|
5203
|
+
} catch {
|
|
5204
|
+
pushMessage(context.messages, {
|
|
5205
|
+
id: MessageId.RSC_016,
|
|
5206
|
+
message: "Media Overlay document is not well-formed XML",
|
|
5207
|
+
location: { path }
|
|
5208
|
+
});
|
|
5209
|
+
return result;
|
|
5210
|
+
}
|
|
5211
|
+
try {
|
|
5212
|
+
const root = doc.root;
|
|
5213
|
+
this.validateStructure(context, path, root);
|
|
5214
|
+
this.validateAudioElements(context, path, root, manifestByPath, result);
|
|
5215
|
+
this.validateEpubTypes(context, path, root);
|
|
5216
|
+
this.extractTextReferences(path, root, result);
|
|
5217
|
+
} finally {
|
|
5218
|
+
doc.dispose();
|
|
5219
|
+
}
|
|
5220
|
+
return result;
|
|
5221
|
+
}
|
|
5222
|
+
validateStructure(context, path, root) {
|
|
5223
|
+
try {
|
|
5224
|
+
for (const text of root.find(".//smil:seq/smil:text", SMIL_NS)) {
|
|
5225
|
+
pushMessage(context.messages, {
|
|
5226
|
+
id: MessageId.RSC_005,
|
|
5227
|
+
message: "element 'text' not allowed here; expected 'seq' or 'par'",
|
|
5228
|
+
location: { path, line: text.line }
|
|
5229
|
+
});
|
|
5230
|
+
}
|
|
5231
|
+
for (const audio of root.find(".//smil:seq/smil:audio", SMIL_NS)) {
|
|
5232
|
+
pushMessage(context.messages, {
|
|
5233
|
+
id: MessageId.RSC_005,
|
|
5234
|
+
message: "element 'audio' not allowed here; expected 'seq' or 'par'",
|
|
5235
|
+
location: { path, line: audio.line }
|
|
5236
|
+
});
|
|
5237
|
+
}
|
|
5238
|
+
} catch {
|
|
5239
|
+
}
|
|
5240
|
+
try {
|
|
5241
|
+
for (const seq of root.find(".//smil:par/smil:seq", SMIL_NS)) {
|
|
5242
|
+
pushMessage(context.messages, {
|
|
5243
|
+
id: MessageId.RSC_005,
|
|
5244
|
+
message: "element 'seq' not allowed here; expected 'text' or 'audio'",
|
|
5245
|
+
location: { path, line: seq.line }
|
|
5246
|
+
});
|
|
5247
|
+
}
|
|
5248
|
+
const parElements = root.find(".//smil:par", SMIL_NS);
|
|
5249
|
+
for (const par of parElements) {
|
|
5250
|
+
const textChildren = par.find("./smil:text", SMIL_NS);
|
|
5251
|
+
for (let i = 1; i < textChildren.length; i++) {
|
|
5252
|
+
const extra = textChildren[i];
|
|
5253
|
+
if (!extra) continue;
|
|
5254
|
+
pushMessage(context.messages, {
|
|
5255
|
+
id: MessageId.RSC_005,
|
|
5256
|
+
message: "element 'text' not allowed here; only one 'text' element is allowed in 'par'",
|
|
5257
|
+
location: { path, line: extra.line }
|
|
5258
|
+
});
|
|
5259
|
+
}
|
|
5260
|
+
}
|
|
5261
|
+
} catch {
|
|
5262
|
+
}
|
|
5263
|
+
try {
|
|
5264
|
+
const headMetaElements = root.find(".//smil:head/smil:meta", SMIL_NS);
|
|
5265
|
+
for (const meta of headMetaElements) {
|
|
5266
|
+
pushMessage(context.messages, {
|
|
5267
|
+
id: MessageId.RSC_005,
|
|
5268
|
+
message: "element 'meta' not allowed here; expected 'metadata'",
|
|
5269
|
+
location: { path, line: meta.line }
|
|
5270
|
+
});
|
|
5271
|
+
}
|
|
5272
|
+
} catch {
|
|
5273
|
+
}
|
|
5274
|
+
}
|
|
5275
|
+
validateAudioElements(context, path, root, manifestByPath, result) {
|
|
5276
|
+
try {
|
|
5277
|
+
const audioElements = root.find(".//smil:audio", SMIL_NS);
|
|
5278
|
+
for (const audio of audioElements) {
|
|
5279
|
+
const elem = audio;
|
|
5280
|
+
const src = this.getAttribute(elem, "src");
|
|
5281
|
+
if (src) {
|
|
5282
|
+
if (isRemoteURL(src)) {
|
|
5283
|
+
result.hasRemoteResources = true;
|
|
5284
|
+
}
|
|
5285
|
+
if (src.includes("#")) {
|
|
5286
|
+
pushMessage(context.messages, {
|
|
5287
|
+
id: MessageId.MED_014,
|
|
5288
|
+
message: `Media overlay audio file URLs must not have a fragment: "${src}"`,
|
|
5289
|
+
location: { path, line: audio.line }
|
|
5290
|
+
});
|
|
5291
|
+
}
|
|
5292
|
+
if (manifestByPath) {
|
|
5293
|
+
const audioPath = this.resolveRelativePath(path, src.split("#")[0] ?? src);
|
|
5294
|
+
const audioItem = manifestByPath.get(audioPath);
|
|
5295
|
+
if (audioItem && !isBlessedAudioType(audioItem.mediaType)) {
|
|
5296
|
+
pushMessage(context.messages, {
|
|
5297
|
+
id: MessageId.MED_005,
|
|
5298
|
+
message: `Media Overlay audio reference "${src}" to non-standard audio type "${audioItem.mediaType}"`,
|
|
5299
|
+
location: { path, line: audio.line }
|
|
5300
|
+
});
|
|
5301
|
+
}
|
|
5302
|
+
}
|
|
5303
|
+
}
|
|
5304
|
+
const clipBegin = this.getAttribute(elem, "clipBegin");
|
|
5305
|
+
const clipEnd = this.getAttribute(elem, "clipEnd");
|
|
5306
|
+
this.checkClipTiming(context, path, audio.line, clipBegin, clipEnd);
|
|
5307
|
+
}
|
|
5308
|
+
} catch {
|
|
5309
|
+
}
|
|
5310
|
+
}
|
|
5311
|
+
checkClipTiming(context, path, line, clipBegin, clipEnd) {
|
|
5312
|
+
if (clipEnd === null) return;
|
|
5313
|
+
const beginStr = clipBegin ?? "0";
|
|
5314
|
+
const start = parseSmilClock(beginStr);
|
|
5315
|
+
const end = parseSmilClock(clipEnd);
|
|
5316
|
+
const location = line != null ? { path, line } : { path };
|
|
5317
|
+
if (clipBegin !== null && Number.isNaN(start)) {
|
|
5318
|
+
pushMessage(context.messages, {
|
|
5319
|
+
id: MessageId.RSC_005,
|
|
5320
|
+
message: `Invalid SMIL clock value "${clipBegin}" in clipBegin attribute`,
|
|
5321
|
+
location
|
|
5322
|
+
});
|
|
5323
|
+
}
|
|
5324
|
+
if (Number.isNaN(end)) {
|
|
5325
|
+
pushMessage(context.messages, {
|
|
5326
|
+
id: MessageId.RSC_005,
|
|
5327
|
+
message: `Invalid SMIL clock value "${clipEnd}" in clipEnd attribute`,
|
|
5328
|
+
location
|
|
5329
|
+
});
|
|
5330
|
+
}
|
|
5331
|
+
if (Number.isNaN(start) || Number.isNaN(end)) return;
|
|
5332
|
+
if (start > end) {
|
|
5333
|
+
pushMessage(context.messages, {
|
|
5334
|
+
id: MessageId.MED_008,
|
|
5335
|
+
message: "The time specified in the clipBegin attribute must not be after clipEnd",
|
|
5336
|
+
location
|
|
5337
|
+
});
|
|
5338
|
+
} else if (start === end) {
|
|
5339
|
+
pushMessage(context.messages, {
|
|
5340
|
+
id: MessageId.MED_009,
|
|
5341
|
+
message: "The time specified in the clipBegin attribute must not be the same as clipEnd",
|
|
5342
|
+
location
|
|
5343
|
+
});
|
|
5344
|
+
}
|
|
5345
|
+
}
|
|
5346
|
+
/**
|
|
5347
|
+
* Validate epub:type attribute values against the EPUB SSV vocabulary.
|
|
5348
|
+
* Only emits OPF-088 (usage) for unknown local names. Prefixed values
|
|
5349
|
+
* from declared vocabularies are allowed.
|
|
5350
|
+
*/
|
|
5351
|
+
validateEpubTypes(context, path, root) {
|
|
5352
|
+
try {
|
|
5353
|
+
const epubTypeElements = root.find(".//*[@epub:type]", {
|
|
5354
|
+
epub: "http://www.idpf.org/2007/ops"
|
|
5355
|
+
});
|
|
5356
|
+
for (const elem of epubTypeElements) {
|
|
5357
|
+
const elemTyped = elem;
|
|
5358
|
+
const epubTypeAttr = elemTyped.attr("type", "epub");
|
|
5359
|
+
if (!epubTypeAttr?.value) continue;
|
|
5360
|
+
for (const part of epubTypeAttr.value.split(/\s+/)) {
|
|
5361
|
+
if (!part || part.includes(":")) continue;
|
|
5362
|
+
if (!EPUB_SSV_ALL.has(part)) {
|
|
5363
|
+
pushMessage(context.messages, {
|
|
5364
|
+
id: MessageId.OPF_088,
|
|
5365
|
+
message: `Unrecognized epub:type value "${part}"`,
|
|
5366
|
+
location: { path, line: elem.line }
|
|
5367
|
+
});
|
|
5368
|
+
}
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5371
|
+
} catch {
|
|
5372
|
+
}
|
|
5373
|
+
}
|
|
5374
|
+
extractTextReferences(path, root, result) {
|
|
5375
|
+
try {
|
|
5376
|
+
const textElements = root.find(".//smil:text", SMIL_NS);
|
|
5377
|
+
for (const text of textElements) {
|
|
5378
|
+
const src = this.getAttribute(text, "src");
|
|
5379
|
+
if (!src) continue;
|
|
5380
|
+
const hashIndex = src.indexOf("#");
|
|
5381
|
+
const docRef = hashIndex >= 0 ? src.substring(0, hashIndex) : src;
|
|
5382
|
+
const fragment = hashIndex >= 0 ? src.substring(hashIndex + 1) : void 0;
|
|
5383
|
+
const docPath = this.resolveRelativePath(path, docRef);
|
|
5384
|
+
result.textReferences.push({ docPath, fragment, line: text.line });
|
|
5385
|
+
result.referencedDocuments.add(docPath);
|
|
5386
|
+
}
|
|
5387
|
+
const bodyElements = root.find(".//smil:body", SMIL_NS);
|
|
5388
|
+
const seqElements = root.find(".//smil:seq", SMIL_NS);
|
|
5389
|
+
for (const elem of [...bodyElements, ...seqElements]) {
|
|
5390
|
+
const textref = this.getEpubAttribute(elem, "textref");
|
|
5391
|
+
if (!textref) continue;
|
|
5392
|
+
const hashIndex = textref.indexOf("#");
|
|
5393
|
+
const docRef = hashIndex >= 0 ? textref.substring(0, hashIndex) : textref;
|
|
5394
|
+
const fragment = hashIndex >= 0 ? textref.substring(hashIndex + 1) : void 0;
|
|
5395
|
+
const docPath = this.resolveRelativePath(path, docRef);
|
|
5396
|
+
result.textReferences.push({ docPath, fragment, line: elem.line });
|
|
5397
|
+
result.referencedDocuments.add(docPath);
|
|
5398
|
+
}
|
|
5399
|
+
} catch {
|
|
5400
|
+
}
|
|
5401
|
+
}
|
|
5402
|
+
resolveRelativePath(basePath, relativePath) {
|
|
5403
|
+
const decoded = tryDecodeUriComponent(relativePath);
|
|
5404
|
+
if (decoded.startsWith("/") || /^[a-zA-Z]+:/.test(decoded)) {
|
|
5405
|
+
return decoded.normalize("NFC");
|
|
5406
|
+
}
|
|
5407
|
+
const baseDir = basePath.includes("/") ? basePath.substring(0, basePath.lastIndexOf("/")) : "";
|
|
5408
|
+
if (!baseDir) return decoded.normalize("NFC");
|
|
5409
|
+
const segments = `${baseDir}/${decoded}`.split("/");
|
|
5410
|
+
const resolved = [];
|
|
5411
|
+
for (const seg of segments) {
|
|
5412
|
+
if (seg === "..") {
|
|
5413
|
+
resolved.pop();
|
|
5414
|
+
} else if (seg !== ".") {
|
|
5415
|
+
resolved.push(seg);
|
|
5416
|
+
}
|
|
5417
|
+
}
|
|
5418
|
+
return resolved.join("/").normalize("NFC");
|
|
5419
|
+
}
|
|
5420
|
+
};
|
|
5279
5421
|
|
|
5280
5422
|
// src/references/uri-schemes.ts
|
|
5281
5423
|
var URI_SCHEMES = /* @__PURE__ */ new Set([
|
|
@@ -5366,7 +5508,9 @@ var ABSOLUTE_URI_RE = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
|
|
|
5366
5508
|
var SPECIAL_URL_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "ftp", "ws", "wss"]);
|
|
5367
5509
|
var CSS_CHARSET_RE = /^@charset\s+"([^"]+)"\s*;/;
|
|
5368
5510
|
var EPUB_XMLNS_RE = /xmlns:epub\s*=\s*"([^"]*)"/;
|
|
5369
|
-
var
|
|
5511
|
+
var XHTML_NS_URI = "http://www.w3.org/1999/xhtml";
|
|
5512
|
+
var XML_NS_URI = "http://www.w3.org/XML/1998/namespace";
|
|
5513
|
+
var XHTML_NS = { html: XHTML_NS_URI };
|
|
5370
5514
|
var EPUB_OPS_NS = { epub: "http://www.idpf.org/2007/ops" };
|
|
5371
5515
|
var EPUB_TYPE_FORBIDDEN_ELEMENTS = /* @__PURE__ */ new Set([
|
|
5372
5516
|
"head",
|
|
@@ -5650,6 +5794,112 @@ var HTML5_ELEMENTS = /* @__PURE__ */ new Set([
|
|
|
5650
5794
|
"video",
|
|
5651
5795
|
"wbr"
|
|
5652
5796
|
]);
|
|
5797
|
+
var XHTML11_ELEMENTS = /* @__PURE__ */ new Set([
|
|
5798
|
+
// struct
|
|
5799
|
+
"html",
|
|
5800
|
+
"head",
|
|
5801
|
+
"title",
|
|
5802
|
+
"body",
|
|
5803
|
+
"meta",
|
|
5804
|
+
"link",
|
|
5805
|
+
"base",
|
|
5806
|
+
"style",
|
|
5807
|
+
"script",
|
|
5808
|
+
"noscript",
|
|
5809
|
+
// text
|
|
5810
|
+
"br",
|
|
5811
|
+
"span",
|
|
5812
|
+
"abbr",
|
|
5813
|
+
"acronym",
|
|
5814
|
+
"cite",
|
|
5815
|
+
"code",
|
|
5816
|
+
"dfn",
|
|
5817
|
+
"em",
|
|
5818
|
+
"kbd",
|
|
5819
|
+
"q",
|
|
5820
|
+
"samp",
|
|
5821
|
+
"strong",
|
|
5822
|
+
"var",
|
|
5823
|
+
"div",
|
|
5824
|
+
"p",
|
|
5825
|
+
"address",
|
|
5826
|
+
"blockquote",
|
|
5827
|
+
"pre",
|
|
5828
|
+
"h1",
|
|
5829
|
+
"h2",
|
|
5830
|
+
"h3",
|
|
5831
|
+
"h4",
|
|
5832
|
+
"h5",
|
|
5833
|
+
"h6",
|
|
5834
|
+
// pres / legacy
|
|
5835
|
+
"hr",
|
|
5836
|
+
"b",
|
|
5837
|
+
"big",
|
|
5838
|
+
"i",
|
|
5839
|
+
"small",
|
|
5840
|
+
"sub",
|
|
5841
|
+
"sup",
|
|
5842
|
+
"tt",
|
|
5843
|
+
"basefont",
|
|
5844
|
+
"center",
|
|
5845
|
+
"font",
|
|
5846
|
+
"s",
|
|
5847
|
+
"strike",
|
|
5848
|
+
"u",
|
|
5849
|
+
"dir",
|
|
5850
|
+
"menu",
|
|
5851
|
+
"isindex",
|
|
5852
|
+
// list
|
|
5853
|
+
"dl",
|
|
5854
|
+
"dt",
|
|
5855
|
+
"dd",
|
|
5856
|
+
"ol",
|
|
5857
|
+
"ul",
|
|
5858
|
+
"li",
|
|
5859
|
+
// table
|
|
5860
|
+
"table",
|
|
5861
|
+
"caption",
|
|
5862
|
+
"tr",
|
|
5863
|
+
"th",
|
|
5864
|
+
"td",
|
|
5865
|
+
"col",
|
|
5866
|
+
"colgroup",
|
|
5867
|
+
"tbody",
|
|
5868
|
+
"thead",
|
|
5869
|
+
"tfoot",
|
|
5870
|
+
// hypertext / image / object / form / edit / ruby / map / iframe / applet / bdo / param
|
|
5871
|
+
"a",
|
|
5872
|
+
"img",
|
|
5873
|
+
"object",
|
|
5874
|
+
"param",
|
|
5875
|
+
"form",
|
|
5876
|
+
"label",
|
|
5877
|
+
"input",
|
|
5878
|
+
"select",
|
|
5879
|
+
"option",
|
|
5880
|
+
"optgroup",
|
|
5881
|
+
"fieldset",
|
|
5882
|
+
"button",
|
|
5883
|
+
"legend",
|
|
5884
|
+
"textarea",
|
|
5885
|
+
"ins",
|
|
5886
|
+
"del",
|
|
5887
|
+
"ruby",
|
|
5888
|
+
"rbc",
|
|
5889
|
+
"rtc",
|
|
5890
|
+
"rb",
|
|
5891
|
+
"rt",
|
|
5892
|
+
"rp",
|
|
5893
|
+
"map",
|
|
5894
|
+
"area",
|
|
5895
|
+
"iframe",
|
|
5896
|
+
"applet",
|
|
5897
|
+
"bdo",
|
|
5898
|
+
// frames
|
|
5899
|
+
"frameset",
|
|
5900
|
+
"frame",
|
|
5901
|
+
"noframes"
|
|
5902
|
+
]);
|
|
5653
5903
|
function isItemFixedLayout(packageDoc, itemId) {
|
|
5654
5904
|
const spineItem = packageDoc.spine.find((s) => s.idref === itemId);
|
|
5655
5905
|
if (!spineItem) return false;
|
|
@@ -5716,6 +5966,10 @@ var ContentValidator = class {
|
|
|
5716
5966
|
if (refValidator) {
|
|
5717
5967
|
this.extractSVGReferences(context, fullPath, opfDir, refValidator);
|
|
5718
5968
|
}
|
|
5969
|
+
} else if (item.mediaType === "application/vnd.epub.search-key-map+xml") {
|
|
5970
|
+
const fullPath = resolveManifestHref(opfDir, item.href);
|
|
5971
|
+
const skmValidator = new SKMValidator();
|
|
5972
|
+
skmValidator.validate(context, fullPath, refValidator);
|
|
5719
5973
|
} else if (item.mediaType === "application/smil+xml") {
|
|
5720
5974
|
const fullPath = resolveManifestHref(opfDir, item.href);
|
|
5721
5975
|
const smilValidator = new SMILValidator();
|
|
@@ -6478,6 +6732,9 @@ var ContentValidator = class {
|
|
|
6478
6732
|
}
|
|
6479
6733
|
}
|
|
6480
6734
|
}
|
|
6735
|
+
if (context.version === "2.0") {
|
|
6736
|
+
this.checkEpub2XhtmlStrict(context, path, root);
|
|
6737
|
+
}
|
|
6481
6738
|
this.checkDiscouragedElements(context, path, root);
|
|
6482
6739
|
this.checkSSMLPh(context, path, root, content);
|
|
6483
6740
|
this.checkObsoleteHTML(context, path, root);
|
|
@@ -6508,6 +6765,9 @@ var ContentValidator = class {
|
|
|
6508
6765
|
this.validateEpubTypes(context, path, root);
|
|
6509
6766
|
this.validateRegionBasedNav(context, path, root, manifestItem);
|
|
6510
6767
|
}
|
|
6768
|
+
if (context.version.startsWith("3") && context.options.profile === "dict") {
|
|
6769
|
+
this.validateDictionaryContent(context, path, root);
|
|
6770
|
+
}
|
|
6511
6771
|
if (context.version.startsWith("3") && context.options.profile === "edupub") {
|
|
6512
6772
|
const isFxl = manifestItem && packageDoc ? isItemFixedLayout(packageDoc, manifestItem.id) : false;
|
|
6513
6773
|
const isNonLinear = manifestItem && packageDoc ? packageDoc.spine.find((ref) => ref.idref === manifestItem.id)?.linear === false : false;
|
|
@@ -6516,7 +6776,7 @@ var ContentValidator = class {
|
|
|
6516
6776
|
}
|
|
6517
6777
|
}
|
|
6518
6778
|
if (context.version.startsWith("3")) {
|
|
6519
|
-
this.collectFeatures(context, root);
|
|
6779
|
+
this.collectFeatures(context, path, root);
|
|
6520
6780
|
}
|
|
6521
6781
|
this.validateEpubSwitch(context, path, root);
|
|
6522
6782
|
this.validateEpubTrigger(context, path, root);
|
|
@@ -7726,6 +7986,7 @@ var ContentValidator = class {
|
|
|
7726
7986
|
}
|
|
7727
7987
|
}
|
|
7728
7988
|
checkUsemapAttribute(context, path, root) {
|
|
7989
|
+
if (context.version === "2.0") return;
|
|
7729
7990
|
try {
|
|
7730
7991
|
const elements = root.find(".//html:*[@usemap]", XHTML_NS);
|
|
7731
7992
|
for (const elem of elements) {
|
|
@@ -8038,7 +8299,7 @@ var ContentValidator = class {
|
|
|
8038
8299
|
}
|
|
8039
8300
|
}
|
|
8040
8301
|
}
|
|
8041
|
-
collectFeatures(context, root) {
|
|
8302
|
+
collectFeatures(context, path, root) {
|
|
8042
8303
|
const features = context.contentFeatures;
|
|
8043
8304
|
if (!features) return;
|
|
8044
8305
|
if (!features.hasTable && root.get(".//html:table", XHTML_NS)) {
|
|
@@ -8053,22 +8314,20 @@ var ContentValidator = class {
|
|
|
8053
8314
|
if (!features.hasVideo && root.get(".//html:video", XHTML_NS)) {
|
|
8054
8315
|
features.hasVideo = true;
|
|
8055
8316
|
}
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
}
|
|
8071
|
-
if (features.hasPageBreak && features.hasDictionary && features.hasIndex) break;
|
|
8317
|
+
const epubTypeElements = root.find(".//*[@epub:type]", EPUB_OPS_NS);
|
|
8318
|
+
for (const el of epubTypeElements) {
|
|
8319
|
+
const attr = el.attr("type", "epub");
|
|
8320
|
+
if (!attr?.value) continue;
|
|
8321
|
+
const tokens = attr.value.trim().split(/\s+/);
|
|
8322
|
+
if (!features.hasPageBreak && tokens.includes("pagebreak")) {
|
|
8323
|
+
features.hasPageBreak = true;
|
|
8324
|
+
}
|
|
8325
|
+
if (tokens.includes("dictionary")) {
|
|
8326
|
+
features.hasDictionary = true;
|
|
8327
|
+
(features.dictionaryContentPaths ??= /* @__PURE__ */ new Set()).add(path);
|
|
8328
|
+
}
|
|
8329
|
+
if (!features.hasIndex && tokens.includes("index")) {
|
|
8330
|
+
features.hasIndex = true;
|
|
8072
8331
|
}
|
|
8073
8332
|
}
|
|
8074
8333
|
if (!features.hasMicrodata && root.get(".//*[@itemscope]")) {
|
|
@@ -8169,6 +8428,158 @@ var ContentValidator = class {
|
|
|
8169
8428
|
});
|
|
8170
8429
|
}
|
|
8171
8430
|
}
|
|
8431
|
+
this.validateRegionBasedNavRules(context, path, root);
|
|
8432
|
+
}
|
|
8433
|
+
}
|
|
8434
|
+
validateRegionBasedNavRules(context, path, root) {
|
|
8435
|
+
const XHTML_NS2 = { html: "http://www.w3.org/1999/xhtml" };
|
|
8436
|
+
let regionNavs;
|
|
8437
|
+
try {
|
|
8438
|
+
regionNavs = root.find(".//html:nav", XHTML_NS2);
|
|
8439
|
+
} catch {
|
|
8440
|
+
return;
|
|
8441
|
+
}
|
|
8442
|
+
const packageDoc = context.packageDocument;
|
|
8443
|
+
const opfDir = context.opfPath?.includes("/") ? context.opfPath.substring(0, context.opfPath.lastIndexOf("/")) : "";
|
|
8444
|
+
const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
|
|
8445
|
+
const manifestByPath = packageDoc ? new Map(packageDoc.manifest.map((m) => [resolveManifestHref(opfDir, m.href), m])) : void 0;
|
|
8446
|
+
for (const nav of regionNavs) {
|
|
8447
|
+
const epubType = nav.attr("type", "epub")?.value ?? "";
|
|
8448
|
+
if (!epubType.split(/\s+/).includes("region-based")) continue;
|
|
8449
|
+
const childEls = nav.find("./html:*", XHTML_NS2);
|
|
8450
|
+
if (childEls.length !== 1 || childEls[0]?.name !== "ol") {
|
|
8451
|
+
pushMessage(context.messages, {
|
|
8452
|
+
id: MessageId.RSC_017,
|
|
8453
|
+
message: "A region-based nav element must contain exactly one child ol element.",
|
|
8454
|
+
location: { path, line: nav.line }
|
|
8455
|
+
});
|
|
8456
|
+
}
|
|
8457
|
+
const liElements = nav.find(".//html:li", XHTML_NS2);
|
|
8458
|
+
for (const li of liElements) {
|
|
8459
|
+
const liChildren = li.find("./html:*", XHTML_NS2);
|
|
8460
|
+
const first = liChildren[0];
|
|
8461
|
+
if (!first || first.name !== "a" && first.name !== "span") {
|
|
8462
|
+
pushMessage(context.messages, {
|
|
8463
|
+
id: MessageId.RSC_017,
|
|
8464
|
+
message: "The first child of a region-based nav list item must be either an 'a' or 'span' element.",
|
|
8465
|
+
location: { path, line: li.line }
|
|
8466
|
+
});
|
|
8467
|
+
}
|
|
8468
|
+
if (liChildren.length > 1 && (liChildren.length !== 2 || liChildren[1]?.name !== "ol")) {
|
|
8469
|
+
pushMessage(context.messages, {
|
|
8470
|
+
id: MessageId.RSC_017,
|
|
8471
|
+
message: "The first child of a region-based nav list item can only be followed by a single 'ol' element.",
|
|
8472
|
+
location: { path, line: li.line }
|
|
8473
|
+
});
|
|
8474
|
+
}
|
|
8475
|
+
}
|
|
8476
|
+
const spans = nav.find(".//html:span", XHTML_NS2);
|
|
8477
|
+
for (const span of spans) {
|
|
8478
|
+
const spanChildren = span.find("./html:*", XHTML_NS2);
|
|
8479
|
+
const aChildren = spanChildren.filter((c) => c.name === "a");
|
|
8480
|
+
if (spanChildren.length !== 2 || aChildren.length !== 2) {
|
|
8481
|
+
pushMessage(context.messages, {
|
|
8482
|
+
id: MessageId.RSC_017,
|
|
8483
|
+
message: "'span' elements in region-based navs must contain exactly two 'a' elements.",
|
|
8484
|
+
location: { path, line: span.line }
|
|
8485
|
+
});
|
|
8486
|
+
}
|
|
8487
|
+
}
|
|
8488
|
+
const anchors = nav.find(".//html:a", XHTML_NS2);
|
|
8489
|
+
for (const a of anchors) {
|
|
8490
|
+
if (a.content.trim() !== "") {
|
|
8491
|
+
pushMessage(context.messages, {
|
|
8492
|
+
id: MessageId.RSC_017,
|
|
8493
|
+
message: "'a' elements in region-based navs should not contain text labels.",
|
|
8494
|
+
location: { path, line: a.line }
|
|
8495
|
+
});
|
|
8496
|
+
}
|
|
8497
|
+
}
|
|
8498
|
+
if (!packageDoc || !manifestByPath) continue;
|
|
8499
|
+
for (const a of anchors) {
|
|
8500
|
+
const href = a.attr("href")?.value;
|
|
8501
|
+
if (!href || !isRelativeURL(href)) continue;
|
|
8502
|
+
const resolved = this.resolveRelativePath(docDir, href, opfDir);
|
|
8503
|
+
const targetPath = parseURL(resolved).resource;
|
|
8504
|
+
if (!targetPath) continue;
|
|
8505
|
+
const item = manifestByPath.get(targetPath);
|
|
8506
|
+
if (!item) continue;
|
|
8507
|
+
if (!isItemFixedLayout(packageDoc, item.id)) {
|
|
8508
|
+
pushMessage(context.messages, {
|
|
8509
|
+
id: MessageId.NAV_009,
|
|
8510
|
+
message: "Region-based navigation links must point to Fixed-Layout Documents.",
|
|
8511
|
+
location: { path, line: a.line }
|
|
8512
|
+
});
|
|
8513
|
+
}
|
|
8514
|
+
}
|
|
8515
|
+
}
|
|
8516
|
+
}
|
|
8517
|
+
/**
|
|
8518
|
+
* EPUB Dictionaries content document rules.
|
|
8519
|
+
*
|
|
8520
|
+
* Mirrors ../epubcheck/src/main/resources/com/adobe/epubcheck/schema/30/dict/dict-xhtml.sch
|
|
8521
|
+
* (minimum set: `dictionary` must be on body/section with article children; each article or
|
|
8522
|
+
* `dictentry` must have a `dfn` descendant outside of optional `condensed-entry`).
|
|
8523
|
+
*/
|
|
8524
|
+
validateDictionaryContent(context, path, root) {
|
|
8525
|
+
let typedElements;
|
|
8526
|
+
try {
|
|
8527
|
+
typedElements = root.find(".//*[@epub:type]", EPUB_OPS_NS);
|
|
8528
|
+
} catch {
|
|
8529
|
+
return;
|
|
8530
|
+
}
|
|
8531
|
+
for (const el of typedElements) {
|
|
8532
|
+
const tokens = el.attr("type", "epub")?.value.split(/\s+/) ?? [];
|
|
8533
|
+
if (tokens.includes("dictionary")) {
|
|
8534
|
+
if (el.name !== "body" && el.name !== "section") {
|
|
8535
|
+
pushMessage(context.messages, {
|
|
8536
|
+
id: MessageId.RSC_005,
|
|
8537
|
+
message: 'The "dictionary" type is only allowed on "body" or "section" elements.',
|
|
8538
|
+
location: { path, line: el.line }
|
|
8539
|
+
});
|
|
8540
|
+
}
|
|
8541
|
+
const articles = el.find("./html:article", XHTML_NS);
|
|
8542
|
+
if (articles.length === 0) {
|
|
8543
|
+
pushMessage(context.messages, {
|
|
8544
|
+
id: MessageId.RSC_005,
|
|
8545
|
+
message: 'A "dictionary" must have at least one article child.',
|
|
8546
|
+
location: { path, line: el.line }
|
|
8547
|
+
});
|
|
8548
|
+
}
|
|
8549
|
+
for (const article of articles) {
|
|
8550
|
+
this.checkDictionaryEntry(context, path, article);
|
|
8551
|
+
}
|
|
8552
|
+
}
|
|
8553
|
+
if (tokens.includes("dictentry")) {
|
|
8554
|
+
if (el.name !== "article") {
|
|
8555
|
+
pushMessage(context.messages, {
|
|
8556
|
+
id: MessageId.RSC_005,
|
|
8557
|
+
message: 'The "dictentry" type is only allowed on "article" elements.',
|
|
8558
|
+
location: { path, line: el.line }
|
|
8559
|
+
});
|
|
8560
|
+
} else {
|
|
8561
|
+
this.checkDictionaryEntry(context, path, el);
|
|
8562
|
+
}
|
|
8563
|
+
}
|
|
8564
|
+
}
|
|
8565
|
+
}
|
|
8566
|
+
checkDictionaryEntry(context, path, article) {
|
|
8567
|
+
const dfns = article.find(".//html:dfn", XHTML_NS);
|
|
8568
|
+
const hasDfnOutsideCondensed = dfns.some((dfn) => {
|
|
8569
|
+
let parent = dfn.parent;
|
|
8570
|
+
while (parent) {
|
|
8571
|
+
const type = parent.attr("type", "epub")?.value;
|
|
8572
|
+
if (type?.split(/\s+/).includes("condensed-entry")) return false;
|
|
8573
|
+
parent = parent.parent;
|
|
8574
|
+
}
|
|
8575
|
+
return true;
|
|
8576
|
+
});
|
|
8577
|
+
if (!hasDfnOutsideCondensed) {
|
|
8578
|
+
pushMessage(context.messages, {
|
|
8579
|
+
id: MessageId.RSC_005,
|
|
8580
|
+
message: 'A dictionary entry must have at least one "dfn" descendant (outside of the optional condensed entry "aside").',
|
|
8581
|
+
location: { path, line: article.line }
|
|
8582
|
+
});
|
|
8172
8583
|
}
|
|
8173
8584
|
}
|
|
8174
8585
|
/**
|
|
@@ -9565,13 +9976,11 @@ var ContentValidator = class {
|
|
|
9565
9976
|
}
|
|
9566
9977
|
}
|
|
9567
9978
|
checkUnknownElements(context, path, root) {
|
|
9568
|
-
const XHTML_NS2 = "http://www.w3.org/1999/xhtml";
|
|
9569
9979
|
try {
|
|
9570
9980
|
const allElements = root.find(".//*");
|
|
9571
9981
|
for (const el of allElements) {
|
|
9572
9982
|
const xmlEl = el;
|
|
9573
|
-
|
|
9574
|
-
if (ns !== XHTML_NS2) continue;
|
|
9983
|
+
if (xmlEl.namespaceUri !== XHTML_NS_URI) continue;
|
|
9575
9984
|
const localName = xmlEl.name.includes(":") ? xmlEl.name.substring(xmlEl.name.indexOf(":") + 1) : xmlEl.name;
|
|
9576
9985
|
if (localName.includes("-")) continue;
|
|
9577
9986
|
if (!HTML5_ELEMENTS.has(localName)) {
|
|
@@ -9585,6 +9994,51 @@ var ContentValidator = class {
|
|
|
9585
9994
|
} catch {
|
|
9586
9995
|
}
|
|
9587
9996
|
}
|
|
9997
|
+
checkEpub2XhtmlStrict(context, path, root) {
|
|
9998
|
+
if (!root.namespaceUri) {
|
|
9999
|
+
pushMessage(context.messages, {
|
|
10000
|
+
id: MessageId.RSC_005,
|
|
10001
|
+
message: `element "${root.name}" from namespace "" is not allowed`,
|
|
10002
|
+
location: { path, line: root.line }
|
|
10003
|
+
});
|
|
10004
|
+
return;
|
|
10005
|
+
}
|
|
10006
|
+
const checkElement = (xmlEl) => {
|
|
10007
|
+
if (xmlEl.namespaceUri !== XHTML_NS_URI) return;
|
|
10008
|
+
const localName = xmlEl.name.includes(":") ? xmlEl.name.substring(xmlEl.name.indexOf(":") + 1) : xmlEl.name;
|
|
10009
|
+
if (!XHTML11_ELEMENTS.has(localName)) {
|
|
10010
|
+
pushMessage(context.messages, {
|
|
10011
|
+
id: MessageId.RSC_005,
|
|
10012
|
+
message: `element "${localName}" not allowed here`,
|
|
10013
|
+
location: { path, line: xmlEl.line }
|
|
10014
|
+
});
|
|
10015
|
+
}
|
|
10016
|
+
for (const attr of xmlEl.attrs) {
|
|
10017
|
+
const ns = attr.namespaceUri;
|
|
10018
|
+
if (!ns || ns === XHTML_NS_URI || ns === XML_NS_URI) continue;
|
|
10019
|
+
const qname = attr.prefix ? `${attr.prefix}:${attr.name}` : attr.name;
|
|
10020
|
+
pushMessage(context.messages, {
|
|
10021
|
+
id: MessageId.RSC_005,
|
|
10022
|
+
message: `attribute "${qname}" not allowed here`,
|
|
10023
|
+
location: { path, line: xmlEl.line }
|
|
10024
|
+
});
|
|
10025
|
+
}
|
|
10026
|
+
};
|
|
10027
|
+
checkElement(root);
|
|
10028
|
+
try {
|
|
10029
|
+
for (const el of root.find(".//*")) {
|
|
10030
|
+
checkElement(el);
|
|
10031
|
+
}
|
|
10032
|
+
for (const a of root.find(".//html:a//html:a", XHTML_NS)) {
|
|
10033
|
+
pushMessage(context.messages, {
|
|
10034
|
+
id: MessageId.RSC_005,
|
|
10035
|
+
message: 'The "a" element cannot contain any nested "a" elements',
|
|
10036
|
+
location: { path, line: a.line }
|
|
10037
|
+
});
|
|
10038
|
+
}
|
|
10039
|
+
} catch {
|
|
10040
|
+
}
|
|
10041
|
+
}
|
|
9588
10042
|
checkForeignObjectContent(context, path, root, isSVGDoc) {
|
|
9589
10043
|
const SVG_NS = { svg: "http://www.w3.org/2000/svg" };
|
|
9590
10044
|
const XHTML_URI = "http://www.w3.org/1999/xhtml";
|
|
@@ -9793,6 +10247,7 @@ var NCXValidator = class {
|
|
|
9793
10247
|
this.checkContentSrc(context, root, ncxPath, registry);
|
|
9794
10248
|
this.checkEmptyLabels(context, root, ncxPath);
|
|
9795
10249
|
this.checkPageTargets(context, root, ncxPath);
|
|
10250
|
+
this.checkIdSyntax(context, root, ncxPath);
|
|
9796
10251
|
} finally {
|
|
9797
10252
|
doc.dispose();
|
|
9798
10253
|
}
|
|
@@ -9840,8 +10295,8 @@ var NCXValidator = class {
|
|
|
9840
10295
|
const hashIdx = src.indexOf("#");
|
|
9841
10296
|
const srcBase = hashIdx >= 0 ? src.substring(0, hashIdx) : src;
|
|
9842
10297
|
const fragment = hashIdx >= 0 ? src.substring(hashIdx + 1) : "";
|
|
9843
|
-
const isRemote =
|
|
9844
|
-
const fullPath = isRemote ? srcBase : resolvePath(ncxPath, srcBase);
|
|
10298
|
+
const isRemote = isRemoteURL(srcBase);
|
|
10299
|
+
const fullPath = isRemote ? srcBase : resolvePath(ncxPath, tryDecodeUriComponent(srcBase)).normalize("NFC");
|
|
9845
10300
|
if (!context.files.has(fullPath) && !isRemote) {
|
|
9846
10301
|
const line = contentElem.line;
|
|
9847
10302
|
pushMessage(context.messages, {
|
|
@@ -9896,10 +10351,21 @@ var NCXValidator = class {
|
|
|
9896
10351
|
}
|
|
9897
10352
|
}
|
|
9898
10353
|
}
|
|
9899
|
-
|
|
9900
|
-
|
|
9901
|
-
|
|
9902
|
-
|
|
10354
|
+
checkIdSyntax(context, root, ncxPath) {
|
|
10355
|
+
const nodes = root.find(".//*[@id]", {
|
|
10356
|
+
ncx: "http://www.daisy.org/z3986/2005/ncx/"
|
|
10357
|
+
});
|
|
10358
|
+
for (const node of nodes) {
|
|
10359
|
+
const idValue = node.attr("id")?.value;
|
|
10360
|
+
if (idValue && !NC_NAME_REGEX.test(idValue)) {
|
|
10361
|
+
pushMessage(context.messages, {
|
|
10362
|
+
id: MessageId.RSC_005,
|
|
10363
|
+
message: `Invalid id "${idValue}"; must match xs:ID syntax (NCName)`,
|
|
10364
|
+
location: { path: ncxPath, line: node.line }
|
|
10365
|
+
});
|
|
10366
|
+
}
|
|
10367
|
+
}
|
|
10368
|
+
}
|
|
9903
10369
|
checkPageTargets(context, root, ncxPath) {
|
|
9904
10370
|
const pageTargets = root.find(".//ncx:pageTarget[@type]", {
|
|
9905
10371
|
ncx: "http://www.daisy.org/z3986/2005/ncx/"
|
|
@@ -9917,6 +10383,7 @@ var NCXValidator = class {
|
|
|
9917
10383
|
}
|
|
9918
10384
|
}
|
|
9919
10385
|
};
|
|
10386
|
+
var NC_NAME_REGEX = /^[A-Za-z_][A-Za-z0-9._-]*$/;
|
|
9920
10387
|
var PAGE_TARGET_TYPES = /* @__PURE__ */ new Set(["front", "normal", "special"]);
|
|
9921
10388
|
var OPS_MEDIA_TYPES = /* @__PURE__ */ new Set([
|
|
9922
10389
|
"application/xhtml+xml",
|
|
@@ -10046,15 +10513,6 @@ function parseContainerContent(content, context, fileExists, getFileContent) {
|
|
|
10046
10513
|
}
|
|
10047
10514
|
}
|
|
10048
10515
|
}
|
|
10049
|
-
for (const rootfile of context.rootfiles) {
|
|
10050
|
-
if (!fileExists(rootfile.path)) {
|
|
10051
|
-
pushMessage(context.messages, {
|
|
10052
|
-
id: MessageId.PKG_010,
|
|
10053
|
-
message: `Rootfile "${rootfile.path}" not found in EPUB`,
|
|
10054
|
-
location: { path: containerPath }
|
|
10055
|
-
});
|
|
10056
|
-
}
|
|
10057
|
-
}
|
|
10058
10516
|
}
|
|
10059
10517
|
function validateMappingDocumentContent(xml, mappingPath, context) {
|
|
10060
10518
|
const stripped = stripXmlComments(xml);
|
|
@@ -10973,19 +11431,7 @@ var ReferenceValidator = class {
|
|
|
10973
11431
|
message: "Absolute paths are not allowed in EPUB",
|
|
10974
11432
|
location: reference.location
|
|
10975
11433
|
});
|
|
10976
|
-
}
|
|
10977
|
-
const forbiddenParentDirTypes = [
|
|
10978
|
-
"hyperlink" /* HYPERLINK */,
|
|
10979
|
-
"nav-toc-link" /* NAV_TOC_LINK */,
|
|
10980
|
-
"nav-pagelist-link" /* NAV_PAGELIST_LINK */
|
|
10981
|
-
];
|
|
10982
|
-
if (hasParentDirectoryReference(reference.url) && forbiddenParentDirTypes.includes(reference.type)) {
|
|
10983
|
-
pushMessage(context.messages, {
|
|
10984
|
-
id: MessageId.RSC_026,
|
|
10985
|
-
message: "Parent directory references (..) are not allowed",
|
|
10986
|
-
location: reference.location
|
|
10987
|
-
});
|
|
10988
|
-
} else if (!hasAbsolutePath(resourcePath) && !hasParentDirectoryReference(reference.url) && checkUrlLeaking(reference.url)) {
|
|
11434
|
+
} else if (checkUrlLeaking(reference.url, reference.location.path)) {
|
|
10989
11435
|
pushMessage(context.messages, {
|
|
10990
11436
|
id: MessageId.RSC_026,
|
|
10991
11437
|
message: `URL "${reference.url}" leaks outside the container`,
|
|
@@ -11023,6 +11469,16 @@ var ReferenceValidator = class {
|
|
|
11023
11469
|
location: reference.location
|
|
11024
11470
|
});
|
|
11025
11471
|
}
|
|
11472
|
+
if (reference.type === "search-key" /* SEARCH_KEY */ && !resource?.inSpine) {
|
|
11473
|
+
const isEpubCfi = reference.fragment?.startsWith("epubcfi(") ?? false;
|
|
11474
|
+
if (!isEpubCfi) {
|
|
11475
|
+
pushMessage(context.messages, {
|
|
11476
|
+
id: MessageId.RSC_021,
|
|
11477
|
+
message: `Search Key Map target "${resourcePath}" must be a Content Document in the spine`,
|
|
11478
|
+
location: reference.location
|
|
11479
|
+
});
|
|
11480
|
+
}
|
|
11481
|
+
}
|
|
11026
11482
|
if (isHyperlinkLike || reference.type === "overlay-text-link" /* OVERLAY_TEXT_LINK */) {
|
|
11027
11483
|
const targetMimeType = resource?.mimeType;
|
|
11028
11484
|
if (targetMimeType && !this.isBlessedItemType(targetMimeType, context.version) && !this.isDeprecatedBlessedItemType(targetMimeType) && !resource.hasCoreMediaTypeFallback) {
|
|
@@ -11507,11 +11963,11 @@ var RelaxNGValidator = class extends BaseSchemaValidator {
|
|
|
11507
11963
|
try {
|
|
11508
11964
|
const libxml2 = await import('libxml2-wasm');
|
|
11509
11965
|
const LibRelaxNGValidator = libxml2.RelaxNGValidator;
|
|
11510
|
-
const { XmlDocument:
|
|
11511
|
-
const doc =
|
|
11966
|
+
const { XmlDocument: XmlDocument5 } = libxml2;
|
|
11967
|
+
const doc = XmlDocument5.fromString(xml);
|
|
11512
11968
|
try {
|
|
11513
11969
|
const schemaContent = await loadSchema(schemaPath);
|
|
11514
|
-
const schemaDoc =
|
|
11970
|
+
const schemaDoc = XmlDocument5.fromString(schemaContent);
|
|
11515
11971
|
try {
|
|
11516
11972
|
const validator = LibRelaxNGValidator.fromDoc(schemaDoc);
|
|
11517
11973
|
try {
|
|
@@ -11975,6 +12431,73 @@ var EpubCheck = class _EpubCheck {
|
|
|
11975
12431
|
location: { path: opfPath }
|
|
11976
12432
|
});
|
|
11977
12433
|
}
|
|
12434
|
+
if (profile === "dict" && context.packageDocument) {
|
|
12435
|
+
const opfDir = opfPath.includes("/") ? opfPath.substring(0, opfPath.lastIndexOf("/")) : "";
|
|
12436
|
+
const manifestByPath = /* @__PURE__ */ new Map();
|
|
12437
|
+
for (const item of context.packageDocument.manifest) {
|
|
12438
|
+
manifestByPath.set(resolveManifestHref(opfDir, item.href), item);
|
|
12439
|
+
}
|
|
12440
|
+
const dictPaths = features.dictionaryContentPaths ?? /* @__PURE__ */ new Set();
|
|
12441
|
+
for (const coll of context.packageDocument.collections) {
|
|
12442
|
+
if (coll.role !== "dictionary") continue;
|
|
12443
|
+
const hasDictContent = coll.links.some((href) => {
|
|
12444
|
+
const full = resolveManifestHref(opfDir, href);
|
|
12445
|
+
const item = manifestByPath.get(full);
|
|
12446
|
+
return item?.mediaType === "application/xhtml+xml" && dictPaths.has(full);
|
|
12447
|
+
});
|
|
12448
|
+
if (!hasDictContent) {
|
|
12449
|
+
pushMessage(context.messages, {
|
|
12450
|
+
id: MessageId.OPF_078,
|
|
12451
|
+
message: 'An EPUB Dictionary must contain at least one Content Document with dictionary content (epub:type "dictionary")',
|
|
12452
|
+
location: { path: opfPath }
|
|
12453
|
+
});
|
|
12454
|
+
}
|
|
12455
|
+
}
|
|
12456
|
+
}
|
|
12457
|
+
}
|
|
12458
|
+
/**
|
|
12459
|
+
* Mirrors ../epubcheck/src/main/resources/com/adobe/epubcheck/schema/30/edupub/
|
|
12460
|
+
* edu-ocf-metadata.sch (META-INF/metadata.xml) and edu-opf.sch (each OPF).
|
|
12461
|
+
*
|
|
12462
|
+
* Non-primary OPFs are otherwise unreached: runPipeline only runs OPFValidator
|
|
12463
|
+
* on context.opfPath.
|
|
12464
|
+
*/
|
|
12465
|
+
validateEdupubMultiRendition(context) {
|
|
12466
|
+
if (context.options.profile !== "edupub") return;
|
|
12467
|
+
if (context.rootfiles.length <= 1) return;
|
|
12468
|
+
const metadataPath = "META-INF/metadata.xml";
|
|
12469
|
+
const metadataData = context.files.get(metadataPath);
|
|
12470
|
+
if (metadataData) {
|
|
12471
|
+
const content = typeof metadataData === "string" ? metadataData : new TextDecoder().decode(metadataData);
|
|
12472
|
+
const stripped = stripXmlComments(content);
|
|
12473
|
+
if (!/<dc:type\b[^>]*>\s*edupub\s*<\/dc:type>/i.test(stripped)) {
|
|
12474
|
+
pushMessage(context.messages, {
|
|
12475
|
+
id: MessageId.RSC_005,
|
|
12476
|
+
message: 'A dc:type element with the value "edupub" is required.',
|
|
12477
|
+
location: { path: metadataPath }
|
|
12478
|
+
});
|
|
12479
|
+
}
|
|
12480
|
+
}
|
|
12481
|
+
const primary = context.opfPath;
|
|
12482
|
+
for (const rootfile of context.rootfiles) {
|
|
12483
|
+
if (rootfile.path === primary) continue;
|
|
12484
|
+
if (rootfile.mediaType !== "application/oebps-package+xml") continue;
|
|
12485
|
+
const path = rootfile.path.normalize("NFC");
|
|
12486
|
+
const opfData = context.files.get(path);
|
|
12487
|
+
if (!opfData) continue;
|
|
12488
|
+
const xml = typeof opfData === "string" ? opfData : new TextDecoder().decode(opfData);
|
|
12489
|
+
const pkg = parseOPF(xml);
|
|
12490
|
+
const hasType = pkg.dcElements.some(
|
|
12491
|
+
(dc) => dc.name === "type" && dc.value.trim() === "edupub"
|
|
12492
|
+
);
|
|
12493
|
+
if (!hasType) {
|
|
12494
|
+
pushMessage(context.messages, {
|
|
12495
|
+
id: MessageId.RSC_005,
|
|
12496
|
+
message: 'The dc:type identifier "edupub" is required.',
|
|
12497
|
+
location: { path }
|
|
12498
|
+
});
|
|
12499
|
+
}
|
|
12500
|
+
}
|
|
11978
12501
|
}
|
|
11979
12502
|
/**
|
|
11980
12503
|
* Validate NCX navigation document (EPUB 2 always, EPUB 3 when NCX present)
|
|
@@ -12120,6 +12643,7 @@ var EpubCheck = class _EpubCheck {
|
|
|
12120
12643
|
const contentValidator = new ContentValidator();
|
|
12121
12644
|
contentValidator.validate(context, registry, refValidator);
|
|
12122
12645
|
this.validateCrossDocumentFeatures(context);
|
|
12646
|
+
this.validateEdupubMultiRendition(context);
|
|
12123
12647
|
if (context.packageDocument) {
|
|
12124
12648
|
this.validateNCX(context, registry);
|
|
12125
12649
|
}
|