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