@storyteller-platform/epub 0.7.0 → 0.7.1

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
@@ -3,14 +3,9 @@ import { cp, mkdir } from "node:fs/promises";
3
3
  import { Mutex } from "async-mutex";
4
4
  import { XMLBuilder, XMLParser } from "fast-xml-parser";
5
5
  import memoize from "mem";
6
- import { lookup } from "mime-types";
7
6
  import { nanoid } from "nanoid";
8
- import {
9
- dirname,
10
- hrefToPlatformPath,
11
- join,
12
- resolve
13
- } from "@storyteller-platform/path";
7
+ import { MediaType } from "@storyteller-platform/media-types";
8
+ import { dirname } from "@storyteller-platform/path";
14
9
  import { TmpFsAdapter } from "./adapters/tmpfs.js";
15
10
  import * as Upgrade from "./upgrade.js";
16
11
  import { MemoryAdapter } from "./adapters/memory.js";
@@ -19,6 +14,61 @@ class EpubVersionError extends Error {
19
14
  }
20
15
  class EpubReadOnlyError extends Error {
21
16
  }
17
+ const OPF_NAMESPACE = "http://www.idpf.org/2007/opf";
18
+ const ENTRY_ORIGIN = "epub:///";
19
+ function entryToUrlPath(entry) {
20
+ return entry.split("/").map(encodeURIComponent).join("/");
21
+ }
22
+ function resolveEntryUrl(from, href) {
23
+ const base = new URL(entryToUrlPath(from), ENTRY_ORIGIN);
24
+ if (!URL.canParse(href, base.toString())) return null;
25
+ const url = new URL(href, base);
26
+ return url.href.startsWith(ENTRY_ORIGIN) ? url : null;
27
+ }
28
+ function renameOpfAttributes(attrs, prefix) {
29
+ const attrPrefix = `@_${prefix}:`;
30
+ const renamed = {};
31
+ for (const [key, value] of Object.entries(attrs)) {
32
+ if (key === `@_xmlns:${prefix}`) continue;
33
+ if (key.startsWith(attrPrefix)) {
34
+ renamed[`@_opf:${key.slice(attrPrefix.length)}`] = value;
35
+ } else {
36
+ renamed[key] = value;
37
+ }
38
+ }
39
+ return renamed;
40
+ }
41
+ function stripOpfPrefix(node, prefix) {
42
+ if (Epub.isXmlTextNode(node)) return node;
43
+ const name = Epub.getXmlElementName(node);
44
+ const bareName = name.startsWith(`${prefix}:`) ? name.slice(prefix.length + 1) : name;
45
+ const element = {
46
+ [bareName]: Epub.getXmlChildren(node).map(
47
+ (child) => stripOpfPrefix(child, prefix)
48
+ )
49
+ };
50
+ if (node[":@"]) {
51
+ element[":@"] = renameOpfAttributes(node[":@"], prefix);
52
+ }
53
+ return element;
54
+ }
55
+ function normalizeOpfNamespacePrefixes(doc) {
56
+ var _a;
57
+ const root = doc.find(
58
+ (node) => !Epub.isXmlTextNode(node) && Epub.getXmlElementName(node).endsWith(":package")
59
+ );
60
+ if (!root) return doc;
61
+ const prefix = Epub.getXmlElementName(root).slice(0, -":package".length);
62
+ if (((_a = root[":@"]) == null ? void 0 : _a[`@_xmlns:${prefix}`]) !== OPF_NAMESPACE) return doc;
63
+ const stripped = stripOpfPrefix(root, prefix);
64
+ stripped[":@"] = {
65
+ ...stripped[":@"],
66
+ "@_xmlns": OPF_NAMESPACE,
67
+ "@_xmlns:opf": OPF_NAMESPACE
68
+ };
69
+ doc[doc.indexOf(root)] = stripped;
70
+ return doc;
71
+ }
22
72
  class Epub {
23
73
  /**
24
74
  * Prefer the static factories ({@link Epub.using}, {@link Epub.from},
@@ -299,14 +349,7 @@ ${JSON.stringify(element, null, 2)}`
299
349
  );
300
350
  }
301
351
  const rootfile = await this.getRootfile();
302
- const filename = this.resolveInternalHref(rootfile, href);
303
- await this.adapter.remove(filename);
304
- }
305
- async getFileData(path, encoding) {
306
- if (encoding) {
307
- return this.adapter.read(path, encoding);
308
- }
309
- return this.adapter.read(path);
352
+ await this.adapter.remove(await this.resolveEntry(rootfile, href));
310
353
  }
311
354
  /**
312
355
  * Length of the underlying archive entry for a manifest item, in bytes
@@ -319,14 +362,15 @@ ${JSON.stringify(element, null, 2)}`
319
362
  const manifestItem = manifest[id];
320
363
  if (!manifestItem)
321
364
  throw new Error(`Could not find item with id "${id}" in manifest`);
322
- const path = this.resolveInternalHref(rootfile, manifestItem.href);
323
- return this.adapter.archiveLength(path);
365
+ return this.adapter.archiveLength(
366
+ await this.resolveEntry(rootfile, manifestItem.href)
367
+ );
324
368
  }
325
369
  async getRootfile() {
326
370
  var _a;
327
371
  if (this.rootfile !== null) return this.rootfile;
328
- const containerString = await this.getFileData(
329
- join(this.adapter.rootPath, "META-INF", "container.xml"),
372
+ const containerString = await this.adapter.read(
373
+ "META-INF/container.xml",
330
374
  "utf-8"
331
375
  );
332
376
  if (!containerString)
@@ -350,7 +394,7 @@ ${JSON.stringify(element, null, 2)}`
350
394
  Epub.getXmlChildren(rootfiles),
351
395
  (node) => {
352
396
  var _a2;
353
- return !Epub.isXmlTextNode(node) && ((_a2 = node[":@"]) == null ? void 0 : _a2["@_media-type"]) === "application/oebps-package+xml";
397
+ return !Epub.isXmlTextNode(node) && ((_a2 = node[":@"]) == null ? void 0 : _a2["@_media-type"]) === MediaType.OPF.mime;
354
398
  }
355
399
  );
356
400
  const fullPath = (_a = rootfile == null ? void 0 : rootfile[":@"]) == null ? void 0 : _a["@_full-path"];
@@ -358,12 +402,12 @@ ${JSON.stringify(element, null, 2)}`
358
402
  throw new Error(
359
403
  "Failed to parse EPUB container.xml: Found no rootfile element"
360
404
  );
361
- this.rootfile = resolve(this.adapter.rootPath, fullPath);
405
+ this.rootfile = await this.resolveEntry("", fullPath);
362
406
  return this.rootfile;
363
407
  }
364
408
  async getPackageDocument() {
365
409
  const rootfile = await this.getRootfile();
366
- const packageDocumentString = await this.getFileData(rootfile, "utf-8");
410
+ const packageDocumentString = await this.adapter.read(rootfile, "utf-8");
367
411
  if (!packageDocumentString)
368
412
  throw new Error(
369
413
  `Failed to parse EPUB: could not find package document at ${rootfile}`
@@ -371,7 +415,7 @@ ${JSON.stringify(element, null, 2)}`
371
415
  const packageDocument = Epub.xmlParser.parse(
372
416
  packageDocumentString
373
417
  );
374
- return packageDocument;
418
+ return normalizeOpfNamespacePrefixes(packageDocument);
375
419
  }
376
420
  async getPackageElement() {
377
421
  const packageDocument = await this.getPackageDocument();
@@ -1043,11 +1087,12 @@ ${JSON.stringify(element, null, 2)}`
1043
1087
  * the provided href within the publication.
1044
1088
  */
1045
1089
  async setCoverImage(href, data) {
1090
+ var _a;
1046
1091
  const coverImageItem = await this.getCoverImageItem();
1047
1092
  if (coverImageItem) {
1048
1093
  await this.removeManifestItem(coverImageItem.id);
1049
1094
  }
1050
- const mediaType = lookup(href);
1095
+ const mediaType = (_a = MediaType.fromPath(href)) == null ? void 0 : _a.mime;
1051
1096
  if (!mediaType)
1052
1097
  throw new Error(`Invalid file extension for cover image: ${href}`);
1053
1098
  await this.addManifestItem(
@@ -2026,35 +2071,81 @@ ${JSON.stringify(element, null, 2)}`
2026
2071
  return this.getNavigation("page-list", { resolveToRoot });
2027
2072
  }
2028
2073
  /**
2029
- * Returns a Zip Entry path for an HREF
2074
+ * Name the entry a resolved URL addresses.
2075
+ *
2076
+ * A correctly authored publication percent-encodes its hrefs, so the
2077
+ * decoded reading is the one the spec calls for and the one we prefer.
2078
+ * Some publications instead write the entry name verbatim, so when the two
2079
+ * readings differ we ask the archive which one it holds. A verbatim name
2080
+ * carrying a bare `%`, as `100%.xhtml` does, is not a valid encoding at all
2081
+ * and can only be its own answer.
2030
2082
  */
2031
- resolveInternalHref(from, href) {
2032
- const startPath = dirname(from);
2033
- return resolve(
2034
- this.adapter.rootPath,
2035
- hrefToPlatformPath(startPath),
2036
- hrefToPlatformPath(href)
2037
- );
2083
+ async entryForUrl(url) {
2084
+ const asWritten = url.pathname.slice(1);
2085
+ let decoded;
2086
+ try {
2087
+ decoded = decodeURIComponent(asWritten);
2088
+ } catch {
2089
+ return asWritten;
2090
+ }
2091
+ if (decoded === asWritten) {
2092
+ return decoded;
2093
+ }
2094
+ if (await this.adapter.exists(decoded)) {
2095
+ return decoded;
2096
+ }
2097
+ return await this.adapter.exists(asWritten) ? asWritten : decoded;
2098
+ }
2099
+ /**
2100
+ * Resolve an href to the name of the archive entry it addresses.
2101
+ *
2102
+ * @param from The entry the href appears in; it resolves against that
2103
+ * entry's directory
2104
+ * @throws when the href addresses something outside the publication
2105
+ */
2106
+ async resolveEntry(from, href) {
2107
+ const url = resolveEntryUrl(from, href);
2108
+ if (!url) {
2109
+ throw new Error(
2110
+ `href does not address an entry in this publication: ${href}`
2111
+ );
2112
+ }
2113
+ return this.entryForUrl(url);
2038
2114
  }
2039
2115
  /**
2040
2116
  * Returns a path-relative-scheme-less URL, relative to the
2041
2117
  * container root.
2042
2118
  *
2119
+ * An href that points outside the publication, such as an external link in
2120
+ * a navigation document, is handed back unchanged.
2121
+ *
2043
2122
  * @param href The href to resolve
2044
2123
  * @param [relativeTo] Optional - The href to resolve this href relative to.
2045
2124
  Use if resolving a relative href from a file other than the package document.
2046
2125
  */
2047
2126
  async resolveHref(href, relativeTo, { toRoot } = {}) {
2127
+ if (href.startsWith("#")) return href;
2048
2128
  const rootfile = await this.getRootfile();
2049
- const from = relativeTo ? this.resolveInternalHref(rootfile, relativeTo) : rootfile;
2050
- const path = this.resolveInternalHref(from, href);
2051
- return path.replace(toRoot ? this.adapter.rootPath : dirname(rootfile), "").slice(1);
2129
+ const from = relativeTo ? await this.resolveEntry(rootfile, relativeTo) : rootfile;
2130
+ const url = resolveEntryUrl(from, href);
2131
+ if (!url) return href;
2132
+ const entry = (await this.entryForUrl(url)).split("/");
2133
+ const base = toRoot ? [] : rootfile.split("/").slice(0, -1);
2134
+ let shared = 0;
2135
+ while (shared < base.length && base[shared] === entry[shared]) {
2136
+ shared++;
2137
+ }
2138
+ const relative = [
2139
+ ...Array(base.length - shared).fill(".."),
2140
+ ...entry.slice(shared).map(encodeURIComponent)
2141
+ ];
2142
+ return relative.join("/") + url.hash;
2052
2143
  }
2053
2144
  async readFileContents(href, relativeTo, encoding) {
2054
2145
  const rootfile = await this.getRootfile();
2055
- const from = relativeTo ? this.resolveInternalHref(rootfile, relativeTo) : rootfile;
2056
- const path = this.resolveInternalHref(from, href);
2057
- const itemEntry = encoding ? await this.getFileData(path, encoding) : await this.getFileData(path);
2146
+ const from = relativeTo ? await this.resolveEntry(rootfile, relativeTo) : rootfile;
2147
+ const entry = await this.resolveEntry(from, href);
2148
+ const itemEntry = encoding ? await this.adapter.read(entry, encoding) : await this.adapter.read(entry);
2058
2149
  return itemEntry;
2059
2150
  }
2060
2151
  async readItemContents(id, encoding) {
@@ -2063,8 +2154,8 @@ ${JSON.stringify(element, null, 2)}`
2063
2154
  const manifestItem = manifest[id];
2064
2155
  if (!manifestItem)
2065
2156
  throw new Error(`Could not find item with id "${id}" in manifest`);
2066
- const path = this.resolveInternalHref(rootfile, manifestItem.href);
2067
- const itemEntry = encoding ? await this.getFileData(path, encoding) : await this.getFileData(path);
2157
+ const entry = await this.resolveEntry(rootfile, manifestItem.href);
2158
+ const itemEntry = encoding ? await this.adapter.read(entry, encoding) : await this.adapter.read(entry);
2068
2159
  return itemEntry;
2069
2160
  }
2070
2161
  /**
@@ -2122,11 +2213,11 @@ ${JSON.stringify(element, null, 2)}`
2122
2213
  if (!manifestItem)
2123
2214
  throw new Error(`Could not find item with id "${id}" in manifest`);
2124
2215
  memoize.clear(this.readXhtmlItemContents);
2125
- const href = this.resolveInternalHref(rootfile, manifestItem.href);
2216
+ const entry = await this.resolveEntry(rootfile, manifestItem.href);
2126
2217
  if (encoding === "utf-8") {
2127
- await this.writeEntryContents(href, contents, encoding);
2218
+ await this.writeEntryContents(entry, contents, encoding);
2128
2219
  } else {
2129
- await this.writeEntryContents(href, contents);
2220
+ await this.writeEntryContents(entry, contents);
2130
2221
  }
2131
2222
  }
2132
2223
  /**
@@ -2197,7 +2288,7 @@ ${JSON.stringify(element, null, 2)}`
2197
2288
  });
2198
2289
  this.manifest = null;
2199
2290
  const rootfile = await this.getRootfile();
2200
- const filename = this.resolveInternalHref(rootfile, item.href);
2291
+ const filename = await this.resolveEntry(rootfile, item.href);
2201
2292
  const data = encoding === "utf-8" || encoding === "xml" ? new TextEncoder().encode(
2202
2293
  encoding === "utf-8" ? contents : await Epub.xmlBuilder.build(
2203
2294
  contents
@@ -2381,10 +2472,7 @@ ${JSON.stringify(element, null, 2)}`
2381
2472
  );
2382
2473
  const spineTocId = (_a = spine == null ? void 0 : spine[":@"]) == null ? void 0 : _a["@_toc"];
2383
2474
  const ncxItem = spineTocId ? manifest[spineTocId] : Object.values(manifest).find(
2384
- (item) => {
2385
- var _a2;
2386
- return ((_a2 = item.mediaType) == null ? void 0 : _a2.toLowerCase()) === "application/x-dtbncx+xml";
2387
- }
2475
+ (item) => MediaType.fromMime(item.mediaType ?? "") === MediaType.NCX
2388
2476
  );
2389
2477
  if (!ncxItem) return [];
2390
2478
  const ncxContent = await this.readItemContents(ncxItem.id, "utf-8");
@@ -2525,7 +2613,12 @@ class EpubFactory {
2525
2613
  "This is not a valid EPUB publication. Could not read the package document."
2526
2614
  );
2527
2615
  }
2528
- await Epub.assertEpub3(epub);
2616
+ try {
2617
+ await Epub.assertEpub3(epub);
2618
+ } catch (error) {
2619
+ epub.discardAndClose();
2620
+ throw error;
2621
+ }
2529
2622
  return epub;
2530
2623
  }
2531
2624
  /**
@@ -2565,14 +2658,11 @@ class EpubFactory {
2565
2658
  const container = encoder.encode(`<?xml version="1.0"?>
2566
2659
  <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
2567
2660
  <rootfiles>
2568
- <rootfile media-type="application/oebps-package+xml" full-path="OEBPS/content.opf"/>
2661
+ <rootfile media-type="${MediaType.OPF.mime}" full-path="OEBPS/content.opf"/>
2569
2662
  </rootfiles>
2570
2663
  </container>
2571
2664
  `);
2572
- await adapter.write(
2573
- join(adapter.rootPath, "META-INF", "container.xml"),
2574
- container
2575
- );
2665
+ await adapter.write("META-INF/container.xml", container);
2576
2666
  const packageDocument = encoder.encode(`<?xml version="1.0"?>
2577
2667
  <package unique-identifier="pub-id" dir="${language.textInfo.direction}" xml:lang="${language.toString()}" version="3.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
2578
2668
  <metadata>
@@ -2583,10 +2673,7 @@ class EpubFactory {
2583
2673
  </spine>
2584
2674
  </package>
2585
2675
  `);
2586
- await adapter.write(
2587
- join(adapter.rootPath, "OEBPS", "content.opf"),
2588
- packageDocument
2589
- );
2676
+ await adapter.write("OEBPS/content.opf", packageDocument);
2590
2677
  const epub = new Epub(this.adapterClass, adapter, path);
2591
2678
  const metadata = [
2592
2679
  {
@@ -2635,7 +2722,6 @@ class EpubFactory {
2635
2722
  * @throws when the adapter is read-only
2636
2723
  */
2637
2724
  async upgrade(path, options = {}) {
2638
- var _a;
2639
2725
  if (!this.adapterClass.capabilities.writable) {
2640
2726
  throw new EpubReadOnlyError(
2641
2727
  `adapter ${this.adapterClass.kind} is read-only; cannot upgrade`
@@ -2661,49 +2747,55 @@ class EpubFactory {
2661
2747
  "This is not a valid EPUB publication. Could not read the package document."
2662
2748
  );
2663
2749
  }
2664
- const version = await epub.getVersion();
2665
- if (version.startsWith("3.")) {
2666
- return epub;
2667
- }
2668
- const tocEntries = await epub.getNcxTableOfContents();
2669
- let landmarks = [];
2670
- await epub.withPackage((pkg) => {
2671
- landmarks = Upgrade.extractGuideLandmarks(pkg);
2672
- Upgrade.upgradePackageMetadata(pkg);
2673
- Upgrade.fixFontMimeTypes(pkg);
2674
- Upgrade.removeGuide(pkg);
2750
+ try {
2751
+ const version = await epub.getVersion();
2752
+ if (version.startsWith("3.")) {
2753
+ return epub;
2754
+ }
2755
+ const tocEntries = await epub.getNcxTableOfContents();
2756
+ let landmarks = [];
2757
+ await epub.withPackage((pkg) => {
2758
+ landmarks = Upgrade.extractGuideLandmarks(pkg);
2759
+ Upgrade.upgradePackageMetadata(pkg);
2760
+ Upgrade.fixFontMimeTypes(pkg);
2761
+ Upgrade.removeGuide(pkg);
2762
+ if (removeNcx) {
2763
+ Upgrade.removeSpineTocRef(pkg);
2764
+ }
2765
+ Upgrade.setPackageVersion(pkg, "3.0");
2766
+ });
2767
+ await Upgrade.collectManifestProperties(epub);
2675
2768
  if (removeNcx) {
2676
- Upgrade.removeSpineTocRef(pkg);
2769
+ await Upgrade.removeNcx(epub);
2677
2770
  }
2678
- Upgrade.setPackageVersion(pkg, "3.0");
2679
- });
2680
- await Upgrade.collectManifestProperties(epub);
2681
- if (removeNcx) {
2682
- await Upgrade.removeNcx(epub);
2683
- }
2684
- const navHref = await Upgrade.chooseNavHref(epub);
2685
- const navContent = await Upgrade.buildNavDocument(
2686
- epub,
2687
- tocEntries,
2688
- landmarks
2689
- );
2690
- await epub.addManifestItem(
2691
- {
2692
- id: "nav",
2693
- href: navHref,
2694
- mediaType: "application/xhtml+xml",
2695
- properties: ["nav"]
2696
- },
2697
- navContent,
2698
- "utf-8"
2699
- );
2700
- const manifest = await epub.getManifest();
2701
- for (const item of Object.values(manifest)) {
2702
- if (((_a = item.mediaType) == null ? void 0 : _a.toLowerCase()) !== "application/xhtml+xml") continue;
2703
- const contents = await epub.readXhtmlItemContents(item.id);
2704
- await epub.writeXhtmlItemContents(item.id, contents);
2771
+ const navHref = await Upgrade.chooseNavHref(epub);
2772
+ const navContent = await Upgrade.buildNavDocument(
2773
+ epub,
2774
+ tocEntries,
2775
+ landmarks
2776
+ );
2777
+ await epub.addManifestItem(
2778
+ {
2779
+ id: "nav",
2780
+ href: navHref,
2781
+ mediaType: MediaType.XHTML.mime,
2782
+ properties: ["nav"]
2783
+ },
2784
+ navContent,
2785
+ "utf-8"
2786
+ );
2787
+ const manifest = await epub.getManifest();
2788
+ for (const item of Object.values(manifest)) {
2789
+ if (MediaType.fromMime(item.mediaType ?? "") !== MediaType.XHTML)
2790
+ continue;
2791
+ const contents = await epub.readXhtmlItemContents(item.id);
2792
+ await epub.writeXhtmlItemContents(item.id, contents);
2793
+ }
2794
+ return epub;
2795
+ } catch (error) {
2796
+ epub.discardAndClose();
2797
+ throw error;
2705
2798
  }
2706
- return epub;
2707
2799
  }
2708
2800
  }
2709
2801
  export {
package/dist/upgrade.cjs CHANGED
@@ -41,6 +41,7 @@ __export(upgrade_exports, {
41
41
  });
42
42
  module.exports = __toCommonJS(upgrade_exports);
43
43
  var import_nanoid = require("nanoid");
44
+ var import_media_types = require("@storyteller-platform/media-types");
44
45
  var import_index = require("./index.cjs");
45
46
  const GUIDE_TO_EPUBTYPE = {
46
47
  acknowledgements: "acknowledgments",
@@ -88,31 +89,7 @@ const GUIDE_TO_EPUBTYPE = {
88
89
  "other.volume": "volume",
89
90
  "other.warning": "warning"
90
91
  };
91
- const XHTML_MEDIA_TYPES = /* @__PURE__ */ new Set([
92
- "application/xhtml+xml",
93
- "application/vnd.adobe-page-template+xml",
94
- "text/html"
95
- ]);
96
- const OEB_FONTS = /* @__PURE__ */ new Set([
97
- "application/vnd.ms-opentype",
98
- "application/x-font-ttf",
99
- "application/x-font-otf",
100
- "application/x-font-truetype",
101
- "application/font-sfnt",
102
- "application/font-woff",
103
- "application/font-woff2",
104
- "font/woff",
105
- "font/woff2",
106
- "font/otf",
107
- "font/ttf",
108
- "font/sfnt"
109
- ]);
110
- const FONT_MIME_BY_EXT = {
111
- ".ttf": "application/font-sfnt",
112
- ".otf": "application/font-sfnt",
113
- ".woff": "application/font-woff",
114
- ".woff2": "font/woff2"
115
- };
92
+ const XHTML_MEDIA_TYPES = /* @__PURE__ */ new Set([import_media_types.MediaType.XHTML, import_media_types.MediaType.HTML]);
116
93
  function getMetadataElement(pkg) {
117
94
  return import_index.Epub.findXmlChildByName("metadata", import_index.Epub.getXmlChildren(pkg));
118
95
  }
@@ -389,11 +366,10 @@ function removeSpineTocRef(pkg) {
389
366
  delete spine[":@"]["@_toc"];
390
367
  }
391
368
  async function collectManifestProperties(epub) {
392
- var _a;
393
369
  const manifest = await epub.getManifest();
394
370
  for (const item of Object.values(manifest)) {
395
- const mediaType = ((_a = item.mediaType) == null ? void 0 : _a.toLowerCase()) ?? "";
396
- if (!XHTML_MEDIA_TYPES.has(mediaType)) continue;
371
+ const mediaType = import_media_types.MediaType.fromMime(item.mediaType ?? "");
372
+ if (!mediaType || !XHTML_MEDIA_TYPES.has(mediaType)) continue;
397
373
  let xml;
398
374
  try {
399
375
  xml = await epub.readXhtmlItemContents(item.id);
@@ -414,21 +390,16 @@ async function collectManifestProperties(epub) {
414
390
  }
415
391
  }
416
392
  function fixFontMimeTypes(pkg) {
417
- var _a, _b;
393
+ var _a, _b, _c;
418
394
  const manifest = getManifestElement(pkg);
419
395
  if (!manifest) return;
420
396
  for (const item of findAllByName("item", manifest.manifest)) {
421
- const mt = (((_a = item[":@"]) == null ? void 0 : _a["@_media-type"]) ?? "").toLowerCase();
422
- if (!OEB_FONTS.has(mt)) continue;
423
- const href = ((_b = item[":@"]) == null ? void 0 : _b["@_href"]) ?? "";
424
- const dotIdx = href.lastIndexOf(".");
425
- if (dotIdx === -1) continue;
426
- const ext = href.slice(dotIdx).toLowerCase();
427
- const corrected = FONT_MIME_BY_EXT[ext];
428
- if (corrected && corrected !== mt) {
429
- item[":@"] ??= {};
430
- item[":@"]["@_media-type"] = corrected;
431
- }
397
+ const declared = ((_a = item[":@"]) == null ? void 0 : _a["@_media-type"]) ?? "";
398
+ if (!((_b = import_media_types.MediaType.fromMime(declared)) == null ? void 0 : _b.is("font"))) continue;
399
+ const corrected = import_media_types.MediaType.fromPath(((_c = item[":@"]) == null ? void 0 : _c["@_href"]) ?? "");
400
+ if (!(corrected == null ? void 0 : corrected.is("font")) || corrected.mime === declared) continue;
401
+ item[":@"] ??= {};
402
+ item[":@"]["@_media-type"] = corrected.mime;
432
403
  }
433
404
  }
434
405
  function buildTocOl(entries) {
@@ -521,10 +492,7 @@ async function chooseNavHref(epub) {
521
492
  async function removeNcx(epub) {
522
493
  const manifest = await epub.getManifest();
523
494
  const ncxItem = Object.values(manifest).find(
524
- (item) => {
525
- var _a;
526
- return ((_a = item.mediaType) == null ? void 0 : _a.toLowerCase()) === "application/x-dtbncx+xml";
527
- }
495
+ (item) => import_media_types.MediaType.fromMime(item.mediaType ?? "") === import_media_types.MediaType.NCX
528
496
  );
529
497
  if (ncxItem) {
530
498
  await epub.removeManifestItem(ncxItem.id);
@@ -339,10 +339,6 @@ declare class Epub {
339
339
  static assertEpub3(epub: Epub): Promise<void>;
340
340
  copy(path?: string): Promise<Epub>;
341
341
  private removeEntry;
342
- /**
343
- * Read raw bytes (or utf-8 text) from the underlying adapter
344
- */
345
- private getFileData;
346
342
  /**
347
343
  * Length of the underlying archive entry for a manifest item, in bytes
348
344
  * Necessary to compute the readium page count which is for COMPRESSED content
@@ -391,7 +387,7 @@ declare class Epub {
391
387
  */
392
388
  findMetadataItem(predicate: (entry: MetadataEntry) => boolean): Promise<{
393
389
  id: string | undefined;
394
- type: `?${string}` | `a${string}` | `b${string}` | `c${string}` | `d${string}` | `e${string}` | `f${string}` | `g${string}` | `h${string}` | `i${string}` | `j${string}` | `k${string}` | `l${string}` | `m${string}` | `n${string}` | `o${string}` | `p${string}` | `q${string}` | `r${string}` | `s${string}` | `t${string}` | `u${string}` | `v${string}` | `w${string}` | `x${string}` | `y${string}` | `z${string}` | `A${string}` | `B${string}` | `C${string}` | `D${string}` | `E${string}` | `F${string}` | `G${string}` | `H${string}` | `I${string}` | `J${string}` | `K${string}` | `L${string}` | `M${string}` | `N${string}` | `O${string}` | `P${string}` | `Q${string}` | `R${string}` | `S${string}` | `T${string}` | `U${string}` | `V${string}` | `W${string}` | `X${string}` | `Y${string}` | `Z${string}`;
390
+ type: `a${string}` | `b${string}` | `c${string}` | `d${string}` | `e${string}` | `f${string}` | `g${string}` | `h${string}` | `i${string}` | `j${string}` | `k${string}` | `l${string}` | `m${string}` | `n${string}` | `o${string}` | `p${string}` | `q${string}` | `r${string}` | `s${string}` | `t${string}` | `u${string}` | `v${string}` | `w${string}` | `x${string}` | `y${string}` | `z${string}` | `A${string}` | `B${string}` | `C${string}` | `D${string}` | `E${string}` | `F${string}` | `G${string}` | `H${string}` | `I${string}` | `J${string}` | `K${string}` | `L${string}` | `M${string}` | `N${string}` | `O${string}` | `P${string}` | `Q${string}` | `R${string}` | `S${string}` | `T${string}` | `U${string}` | `V${string}` | `W${string}` | `X${string}` | `Y${string}` | `Z${string}` | `?${string}`;
395
391
  properties: {
396
392
  [k: string]: string;
397
393
  };
@@ -403,7 +399,7 @@ declare class Epub {
403
399
  */
404
400
  findAllMetadataItems(predicate: (entry: MetadataEntry) => boolean): Promise<{
405
401
  id: string | undefined;
406
- type: `?${string}` | `a${string}` | `b${string}` | `c${string}` | `d${string}` | `e${string}` | `f${string}` | `g${string}` | `h${string}` | `i${string}` | `j${string}` | `k${string}` | `l${string}` | `m${string}` | `n${string}` | `o${string}` | `p${string}` | `q${string}` | `r${string}` | `s${string}` | `t${string}` | `u${string}` | `v${string}` | `w${string}` | `x${string}` | `y${string}` | `z${string}` | `A${string}` | `B${string}` | `C${string}` | `D${string}` | `E${string}` | `F${string}` | `G${string}` | `H${string}` | `I${string}` | `J${string}` | `K${string}` | `L${string}` | `M${string}` | `N${string}` | `O${string}` | `P${string}` | `Q${string}` | `R${string}` | `S${string}` | `T${string}` | `U${string}` | `V${string}` | `W${string}` | `X${string}` | `Y${string}` | `Z${string}`;
402
+ type: `a${string}` | `b${string}` | `c${string}` | `d${string}` | `e${string}` | `f${string}` | `g${string}` | `h${string}` | `i${string}` | `j${string}` | `k${string}` | `l${string}` | `m${string}` | `n${string}` | `o${string}` | `p${string}` | `q${string}` | `r${string}` | `s${string}` | `t${string}` | `u${string}` | `v${string}` | `w${string}` | `x${string}` | `y${string}` | `z${string}` | `A${string}` | `B${string}` | `C${string}` | `D${string}` | `E${string}` | `F${string}` | `G${string}` | `H${string}` | `I${string}` | `J${string}` | `K${string}` | `L${string}` | `M${string}` | `N${string}` | `O${string}` | `P${string}` | `Q${string}` | `R${string}` | `S${string}` | `T${string}` | `U${string}` | `V${string}` | `W${string}` | `X${string}` | `Y${string}` | `Z${string}` | `?${string}`;
407
403
  properties: {
408
404
  [k: string]: string;
409
405
  };
@@ -922,13 +918,31 @@ declare class Epub {
922
918
  resolveToRoot?: boolean;
923
919
  }): Promise<Navigation | null>;
924
920
  /**
925
- * Returns a Zip Entry path for an HREF
921
+ * Name the entry a resolved URL addresses.
922
+ *
923
+ * A correctly authored publication percent-encodes its hrefs, so the
924
+ * decoded reading is the one the spec calls for and the one we prefer.
925
+ * Some publications instead write the entry name verbatim, so when the two
926
+ * readings differ we ask the archive which one it holds. A verbatim name
927
+ * carrying a bare `%`, as `100%.xhtml` does, is not a valid encoding at all
928
+ * and can only be its own answer.
926
929
  */
927
- private resolveInternalHref;
930
+ private entryForUrl;
931
+ /**
932
+ * Resolve an href to the name of the archive entry it addresses.
933
+ *
934
+ * @param from The entry the href appears in; it resolves against that
935
+ * entry's directory
936
+ * @throws when the href addresses something outside the publication
937
+ */
938
+ private resolveEntry;
928
939
  /**
929
940
  * Returns a path-relative-scheme-less URL, relative to the
930
941
  * container root.
931
942
  *
943
+ * An href that points outside the publication, such as an external link in
944
+ * a navigation document, is handed back unchanged.
945
+ *
932
946
  * @param href The href to resolve
933
947
  * @param [relativeTo] Optional - The href to resolve this href relative to.
934
948
  Use if resolving a relative href from a file other than the package document.