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