@likecoin/epubcheck-ts 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1666,144 +1666,6 @@ var CSSValidator = class {
1666
1666
  }
1667
1667
  };
1668
1668
 
1669
- // src/vocab/epub-ssv.ts
1670
- var EPUB_SSV_DEPRECATED = /* @__PURE__ */ new Set([
1671
- "annoref",
1672
- "annotation",
1673
- "biblioentry",
1674
- "bridgehead",
1675
- "endnote",
1676
- "help",
1677
- "marginalia",
1678
- "note",
1679
- "rearnote",
1680
- "rearnotes",
1681
- "sidebar",
1682
- "subchapter",
1683
- "warning"
1684
- ]);
1685
- var EPUB_SSV_DISALLOWED_ON_CONTENT = /* @__PURE__ */ new Set([
1686
- "aside",
1687
- "figure",
1688
- "list",
1689
- "list-item",
1690
- "table",
1691
- "table-cell",
1692
- "table-row"
1693
- ]);
1694
- var EPUB_SSV_ALL = /* @__PURE__ */ new Set([
1695
- ...EPUB_SSV_DEPRECATED,
1696
- ...EPUB_SSV_DISALLOWED_ON_CONTENT,
1697
- "abstract",
1698
- "acknowledgments",
1699
- "afterword",
1700
- "appendix",
1701
- "assessment",
1702
- "assessments",
1703
- "backlink",
1704
- "backmatter",
1705
- "balloon",
1706
- "bibliography",
1707
- "biblioref",
1708
- "bodymatter",
1709
- "case-study",
1710
- "chapter",
1711
- "colophon",
1712
- "concluding-sentence",
1713
- "conclusion",
1714
- "contributors",
1715
- "copyright-page",
1716
- "cover",
1717
- "covertitle",
1718
- "credit",
1719
- "credits",
1720
- "dedication",
1721
- "division",
1722
- "endnotes",
1723
- "epigraph",
1724
- "epilogue",
1725
- "errata",
1726
- "fill-in-the-blank-problem",
1727
- "footnote",
1728
- "footnotes",
1729
- "foreword",
1730
- "frontmatter",
1731
- "fulltitle",
1732
- "general-problem",
1733
- "glossary",
1734
- "glossdef",
1735
- "glossref",
1736
- "glossterm",
1737
- "halftitle",
1738
- "halftitlepage",
1739
- "imprimatur",
1740
- "imprint",
1741
- "index",
1742
- "index-editor-note",
1743
- "index-entry",
1744
- "index-entry-list",
1745
- "index-group",
1746
- "index-headnotes",
1747
- "index-legend",
1748
- "index-locator",
1749
- "index-locator-list",
1750
- "index-locator-range",
1751
- "index-term",
1752
- "index-term-categories",
1753
- "index-term-category",
1754
- "index-xref-preferred",
1755
- "index-xref-related",
1756
- "introduction",
1757
- "keyword",
1758
- "keywords",
1759
- "label",
1760
- "landmarks",
1761
- "learning-objective",
1762
- "learning-objectives",
1763
- "learning-outcome",
1764
- "learning-outcomes",
1765
- "learning-resource",
1766
- "learning-resources",
1767
- "learning-standard",
1768
- "learning-standards",
1769
- "loa",
1770
- "loi",
1771
- "lot",
1772
- "lov",
1773
- "match-problem",
1774
- "multiple-choice-problem",
1775
- "noteref",
1776
- "notice",
1777
- "ordinal",
1778
- "other-credits",
1779
- "page-list",
1780
- "pagebreak",
1781
- "panel",
1782
- "panel-group",
1783
- "part",
1784
- "practice",
1785
- "practices",
1786
- "preamble",
1787
- "preface",
1788
- "prologue",
1789
- "pullquote",
1790
- "qna",
1791
- "question",
1792
- "referrer",
1793
- "revision-history",
1794
- "seriespage",
1795
- "sound-area",
1796
- "subtitle",
1797
- "tip",
1798
- "title",
1799
- "titlepage",
1800
- "toc",
1801
- "toc-brief",
1802
- "topic-sentence",
1803
- "true-false-problem",
1804
- "volume"
1805
- ]);
1806
-
1807
1669
  // src/references/url.ts
1808
1670
  function parseURL(urlString) {
1809
1671
  const hashIndex = urlString.indexOf("#");
@@ -1832,12 +1694,13 @@ function isDataURL(url) {
1832
1694
  function isFileURL(url) {
1833
1695
  return url.startsWith("file:");
1834
1696
  }
1697
+ function isRelativeURL(url) {
1698
+ const regex = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
1699
+ return regex.exec(url) === null;
1700
+ }
1835
1701
  function hasAbsolutePath(url) {
1836
1702
  return url.startsWith("/");
1837
1703
  }
1838
- function hasParentDirectoryReference(url) {
1839
- return url.includes("..");
1840
- }
1841
1704
  function isMalformedURL(url) {
1842
1705
  if (!url.trim()) return true;
1843
1706
  if (/[\s<>]/.test(url)) return true;
@@ -1852,12 +1715,14 @@ function isHTTP(url) {
1852
1715
  function isRemoteURL(url) {
1853
1716
  return isHTTP(url) || isHTTPS(url);
1854
1717
  }
1855
- function checkUrlLeaking(href) {
1718
+ function checkUrlLeaking(href, resourcePath) {
1856
1719
  const TEST_BASE_A = "https://a.example.org/A/";
1857
1720
  const TEST_BASE_B = "https://b.example.org/B/";
1858
1721
  try {
1859
- const urlA = new URL(href, TEST_BASE_A).toString();
1860
- const urlB = new URL(href, TEST_BASE_B).toString();
1722
+ const baseA = resourcePath ? new URL(resourcePath, TEST_BASE_A).toString() : TEST_BASE_A;
1723
+ const baseB = resourcePath ? new URL(resourcePath, TEST_BASE_B).toString() : TEST_BASE_B;
1724
+ const urlA = new URL(href, baseA).toString();
1725
+ const urlB = new URL(href, baseB).toString();
1861
1726
  return !urlA.startsWith(TEST_BASE_A) || !urlB.startsWith(TEST_BASE_B);
1862
1727
  } catch {
1863
1728
  return false;
@@ -2029,6 +1894,8 @@ function parseOPF(xml) {
2029
1894
  }
2030
1895
  }
2031
1896
  }
1897
+ const nsRegex = /<package[^>]*\sxmlns=["']([^"']+)["']/;
1898
+ const isLegacyOebps12 = nsRegex.exec(xml)?.[1] === "http://openebook.org/namespaces/oeb-package/1.0/";
2032
1899
  const prefixes = parsePrefixes(xml);
2033
1900
  const dirRegex = /<package[^>]*\sdir=["']([^"']+)["']/;
2034
1901
  const dirMatch = dirRegex.exec(xml);
@@ -2067,6 +1934,9 @@ function parseOPF(xml) {
2067
1934
  if (!versionDeclared) {
2068
1935
  result.versionDeclared = false;
2069
1936
  }
1937
+ if (isLegacyOebps12) {
1938
+ result.isLegacyOebps12 = true;
1939
+ }
2070
1940
  if (Object.keys(prefixes).length > 0) {
2071
1941
  result.prefixes = prefixes;
2072
1942
  }
@@ -2157,7 +2027,7 @@ function parseDCElements(metadataXml) {
2157
2027
  }
2158
2028
  function parseMetaElements(metadataXml) {
2159
2029
  const elements = [];
2160
- const metaRegex = /<meta([^>]*property=["'][^"']+["'][^>]*)>([^<]*)<\/meta>/g;
2030
+ const metaRegex = /<(?:[A-Za-z_][\w.-]*:)?meta([^>]*property=["'][^"']+["'][^>]*)>([^<]*)<\/(?:[A-Za-z_][\w.-]*:)?meta>/g;
2161
2031
  let match;
2162
2032
  while ((match = metaRegex.exec(metadataXml)) !== null) {
2163
2033
  const attrsStr = match[1] ?? "";
@@ -2927,6 +2797,9 @@ var OPFValidator = class {
2927
2797
  }
2928
2798
  this.validatePackageAttributes(context, opfPath);
2929
2799
  this.validateMetadata(context, opfPath);
2800
+ if (this.packageDoc.version !== "2.0") {
2801
+ this.validateMetaPrefixes(context, opfPath, opfXml);
2802
+ }
2930
2803
  this.validateLinkElements(context, opfPath);
2931
2804
  this.validateManifest(context, opfPath);
2932
2805
  this.validateSpine(context, opfPath);
@@ -4120,6 +3993,7 @@ var OPFValidator = class {
4120
3993
  if (!this.packageDoc) return;
4121
3994
  const seenIds = /* @__PURE__ */ new Set();
4122
3995
  const seenHrefs = /* @__PURE__ */ new Set();
3996
+ const declaredPrefixes = this.packageDoc.prefixes ?? {};
4123
3997
  for (const item of this.packageDoc.manifest) {
4124
3998
  if (seenIds.has(item.id)) {
4125
3999
  pushMessage(context.messages, {
@@ -4163,7 +4037,7 @@ var OPFValidator = class {
4163
4037
  });
4164
4038
  }
4165
4039
  if (!item.href.startsWith("http") && !item.href.startsWith("mailto:")) {
4166
- const leaked = checkUrlLeaking(item.href);
4040
+ const leaked = checkUrlLeaking(item.href, opfPath);
4167
4041
  if (leaked) {
4168
4042
  pushMessage(context.messages, {
4169
4043
  id: MessageId.RSC_026,
@@ -4198,11 +4072,11 @@ var OPFValidator = class {
4198
4072
  if (DEPRECATED_MEDIA_TYPES.has(item.mediaType) || item.mediaType === "text/html") {
4199
4073
  if (this.packageDoc.version === "2.0" && item.mediaType === "text/html") {
4200
4074
  pushMessage(context.messages, {
4201
- id: MessageId.OPF_035,
4075
+ id: this.packageDoc.isLegacyOebps12 ? MessageId.OPF_038 : MessageId.OPF_035,
4202
4076
  message: `XHTML Content Document "${item.id}" is declared as "text/html"`,
4203
4077
  location: { path: opfPath }
4204
4078
  });
4205
- } else if (this.packageDoc.version === "2.0") {
4079
+ } else if (this.packageDoc.version === "2.0" && !this.packageDoc.isLegacyOebps12) {
4206
4080
  pushMessage(context.messages, {
4207
4081
  id: MessageId.OPF_037,
4208
4082
  message: `Found deprecated media-type "${item.mediaType}"`,
@@ -4210,6 +4084,13 @@ var OPFValidator = class {
4210
4084
  });
4211
4085
  }
4212
4086
  }
4087
+ if (this.packageDoc.version === "2.0" && this.packageDoc.isLegacyOebps12 && item.mediaType === "text/css" && !item.fallback) {
4088
+ pushMessage(context.messages, {
4089
+ id: MessageId.OPF_039,
4090
+ message: `Media type "${item.mediaType}" requires a fallback in legacy OEBPS 1.2 context`,
4091
+ location: { path: opfPath }
4092
+ });
4093
+ }
4213
4094
  const preferred = getPreferredMediaType(item.mediaType, fullPath);
4214
4095
  if (preferred !== null) {
4215
4096
  pushMessage(context.messages, {
@@ -4220,13 +4101,14 @@ var OPFValidator = class {
4220
4101
  }
4221
4102
  if (this.packageDoc.version !== "2.0" && item.properties) {
4222
4103
  for (const prop of item.properties) {
4223
- if (!ITEM_PROPERTIES.has(prop)) {
4224
- pushMessage(context.messages, {
4225
- id: MessageId.OPF_027,
4226
- message: `Undefined property: "${prop}"`,
4227
- location: { path: opfPath }
4228
- });
4229
- }
4104
+ if (ITEM_PROPERTIES.has(prop)) continue;
4105
+ const colon = prop.indexOf(":");
4106
+ if (colon > 0 && declaredPrefixes[prop.slice(0, colon)] !== void 0) continue;
4107
+ pushMessage(context.messages, {
4108
+ id: MessageId.OPF_027,
4109
+ message: `Undefined property: "${prop}"`,
4110
+ location: { path: opfPath }
4111
+ });
4230
4112
  }
4231
4113
  if (item.properties.includes("nav")) {
4232
4114
  if (item.mediaType !== "application/xhtml+xml") {
@@ -4389,6 +4271,42 @@ var OPFValidator = class {
4389
4271
  * emits one assertion failure per offending element (so two duplicate ids
4390
4272
  * produce two RSC-005 messages).
4391
4273
  */
4274
+ validateMetaPrefixes(context, opfPath, opfXml) {
4275
+ if (!this.packageDoc) return;
4276
+ const RESERVED = /* @__PURE__ */ new Set([
4277
+ "dcterms",
4278
+ "marc",
4279
+ "onix",
4280
+ "schema",
4281
+ "xsd",
4282
+ "a11y",
4283
+ "media",
4284
+ "rendition"
4285
+ ]);
4286
+ const declared = new Set(Object.keys(this.packageDoc.prefixes ?? {}));
4287
+ const reported = /* @__PURE__ */ new Set();
4288
+ const reportIfUndeclared = (prefix) => {
4289
+ if (!prefix || reported.has(prefix)) return;
4290
+ if (RESERVED.has(prefix) || declared.has(prefix)) return;
4291
+ reported.add(prefix);
4292
+ pushMessage(context.messages, {
4293
+ id: MessageId.OPF_028,
4294
+ message: `Undeclared prefix: "${prefix}"`,
4295
+ location: { path: opfPath }
4296
+ });
4297
+ };
4298
+ const stripped = stripXmlComments(opfXml);
4299
+ const attrRegex = /\b(?:property|scheme|rel)\s*=\s*["']([^"']+)["']/g;
4300
+ for (const match of stripped.matchAll(attrRegex)) {
4301
+ const value = match[1]?.trim();
4302
+ if (!value) continue;
4303
+ for (const token of value.split(/\s+/)) {
4304
+ const colon = token.indexOf(":");
4305
+ if (colon <= 0) continue;
4306
+ reportIfUndeclared(token.slice(0, colon));
4307
+ }
4308
+ }
4309
+ }
4392
4310
  validateOpfIdUniqueness(context, opfPath, opfXml) {
4393
4311
  const stripped = stripXmlComments(opfXml);
4394
4312
  const counts = /* @__PURE__ */ new Map();
@@ -4420,11 +4338,12 @@ var OPFValidator = class {
4420
4338
  for (const item of this.packageDoc.manifest) {
4421
4339
  const hrefBase = item.href.split("?")[0] ?? item.href;
4422
4340
  if (/^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(hrefBase)) continue;
4423
- manifestPaths.add(resolvePath(opfPath, hrefBase).normalize("NFC"));
4341
+ manifestPaths.add(resolvePath(opfPath, tryDecodeUriComponent(hrefBase)).normalize("NFC"));
4424
4342
  }
4425
4343
  const rootfilePaths = new Set(context.rootfiles.map((r) => r.path.normalize("NFC")));
4426
4344
  for (const path of context.files.keys()) {
4427
4345
  if (path === "mimetype") continue;
4346
+ if (path.endsWith("/")) continue;
4428
4347
  if (path.startsWith("META-INF/")) continue;
4429
4348
  if (rootfilePaths.has(path)) continue;
4430
4349
  if (manifestPaths.has(path)) continue;
@@ -5013,6 +4932,246 @@ function isValidW3CDateFormat(dateStr) {
5013
4932
  return false;
5014
4933
  }
5015
4934
 
4935
+ // src/references/types.ts
4936
+ var PUBLICATION_RESOURCE_TYPES = /* @__PURE__ */ new Set([
4937
+ "generic" /* GENERIC */,
4938
+ "stylesheet" /* STYLESHEET */,
4939
+ "font" /* FONT */,
4940
+ "image" /* IMAGE */,
4941
+ "audio" /* AUDIO */,
4942
+ "video" /* VIDEO */,
4943
+ "track" /* TRACK */,
4944
+ "media-overlay" /* MEDIA_OVERLAY */,
4945
+ "svg-symbol" /* SVG_SYMBOL */,
4946
+ "svg-paint" /* SVG_PAINT */,
4947
+ "svg-clip-path" /* SVG_CLIP_PATH */
4948
+ ]);
4949
+ function isPublicationResourceReference(type) {
4950
+ return PUBLICATION_RESOURCE_TYPES.has(type);
4951
+ }
4952
+
4953
+ // src/skm/validator.ts
4954
+ var OPS_NS_URI = "http://www.idpf.org/2007/ops";
4955
+ var SKM_NS = { ops: OPS_NS_URI };
4956
+ var SKMValidator = class {
4957
+ validate(context, path, refValidator) {
4958
+ const data = context.files.get(path);
4959
+ if (!data) return;
4960
+ const content = typeof data === "string" ? data : new TextDecoder().decode(data);
4961
+ let doc = null;
4962
+ try {
4963
+ doc = XmlDocument.fromString(content);
4964
+ } catch {
4965
+ pushMessage(context.messages, {
4966
+ id: MessageId.RSC_016,
4967
+ message: "Search Key Map document is not well-formed XML",
4968
+ location: { path }
4969
+ });
4970
+ return;
4971
+ }
4972
+ try {
4973
+ const root = doc.root;
4974
+ if (root.namespaceUri !== OPS_NS_URI || root.name !== "search-key-map") {
4975
+ pushMessage(context.messages, {
4976
+ id: MessageId.RSC_005,
4977
+ message: `Root element must be "search-key-map" in the OPS namespace`,
4978
+ location: { path, line: root.line }
4979
+ });
4980
+ return;
4981
+ }
4982
+ const groups = root.find("./ops:search-key-group", SKM_NS);
4983
+ if (groups.length === 0) {
4984
+ pushMessage(context.messages, {
4985
+ id: MessageId.RSC_005,
4986
+ message: 'A "search-key-map" must contain at least one "search-key-group"',
4987
+ location: { path, line: root.line }
4988
+ });
4989
+ }
4990
+ for (const group of groups) {
4991
+ const groupEl = group;
4992
+ const href = groupEl.attr("href")?.value;
4993
+ if (!href) {
4994
+ pushMessage(context.messages, {
4995
+ id: MessageId.RSC_005,
4996
+ message: 'The "href" attribute is required on "search-key-group"',
4997
+ location: { path, line: groupEl.line }
4998
+ });
4999
+ } else if (refValidator) {
5000
+ registerSkmRef(refValidator, path, href, groupEl.line);
5001
+ }
5002
+ const matches = groupEl.find("./ops:match", SKM_NS);
5003
+ if (matches.length === 0) {
5004
+ pushMessage(context.messages, {
5005
+ id: MessageId.RSC_005,
5006
+ message: 'A "search-key-group" must contain at least one "match"',
5007
+ location: { path, line: groupEl.line }
5008
+ });
5009
+ }
5010
+ for (const match of matches) {
5011
+ const matchEl = match;
5012
+ const matchHref = matchEl.attr("href")?.value;
5013
+ if (matchHref && refValidator) {
5014
+ registerSkmRef(refValidator, path, matchHref, matchEl.line);
5015
+ }
5016
+ }
5017
+ }
5018
+ } finally {
5019
+ doc.dispose();
5020
+ }
5021
+ }
5022
+ };
5023
+ function registerSkmRef(refValidator, path, href, line) {
5024
+ const parsed = parseURL(href);
5025
+ const targetResource = resolvePath(path, tryDecodeUriComponent(parsed.resource)).normalize("NFC");
5026
+ const location = line != null ? { path, line } : { path };
5027
+ const ref = {
5028
+ url: parsed.hasFragment ? `${targetResource}#${parsed.fragment ?? ""}` : targetResource,
5029
+ targetResource,
5030
+ type: "search-key" /* SEARCH_KEY */,
5031
+ location
5032
+ };
5033
+ if (parsed.fragment !== void 0) ref.fragment = parsed.fragment;
5034
+ refValidator.addReference(ref);
5035
+ }
5036
+
5037
+ // src/vocab/epub-ssv.ts
5038
+ var EPUB_SSV_DEPRECATED = /* @__PURE__ */ new Set([
5039
+ "annoref",
5040
+ "annotation",
5041
+ "biblioentry",
5042
+ "bridgehead",
5043
+ "endnote",
5044
+ "help",
5045
+ "marginalia",
5046
+ "note",
5047
+ "rearnote",
5048
+ "rearnotes",
5049
+ "sidebar",
5050
+ "subchapter",
5051
+ "warning"
5052
+ ]);
5053
+ var EPUB_SSV_DISALLOWED_ON_CONTENT = /* @__PURE__ */ new Set([
5054
+ "aside",
5055
+ "figure",
5056
+ "list",
5057
+ "list-item",
5058
+ "table",
5059
+ "table-cell",
5060
+ "table-row"
5061
+ ]);
5062
+ var EPUB_SSV_ALL = /* @__PURE__ */ new Set([
5063
+ ...EPUB_SSV_DEPRECATED,
5064
+ ...EPUB_SSV_DISALLOWED_ON_CONTENT,
5065
+ "abstract",
5066
+ "acknowledgments",
5067
+ "afterword",
5068
+ "appendix",
5069
+ "assessment",
5070
+ "assessments",
5071
+ "backlink",
5072
+ "backmatter",
5073
+ "balloon",
5074
+ "bibliography",
5075
+ "biblioref",
5076
+ "bodymatter",
5077
+ "case-study",
5078
+ "chapter",
5079
+ "colophon",
5080
+ "concluding-sentence",
5081
+ "conclusion",
5082
+ "contributors",
5083
+ "copyright-page",
5084
+ "cover",
5085
+ "covertitle",
5086
+ "credit",
5087
+ "credits",
5088
+ "dedication",
5089
+ "division",
5090
+ "endnotes",
5091
+ "epigraph",
5092
+ "epilogue",
5093
+ "errata",
5094
+ "fill-in-the-blank-problem",
5095
+ "footnote",
5096
+ "footnotes",
5097
+ "foreword",
5098
+ "frontmatter",
5099
+ "fulltitle",
5100
+ "general-problem",
5101
+ "glossary",
5102
+ "glossdef",
5103
+ "glossref",
5104
+ "glossterm",
5105
+ "halftitle",
5106
+ "halftitlepage",
5107
+ "imprimatur",
5108
+ "imprint",
5109
+ "index",
5110
+ "index-editor-note",
5111
+ "index-entry",
5112
+ "index-entry-list",
5113
+ "index-group",
5114
+ "index-headnotes",
5115
+ "index-legend",
5116
+ "index-locator",
5117
+ "index-locator-list",
5118
+ "index-locator-range",
5119
+ "index-term",
5120
+ "index-term-categories",
5121
+ "index-term-category",
5122
+ "index-xref-preferred",
5123
+ "index-xref-related",
5124
+ "introduction",
5125
+ "keyword",
5126
+ "keywords",
5127
+ "label",
5128
+ "landmarks",
5129
+ "learning-objective",
5130
+ "learning-objectives",
5131
+ "learning-outcome",
5132
+ "learning-outcomes",
5133
+ "learning-resource",
5134
+ "learning-resources",
5135
+ "learning-standard",
5136
+ "learning-standards",
5137
+ "loa",
5138
+ "loi",
5139
+ "lot",
5140
+ "lov",
5141
+ "match-problem",
5142
+ "multiple-choice-problem",
5143
+ "noteref",
5144
+ "notice",
5145
+ "ordinal",
5146
+ "other-credits",
5147
+ "page-list",
5148
+ "pagebreak",
5149
+ "panel",
5150
+ "panel-group",
5151
+ "part",
5152
+ "practice",
5153
+ "practices",
5154
+ "preamble",
5155
+ "preface",
5156
+ "prologue",
5157
+ "pullquote",
5158
+ "qna",
5159
+ "question",
5160
+ "referrer",
5161
+ "revision-history",
5162
+ "seriespage",
5163
+ "sound-area",
5164
+ "subtitle",
5165
+ "tip",
5166
+ "title",
5167
+ "titlepage",
5168
+ "toc",
5169
+ "toc-brief",
5170
+ "topic-sentence",
5171
+ "true-false-problem",
5172
+ "volume"
5173
+ ]);
5174
+
5016
5175
  // src/smil/validator.ts
5017
5176
  var SMIL_NS = { smil: "http://www.w3.org/ns/SMIL" };
5018
5177
  var BLESSED_AUDIO_TYPES = /* @__PURE__ */ new Set(["audio/mpeg", "audio/mp4"]);
@@ -5258,24 +5417,6 @@ var SMILValidator = class {
5258
5417
  }
5259
5418
  };
5260
5419
 
5261
- // src/references/types.ts
5262
- var PUBLICATION_RESOURCE_TYPES = /* @__PURE__ */ new Set([
5263
- "generic" /* GENERIC */,
5264
- "stylesheet" /* STYLESHEET */,
5265
- "font" /* FONT */,
5266
- "image" /* IMAGE */,
5267
- "audio" /* AUDIO */,
5268
- "video" /* VIDEO */,
5269
- "track" /* TRACK */,
5270
- "media-overlay" /* MEDIA_OVERLAY */,
5271
- "svg-symbol" /* SVG_SYMBOL */,
5272
- "svg-paint" /* SVG_PAINT */,
5273
- "svg-clip-path" /* SVG_CLIP_PATH */
5274
- ]);
5275
- function isPublicationResourceReference(type) {
5276
- return PUBLICATION_RESOURCE_TYPES.has(type);
5277
- }
5278
-
5279
5420
  // src/references/uri-schemes.ts
5280
5421
  var URI_SCHEMES = /* @__PURE__ */ new Set([
5281
5422
  "aaa",
@@ -5365,7 +5506,9 @@ var ABSOLUTE_URI_RE = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
5365
5506
  var SPECIAL_URL_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "ftp", "ws", "wss"]);
5366
5507
  var CSS_CHARSET_RE = /^@charset\s+"([^"]+)"\s*;/;
5367
5508
  var EPUB_XMLNS_RE = /xmlns:epub\s*=\s*"([^"]*)"/;
5368
- var XHTML_NS = { html: "http://www.w3.org/1999/xhtml" };
5509
+ var XHTML_NS_URI = "http://www.w3.org/1999/xhtml";
5510
+ var XML_NS_URI = "http://www.w3.org/XML/1998/namespace";
5511
+ var XHTML_NS = { html: XHTML_NS_URI };
5369
5512
  var EPUB_OPS_NS = { epub: "http://www.idpf.org/2007/ops" };
5370
5513
  var EPUB_TYPE_FORBIDDEN_ELEMENTS = /* @__PURE__ */ new Set([
5371
5514
  "head",
@@ -5649,6 +5792,112 @@ var HTML5_ELEMENTS = /* @__PURE__ */ new Set([
5649
5792
  "video",
5650
5793
  "wbr"
5651
5794
  ]);
5795
+ var XHTML11_ELEMENTS = /* @__PURE__ */ new Set([
5796
+ // struct
5797
+ "html",
5798
+ "head",
5799
+ "title",
5800
+ "body",
5801
+ "meta",
5802
+ "link",
5803
+ "base",
5804
+ "style",
5805
+ "script",
5806
+ "noscript",
5807
+ // text
5808
+ "br",
5809
+ "span",
5810
+ "abbr",
5811
+ "acronym",
5812
+ "cite",
5813
+ "code",
5814
+ "dfn",
5815
+ "em",
5816
+ "kbd",
5817
+ "q",
5818
+ "samp",
5819
+ "strong",
5820
+ "var",
5821
+ "div",
5822
+ "p",
5823
+ "address",
5824
+ "blockquote",
5825
+ "pre",
5826
+ "h1",
5827
+ "h2",
5828
+ "h3",
5829
+ "h4",
5830
+ "h5",
5831
+ "h6",
5832
+ // pres / legacy
5833
+ "hr",
5834
+ "b",
5835
+ "big",
5836
+ "i",
5837
+ "small",
5838
+ "sub",
5839
+ "sup",
5840
+ "tt",
5841
+ "basefont",
5842
+ "center",
5843
+ "font",
5844
+ "s",
5845
+ "strike",
5846
+ "u",
5847
+ "dir",
5848
+ "menu",
5849
+ "isindex",
5850
+ // list
5851
+ "dl",
5852
+ "dt",
5853
+ "dd",
5854
+ "ol",
5855
+ "ul",
5856
+ "li",
5857
+ // table
5858
+ "table",
5859
+ "caption",
5860
+ "tr",
5861
+ "th",
5862
+ "td",
5863
+ "col",
5864
+ "colgroup",
5865
+ "tbody",
5866
+ "thead",
5867
+ "tfoot",
5868
+ // hypertext / image / object / form / edit / ruby / map / iframe / applet / bdo / param
5869
+ "a",
5870
+ "img",
5871
+ "object",
5872
+ "param",
5873
+ "form",
5874
+ "label",
5875
+ "input",
5876
+ "select",
5877
+ "option",
5878
+ "optgroup",
5879
+ "fieldset",
5880
+ "button",
5881
+ "legend",
5882
+ "textarea",
5883
+ "ins",
5884
+ "del",
5885
+ "ruby",
5886
+ "rbc",
5887
+ "rtc",
5888
+ "rb",
5889
+ "rt",
5890
+ "rp",
5891
+ "map",
5892
+ "area",
5893
+ "iframe",
5894
+ "applet",
5895
+ "bdo",
5896
+ // frames
5897
+ "frameset",
5898
+ "frame",
5899
+ "noframes"
5900
+ ]);
5652
5901
  function isItemFixedLayout(packageDoc, itemId) {
5653
5902
  const spineItem = packageDoc.spine.find((s) => s.idref === itemId);
5654
5903
  if (!spineItem) return false;
@@ -5715,6 +5964,10 @@ var ContentValidator = class {
5715
5964
  if (refValidator) {
5716
5965
  this.extractSVGReferences(context, fullPath, opfDir, refValidator);
5717
5966
  }
5967
+ } else if (item.mediaType === "application/vnd.epub.search-key-map+xml") {
5968
+ const fullPath = resolveManifestHref(opfDir, item.href);
5969
+ const skmValidator = new SKMValidator();
5970
+ skmValidator.validate(context, fullPath, refValidator);
5718
5971
  } else if (item.mediaType === "application/smil+xml") {
5719
5972
  const fullPath = resolveManifestHref(opfDir, item.href);
5720
5973
  const smilValidator = new SMILValidator();
@@ -6477,6 +6730,9 @@ var ContentValidator = class {
6477
6730
  }
6478
6731
  }
6479
6732
  }
6733
+ if (context.version === "2.0") {
6734
+ this.checkEpub2XhtmlStrict(context, path, root);
6735
+ }
6480
6736
  this.checkDiscouragedElements(context, path, root);
6481
6737
  this.checkSSMLPh(context, path, root, content);
6482
6738
  this.checkObsoleteHTML(context, path, root);
@@ -6507,6 +6763,9 @@ var ContentValidator = class {
6507
6763
  this.validateEpubTypes(context, path, root);
6508
6764
  this.validateRegionBasedNav(context, path, root, manifestItem);
6509
6765
  }
6766
+ if (context.version.startsWith("3") && context.options.profile === "dict") {
6767
+ this.validateDictionaryContent(context, path, root);
6768
+ }
6510
6769
  if (context.version.startsWith("3") && context.options.profile === "edupub") {
6511
6770
  const isFxl = manifestItem && packageDoc ? isItemFixedLayout(packageDoc, manifestItem.id) : false;
6512
6771
  const isNonLinear = manifestItem && packageDoc ? packageDoc.spine.find((ref) => ref.idref === manifestItem.id)?.linear === false : false;
@@ -6515,7 +6774,7 @@ var ContentValidator = class {
6515
6774
  }
6516
6775
  }
6517
6776
  if (context.version.startsWith("3")) {
6518
- this.collectFeatures(context, root);
6777
+ this.collectFeatures(context, path, root);
6519
6778
  }
6520
6779
  this.validateEpubSwitch(context, path, root);
6521
6780
  this.validateEpubTrigger(context, path, root);
@@ -8038,7 +8297,7 @@ var ContentValidator = class {
8038
8297
  }
8039
8298
  }
8040
8299
  }
8041
- collectFeatures(context, root) {
8300
+ collectFeatures(context, path, root) {
8042
8301
  const features = context.contentFeatures;
8043
8302
  if (!features) return;
8044
8303
  if (!features.hasTable && root.get(".//html:table", XHTML_NS)) {
@@ -8053,22 +8312,20 @@ var ContentValidator = class {
8053
8312
  if (!features.hasVideo && root.get(".//html:video", XHTML_NS)) {
8054
8313
  features.hasVideo = true;
8055
8314
  }
8056
- if (!features.hasPageBreak || !features.hasDictionary || !features.hasIndex) {
8057
- const epubTypeElements = root.find(".//*[@epub:type]", EPUB_OPS_NS);
8058
- for (const el of epubTypeElements) {
8059
- const attr = el.attr("type", "epub");
8060
- if (!attr?.value) continue;
8061
- const tokens = attr.value.trim().split(/\s+/);
8062
- if (!features.hasPageBreak && tokens.includes("pagebreak")) {
8063
- features.hasPageBreak = true;
8064
- }
8065
- if (!features.hasDictionary && tokens.includes("dictionary")) {
8066
- features.hasDictionary = true;
8067
- }
8068
- if (!features.hasIndex && tokens.includes("index")) {
8069
- features.hasIndex = true;
8070
- }
8071
- if (features.hasPageBreak && features.hasDictionary && features.hasIndex) break;
8315
+ const epubTypeElements = root.find(".//*[@epub:type]", EPUB_OPS_NS);
8316
+ for (const el of epubTypeElements) {
8317
+ const attr = el.attr("type", "epub");
8318
+ if (!attr?.value) continue;
8319
+ const tokens = attr.value.trim().split(/\s+/);
8320
+ if (!features.hasPageBreak && tokens.includes("pagebreak")) {
8321
+ features.hasPageBreak = true;
8322
+ }
8323
+ if (tokens.includes("dictionary")) {
8324
+ features.hasDictionary = true;
8325
+ (features.dictionaryContentPaths ??= /* @__PURE__ */ new Set()).add(path);
8326
+ }
8327
+ if (!features.hasIndex && tokens.includes("index")) {
8328
+ features.hasIndex = true;
8072
8329
  }
8073
8330
  }
8074
8331
  if (!features.hasMicrodata && root.get(".//*[@itemscope]")) {
@@ -8169,6 +8426,158 @@ var ContentValidator = class {
8169
8426
  });
8170
8427
  }
8171
8428
  }
8429
+ this.validateRegionBasedNavRules(context, path, root);
8430
+ }
8431
+ }
8432
+ validateRegionBasedNavRules(context, path, root) {
8433
+ const XHTML_NS2 = { html: "http://www.w3.org/1999/xhtml" };
8434
+ let regionNavs;
8435
+ try {
8436
+ regionNavs = root.find(".//html:nav", XHTML_NS2);
8437
+ } catch {
8438
+ return;
8439
+ }
8440
+ const packageDoc = context.packageDocument;
8441
+ const opfDir = context.opfPath?.includes("/") ? context.opfPath.substring(0, context.opfPath.lastIndexOf("/")) : "";
8442
+ const docDir = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
8443
+ const manifestByPath = packageDoc ? new Map(packageDoc.manifest.map((m) => [resolveManifestHref(opfDir, m.href), m])) : void 0;
8444
+ for (const nav of regionNavs) {
8445
+ const epubType = nav.attr("type", "epub")?.value ?? "";
8446
+ if (!epubType.split(/\s+/).includes("region-based")) continue;
8447
+ const childEls = nav.find("./html:*", XHTML_NS2);
8448
+ if (childEls.length !== 1 || childEls[0]?.name !== "ol") {
8449
+ pushMessage(context.messages, {
8450
+ id: MessageId.RSC_017,
8451
+ message: "A region-based nav element must contain exactly one child ol element.",
8452
+ location: { path, line: nav.line }
8453
+ });
8454
+ }
8455
+ const liElements = nav.find(".//html:li", XHTML_NS2);
8456
+ for (const li of liElements) {
8457
+ const liChildren = li.find("./html:*", XHTML_NS2);
8458
+ const first = liChildren[0];
8459
+ if (!first || first.name !== "a" && first.name !== "span") {
8460
+ pushMessage(context.messages, {
8461
+ id: MessageId.RSC_017,
8462
+ message: "The first child of a region-based nav list item must be either an 'a' or 'span' element.",
8463
+ location: { path, line: li.line }
8464
+ });
8465
+ }
8466
+ if (liChildren.length > 1 && (liChildren.length !== 2 || liChildren[1]?.name !== "ol")) {
8467
+ pushMessage(context.messages, {
8468
+ id: MessageId.RSC_017,
8469
+ message: "The first child of a region-based nav list item can only be followed by a single 'ol' element.",
8470
+ location: { path, line: li.line }
8471
+ });
8472
+ }
8473
+ }
8474
+ const spans = nav.find(".//html:span", XHTML_NS2);
8475
+ for (const span of spans) {
8476
+ const spanChildren = span.find("./html:*", XHTML_NS2);
8477
+ const aChildren = spanChildren.filter((c) => c.name === "a");
8478
+ if (spanChildren.length !== 2 || aChildren.length !== 2) {
8479
+ pushMessage(context.messages, {
8480
+ id: MessageId.RSC_017,
8481
+ message: "'span' elements in region-based navs must contain exactly two 'a' elements.",
8482
+ location: { path, line: span.line }
8483
+ });
8484
+ }
8485
+ }
8486
+ const anchors = nav.find(".//html:a", XHTML_NS2);
8487
+ for (const a of anchors) {
8488
+ if (a.content.trim() !== "") {
8489
+ pushMessage(context.messages, {
8490
+ id: MessageId.RSC_017,
8491
+ message: "'a' elements in region-based navs should not contain text labels.",
8492
+ location: { path, line: a.line }
8493
+ });
8494
+ }
8495
+ }
8496
+ if (!packageDoc || !manifestByPath) continue;
8497
+ for (const a of anchors) {
8498
+ const href = a.attr("href")?.value;
8499
+ if (!href || !isRelativeURL(href)) continue;
8500
+ const resolved = this.resolveRelativePath(docDir, href, opfDir);
8501
+ const targetPath = parseURL(resolved).resource;
8502
+ if (!targetPath) continue;
8503
+ const item = manifestByPath.get(targetPath);
8504
+ if (!item) continue;
8505
+ if (!isItemFixedLayout(packageDoc, item.id)) {
8506
+ pushMessage(context.messages, {
8507
+ id: MessageId.NAV_009,
8508
+ message: "Region-based navigation links must point to Fixed-Layout Documents.",
8509
+ location: { path, line: a.line }
8510
+ });
8511
+ }
8512
+ }
8513
+ }
8514
+ }
8515
+ /**
8516
+ * EPUB Dictionaries content document rules.
8517
+ *
8518
+ * Mirrors ../epubcheck/src/main/resources/com/adobe/epubcheck/schema/30/dict/dict-xhtml.sch
8519
+ * (minimum set: `dictionary` must be on body/section with article children; each article or
8520
+ * `dictentry` must have a `dfn` descendant outside of optional `condensed-entry`).
8521
+ */
8522
+ validateDictionaryContent(context, path, root) {
8523
+ let typedElements;
8524
+ try {
8525
+ typedElements = root.find(".//*[@epub:type]", EPUB_OPS_NS);
8526
+ } catch {
8527
+ return;
8528
+ }
8529
+ for (const el of typedElements) {
8530
+ const tokens = el.attr("type", "epub")?.value.split(/\s+/) ?? [];
8531
+ if (tokens.includes("dictionary")) {
8532
+ if (el.name !== "body" && el.name !== "section") {
8533
+ pushMessage(context.messages, {
8534
+ id: MessageId.RSC_005,
8535
+ message: 'The "dictionary" type is only allowed on "body" or "section" elements.',
8536
+ location: { path, line: el.line }
8537
+ });
8538
+ }
8539
+ const articles = el.find("./html:article", XHTML_NS);
8540
+ if (articles.length === 0) {
8541
+ pushMessage(context.messages, {
8542
+ id: MessageId.RSC_005,
8543
+ message: 'A "dictionary" must have at least one article child.',
8544
+ location: { path, line: el.line }
8545
+ });
8546
+ }
8547
+ for (const article of articles) {
8548
+ this.checkDictionaryEntry(context, path, article);
8549
+ }
8550
+ }
8551
+ if (tokens.includes("dictentry")) {
8552
+ if (el.name !== "article") {
8553
+ pushMessage(context.messages, {
8554
+ id: MessageId.RSC_005,
8555
+ message: 'The "dictentry" type is only allowed on "article" elements.',
8556
+ location: { path, line: el.line }
8557
+ });
8558
+ } else {
8559
+ this.checkDictionaryEntry(context, path, el);
8560
+ }
8561
+ }
8562
+ }
8563
+ }
8564
+ checkDictionaryEntry(context, path, article) {
8565
+ const dfns = article.find(".//html:dfn", XHTML_NS);
8566
+ const hasDfnOutsideCondensed = dfns.some((dfn) => {
8567
+ let parent = dfn.parent;
8568
+ while (parent) {
8569
+ const type = parent.attr("type", "epub")?.value;
8570
+ if (type?.split(/\s+/).includes("condensed-entry")) return false;
8571
+ parent = parent.parent;
8572
+ }
8573
+ return true;
8574
+ });
8575
+ if (!hasDfnOutsideCondensed) {
8576
+ pushMessage(context.messages, {
8577
+ id: MessageId.RSC_005,
8578
+ message: 'A dictionary entry must have at least one "dfn" descendant (outside of the optional condensed entry "aside").',
8579
+ location: { path, line: article.line }
8580
+ });
8172
8581
  }
8173
8582
  }
8174
8583
  /**
@@ -9565,13 +9974,11 @@ var ContentValidator = class {
9565
9974
  }
9566
9975
  }
9567
9976
  checkUnknownElements(context, path, root) {
9568
- const XHTML_NS2 = "http://www.w3.org/1999/xhtml";
9569
9977
  try {
9570
9978
  const allElements = root.find(".//*");
9571
9979
  for (const el of allElements) {
9572
9980
  const xmlEl = el;
9573
- const ns = xmlEl.namespaceUri;
9574
- if (ns !== XHTML_NS2) continue;
9981
+ if (xmlEl.namespaceUri !== XHTML_NS_URI) continue;
9575
9982
  const localName = xmlEl.name.includes(":") ? xmlEl.name.substring(xmlEl.name.indexOf(":") + 1) : xmlEl.name;
9576
9983
  if (localName.includes("-")) continue;
9577
9984
  if (!HTML5_ELEMENTS.has(localName)) {
@@ -9585,6 +9992,51 @@ var ContentValidator = class {
9585
9992
  } catch {
9586
9993
  }
9587
9994
  }
9995
+ checkEpub2XhtmlStrict(context, path, root) {
9996
+ if (!root.namespaceUri) {
9997
+ pushMessage(context.messages, {
9998
+ id: MessageId.RSC_005,
9999
+ message: `element "${root.name}" from namespace "" is not allowed`,
10000
+ location: { path, line: root.line }
10001
+ });
10002
+ return;
10003
+ }
10004
+ const checkElement = (xmlEl) => {
10005
+ if (xmlEl.namespaceUri !== XHTML_NS_URI) return;
10006
+ const localName = xmlEl.name.includes(":") ? xmlEl.name.substring(xmlEl.name.indexOf(":") + 1) : xmlEl.name;
10007
+ if (!XHTML11_ELEMENTS.has(localName)) {
10008
+ pushMessage(context.messages, {
10009
+ id: MessageId.RSC_005,
10010
+ message: `element "${localName}" not allowed here`,
10011
+ location: { path, line: xmlEl.line }
10012
+ });
10013
+ }
10014
+ for (const attr of xmlEl.attrs) {
10015
+ const ns = attr.namespaceUri;
10016
+ if (!ns || ns === XHTML_NS_URI || ns === XML_NS_URI) continue;
10017
+ const qname = attr.prefix ? `${attr.prefix}:${attr.name}` : attr.name;
10018
+ pushMessage(context.messages, {
10019
+ id: MessageId.RSC_005,
10020
+ message: `attribute "${qname}" not allowed here`,
10021
+ location: { path, line: xmlEl.line }
10022
+ });
10023
+ }
10024
+ };
10025
+ checkElement(root);
10026
+ try {
10027
+ for (const el of root.find(".//*")) {
10028
+ checkElement(el);
10029
+ }
10030
+ for (const a of root.find(".//html:a//html:a", XHTML_NS)) {
10031
+ pushMessage(context.messages, {
10032
+ id: MessageId.RSC_005,
10033
+ message: 'The "a" element cannot contain any nested "a" elements',
10034
+ location: { path, line: a.line }
10035
+ });
10036
+ }
10037
+ } catch {
10038
+ }
10039
+ }
9588
10040
  checkForeignObjectContent(context, path, root, isSVGDoc) {
9589
10041
  const SVG_NS = { svg: "http://www.w3.org/2000/svg" };
9590
10042
  const XHTML_URI = "http://www.w3.org/1999/xhtml";
@@ -9793,6 +10245,7 @@ var NCXValidator = class {
9793
10245
  this.checkContentSrc(context, root, ncxPath, registry);
9794
10246
  this.checkEmptyLabels(context, root, ncxPath);
9795
10247
  this.checkPageTargets(context, root, ncxPath);
10248
+ this.checkIdSyntax(context, root, ncxPath);
9796
10249
  } finally {
9797
10250
  doc.dispose();
9798
10251
  }
@@ -9896,10 +10349,21 @@ var NCXValidator = class {
9896
10349
  }
9897
10350
  }
9898
10351
  }
9899
- /**
9900
- * pageTarget@type must be one of "front", "normal", "special".
9901
- * Reported via RSC-005 to mirror Java Schematron output.
9902
- */
10352
+ checkIdSyntax(context, root, ncxPath) {
10353
+ const nodes = root.find(".//*[@id]", {
10354
+ ncx: "http://www.daisy.org/z3986/2005/ncx/"
10355
+ });
10356
+ for (const node of nodes) {
10357
+ const idValue = node.attr("id")?.value;
10358
+ if (idValue && !NC_NAME_REGEX.test(idValue)) {
10359
+ pushMessage(context.messages, {
10360
+ id: MessageId.RSC_005,
10361
+ message: `Invalid id "${idValue}"; must match xs:ID syntax (NCName)`,
10362
+ location: { path: ncxPath, line: node.line }
10363
+ });
10364
+ }
10365
+ }
10366
+ }
9903
10367
  checkPageTargets(context, root, ncxPath) {
9904
10368
  const pageTargets = root.find(".//ncx:pageTarget[@type]", {
9905
10369
  ncx: "http://www.daisy.org/z3986/2005/ncx/"
@@ -9917,6 +10381,7 @@ var NCXValidator = class {
9917
10381
  }
9918
10382
  }
9919
10383
  };
10384
+ var NC_NAME_REGEX = /^[A-Za-z_][A-Za-z0-9._-]*$/;
9920
10385
  var PAGE_TARGET_TYPES = /* @__PURE__ */ new Set(["front", "normal", "special"]);
9921
10386
  var OPS_MEDIA_TYPES = /* @__PURE__ */ new Set([
9922
10387
  "application/xhtml+xml",
@@ -10046,15 +10511,6 @@ function parseContainerContent(content, context, fileExists, getFileContent) {
10046
10511
  }
10047
10512
  }
10048
10513
  }
10049
- for (const rootfile of context.rootfiles) {
10050
- if (!fileExists(rootfile.path)) {
10051
- pushMessage(context.messages, {
10052
- id: MessageId.PKG_010,
10053
- message: `Rootfile "${rootfile.path}" not found in EPUB`,
10054
- location: { path: containerPath }
10055
- });
10056
- }
10057
- }
10058
10514
  }
10059
10515
  function validateMappingDocumentContent(xml, mappingPath, context) {
10060
10516
  const stripped = stripXmlComments(xml);
@@ -10973,19 +11429,7 @@ var ReferenceValidator = class {
10973
11429
  message: "Absolute paths are not allowed in EPUB",
10974
11430
  location: reference.location
10975
11431
  });
10976
- }
10977
- const forbiddenParentDirTypes = [
10978
- "hyperlink" /* HYPERLINK */,
10979
- "nav-toc-link" /* NAV_TOC_LINK */,
10980
- "nav-pagelist-link" /* NAV_PAGELIST_LINK */
10981
- ];
10982
- if (hasParentDirectoryReference(reference.url) && forbiddenParentDirTypes.includes(reference.type)) {
10983
- pushMessage(context.messages, {
10984
- id: MessageId.RSC_026,
10985
- message: "Parent directory references (..) are not allowed",
10986
- location: reference.location
10987
- });
10988
- } else if (!hasAbsolutePath(resourcePath) && !hasParentDirectoryReference(reference.url) && checkUrlLeaking(reference.url)) {
11432
+ } else if (checkUrlLeaking(reference.url, reference.location.path)) {
10989
11433
  pushMessage(context.messages, {
10990
11434
  id: MessageId.RSC_026,
10991
11435
  message: `URL "${reference.url}" leaks outside the container`,
@@ -11023,6 +11467,16 @@ var ReferenceValidator = class {
11023
11467
  location: reference.location
11024
11468
  });
11025
11469
  }
11470
+ if (reference.type === "search-key" /* SEARCH_KEY */ && !resource?.inSpine) {
11471
+ const isEpubCfi = reference.fragment?.startsWith("epubcfi(") ?? false;
11472
+ if (!isEpubCfi) {
11473
+ pushMessage(context.messages, {
11474
+ id: MessageId.RSC_021,
11475
+ message: `Search Key Map target "${resourcePath}" must be a Content Document in the spine`,
11476
+ location: reference.location
11477
+ });
11478
+ }
11479
+ }
11026
11480
  if (isHyperlinkLike || reference.type === "overlay-text-link" /* OVERLAY_TEXT_LINK */) {
11027
11481
  const targetMimeType = resource?.mimeType;
11028
11482
  if (targetMimeType && !this.isBlessedItemType(targetMimeType, context.version) && !this.isDeprecatedBlessedItemType(targetMimeType) && !resource.hasCoreMediaTypeFallback) {
@@ -11507,11 +11961,11 @@ var RelaxNGValidator = class extends BaseSchemaValidator {
11507
11961
  try {
11508
11962
  const libxml2 = await import('libxml2-wasm');
11509
11963
  const LibRelaxNGValidator = libxml2.RelaxNGValidator;
11510
- const { XmlDocument: XmlDocument4 } = libxml2;
11511
- const doc = XmlDocument4.fromString(xml);
11964
+ const { XmlDocument: XmlDocument5 } = libxml2;
11965
+ const doc = XmlDocument5.fromString(xml);
11512
11966
  try {
11513
11967
  const schemaContent = await loadSchema(schemaPath);
11514
- const schemaDoc = XmlDocument4.fromString(schemaContent);
11968
+ const schemaDoc = XmlDocument5.fromString(schemaContent);
11515
11969
  try {
11516
11970
  const validator = LibRelaxNGValidator.fromDoc(schemaDoc);
11517
11971
  try {
@@ -11975,6 +12429,73 @@ var EpubCheck = class _EpubCheck {
11975
12429
  location: { path: opfPath }
11976
12430
  });
11977
12431
  }
12432
+ if (profile === "dict" && context.packageDocument) {
12433
+ const opfDir = opfPath.includes("/") ? opfPath.substring(0, opfPath.lastIndexOf("/")) : "";
12434
+ const manifestByPath = /* @__PURE__ */ new Map();
12435
+ for (const item of context.packageDocument.manifest) {
12436
+ manifestByPath.set(resolveManifestHref(opfDir, item.href), item);
12437
+ }
12438
+ const dictPaths = features.dictionaryContentPaths ?? /* @__PURE__ */ new Set();
12439
+ for (const coll of context.packageDocument.collections) {
12440
+ if (coll.role !== "dictionary") continue;
12441
+ const hasDictContent = coll.links.some((href) => {
12442
+ const full = resolveManifestHref(opfDir, href);
12443
+ const item = manifestByPath.get(full);
12444
+ return item?.mediaType === "application/xhtml+xml" && dictPaths.has(full);
12445
+ });
12446
+ if (!hasDictContent) {
12447
+ pushMessage(context.messages, {
12448
+ id: MessageId.OPF_078,
12449
+ message: 'An EPUB Dictionary must contain at least one Content Document with dictionary content (epub:type "dictionary")',
12450
+ location: { path: opfPath }
12451
+ });
12452
+ }
12453
+ }
12454
+ }
12455
+ }
12456
+ /**
12457
+ * Mirrors ../epubcheck/src/main/resources/com/adobe/epubcheck/schema/30/edupub/
12458
+ * edu-ocf-metadata.sch (META-INF/metadata.xml) and edu-opf.sch (each OPF).
12459
+ *
12460
+ * Non-primary OPFs are otherwise unreached: runPipeline only runs OPFValidator
12461
+ * on context.opfPath.
12462
+ */
12463
+ validateEdupubMultiRendition(context) {
12464
+ if (context.options.profile !== "edupub") return;
12465
+ if (context.rootfiles.length <= 1) return;
12466
+ const metadataPath = "META-INF/metadata.xml";
12467
+ const metadataData = context.files.get(metadataPath);
12468
+ if (metadataData) {
12469
+ const content = typeof metadataData === "string" ? metadataData : new TextDecoder().decode(metadataData);
12470
+ const stripped = stripXmlComments(content);
12471
+ if (!/<dc:type\b[^>]*>\s*edupub\s*<\/dc:type>/i.test(stripped)) {
12472
+ pushMessage(context.messages, {
12473
+ id: MessageId.RSC_005,
12474
+ message: 'A dc:type element with the value "edupub" is required.',
12475
+ location: { path: metadataPath }
12476
+ });
12477
+ }
12478
+ }
12479
+ const primary = context.opfPath;
12480
+ for (const rootfile of context.rootfiles) {
12481
+ if (rootfile.path === primary) continue;
12482
+ if (rootfile.mediaType !== "application/oebps-package+xml") continue;
12483
+ const path = rootfile.path.normalize("NFC");
12484
+ const opfData = context.files.get(path);
12485
+ if (!opfData) continue;
12486
+ const xml = typeof opfData === "string" ? opfData : new TextDecoder().decode(opfData);
12487
+ const pkg = parseOPF(xml);
12488
+ const hasType = pkg.dcElements.some(
12489
+ (dc) => dc.name === "type" && dc.value.trim() === "edupub"
12490
+ );
12491
+ if (!hasType) {
12492
+ pushMessage(context.messages, {
12493
+ id: MessageId.RSC_005,
12494
+ message: 'The dc:type identifier "edupub" is required.',
12495
+ location: { path }
12496
+ });
12497
+ }
12498
+ }
11978
12499
  }
11979
12500
  /**
11980
12501
  * Validate NCX navigation document (EPUB 2 always, EPUB 3 when NCX present)
@@ -12120,6 +12641,7 @@ var EpubCheck = class _EpubCheck {
12120
12641
  const contentValidator = new ContentValidator();
12121
12642
  contentValidator.validate(context, registry, refValidator);
12122
12643
  this.validateCrossDocumentFeatures(context);
12644
+ this.validateEdupubMultiRendition(context);
12123
12645
  if (context.packageDocument) {
12124
12646
  this.validateNCX(context, registry);
12125
12647
  }