@likecoin/epubcheck-ts 0.3.5 → 0.3.7
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 +2537 -459
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +2537 -459
- 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",
|
|
@@ -870,7 +875,7 @@ var MessageDefs = {
|
|
|
870
875
|
NAV_001: {
|
|
871
876
|
id: "NAV-001",
|
|
872
877
|
severity: "error",
|
|
873
|
-
description:
|
|
878
|
+
description: 'Navigation Document must have a nav element with epub:type="toc"'
|
|
874
879
|
},
|
|
875
880
|
NAV_002: { id: "NAV-002", severity: "suppressed", description: "Missing toc nav element" },
|
|
876
881
|
NAV_003: {
|
|
@@ -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
|
|
@@ -1634,18 +1632,97 @@ var CSSValidator = class {
|
|
|
1634
1632
|
}
|
|
1635
1633
|
};
|
|
1636
1634
|
|
|
1635
|
+
// src/opf/types.ts
|
|
1636
|
+
var CORE_MEDIA_TYPES = /* @__PURE__ */ new Set([
|
|
1637
|
+
// Image types
|
|
1638
|
+
"image/gif",
|
|
1639
|
+
"image/jpeg",
|
|
1640
|
+
"image/png",
|
|
1641
|
+
"image/svg+xml",
|
|
1642
|
+
"image/webp",
|
|
1643
|
+
// Audio types
|
|
1644
|
+
"audio/mpeg",
|
|
1645
|
+
"audio/mp4",
|
|
1646
|
+
"audio/ogg",
|
|
1647
|
+
// CSS
|
|
1648
|
+
"text/css",
|
|
1649
|
+
// Fonts
|
|
1650
|
+
"font/otf",
|
|
1651
|
+
"font/ttf",
|
|
1652
|
+
"font/woff",
|
|
1653
|
+
"font/woff2",
|
|
1654
|
+
"application/font-sfnt",
|
|
1655
|
+
// deprecated alias for font/otf, font/ttf
|
|
1656
|
+
"application/font-woff",
|
|
1657
|
+
// deprecated alias for font/woff
|
|
1658
|
+
"application/vnd.ms-opentype",
|
|
1659
|
+
// deprecated alias
|
|
1660
|
+
// Content documents
|
|
1661
|
+
"application/xhtml+xml",
|
|
1662
|
+
"application/x-dtbncx+xml",
|
|
1663
|
+
// NCX
|
|
1664
|
+
// JavaScript (EPUB 3)
|
|
1665
|
+
"text/javascript",
|
|
1666
|
+
"application/javascript",
|
|
1667
|
+
// Media overlays
|
|
1668
|
+
"application/smil+xml",
|
|
1669
|
+
// PLS (Pronunciation Lexicon)
|
|
1670
|
+
"application/pls+xml"
|
|
1671
|
+
]);
|
|
1672
|
+
function isCoreMediaType(mimeType) {
|
|
1673
|
+
if (CORE_MEDIA_TYPES.has(mimeType)) return true;
|
|
1674
|
+
if (mimeType.startsWith("video/")) return true;
|
|
1675
|
+
if (/^audio\/ogg\s*;\s*codecs=opus$/i.test(mimeType)) return true;
|
|
1676
|
+
const semicolonIndex = mimeType.indexOf(";");
|
|
1677
|
+
if (semicolonIndex >= 0) {
|
|
1678
|
+
const baseType = mimeType.substring(0, semicolonIndex).trim();
|
|
1679
|
+
if (CORE_MEDIA_TYPES.has(baseType)) return true;
|
|
1680
|
+
if (baseType.startsWith("video/")) return true;
|
|
1681
|
+
}
|
|
1682
|
+
return false;
|
|
1683
|
+
}
|
|
1684
|
+
var ITEM_PROPERTIES = /* @__PURE__ */ new Set([
|
|
1685
|
+
"cover-image",
|
|
1686
|
+
"mathml",
|
|
1687
|
+
"nav",
|
|
1688
|
+
"remote-resources",
|
|
1689
|
+
"scripted",
|
|
1690
|
+
"svg",
|
|
1691
|
+
"switch"
|
|
1692
|
+
]);
|
|
1693
|
+
var LINK_PROPERTIES = /* @__PURE__ */ new Set(["onix", "marc21xml-record", "mods-record", "xmp-record"]);
|
|
1694
|
+
var SPINE_PROPERTIES = /* @__PURE__ */ new Set([
|
|
1695
|
+
"page-spread-left",
|
|
1696
|
+
"page-spread-right",
|
|
1697
|
+
"rendition:spread-none",
|
|
1698
|
+
"rendition:spread-landscape",
|
|
1699
|
+
"rendition:spread-portrait",
|
|
1700
|
+
"rendition:spread-both",
|
|
1701
|
+
"rendition:spread-auto",
|
|
1702
|
+
"rendition:page-spread-center",
|
|
1703
|
+
"rendition:layout-reflowable",
|
|
1704
|
+
"rendition:layout-pre-paginated",
|
|
1705
|
+
"rendition:orientation-auto",
|
|
1706
|
+
"rendition:orientation-landscape",
|
|
1707
|
+
"rendition:orientation-portrait"
|
|
1708
|
+
]);
|
|
1709
|
+
|
|
1637
1710
|
// src/references/types.ts
|
|
1711
|
+
var PUBLICATION_RESOURCE_TYPES = /* @__PURE__ */ new Set([
|
|
1712
|
+
"generic" /* GENERIC */,
|
|
1713
|
+
"stylesheet" /* STYLESHEET */,
|
|
1714
|
+
"font" /* FONT */,
|
|
1715
|
+
"image" /* IMAGE */,
|
|
1716
|
+
"audio" /* AUDIO */,
|
|
1717
|
+
"video" /* VIDEO */,
|
|
1718
|
+
"track" /* TRACK */,
|
|
1719
|
+
"media-overlay" /* MEDIA_OVERLAY */,
|
|
1720
|
+
"svg-symbol" /* SVG_SYMBOL */,
|
|
1721
|
+
"svg-paint" /* SVG_PAINT */,
|
|
1722
|
+
"svg-clip-path" /* SVG_CLIP_PATH */
|
|
1723
|
+
]);
|
|
1638
1724
|
function isPublicationResourceReference(type) {
|
|
1639
|
-
return
|
|
1640
|
-
"generic" /* GENERIC */,
|
|
1641
|
-
"stylesheet" /* STYLESHEET */,
|
|
1642
|
-
"font" /* FONT */,
|
|
1643
|
-
"image" /* IMAGE */,
|
|
1644
|
-
"audio" /* AUDIO */,
|
|
1645
|
-
"video" /* VIDEO */,
|
|
1646
|
-
"track" /* TRACK */,
|
|
1647
|
-
"media-overlay" /* MEDIA_OVERLAY */
|
|
1648
|
-
].includes(type);
|
|
1725
|
+
return PUBLICATION_RESOURCE_TYPES.has(type);
|
|
1649
1726
|
}
|
|
1650
1727
|
|
|
1651
1728
|
// src/references/url.ts
|
|
@@ -1702,6 +1779,17 @@ function isHTTP(url) {
|
|
|
1702
1779
|
function isRemoteURL(url) {
|
|
1703
1780
|
return isHTTP(url) || isHTTPS(url);
|
|
1704
1781
|
}
|
|
1782
|
+
function checkUrlLeaking(href) {
|
|
1783
|
+
const TEST_BASE_A = "https://a.example.org/A/";
|
|
1784
|
+
const TEST_BASE_B = "https://b.example.org/B/";
|
|
1785
|
+
try {
|
|
1786
|
+
const urlA = new URL(href, TEST_BASE_A).toString();
|
|
1787
|
+
const urlB = new URL(href, TEST_BASE_B).toString();
|
|
1788
|
+
return !urlA.startsWith(TEST_BASE_A) || !urlB.startsWith(TEST_BASE_B);
|
|
1789
|
+
} catch {
|
|
1790
|
+
return false;
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1705
1793
|
function resolveManifestHref(opfDir, href) {
|
|
1706
1794
|
if (isRemoteURL(href)) return href;
|
|
1707
1795
|
try {
|
|
@@ -1715,7 +1803,184 @@ function resolveManifestHref(opfDir, href) {
|
|
|
1715
1803
|
}
|
|
1716
1804
|
|
|
1717
1805
|
// src/content/validator.ts
|
|
1718
|
-
var DISCOURAGED_ELEMENTS = /* @__PURE__ */ new Set(["base", "embed"]);
|
|
1806
|
+
var DISCOURAGED_ELEMENTS = /* @__PURE__ */ new Set(["base", "embed", "rp"]);
|
|
1807
|
+
var ABSOLUTE_URI_RE = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
|
|
1808
|
+
var IMAGE_MAGIC = [
|
|
1809
|
+
{ mime: "image/jpeg", bytes: [255, 216], extensions: [".jpg", ".jpeg", ".jpe"] },
|
|
1810
|
+
{ mime: "image/gif", bytes: [71, 73, 70, 56], extensions: [".gif"] },
|
|
1811
|
+
{ mime: "image/png", bytes: [137, 80, 78, 71], extensions: [".png"] },
|
|
1812
|
+
{ mime: "image/webp", bytes: [82, 73, 70, 70], extensions: [".webp"] }
|
|
1813
|
+
];
|
|
1814
|
+
function stripMimeParams(t) {
|
|
1815
|
+
const idx = t.indexOf(";");
|
|
1816
|
+
return (idx >= 0 ? t.substring(0, idx) : t).trim();
|
|
1817
|
+
}
|
|
1818
|
+
var EPUB_SSV_DEPRECATED = /* @__PURE__ */ new Set([
|
|
1819
|
+
"annoref",
|
|
1820
|
+
"annotation",
|
|
1821
|
+
"biblioentry",
|
|
1822
|
+
"bridgehead",
|
|
1823
|
+
"endnote",
|
|
1824
|
+
"help",
|
|
1825
|
+
"marginalia",
|
|
1826
|
+
"note",
|
|
1827
|
+
"rearnote",
|
|
1828
|
+
"rearnotes",
|
|
1829
|
+
"sidebar",
|
|
1830
|
+
"subchapter",
|
|
1831
|
+
"warning"
|
|
1832
|
+
]);
|
|
1833
|
+
var EPUB_SSV_DISALLOWED_ON_CONTENT = /* @__PURE__ */ new Set([
|
|
1834
|
+
"aside",
|
|
1835
|
+
"figure",
|
|
1836
|
+
"list",
|
|
1837
|
+
"list-item",
|
|
1838
|
+
"table",
|
|
1839
|
+
"table-cell",
|
|
1840
|
+
"table-row"
|
|
1841
|
+
]);
|
|
1842
|
+
var EPUB_SSV_ALL = /* @__PURE__ */ new Set([
|
|
1843
|
+
...EPUB_SSV_DEPRECATED,
|
|
1844
|
+
...EPUB_SSV_DISALLOWED_ON_CONTENT,
|
|
1845
|
+
"abstract",
|
|
1846
|
+
"acknowledgments",
|
|
1847
|
+
"afterword",
|
|
1848
|
+
"appendix",
|
|
1849
|
+
"assessment",
|
|
1850
|
+
"assessments",
|
|
1851
|
+
"backlink",
|
|
1852
|
+
"backmatter",
|
|
1853
|
+
"balloon",
|
|
1854
|
+
"bibliography",
|
|
1855
|
+
"biblioref",
|
|
1856
|
+
"bodymatter",
|
|
1857
|
+
"case-study",
|
|
1858
|
+
"chapter",
|
|
1859
|
+
"colophon",
|
|
1860
|
+
"concluding-sentence",
|
|
1861
|
+
"conclusion",
|
|
1862
|
+
"contributors",
|
|
1863
|
+
"copyright-page",
|
|
1864
|
+
"cover",
|
|
1865
|
+
"covertitle",
|
|
1866
|
+
"credit",
|
|
1867
|
+
"credits",
|
|
1868
|
+
"dedication",
|
|
1869
|
+
"division",
|
|
1870
|
+
"endnotes",
|
|
1871
|
+
"epigraph",
|
|
1872
|
+
"epilogue",
|
|
1873
|
+
"errata",
|
|
1874
|
+
"fill-in-the-blank-problem",
|
|
1875
|
+
"footnote",
|
|
1876
|
+
"footnotes",
|
|
1877
|
+
"foreword",
|
|
1878
|
+
"frontmatter",
|
|
1879
|
+
"fulltitle",
|
|
1880
|
+
"general-problem",
|
|
1881
|
+
"glossary",
|
|
1882
|
+
"glossdef",
|
|
1883
|
+
"glossref",
|
|
1884
|
+
"glossterm",
|
|
1885
|
+
"halftitle",
|
|
1886
|
+
"halftitlepage",
|
|
1887
|
+
"imprimatur",
|
|
1888
|
+
"imprint",
|
|
1889
|
+
"index",
|
|
1890
|
+
"index-editor-note",
|
|
1891
|
+
"index-entry",
|
|
1892
|
+
"index-entry-list",
|
|
1893
|
+
"index-group",
|
|
1894
|
+
"index-headnotes",
|
|
1895
|
+
"index-legend",
|
|
1896
|
+
"index-locator",
|
|
1897
|
+
"index-locator-list",
|
|
1898
|
+
"index-locator-range",
|
|
1899
|
+
"index-term",
|
|
1900
|
+
"index-term-categories",
|
|
1901
|
+
"index-term-category",
|
|
1902
|
+
"index-xref-preferred",
|
|
1903
|
+
"index-xref-related",
|
|
1904
|
+
"introduction",
|
|
1905
|
+
"keyword",
|
|
1906
|
+
"keywords",
|
|
1907
|
+
"label",
|
|
1908
|
+
"landmarks",
|
|
1909
|
+
"learning-objective",
|
|
1910
|
+
"learning-objectives",
|
|
1911
|
+
"learning-outcome",
|
|
1912
|
+
"learning-outcomes",
|
|
1913
|
+
"learning-resource",
|
|
1914
|
+
"learning-resources",
|
|
1915
|
+
"learning-standard",
|
|
1916
|
+
"learning-standards",
|
|
1917
|
+
"loa",
|
|
1918
|
+
"loi",
|
|
1919
|
+
"lot",
|
|
1920
|
+
"lov",
|
|
1921
|
+
"match-problem",
|
|
1922
|
+
"multiple-choice-problem",
|
|
1923
|
+
"noteref",
|
|
1924
|
+
"notice",
|
|
1925
|
+
"ordinal",
|
|
1926
|
+
"other-credits",
|
|
1927
|
+
"page-list",
|
|
1928
|
+
"pagebreak",
|
|
1929
|
+
"panel",
|
|
1930
|
+
"panel-group",
|
|
1931
|
+
"part",
|
|
1932
|
+
"practice",
|
|
1933
|
+
"practices",
|
|
1934
|
+
"preamble",
|
|
1935
|
+
"preface",
|
|
1936
|
+
"prologue",
|
|
1937
|
+
"pullquote",
|
|
1938
|
+
"qna",
|
|
1939
|
+
"question",
|
|
1940
|
+
"referrer",
|
|
1941
|
+
"revision-history",
|
|
1942
|
+
"seriespage",
|
|
1943
|
+
"sound-area",
|
|
1944
|
+
"subtitle",
|
|
1945
|
+
"tip",
|
|
1946
|
+
"title",
|
|
1947
|
+
"titlepage",
|
|
1948
|
+
"toc",
|
|
1949
|
+
"toc-brief",
|
|
1950
|
+
"topic-sentence",
|
|
1951
|
+
"true-false-problem",
|
|
1952
|
+
"volume"
|
|
1953
|
+
]);
|
|
1954
|
+
var TIME_RE = /^(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d{1,3})?)?$/;
|
|
1955
|
+
var TZ_RE = /(?:Z|[+-](?:[01]\d|2[0-3]):?[0-5]\d)$/;
|
|
1956
|
+
var DATE_RE = /^\d{4,}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/;
|
|
1957
|
+
var ISO_DURATION_RE = /^P(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d{1,3})?S)?)?$/;
|
|
1958
|
+
var INFORMAL_DURATION_RE = /^\s*(?:\d+(?:\.\d{1,3})?[WDHMS]\s*)+$/;
|
|
1959
|
+
function isValidDatetime(value) {
|
|
1960
|
+
const trimmed = value.trim();
|
|
1961
|
+
if (trimmed === "") return false;
|
|
1962
|
+
if (trimmed.startsWith("P")) {
|
|
1963
|
+
if (!ISO_DURATION_RE.test(trimmed)) return false;
|
|
1964
|
+
if (trimmed === "P" || trimmed === "PT") return false;
|
|
1965
|
+
if (trimmed.endsWith("T")) return false;
|
|
1966
|
+
return true;
|
|
1967
|
+
}
|
|
1968
|
+
if (INFORMAL_DURATION_RE.test(value)) return true;
|
|
1969
|
+
if (/^\d{4,}$/.test(trimmed)) return true;
|
|
1970
|
+
if (/^\d{4,}-(?:0[1-9]|1[0-2])$/.test(trimmed)) return true;
|
|
1971
|
+
if (/^-?-?(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/.test(trimmed)) return true;
|
|
1972
|
+
if (DATE_RE.test(trimmed)) return true;
|
|
1973
|
+
if (/^\d{4,}-W(?:0[1-9]|[1-4]\d|5[0-3])$/.test(trimmed)) return true;
|
|
1974
|
+
if (TIME_RE.test(trimmed)) return true;
|
|
1975
|
+
const dtMatch = /^(\d{4,}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01]))[T ]([\s\S]+)$/.exec(trimmed);
|
|
1976
|
+
if (dtMatch?.[2]) {
|
|
1977
|
+
let timePart = dtMatch[2];
|
|
1978
|
+
const tzMatch = TZ_RE.exec(timePart);
|
|
1979
|
+
if (tzMatch) timePart = timePart.substring(0, timePart.length - tzMatch[0].length);
|
|
1980
|
+
return TIME_RE.test(timePart);
|
|
1981
|
+
}
|
|
1982
|
+
return false;
|
|
1983
|
+
}
|
|
1719
1984
|
var HTML_ENTITIES = /* @__PURE__ */ new Set([
|
|
1720
1985
|
"nbsp",
|
|
1721
1986
|
"iexcl",
|
|
@@ -1815,6 +2080,7 @@ var HTML_ENTITIES = /* @__PURE__ */ new Set([
|
|
|
1815
2080
|
"yuml"
|
|
1816
2081
|
]);
|
|
1817
2082
|
var ContentValidator = class {
|
|
2083
|
+
cssWithRemoteResources = /* @__PURE__ */ new Set();
|
|
1818
2084
|
validate(context, registry, refValidator) {
|
|
1819
2085
|
const packageDoc = context.packageDocument;
|
|
1820
2086
|
if (!packageDoc) {
|
|
@@ -1822,13 +2088,18 @@ var ContentValidator = class {
|
|
|
1822
2088
|
}
|
|
1823
2089
|
const opfPath = context.opfPath ?? "";
|
|
1824
2090
|
const opfDir = opfPath.includes("/") ? opfPath.substring(0, opfPath.lastIndexOf("/")) : "";
|
|
2091
|
+
if (refValidator) {
|
|
2092
|
+
for (const item of packageDoc.manifest) {
|
|
2093
|
+
if (item.mediaType === "text/css") {
|
|
2094
|
+
const fullPath = resolveManifestHref(opfDir, item.href);
|
|
2095
|
+
this.validateCSSDocument(context, fullPath, opfDir, refValidator);
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
1825
2099
|
for (const item of packageDoc.manifest) {
|
|
1826
2100
|
if (item.mediaType === "application/xhtml+xml") {
|
|
1827
2101
|
const fullPath = resolveManifestHref(opfDir, item.href);
|
|
1828
2102
|
this.validateXHTMLDocument(context, fullPath, item.id, opfDir, registry, refValidator);
|
|
1829
|
-
} else if (item.mediaType === "text/css" && refValidator) {
|
|
1830
|
-
const fullPath = resolveManifestHref(opfDir, item.href);
|
|
1831
|
-
this.validateCSSDocument(context, fullPath, opfDir, refValidator);
|
|
1832
2103
|
} else if (item.mediaType === "image/svg+xml") {
|
|
1833
2104
|
const fullPath = resolveManifestHref(opfDir, item.href);
|
|
1834
2105
|
if (registry) {
|
|
@@ -1837,7 +2108,51 @@ var ContentValidator = class {
|
|
|
1837
2108
|
if (context.version.startsWith("3")) {
|
|
1838
2109
|
this.validateSVGDocument(context, fullPath, item);
|
|
1839
2110
|
}
|
|
2111
|
+
if (refValidator) {
|
|
2112
|
+
this.extractSVGReferences(context, fullPath, opfDir, refValidator);
|
|
2113
|
+
}
|
|
1840
2114
|
}
|
|
2115
|
+
this.validateMediaFile(context, item, opfDir);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
validateMediaFile(context, item, opfDir) {
|
|
2119
|
+
const declaredType = item.mediaType;
|
|
2120
|
+
const magicEntry = IMAGE_MAGIC.find((m) => m.mime === declaredType);
|
|
2121
|
+
if (!magicEntry) return;
|
|
2122
|
+
const fullPath = resolveManifestHref(opfDir, item.href);
|
|
2123
|
+
const fileData = context.files.get(fullPath);
|
|
2124
|
+
if (!fileData) return;
|
|
2125
|
+
const bytes = typeof fileData === "string" ? new TextEncoder().encode(fileData) : fileData;
|
|
2126
|
+
if (bytes.length < 4) {
|
|
2127
|
+
pushMessage(context.messages, {
|
|
2128
|
+
id: MessageId.MED_004,
|
|
2129
|
+
message: "Image file header may be corrupted",
|
|
2130
|
+
location: { path: fullPath }
|
|
2131
|
+
});
|
|
2132
|
+
pushMessage(context.messages, {
|
|
2133
|
+
id: MessageId.PKG_021,
|
|
2134
|
+
message: "Corrupted image file encountered",
|
|
2135
|
+
location: { path: fullPath }
|
|
2136
|
+
});
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
2139
|
+
const headerMatches = magicEntry.bytes.every((b, i) => bytes[i] === b);
|
|
2140
|
+
if (!headerMatches) {
|
|
2141
|
+
const actualType = IMAGE_MAGIC.find((m) => m.bytes.every((b, i) => bytes[i] === b));
|
|
2142
|
+
pushMessage(context.messages, {
|
|
2143
|
+
id: MessageId.OPF_029,
|
|
2144
|
+
message: `File does not match declared media type "${declaredType}"${actualType ? ` (appears to be ${actualType.mime})` : ""}`,
|
|
2145
|
+
location: { path: fullPath }
|
|
2146
|
+
});
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
2149
|
+
const ext = item.href.includes(".") ? item.href.substring(item.href.lastIndexOf(".")).toLowerCase() : "";
|
|
2150
|
+
if (ext && !magicEntry.extensions.includes(ext)) {
|
|
2151
|
+
pushMessage(context.messages, {
|
|
2152
|
+
id: MessageId.PKG_022,
|
|
2153
|
+
message: `Wrong file extension "${ext}" for declared media type "${declaredType}"`,
|
|
2154
|
+
location: { path: fullPath }
|
|
2155
|
+
});
|
|
1841
2156
|
}
|
|
1842
2157
|
}
|
|
1843
2158
|
extractSVGIDs(context, path, registry) {
|
|
@@ -1880,9 +2195,154 @@ var ContentValidator = class {
|
|
|
1880
2195
|
location: { path }
|
|
1881
2196
|
});
|
|
1882
2197
|
}
|
|
2198
|
+
this.checkDuplicateIDs(context, path, root);
|
|
2199
|
+
this.checkSVGInvalidIDs(context, path, root);
|
|
2200
|
+
this.validateSvgEpubType(context, path, root);
|
|
2201
|
+
this.checkUnknownEpubAttributes(context, path, root);
|
|
2202
|
+
} finally {
|
|
2203
|
+
doc.dispose();
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
/**
|
|
2207
|
+
* Extract references from SVG documents: font-face-uri, xml-stylesheet PI, @import in style
|
|
2208
|
+
*/
|
|
2209
|
+
extractSVGReferences(context, path, opfDir, refValidator) {
|
|
2210
|
+
const svgData = context.files.get(path);
|
|
2211
|
+
if (!svgData) return;
|
|
2212
|
+
const svgContent = new TextDecoder().decode(svgData);
|
|
2213
|
+
let doc;
|
|
2214
|
+
try {
|
|
2215
|
+
doc = libxml2Wasm.XmlDocument.fromString(svgContent);
|
|
2216
|
+
} catch {
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2219
|
+
const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
|
|
2220
|
+
try {
|
|
2221
|
+
const root = doc.root;
|
|
2222
|
+
try {
|
|
2223
|
+
const fontFaceUris = root.find(".//svg:font-face-uri", {
|
|
2224
|
+
svg: "http://www.w3.org/2000/svg"
|
|
2225
|
+
});
|
|
2226
|
+
for (const uri of fontFaceUris) {
|
|
2227
|
+
const href = this.getAttribute(uri, "xlink:href") ?? this.getAttribute(uri, "href");
|
|
2228
|
+
if (!href) continue;
|
|
2229
|
+
if (href.startsWith("http://") || href.startsWith("https://")) {
|
|
2230
|
+
refValidator.addReference({
|
|
2231
|
+
url: href,
|
|
2232
|
+
targetResource: href,
|
|
2233
|
+
type: "font" /* FONT */,
|
|
2234
|
+
location: { path, line: uri.line }
|
|
2235
|
+
});
|
|
2236
|
+
} else {
|
|
2237
|
+
const resolvedPath = this.resolveRelativePath(docDir, href, opfDir);
|
|
2238
|
+
refValidator.addReference({
|
|
2239
|
+
url: href,
|
|
2240
|
+
targetResource: resolvedPath,
|
|
2241
|
+
type: "font" /* FONT */,
|
|
2242
|
+
location: { path, line: uri.line }
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
} catch {
|
|
2247
|
+
}
|
|
2248
|
+
try {
|
|
2249
|
+
const styles = root.find(".//svg:style", { svg: "http://www.w3.org/2000/svg" });
|
|
2250
|
+
for (const style of styles) {
|
|
2251
|
+
const cssContent = style.content;
|
|
2252
|
+
if (cssContent) {
|
|
2253
|
+
this.extractCSSImports(path, cssContent, opfDir, refValidator);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
} catch {
|
|
2257
|
+
}
|
|
2258
|
+
this.extractSVGUseReferences(context, path, root, docDir, opfDir, refValidator);
|
|
1883
2259
|
} finally {
|
|
1884
2260
|
doc.dispose();
|
|
1885
2261
|
}
|
|
2262
|
+
this.extractXmlStylesheetPIs(svgContent, path, docDir, opfDir, refValidator);
|
|
2263
|
+
}
|
|
2264
|
+
/**
|
|
2265
|
+
* Extract href from <?xml-stylesheet?> processing instructions
|
|
2266
|
+
*/
|
|
2267
|
+
extractXmlStylesheetPIs(content, path, docDir, opfDir, refValidator) {
|
|
2268
|
+
const piRegex = /<\?xml-stylesheet\s+([^?]*)\?>/g;
|
|
2269
|
+
let match;
|
|
2270
|
+
while ((match = piRegex.exec(content)) !== null) {
|
|
2271
|
+
const attrs = match[1];
|
|
2272
|
+
if (!attrs) continue;
|
|
2273
|
+
const hrefMatch = /href\s*=\s*["']([^"']*)["']/.exec(attrs);
|
|
2274
|
+
if (!hrefMatch?.[1]) continue;
|
|
2275
|
+
const href = hrefMatch[1];
|
|
2276
|
+
const beforeMatch = content.substring(0, match.index);
|
|
2277
|
+
const line = beforeMatch.split("\n").length;
|
|
2278
|
+
if (href.startsWith("http://") || href.startsWith("https://")) {
|
|
2279
|
+
refValidator.addReference({
|
|
2280
|
+
url: href,
|
|
2281
|
+
targetResource: href,
|
|
2282
|
+
type: "stylesheet" /* STYLESHEET */,
|
|
2283
|
+
location: { path, line }
|
|
2284
|
+
});
|
|
2285
|
+
} else {
|
|
2286
|
+
const resolvedPath = this.resolveRelativePath(docDir, href, opfDir);
|
|
2287
|
+
refValidator.addReference({
|
|
2288
|
+
url: href,
|
|
2289
|
+
targetResource: resolvedPath,
|
|
2290
|
+
type: "stylesheet" /* STYLESHEET */,
|
|
2291
|
+
location: { path, line }
|
|
2292
|
+
});
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
extractSVGUseReferences(context, path, root, docDir, opfDir, refValidator) {
|
|
2297
|
+
try {
|
|
2298
|
+
const svgUseXlink = root.find(".//svg:use[@xlink:href]", {
|
|
2299
|
+
svg: "http://www.w3.org/2000/svg",
|
|
2300
|
+
xlink: "http://www.w3.org/1999/xlink"
|
|
2301
|
+
});
|
|
2302
|
+
const svgUseHref = root.find(".//svg:use[@href]", {
|
|
2303
|
+
svg: "http://www.w3.org/2000/svg"
|
|
2304
|
+
});
|
|
2305
|
+
for (const useNode of [...svgUseXlink, ...svgUseHref]) {
|
|
2306
|
+
const useElem = useNode;
|
|
2307
|
+
const href = this.getAttribute(useElem, "xlink:href") ?? this.getAttribute(useElem, "href");
|
|
2308
|
+
if (href === null) continue;
|
|
2309
|
+
if (href.startsWith("http://") || href.startsWith("https://")) continue;
|
|
2310
|
+
const line = useNode.line;
|
|
2311
|
+
if (href === "" || !href.includes("#")) {
|
|
2312
|
+
pushMessage(context.messages, {
|
|
2313
|
+
id: MessageId.RSC_015,
|
|
2314
|
+
message: `SVG "use" element requires a fragment identifier, but found "${href}"`,
|
|
2315
|
+
location: { path, line }
|
|
2316
|
+
});
|
|
2317
|
+
continue;
|
|
2318
|
+
}
|
|
2319
|
+
if (href.startsWith("#")) {
|
|
2320
|
+
refValidator.addReference({
|
|
2321
|
+
url: href,
|
|
2322
|
+
targetResource: path,
|
|
2323
|
+
fragment: href.slice(1),
|
|
2324
|
+
type: "svg-symbol" /* SVG_SYMBOL */,
|
|
2325
|
+
location: { path, line }
|
|
2326
|
+
});
|
|
2327
|
+
continue;
|
|
2328
|
+
}
|
|
2329
|
+
const resolvedPath = this.resolveRelativePath(docDir, href, opfDir);
|
|
2330
|
+
const hashIndex = resolvedPath.indexOf("#");
|
|
2331
|
+
const targetResource = hashIndex >= 0 ? resolvedPath.slice(0, hashIndex) : path;
|
|
2332
|
+
const fragment = hashIndex >= 0 ? resolvedPath.slice(hashIndex + 1) : void 0;
|
|
2333
|
+
const useRef = {
|
|
2334
|
+
url: href,
|
|
2335
|
+
targetResource,
|
|
2336
|
+
type: "svg-symbol" /* SVG_SYMBOL */,
|
|
2337
|
+
location: { path, line }
|
|
2338
|
+
};
|
|
2339
|
+
if (fragment) {
|
|
2340
|
+
useRef.fragment = fragment;
|
|
2341
|
+
}
|
|
2342
|
+
refValidator.addReference(useRef);
|
|
2343
|
+
}
|
|
2344
|
+
} catch {
|
|
2345
|
+
}
|
|
1886
2346
|
}
|
|
1887
2347
|
detectSVGRemoteResources(root) {
|
|
1888
2348
|
try {
|
|
@@ -1931,6 +2391,7 @@ var ContentValidator = class {
|
|
|
1931
2391
|
(ref) => ref.url.startsWith("http://") || ref.url.startsWith("https://")
|
|
1932
2392
|
);
|
|
1933
2393
|
if (hasRemoteResources) {
|
|
2394
|
+
this.cssWithRemoteResources.add(path);
|
|
1934
2395
|
const packageDoc = context.packageDocument;
|
|
1935
2396
|
if (packageDoc) {
|
|
1936
2397
|
const manifestItem = packageDoc.manifest.find(
|
|
@@ -1949,9 +2410,11 @@ var ContentValidator = class {
|
|
|
1949
2410
|
for (const ref of result.references) {
|
|
1950
2411
|
if (ref.type === "font") {
|
|
1951
2412
|
if (ref.url.startsWith("http://") || ref.url.startsWith("https://")) {
|
|
2413
|
+
const hashIndex = ref.url.indexOf("#");
|
|
2414
|
+
const targetResource = hashIndex >= 0 ? ref.url.slice(0, hashIndex) : ref.url;
|
|
1952
2415
|
refValidator.addReference({
|
|
1953
2416
|
url: ref.url,
|
|
1954
|
-
targetResource
|
|
2417
|
+
targetResource,
|
|
1955
2418
|
type: "font" /* FONT */,
|
|
1956
2419
|
location: { path }
|
|
1957
2420
|
});
|
|
@@ -1968,9 +2431,11 @@ var ContentValidator = class {
|
|
|
1968
2431
|
}
|
|
1969
2432
|
} else if (ref.type === "image") {
|
|
1970
2433
|
if (ref.url.startsWith("http://") || ref.url.startsWith("https://")) {
|
|
2434
|
+
const hashIndex = ref.url.indexOf("#");
|
|
2435
|
+
const targetResource = hashIndex >= 0 ? ref.url.slice(0, hashIndex) : ref.url;
|
|
1971
2436
|
refValidator.addReference({
|
|
1972
2437
|
url: ref.url,
|
|
1973
|
-
targetResource
|
|
2438
|
+
targetResource,
|
|
1974
2439
|
type: "image" /* IMAGE */,
|
|
1975
2440
|
location: { path }
|
|
1976
2441
|
});
|
|
@@ -1985,9 +2450,27 @@ var ContentValidator = class {
|
|
|
1985
2450
|
location: { path }
|
|
1986
2451
|
});
|
|
1987
2452
|
}
|
|
2453
|
+
} else if (ref.type === "import") {
|
|
2454
|
+
const location = { path };
|
|
2455
|
+
if (ref.line !== void 0) location.line = ref.line;
|
|
2456
|
+
if (ref.url.startsWith("http://") || ref.url.startsWith("https://")) {
|
|
2457
|
+
refValidator.addReference({
|
|
2458
|
+
url: ref.url,
|
|
2459
|
+
targetResource: ref.url,
|
|
2460
|
+
type: "stylesheet" /* STYLESHEET */,
|
|
2461
|
+
location
|
|
2462
|
+
});
|
|
2463
|
+
} else {
|
|
2464
|
+
const resolvedPath = this.resolveRelativePath(cssDir, ref.url, opfDir);
|
|
2465
|
+
refValidator.addReference({
|
|
2466
|
+
url: ref.url,
|
|
2467
|
+
targetResource: resolvedPath,
|
|
2468
|
+
type: "stylesheet" /* STYLESHEET */,
|
|
2469
|
+
location
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
1988
2472
|
}
|
|
1989
2473
|
}
|
|
1990
|
-
this.extractCSSImports(path, cssContent, opfDir, refValidator);
|
|
1991
2474
|
}
|
|
1992
2475
|
validateXHTMLDocument(context, path, itemId, opfDir, registry, refValidator) {
|
|
1993
2476
|
const data = context.files.get(path);
|
|
@@ -2000,6 +2483,43 @@ var ContentValidator = class {
|
|
|
2000
2483
|
return;
|
|
2001
2484
|
}
|
|
2002
2485
|
this.checkUnescapedAmpersands(context, path, content);
|
|
2486
|
+
if (context.version !== "2.0") {
|
|
2487
|
+
const doctypeMatch = /<!DOCTYPE\s+html\b([\s\S]*?)>/i.exec(content);
|
|
2488
|
+
if (doctypeMatch) {
|
|
2489
|
+
const inner = doctypeMatch[1] ?? "";
|
|
2490
|
+
const hasPublic = /\bPUBLIC\b/i.test(inner);
|
|
2491
|
+
const hasSystem = /\bSYSTEM\b/i.test(inner);
|
|
2492
|
+
const isLegacyCompat = /['"]about:legacy-compat['"]/.test(inner);
|
|
2493
|
+
if (hasPublic || hasSystem && !isLegacyCompat) {
|
|
2494
|
+
pushMessage(context.messages, {
|
|
2495
|
+
id: MessageId.HTM_004,
|
|
2496
|
+
message: 'Irregular DOCTYPE found; expected "<!DOCTYPE html>"',
|
|
2497
|
+
location: { path }
|
|
2498
|
+
});
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
if (context.version !== "2.0") {
|
|
2503
|
+
const entityRe = /<!ENTITY\s+\w+\s+(?:SYSTEM|PUBLIC)\s/gi;
|
|
2504
|
+
let entityMatch = entityRe.exec(content);
|
|
2505
|
+
while (entityMatch) {
|
|
2506
|
+
pushMessage(context.messages, {
|
|
2507
|
+
id: MessageId.HTM_003,
|
|
2508
|
+
message: "External entities are not allowed in EPUB 3 content documents",
|
|
2509
|
+
location: { path }
|
|
2510
|
+
});
|
|
2511
|
+
entityMatch = entityRe.exec(content);
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
const xmlVersionMatch = /<\?xml\s[^?]*version\s*=\s*["']([^"']+)["']/.exec(content);
|
|
2515
|
+
if (xmlVersionMatch?.[1] && xmlVersionMatch[1] !== "1.0") {
|
|
2516
|
+
pushMessage(context.messages, {
|
|
2517
|
+
id: MessageId.HTM_001,
|
|
2518
|
+
message: `XML version "${xmlVersionMatch[1]}" is not allowed; must be "1.0"`,
|
|
2519
|
+
location: { path }
|
|
2520
|
+
});
|
|
2521
|
+
return;
|
|
2522
|
+
}
|
|
2003
2523
|
let doc = null;
|
|
2004
2524
|
try {
|
|
2005
2525
|
doc = libxml2Wasm.XmlDocument.fromString(content);
|
|
@@ -2019,8 +2539,9 @@ var ContentValidator = class {
|
|
|
2019
2539
|
if (column !== void 0) {
|
|
2020
2540
|
location.column = column;
|
|
2021
2541
|
}
|
|
2542
|
+
const isEntityError = error.message.includes("Entity '") || error.message.includes("EntityRef:");
|
|
2022
2543
|
pushMessage(context.messages, {
|
|
2023
|
-
id: MessageId.HTM_004,
|
|
2544
|
+
id: isEntityError ? MessageId.RSC_016 : MessageId.HTM_004,
|
|
2024
2545
|
message,
|
|
2025
2546
|
location
|
|
2026
2547
|
});
|
|
@@ -2050,10 +2571,19 @@ var ContentValidator = class {
|
|
|
2050
2571
|
const title = root.get(".//html:title", { html: "http://www.w3.org/1999/xhtml" });
|
|
2051
2572
|
if (!title) {
|
|
2052
2573
|
pushMessage(context.messages, {
|
|
2053
|
-
id: MessageId.
|
|
2054
|
-
message: "
|
|
2574
|
+
id: MessageId.RSC_017,
|
|
2575
|
+
message: 'The "head" element should have a "title" child element',
|
|
2055
2576
|
location: { path }
|
|
2056
2577
|
});
|
|
2578
|
+
} else {
|
|
2579
|
+
const titleText = title.content.trim();
|
|
2580
|
+
if (titleText === "") {
|
|
2581
|
+
pushMessage(context.messages, {
|
|
2582
|
+
id: MessageId.RSC_005,
|
|
2583
|
+
message: 'The "title" element must not be empty',
|
|
2584
|
+
location: { path, line: title.line }
|
|
2585
|
+
});
|
|
2586
|
+
}
|
|
2057
2587
|
}
|
|
2058
2588
|
const body = root.get(".//html:body", { html: "http://www.w3.org/1999/xhtml" });
|
|
2059
2589
|
if (!body) {
|
|
@@ -2094,6 +2624,13 @@ var ContentValidator = class {
|
|
|
2094
2624
|
location: { path }
|
|
2095
2625
|
});
|
|
2096
2626
|
}
|
|
2627
|
+
if (!hasMathML && manifestItem?.properties?.includes("mathml")) {
|
|
2628
|
+
pushMessage(context.messages, {
|
|
2629
|
+
id: MessageId.OPF_015,
|
|
2630
|
+
message: 'The property "mathml" should not be declared in the OPF file',
|
|
2631
|
+
location: { path }
|
|
2632
|
+
});
|
|
2633
|
+
}
|
|
2097
2634
|
const hasSVG = this.detectSVG(context, path, root);
|
|
2098
2635
|
if (hasSVG && !manifestItem?.properties?.includes("svg")) {
|
|
2099
2636
|
pushMessage(context.messages, {
|
|
@@ -2117,7 +2654,14 @@ var ContentValidator = class {
|
|
|
2117
2654
|
location: { path }
|
|
2118
2655
|
});
|
|
2119
2656
|
}
|
|
2120
|
-
|
|
2657
|
+
if (!hasSwitch && manifestItem?.properties?.includes("switch")) {
|
|
2658
|
+
pushMessage(context.messages, {
|
|
2659
|
+
id: MessageId.OPF_015,
|
|
2660
|
+
message: 'The property "switch" should not be declared in the OPF file',
|
|
2661
|
+
location: { path }
|
|
2662
|
+
});
|
|
2663
|
+
}
|
|
2664
|
+
const hasRemoteResources = this.detectRemoteResources(context, path, root, opfDir);
|
|
2121
2665
|
if (hasRemoteResources && !manifestItem?.properties?.includes("remote-resources")) {
|
|
2122
2666
|
pushMessage(context.messages, {
|
|
2123
2667
|
id: MessageId.OPF_014,
|
|
@@ -2134,24 +2678,49 @@ var ContentValidator = class {
|
|
|
2134
2678
|
}
|
|
2135
2679
|
}
|
|
2136
2680
|
this.checkDiscouragedElements(context, path, root);
|
|
2681
|
+
this.checkSSMLPh(context, path, root, content);
|
|
2682
|
+
this.checkObsoleteHTML(context, path, root);
|
|
2683
|
+
this.checkDuplicateIDs(context, path, root);
|
|
2684
|
+
this.checkImgSrcEmpty(context, path, root);
|
|
2685
|
+
this.checkStyleInBody(context, path, root);
|
|
2686
|
+
this.validateInlineStyles(context, path, root);
|
|
2687
|
+
this.checkHttpEquivCharset(context, path, root);
|
|
2688
|
+
this.checkLangMismatch(context, path, root);
|
|
2689
|
+
this.checkDpubAriaDeprecated(context, path, root);
|
|
2690
|
+
this.checkTableBorder(context, path, root);
|
|
2691
|
+
this.checkTimeElement(context, path, root);
|
|
2692
|
+
this.checkMathMLAnnotations(context, path, root);
|
|
2693
|
+
this.checkReservedNamespace(context, path, content);
|
|
2694
|
+
this.checkDataAttributes(context, path, root);
|
|
2137
2695
|
this.checkAccessibility(context, path, root);
|
|
2138
2696
|
this.validateImages(context, path, root);
|
|
2139
2697
|
if (context.version.startsWith("3")) {
|
|
2140
2698
|
this.validateEpubTypes(context, path, root);
|
|
2141
2699
|
}
|
|
2700
|
+
this.validateEpubSwitch(context, path, root);
|
|
2701
|
+
this.validateEpubTrigger(context, path, root);
|
|
2702
|
+
this.validateStyleAttributes(context, path, root);
|
|
2142
2703
|
this.validateStylesheetLinks(context, path, root);
|
|
2143
2704
|
this.validateViewportMeta(context, path, root, manifestItem);
|
|
2144
2705
|
if (registry) {
|
|
2145
2706
|
this.extractAndRegisterIDs(path, root, registry);
|
|
2146
2707
|
}
|
|
2147
2708
|
if (refValidator && opfDir !== void 0) {
|
|
2148
|
-
this.extractAndRegisterHyperlinks(context, path, root, opfDir, refValidator);
|
|
2709
|
+
this.extractAndRegisterHyperlinks(context, path, root, opfDir, refValidator, !!isNavItem);
|
|
2149
2710
|
this.extractAndRegisterStylesheets(path, root, opfDir, refValidator);
|
|
2150
|
-
this.extractAndRegisterImages(path, root, opfDir, refValidator);
|
|
2711
|
+
this.extractAndRegisterImages(context, path, root, opfDir, refValidator, registry);
|
|
2151
2712
|
this.extractAndRegisterMathMLAltimg(path, root, opfDir, refValidator);
|
|
2152
2713
|
this.extractAndRegisterScripts(path, root, opfDir, refValidator);
|
|
2153
2714
|
this.extractAndRegisterCiteAttributes(path, root, opfDir, refValidator);
|
|
2154
|
-
this.extractAndRegisterMediaElements(path, root, opfDir, refValidator);
|
|
2715
|
+
this.extractAndRegisterMediaElements(context, path, root, opfDir, refValidator, registry);
|
|
2716
|
+
this.extractAndRegisterEmbeddedElements(
|
|
2717
|
+
context,
|
|
2718
|
+
path,
|
|
2719
|
+
root,
|
|
2720
|
+
opfDir,
|
|
2721
|
+
refValidator,
|
|
2722
|
+
registry
|
|
2723
|
+
);
|
|
2155
2724
|
}
|
|
2156
2725
|
} finally {
|
|
2157
2726
|
doc.dispose();
|
|
@@ -2207,14 +2776,12 @@ var ContentValidator = class {
|
|
|
2207
2776
|
return epubTypeAttr ? epubTypeAttr.value.trim().split(/\s+/) : [];
|
|
2208
2777
|
};
|
|
2209
2778
|
let tocNav;
|
|
2210
|
-
let tocEpubTypeValue = "";
|
|
2211
2779
|
let pageListCount = 0;
|
|
2212
2780
|
let landmarksCount = 0;
|
|
2213
2781
|
for (const nav of navElements) {
|
|
2214
2782
|
const types = getNavTypes(nav);
|
|
2215
2783
|
if (types.includes("toc") && !tocNav) {
|
|
2216
2784
|
tocNav = nav;
|
|
2217
|
-
tocEpubTypeValue = types.join(" ");
|
|
2218
2785
|
}
|
|
2219
2786
|
if (types.includes("page-list")) pageListCount++;
|
|
2220
2787
|
if (types.includes("landmarks")) landmarksCount++;
|
|
@@ -2265,7 +2832,7 @@ var ContentValidator = class {
|
|
|
2265
2832
|
}
|
|
2266
2833
|
this.checkNavHeadingContent(context, path, root);
|
|
2267
2834
|
this.checkNavHiddenAttribute(context, path, root);
|
|
2268
|
-
this.checkNavRemoteLinks(context, path, root
|
|
2835
|
+
this.checkNavRemoteLinks(context, path, root);
|
|
2269
2836
|
this.collectTocLinks(context, path, tocNav);
|
|
2270
2837
|
}
|
|
2271
2838
|
checkNavFirstChildHeading(context, path, navElem) {
|
|
@@ -2448,27 +3015,33 @@ var ContentValidator = class {
|
|
|
2448
3015
|
}
|
|
2449
3016
|
}
|
|
2450
3017
|
}
|
|
2451
|
-
checkNavRemoteLinks(context, path, root
|
|
2452
|
-
const
|
|
2453
|
-
const
|
|
2454
|
-
const
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
const
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
3018
|
+
checkNavRemoteLinks(context, path, root) {
|
|
3019
|
+
const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
|
|
3020
|
+
const navElements = root.find(".//html:nav", HTML_NS);
|
|
3021
|
+
for (const nav of navElements) {
|
|
3022
|
+
const navElem = nav;
|
|
3023
|
+
const epubTypeAttr = "attrs" in navElem ? navElem.attrs.find(
|
|
3024
|
+
(attr) => attr.name === "type" && attr.prefix === "epub" && attr.namespaceUri === "http://www.idpf.org/2007/ops"
|
|
3025
|
+
) : void 0;
|
|
3026
|
+
const types = epubTypeAttr ? epubTypeAttr.value.trim().split(/\s+/) : [];
|
|
3027
|
+
const isToc = types.includes("toc");
|
|
3028
|
+
const isLandmarks = types.includes("landmarks");
|
|
3029
|
+
const isPageList = types.includes("page-list");
|
|
3030
|
+
if (!isToc && !isLandmarks && !isPageList) continue;
|
|
3031
|
+
const navType = isToc ? "toc" : isLandmarks ? "landmarks" : "page-list";
|
|
3032
|
+
const links = navElem.find(".//html:a[@href]", HTML_NS);
|
|
3033
|
+
for (const link of links) {
|
|
3034
|
+
const href = this.getAttribute(link, "href");
|
|
3035
|
+
if (href && (href.startsWith("http://") || href.startsWith("https://"))) {
|
|
3036
|
+
pushMessage(context.messages, {
|
|
3037
|
+
id: MessageId.NAV_010,
|
|
3038
|
+
message: `"${navType}" nav must not link to remote resources; found link to "${href}"`,
|
|
3039
|
+
location: { path }
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
2472
3045
|
collectTocLinks(context, path, tocNav) {
|
|
2473
3046
|
const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
|
|
2474
3047
|
const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
|
|
@@ -2574,7 +3147,7 @@ var ContentValidator = class {
|
|
|
2574
3147
|
* - Remote scripts do NOT require the property (scripted property is used instead)
|
|
2575
3148
|
* - Remote stylesheets DO require the property
|
|
2576
3149
|
*/
|
|
2577
|
-
detectRemoteResources(_context,
|
|
3150
|
+
detectRemoteResources(_context, path, root, opfDir) {
|
|
2578
3151
|
const images = root.find(".//html:img[@src]", { html: "http://www.w3.org/1999/xhtml" });
|
|
2579
3152
|
for (const img of images) {
|
|
2580
3153
|
const src = this.getAttribute(img, "src");
|
|
@@ -2603,9 +3176,24 @@ var ContentValidator = class {
|
|
|
2603
3176
|
return true;
|
|
2604
3177
|
}
|
|
2605
3178
|
}
|
|
3179
|
+
const objects = root.find(".//html:object[@data]", { html: "http://www.w3.org/1999/xhtml" });
|
|
3180
|
+
for (const obj of objects) {
|
|
3181
|
+
const data = this.getAttribute(obj, "data");
|
|
3182
|
+
if (data && (data.startsWith("http://") || data.startsWith("https://"))) {
|
|
3183
|
+
return true;
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
const embeds = root.find(".//html:embed[@src]", { html: "http://www.w3.org/1999/xhtml" });
|
|
3187
|
+
for (const embed of embeds) {
|
|
3188
|
+
const src = this.getAttribute(embed, "src");
|
|
3189
|
+
if (src && (src.startsWith("http://") || src.startsWith("https://"))) {
|
|
3190
|
+
return true;
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
2606
3193
|
const linkElements = root.find(".//html:link[@rel and @href]", {
|
|
2607
3194
|
html: "http://www.w3.org/1999/xhtml"
|
|
2608
3195
|
});
|
|
3196
|
+
const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
|
|
2609
3197
|
for (const linkElem of linkElements) {
|
|
2610
3198
|
const rel = this.getAttribute(linkElem, "rel");
|
|
2611
3199
|
const href = this.getAttribute(linkElem, "href");
|
|
@@ -2613,6 +3201,10 @@ var ContentValidator = class {
|
|
|
2613
3201
|
if (href.startsWith("http://") || href.startsWith("https://")) {
|
|
2614
3202
|
return true;
|
|
2615
3203
|
}
|
|
3204
|
+
const resolvedCss = this.resolveRelativePath(docDir, href, opfDir ?? "");
|
|
3205
|
+
if (this.cssWithRemoteResources.has(resolvedCss)) {
|
|
3206
|
+
return true;
|
|
3207
|
+
}
|
|
2616
3208
|
}
|
|
2617
3209
|
}
|
|
2618
3210
|
const styleElements = root.find(".//html:style", { html: "http://www.w3.org/1999/xhtml" });
|
|
@@ -2640,6 +3232,654 @@ var ContentValidator = class {
|
|
|
2640
3232
|
}
|
|
2641
3233
|
}
|
|
2642
3234
|
}
|
|
3235
|
+
checkSSMLPh(context, path, root, content) {
|
|
3236
|
+
const ssmlPhPattern = /\bssml:ph\s*=\s*"([^"]*)"/g;
|
|
3237
|
+
let match;
|
|
3238
|
+
while ((match = ssmlPhPattern.exec(content)) !== null) {
|
|
3239
|
+
if (match[1]?.trim() === "") {
|
|
3240
|
+
const line = content.substring(0, match.index).split("\n").length;
|
|
3241
|
+
pushMessage(context.messages, {
|
|
3242
|
+
id: MessageId.HTM_007,
|
|
3243
|
+
message: "The ssml:ph attribute value should not be empty",
|
|
3244
|
+
location: { path, line }
|
|
3245
|
+
});
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
checkObsoleteHTML(context, path, root) {
|
|
3250
|
+
const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
|
|
3251
|
+
const obsoleteGlobalAttrs = ["contextmenu", "dropzone"];
|
|
3252
|
+
for (const attr of obsoleteGlobalAttrs) {
|
|
3253
|
+
try {
|
|
3254
|
+
const elements = root.find(`.//*[@${attr}]`);
|
|
3255
|
+
for (const el of elements) {
|
|
3256
|
+
pushMessage(context.messages, {
|
|
3257
|
+
id: MessageId.RSC_005,
|
|
3258
|
+
message: `The "${attr}" attribute is obsolete`,
|
|
3259
|
+
location: { path, line: el.line }
|
|
3260
|
+
});
|
|
3261
|
+
}
|
|
3262
|
+
} catch {
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
const obsoleteElementAttrs = [
|
|
3266
|
+
["typemustmatch", ".//html:object[@typemustmatch]"],
|
|
3267
|
+
["pubdate", ".//html:time[@pubdate]"],
|
|
3268
|
+
["seamless", ".//html:iframe[@seamless]"]
|
|
3269
|
+
];
|
|
3270
|
+
for (const [attr, xpath] of obsoleteElementAttrs) {
|
|
3271
|
+
try {
|
|
3272
|
+
const elements = root.find(xpath, HTML_NS);
|
|
3273
|
+
for (const el of elements) {
|
|
3274
|
+
pushMessage(context.messages, {
|
|
3275
|
+
id: MessageId.RSC_005,
|
|
3276
|
+
message: `The "${attr}" attribute is obsolete`,
|
|
3277
|
+
location: { path, line: el.line }
|
|
3278
|
+
});
|
|
3279
|
+
}
|
|
3280
|
+
} catch {
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
try {
|
|
3284
|
+
const keygens = root.find(".//html:keygen", HTML_NS);
|
|
3285
|
+
for (const keygen of keygens) {
|
|
3286
|
+
pushMessage(context.messages, {
|
|
3287
|
+
id: MessageId.RSC_005,
|
|
3288
|
+
message: 'The "keygen" element is obsolete',
|
|
3289
|
+
location: { path, line: keygen.line }
|
|
3290
|
+
});
|
|
3291
|
+
}
|
|
3292
|
+
} catch {
|
|
3293
|
+
}
|
|
3294
|
+
try {
|
|
3295
|
+
const menuTypes = root.find(".//html:menu[@type]", HTML_NS);
|
|
3296
|
+
for (const menuType of menuTypes) {
|
|
3297
|
+
pushMessage(context.messages, {
|
|
3298
|
+
id: MessageId.RSC_005,
|
|
3299
|
+
message: 'The "type" attribute on the "menu" element is obsolete',
|
|
3300
|
+
location: { path, line: menuType.line }
|
|
3301
|
+
});
|
|
3302
|
+
}
|
|
3303
|
+
} catch {
|
|
3304
|
+
}
|
|
3305
|
+
try {
|
|
3306
|
+
const commands = root.find(".//html:command", HTML_NS);
|
|
3307
|
+
for (const command of commands) {
|
|
3308
|
+
pushMessage(context.messages, {
|
|
3309
|
+
id: MessageId.RSC_005,
|
|
3310
|
+
message: 'The "command" element is obsolete',
|
|
3311
|
+
location: { path, line: command.line }
|
|
3312
|
+
});
|
|
3313
|
+
}
|
|
3314
|
+
} catch {
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
checkDuplicateIDs(context, path, root) {
|
|
3318
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3319
|
+
const elements = root.find(".//*[@id]");
|
|
3320
|
+
for (const elem of elements) {
|
|
3321
|
+
const id = this.getAttribute(elem, "id");
|
|
3322
|
+
if (id) {
|
|
3323
|
+
if (seen.has(id)) {
|
|
3324
|
+
pushMessage(context.messages, {
|
|
3325
|
+
id: MessageId.RSC_005,
|
|
3326
|
+
message: `Duplicate ID "${id}"`,
|
|
3327
|
+
location: { path, line: elem.line }
|
|
3328
|
+
});
|
|
3329
|
+
} else {
|
|
3330
|
+
seen.set(id, elem.line);
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
checkImgSrcEmpty(context, path, root) {
|
|
3336
|
+
const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
|
|
3337
|
+
try {
|
|
3338
|
+
const imgs = root.find(".//html:img[@src]", HTML_NS);
|
|
3339
|
+
for (const img of imgs) {
|
|
3340
|
+
const src = this.getAttribute(img, "src");
|
|
3341
|
+
if (src !== null && src.trim() === "") {
|
|
3342
|
+
pushMessage(context.messages, {
|
|
3343
|
+
id: MessageId.RSC_005,
|
|
3344
|
+
message: 'The "src" attribute must not be empty',
|
|
3345
|
+
location: { path, line: img.line }
|
|
3346
|
+
});
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
} catch {
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
checkStyleInBody(context, path, root) {
|
|
3353
|
+
const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
|
|
3354
|
+
try {
|
|
3355
|
+
const bodyStyles = root.find(".//html:body//html:style", HTML_NS);
|
|
3356
|
+
for (const style of bodyStyles) {
|
|
3357
|
+
pushMessage(context.messages, {
|
|
3358
|
+
id: MessageId.RSC_005,
|
|
3359
|
+
message: 'The "style" element must not appear in the document body',
|
|
3360
|
+
location: { path, line: style.line }
|
|
3361
|
+
});
|
|
3362
|
+
}
|
|
3363
|
+
} catch {
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
checkHttpEquivCharset(context, path, root) {
|
|
3367
|
+
const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
|
|
3368
|
+
try {
|
|
3369
|
+
const metas = root.find(".//html:head/html:meta", HTML_NS);
|
|
3370
|
+
let hasCharsetMeta = false;
|
|
3371
|
+
let hasHttpEquivContentType = false;
|
|
3372
|
+
for (const meta of metas) {
|
|
3373
|
+
const el = meta;
|
|
3374
|
+
const charset = this.getAttribute(el, "charset");
|
|
3375
|
+
if (charset !== null) {
|
|
3376
|
+
hasCharsetMeta = true;
|
|
3377
|
+
}
|
|
3378
|
+
const httpEquiv = this.getAttribute(el, "http-equiv");
|
|
3379
|
+
if (httpEquiv?.toLowerCase() === "content-type") {
|
|
3380
|
+
hasHttpEquivContentType = true;
|
|
3381
|
+
const contentAttr = (this.getAttribute(el, "content") ?? "").trim();
|
|
3382
|
+
if (!/^text\/html;\s*charset=utf-8$/i.test(contentAttr)) {
|
|
3383
|
+
pushMessage(context.messages, {
|
|
3384
|
+
id: MessageId.RSC_005,
|
|
3385
|
+
message: `The meta element in encoding declaration state must have the value "text/html; charset=utf-8"`,
|
|
3386
|
+
location: { path, line: el.line }
|
|
3387
|
+
});
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
if (hasCharsetMeta && hasHttpEquivContentType) {
|
|
3392
|
+
pushMessage(context.messages, {
|
|
3393
|
+
id: MessageId.RSC_005,
|
|
3394
|
+
message: "The document must not contain both a meta charset declaration and a meta http-equiv Content-Type declaration",
|
|
3395
|
+
location: { path }
|
|
3396
|
+
});
|
|
3397
|
+
}
|
|
3398
|
+
} catch {
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
checkSVGInvalidIDs(context, path, root) {
|
|
3402
|
+
const XML_NAME_START_RE = /^[a-zA-Z_:\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF]/;
|
|
3403
|
+
const elements = root.find(".//*[@id]");
|
|
3404
|
+
for (const elem of elements) {
|
|
3405
|
+
const id = this.getAttribute(elem, "id");
|
|
3406
|
+
if (id && !XML_NAME_START_RE.test(id)) {
|
|
3407
|
+
pushMessage(context.messages, {
|
|
3408
|
+
id: MessageId.RSC_005,
|
|
3409
|
+
message: `Invalid ID value "${id}"`,
|
|
3410
|
+
location: { path, line: elem.line }
|
|
3411
|
+
});
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
const rootId = this.getAttribute(root, "id");
|
|
3415
|
+
if (rootId && !XML_NAME_START_RE.test(rootId)) {
|
|
3416
|
+
pushMessage(context.messages, {
|
|
3417
|
+
id: MessageId.RSC_005,
|
|
3418
|
+
message: `Invalid ID value "${rootId}"`,
|
|
3419
|
+
location: { path, line: root.line }
|
|
3420
|
+
});
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
validateInlineStyles(context, path, root) {
|
|
3424
|
+
const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
|
|
3425
|
+
try {
|
|
3426
|
+
const styles = root.find(".//html:style", HTML_NS);
|
|
3427
|
+
for (const style of styles) {
|
|
3428
|
+
const cssContent = style.content;
|
|
3429
|
+
if (cssContent) {
|
|
3430
|
+
const cssValidator = new CSSValidator();
|
|
3431
|
+
cssValidator.validate(context, cssContent, path);
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
} catch {
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
checkLangMismatch(context, path, root) {
|
|
3438
|
+
const lang = root.attr("lang")?.value ?? null;
|
|
3439
|
+
const xmlLang = root.attr("lang", "xml")?.value ?? null;
|
|
3440
|
+
if (lang !== null && xmlLang !== null && lang.toLowerCase() !== xmlLang.toLowerCase()) {
|
|
3441
|
+
pushMessage(context.messages, {
|
|
3442
|
+
id: MessageId.RSC_005,
|
|
3443
|
+
message: "The lang and xml:lang attributes must have the same value",
|
|
3444
|
+
location: { path, line: root.line }
|
|
3445
|
+
});
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
checkDpubAriaDeprecated(context, path, root) {
|
|
3449
|
+
const DEPRECATED_ROLES = ["doc-endnote", "doc-biblioentry"];
|
|
3450
|
+
try {
|
|
3451
|
+
const elements = root.find(".//*[@role]");
|
|
3452
|
+
for (const elem of elements) {
|
|
3453
|
+
const roleAttr = this.getAttribute(elem, "role");
|
|
3454
|
+
if (!roleAttr) continue;
|
|
3455
|
+
const roles = roleAttr.split(/\s+/);
|
|
3456
|
+
for (const role of DEPRECATED_ROLES) {
|
|
3457
|
+
if (roles.includes(role)) {
|
|
3458
|
+
pushMessage(context.messages, {
|
|
3459
|
+
id: MessageId.RSC_017,
|
|
3460
|
+
message: `The "${role}" role is deprecated and should not be used`,
|
|
3461
|
+
location: { path, line: elem.line }
|
|
3462
|
+
});
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
} catch {
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3469
|
+
validateEpubSwitch(context, path, root) {
|
|
3470
|
+
const EPUB_NS = { epub: "http://www.idpf.org/2007/ops" };
|
|
3471
|
+
try {
|
|
3472
|
+
const switches = root.find(".//epub:switch", EPUB_NS);
|
|
3473
|
+
for (const sw of switches) {
|
|
3474
|
+
pushMessage(context.messages, {
|
|
3475
|
+
id: MessageId.RSC_017,
|
|
3476
|
+
message: 'The "epub:switch" element is deprecated',
|
|
3477
|
+
location: { path, line: sw.line }
|
|
3478
|
+
});
|
|
3479
|
+
const swElem = sw;
|
|
3480
|
+
const cases = [];
|
|
3481
|
+
const defaults = [];
|
|
3482
|
+
let defaultBeforeCase = false;
|
|
3483
|
+
try {
|
|
3484
|
+
const childCases = swElem.find("./epub:case", EPUB_NS);
|
|
3485
|
+
const childDefaults = swElem.find("./epub:default", EPUB_NS);
|
|
3486
|
+
cases.push(...childCases);
|
|
3487
|
+
defaults.push(...childDefaults);
|
|
3488
|
+
const firstDefault = childDefaults[0];
|
|
3489
|
+
const lastCase = childCases[childCases.length - 1];
|
|
3490
|
+
if (firstDefault && lastCase && firstDefault.line < lastCase.line) {
|
|
3491
|
+
defaultBeforeCase = true;
|
|
3492
|
+
}
|
|
3493
|
+
} catch {
|
|
3494
|
+
}
|
|
3495
|
+
if (cases.length === 0) {
|
|
3496
|
+
pushMessage(context.messages, {
|
|
3497
|
+
id: MessageId.RSC_005,
|
|
3498
|
+
message: 'The "epub:switch" element must contain at least one "epub:case" child element',
|
|
3499
|
+
location: { path, line: sw.line }
|
|
3500
|
+
});
|
|
3501
|
+
}
|
|
3502
|
+
if (defaults.length === 0) {
|
|
3503
|
+
pushMessage(context.messages, {
|
|
3504
|
+
id: MessageId.RSC_005,
|
|
3505
|
+
message: 'The "epub:switch" element must contain an "epub:default" child element',
|
|
3506
|
+
location: { path, line: sw.line }
|
|
3507
|
+
});
|
|
3508
|
+
}
|
|
3509
|
+
const secondDefault = defaults[1];
|
|
3510
|
+
if (secondDefault) {
|
|
3511
|
+
pushMessage(context.messages, {
|
|
3512
|
+
id: MessageId.RSC_005,
|
|
3513
|
+
message: 'The "epub:switch" element must not contain more than one "epub:default" child element',
|
|
3514
|
+
location: { path, line: secondDefault.line }
|
|
3515
|
+
});
|
|
3516
|
+
}
|
|
3517
|
+
const firstDefaultElem = defaults[0];
|
|
3518
|
+
if (defaultBeforeCase && firstDefaultElem) {
|
|
3519
|
+
pushMessage(context.messages, {
|
|
3520
|
+
id: MessageId.RSC_005,
|
|
3521
|
+
message: 'The "epub:default" element must appear after all "epub:case" elements',
|
|
3522
|
+
location: { path, line: firstDefaultElem.line }
|
|
3523
|
+
});
|
|
3524
|
+
}
|
|
3525
|
+
for (const c of cases) {
|
|
3526
|
+
const caseElem = c;
|
|
3527
|
+
const reqNs = caseElem.attr("required-namespace");
|
|
3528
|
+
if (!reqNs) {
|
|
3529
|
+
pushMessage(context.messages, {
|
|
3530
|
+
id: MessageId.RSC_005,
|
|
3531
|
+
message: 'The "epub:case" element must have a "required-namespace" attribute',
|
|
3532
|
+
location: { path, line: c.line }
|
|
3533
|
+
});
|
|
3534
|
+
}
|
|
3535
|
+
}
|
|
3536
|
+
try {
|
|
3537
|
+
const MATH_NS = { m: "http://www.w3.org/1998/Math/MathML" };
|
|
3538
|
+
const nestedMath = swElem.find(".//m:math//m:math", MATH_NS);
|
|
3539
|
+
for (const nested of nestedMath) {
|
|
3540
|
+
pushMessage(context.messages, {
|
|
3541
|
+
id: MessageId.RSC_005,
|
|
3542
|
+
message: 'The "math" element must not be nested inside another "math" element',
|
|
3543
|
+
location: { path, line: nested.line }
|
|
3544
|
+
});
|
|
3545
|
+
}
|
|
3546
|
+
} catch {
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
} catch {
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
validateEpubTrigger(context, path, root) {
|
|
3553
|
+
const EPUB_NS = { epub: "http://www.idpf.org/2007/ops" };
|
|
3554
|
+
try {
|
|
3555
|
+
const triggers = root.find(".//epub:trigger", EPUB_NS);
|
|
3556
|
+
if (triggers.length === 0) return;
|
|
3557
|
+
const allIds = /* @__PURE__ */ new Set();
|
|
3558
|
+
try {
|
|
3559
|
+
const idElements = root.find(".//*[@id]");
|
|
3560
|
+
for (const el of idElements) {
|
|
3561
|
+
const idAttr = this.getAttribute(el, "id");
|
|
3562
|
+
if (idAttr) allIds.add(idAttr);
|
|
3563
|
+
}
|
|
3564
|
+
} catch {
|
|
3565
|
+
}
|
|
3566
|
+
for (const trigger of triggers) {
|
|
3567
|
+
pushMessage(context.messages, {
|
|
3568
|
+
id: MessageId.RSC_017,
|
|
3569
|
+
message: 'The "epub:trigger" element is deprecated',
|
|
3570
|
+
location: { path, line: trigger.line }
|
|
3571
|
+
});
|
|
3572
|
+
const triggerElem = trigger;
|
|
3573
|
+
const ref = triggerElem.attr("ref");
|
|
3574
|
+
if (ref?.value && !allIds.has(ref.value)) {
|
|
3575
|
+
pushMessage(context.messages, {
|
|
3576
|
+
id: MessageId.RSC_005,
|
|
3577
|
+
message: `The "ref" attribute value "${ref.value}" does not reference a valid ID in the document`,
|
|
3578
|
+
location: { path, line: trigger.line }
|
|
3579
|
+
});
|
|
3580
|
+
}
|
|
3581
|
+
const observer = triggerElem.attr("observer", "ev") ?? triggerElem.attr("ev:observer");
|
|
3582
|
+
if (observer?.value && !allIds.has(observer.value)) {
|
|
3583
|
+
pushMessage(context.messages, {
|
|
3584
|
+
id: MessageId.RSC_005,
|
|
3585
|
+
message: `The "ev:observer" attribute value "${observer.value}" does not reference a valid ID in the document`,
|
|
3586
|
+
location: { path, line: trigger.line }
|
|
3587
|
+
});
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
} catch {
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
validateStyleAttributes(context, path, root) {
|
|
3594
|
+
try {
|
|
3595
|
+
const elements = root.find(".//*[@style]");
|
|
3596
|
+
for (const elem of elements) {
|
|
3597
|
+
const style = this.getAttribute(elem, "style");
|
|
3598
|
+
if (!style) continue;
|
|
3599
|
+
const wrappedCss = `* { ${style} }`;
|
|
3600
|
+
const cssValidator = new CSSValidator();
|
|
3601
|
+
cssValidator.validate(context, wrappedCss, path);
|
|
3602
|
+
}
|
|
3603
|
+
} catch {
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
validateSvgEpubType(context, path, root) {
|
|
3607
|
+
const ALLOWED_ELEMENTS = /* @__PURE__ */ new Set([
|
|
3608
|
+
"svg",
|
|
3609
|
+
"a",
|
|
3610
|
+
"audio",
|
|
3611
|
+
"canvas",
|
|
3612
|
+
"circle",
|
|
3613
|
+
"ellipse",
|
|
3614
|
+
"g",
|
|
3615
|
+
"iframe",
|
|
3616
|
+
"image",
|
|
3617
|
+
"line",
|
|
3618
|
+
"path",
|
|
3619
|
+
"polygon",
|
|
3620
|
+
"polyline",
|
|
3621
|
+
"rect",
|
|
3622
|
+
"switch",
|
|
3623
|
+
"symbol",
|
|
3624
|
+
"text",
|
|
3625
|
+
"textPath",
|
|
3626
|
+
"tspan",
|
|
3627
|
+
"unknown",
|
|
3628
|
+
"use",
|
|
3629
|
+
"video"
|
|
3630
|
+
]);
|
|
3631
|
+
const EPUB_NS = { epub: "http://www.idpf.org/2007/ops" };
|
|
3632
|
+
try {
|
|
3633
|
+
const elements = root.find(".//*[@epub:type]", EPUB_NS);
|
|
3634
|
+
for (const elem of elements) {
|
|
3635
|
+
const elemTyped = elem;
|
|
3636
|
+
const localName = elemTyped.name;
|
|
3637
|
+
if (!ALLOWED_ELEMENTS.has(localName)) {
|
|
3638
|
+
pushMessage(context.messages, {
|
|
3639
|
+
id: MessageId.RSC_005,
|
|
3640
|
+
message: `Attribute "epub:type" not allowed on SVG element "${localName}"`,
|
|
3641
|
+
location: { path, line: elem.line }
|
|
3642
|
+
});
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3645
|
+
const rootEpubType = root.attr("type", "epub");
|
|
3646
|
+
if (rootEpubType && !ALLOWED_ELEMENTS.has(root.name)) {
|
|
3647
|
+
pushMessage(context.messages, {
|
|
3648
|
+
id: MessageId.RSC_005,
|
|
3649
|
+
message: `Attribute "epub:type" not allowed on SVG element "${root.name}"`,
|
|
3650
|
+
location: { path, line: root.line }
|
|
3651
|
+
});
|
|
3652
|
+
}
|
|
3653
|
+
} catch {
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
checkUnknownEpubAttributes(context, path, root) {
|
|
3657
|
+
const KNOWN_EPUB_ATTRS = /* @__PURE__ */ new Set(["type"]);
|
|
3658
|
+
const checkElement = (elem) => {
|
|
3659
|
+
if (!("attrs" in elem)) return;
|
|
3660
|
+
for (const attr of elem.attrs) {
|
|
3661
|
+
if (attr.prefix === "epub" && !KNOWN_EPUB_ATTRS.has(attr.name)) {
|
|
3662
|
+
pushMessage(context.messages, {
|
|
3663
|
+
id: MessageId.RSC_005,
|
|
3664
|
+
message: `Attribute "epub:${attr.name}" not allowed`,
|
|
3665
|
+
location: { path, line: elem.line }
|
|
3666
|
+
});
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
};
|
|
3670
|
+
checkElement(root);
|
|
3671
|
+
try {
|
|
3672
|
+
const allElements = root.find(".//*");
|
|
3673
|
+
for (const elem of allElements) {
|
|
3674
|
+
checkElement(elem);
|
|
3675
|
+
}
|
|
3676
|
+
} catch {
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
checkTableBorder(context, path, root) {
|
|
3680
|
+
const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
|
|
3681
|
+
try {
|
|
3682
|
+
const tables = root.find(".//html:table[@border]", HTML_NS);
|
|
3683
|
+
for (const table of tables) {
|
|
3684
|
+
const border = this.getAttribute(table, "border");
|
|
3685
|
+
if (border !== null && border !== "" && border !== "1") {
|
|
3686
|
+
pushMessage(context.messages, {
|
|
3687
|
+
id: MessageId.RSC_005,
|
|
3688
|
+
message: `The value of the "border" attribute on the "table" element must be either "1" or the empty string`,
|
|
3689
|
+
location: { path, line: table.line }
|
|
3690
|
+
});
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
3693
|
+
} catch {
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3696
|
+
checkTimeElement(context, path, root) {
|
|
3697
|
+
const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
|
|
3698
|
+
try {
|
|
3699
|
+
const nestedTimes = root.find(".//html:time//html:time", HTML_NS);
|
|
3700
|
+
for (const nested of nestedTimes) {
|
|
3701
|
+
pushMessage(context.messages, {
|
|
3702
|
+
id: MessageId.RSC_005,
|
|
3703
|
+
message: 'The element "time" must not appear as a descendant of the "time" element',
|
|
3704
|
+
location: { path, line: nested.line }
|
|
3705
|
+
});
|
|
3706
|
+
}
|
|
3707
|
+
} catch {
|
|
3708
|
+
}
|
|
3709
|
+
try {
|
|
3710
|
+
const times = root.find(".//html:time[@datetime]", HTML_NS);
|
|
3711
|
+
for (const time of times) {
|
|
3712
|
+
const datetime = this.getAttribute(time, "datetime");
|
|
3713
|
+
if (datetime !== null && !isValidDatetime(datetime)) {
|
|
3714
|
+
pushMessage(context.messages, {
|
|
3715
|
+
id: MessageId.RSC_005,
|
|
3716
|
+
message: `The "datetime" attribute value "${datetime}" is not a valid date, time, or duration`,
|
|
3717
|
+
location: { path, line: time.line }
|
|
3718
|
+
});
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
} catch {
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
checkMathMLAnnotations(context, path, root) {
|
|
3725
|
+
const MATH_NS = { math: "http://www.w3.org/1998/Math/MathML" };
|
|
3726
|
+
const CONTENT_MATHML_ENCODINGS = /* @__PURE__ */ new Set(["mathml-content", "application/mathml-content+xml"]);
|
|
3727
|
+
const CONTENT_MATHML_ELEMENTS = /* @__PURE__ */ new Set([
|
|
3728
|
+
"apply",
|
|
3729
|
+
"bind",
|
|
3730
|
+
"ci",
|
|
3731
|
+
"cn",
|
|
3732
|
+
"cs",
|
|
3733
|
+
"csymbol",
|
|
3734
|
+
"cbytes",
|
|
3735
|
+
"cerror",
|
|
3736
|
+
"share",
|
|
3737
|
+
"piecewise",
|
|
3738
|
+
"lambda",
|
|
3739
|
+
"set",
|
|
3740
|
+
"list",
|
|
3741
|
+
"vector",
|
|
3742
|
+
"matrix",
|
|
3743
|
+
"matrixrow",
|
|
3744
|
+
"interval"
|
|
3745
|
+
]);
|
|
3746
|
+
const contentMathMLNames = [...CONTENT_MATHML_ELEMENTS];
|
|
3747
|
+
try {
|
|
3748
|
+
const annotations = root.find(".//math:annotation-xml", MATH_NS);
|
|
3749
|
+
for (const anno of annotations) {
|
|
3750
|
+
const el = anno;
|
|
3751
|
+
const encoding = this.getAttribute(el, "encoding");
|
|
3752
|
+
const name = this.getAttribute(el, "name");
|
|
3753
|
+
if (encoding) {
|
|
3754
|
+
const encodingLower = encoding.toLowerCase();
|
|
3755
|
+
if (CONTENT_MATHML_ENCODINGS.has(encodingLower)) {
|
|
3756
|
+
if (!name) {
|
|
3757
|
+
pushMessage(context.messages, {
|
|
3758
|
+
id: MessageId.RSC_005,
|
|
3759
|
+
message: 'The "annotation-xml" element with Content MathML encoding must have a "name" attribute with value "contentequiv"',
|
|
3760
|
+
location: { path, line: el.line }
|
|
3761
|
+
});
|
|
3762
|
+
} else if (name !== "contentequiv") {
|
|
3763
|
+
pushMessage(context.messages, {
|
|
3764
|
+
id: MessageId.RSC_005,
|
|
3765
|
+
message: `The "name" attribute on "annotation-xml" with Content MathML encoding must be "contentequiv", but found "${name}"`,
|
|
3766
|
+
location: { path, line: el.line }
|
|
3767
|
+
});
|
|
3768
|
+
}
|
|
3769
|
+
} else {
|
|
3770
|
+
for (const cElemName of contentMathMLNames) {
|
|
3771
|
+
try {
|
|
3772
|
+
const found = el.get(`./math:${cElemName}`, MATH_NS);
|
|
3773
|
+
if (found) {
|
|
3774
|
+
pushMessage(context.messages, {
|
|
3775
|
+
id: MessageId.RSC_005,
|
|
3776
|
+
message: `Content MathML element "${cElemName}" found in annotation-xml with encoding "${encoding}"`,
|
|
3777
|
+
location: { path, line: found.line }
|
|
3778
|
+
});
|
|
3779
|
+
break;
|
|
3780
|
+
}
|
|
3781
|
+
} catch {
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
if (encodingLower === "application/xml+xhtml") {
|
|
3786
|
+
pushMessage(context.messages, {
|
|
3787
|
+
id: MessageId.RSC_005,
|
|
3788
|
+
message: 'The encoding "application/xml+xhtml" is not valid; use "application/xhtml+xml" instead',
|
|
3789
|
+
location: { path, line: el.line }
|
|
3790
|
+
});
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
} catch {
|
|
3795
|
+
}
|
|
3796
|
+
for (const elemName of contentMathMLNames) {
|
|
3797
|
+
try {
|
|
3798
|
+
const found = root.get(`.//math:math/math:${elemName}`, MATH_NS);
|
|
3799
|
+
if (found) {
|
|
3800
|
+
pushMessage(context.messages, {
|
|
3801
|
+
id: MessageId.RSC_005,
|
|
3802
|
+
message: `Content MathML element "${elemName}" must not appear as a direct child of "math"; use "semantics" with "annotation-xml" instead`,
|
|
3803
|
+
location: { path, line: found.line }
|
|
3804
|
+
});
|
|
3805
|
+
break;
|
|
3806
|
+
}
|
|
3807
|
+
} catch {
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
checkReservedNamespace(context, path, content) {
|
|
3812
|
+
const nsPattern = /xmlns:(\w+)="([^"]+)"/g;
|
|
3813
|
+
const STANDARD_PREFIXES = /* @__PURE__ */ new Set([
|
|
3814
|
+
"xml",
|
|
3815
|
+
"xmlns",
|
|
3816
|
+
"xlink",
|
|
3817
|
+
"epub",
|
|
3818
|
+
"ops",
|
|
3819
|
+
"dc",
|
|
3820
|
+
"dcterms",
|
|
3821
|
+
"svg",
|
|
3822
|
+
"math",
|
|
3823
|
+
"ssml",
|
|
3824
|
+
"ev",
|
|
3825
|
+
"xsi"
|
|
3826
|
+
]);
|
|
3827
|
+
const STANDARD_NAMESPACES = /* @__PURE__ */ new Set([
|
|
3828
|
+
"http://www.w3.org/XML/1998/namespace",
|
|
3829
|
+
"http://www.w3.org/2000/xmlns/",
|
|
3830
|
+
"http://www.w3.org/1999/xhtml",
|
|
3831
|
+
"http://www.w3.org/1999/xlink",
|
|
3832
|
+
"http://www.w3.org/2000/svg",
|
|
3833
|
+
"http://www.w3.org/1998/Math/MathML",
|
|
3834
|
+
"http://www.idpf.org/2007/ops",
|
|
3835
|
+
"http://purl.org/dc/elements/1.1/",
|
|
3836
|
+
"http://purl.org/dc/terms/",
|
|
3837
|
+
"http://www.w3.org/2001/10/synthesis",
|
|
3838
|
+
"http://www.w3.org/2001/xml-events",
|
|
3839
|
+
"http://www.w3.org/2001/XMLSchema-instance"
|
|
3840
|
+
]);
|
|
3841
|
+
let match;
|
|
3842
|
+
while ((match = nsPattern.exec(content)) !== null) {
|
|
3843
|
+
const prefix = match[1] ?? "";
|
|
3844
|
+
const uri = match[2] ?? "";
|
|
3845
|
+
if (STANDARD_PREFIXES.has(prefix) || STANDARD_NAMESPACES.has(uri)) continue;
|
|
3846
|
+
try {
|
|
3847
|
+
const url = new URL(uri);
|
|
3848
|
+
const host = url.hostname.toLowerCase();
|
|
3849
|
+
for (const reserved of ["w3.org", "idpf.org"]) {
|
|
3850
|
+
if (host.includes(reserved)) {
|
|
3851
|
+
const line = content.substring(0, match.index).split("\n").length;
|
|
3852
|
+
pushMessage(context.messages, {
|
|
3853
|
+
id: MessageId.HTM_054,
|
|
3854
|
+
message: `Custom attribute namespace ("${uri}") must not include the string "${reserved}" in its domain`,
|
|
3855
|
+
location: { path, line }
|
|
3856
|
+
});
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
} catch {
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
checkDataAttributes(context, path, root) {
|
|
3864
|
+
const elements = root.find(".//*");
|
|
3865
|
+
const XML_NCNAME_RE = /^[a-z_\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF][a-z0-9._\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF-]*$/;
|
|
3866
|
+
for (const elem of elements) {
|
|
3867
|
+
const el = elem;
|
|
3868
|
+
if (!("attrs" in el)) continue;
|
|
3869
|
+
const attrs = el.attrs;
|
|
3870
|
+
for (const attr of attrs) {
|
|
3871
|
+
if (!attr.name.startsWith("data-")) continue;
|
|
3872
|
+
const suffix = attr.name.substring(5);
|
|
3873
|
+
if (suffix.length === 0 || !XML_NCNAME_RE.test(suffix) || /[A-Z]/.test(attr.name)) {
|
|
3874
|
+
pushMessage(context.messages, {
|
|
3875
|
+
id: MessageId.HTM_061,
|
|
3876
|
+
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)`,
|
|
3877
|
+
location: { path, line: el.line }
|
|
3878
|
+
});
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
2643
3883
|
checkAccessibility(context, path, root) {
|
|
2644
3884
|
const links = root.find(".//html:a", { html: "http://www.w3.org/1999/xhtml" });
|
|
2645
3885
|
for (const link of links) {
|
|
@@ -2752,24 +3992,32 @@ var ContentValidator = class {
|
|
|
2752
3992
|
const epubTypeElements = root.find(".//*[@epub:type]", {
|
|
2753
3993
|
epub: "http://www.idpf.org/2007/ops"
|
|
2754
3994
|
});
|
|
2755
|
-
const knownPrefixes = /* @__PURE__ */ new Set([
|
|
2756
|
-
"",
|
|
2757
|
-
"http://idpf.org/epub/structure/v1/",
|
|
2758
|
-
"http://idpf.org/epub/vocab/structure/",
|
|
2759
|
-
"http://www.idpf.org/2007/ops"
|
|
2760
|
-
]);
|
|
2761
3995
|
for (const elem of epubTypeElements) {
|
|
2762
3996
|
const elemTyped = elem;
|
|
2763
|
-
const epubTypeAttr = elemTyped.attr("
|
|
3997
|
+
const epubTypeAttr = elemTyped.attr("type", "epub");
|
|
2764
3998
|
if (!epubTypeAttr?.value) continue;
|
|
2765
|
-
const
|
|
2766
|
-
|
|
2767
|
-
const
|
|
2768
|
-
|
|
3999
|
+
for (const part of epubTypeAttr.value.split(/\s+/)) {
|
|
4000
|
+
if (!part) continue;
|
|
4001
|
+
const hasPrefix = part.includes(":");
|
|
4002
|
+
const localName = hasPrefix ? part.substring(part.indexOf(":") + 1) : part;
|
|
4003
|
+
if (hasPrefix) continue;
|
|
4004
|
+
if (EPUB_SSV_DEPRECATED.has(localName)) {
|
|
4005
|
+
pushMessage(context.messages, {
|
|
4006
|
+
id: MessageId.OPF_086b,
|
|
4007
|
+
message: `epub:type value "${localName}" is deprecated`,
|
|
4008
|
+
location: { path, line: elem.line }
|
|
4009
|
+
});
|
|
4010
|
+
} else if (EPUB_SSV_DISALLOWED_ON_CONTENT.has(localName)) {
|
|
4011
|
+
pushMessage(context.messages, {
|
|
4012
|
+
id: MessageId.OPF_087,
|
|
4013
|
+
message: `epub:type value "${localName}" is not allowed on documents of type "application/xhtml+xml"`,
|
|
4014
|
+
location: { path, line: elem.line }
|
|
4015
|
+
});
|
|
4016
|
+
} else if (!EPUB_SSV_ALL.has(localName)) {
|
|
2769
4017
|
pushMessage(context.messages, {
|
|
2770
4018
|
id: MessageId.OPF_088,
|
|
2771
|
-
message: `
|
|
2772
|
-
location: { path }
|
|
4019
|
+
message: `Unrecognized epub:type value "${localName}"`,
|
|
4020
|
+
location: { path, line: elem.line }
|
|
2773
4021
|
});
|
|
2774
4022
|
}
|
|
2775
4023
|
}
|
|
@@ -2881,14 +4129,39 @@ var ContentValidator = class {
|
|
|
2881
4129
|
extractAndRegisterIDs(path, root, registry) {
|
|
2882
4130
|
const elementsWithId = root.find(".//*[@id]");
|
|
2883
4131
|
for (const elem of elementsWithId) {
|
|
2884
|
-
const
|
|
4132
|
+
const xmlElem = elem;
|
|
4133
|
+
const id = this.getAttribute(xmlElem, "id");
|
|
2885
4134
|
if (id) {
|
|
2886
4135
|
registry.registerID(path, id);
|
|
4136
|
+
const localName = xmlElem.name.includes(":") ? xmlElem.name.split(":").pop() : xmlElem.name;
|
|
4137
|
+
if (localName === "symbol") {
|
|
4138
|
+
registry.registerSVGSymbolID(path, id);
|
|
4139
|
+
}
|
|
2887
4140
|
}
|
|
2888
4141
|
}
|
|
2889
4142
|
}
|
|
2890
|
-
extractAndRegisterHyperlinks(context, path, root, opfDir, refValidator) {
|
|
4143
|
+
extractAndRegisterHyperlinks(context, path, root, opfDir, refValidator, isNavDocument = false) {
|
|
2891
4144
|
const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
|
|
4145
|
+
const navAnchorTypes = /* @__PURE__ */ new Map();
|
|
4146
|
+
if (isNavDocument) {
|
|
4147
|
+
const HTML_NS = { html: "http://www.w3.org/1999/xhtml" };
|
|
4148
|
+
const navElements = root.find(".//html:nav", HTML_NS);
|
|
4149
|
+
for (const nav of navElements) {
|
|
4150
|
+
const navElem = nav;
|
|
4151
|
+
const epubTypeAttr = "attrs" in navElem ? navElem.attrs.find(
|
|
4152
|
+
(attr) => attr.name === "type" && attr.prefix === "epub" && attr.namespaceUri === "http://www.idpf.org/2007/ops"
|
|
4153
|
+
) : void 0;
|
|
4154
|
+
const types = epubTypeAttr ? epubTypeAttr.value.trim().split(/\s+/) : [];
|
|
4155
|
+
let refType = "hyperlink" /* HYPERLINK */;
|
|
4156
|
+
if (types.includes("toc")) refType = "nav-toc-link" /* NAV_TOC_LINK */;
|
|
4157
|
+
else if (types.includes("page-list")) refType = "nav-pagelist-link" /* NAV_PAGELIST_LINK */;
|
|
4158
|
+
const navAnchors = navElem.find(".//html:a[@href]", HTML_NS);
|
|
4159
|
+
for (const a of navAnchors) {
|
|
4160
|
+
const anchorHref = this.getAttribute(a, "href") ?? "";
|
|
4161
|
+
navAnchorTypes.set(`${String(a.line)}:${anchorHref}`, refType);
|
|
4162
|
+
}
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
2892
4165
|
const links = root.find(".//html:a[@href]", { html: "http://www.w3.org/1999/xhtml" });
|
|
2893
4166
|
for (const link of links) {
|
|
2894
4167
|
const href = this.getAttribute(link, "href")?.trim() ?? null;
|
|
@@ -2902,10 +4175,8 @@ var ContentValidator = class {
|
|
|
2902
4175
|
continue;
|
|
2903
4176
|
}
|
|
2904
4177
|
const line = link.line;
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
}
|
|
2908
|
-
if (href.startsWith("mailto:") || href.startsWith("tel:")) {
|
|
4178
|
+
const refType = isNavDocument ? navAnchorTypes.get(`${String(line)}:${href}`) ?? "hyperlink" /* HYPERLINK */ : "hyperlink" /* HYPERLINK */;
|
|
4179
|
+
if (ABSOLUTE_URI_RE.test(href)) {
|
|
2909
4180
|
continue;
|
|
2910
4181
|
}
|
|
2911
4182
|
if (href.includes("#epubcfi(")) {
|
|
@@ -2918,7 +4189,7 @@ var ContentValidator = class {
|
|
|
2918
4189
|
url: href,
|
|
2919
4190
|
targetResource: targetResource2,
|
|
2920
4191
|
fragment,
|
|
2921
|
-
type:
|
|
4192
|
+
type: refType,
|
|
2922
4193
|
location: { path, line }
|
|
2923
4194
|
});
|
|
2924
4195
|
continue;
|
|
@@ -2930,7 +4201,7 @@ var ContentValidator = class {
|
|
|
2930
4201
|
const ref = {
|
|
2931
4202
|
url: href,
|
|
2932
4203
|
targetResource,
|
|
2933
|
-
type:
|
|
4204
|
+
type: refType,
|
|
2934
4205
|
location: { path, line }
|
|
2935
4206
|
};
|
|
2936
4207
|
if (fragmentPart) {
|
|
@@ -2938,6 +4209,38 @@ var ContentValidator = class {
|
|
|
2938
4209
|
}
|
|
2939
4210
|
refValidator.addReference(ref);
|
|
2940
4211
|
}
|
|
4212
|
+
const areaLinks = root.find(".//html:area[@href]", { html: "http://www.w3.org/1999/xhtml" });
|
|
4213
|
+
for (const area of areaLinks) {
|
|
4214
|
+
const href = this.getAttribute(area, "href")?.trim();
|
|
4215
|
+
if (!href) continue;
|
|
4216
|
+
const line = area.line;
|
|
4217
|
+
if (ABSOLUTE_URI_RE.test(href)) continue;
|
|
4218
|
+
if (href.includes("#epubcfi(")) continue;
|
|
4219
|
+
if (href.startsWith("#")) {
|
|
4220
|
+
refValidator.addReference({
|
|
4221
|
+
url: href,
|
|
4222
|
+
targetResource: path,
|
|
4223
|
+
fragment: href.slice(1),
|
|
4224
|
+
type: "hyperlink" /* HYPERLINK */,
|
|
4225
|
+
location: { path, line }
|
|
4226
|
+
});
|
|
4227
|
+
continue;
|
|
4228
|
+
}
|
|
4229
|
+
const resolvedAreaPath = this.resolveRelativePath(docDir, href, opfDir);
|
|
4230
|
+
const areaHashIndex = resolvedAreaPath.indexOf("#");
|
|
4231
|
+
const areaTarget = areaHashIndex >= 0 ? resolvedAreaPath.slice(0, areaHashIndex) : resolvedAreaPath;
|
|
4232
|
+
const areaFragment = areaHashIndex >= 0 ? resolvedAreaPath.slice(areaHashIndex + 1) : void 0;
|
|
4233
|
+
const areaRef = {
|
|
4234
|
+
url: href,
|
|
4235
|
+
targetResource: areaTarget,
|
|
4236
|
+
type: "hyperlink" /* HYPERLINK */,
|
|
4237
|
+
location: { path, line }
|
|
4238
|
+
};
|
|
4239
|
+
if (areaFragment) {
|
|
4240
|
+
areaRef.fragment = areaFragment;
|
|
4241
|
+
}
|
|
4242
|
+
refValidator.addReference(areaRef);
|
|
4243
|
+
}
|
|
2941
4244
|
const svgLinks = root.find(".//svg:a", {
|
|
2942
4245
|
svg: "http://www.w3.org/2000/svg",
|
|
2943
4246
|
xlink: "http://www.w3.org/1999/xlink"
|
|
@@ -2985,9 +4288,9 @@ var ContentValidator = class {
|
|
|
2985
4288
|
const href = this.getAttribute(linkElem, "href");
|
|
2986
4289
|
const rel = this.getAttribute(linkElem, "rel");
|
|
2987
4290
|
if (!href) continue;
|
|
4291
|
+
if (!rel?.toLowerCase().includes("stylesheet")) continue;
|
|
2988
4292
|
const line = linkElem.line;
|
|
2989
|
-
const
|
|
2990
|
-
const type = isStylesheet ? "stylesheet" /* STYLESHEET */ : "link" /* LINK */;
|
|
4293
|
+
const type = "stylesheet" /* STYLESHEET */;
|
|
2991
4294
|
if (href.startsWith("http://") || href.startsWith("https://")) {
|
|
2992
4295
|
refValidator.addReference({
|
|
2993
4296
|
url: href,
|
|
@@ -3014,10 +4317,10 @@ var ContentValidator = class {
|
|
|
3014
4317
|
extractCSSImports(cssPath, cssContent, opfDir, refValidator) {
|
|
3015
4318
|
const cssDir = cssPath.includes("/") ? cssPath.substring(0, cssPath.lastIndexOf("/")) : "";
|
|
3016
4319
|
const cleanedCSS = cssContent.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
3017
|
-
const importRegex = /@import\s+(?:url\s*\(\s*
|
|
4320
|
+
const importRegex = /@import\s+(?:url\s*\(\s*["']?([^"')]+?)["']?\s*\)|["']([^"']+)["'])[^;]*;/gi;
|
|
3018
4321
|
let match;
|
|
3019
4322
|
while ((match = importRegex.exec(cleanedCSS)) !== null) {
|
|
3020
|
-
const importUrl = match[1];
|
|
4323
|
+
const importUrl = match[1] ?? match[2];
|
|
3021
4324
|
if (!importUrl) continue;
|
|
3022
4325
|
const beforeMatch = cleanedCSS.substring(0, match.index);
|
|
3023
4326
|
const line = beforeMatch.split("\n").length;
|
|
@@ -3039,21 +4342,56 @@ var ContentValidator = class {
|
|
|
3039
4342
|
});
|
|
3040
4343
|
}
|
|
3041
4344
|
}
|
|
3042
|
-
extractAndRegisterImages(path, root, opfDir, refValidator) {
|
|
4345
|
+
extractAndRegisterImages(context, path, root, opfDir, refValidator, registry) {
|
|
3043
4346
|
const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
|
|
3044
|
-
const
|
|
4347
|
+
const ns = { html: "http://www.w3.org/1999/xhtml" };
|
|
4348
|
+
const pictureHasCMTSource = /* @__PURE__ */ new Set();
|
|
4349
|
+
if (registry) {
|
|
4350
|
+
const pictures = root.find(".//html:picture", ns);
|
|
4351
|
+
for (const pic of pictures) {
|
|
4352
|
+
const picElem = pic;
|
|
4353
|
+
const sources = picElem.find("html:source[@src]", ns);
|
|
4354
|
+
const sourcesWithSrcset = picElem.find("html:source[@srcset]", ns);
|
|
4355
|
+
for (const source of [...sources, ...sourcesWithSrcset]) {
|
|
4356
|
+
const srcAttr = this.getAttribute(source, "src");
|
|
4357
|
+
const srcsetAttr = this.getAttribute(source, "srcset");
|
|
4358
|
+
const sourceUrl = srcAttr ?? srcsetAttr?.split(",")[0]?.trim().split(/\s+/)[0];
|
|
4359
|
+
if (!sourceUrl || sourceUrl.startsWith("http://") || sourceUrl.startsWith("https://"))
|
|
4360
|
+
continue;
|
|
4361
|
+
const resolvedSource = this.resolveRelativePath(docDir, sourceUrl, opfDir);
|
|
4362
|
+
const resource = registry.getResource(resolvedSource);
|
|
4363
|
+
if (resource && isCoreMediaType(resource.mimeType)) {
|
|
4364
|
+
pictureHasCMTSource.add(pic.line);
|
|
4365
|
+
break;
|
|
4366
|
+
}
|
|
4367
|
+
}
|
|
4368
|
+
}
|
|
4369
|
+
}
|
|
4370
|
+
const images = root.find(".//html:img[@src]", ns);
|
|
3045
4371
|
for (const img of images) {
|
|
3046
4372
|
const imgElem = img;
|
|
3047
4373
|
const src = this.getAttribute(imgElem, "src");
|
|
3048
4374
|
if (!src) continue;
|
|
3049
4375
|
const line = img.line;
|
|
4376
|
+
let hasIntrinsicFallback;
|
|
4377
|
+
if (pictureHasCMTSource.size > 0) {
|
|
4378
|
+
try {
|
|
4379
|
+
const pictureParent = imgElem.get("ancestor::html:picture", ns);
|
|
4380
|
+
if (pictureParent && pictureHasCMTSource.has(pictureParent.line)) {
|
|
4381
|
+
hasIntrinsicFallback = true;
|
|
4382
|
+
}
|
|
4383
|
+
} catch {
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
3050
4386
|
if (src.startsWith("http://") || src.startsWith("https://")) {
|
|
3051
|
-
|
|
4387
|
+
const ref = {
|
|
3052
4388
|
url: src,
|
|
3053
4389
|
targetResource: src,
|
|
3054
4390
|
type: "image" /* IMAGE */,
|
|
3055
4391
|
location: { path, line }
|
|
3056
|
-
}
|
|
4392
|
+
};
|
|
4393
|
+
if (hasIntrinsicFallback) ref.hasIntrinsicFallback = true;
|
|
4394
|
+
refValidator.addReference(ref);
|
|
3057
4395
|
} else {
|
|
3058
4396
|
const resolvedPath = this.resolveRelativePath(docDir, src, opfDir);
|
|
3059
4397
|
const hashIndex = resolvedPath.indexOf("#");
|
|
@@ -3065,6 +4403,7 @@ var ContentValidator = class {
|
|
|
3065
4403
|
type: "image" /* IMAGE */,
|
|
3066
4404
|
location: { path, line }
|
|
3067
4405
|
};
|
|
4406
|
+
if (hasIntrinsicFallback) ref.hasIntrinsicFallback = true;
|
|
3068
4407
|
if (fragment) {
|
|
3069
4408
|
ref.fragment = fragment;
|
|
3070
4409
|
}
|
|
@@ -3117,6 +4456,7 @@ var ContentValidator = class {
|
|
|
3117
4456
|
}
|
|
3118
4457
|
refValidator.addReference(svgImgRef);
|
|
3119
4458
|
}
|
|
4459
|
+
this.extractSVGUseReferences(context, path, root, docDir, opfDir, refValidator);
|
|
3120
4460
|
const videos = root.find(".//html:video[@poster]", { html: "http://www.w3.org/1999/xhtml" });
|
|
3121
4461
|
for (const video of videos) {
|
|
3122
4462
|
const poster = this.getAttribute(video, "poster");
|
|
@@ -3213,110 +4553,88 @@ var ContentValidator = class {
|
|
|
3213
4553
|
}
|
|
3214
4554
|
if (cite.startsWith("#")) {
|
|
3215
4555
|
const targetResource2 = path;
|
|
3216
|
-
const fragment2 = cite.slice(1);
|
|
3217
|
-
refValidator.addReference({
|
|
3218
|
-
url: cite,
|
|
3219
|
-
targetResource: targetResource2,
|
|
3220
|
-
fragment: fragment2,
|
|
3221
|
-
type: "hyperlink" /* HYPERLINK */,
|
|
3222
|
-
location: { path, line }
|
|
3223
|
-
});
|
|
3224
|
-
continue;
|
|
3225
|
-
}
|
|
3226
|
-
const resolvedPath = this.resolveRelativePath(docDir, cite, opfDir);
|
|
3227
|
-
const hashIndex = resolvedPath.indexOf("#");
|
|
3228
|
-
const targetResource = hashIndex >= 0 ? resolvedPath.slice(0, hashIndex) : resolvedPath;
|
|
3229
|
-
const fragment = hashIndex >= 0 ? resolvedPath.slice(hashIndex + 1) : void 0;
|
|
3230
|
-
const ref = {
|
|
3231
|
-
url: cite,
|
|
3232
|
-
targetResource,
|
|
3233
|
-
type: "hyperlink" /* HYPERLINK */,
|
|
3234
|
-
location: { path, line }
|
|
3235
|
-
};
|
|
3236
|
-
if (fragment) {
|
|
3237
|
-
ref.fragment = fragment;
|
|
3238
|
-
}
|
|
3239
|
-
refValidator.addReference(ref);
|
|
3240
|
-
}
|
|
3241
|
-
}
|
|
3242
|
-
extractAndRegisterMediaElements(path, root, opfDir, refValidator) {
|
|
3243
|
-
const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
|
|
3244
|
-
const audioElements = root.find(".//html:audio[@src]", {
|
|
3245
|
-
html: "http://www.w3.org/1999/xhtml"
|
|
3246
|
-
});
|
|
3247
|
-
for (const audio of audioElements) {
|
|
3248
|
-
const src = this.getAttribute(audio, "src");
|
|
3249
|
-
if (!src) continue;
|
|
3250
|
-
const line = audio.line;
|
|
3251
|
-
if (src.startsWith("http://") || src.startsWith("https://")) {
|
|
3252
|
-
refValidator.addReference({
|
|
3253
|
-
url: src,
|
|
3254
|
-
targetResource: src,
|
|
3255
|
-
type: "audio" /* AUDIO */,
|
|
3256
|
-
location: line !== void 0 ? { path, line } : { path }
|
|
3257
|
-
});
|
|
3258
|
-
} else {
|
|
3259
|
-
const resolvedPath = this.resolveRelativePath(docDir, src, opfDir);
|
|
3260
|
-
refValidator.addReference({
|
|
3261
|
-
url: src,
|
|
3262
|
-
targetResource: resolvedPath,
|
|
3263
|
-
type: "audio" /* AUDIO */,
|
|
3264
|
-
location: line !== void 0 ? { path, line } : { path }
|
|
3265
|
-
});
|
|
3266
|
-
}
|
|
3267
|
-
}
|
|
3268
|
-
const videoElements = root.find(".//html:video[@src]", {
|
|
3269
|
-
html: "http://www.w3.org/1999/xhtml"
|
|
3270
|
-
});
|
|
3271
|
-
for (const video of videoElements) {
|
|
3272
|
-
const src = this.getAttribute(video, "src");
|
|
3273
|
-
if (!src) continue;
|
|
3274
|
-
const line = video.line;
|
|
3275
|
-
if (src.startsWith("http://") || src.startsWith("https://")) {
|
|
3276
|
-
refValidator.addReference({
|
|
3277
|
-
url: src,
|
|
3278
|
-
targetResource: src,
|
|
3279
|
-
type: "video" /* VIDEO */,
|
|
3280
|
-
location: line !== void 0 ? { path, line } : { path }
|
|
3281
|
-
});
|
|
3282
|
-
} else {
|
|
3283
|
-
const resolvedPath = this.resolveRelativePath(docDir, src, opfDir);
|
|
4556
|
+
const fragment2 = cite.slice(1);
|
|
3284
4557
|
refValidator.addReference({
|
|
3285
|
-
url:
|
|
3286
|
-
targetResource:
|
|
3287
|
-
|
|
3288
|
-
|
|
4558
|
+
url: cite,
|
|
4559
|
+
targetResource: targetResource2,
|
|
4560
|
+
fragment: fragment2,
|
|
4561
|
+
type: "cite" /* CITE */,
|
|
4562
|
+
location: { path, line }
|
|
3289
4563
|
});
|
|
4564
|
+
continue;
|
|
4565
|
+
}
|
|
4566
|
+
const resolvedPath = this.resolveRelativePath(docDir, cite, opfDir);
|
|
4567
|
+
const hashIndex = resolvedPath.indexOf("#");
|
|
4568
|
+
const targetResource = hashIndex >= 0 ? resolvedPath.slice(0, hashIndex) : resolvedPath;
|
|
4569
|
+
const fragment = hashIndex >= 0 ? resolvedPath.slice(hashIndex + 1) : void 0;
|
|
4570
|
+
const ref = {
|
|
4571
|
+
url: cite,
|
|
4572
|
+
targetResource,
|
|
4573
|
+
type: "cite" /* CITE */,
|
|
4574
|
+
location: { path, line }
|
|
4575
|
+
};
|
|
4576
|
+
if (fragment) {
|
|
4577
|
+
ref.fragment = fragment;
|
|
3290
4578
|
}
|
|
4579
|
+
refValidator.addReference(ref);
|
|
3291
4580
|
}
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
const
|
|
3299
|
-
const
|
|
3300
|
-
const
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
4581
|
+
}
|
|
4582
|
+
extractAndRegisterMediaElements(context, path, root, opfDir, refValidator, registry) {
|
|
4583
|
+
const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
|
|
4584
|
+
const ns = { html: "http://www.w3.org/1999/xhtml" };
|
|
4585
|
+
for (const tagName of ["audio", "video"]) {
|
|
4586
|
+
const isAudio = tagName === "audio";
|
|
4587
|
+
const refType = isAudio ? "audio" /* AUDIO */ : "video" /* VIDEO */;
|
|
4588
|
+
const elements = root.find(`.//html:${tagName}`, ns);
|
|
4589
|
+
for (const elem of elements) {
|
|
4590
|
+
const mediaElem = elem;
|
|
4591
|
+
const pendingRefs = [];
|
|
4592
|
+
const src = this.getAttribute(mediaElem, "src");
|
|
4593
|
+
if (src) {
|
|
4594
|
+
const line = elem.line;
|
|
4595
|
+
if (src.startsWith("http://") || src.startsWith("https://")) {
|
|
4596
|
+
pendingRefs.push({ url: src, targetResource: src, type: refType, line });
|
|
4597
|
+
} else {
|
|
4598
|
+
const resolvedPath = this.resolveRelativePath(docDir, src, opfDir);
|
|
4599
|
+
pendingRefs.push({ url: src, targetResource: resolvedPath, type: refType, line });
|
|
4600
|
+
}
|
|
4601
|
+
}
|
|
4602
|
+
const sources = mediaElem.find("html:source[@src]", ns);
|
|
4603
|
+
for (const source of sources) {
|
|
4604
|
+
const sourceElem = source;
|
|
4605
|
+
const sourceSrc = this.getAttribute(sourceElem, "src");
|
|
4606
|
+
if (!sourceSrc) continue;
|
|
4607
|
+
const line = source.line;
|
|
4608
|
+
if (sourceSrc.startsWith("http://") || sourceSrc.startsWith("https://")) {
|
|
4609
|
+
pendingRefs.push({ url: sourceSrc, targetResource: sourceSrc, type: refType, line });
|
|
4610
|
+
} else {
|
|
4611
|
+
const resolvedPath = this.resolveRelativePath(docDir, sourceSrc, opfDir);
|
|
4612
|
+
pendingRefs.push({ url: sourceSrc, targetResource: resolvedPath, type: refType, line });
|
|
4613
|
+
}
|
|
4614
|
+
if (registry) {
|
|
4615
|
+
this.checkMimeTypeMatch(context, path, docDir, opfDir, sourceElem, "src", registry);
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
let hasIntrinsicFallback = false;
|
|
4619
|
+
if (registry && pendingRefs.length > 1) {
|
|
4620
|
+
hasIntrinsicFallback = pendingRefs.some((ref) => {
|
|
4621
|
+
const resource = registry.getResource(ref.targetResource);
|
|
4622
|
+
return resource && isCoreMediaType(resource.mimeType);
|
|
4623
|
+
});
|
|
4624
|
+
}
|
|
4625
|
+
for (const ref of pendingRefs) {
|
|
4626
|
+
const reference = {
|
|
4627
|
+
url: ref.url,
|
|
4628
|
+
targetResource: ref.targetResource,
|
|
4629
|
+
type: ref.type,
|
|
4630
|
+
location: ref.line !== void 0 ? { path, line: ref.line } : { path }
|
|
4631
|
+
};
|
|
4632
|
+
if (hasIntrinsicFallback) reference.hasIntrinsicFallback = true;
|
|
4633
|
+
refValidator.addReference(reference);
|
|
4634
|
+
}
|
|
3318
4635
|
}
|
|
3319
4636
|
}
|
|
4637
|
+
this.extractAndRegisterPictureElements(context, path, root, opfDir, refValidator, registry);
|
|
3320
4638
|
const iframeElements = root.find(".//html:iframe[@src]", {
|
|
3321
4639
|
html: "http://www.w3.org/1999/xhtml"
|
|
3322
4640
|
});
|
|
@@ -3366,6 +4684,180 @@ var ContentValidator = class {
|
|
|
3366
4684
|
}
|
|
3367
4685
|
}
|
|
3368
4686
|
}
|
|
4687
|
+
extractAndRegisterEmbeddedElements(context, path, root, opfDir, refValidator, registry) {
|
|
4688
|
+
const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
|
|
4689
|
+
const ns = { html: "http://www.w3.org/1999/xhtml" };
|
|
4690
|
+
const addRef = (src, type, line, hasIntrinsicFallback) => {
|
|
4691
|
+
const location = line !== void 0 ? { path, line } : { path };
|
|
4692
|
+
if (src.startsWith("http://") || src.startsWith("https://")) {
|
|
4693
|
+
const ref = {
|
|
4694
|
+
url: src,
|
|
4695
|
+
targetResource: src,
|
|
4696
|
+
type,
|
|
4697
|
+
location
|
|
4698
|
+
};
|
|
4699
|
+
if (hasIntrinsicFallback) ref.hasIntrinsicFallback = true;
|
|
4700
|
+
refValidator.addReference(ref);
|
|
4701
|
+
} else {
|
|
4702
|
+
const resolvedPath = this.resolveRelativePath(docDir, src, opfDir);
|
|
4703
|
+
const hashIndex = resolvedPath.indexOf("#");
|
|
4704
|
+
const targetResource = hashIndex >= 0 ? resolvedPath.slice(0, hashIndex) : resolvedPath;
|
|
4705
|
+
const ref = {
|
|
4706
|
+
url: src,
|
|
4707
|
+
targetResource,
|
|
4708
|
+
type,
|
|
4709
|
+
location
|
|
4710
|
+
};
|
|
4711
|
+
if (hashIndex >= 0) ref.fragment = resolvedPath.slice(hashIndex + 1);
|
|
4712
|
+
if (hasIntrinsicFallback) ref.hasIntrinsicFallback = true;
|
|
4713
|
+
refValidator.addReference(ref);
|
|
4714
|
+
}
|
|
4715
|
+
};
|
|
4716
|
+
for (const elem of root.find(".//html:embed[@src]", ns)) {
|
|
4717
|
+
const embedElem = elem;
|
|
4718
|
+
const src = this.getAttribute(embedElem, "src");
|
|
4719
|
+
if (src) addRef(src, "generic" /* GENERIC */, elem.line);
|
|
4720
|
+
if (registry) {
|
|
4721
|
+
this.checkMimeTypeMatch(context, path, docDir, opfDir, embedElem, "src", registry);
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4724
|
+
for (const elem of root.find(".//html:input[@src]", ns)) {
|
|
4725
|
+
const type = this.getAttribute(elem, "type");
|
|
4726
|
+
if (type?.toLowerCase() === "image") {
|
|
4727
|
+
const src = this.getAttribute(elem, "src");
|
|
4728
|
+
if (src) addRef(src, "image" /* IMAGE */, elem.line);
|
|
4729
|
+
}
|
|
4730
|
+
}
|
|
4731
|
+
for (const elem of root.find(".//html:object[@data]", ns)) {
|
|
4732
|
+
const objElem = elem;
|
|
4733
|
+
const data = this.getAttribute(objElem, "data");
|
|
4734
|
+
if (!data) continue;
|
|
4735
|
+
const allChildren = objElem.find("html:*", ns);
|
|
4736
|
+
const hasFallbackContent = allChildren.some((child) => {
|
|
4737
|
+
const c = child;
|
|
4738
|
+
return c.name !== "param" && this.getAttribute(c, "hidden") === null;
|
|
4739
|
+
});
|
|
4740
|
+
addRef(data, "generic" /* GENERIC */, elem.line, hasFallbackContent || void 0);
|
|
4741
|
+
if (registry) {
|
|
4742
|
+
this.checkMimeTypeMatch(context, path, docDir, opfDir, objElem, "data", registry);
|
|
4743
|
+
}
|
|
4744
|
+
}
|
|
4745
|
+
}
|
|
4746
|
+
/**
|
|
4747
|
+
* Check if an element's type attribute matches the manifest MIME type (OPF-013)
|
|
4748
|
+
*/
|
|
4749
|
+
checkMimeTypeMatch(context, path, docDir, opfDir, element, srcAttr, registry) {
|
|
4750
|
+
const typeAttr = this.getAttribute(element, "type");
|
|
4751
|
+
if (!typeAttr) return;
|
|
4752
|
+
const src = this.getAttribute(element, srcAttr);
|
|
4753
|
+
if (!src || src.startsWith("http://") || src.startsWith("https://")) return;
|
|
4754
|
+
const resolvedPath = this.resolveRelativePath(docDir, src, opfDir);
|
|
4755
|
+
const hashIndex = resolvedPath.indexOf("#");
|
|
4756
|
+
const targetResource = hashIndex >= 0 ? resolvedPath.slice(0, hashIndex) : resolvedPath;
|
|
4757
|
+
const resource = registry.getResource(targetResource);
|
|
4758
|
+
if (!resource) return;
|
|
4759
|
+
const declaredType = stripMimeParams(typeAttr);
|
|
4760
|
+
const manifestType = stripMimeParams(resource.mimeType);
|
|
4761
|
+
if (declaredType && declaredType !== manifestType) {
|
|
4762
|
+
pushMessage(context.messages, {
|
|
4763
|
+
id: MessageId.OPF_013,
|
|
4764
|
+
message: `Resource "${targetResource}" is declared with MIME type "${declaredType}" in content, but has MIME type "${manifestType}" in the package document`,
|
|
4765
|
+
location: { path, line: element.line }
|
|
4766
|
+
});
|
|
4767
|
+
}
|
|
4768
|
+
}
|
|
4769
|
+
/**
|
|
4770
|
+
* Extract and validate picture elements (MED-003, MED-007, OPF-013)
|
|
4771
|
+
*/
|
|
4772
|
+
extractAndRegisterPictureElements(context, path, root, opfDir, refValidator, registry) {
|
|
4773
|
+
const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
|
|
4774
|
+
const ns = { html: "http://www.w3.org/1999/xhtml" };
|
|
4775
|
+
const BLESSED_IMAGE_TYPES = /* @__PURE__ */ new Set([
|
|
4776
|
+
"image/gif",
|
|
4777
|
+
"image/jpeg",
|
|
4778
|
+
"image/png",
|
|
4779
|
+
"image/svg+xml",
|
|
4780
|
+
"image/webp"
|
|
4781
|
+
]);
|
|
4782
|
+
const pictures = root.find(".//html:picture", ns);
|
|
4783
|
+
for (const pic of pictures) {
|
|
4784
|
+
const picElem = pic;
|
|
4785
|
+
const imgs = picElem.find("html:img[@src]", ns);
|
|
4786
|
+
for (const img of imgs) {
|
|
4787
|
+
const imgElem = img;
|
|
4788
|
+
const src = this.getAttribute(imgElem, "src");
|
|
4789
|
+
if (!src || src.startsWith("http://") || src.startsWith("https://")) continue;
|
|
4790
|
+
if (registry) {
|
|
4791
|
+
const resolvedPath = this.resolveRelativePath(docDir, src, opfDir);
|
|
4792
|
+
const resource = registry.getResource(resolvedPath);
|
|
4793
|
+
if (resource && !BLESSED_IMAGE_TYPES.has(resource.mimeType)) {
|
|
4794
|
+
pushMessage(context.messages, {
|
|
4795
|
+
id: MessageId.MED_003,
|
|
4796
|
+
message: `Image in "picture" element must be a core image type, but found "${resource.mimeType}"`,
|
|
4797
|
+
location: { path, line: img.line }
|
|
4798
|
+
});
|
|
4799
|
+
}
|
|
4800
|
+
}
|
|
4801
|
+
const srcset = this.getAttribute(imgElem, "srcset");
|
|
4802
|
+
if (srcset && registry) {
|
|
4803
|
+
const entries = srcset.split(",");
|
|
4804
|
+
for (const entry of entries) {
|
|
4805
|
+
const url = entry.trim().split(/\s+/)[0];
|
|
4806
|
+
if (!url || url.startsWith("http://") || url.startsWith("https://")) continue;
|
|
4807
|
+
const resolvedPath = this.resolveRelativePath(docDir, url, opfDir);
|
|
4808
|
+
const resource = registry.getResource(resolvedPath);
|
|
4809
|
+
if (resource && !BLESSED_IMAGE_TYPES.has(resource.mimeType)) {
|
|
4810
|
+
pushMessage(context.messages, {
|
|
4811
|
+
id: MessageId.MED_003,
|
|
4812
|
+
message: `Image in "picture" element must be a core image type, but found "${resource.mimeType}"`,
|
|
4813
|
+
location: { path, line: img.line }
|
|
4814
|
+
});
|
|
4815
|
+
}
|
|
4816
|
+
}
|
|
4817
|
+
}
|
|
4818
|
+
}
|
|
4819
|
+
const sourcesWithSrc = picElem.find("html:source[@src]", ns);
|
|
4820
|
+
const sourcesWithSrcset = picElem.find("html:source[@srcset]", ns);
|
|
4821
|
+
const allSources = /* @__PURE__ */ new Set([...sourcesWithSrc, ...sourcesWithSrcset]);
|
|
4822
|
+
for (const source of allSources) {
|
|
4823
|
+
const sourceElem = source;
|
|
4824
|
+
const typeAttr = this.getAttribute(sourceElem, "type");
|
|
4825
|
+
const src = this.getAttribute(sourceElem, "src");
|
|
4826
|
+
const srcset = this.getAttribute(sourceElem, "srcset");
|
|
4827
|
+
const sourceUrl = src ?? srcset?.split(",")[0]?.trim().split(/\s+/)[0];
|
|
4828
|
+
if (!sourceUrl || sourceUrl.startsWith("http://") || sourceUrl.startsWith("https://"))
|
|
4829
|
+
continue;
|
|
4830
|
+
if (registry) {
|
|
4831
|
+
if (src) {
|
|
4832
|
+
this.checkMimeTypeMatch(context, path, docDir, opfDir, sourceElem, "src", registry);
|
|
4833
|
+
} else if (srcset && typeAttr) {
|
|
4834
|
+
const resolvedPath2 = this.resolveRelativePath(docDir, sourceUrl, opfDir);
|
|
4835
|
+
const resource2 = registry.getResource(resolvedPath2);
|
|
4836
|
+
if (resource2) {
|
|
4837
|
+
const declaredType = stripMimeParams(typeAttr);
|
|
4838
|
+
const manifestType = stripMimeParams(resource2.mimeType);
|
|
4839
|
+
if (declaredType && declaredType !== manifestType) {
|
|
4840
|
+
pushMessage(context.messages, {
|
|
4841
|
+
id: MessageId.OPF_013,
|
|
4842
|
+
message: `Resource "${resolvedPath2}" is declared with MIME type "${declaredType}" in content, but has MIME type "${manifestType}" in the package document`,
|
|
4843
|
+
location: { path, line: source.line }
|
|
4844
|
+
});
|
|
4845
|
+
}
|
|
4846
|
+
}
|
|
4847
|
+
}
|
|
4848
|
+
const resolvedPath = this.resolveRelativePath(docDir, sourceUrl, opfDir);
|
|
4849
|
+
const resource = registry.getResource(resolvedPath);
|
|
4850
|
+
if (resource && !BLESSED_IMAGE_TYPES.has(resource.mimeType) && !typeAttr) {
|
|
4851
|
+
pushMessage(context.messages, {
|
|
4852
|
+
id: MessageId.MED_007,
|
|
4853
|
+
message: `Source element in "picture" with foreign resource type "${resource.mimeType}" must declare a "type" attribute`,
|
|
4854
|
+
location: { path, line: source.line }
|
|
4855
|
+
});
|
|
4856
|
+
}
|
|
4857
|
+
}
|
|
4858
|
+
}
|
|
4859
|
+
}
|
|
4860
|
+
}
|
|
3369
4861
|
parseSrcset(srcset, docDir, opfDir, path, line, refValidator) {
|
|
3370
4862
|
const entries = srcset.split(",");
|
|
3371
4863
|
for (const entry of entries) {
|
|
@@ -4489,6 +5981,9 @@ function parseSpine(spineXml, spineAttrs) {
|
|
|
4489
5981
|
idref,
|
|
4490
5982
|
linear: attrs.linear !== "no"
|
|
4491
5983
|
};
|
|
5984
|
+
if (attrs.id) {
|
|
5985
|
+
itemref.id = attrs.id.trim();
|
|
5986
|
+
}
|
|
4492
5987
|
if (attrs.properties) {
|
|
4493
5988
|
itemref.properties = attrs.properties.split(/\s+/);
|
|
4494
5989
|
}
|
|
@@ -4527,9 +6022,14 @@ function parseAttributes(attrsStr) {
|
|
|
4527
6022
|
const name = match[1];
|
|
4528
6023
|
const value = match[2];
|
|
4529
6024
|
if (name !== void 0 && value !== void 0) {
|
|
6025
|
+
attrs[name] = value;
|
|
4530
6026
|
const colonIdx = name.indexOf(":");
|
|
4531
|
-
|
|
4532
|
-
|
|
6027
|
+
if (colonIdx >= 0) {
|
|
6028
|
+
const localName = name.slice(colonIdx + 1);
|
|
6029
|
+
if (!(localName in attrs)) {
|
|
6030
|
+
attrs[localName] = value;
|
|
6031
|
+
}
|
|
6032
|
+
}
|
|
4533
6033
|
}
|
|
4534
6034
|
}
|
|
4535
6035
|
return attrs;
|
|
@@ -4574,70 +6074,324 @@ function parseCollections(xml) {
|
|
|
4574
6074
|
return collections;
|
|
4575
6075
|
}
|
|
4576
6076
|
|
|
4577
|
-
// src/opf/
|
|
4578
|
-
var
|
|
4579
|
-
|
|
4580
|
-
"
|
|
4581
|
-
"
|
|
4582
|
-
"
|
|
4583
|
-
"
|
|
4584
|
-
"image/webp",
|
|
4585
|
-
// Audio types
|
|
4586
|
-
"audio/mpeg",
|
|
4587
|
-
"audio/mp4",
|
|
4588
|
-
"audio/ogg",
|
|
4589
|
-
// CSS
|
|
4590
|
-
"text/css",
|
|
4591
|
-
// Fonts
|
|
4592
|
-
"font/otf",
|
|
4593
|
-
"font/ttf",
|
|
4594
|
-
"font/woff",
|
|
4595
|
-
"font/woff2",
|
|
4596
|
-
"application/font-sfnt",
|
|
4597
|
-
// deprecated alias for font/otf, font/ttf
|
|
4598
|
-
"application/font-woff",
|
|
4599
|
-
// deprecated alias for font/woff
|
|
4600
|
-
"application/vnd.ms-opentype",
|
|
4601
|
-
// deprecated alias
|
|
4602
|
-
// Content documents
|
|
4603
|
-
"application/xhtml+xml",
|
|
4604
|
-
"application/x-dtbncx+xml",
|
|
4605
|
-
// NCX
|
|
4606
|
-
// JavaScript (EPUB 3)
|
|
4607
|
-
"text/javascript",
|
|
4608
|
-
"application/javascript",
|
|
4609
|
-
// Media overlays
|
|
4610
|
-
"application/smil+xml",
|
|
4611
|
-
// PLS (Pronunciation Lexicon)
|
|
4612
|
-
"application/pls+xml"
|
|
6077
|
+
// src/opf/validator.ts
|
|
6078
|
+
var VALID_VERSIONS = /* @__PURE__ */ new Set(["2.0", "3.0", "3.1", "3.2", "3.3"]);
|
|
6079
|
+
var DEPRECATED_MEDIA_TYPES = /* @__PURE__ */ new Set([
|
|
6080
|
+
"text/x-oeb1-document",
|
|
6081
|
+
"text/x-oeb1-css",
|
|
6082
|
+
"application/x-oeb1-package",
|
|
6083
|
+
"text/x-oeb1-html"
|
|
4613
6084
|
]);
|
|
4614
|
-
var
|
|
4615
|
-
"
|
|
4616
|
-
"
|
|
4617
|
-
"
|
|
4618
|
-
"
|
|
4619
|
-
"
|
|
4620
|
-
"
|
|
4621
|
-
"
|
|
6085
|
+
var VALID_RELATOR_CODES = /* @__PURE__ */ new Set([
|
|
6086
|
+
"abr",
|
|
6087
|
+
"acp",
|
|
6088
|
+
"act",
|
|
6089
|
+
"adi",
|
|
6090
|
+
"adp",
|
|
6091
|
+
"aft",
|
|
6092
|
+
"anl",
|
|
6093
|
+
"anm",
|
|
6094
|
+
"ann",
|
|
6095
|
+
"ant",
|
|
6096
|
+
"ape",
|
|
6097
|
+
"apl",
|
|
6098
|
+
"app",
|
|
6099
|
+
"aqt",
|
|
6100
|
+
"arc",
|
|
6101
|
+
"ard",
|
|
6102
|
+
"arr",
|
|
6103
|
+
"art",
|
|
6104
|
+
"asg",
|
|
6105
|
+
"asn",
|
|
6106
|
+
"ato",
|
|
6107
|
+
"att",
|
|
6108
|
+
"auc",
|
|
6109
|
+
"aud",
|
|
6110
|
+
"aui",
|
|
6111
|
+
"aus",
|
|
6112
|
+
"aut",
|
|
6113
|
+
"bdd",
|
|
6114
|
+
"bjd",
|
|
6115
|
+
"bkd",
|
|
6116
|
+
"bkp",
|
|
6117
|
+
"blw",
|
|
6118
|
+
"bnd",
|
|
6119
|
+
"bpd",
|
|
6120
|
+
"brd",
|
|
6121
|
+
"brl",
|
|
6122
|
+
"bsl",
|
|
6123
|
+
"cas",
|
|
6124
|
+
"ccp",
|
|
6125
|
+
"chr",
|
|
6126
|
+
"clb",
|
|
6127
|
+
"cli",
|
|
6128
|
+
"cll",
|
|
6129
|
+
"clr",
|
|
6130
|
+
"clt",
|
|
6131
|
+
"cmm",
|
|
6132
|
+
"cmp",
|
|
6133
|
+
"cmt",
|
|
6134
|
+
"cnd",
|
|
6135
|
+
"cng",
|
|
6136
|
+
"cns",
|
|
6137
|
+
"coe",
|
|
6138
|
+
"col",
|
|
6139
|
+
"com",
|
|
6140
|
+
"con",
|
|
6141
|
+
"cor",
|
|
6142
|
+
"cos",
|
|
6143
|
+
"cot",
|
|
6144
|
+
"cou",
|
|
6145
|
+
"cov",
|
|
6146
|
+
"cpc",
|
|
6147
|
+
"cpe",
|
|
6148
|
+
"cph",
|
|
6149
|
+
"cpl",
|
|
6150
|
+
"cpt",
|
|
6151
|
+
"cre",
|
|
6152
|
+
"crp",
|
|
6153
|
+
"crr",
|
|
6154
|
+
"crt",
|
|
6155
|
+
"csl",
|
|
6156
|
+
"csp",
|
|
6157
|
+
"cst",
|
|
6158
|
+
"ctb",
|
|
6159
|
+
"cte",
|
|
6160
|
+
"ctg",
|
|
6161
|
+
"ctr",
|
|
6162
|
+
"cts",
|
|
6163
|
+
"ctt",
|
|
6164
|
+
"cur",
|
|
6165
|
+
"cwt",
|
|
6166
|
+
"dbp",
|
|
6167
|
+
"dfd",
|
|
6168
|
+
"dfe",
|
|
6169
|
+
"dft",
|
|
6170
|
+
"dgc",
|
|
6171
|
+
"dgg",
|
|
6172
|
+
"dgs",
|
|
6173
|
+
"dis",
|
|
6174
|
+
"dln",
|
|
6175
|
+
"dnc",
|
|
6176
|
+
"dnr",
|
|
6177
|
+
"dpc",
|
|
6178
|
+
"dpt",
|
|
6179
|
+
"drm",
|
|
6180
|
+
"drt",
|
|
6181
|
+
"dsr",
|
|
6182
|
+
"dst",
|
|
6183
|
+
"dtc",
|
|
6184
|
+
"dte",
|
|
6185
|
+
"dtm",
|
|
6186
|
+
"dto",
|
|
6187
|
+
"dub",
|
|
6188
|
+
"edc",
|
|
6189
|
+
"edm",
|
|
6190
|
+
"edt",
|
|
6191
|
+
"egr",
|
|
6192
|
+
"elg",
|
|
6193
|
+
"elt",
|
|
6194
|
+
"eng",
|
|
6195
|
+
"enj",
|
|
6196
|
+
"etr",
|
|
6197
|
+
"evp",
|
|
6198
|
+
"exp",
|
|
6199
|
+
"fac",
|
|
6200
|
+
"fds",
|
|
6201
|
+
"fld",
|
|
6202
|
+
"flm",
|
|
6203
|
+
"fmd",
|
|
6204
|
+
"fmk",
|
|
6205
|
+
"fmo",
|
|
6206
|
+
"fmp",
|
|
6207
|
+
"fnd",
|
|
6208
|
+
"fpy",
|
|
6209
|
+
"frg",
|
|
6210
|
+
"gis",
|
|
6211
|
+
"grt",
|
|
6212
|
+
"his",
|
|
6213
|
+
"hnr",
|
|
6214
|
+
"hst",
|
|
6215
|
+
"ill",
|
|
6216
|
+
"ilu",
|
|
6217
|
+
"ins",
|
|
6218
|
+
"inv",
|
|
6219
|
+
"isb",
|
|
6220
|
+
"itr",
|
|
6221
|
+
"ive",
|
|
6222
|
+
"ivr",
|
|
6223
|
+
"jud",
|
|
6224
|
+
"jug",
|
|
6225
|
+
"lbr",
|
|
6226
|
+
"lbt",
|
|
6227
|
+
"ldr",
|
|
6228
|
+
"led",
|
|
6229
|
+
"lee",
|
|
6230
|
+
"lel",
|
|
6231
|
+
"len",
|
|
6232
|
+
"let",
|
|
6233
|
+
"lgd",
|
|
6234
|
+
"lie",
|
|
6235
|
+
"lil",
|
|
6236
|
+
"lit",
|
|
6237
|
+
"lsa",
|
|
6238
|
+
"lse",
|
|
6239
|
+
"lso",
|
|
6240
|
+
"ltg",
|
|
6241
|
+
"lyr",
|
|
6242
|
+
"mcp",
|
|
6243
|
+
"mdc",
|
|
6244
|
+
"med",
|
|
6245
|
+
"mfp",
|
|
6246
|
+
"mfr",
|
|
6247
|
+
"mod",
|
|
6248
|
+
"mon",
|
|
6249
|
+
"mrb",
|
|
6250
|
+
"mrk",
|
|
6251
|
+
"msd",
|
|
6252
|
+
"mte",
|
|
6253
|
+
"mtk",
|
|
6254
|
+
"mus",
|
|
6255
|
+
"nrt",
|
|
6256
|
+
"opn",
|
|
6257
|
+
"org",
|
|
6258
|
+
"orm",
|
|
6259
|
+
"osp",
|
|
6260
|
+
"oth",
|
|
6261
|
+
"own",
|
|
6262
|
+
"pad",
|
|
6263
|
+
"pan",
|
|
6264
|
+
"pat",
|
|
6265
|
+
"pbd",
|
|
6266
|
+
"pbl",
|
|
6267
|
+
"pdr",
|
|
6268
|
+
"pfr",
|
|
6269
|
+
"pht",
|
|
6270
|
+
"plt",
|
|
6271
|
+
"pma",
|
|
6272
|
+
"pmn",
|
|
6273
|
+
"pop",
|
|
6274
|
+
"ppm",
|
|
6275
|
+
"ppt",
|
|
6276
|
+
"pra",
|
|
6277
|
+
"prc",
|
|
6278
|
+
"prd",
|
|
6279
|
+
"pre",
|
|
6280
|
+
"prf",
|
|
6281
|
+
"prg",
|
|
6282
|
+
"prm",
|
|
6283
|
+
"prn",
|
|
6284
|
+
"pro",
|
|
6285
|
+
"prp",
|
|
6286
|
+
"prs",
|
|
6287
|
+
"prt",
|
|
6288
|
+
"prv",
|
|
6289
|
+
"pta",
|
|
6290
|
+
"pte",
|
|
6291
|
+
"ptf",
|
|
6292
|
+
"pth",
|
|
6293
|
+
"ptt",
|
|
6294
|
+
"pup",
|
|
6295
|
+
"rbr",
|
|
6296
|
+
"rcd",
|
|
6297
|
+
"rce",
|
|
6298
|
+
"rcp",
|
|
6299
|
+
"rdd",
|
|
6300
|
+
"red",
|
|
6301
|
+
"ren",
|
|
6302
|
+
"res",
|
|
6303
|
+
"rev",
|
|
6304
|
+
"rpc",
|
|
6305
|
+
"rps",
|
|
6306
|
+
"rpt",
|
|
6307
|
+
"rpy",
|
|
6308
|
+
"rse",
|
|
6309
|
+
"rsg",
|
|
6310
|
+
"rsp",
|
|
6311
|
+
"rsr",
|
|
6312
|
+
"rst",
|
|
6313
|
+
"rth",
|
|
6314
|
+
"rtm",
|
|
6315
|
+
"sad",
|
|
6316
|
+
"sce",
|
|
6317
|
+
"scl",
|
|
6318
|
+
"scr",
|
|
6319
|
+
"sds",
|
|
6320
|
+
"sec",
|
|
6321
|
+
"sgd",
|
|
6322
|
+
"sgn",
|
|
6323
|
+
"sht",
|
|
6324
|
+
"sll",
|
|
6325
|
+
"sng",
|
|
6326
|
+
"spk",
|
|
6327
|
+
"spn",
|
|
6328
|
+
"spy",
|
|
6329
|
+
"srv",
|
|
6330
|
+
"std",
|
|
6331
|
+
"stg",
|
|
6332
|
+
"stl",
|
|
6333
|
+
"stm",
|
|
6334
|
+
"stn",
|
|
6335
|
+
"str",
|
|
6336
|
+
"tcd",
|
|
6337
|
+
"tch",
|
|
6338
|
+
"ths",
|
|
6339
|
+
"tld",
|
|
6340
|
+
"tlp",
|
|
6341
|
+
"trc",
|
|
6342
|
+
"trl",
|
|
6343
|
+
"tyd",
|
|
6344
|
+
"tyg",
|
|
6345
|
+
"uvp",
|
|
6346
|
+
"vac",
|
|
6347
|
+
"vdg",
|
|
6348
|
+
"voc",
|
|
6349
|
+
"wac",
|
|
6350
|
+
"wal",
|
|
6351
|
+
"wam",
|
|
6352
|
+
"wat",
|
|
6353
|
+
"wdc",
|
|
6354
|
+
"wde",
|
|
6355
|
+
"win",
|
|
6356
|
+
"wit",
|
|
6357
|
+
"wpr",
|
|
6358
|
+
"wst"
|
|
4622
6359
|
]);
|
|
4623
|
-
var
|
|
4624
|
-
|
|
4625
|
-
"
|
|
4626
|
-
"
|
|
4627
|
-
"
|
|
4628
|
-
"
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
"
|
|
4632
|
-
"
|
|
4633
|
-
"
|
|
4634
|
-
"
|
|
4635
|
-
"
|
|
4636
|
-
"
|
|
4637
|
-
"
|
|
6360
|
+
var DEPRECATED_LINK_REL = /* @__PURE__ */ new Set([
|
|
6361
|
+
"marc21xml-record",
|
|
6362
|
+
"mods-record",
|
|
6363
|
+
"onix-record",
|
|
6364
|
+
"xmp-record",
|
|
6365
|
+
"xml-signature"
|
|
6366
|
+
]);
|
|
6367
|
+
var GRANDFATHERED_LANG_TAGS = /* @__PURE__ */ new Set([
|
|
6368
|
+
"en-GB-oed",
|
|
6369
|
+
"i-ami",
|
|
6370
|
+
"i-bnn",
|
|
6371
|
+
"i-default",
|
|
6372
|
+
"i-enochian",
|
|
6373
|
+
"i-hak",
|
|
6374
|
+
"i-klingon",
|
|
6375
|
+
"i-lux",
|
|
6376
|
+
"i-mingo",
|
|
6377
|
+
"i-navajo",
|
|
6378
|
+
"i-pwn",
|
|
6379
|
+
"i-tao",
|
|
6380
|
+
"i-tay",
|
|
6381
|
+
"i-tsu",
|
|
6382
|
+
"sgn-BE-FR",
|
|
6383
|
+
"sgn-BE-NL",
|
|
6384
|
+
"sgn-CH-DE",
|
|
6385
|
+
"art-lojban",
|
|
6386
|
+
"cel-gaulish",
|
|
6387
|
+
"no-bok",
|
|
6388
|
+
"no-nyn",
|
|
6389
|
+
"zh-guoyu",
|
|
6390
|
+
"zh-hakka",
|
|
6391
|
+
"zh-min",
|
|
6392
|
+
"zh-min-nan",
|
|
6393
|
+
"zh-xiang"
|
|
4638
6394
|
]);
|
|
4639
|
-
|
|
4640
|
-
// src/opf/validator.ts
|
|
4641
6395
|
var OPFValidator = class {
|
|
4642
6396
|
packageDoc = null;
|
|
4643
6397
|
manifestById = /* @__PURE__ */ new Map();
|
|
@@ -4699,13 +6453,7 @@ var OPFValidator = class {
|
|
|
4699
6453
|
if (this.packageDoc.xmlLangs) {
|
|
4700
6454
|
for (const lang of this.packageDoc.xmlLangs) {
|
|
4701
6455
|
if (lang === "") continue;
|
|
4702
|
-
if (lang !== lang.trim()) {
|
|
4703
|
-
pushMessage(context.messages, {
|
|
4704
|
-
id: MessageId.OPF_092,
|
|
4705
|
-
message: `Language tag "${lang}" is not well-formed`,
|
|
4706
|
-
location: { path: opfPath }
|
|
4707
|
-
});
|
|
4708
|
-
} else if (!isValidLanguageTag(lang)) {
|
|
6456
|
+
if (lang !== lang.trim() || !isValidLanguageTag(lang)) {
|
|
4709
6457
|
pushMessage(context.messages, {
|
|
4710
6458
|
id: MessageId.OPF_092,
|
|
4711
6459
|
message: `Language tag "${lang}" is not well-formed`,
|
|
@@ -4731,11 +6479,10 @@ var OPFValidator = class {
|
|
|
4731
6479
|
*/
|
|
4732
6480
|
validatePackageAttributes(context, opfPath) {
|
|
4733
6481
|
if (!this.packageDoc) return;
|
|
4734
|
-
|
|
4735
|
-
if (!validVersions.has(this.packageDoc.version)) {
|
|
6482
|
+
if (!VALID_VERSIONS.has(this.packageDoc.version)) {
|
|
4736
6483
|
pushMessage(context.messages, {
|
|
4737
6484
|
id: MessageId.OPF_001,
|
|
4738
|
-
message: `Invalid package version "${this.packageDoc.version}"; must be one of: ${Array.from(
|
|
6485
|
+
message: `Invalid package version "${this.packageDoc.version}"; must be one of: ${Array.from(VALID_VERSIONS).join(", ")}`,
|
|
4739
6486
|
location: { path: opfPath }
|
|
4740
6487
|
});
|
|
4741
6488
|
}
|
|
@@ -4835,111 +6582,13 @@ var OPFValidator = class {
|
|
|
4835
6582
|
}
|
|
4836
6583
|
}
|
|
4837
6584
|
if (dc.name === "creator" && dc.attributes) {
|
|
4838
|
-
const
|
|
4839
|
-
if (
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
"
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
"ccp",
|
|
4846
|
-
"com",
|
|
4847
|
-
"ctb",
|
|
4848
|
-
"csl",
|
|
4849
|
-
"edt",
|
|
4850
|
-
"ill",
|
|
4851
|
-
"itr",
|
|
4852
|
-
"pbl",
|
|
4853
|
-
"pdr",
|
|
4854
|
-
"prt",
|
|
4855
|
-
"trl",
|
|
4856
|
-
"cre",
|
|
4857
|
-
"art",
|
|
4858
|
-
"ctb",
|
|
4859
|
-
"edt",
|
|
4860
|
-
"pfr",
|
|
4861
|
-
"red",
|
|
4862
|
-
"rev",
|
|
4863
|
-
"spn",
|
|
4864
|
-
"dsx",
|
|
4865
|
-
"pmc",
|
|
4866
|
-
"dte",
|
|
4867
|
-
"ove",
|
|
4868
|
-
"trc",
|
|
4869
|
-
"ldr",
|
|
4870
|
-
"led",
|
|
4871
|
-
"prg",
|
|
4872
|
-
"rap",
|
|
4873
|
-
"rce",
|
|
4874
|
-
"rpc",
|
|
4875
|
-
"rtr",
|
|
4876
|
-
"sad",
|
|
4877
|
-
"sgn",
|
|
4878
|
-
"tce",
|
|
4879
|
-
"aac",
|
|
4880
|
-
"acq",
|
|
4881
|
-
"ant",
|
|
4882
|
-
"arr",
|
|
4883
|
-
"art",
|
|
4884
|
-
"ard",
|
|
4885
|
-
"asg",
|
|
4886
|
-
"aus",
|
|
4887
|
-
"aft",
|
|
4888
|
-
"bdd",
|
|
4889
|
-
"bdd",
|
|
4890
|
-
"clb",
|
|
4891
|
-
"clc",
|
|
4892
|
-
"drd",
|
|
4893
|
-
"edt",
|
|
4894
|
-
"edt",
|
|
4895
|
-
"fmd",
|
|
4896
|
-
"flm",
|
|
4897
|
-
"fmo",
|
|
4898
|
-
"fpy",
|
|
4899
|
-
"hnr",
|
|
4900
|
-
"ill",
|
|
4901
|
-
"ilt",
|
|
4902
|
-
"img",
|
|
4903
|
-
"itr",
|
|
4904
|
-
"lrg",
|
|
4905
|
-
"lsa",
|
|
4906
|
-
"led",
|
|
4907
|
-
"lee",
|
|
4908
|
-
"lel",
|
|
4909
|
-
"lgd",
|
|
4910
|
-
"lse",
|
|
4911
|
-
"mfr",
|
|
4912
|
-
"mod",
|
|
4913
|
-
"mon",
|
|
4914
|
-
"mus",
|
|
4915
|
-
"nrt",
|
|
4916
|
-
"ogt",
|
|
4917
|
-
"org",
|
|
4918
|
-
"oth",
|
|
4919
|
-
"pnt",
|
|
4920
|
-
"ppa",
|
|
4921
|
-
"prv",
|
|
4922
|
-
"pup",
|
|
4923
|
-
"red",
|
|
4924
|
-
"rev",
|
|
4925
|
-
"rsg",
|
|
4926
|
-
"srv",
|
|
4927
|
-
"stn",
|
|
4928
|
-
"stl",
|
|
4929
|
-
"trc",
|
|
4930
|
-
"typ",
|
|
4931
|
-
"vdg",
|
|
4932
|
-
"voc",
|
|
4933
|
-
"wac",
|
|
4934
|
-
"wdc"
|
|
4935
|
-
]);
|
|
4936
|
-
if (!validRelatorCodes.has(relatorCode)) {
|
|
4937
|
-
pushMessage(context.messages, {
|
|
4938
|
-
id: MessageId.OPF_052,
|
|
4939
|
-
message: `Unknown MARC relator code "${relatorCode}" in dc:creator`,
|
|
4940
|
-
location: { path: opfPath }
|
|
4941
|
-
});
|
|
4942
|
-
}
|
|
6585
|
+
const role = dc.attributes["opf:role"];
|
|
6586
|
+
if (role && !VALID_RELATOR_CODES.has(role) && !role.startsWith("oth.")) {
|
|
6587
|
+
pushMessage(context.messages, {
|
|
6588
|
+
id: MessageId.OPF_052,
|
|
6589
|
+
message: `Invalid role value "${role}" in dc:creator`,
|
|
6590
|
+
location: { path: opfPath }
|
|
6591
|
+
});
|
|
4943
6592
|
}
|
|
4944
6593
|
}
|
|
4945
6594
|
}
|
|
@@ -5009,6 +6658,9 @@ var OPFValidator = class {
|
|
|
5009
6658
|
for (const item of this.packageDoc.manifest) {
|
|
5010
6659
|
allIdSources.push({ id: item.id, normalized: item.id.trim() });
|
|
5011
6660
|
}
|
|
6661
|
+
for (const itemref of this.packageDoc.spine) {
|
|
6662
|
+
if (itemref.id) allIdSources.push({ id: itemref.id, normalized: itemref.id.trim() });
|
|
6663
|
+
}
|
|
5012
6664
|
for (const src of allIdSources) {
|
|
5013
6665
|
if (seenGlobalIds.has(src.normalized)) {
|
|
5014
6666
|
pushMessage(context.messages, {
|
|
@@ -5031,11 +6683,14 @@ var OPFValidator = class {
|
|
|
5031
6683
|
continue;
|
|
5032
6684
|
}
|
|
5033
6685
|
if (!refines.startsWith("#")) {
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
6686
|
+
const isManifestHref = this.packageDoc.manifest.some((item) => item.href === refines);
|
|
6687
|
+
if (isManifestHref) {
|
|
6688
|
+
pushMessage(context.messages, {
|
|
6689
|
+
id: MessageId.RSC_017,
|
|
6690
|
+
message: `@refines should instead refer to "${refines}" using a fragment identifier pointing to its manifest item`,
|
|
6691
|
+
location: { path: opfPath }
|
|
6692
|
+
});
|
|
6693
|
+
}
|
|
5039
6694
|
continue;
|
|
5040
6695
|
}
|
|
5041
6696
|
const targetId = refines.substring(1);
|
|
@@ -5050,35 +6705,293 @@ var OPFValidator = class {
|
|
|
5050
6705
|
this.detectRefinesCycles(context, opfPath);
|
|
5051
6706
|
}
|
|
5052
6707
|
if (this.packageDoc.version !== "2.0") {
|
|
5053
|
-
|
|
6708
|
+
this.validateMetaPropertiesVocab(context, opfPath, dcElements);
|
|
6709
|
+
}
|
|
6710
|
+
if (this.packageDoc.version !== "2.0") {
|
|
6711
|
+
const modifiedMetas = this.packageDoc.metaElements.filter(
|
|
5054
6712
|
(meta) => meta.property === "dcterms:modified"
|
|
5055
6713
|
);
|
|
6714
|
+
const modifiedMeta = modifiedMetas[0];
|
|
6715
|
+
if (modifiedMetas.length > 1) {
|
|
6716
|
+
pushMessage(context.messages, {
|
|
6717
|
+
id: MessageId.RSC_005,
|
|
6718
|
+
message: "package dcterms:modified meta element must occur exactly once",
|
|
6719
|
+
location: { path: opfPath }
|
|
6720
|
+
});
|
|
6721
|
+
}
|
|
5056
6722
|
if (!modifiedMeta) {
|
|
5057
6723
|
pushMessage(context.messages, {
|
|
5058
6724
|
id: MessageId.RSC_005,
|
|
5059
|
-
message: "package dcterms:modified meta element must occur exactly once",
|
|
6725
|
+
message: "package dcterms:modified meta element must occur exactly once",
|
|
6726
|
+
location: { path: opfPath }
|
|
6727
|
+
});
|
|
6728
|
+
pushMessage(context.messages, {
|
|
6729
|
+
id: MessageId.OPF_054,
|
|
6730
|
+
message: "EPUB 3 metadata must include a dcterms:modified meta element",
|
|
6731
|
+
location: { path: opfPath }
|
|
6732
|
+
});
|
|
6733
|
+
} else if (modifiedMeta.value) {
|
|
6734
|
+
const strictModifiedPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
|
|
6735
|
+
if (!strictModifiedPattern.test(modifiedMeta.value.trim())) {
|
|
6736
|
+
pushMessage(context.messages, {
|
|
6737
|
+
id: MessageId.RSC_005,
|
|
6738
|
+
message: `dcterms:modified illegal syntax (expecting: "CCYY-MM-DDThh:mm:ssZ")`,
|
|
6739
|
+
location: { path: opfPath }
|
|
6740
|
+
});
|
|
6741
|
+
}
|
|
6742
|
+
if (!isValidW3CDateFormat(modifiedMeta.value)) {
|
|
6743
|
+
pushMessage(context.messages, {
|
|
6744
|
+
id: MessageId.OPF_054,
|
|
6745
|
+
message: `Invalid dcterms:modified date format "${modifiedMeta.value}"; must be W3C date format (ISO 8601)`,
|
|
6746
|
+
location: { path: opfPath }
|
|
6747
|
+
});
|
|
6748
|
+
}
|
|
6749
|
+
}
|
|
6750
|
+
}
|
|
6751
|
+
}
|
|
6752
|
+
/**
|
|
6753
|
+
* Validate EPUB 3 meta element vocabulary (D-vocabularies: meta-properties)
|
|
6754
|
+
* Ports package-30.sch Schematron patterns for authority, term, belongs-to-collection,
|
|
6755
|
+
* collection-type, display-seq, file-as, group-position, identifier-type, meta-auth,
|
|
6756
|
+
* role, source-of, and title-type.
|
|
6757
|
+
*/
|
|
6758
|
+
validateMetaPropertiesVocab(context, opfPath, dcElements) {
|
|
6759
|
+
if (!this.packageDoc) return;
|
|
6760
|
+
const metaElements = this.packageDoc.metaElements;
|
|
6761
|
+
const metaIdToProp = /* @__PURE__ */ new Map();
|
|
6762
|
+
for (const meta of metaElements) {
|
|
6763
|
+
if (meta.id) metaIdToProp.set(meta.id.trim(), meta.property.trim());
|
|
6764
|
+
}
|
|
6765
|
+
for (const dc of dcElements) {
|
|
6766
|
+
if (dc.name !== "subject" || !dc.id) continue;
|
|
6767
|
+
const subjectId = dc.id.trim();
|
|
6768
|
+
const authorityCount = metaElements.filter(
|
|
6769
|
+
(m) => m.property.trim() === "authority" && m.refines?.trim().substring(1) === subjectId
|
|
6770
|
+
).length;
|
|
6771
|
+
const termCount = metaElements.filter(
|
|
6772
|
+
(m) => m.property.trim() === "term" && m.refines?.trim().substring(1) === subjectId
|
|
6773
|
+
).length;
|
|
6774
|
+
if (authorityCount > 1 || termCount > 1) {
|
|
6775
|
+
pushMessage(context.messages, {
|
|
6776
|
+
id: MessageId.RSC_005,
|
|
6777
|
+
message: "Only one pair of authority and term properties can be associated with a dc:subject",
|
|
6778
|
+
location: { path: opfPath }
|
|
6779
|
+
});
|
|
6780
|
+
} else if (authorityCount === 1 && termCount === 0) {
|
|
6781
|
+
pushMessage(context.messages, {
|
|
6782
|
+
id: MessageId.RSC_005,
|
|
6783
|
+
message: "A term property must be associated with a dc:subject when an authority is specified",
|
|
5060
6784
|
location: { path: opfPath }
|
|
5061
6785
|
});
|
|
6786
|
+
} else if (authorityCount === 0 && termCount === 1) {
|
|
5062
6787
|
pushMessage(context.messages, {
|
|
5063
|
-
id: MessageId.
|
|
5064
|
-
message: "
|
|
6788
|
+
id: MessageId.RSC_005,
|
|
6789
|
+
message: "An authority property must be associated with a dc:subject when a term is specified",
|
|
5065
6790
|
location: { path: opfPath }
|
|
5066
6791
|
});
|
|
5067
|
-
}
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
6792
|
+
}
|
|
6793
|
+
}
|
|
6794
|
+
const seenPropertyRefines = /* @__PURE__ */ new Set();
|
|
6795
|
+
for (const meta of metaElements) {
|
|
6796
|
+
const prop = meta.property.trim();
|
|
6797
|
+
const refines = meta.refines?.trim();
|
|
6798
|
+
switch (prop) {
|
|
6799
|
+
case "authority": {
|
|
6800
|
+
const ok = dcElements.some(
|
|
6801
|
+
(dc) => dc.name === "subject" && dc.id && "#" + dc.id.trim() === refines
|
|
6802
|
+
);
|
|
6803
|
+
if (!ok) {
|
|
6804
|
+
pushMessage(context.messages, {
|
|
6805
|
+
id: MessageId.RSC_005,
|
|
6806
|
+
message: 'Property "authority" must refine a "subject" property.',
|
|
6807
|
+
location: { path: opfPath }
|
|
6808
|
+
});
|
|
6809
|
+
}
|
|
6810
|
+
break;
|
|
5075
6811
|
}
|
|
5076
|
-
|
|
6812
|
+
case "term": {
|
|
6813
|
+
const ok = dcElements.some(
|
|
6814
|
+
(dc) => dc.name === "subject" && dc.id && "#" + dc.id.trim() === refines
|
|
6815
|
+
);
|
|
6816
|
+
if (!ok) {
|
|
6817
|
+
pushMessage(context.messages, {
|
|
6818
|
+
id: MessageId.RSC_005,
|
|
6819
|
+
message: 'Property "term" must refine a "subject" property.',
|
|
6820
|
+
location: { path: opfPath }
|
|
6821
|
+
});
|
|
6822
|
+
}
|
|
6823
|
+
break;
|
|
6824
|
+
}
|
|
6825
|
+
case "belongs-to-collection": {
|
|
6826
|
+
if (refines) {
|
|
6827
|
+
const targetId = refines.substring(1);
|
|
6828
|
+
if (metaIdToProp.get(targetId) !== "belongs-to-collection") {
|
|
6829
|
+
pushMessage(context.messages, {
|
|
6830
|
+
id: MessageId.RSC_005,
|
|
6831
|
+
message: 'Property "belongs-to-collection" can only refine other "belongs-to-collection" properties.',
|
|
6832
|
+
location: { path: opfPath }
|
|
6833
|
+
});
|
|
6834
|
+
}
|
|
6835
|
+
}
|
|
6836
|
+
break;
|
|
6837
|
+
}
|
|
6838
|
+
case "collection-type": {
|
|
6839
|
+
if (!refines) {
|
|
6840
|
+
pushMessage(context.messages, {
|
|
6841
|
+
id: MessageId.RSC_005,
|
|
6842
|
+
message: 'Property "collection-type" must refine a "belongs-to-collection" property.',
|
|
6843
|
+
location: { path: opfPath }
|
|
6844
|
+
});
|
|
6845
|
+
} else {
|
|
6846
|
+
const targetId = refines.substring(1);
|
|
6847
|
+
if (metaIdToProp.get(targetId) !== "belongs-to-collection") {
|
|
6848
|
+
pushMessage(context.messages, {
|
|
6849
|
+
id: MessageId.RSC_005,
|
|
6850
|
+
message: 'Property "collection-type" must refine a "belongs-to-collection" property.',
|
|
6851
|
+
location: { path: opfPath }
|
|
6852
|
+
});
|
|
6853
|
+
}
|
|
6854
|
+
}
|
|
6855
|
+
const ctKey = `${prop}:${refines ?? ""}`;
|
|
6856
|
+
if (seenPropertyRefines.has(ctKey)) {
|
|
6857
|
+
pushMessage(context.messages, {
|
|
6858
|
+
id: MessageId.RSC_005,
|
|
6859
|
+
message: '"collection-type" cannot be declared more than once to refine the same "belongs-to-collection" expression.',
|
|
6860
|
+
location: { path: opfPath }
|
|
6861
|
+
});
|
|
6862
|
+
}
|
|
6863
|
+
seenPropertyRefines.add(ctKey);
|
|
6864
|
+
break;
|
|
6865
|
+
}
|
|
6866
|
+
case "display-seq": {
|
|
6867
|
+
const key = `${prop}:${refines ?? ""}`;
|
|
6868
|
+
if (seenPropertyRefines.has(key)) {
|
|
6869
|
+
pushMessage(context.messages, {
|
|
6870
|
+
id: MessageId.RSC_005,
|
|
6871
|
+
message: '"display-seq" cannot be declared more than once to refine the same expression.',
|
|
6872
|
+
location: { path: opfPath }
|
|
6873
|
+
});
|
|
6874
|
+
}
|
|
6875
|
+
seenPropertyRefines.add(key);
|
|
6876
|
+
break;
|
|
6877
|
+
}
|
|
6878
|
+
case "file-as": {
|
|
6879
|
+
const key = `${prop}:${refines ?? ""}`;
|
|
6880
|
+
if (seenPropertyRefines.has(key)) {
|
|
6881
|
+
pushMessage(context.messages, {
|
|
6882
|
+
id: MessageId.RSC_005,
|
|
6883
|
+
message: '"file-as" cannot be declared more than once to refine the same expression.',
|
|
6884
|
+
location: { path: opfPath }
|
|
6885
|
+
});
|
|
6886
|
+
}
|
|
6887
|
+
seenPropertyRefines.add(key);
|
|
6888
|
+
break;
|
|
6889
|
+
}
|
|
6890
|
+
case "group-position": {
|
|
6891
|
+
const key = `${prop}:${refines ?? ""}`;
|
|
6892
|
+
if (seenPropertyRefines.has(key)) {
|
|
6893
|
+
pushMessage(context.messages, {
|
|
6894
|
+
id: MessageId.RSC_005,
|
|
6895
|
+
message: '"group-position" cannot be declared more than once to refine the same expression.',
|
|
6896
|
+
location: { path: opfPath }
|
|
6897
|
+
});
|
|
6898
|
+
}
|
|
6899
|
+
seenPropertyRefines.add(key);
|
|
6900
|
+
break;
|
|
6901
|
+
}
|
|
6902
|
+
case "identifier-type": {
|
|
6903
|
+
const ok = dcElements.some(
|
|
6904
|
+
(dc) => (dc.name === "identifier" || dc.name === "source") && dc.id && "#" + dc.id.trim() === refines
|
|
6905
|
+
);
|
|
6906
|
+
if (!ok) {
|
|
6907
|
+
pushMessage(context.messages, {
|
|
6908
|
+
id: MessageId.RSC_005,
|
|
6909
|
+
message: 'Property "identifier-type" must refine an "identifier" or "source" property.',
|
|
6910
|
+
location: { path: opfPath }
|
|
6911
|
+
});
|
|
6912
|
+
}
|
|
6913
|
+
const itKey = `${prop}:${refines ?? ""}`;
|
|
6914
|
+
if (seenPropertyRefines.has(itKey)) {
|
|
6915
|
+
pushMessage(context.messages, {
|
|
6916
|
+
id: MessageId.RSC_005,
|
|
6917
|
+
message: '"identifier-type" cannot be declared more than once to refine the same expression.',
|
|
6918
|
+
location: { path: opfPath }
|
|
6919
|
+
});
|
|
6920
|
+
}
|
|
6921
|
+
seenPropertyRefines.add(itKey);
|
|
6922
|
+
break;
|
|
6923
|
+
}
|
|
6924
|
+
case "meta-auth": {
|
|
5077
6925
|
pushMessage(context.messages, {
|
|
5078
|
-
id: MessageId.
|
|
5079
|
-
message:
|
|
6926
|
+
id: MessageId.RSC_017,
|
|
6927
|
+
message: "Use of the meta-auth property is deprecated",
|
|
5080
6928
|
location: { path: opfPath }
|
|
5081
6929
|
});
|
|
6930
|
+
break;
|
|
6931
|
+
}
|
|
6932
|
+
case "role": {
|
|
6933
|
+
const ok = dcElements.some(
|
|
6934
|
+
(dc) => (dc.name === "creator" || dc.name === "contributor" || dc.name === "publisher") && dc.id && "#" + dc.id.trim() === refines
|
|
6935
|
+
);
|
|
6936
|
+
if (!ok) {
|
|
6937
|
+
pushMessage(context.messages, {
|
|
6938
|
+
id: MessageId.RSC_005,
|
|
6939
|
+
message: 'Property "role" must refine a "creator", "contributor", or "publisher" property.',
|
|
6940
|
+
location: { path: opfPath }
|
|
6941
|
+
});
|
|
6942
|
+
}
|
|
6943
|
+
break;
|
|
6944
|
+
}
|
|
6945
|
+
case "source-of": {
|
|
6946
|
+
if (meta.value.trim() !== "pagination") {
|
|
6947
|
+
pushMessage(context.messages, {
|
|
6948
|
+
id: MessageId.RSC_005,
|
|
6949
|
+
message: 'The "source-of" property must have the value "pagination"',
|
|
6950
|
+
location: { path: opfPath }
|
|
6951
|
+
});
|
|
6952
|
+
}
|
|
6953
|
+
const hasSourceRefines = dcElements.some(
|
|
6954
|
+
(dc) => dc.name === "source" && dc.id && refines?.substring(1) === dc.id.trim()
|
|
6955
|
+
);
|
|
6956
|
+
if (!hasSourceRefines) {
|
|
6957
|
+
pushMessage(context.messages, {
|
|
6958
|
+
id: MessageId.RSC_005,
|
|
6959
|
+
message: 'The "source-of" property must refine a "source" property.',
|
|
6960
|
+
location: { path: opfPath }
|
|
6961
|
+
});
|
|
6962
|
+
}
|
|
6963
|
+
const soKey = `${prop}:${refines ?? ""}`;
|
|
6964
|
+
if (seenPropertyRefines.has(soKey)) {
|
|
6965
|
+
pushMessage(context.messages, {
|
|
6966
|
+
id: MessageId.RSC_005,
|
|
6967
|
+
message: '"source-of" cannot be declared more than once to refine the same "source" expression.',
|
|
6968
|
+
location: { path: opfPath }
|
|
6969
|
+
});
|
|
6970
|
+
}
|
|
6971
|
+
seenPropertyRefines.add(soKey);
|
|
6972
|
+
break;
|
|
6973
|
+
}
|
|
6974
|
+
case "title-type": {
|
|
6975
|
+
const ok = dcElements.some(
|
|
6976
|
+
(dc) => dc.name === "title" && dc.id && "#" + dc.id.trim() === refines
|
|
6977
|
+
);
|
|
6978
|
+
if (!ok) {
|
|
6979
|
+
pushMessage(context.messages, {
|
|
6980
|
+
id: MessageId.RSC_005,
|
|
6981
|
+
message: 'Property "title-type" must refine a "title" property.',
|
|
6982
|
+
location: { path: opfPath }
|
|
6983
|
+
});
|
|
6984
|
+
}
|
|
6985
|
+
const ttKey = `${prop}:${refines ?? ""}`;
|
|
6986
|
+
if (seenPropertyRefines.has(ttKey)) {
|
|
6987
|
+
pushMessage(context.messages, {
|
|
6988
|
+
id: MessageId.RSC_005,
|
|
6989
|
+
message: '"title-type" cannot be declared more than once to refine the same "title" expression.',
|
|
6990
|
+
location: { path: opfPath }
|
|
6991
|
+
});
|
|
6992
|
+
}
|
|
6993
|
+
seenPropertyRefines.add(ttKey);
|
|
6994
|
+
break;
|
|
5082
6995
|
}
|
|
5083
6996
|
}
|
|
5084
6997
|
}
|
|
@@ -5088,7 +7001,6 @@ var OPFValidator = class {
|
|
|
5088
7001
|
*/
|
|
5089
7002
|
validateLinkElements(context, opfPath) {
|
|
5090
7003
|
if (!this.packageDoc) return;
|
|
5091
|
-
const opfDir = opfPath.includes("/") ? opfPath.substring(0, opfPath.lastIndexOf("/")) : "";
|
|
5092
7004
|
for (const link of this.packageDoc.linkElements) {
|
|
5093
7005
|
if (link.hreflang !== void 0 && link.hreflang !== "") {
|
|
5094
7006
|
const lang = link.hreflang;
|
|
@@ -5117,6 +7029,40 @@ var OPFValidator = class {
|
|
|
5117
7029
|
}
|
|
5118
7030
|
}
|
|
5119
7031
|
}
|
|
7032
|
+
const relKeywords = link.rel ? link.rel.trim().split(/\s+/).filter(Boolean) : [];
|
|
7033
|
+
const hasRecord = relKeywords.includes("record");
|
|
7034
|
+
const hasVoicing = relKeywords.includes("voicing");
|
|
7035
|
+
const hasAlternate = relKeywords.includes("alternate");
|
|
7036
|
+
if (hasAlternate && relKeywords.length > 1) {
|
|
7037
|
+
pushMessage(context.messages, {
|
|
7038
|
+
id: MessageId.OPF_089,
|
|
7039
|
+
message: `The "alternate" keyword must not be combined with other keywords in the "rel" attribute`,
|
|
7040
|
+
location: { path: opfPath }
|
|
7041
|
+
});
|
|
7042
|
+
}
|
|
7043
|
+
for (const kw of relKeywords) {
|
|
7044
|
+
if (DEPRECATED_LINK_REL.has(kw)) {
|
|
7045
|
+
pushMessage(context.messages, {
|
|
7046
|
+
id: MessageId.OPF_086,
|
|
7047
|
+
message: `The rel keyword "${kw}" is deprecated`,
|
|
7048
|
+
location: { path: opfPath }
|
|
7049
|
+
});
|
|
7050
|
+
}
|
|
7051
|
+
}
|
|
7052
|
+
if (hasRecord && link.refines) {
|
|
7053
|
+
pushMessage(context.messages, {
|
|
7054
|
+
id: MessageId.RSC_005,
|
|
7055
|
+
message: '"record" links only applies to the Publication (must not have a "refines" attribute).',
|
|
7056
|
+
location: { path: opfPath }
|
|
7057
|
+
});
|
|
7058
|
+
}
|
|
7059
|
+
if (hasVoicing && !link.refines) {
|
|
7060
|
+
pushMessage(context.messages, {
|
|
7061
|
+
id: MessageId.RSC_005,
|
|
7062
|
+
message: '"voicing" links must have a "refines" attribute.',
|
|
7063
|
+
location: { path: opfPath }
|
|
7064
|
+
});
|
|
7065
|
+
}
|
|
5120
7066
|
const href = link.href;
|
|
5121
7067
|
const decodedHref = tryDecodeUriComponent(href);
|
|
5122
7068
|
const basePath = href.includes("#") ? href.substring(0, href.indexOf("#")) : href;
|
|
@@ -5130,6 +7076,20 @@ var OPFValidator = class {
|
|
|
5130
7076
|
continue;
|
|
5131
7077
|
}
|
|
5132
7078
|
const isRemote = /^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(href);
|
|
7079
|
+
if (hasVoicing && link.mediaType && !link.mediaType.startsWith("audio/")) {
|
|
7080
|
+
pushMessage(context.messages, {
|
|
7081
|
+
id: MessageId.OPF_095,
|
|
7082
|
+
message: `The "voicing" link media type must be an audio type, but found "${link.mediaType}"`,
|
|
7083
|
+
location: { path: opfPath }
|
|
7084
|
+
});
|
|
7085
|
+
}
|
|
7086
|
+
if (isRemote && !link.mediaType && (hasRecord || hasVoicing)) {
|
|
7087
|
+
pushMessage(context.messages, {
|
|
7088
|
+
id: MessageId.OPF_094,
|
|
7089
|
+
message: `The "media-type" attribute is required for "record" and "voicing" links`,
|
|
7090
|
+
location: { path: opfPath }
|
|
7091
|
+
});
|
|
7092
|
+
}
|
|
5133
7093
|
if (isRemote) {
|
|
5134
7094
|
continue;
|
|
5135
7095
|
}
|
|
@@ -5140,8 +7100,8 @@ var OPFValidator = class {
|
|
|
5140
7100
|
location: { path: opfPath }
|
|
5141
7101
|
});
|
|
5142
7102
|
}
|
|
5143
|
-
const resolvedPath = resolvePath(
|
|
5144
|
-
const resolvedPathDecoded = basePathDecoded !== basePath ? resolvePath(
|
|
7103
|
+
const resolvedPath = resolvePath(opfPath, basePath);
|
|
7104
|
+
const resolvedPathDecoded = basePathDecoded !== basePath ? resolvePath(opfPath, basePathDecoded) : resolvedPath;
|
|
5145
7105
|
const fileExists = context.files.has(resolvedPath) || context.files.has(resolvedPathDecoded);
|
|
5146
7106
|
const inManifest = this.manifestByHref.has(basePath) || this.manifestByHref.has(basePathDecoded);
|
|
5147
7107
|
if (!fileExists && !inManifest) {
|
|
@@ -5218,13 +7178,7 @@ var OPFValidator = class {
|
|
|
5218
7178
|
location: { path: opfPath }
|
|
5219
7179
|
});
|
|
5220
7180
|
}
|
|
5221
|
-
|
|
5222
|
-
"text/x-oeb1-document",
|
|
5223
|
-
"text/x-oeb1-css",
|
|
5224
|
-
"application/x-oeb1-package",
|
|
5225
|
-
"text/x-oeb1-html"
|
|
5226
|
-
]);
|
|
5227
|
-
if (deprecatedTypes.has(item.mediaType)) {
|
|
7181
|
+
if (DEPRECATED_MEDIA_TYPES.has(item.mediaType)) {
|
|
5228
7182
|
pushMessage(context.messages, {
|
|
5229
7183
|
id: MessageId.OPF_037,
|
|
5230
7184
|
message: `Found deprecated media-type "${item.mediaType}"`,
|
|
@@ -5280,20 +7234,23 @@ var OPFValidator = class {
|
|
|
5280
7234
|
});
|
|
5281
7235
|
}
|
|
5282
7236
|
if (this.packageDoc.version !== "2.0" && (item.href.startsWith("http://") || item.href.startsWith("https://"))) {
|
|
5283
|
-
|
|
5284
|
-
pushMessage(context.messages, {
|
|
5285
|
-
id: MessageId.RSC_006,
|
|
5286
|
-
message: `Remote resource reference is not allowed in this context; resource "${item.href}" must be located in the EPUB container`,
|
|
5287
|
-
location: { path: opfPath }
|
|
5288
|
-
});
|
|
5289
|
-
}
|
|
7237
|
+
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";
|
|
5290
7238
|
const inSpine = this.packageDoc.spine.some((s) => s.idref === item.id);
|
|
5291
|
-
if (inSpine
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
7239
|
+
if (inSpine) {
|
|
7240
|
+
if (!isAllowedRemoteType) {
|
|
7241
|
+
pushMessage(context.messages, {
|
|
7242
|
+
id: MessageId.RSC_006,
|
|
7243
|
+
message: `Remote resource reference is not allowed in this context; resource "${item.href}" must be located in the EPUB container`,
|
|
7244
|
+
location: { path: opfPath }
|
|
7245
|
+
});
|
|
7246
|
+
}
|
|
7247
|
+
if (!item.properties?.includes("remote-resources")) {
|
|
7248
|
+
pushMessage(context.messages, {
|
|
7249
|
+
id: MessageId.RSC_006,
|
|
7250
|
+
message: `Manifest item "${item.id}" references remote resource but is missing "remote-resources" property`,
|
|
7251
|
+
location: { path: opfPath }
|
|
7252
|
+
});
|
|
7253
|
+
}
|
|
5297
7254
|
}
|
|
5298
7255
|
}
|
|
5299
7256
|
}
|
|
@@ -5398,12 +7355,20 @@ var OPFValidator = class {
|
|
|
5398
7355
|
});
|
|
5399
7356
|
}
|
|
5400
7357
|
seenIdrefs.add(itemref.idref);
|
|
5401
|
-
if (!isSpineMediaType(item.mediaType)
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
7358
|
+
if (!isSpineMediaType(item.mediaType)) {
|
|
7359
|
+
if (!item.fallback) {
|
|
7360
|
+
pushMessage(context.messages, {
|
|
7361
|
+
id: MessageId.OPF_043,
|
|
7362
|
+
message: `Spine item "${item.id}" has non-standard media type "${item.mediaType}" without fallback`,
|
|
7363
|
+
location: { path: opfPath }
|
|
7364
|
+
});
|
|
7365
|
+
} else if (!this.fallbackChainResolvesToContentDocument(item.id)) {
|
|
7366
|
+
pushMessage(context.messages, {
|
|
7367
|
+
id: MessageId.OPF_044,
|
|
7368
|
+
message: `Spine item "${item.id}" has non-standard media type "${item.mediaType}" and its fallback chain does not resolve to a content document`,
|
|
7369
|
+
location: { path: opfPath }
|
|
7370
|
+
});
|
|
7371
|
+
}
|
|
5407
7372
|
}
|
|
5408
7373
|
if (this.packageDoc.version !== "2.0" && itemref.properties) {
|
|
5409
7374
|
for (const prop of itemref.properties) {
|
|
@@ -5601,14 +7566,29 @@ var OPFValidator = class {
|
|
|
5601
7566
|
}
|
|
5602
7567
|
}
|
|
5603
7568
|
}
|
|
7569
|
+
fallbackChainResolvesToContentDocument(itemId) {
|
|
7570
|
+
const visited = /* @__PURE__ */ new Set();
|
|
7571
|
+
let currentId = itemId;
|
|
7572
|
+
while (currentId) {
|
|
7573
|
+
if (visited.has(currentId)) return false;
|
|
7574
|
+
visited.add(currentId);
|
|
7575
|
+
const item = this.manifestById.get(currentId);
|
|
7576
|
+
if (!item) return false;
|
|
7577
|
+
if (isSpineMediaType(item.mediaType)) return true;
|
|
7578
|
+
currentId = item.fallback;
|
|
7579
|
+
}
|
|
7580
|
+
return false;
|
|
7581
|
+
}
|
|
5604
7582
|
};
|
|
5605
7583
|
function isSpineMediaType(mediaType) {
|
|
5606
7584
|
return mediaType === "application/xhtml+xml" || mediaType === "image/svg+xml" || // EPUB 2 also allows these in spine
|
|
5607
7585
|
mediaType === "application/x-dtbook+xml";
|
|
5608
7586
|
}
|
|
5609
7587
|
function isValidLanguageTag(tag) {
|
|
5610
|
-
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}))
|
|
5611
|
-
|
|
7588
|
+
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})+)?$/;
|
|
7589
|
+
if (pattern.test(tag)) return true;
|
|
7590
|
+
if (/^x(-[a-zA-Z\d]{1,8})+$/.test(tag)) return true;
|
|
7591
|
+
return GRANDFATHERED_LANG_TAGS.has(tag);
|
|
5612
7592
|
}
|
|
5613
7593
|
function resolvePath(basePath, relativePath) {
|
|
5614
7594
|
if (relativePath.startsWith("/")) {
|
|
@@ -5636,17 +7616,6 @@ function tryDecodeUriComponent(encoded) {
|
|
|
5636
7616
|
return encoded;
|
|
5637
7617
|
}
|
|
5638
7618
|
}
|
|
5639
|
-
function checkUrlLeaking(href) {
|
|
5640
|
-
const TEST_BASE_A = "https://a.example.org/A/";
|
|
5641
|
-
const TEST_BASE_B = "https://b.example.org/B/";
|
|
5642
|
-
try {
|
|
5643
|
-
const urlA = new URL(href, TEST_BASE_A).toString();
|
|
5644
|
-
const urlB = new URL(href, TEST_BASE_B).toString();
|
|
5645
|
-
return !urlA.startsWith(TEST_BASE_A) || !urlB.startsWith(TEST_BASE_B);
|
|
5646
|
-
} catch {
|
|
5647
|
-
return false;
|
|
5648
|
-
}
|
|
5649
|
-
}
|
|
5650
7619
|
function isValidMimeType(mediaType) {
|
|
5651
7620
|
const mimeTypePattern = /^[a-zA-Z][a-zA-Z0-9!#$&\-^_.]*\/[a-zA-Z0-9][a-zA-Z0-9!#$&\-^_.+]*(?:\s*;\s*[a-zA-Z0-9-]+=[^;]+)?$/;
|
|
5652
7621
|
if (!mimeTypePattern.test(mediaType)) {
|
|
@@ -5714,9 +7683,11 @@ function isValidW3CDateFormat(dateStr) {
|
|
|
5714
7683
|
var ResourceRegistry = class {
|
|
5715
7684
|
resources;
|
|
5716
7685
|
ids;
|
|
7686
|
+
svgSymbolIds;
|
|
5717
7687
|
constructor() {
|
|
5718
7688
|
this.resources = /* @__PURE__ */ new Map();
|
|
5719
7689
|
this.ids = /* @__PURE__ */ new Map();
|
|
7690
|
+
this.svgSymbolIds = /* @__PURE__ */ new Map();
|
|
5720
7691
|
}
|
|
5721
7692
|
/**
|
|
5722
7693
|
* Register a resource from manifest
|
|
@@ -5778,6 +7749,21 @@ var ResourceRegistry = class {
|
|
|
5778
7749
|
}
|
|
5779
7750
|
return -1;
|
|
5780
7751
|
}
|
|
7752
|
+
/**
|
|
7753
|
+
* Register an ID as belonging to an SVG symbol element
|
|
7754
|
+
*/
|
|
7755
|
+
registerSVGSymbolID(resourceURL, id) {
|
|
7756
|
+
if (!this.svgSymbolIds.has(resourceURL)) {
|
|
7757
|
+
this.svgSymbolIds.set(resourceURL, /* @__PURE__ */ new Set());
|
|
7758
|
+
}
|
|
7759
|
+
this.svgSymbolIds.get(resourceURL)?.add(id);
|
|
7760
|
+
}
|
|
7761
|
+
/**
|
|
7762
|
+
* Check if an ID in a resource belongs to an SVG symbol element
|
|
7763
|
+
*/
|
|
7764
|
+
isSVGSymbolID(resourceURL, id) {
|
|
7765
|
+
return this.svgSymbolIds.get(resourceURL)?.has(id) ?? false;
|
|
7766
|
+
}
|
|
5781
7767
|
/**
|
|
5782
7768
|
* Get all resources
|
|
5783
7769
|
*/
|
|
@@ -5827,6 +7813,7 @@ var ReferenceValidator = class {
|
|
|
5827
7813
|
for (const reference of this.references) {
|
|
5828
7814
|
this.validateReference(context, reference);
|
|
5829
7815
|
}
|
|
7816
|
+
this.checkRemoteResources(context);
|
|
5830
7817
|
this.checkUndeclaredResources(context);
|
|
5831
7818
|
this.checkReadingOrder(context);
|
|
5832
7819
|
this.checkNonLinearReachability(context);
|
|
@@ -5836,14 +7823,6 @@ var ReferenceValidator = class {
|
|
|
5836
7823
|
*/
|
|
5837
7824
|
validateReference(context, reference) {
|
|
5838
7825
|
const url = reference.url.trim();
|
|
5839
|
-
if (isMalformedURL(url)) {
|
|
5840
|
-
pushMessage(context.messages, {
|
|
5841
|
-
id: MessageId.RSC_020,
|
|
5842
|
-
message: `Malformed URL: ${url}`,
|
|
5843
|
-
location: reference.location
|
|
5844
|
-
});
|
|
5845
|
-
return;
|
|
5846
|
-
}
|
|
5847
7826
|
if (isDataURL(url)) {
|
|
5848
7827
|
if (this.version.startsWith("3.")) {
|
|
5849
7828
|
const forbiddenDataUrlTypes = [
|
|
@@ -5858,10 +7837,35 @@ var ReferenceValidator = class {
|
|
|
5858
7837
|
message: "Data URLs are not allowed in this context",
|
|
5859
7838
|
location: reference.location
|
|
5860
7839
|
});
|
|
7840
|
+
} else {
|
|
7841
|
+
const fallbackCheckedTypes = [
|
|
7842
|
+
"image" /* IMAGE */,
|
|
7843
|
+
"audio" /* AUDIO */,
|
|
7844
|
+
"video" /* VIDEO */,
|
|
7845
|
+
"generic" /* GENERIC */
|
|
7846
|
+
];
|
|
7847
|
+
if (fallbackCheckedTypes.includes(reference.type) && !reference.hasIntrinsicFallback) {
|
|
7848
|
+
const dataUrlMimeType = this.extractDataURLMimeType(url);
|
|
7849
|
+
if (dataUrlMimeType && !isCoreMediaType(dataUrlMimeType)) {
|
|
7850
|
+
pushMessage(context.messages, {
|
|
7851
|
+
id: MessageId.RSC_032,
|
|
7852
|
+
message: `Fallback must be provided for foreign resources, but found none for data URL of type "${dataUrlMimeType}"`,
|
|
7853
|
+
location: reference.location
|
|
7854
|
+
});
|
|
7855
|
+
}
|
|
7856
|
+
}
|
|
5861
7857
|
}
|
|
5862
7858
|
}
|
|
5863
7859
|
return;
|
|
5864
7860
|
}
|
|
7861
|
+
if (isMalformedURL(url)) {
|
|
7862
|
+
pushMessage(context.messages, {
|
|
7863
|
+
id: MessageId.RSC_020,
|
|
7864
|
+
message: `Malformed URL: ${url}`,
|
|
7865
|
+
location: reference.location
|
|
7866
|
+
});
|
|
7867
|
+
return;
|
|
7868
|
+
}
|
|
5865
7869
|
if (isFileURL(url)) {
|
|
5866
7870
|
pushMessage(context.messages, {
|
|
5867
7871
|
id: MessageId.RSC_030,
|
|
@@ -5873,6 +7877,13 @@ var ReferenceValidator = class {
|
|
|
5873
7877
|
const resourcePath = reference.targetResource || parseURL(url).resource;
|
|
5874
7878
|
const fragment = reference.fragment ?? parseURL(url).fragment;
|
|
5875
7879
|
const hasFragment = fragment !== void 0 && fragment !== "";
|
|
7880
|
+
if (!isRemoteURL(url) && url.includes("?")) {
|
|
7881
|
+
pushMessage(context.messages, {
|
|
7882
|
+
id: MessageId.RSC_033,
|
|
7883
|
+
message: `Relative URL strings must not have a query component: "${url}"`,
|
|
7884
|
+
location: reference.location
|
|
7885
|
+
});
|
|
7886
|
+
}
|
|
5876
7887
|
if (!isRemoteURL(url)) {
|
|
5877
7888
|
this.validateLocalReference(context, reference, resourcePath);
|
|
5878
7889
|
} else {
|
|
@@ -5888,7 +7899,7 @@ var ReferenceValidator = class {
|
|
|
5888
7899
|
validateLocalReference(context, reference, resourcePath) {
|
|
5889
7900
|
if (hasAbsolutePath(resourcePath)) {
|
|
5890
7901
|
pushMessage(context.messages, {
|
|
5891
|
-
id: MessageId.
|
|
7902
|
+
id: MessageId.RSC_026,
|
|
5892
7903
|
message: "Absolute paths are not allowed in EPUB",
|
|
5893
7904
|
location: reference.location
|
|
5894
7905
|
});
|
|
@@ -5900,10 +7911,16 @@ var ReferenceValidator = class {
|
|
|
5900
7911
|
];
|
|
5901
7912
|
if (hasParentDirectoryReference(reference.url) && forbiddenParentDirTypes.includes(reference.type)) {
|
|
5902
7913
|
pushMessage(context.messages, {
|
|
5903
|
-
id: MessageId.
|
|
7914
|
+
id: MessageId.RSC_026,
|
|
5904
7915
|
message: "Parent directory references (..) are not allowed",
|
|
5905
7916
|
location: reference.location
|
|
5906
7917
|
});
|
|
7918
|
+
} else if (!hasAbsolutePath(resourcePath) && !hasParentDirectoryReference(reference.url) && checkUrlLeaking(reference.url)) {
|
|
7919
|
+
pushMessage(context.messages, {
|
|
7920
|
+
id: MessageId.RSC_026,
|
|
7921
|
+
message: `URL "${reference.url}" leaks outside the container`,
|
|
7922
|
+
location: reference.location
|
|
7923
|
+
});
|
|
5907
7924
|
}
|
|
5908
7925
|
if (!this.registry.hasResource(resourcePath)) {
|
|
5909
7926
|
const fileExistsInContainer = context.files.has(resourcePath);
|
|
@@ -5928,14 +7945,15 @@ var ReferenceValidator = class {
|
|
|
5928
7945
|
return;
|
|
5929
7946
|
}
|
|
5930
7947
|
const resource = this.registry.getResource(resourcePath);
|
|
5931
|
-
|
|
7948
|
+
const isHyperlinkLike = reference.type === "hyperlink" /* HYPERLINK */ || reference.type === "nav-toc-link" /* NAV_TOC_LINK */ || reference.type === "nav-pagelist-link" /* NAV_PAGELIST_LINK */;
|
|
7949
|
+
if (this.version.startsWith("3") && isHyperlinkLike && !resource?.inSpine) {
|
|
5932
7950
|
pushMessage(context.messages, {
|
|
5933
7951
|
id: MessageId.RSC_011,
|
|
5934
7952
|
message: "Hyperlinks must reference spine items",
|
|
5935
7953
|
location: reference.location
|
|
5936
7954
|
});
|
|
5937
7955
|
}
|
|
5938
|
-
if (
|
|
7956
|
+
if (isHyperlinkLike || reference.type === "overlay-text-link" /* OVERLAY_TEXT_LINK */) {
|
|
5939
7957
|
const targetMimeType = resource?.mimeType;
|
|
5940
7958
|
if (targetMimeType && !this.isBlessedItemType(targetMimeType, context.version) && !this.isDeprecatedBlessedItemType(targetMimeType) && !resource.hasCoreMediaTypeFallback) {
|
|
5941
7959
|
pushMessage(context.messages, {
|
|
@@ -5945,7 +7963,13 @@ var ReferenceValidator = class {
|
|
|
5945
7963
|
});
|
|
5946
7964
|
}
|
|
5947
7965
|
}
|
|
5948
|
-
|
|
7966
|
+
const fallbackCheckedTypes = [
|
|
7967
|
+
"image" /* IMAGE */,
|
|
7968
|
+
"audio" /* AUDIO */,
|
|
7969
|
+
"video" /* VIDEO */,
|
|
7970
|
+
"generic" /* GENERIC */
|
|
7971
|
+
];
|
|
7972
|
+
if (resource && fallbackCheckedTypes.includes(reference.type) && !isCoreMediaType(resource.mimeType) && !resource.hasCoreMediaTypeFallback && !reference.hasIntrinsicFallback) {
|
|
5949
7973
|
pushMessage(context.messages, {
|
|
5950
7974
|
id: MessageId.RSC_032,
|
|
5951
7975
|
message: `Fallback must be provided for foreign resources, but found none for resource "${resourcePath}" of type "${resource.mimeType}"`,
|
|
@@ -5958,20 +7982,24 @@ var ReferenceValidator = class {
|
|
|
5958
7982
|
*/
|
|
5959
7983
|
validateRemoteReference(context, reference) {
|
|
5960
7984
|
const url = reference.url;
|
|
5961
|
-
if (isHTTP(url) && !isHTTPS(url)) {
|
|
5962
|
-
pushMessage(context.messages, {
|
|
5963
|
-
id: MessageId.RSC_031,
|
|
5964
|
-
message: "Remote resources must use HTTPS",
|
|
5965
|
-
location: reference.location
|
|
5966
|
-
});
|
|
5967
|
-
}
|
|
5968
7985
|
if (isPublicationResourceReference(reference.type)) {
|
|
5969
|
-
|
|
7986
|
+
if (isHTTP(url) && !isHTTPS(url)) {
|
|
7987
|
+
pushMessage(context.messages, {
|
|
7988
|
+
id: MessageId.RSC_031,
|
|
7989
|
+
message: "Remote resources must use HTTPS",
|
|
7990
|
+
location: reference.location
|
|
7991
|
+
});
|
|
7992
|
+
}
|
|
7993
|
+
const allowedRemoteRefTypes = /* @__PURE__ */ new Set([
|
|
5970
7994
|
"audio" /* AUDIO */,
|
|
5971
7995
|
"video" /* VIDEO */,
|
|
5972
7996
|
"font" /* FONT */
|
|
5973
7997
|
]);
|
|
5974
|
-
|
|
7998
|
+
const targetResource = reference.targetResource || url;
|
|
7999
|
+
const resource = this.registry.getResource(targetResource);
|
|
8000
|
+
const isAllowedByRefType = allowedRemoteRefTypes.has(reference.type);
|
|
8001
|
+
const isAllowedByMimeType = resource && this.isRemoteResourceType(resource.mimeType);
|
|
8002
|
+
if (!isAllowedByRefType && !isAllowedByMimeType) {
|
|
5975
8003
|
pushMessage(context.messages, {
|
|
5976
8004
|
id: MessageId.RSC_006,
|
|
5977
8005
|
message: "Remote resources are only allowed for audio, video, and fonts",
|
|
@@ -5979,8 +8007,7 @@ var ReferenceValidator = class {
|
|
|
5979
8007
|
});
|
|
5980
8008
|
return;
|
|
5981
8009
|
}
|
|
5982
|
-
|
|
5983
|
-
if (!this.registry.hasResource(targetResource)) {
|
|
8010
|
+
if (!resource) {
|
|
5984
8011
|
pushMessage(context.messages, {
|
|
5985
8012
|
id: MessageId.RSC_008,
|
|
5986
8013
|
message: `Referenced resource "${targetResource}" is not declared in the OPF manifest`,
|
|
@@ -6013,9 +8040,9 @@ var ReferenceValidator = class {
|
|
|
6013
8040
|
});
|
|
6014
8041
|
return;
|
|
6015
8042
|
}
|
|
6016
|
-
if (resource?.mimeType === "image/svg+xml") {
|
|
8043
|
+
if (resource?.mimeType === "image/svg+xml" && reference.type === "hyperlink" /* HYPERLINK */) {
|
|
6017
8044
|
const hasSVGView = fragment.includes("svgView(") || fragment.includes("viewBox(");
|
|
6018
|
-
if (hasSVGView
|
|
8045
|
+
if (hasSVGView) {
|
|
6019
8046
|
pushMessage(context.messages, {
|
|
6020
8047
|
id: MessageId.RSC_014,
|
|
6021
8048
|
message: "SVG view fragments can only be referenced from SVG documents",
|
|
@@ -6023,11 +8050,51 @@ var ReferenceValidator = class {
|
|
|
6023
8050
|
});
|
|
6024
8051
|
}
|
|
6025
8052
|
}
|
|
6026
|
-
if (
|
|
8053
|
+
if (reference.type === "hyperlink" /* HYPERLINK */) {
|
|
8054
|
+
if (this.registry.isSVGSymbolID(resourcePath, fragment)) {
|
|
8055
|
+
pushMessage(context.messages, {
|
|
8056
|
+
id: MessageId.RSC_014,
|
|
8057
|
+
message: `Fragment identifier "${fragment}" defines an incompatible resource type (SVG symbol)`,
|
|
8058
|
+
location: reference.location
|
|
8059
|
+
});
|
|
8060
|
+
}
|
|
8061
|
+
}
|
|
8062
|
+
const parsedMimeTypes = ["application/xhtml+xml", "image/svg+xml"];
|
|
8063
|
+
if (resource && parsedMimeTypes.includes(resource.mimeType) && !isRemoteURL(resourcePath) && !fragment.includes("svgView(") && reference.type !== "svg-symbol" /* SVG_SYMBOL */) {
|
|
8064
|
+
if (!this.registry.hasID(resourcePath, fragment)) {
|
|
8065
|
+
pushMessage(context.messages, {
|
|
8066
|
+
id: MessageId.RSC_012,
|
|
8067
|
+
message: `Fragment identifier not found: #${fragment}`,
|
|
8068
|
+
location: reference.location
|
|
8069
|
+
});
|
|
8070
|
+
}
|
|
8071
|
+
}
|
|
8072
|
+
}
|
|
8073
|
+
/**
|
|
8074
|
+
* Check non-spine remote resources that have non-standard types.
|
|
8075
|
+
* Fires RSC-006 for remote items that aren't audio/video/font types
|
|
8076
|
+
* and aren't referenced as audio/video/font by content documents.
|
|
8077
|
+
* This mirrors Java's checkItemAfterResourceValidation behavior.
|
|
8078
|
+
*/
|
|
8079
|
+
checkRemoteResources(context) {
|
|
8080
|
+
if (!this.version.startsWith("3")) return;
|
|
8081
|
+
const referencedAsAllowed = /* @__PURE__ */ new Set();
|
|
8082
|
+
for (const ref of this.references) {
|
|
8083
|
+
if (isRemoteURL(ref.url) || isRemoteURL(ref.targetResource)) {
|
|
8084
|
+
if (ref.type === "font" /* FONT */ || ref.type === "audio" /* AUDIO */ || ref.type === "video" /* VIDEO */) {
|
|
8085
|
+
referencedAsAllowed.add(ref.targetResource);
|
|
8086
|
+
}
|
|
8087
|
+
}
|
|
8088
|
+
}
|
|
8089
|
+
for (const resource of this.registry.getAllResources()) {
|
|
8090
|
+
if (!isRemoteURL(resource.url)) continue;
|
|
8091
|
+
if (resource.inSpine) continue;
|
|
8092
|
+
if (this.isRemoteResourceType(resource.mimeType)) continue;
|
|
8093
|
+
if (referencedAsAllowed.has(resource.url)) continue;
|
|
6027
8094
|
pushMessage(context.messages, {
|
|
6028
|
-
id: MessageId.
|
|
6029
|
-
message: `
|
|
6030
|
-
location:
|
|
8095
|
+
id: MessageId.RSC_006,
|
|
8096
|
+
message: `Remote resource reference is not allowed; resource "${resource.url}" must be located in the EPUB container`,
|
|
8097
|
+
location: { path: resource.url }
|
|
6031
8098
|
});
|
|
6032
8099
|
}
|
|
6033
8100
|
}
|
|
@@ -6051,9 +8118,9 @@ var ReferenceValidator = class {
|
|
|
6051
8118
|
for (const resource of this.registry.getAllResources()) {
|
|
6052
8119
|
if (resource.inSpine) continue;
|
|
6053
8120
|
if (referencedResources.has(resource.url)) continue;
|
|
6054
|
-
if (resource.
|
|
6055
|
-
if (resource.
|
|
6056
|
-
if (resource.
|
|
8121
|
+
if (resource.isNav) continue;
|
|
8122
|
+
if (resource.isNcx) continue;
|
|
8123
|
+
if (resource.isCoverImage) continue;
|
|
6057
8124
|
pushMessage(context.messages, {
|
|
6058
8125
|
id: MessageId.OPF_097,
|
|
6059
8126
|
message: `Resource declared in manifest but not referenced: ${resource.url}`,
|
|
@@ -6114,7 +8181,7 @@ var ReferenceValidator = class {
|
|
|
6114
8181
|
const opfDir = opfPath.includes("/") ? opfPath.substring(0, opfPath.lastIndexOf("/")) : "";
|
|
6115
8182
|
const hyperlinkTargets = /* @__PURE__ */ new Set();
|
|
6116
8183
|
for (const ref of this.references) {
|
|
6117
|
-
if (ref.type === "hyperlink" /* HYPERLINK */) {
|
|
8184
|
+
if (ref.type === "hyperlink" /* HYPERLINK */ || ref.type === "nav-toc-link" /* NAV_TOC_LINK */ || ref.type === "nav-pagelist-link" /* NAV_PAGELIST_LINK */) {
|
|
6118
8185
|
hyperlinkTargets.add(ref.targetResource);
|
|
6119
8186
|
}
|
|
6120
8187
|
}
|
|
@@ -6151,6 +8218,13 @@ var ReferenceValidator = class {
|
|
|
6151
8218
|
isDeprecatedBlessedItemType(mimeType) {
|
|
6152
8219
|
return mimeType === "text/x-oeb1-document" || mimeType === "text/html";
|
|
6153
8220
|
}
|
|
8221
|
+
extractDataURLMimeType(url) {
|
|
8222
|
+
const match = /^data:([^;,]+)/.exec(url);
|
|
8223
|
+
return match?.[1]?.trim().toLowerCase() ?? "text/plain";
|
|
8224
|
+
}
|
|
8225
|
+
isRemoteResourceType(mimeType) {
|
|
8226
|
+
return mimeType.startsWith("audio/") || mimeType.startsWith("video/") || mimeType.startsWith("font/") || mimeType === "application/font-sfnt" || mimeType === "application/font-woff" || mimeType === "application/font-woff2" || mimeType === "application/vnd.ms-opentype";
|
|
8227
|
+
}
|
|
6154
8228
|
};
|
|
6155
8229
|
var COMPRESSED_SCHEMAS = {
|
|
6156
8230
|
"applications.rng": "H4sIAJC0cWkCA81aX2/bNhB/76fgXKBPtb1u6zC4ToqgSdMC7TqsGbZXWqIlohQpUFQS99PvSOq/RVmSZTdBHhLy/vF49+PxqPXbx4iheyITKvjF7NXi5xki3BM+5cHF7J+79/M/Zm8vn60DiaMISwTUPFnhi1moVLxaLiVh+JEHCyGDJU+WnohirOiGMqp2S8y5UPCvgBkj2XC7eBMlU0+lkhjay2cIrX+az9HzKX7QfG4E+mRLOUEcR+RiBsZGgi+wUjJZUK6IxJ6aIRjeANHFzAwxgu+JsQbY8coXXhoRbld1if6++XT1H/rzFn31QhJhtBUSfbj7/Am9XqF/yQZdxTGjniFG7wnWq0u0pPLn+XrZlGp1Tb32FvOvfJ+a3UFKoHfGG+gKvEE3qSKJy7DSLXYAhkSs5zHLB2BIkm2bmz0B3PALivGGkdmykLFsChkg1Zc4CCaUF1LfJ3wiYbGIBSSYW9p6WXfpemnD9EDEChWC1K5wdRhUhLqxqKe25sY5Quomm4dwMvQrsK/G6IoqrcXEXaG8TR8QeqGgXhF6MHCPWUouATxAtv27ORczrNf8qOaCs52LaotZ4hJR2buqflhMbvYAV5bR6nDidU6AbhjRwytU8PT1X1PJOM+1+WQKF2QJ5lj/BzNbLt5S9115TbZ72bnQ9oWnXFE234qU++cIiRwjHA75y06XHsno+7qkLt7tE5wq4fJIhHmKmWs2hAQ7h6M0HkBpMuiUvmVigxkq1CUI22NZZxgcecR6NUEPVIUoR5wcuE5yDFIepyoS/lQHVhITxqAE8b5NJFBHgodjcAWj36eyUuO5/EZ2OlieznGYh9SQ07DcPyeEcYXIY0w8OMKQjxVGaheTFSo4++ZuU5U7e7ngTiDXp597zpnWqXROQblLnZMcliWp55r2iUcjN5gkBEsvPMGxU0kTx759LSggJBDmPspvPvngCpVS+u7hnt7pTuOToGwj+13XhyqVGerrj1b5bp+I7dYZaB0xr+1xBZiGdk/fcdoJHoT0nZNeiHX5DHfmE4RoDSMdjr8DJEFii240LQIFCk4x+L2nUsEZjYB9IzCsYAV3+VJc391pMcG9N4bYmecduxM4Kw3egVWxJPdUpM69aYWO6r43y7mRexZLEcBdHvYLigdn2QZEQIM+cl83AYRcoRdMvcl5XwTqjWtPspqkoa3cjf3qo3bAFeZRzqtXz3YiE3nVA7kpfb3M7OnpFCvQXSi1n8QVCzprirqahdnO8RVKQ1qEH4fKamZGR1UlKV7QSBefVC1yzRtc26ED7FIw4mKtB/MxpVWrjx14UZ1sM15XP4stE1gtAK7nnAQQ6VCWZYYPz7lypxwWlVMH7IlFAsXfMbbYBHNVmGZ2EYcSJ+0lpu7yvTN9UoBq6B56BOlSLeue6H5ihBWCypZ0tTB1dpZayhrYRsO+dXX0OlD4+hQzEXQC3bUhQRvx+BIKXKh2PAC7lwiaoQ+U++LB4p4V1B/1LH1vzMvs7EK8jGQqvKuJOxXaVZUsRDy8LTkWnjL3D0QmzIhU+6zTIdO+PxwgUJnrKC2B6EQVdS0euwECwOih5/3TpLqhP5zm1cw9IB2CPe1M8TshGJw2NpM1df881tS9s9gY0pXDhmCqDK4ImzZ/xyYdo0mlNdIv5XwqDeDuhjIGUqTxUKYxFmoeOByGspnAGcEzoqCS2KdwJo7wh24WjnCJsvk0mE2SSq3bt1gk+sJrEvQ0mFxJWncafSdSfJGfhXQkPqMVpLL691kOSamiZOJJGqt5ksaxkGWLr1304K7jECAu0fUQDOdO6EZhRBWJLBQz2h+IGe0Pw2BIJwrD/GQgXMj6oRhcIJV27hjYOYbPNAdHQqTmN/g1lNn6bATgjUGtMd45A3IVcT5BhYb5PbZ3KkvvSGK47wGd+Vbkeges1EO3Esch9aBhp3PaCuqf15a+d25ndjYX5qKbKs2r7inuqpOvp37XPtOaTnsDrCpZhIQGYcfr2SBhD9RX4VQvhpC1kzzqtazW9dJfnXV2erTiACLjqN5Ti9scRlUnz2JTJY+7MUxJDL0ZLCGqh+BZI69Gt7qOLaL24HWAigG9sVaYOtQiIwoeQ7sfA/KP7eB95gufX8PzKTwuftQ9P+jwwXjWI7OSBjTJLEP/LllmamebLKOZrE9Wk3eyRllVy1k7ZW33Rt3Y1VRz+AgAdho+AojSRKEN9HfhhRfRLdIWmo9SUFbmvLFvzGizQzhJoI1mPs3Mv94cfamfsPW27+Gn2nurRflejiepftLf1e6b5+7NVSHjgPiauc6i0mzqMqM11WWmI//AySJMRtAfYTKG3giTW9uFMDnNVAhTl/c0Wnn2XX7olWefa7r8re/MAC+5SutuQA5f7TV0htWX4S/HCvj1WAG/HSvg9bECfj9WgDkQDghxx9B6mX0Adfnsf1i38T4sMgAA",
|
|
@@ -6665,11 +8739,15 @@ var EpubCheck = class _EpubCheck {
|
|
|
6665
8739
|
const manifestById = new Map(packageDoc.manifest.map((item) => [item.id, item]));
|
|
6666
8740
|
for (const item of packageDoc.manifest) {
|
|
6667
8741
|
const fullPath = resolveManifestHref(opfDir, item.href);
|
|
8742
|
+
const properties = item.properties ?? [];
|
|
6668
8743
|
registry.registerResource({
|
|
6669
8744
|
url: fullPath,
|
|
6670
8745
|
mimeType: item.mediaType,
|
|
6671
8746
|
inSpine: spineIdrefs.has(item.id),
|
|
6672
8747
|
hasCoreMediaTypeFallback: this.hasCMTFallback(item.id, manifestById),
|
|
8748
|
+
isNav: properties.includes("nav"),
|
|
8749
|
+
isCoverImage: properties.includes("cover-image"),
|
|
8750
|
+
isNcx: item.mediaType === "application/x-dtbncx+xml",
|
|
6673
8751
|
ids: /* @__PURE__ */ new Set()
|
|
6674
8752
|
});
|
|
6675
8753
|
}
|
|
@@ -6685,7 +8763,7 @@ var EpubCheck = class _EpubCheck {
|
|
|
6685
8763
|
visited.add(currentId);
|
|
6686
8764
|
const item = manifestById.get(currentId);
|
|
6687
8765
|
if (!item) return false;
|
|
6688
|
-
if (
|
|
8766
|
+
if (isCoreMediaType(item.mediaType)) return true;
|
|
6689
8767
|
currentId = item.fallback;
|
|
6690
8768
|
}
|
|
6691
8769
|
return false;
|