@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.
@@ -13,39 +13,23 @@ import {
13
13
  stat,
14
14
  writeFile
15
15
  } from "node:fs/promises";
16
- import { tmpdir } from "node:os";
17
16
  import { pipeline } from "node:stream/promises";
18
- import { fromBuffer, open } from "yauzl-promise";
19
17
  import { ZipFile } from "yazl";
18
+ import { scratchPath } from "@storyteller-platform/fs";
19
+ import { MediaType } from "@storyteller-platform/media-types";
20
20
  import { dirname, join, sep } from "@storyteller-platform/path";
21
- const MP3_FILE_EXTENSIONS = [".mp3"];
22
- const MPEG4_FILE_EXTENSIONS = [".mp4", ".m4a", ".m4b"];
23
- const AAC_FILE_EXTENSIONS = [".aac"];
24
- const OGG_FILE_EXTENSIONS = [".ogg", ".oga", ".mogg"];
25
- const OPUS_FILE_EXTENSIONS = [".opus"];
26
- const WAVE_FILE_EXTENSIONS = [".wav"];
27
- const AIFF_FILE_EXTENSIONS = [".aiff"];
28
- const FLAC_FILE_EXTENSIONS = [".flac"];
29
- const ALAC_FILE_EXTENSIONS = [".alac"];
30
- const WEBM_FILE_EXTENSIONS = [".weba"];
31
- const AUDIO_FILE_EXTENSIONS = [
32
- ...MP3_FILE_EXTENSIONS,
33
- ...AAC_FILE_EXTENSIONS,
34
- ...MPEG4_FILE_EXTENSIONS,
35
- ...OPUS_FILE_EXTENSIONS,
36
- ...OGG_FILE_EXTENSIONS,
37
- ...WAVE_FILE_EXTENSIONS,
38
- ...AIFF_FILE_EXTENSIONS,
39
- ...FLAC_FILE_EXTENSIONS,
40
- ...ALAC_FILE_EXTENSIONS,
41
- ...WEBM_FILE_EXTENSIONS
42
- ];
43
- function isAudioFile(filenameOrExt) {
44
- const lowered = filenameOrExt.toLowerCase();
45
- return AUDIO_FILE_EXTENSIONS.some((ext) => lowered.endsWith(ext));
46
- }
21
+ import { openZip } from "../zip.js";
22
+ const PRECOMPRESSED_KINDS = /* @__PURE__ */ new Set([
23
+ "audio",
24
+ "image",
25
+ "archive",
26
+ "package"
27
+ ]);
47
28
  function mintRootPath() {
48
- return join(tmpdir(), `storyteller-platform-epub-${randomUUID()}`);
29
+ return scratchPath(`storyteller-platform-epub-${randomUUID()}`);
30
+ }
31
+ function isStorableEntryName(entry) {
32
+ return !entry.split("/").some((segment) => segment === "" || segment === "." || segment === "..");
49
33
  }
50
34
  class TmpFsAdapter {
51
35
  constructor(rootPath) {
@@ -58,7 +42,7 @@ class TmpFsAdapter {
58
42
  try {
59
43
  var _stack = [];
60
44
  try {
61
- const zipfile = typeof source === "string" ? await open(source) : await fromBuffer(Buffer.from(source));
45
+ const zipfile = await openZip(source);
62
46
  const stack = __using(_stack, new AsyncDisposableStack(), true);
63
47
  stack.defer(async () => {
64
48
  await zipfile.close();
@@ -67,7 +51,8 @@ class TmpFsAdapter {
67
51
  if (entry.filename.endsWith("/")) {
68
52
  continue;
69
53
  }
70
- const writePath = join(rootPath, entry.filename);
54
+ if (!isStorableEntryName(entry.filename)) continue;
55
+ const writePath = join(rootPath, ...entry.filename.split("/"));
71
56
  const readStream = await entry.openReadStream();
72
57
  await mkdir(dirname(writePath), { recursive: true });
73
58
  const writeStream = createWriteStream(writePath);
@@ -90,19 +75,36 @@ class TmpFsAdapter {
90
75
  await mkdir(rootPath, { recursive: true });
91
76
  return new TmpFsAdapter(rootPath);
92
77
  }
93
- async read(path, encoding) {
94
- return await readFile(path, encoding);
78
+ diskPath(entry) {
79
+ if (!isStorableEntryName(entry)) {
80
+ const err = new Error(`ENOENT: no such entry in EPUB archive: ${entry}`);
81
+ err.code = "ENOENT";
82
+ throw err;
83
+ }
84
+ return join(this.rootPath, ...entry.split("/"));
85
+ }
86
+ async read(entry, encoding) {
87
+ return await readFile(this.diskPath(entry), encoding);
95
88
  }
96
- async write(path, data, encoding) {
89
+ async write(entry, data, encoding) {
90
+ const path = this.diskPath(entry);
97
91
  await mkdir(dirname(path), { recursive: true });
98
92
  await writeFile(path, data, encoding);
99
93
  }
100
- async remove(path) {
101
- await rm(path);
94
+ async remove(entry) {
95
+ await rm(this.diskPath(entry));
96
+ }
97
+ async exists(entry) {
98
+ try {
99
+ await stat(this.diskPath(entry));
100
+ return true;
101
+ } catch {
102
+ return false;
103
+ }
102
104
  }
103
- async archiveLength(path) {
105
+ async archiveLength(entry) {
104
106
  try {
105
- const st = await stat(path);
107
+ const st = await stat(this.diskPath(entry));
106
108
  return st.size;
107
109
  } catch {
108
110
  return 0;
@@ -118,7 +120,7 @@ class TmpFsAdapter {
118
120
  const absolutePath = join(entry.parentPath, entry.name);
119
121
  yield {
120
122
  absolutePath,
121
- relativePath: absolutePath.replace(`${this.rootPath}${sep}`, "")
123
+ relativePath: absolutePath.slice(this.rootPath.length + 1).split(sep).join("/")
122
124
  };
123
125
  }
124
126
  }
@@ -133,31 +135,33 @@ class TmpFsAdapter {
133
135
  return new TmpFsAdapter(newRoot);
134
136
  }
135
137
  async serialize(targetPath) {
138
+ var _a;
136
139
  var _stack = [];
137
140
  try {
138
- const tmpArchivePath = join(
139
- tmpdir(),
140
- `storyteller-platform-epub-${randomUUID()}`
141
- );
142
- const { promise, resolve } = Promise.withResolvers();
141
+ const tmpArchivePath = mintRootPath();
142
+ const { promise, resolve, reject } = Promise.withResolvers();
143
143
  const zipfile = new ZipFile();
144
144
  const writeStream = createWriteStream(tmpArchivePath);
145
145
  writeStream.on("close", () => {
146
146
  resolve();
147
147
  });
148
+ writeStream.on("error", reject);
149
+ zipfile.outputStream.on("error", reject);
148
150
  const stack = __using(_stack, new AsyncDisposableStack(), true);
149
151
  stack.defer(async () => {
150
152
  writeStream.close();
151
153
  await rm(tmpArchivePath, { force: true });
152
154
  });
153
155
  zipfile.outputStream.pipe(writeStream);
154
- zipfile.addBuffer(Buffer.from("application/epub+zip"), "mimetype", {
156
+ zipfile.addBuffer(Buffer.from(MediaType.EPUB.mime), "mimetype", {
155
157
  compress: false
156
158
  });
157
159
  for await (const entry of this.list()) {
158
160
  if (entry.relativePath === "mimetype") continue;
159
161
  zipfile.addFile(entry.absolutePath, entry.relativePath, {
160
- compress: !isAudioFile(entry.absolutePath)
162
+ compress: !PRECOMPRESSED_KINDS.has(
163
+ (_a = MediaType.fromPath(entry.absolutePath)) == null ? void 0 : _a.kind
164
+ )
161
165
  });
162
166
  }
163
167
  zipfile.end();
@@ -24,15 +24,15 @@ module.exports = __toCommonJS(epubcheck_exports);
24
24
  var import_node_child_process = require("node:child_process");
25
25
  var import_node_crypto = require("node:crypto");
26
26
  var import_promises = require("node:fs/promises");
27
- var import_node_os = require("node:os");
28
27
  var import_node_path = require("node:path");
29
28
  var import_node_url = require("node:url");
30
29
  var import_node_util = require("node:util");
30
+ var import_fs = require("@storyteller-platform/fs");
31
31
  const import_meta = {};
32
32
  const here = (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
33
33
  const JAR = (0, import_node_path.join)(here, "vendors", "epubcheck-5.3.0", "epubcheck.jar");
34
34
  async function epubcheck(epubFile) {
35
- const output = (0, import_node_path.join)((0, import_node_os.tmpdir)(), `epubcheck-${(0, import_node_crypto.randomUUID)()}.json`);
35
+ const output = (0, import_fs.scratchPath)(`epubcheck-${(0, import_node_crypto.randomUUID)()}.json`);
36
36
  const execFilePromise = (0, import_node_util.promisify)(import_node_child_process.execFile);
37
37
  try {
38
38
  await execFilePromise("java", [
package/dist/epubcheck.js CHANGED
@@ -2,14 +2,14 @@ import "./chunk-BIEQXUOY.js";
2
2
  import { execFile } from "node:child_process";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { readFile, unlink } from "node:fs/promises";
5
- import { tmpdir } from "node:os";
6
5
  import { dirname, join } from "node:path";
7
6
  import { fileURLToPath } from "node:url";
8
7
  import { promisify } from "node:util";
8
+ import { scratchPath } from "@storyteller-platform/fs";
9
9
  const here = dirname(fileURLToPath(import.meta.url));
10
10
  const JAR = join(here, "vendors", "epubcheck-5.3.0", "epubcheck.jar");
11
11
  async function epubcheck(epubFile) {
12
- const output = join(tmpdir(), `epubcheck-${randomUUID()}.json`);
12
+ const output = scratchPath(`epubcheck-${randomUUID()}.json`);
13
13
  const execFilePromise = promisify(execFile);
14
14
  try {
15
15
  await execFilePromise("java", [
package/dist/index.cjs CHANGED
@@ -40,8 +40,8 @@ var import_promises = require("node:fs/promises");
40
40
  var import_async_mutex = require("async-mutex");
41
41
  var import_fast_xml_parser = require("fast-xml-parser");
42
42
  var import_mem = __toESM(require("mem"), 1);
43
- var import_mime_types = require("mime-types");
44
43
  var import_nanoid = require("nanoid");
44
+ var import_media_types = require("@storyteller-platform/media-types");
45
45
  var import_path = require("@storyteller-platform/path");
46
46
  var import_tmpfs = require("./adapters/tmpfs.cjs");
47
47
  var Upgrade = __toESM(require("./upgrade.ts"), 1);
@@ -51,6 +51,61 @@ class EpubVersionError extends Error {
51
51
  }
52
52
  class EpubReadOnlyError extends Error {
53
53
  }
54
+ const OPF_NAMESPACE = "http://www.idpf.org/2007/opf";
55
+ const ENTRY_ORIGIN = "epub:///";
56
+ function entryToUrlPath(entry) {
57
+ return entry.split("/").map(encodeURIComponent).join("/");
58
+ }
59
+ function resolveEntryUrl(from, href) {
60
+ const base = new URL(entryToUrlPath(from), ENTRY_ORIGIN);
61
+ if (!URL.canParse(href, base.toString())) return null;
62
+ const url = new URL(href, base);
63
+ return url.href.startsWith(ENTRY_ORIGIN) ? url : null;
64
+ }
65
+ function renameOpfAttributes(attrs, prefix) {
66
+ const attrPrefix = `@_${prefix}:`;
67
+ const renamed = {};
68
+ for (const [key, value] of Object.entries(attrs)) {
69
+ if (key === `@_xmlns:${prefix}`) continue;
70
+ if (key.startsWith(attrPrefix)) {
71
+ renamed[`@_opf:${key.slice(attrPrefix.length)}`] = value;
72
+ } else {
73
+ renamed[key] = value;
74
+ }
75
+ }
76
+ return renamed;
77
+ }
78
+ function stripOpfPrefix(node, prefix) {
79
+ if (Epub.isXmlTextNode(node)) return node;
80
+ const name = Epub.getXmlElementName(node);
81
+ const bareName = name.startsWith(`${prefix}:`) ? name.slice(prefix.length + 1) : name;
82
+ const element = {
83
+ [bareName]: Epub.getXmlChildren(node).map(
84
+ (child) => stripOpfPrefix(child, prefix)
85
+ )
86
+ };
87
+ if (node[":@"]) {
88
+ element[":@"] = renameOpfAttributes(node[":@"], prefix);
89
+ }
90
+ return element;
91
+ }
92
+ function normalizeOpfNamespacePrefixes(doc) {
93
+ var _a;
94
+ const root = doc.find(
95
+ (node) => !Epub.isXmlTextNode(node) && Epub.getXmlElementName(node).endsWith(":package")
96
+ );
97
+ if (!root) return doc;
98
+ const prefix = Epub.getXmlElementName(root).slice(0, -":package".length);
99
+ if (((_a = root[":@"]) == null ? void 0 : _a[`@_xmlns:${prefix}`]) !== OPF_NAMESPACE) return doc;
100
+ const stripped = stripOpfPrefix(root, prefix);
101
+ stripped[":@"] = {
102
+ ...stripped[":@"],
103
+ "@_xmlns": OPF_NAMESPACE,
104
+ "@_xmlns:opf": OPF_NAMESPACE
105
+ };
106
+ doc[doc.indexOf(root)] = stripped;
107
+ return doc;
108
+ }
54
109
  class Epub {
55
110
  /**
56
111
  * Prefer the static factories ({@link Epub.using}, {@link Epub.from},
@@ -331,14 +386,7 @@ ${JSON.stringify(element, null, 2)}`
331
386
  );
332
387
  }
333
388
  const rootfile = await this.getRootfile();
334
- const filename = this.resolveInternalHref(rootfile, href);
335
- await this.adapter.remove(filename);
336
- }
337
- async getFileData(path, encoding) {
338
- if (encoding) {
339
- return this.adapter.read(path, encoding);
340
- }
341
- return this.adapter.read(path);
389
+ await this.adapter.remove(await this.resolveEntry(rootfile, href));
342
390
  }
343
391
  /**
344
392
  * Length of the underlying archive entry for a manifest item, in bytes
@@ -351,14 +399,15 @@ ${JSON.stringify(element, null, 2)}`
351
399
  const manifestItem = manifest[id];
352
400
  if (!manifestItem)
353
401
  throw new Error(`Could not find item with id "${id}" in manifest`);
354
- const path = this.resolveInternalHref(rootfile, manifestItem.href);
355
- return this.adapter.archiveLength(path);
402
+ return this.adapter.archiveLength(
403
+ await this.resolveEntry(rootfile, manifestItem.href)
404
+ );
356
405
  }
357
406
  async getRootfile() {
358
407
  var _a;
359
408
  if (this.rootfile !== null) return this.rootfile;
360
- const containerString = await this.getFileData(
361
- (0, import_path.join)(this.adapter.rootPath, "META-INF", "container.xml"),
409
+ const containerString = await this.adapter.read(
410
+ "META-INF/container.xml",
362
411
  "utf-8"
363
412
  );
364
413
  if (!containerString)
@@ -382,7 +431,7 @@ ${JSON.stringify(element, null, 2)}`
382
431
  Epub.getXmlChildren(rootfiles),
383
432
  (node) => {
384
433
  var _a2;
385
- return !Epub.isXmlTextNode(node) && ((_a2 = node[":@"]) == null ? void 0 : _a2["@_media-type"]) === "application/oebps-package+xml";
434
+ return !Epub.isXmlTextNode(node) && ((_a2 = node[":@"]) == null ? void 0 : _a2["@_media-type"]) === import_media_types.MediaType.OPF.mime;
386
435
  }
387
436
  );
388
437
  const fullPath = (_a = rootfile == null ? void 0 : rootfile[":@"]) == null ? void 0 : _a["@_full-path"];
@@ -390,12 +439,12 @@ ${JSON.stringify(element, null, 2)}`
390
439
  throw new Error(
391
440
  "Failed to parse EPUB container.xml: Found no rootfile element"
392
441
  );
393
- this.rootfile = (0, import_path.resolve)(this.adapter.rootPath, fullPath);
442
+ this.rootfile = await this.resolveEntry("", fullPath);
394
443
  return this.rootfile;
395
444
  }
396
445
  async getPackageDocument() {
397
446
  const rootfile = await this.getRootfile();
398
- const packageDocumentString = await this.getFileData(rootfile, "utf-8");
447
+ const packageDocumentString = await this.adapter.read(rootfile, "utf-8");
399
448
  if (!packageDocumentString)
400
449
  throw new Error(
401
450
  `Failed to parse EPUB: could not find package document at ${rootfile}`
@@ -403,7 +452,7 @@ ${JSON.stringify(element, null, 2)}`
403
452
  const packageDocument = Epub.xmlParser.parse(
404
453
  packageDocumentString
405
454
  );
406
- return packageDocument;
455
+ return normalizeOpfNamespacePrefixes(packageDocument);
407
456
  }
408
457
  async getPackageElement() {
409
458
  const packageDocument = await this.getPackageDocument();
@@ -1075,11 +1124,12 @@ ${JSON.stringify(element, null, 2)}`
1075
1124
  * the provided href within the publication.
1076
1125
  */
1077
1126
  async setCoverImage(href, data) {
1127
+ var _a;
1078
1128
  const coverImageItem = await this.getCoverImageItem();
1079
1129
  if (coverImageItem) {
1080
1130
  await this.removeManifestItem(coverImageItem.id);
1081
1131
  }
1082
- const mediaType = (0, import_mime_types.lookup)(href);
1132
+ const mediaType = (_a = import_media_types.MediaType.fromPath(href)) == null ? void 0 : _a.mime;
1083
1133
  if (!mediaType)
1084
1134
  throw new Error(`Invalid file extension for cover image: ${href}`);
1085
1135
  await this.addManifestItem(
@@ -2058,35 +2108,81 @@ ${JSON.stringify(element, null, 2)}`
2058
2108
  return this.getNavigation("page-list", { resolveToRoot });
2059
2109
  }
2060
2110
  /**
2061
- * Returns a Zip Entry path for an HREF
2111
+ * Name the entry a resolved URL addresses.
2112
+ *
2113
+ * A correctly authored publication percent-encodes its hrefs, so the
2114
+ * decoded reading is the one the spec calls for and the one we prefer.
2115
+ * Some publications instead write the entry name verbatim, so when the two
2116
+ * readings differ we ask the archive which one it holds. A verbatim name
2117
+ * carrying a bare `%`, as `100%.xhtml` does, is not a valid encoding at all
2118
+ * and can only be its own answer.
2062
2119
  */
2063
- resolveInternalHref(from, href) {
2064
- const startPath = (0, import_path.dirname)(from);
2065
- return (0, import_path.resolve)(
2066
- this.adapter.rootPath,
2067
- (0, import_path.hrefToPlatformPath)(startPath),
2068
- (0, import_path.hrefToPlatformPath)(href)
2069
- );
2120
+ async entryForUrl(url) {
2121
+ const asWritten = url.pathname.slice(1);
2122
+ let decoded;
2123
+ try {
2124
+ decoded = decodeURIComponent(asWritten);
2125
+ } catch {
2126
+ return asWritten;
2127
+ }
2128
+ if (decoded === asWritten) {
2129
+ return decoded;
2130
+ }
2131
+ if (await this.adapter.exists(decoded)) {
2132
+ return decoded;
2133
+ }
2134
+ return await this.adapter.exists(asWritten) ? asWritten : decoded;
2135
+ }
2136
+ /**
2137
+ * Resolve an href to the name of the archive entry it addresses.
2138
+ *
2139
+ * @param from The entry the href appears in; it resolves against that
2140
+ * entry's directory
2141
+ * @throws when the href addresses something outside the publication
2142
+ */
2143
+ async resolveEntry(from, href) {
2144
+ const url = resolveEntryUrl(from, href);
2145
+ if (!url) {
2146
+ throw new Error(
2147
+ `href does not address an entry in this publication: ${href}`
2148
+ );
2149
+ }
2150
+ return this.entryForUrl(url);
2070
2151
  }
2071
2152
  /**
2072
2153
  * Returns a path-relative-scheme-less URL, relative to the
2073
2154
  * container root.
2074
2155
  *
2156
+ * An href that points outside the publication, such as an external link in
2157
+ * a navigation document, is handed back unchanged.
2158
+ *
2075
2159
  * @param href The href to resolve
2076
2160
  * @param [relativeTo] Optional - The href to resolve this href relative to.
2077
2161
  Use if resolving a relative href from a file other than the package document.
2078
2162
  */
2079
2163
  async resolveHref(href, relativeTo, { toRoot } = {}) {
2164
+ if (href.startsWith("#")) return href;
2080
2165
  const rootfile = await this.getRootfile();
2081
- const from = relativeTo ? this.resolveInternalHref(rootfile, relativeTo) : rootfile;
2082
- const path = this.resolveInternalHref(from, href);
2083
- return path.replace(toRoot ? this.adapter.rootPath : (0, import_path.dirname)(rootfile), "").slice(1);
2166
+ const from = relativeTo ? await this.resolveEntry(rootfile, relativeTo) : rootfile;
2167
+ const url = resolveEntryUrl(from, href);
2168
+ if (!url) return href;
2169
+ const entry = (await this.entryForUrl(url)).split("/");
2170
+ const base = toRoot ? [] : rootfile.split("/").slice(0, -1);
2171
+ let shared = 0;
2172
+ while (shared < base.length && base[shared] === entry[shared]) {
2173
+ shared++;
2174
+ }
2175
+ const relative = [
2176
+ ...Array(base.length - shared).fill(".."),
2177
+ ...entry.slice(shared).map(encodeURIComponent)
2178
+ ];
2179
+ return relative.join("/") + url.hash;
2084
2180
  }
2085
2181
  async readFileContents(href, relativeTo, encoding) {
2086
2182
  const rootfile = await this.getRootfile();
2087
- const from = relativeTo ? this.resolveInternalHref(rootfile, relativeTo) : rootfile;
2088
- const path = this.resolveInternalHref(from, href);
2089
- const itemEntry = encoding ? await this.getFileData(path, encoding) : await this.getFileData(path);
2183
+ const from = relativeTo ? await this.resolveEntry(rootfile, relativeTo) : rootfile;
2184
+ const entry = await this.resolveEntry(from, href);
2185
+ const itemEntry = encoding ? await this.adapter.read(entry, encoding) : await this.adapter.read(entry);
2090
2186
  return itemEntry;
2091
2187
  }
2092
2188
  async readItemContents(id, encoding) {
@@ -2095,8 +2191,8 @@ ${JSON.stringify(element, null, 2)}`
2095
2191
  const manifestItem = manifest[id];
2096
2192
  if (!manifestItem)
2097
2193
  throw new Error(`Could not find item with id "${id}" in manifest`);
2098
- const path = this.resolveInternalHref(rootfile, manifestItem.href);
2099
- const itemEntry = encoding ? await this.getFileData(path, encoding) : await this.getFileData(path);
2194
+ const entry = await this.resolveEntry(rootfile, manifestItem.href);
2195
+ const itemEntry = encoding ? await this.adapter.read(entry, encoding) : await this.adapter.read(entry);
2100
2196
  return itemEntry;
2101
2197
  }
2102
2198
  /**
@@ -2154,11 +2250,11 @@ ${JSON.stringify(element, null, 2)}`
2154
2250
  if (!manifestItem)
2155
2251
  throw new Error(`Could not find item with id "${id}" in manifest`);
2156
2252
  import_mem.default.clear(this.readXhtmlItemContents);
2157
- const href = this.resolveInternalHref(rootfile, manifestItem.href);
2253
+ const entry = await this.resolveEntry(rootfile, manifestItem.href);
2158
2254
  if (encoding === "utf-8") {
2159
- await this.writeEntryContents(href, contents, encoding);
2255
+ await this.writeEntryContents(entry, contents, encoding);
2160
2256
  } else {
2161
- await this.writeEntryContents(href, contents);
2257
+ await this.writeEntryContents(entry, contents);
2162
2258
  }
2163
2259
  }
2164
2260
  /**
@@ -2229,7 +2325,7 @@ ${JSON.stringify(element, null, 2)}`
2229
2325
  });
2230
2326
  this.manifest = null;
2231
2327
  const rootfile = await this.getRootfile();
2232
- const filename = this.resolveInternalHref(rootfile, item.href);
2328
+ const filename = await this.resolveEntry(rootfile, item.href);
2233
2329
  const data = encoding === "utf-8" || encoding === "xml" ? new TextEncoder().encode(
2234
2330
  encoding === "utf-8" ? contents : await Epub.xmlBuilder.build(
2235
2331
  contents
@@ -2413,10 +2509,7 @@ ${JSON.stringify(element, null, 2)}`
2413
2509
  );
2414
2510
  const spineTocId = (_a = spine == null ? void 0 : spine[":@"]) == null ? void 0 : _a["@_toc"];
2415
2511
  const ncxItem = spineTocId ? manifest[spineTocId] : Object.values(manifest).find(
2416
- (item) => {
2417
- var _a2;
2418
- return ((_a2 = item.mediaType) == null ? void 0 : _a2.toLowerCase()) === "application/x-dtbncx+xml";
2419
- }
2512
+ (item) => import_media_types.MediaType.fromMime(item.mediaType ?? "") === import_media_types.MediaType.NCX
2420
2513
  );
2421
2514
  if (!ncxItem) return [];
2422
2515
  const ncxContent = await this.readItemContents(ncxItem.id, "utf-8");
@@ -2557,7 +2650,12 @@ class EpubFactory {
2557
2650
  "This is not a valid EPUB publication. Could not read the package document."
2558
2651
  );
2559
2652
  }
2560
- await Epub.assertEpub3(epub);
2653
+ try {
2654
+ await Epub.assertEpub3(epub);
2655
+ } catch (error) {
2656
+ epub.discardAndClose();
2657
+ throw error;
2658
+ }
2561
2659
  return epub;
2562
2660
  }
2563
2661
  /**
@@ -2597,14 +2695,11 @@ class EpubFactory {
2597
2695
  const container = encoder.encode(`<?xml version="1.0"?>
2598
2696
  <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
2599
2697
  <rootfiles>
2600
- <rootfile media-type="application/oebps-package+xml" full-path="OEBPS/content.opf"/>
2698
+ <rootfile media-type="${import_media_types.MediaType.OPF.mime}" full-path="OEBPS/content.opf"/>
2601
2699
  </rootfiles>
2602
2700
  </container>
2603
2701
  `);
2604
- await adapter.write(
2605
- (0, import_path.join)(adapter.rootPath, "META-INF", "container.xml"),
2606
- container
2607
- );
2702
+ await adapter.write("META-INF/container.xml", container);
2608
2703
  const packageDocument = encoder.encode(`<?xml version="1.0"?>
2609
2704
  <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/">
2610
2705
  <metadata>
@@ -2615,10 +2710,7 @@ class EpubFactory {
2615
2710
  </spine>
2616
2711
  </package>
2617
2712
  `);
2618
- await adapter.write(
2619
- (0, import_path.join)(adapter.rootPath, "OEBPS", "content.opf"),
2620
- packageDocument
2621
- );
2713
+ await adapter.write("OEBPS/content.opf", packageDocument);
2622
2714
  const epub = new Epub(this.adapterClass, adapter, path);
2623
2715
  const metadata = [
2624
2716
  {
@@ -2667,7 +2759,6 @@ class EpubFactory {
2667
2759
  * @throws when the adapter is read-only
2668
2760
  */
2669
2761
  async upgrade(path, options = {}) {
2670
- var _a;
2671
2762
  if (!this.adapterClass.capabilities.writable) {
2672
2763
  throw new EpubReadOnlyError(
2673
2764
  `adapter ${this.adapterClass.kind} is read-only; cannot upgrade`
@@ -2693,49 +2784,55 @@ class EpubFactory {
2693
2784
  "This is not a valid EPUB publication. Could not read the package document."
2694
2785
  );
2695
2786
  }
2696
- const version = await epub.getVersion();
2697
- if (version.startsWith("3.")) {
2698
- return epub;
2699
- }
2700
- const tocEntries = await epub.getNcxTableOfContents();
2701
- let landmarks = [];
2702
- await epub.withPackage((pkg) => {
2703
- landmarks = Upgrade.extractGuideLandmarks(pkg);
2704
- Upgrade.upgradePackageMetadata(pkg);
2705
- Upgrade.fixFontMimeTypes(pkg);
2706
- Upgrade.removeGuide(pkg);
2787
+ try {
2788
+ const version = await epub.getVersion();
2789
+ if (version.startsWith("3.")) {
2790
+ return epub;
2791
+ }
2792
+ const tocEntries = await epub.getNcxTableOfContents();
2793
+ let landmarks = [];
2794
+ await epub.withPackage((pkg) => {
2795
+ landmarks = Upgrade.extractGuideLandmarks(pkg);
2796
+ Upgrade.upgradePackageMetadata(pkg);
2797
+ Upgrade.fixFontMimeTypes(pkg);
2798
+ Upgrade.removeGuide(pkg);
2799
+ if (removeNcx) {
2800
+ Upgrade.removeSpineTocRef(pkg);
2801
+ }
2802
+ Upgrade.setPackageVersion(pkg, "3.0");
2803
+ });
2804
+ await Upgrade.collectManifestProperties(epub);
2707
2805
  if (removeNcx) {
2708
- Upgrade.removeSpineTocRef(pkg);
2806
+ await Upgrade.removeNcx(epub);
2709
2807
  }
2710
- Upgrade.setPackageVersion(pkg, "3.0");
2711
- });
2712
- await Upgrade.collectManifestProperties(epub);
2713
- if (removeNcx) {
2714
- await Upgrade.removeNcx(epub);
2715
- }
2716
- const navHref = await Upgrade.chooseNavHref(epub);
2717
- const navContent = await Upgrade.buildNavDocument(
2718
- epub,
2719
- tocEntries,
2720
- landmarks
2721
- );
2722
- await epub.addManifestItem(
2723
- {
2724
- id: "nav",
2725
- href: navHref,
2726
- mediaType: "application/xhtml+xml",
2727
- properties: ["nav"]
2728
- },
2729
- navContent,
2730
- "utf-8"
2731
- );
2732
- const manifest = await epub.getManifest();
2733
- for (const item of Object.values(manifest)) {
2734
- if (((_a = item.mediaType) == null ? void 0 : _a.toLowerCase()) !== "application/xhtml+xml") continue;
2735
- const contents = await epub.readXhtmlItemContents(item.id);
2736
- await epub.writeXhtmlItemContents(item.id, contents);
2808
+ const navHref = await Upgrade.chooseNavHref(epub);
2809
+ const navContent = await Upgrade.buildNavDocument(
2810
+ epub,
2811
+ tocEntries,
2812
+ landmarks
2813
+ );
2814
+ await epub.addManifestItem(
2815
+ {
2816
+ id: "nav",
2817
+ href: navHref,
2818
+ mediaType: import_media_types.MediaType.XHTML.mime,
2819
+ properties: ["nav"]
2820
+ },
2821
+ navContent,
2822
+ "utf-8"
2823
+ );
2824
+ const manifest = await epub.getManifest();
2825
+ for (const item of Object.values(manifest)) {
2826
+ if (import_media_types.MediaType.fromMime(item.mediaType ?? "") !== import_media_types.MediaType.XHTML)
2827
+ continue;
2828
+ const contents = await epub.readXhtmlItemContents(item.id);
2829
+ await epub.writeXhtmlItemContents(item.id, contents);
2830
+ }
2831
+ return epub;
2832
+ } catch (error) {
2833
+ epub.discardAndClose();
2834
+ throw error;
2737
2835
  }
2738
- return epub;
2739
2836
  }
2740
2837
  }
2741
2838
  // Annotate the CommonJS export names for ESM import in node: