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