@storyteller-platform/epub 0.5.1 → 0.6.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.d.cts CHANGED
@@ -1,2 +1,5 @@
1
1
  import 'fast-xml-parser';
2
- export { A as AlternateScript, C as Collection, e as DcCreator, D as DcSubject, f as DublinCore, E as ElementName, k as Epub, Epub2UpgradeOptions, d as EpubMetadata, j as EpubVersionError, Landmark, M as ManifestItem, c as MetadataEntry, h as Navigation, N as NavigationItem, g as NavigationList, i as PackageElement, P as ParsedXml, X as XmlElement, b as XmlNode, a as XmlTextNode } from './upgrade.cjs';
2
+ export { AdapterOptions, EpubListEntry, EpubStorageAdapter, EpubStorageAdapterClass, EpubStorageCapabilities, EpubStorageKind } from './adapters/interface.cjs';
3
+ export { A as AlternateScript, C as Collection, e as DcCreator, D as DcSubject, f as DublinCore, E as ElementName, m as Epub, Epub2UpgradeOptions, o as EpubFactory, n as EpubInstanceFor, d as EpubMetadata, l as EpubReadOnlyError, j as EpubReader, k as EpubVersionError, F as FromOptions, I as InMemoryEpubReader, Landmark, M as ManifestItem, c as MetadataEntry, h as Navigation, N as NavigationItem, g as NavigationList, i as PackageElement, P as ParsedXml, X as XmlElement, b as XmlNode, a as XmlTextNode } from './upgrade.cjs';
4
+ export { MemoryAdapter, MemoryAdapterOptions } from './adapters/memory.cjs';
5
+ export { TmpFsAdapter } from './adapters/tmpfs.cjs';
package/dist/index.d.ts CHANGED
@@ -1,2 +1,5 @@
1
1
  import 'fast-xml-parser';
2
- export { A as AlternateScript, C as Collection, e as DcCreator, D as DcSubject, f as DublinCore, E as ElementName, k as Epub, Epub2UpgradeOptions, d as EpubMetadata, j as EpubVersionError, Landmark, M as ManifestItem, c as MetadataEntry, h as Navigation, N as NavigationItem, g as NavigationList, i as PackageElement, P as ParsedXml, X as XmlElement, b as XmlNode, a as XmlTextNode } from './upgrade.js';
2
+ export { AdapterOptions, EpubListEntry, EpubStorageAdapter, EpubStorageAdapterClass, EpubStorageCapabilities, EpubStorageKind } from './adapters/interface.js';
3
+ export { A as AlternateScript, C as Collection, e as DcCreator, D as DcSubject, f as DublinCore, E as ElementName, m as Epub, Epub2UpgradeOptions, o as EpubFactory, n as EpubInstanceFor, d as EpubMetadata, l as EpubReadOnlyError, j as EpubReader, k as EpubVersionError, F as FromOptions, I as InMemoryEpubReader, Landmark, M as ManifestItem, c as MetadataEntry, h as Navigation, N as NavigationItem, g as NavigationList, i as PackageElement, P as ParsedXml, X as XmlElement, b as XmlNode, a as XmlTextNode } from './upgrade.js';
4
+ export { MemoryAdapter, MemoryAdapterOptions } from './adapters/memory.js';
5
+ export { TmpFsAdapter } from './adapters/tmpfs.js';
package/dist/index.js CHANGED
@@ -1,58 +1,37 @@
1
- import {
2
- __callDispose,
3
- __using
4
- } from "./chunk-BIEQXUOY.js";
5
- import { randomUUID } from "node:crypto";
6
- import { createWriteStream, rmSync } from "node:fs";
7
- import { cp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
8
- import { tmpdir } from "node:os";
9
- import { pipeline } from "node:stream/promises";
1
+ import "./chunk-BIEQXUOY.js";
2
+ import { cp, mkdir } from "node:fs/promises";
10
3
  import { Mutex } from "async-mutex";
11
4
  import { XMLBuilder, XMLParser } from "fast-xml-parser";
12
5
  import memoize from "mem";
13
6
  import { lookup } from "mime-types";
14
7
  import { nanoid } from "nanoid";
15
- import { fromBuffer, open } from "yauzl-promise";
16
- import { ZipFile } from "yazl";
17
8
  import {
18
9
  dirname,
19
10
  hrefToPlatformPath,
20
11
  join,
21
- resolve,
22
- sep
12
+ resolve
23
13
  } from "@storyteller-platform/path";
14
+ import { TmpFsAdapter } from "./adapters/tmpfs.js";
24
15
  import * as Upgrade from "./upgrade.js";
25
- const MP3_FILE_EXTENSIONS = [".mp3"];
26
- const MPEG4_FILE_EXTENSIONS = [".mp4", ".m4a", ".m4b"];
27
- const AAC_FILE_EXTENSIONS = [".aac"];
28
- const OGG_FILE_EXTENSIONS = [".ogg", ".oga", ".mogg"];
29
- const OPUS_FILE_EXTENSIONS = [".opus"];
30
- const WAVE_FILE_EXTENSIONS = [".wav"];
31
- const AIFF_FILE_EXTENSIONS = [".aiff"];
32
- const FLAC_FILE_EXTENSIONS = [".flac"];
33
- const ALAC_FILE_EXTENSIONS = [".alac"];
34
- const WEBM_FILE_EXTENSIONS = [".weba"];
35
- const AUDIO_FILE_EXTENSIONS = [
36
- ...MP3_FILE_EXTENSIONS,
37
- ...AAC_FILE_EXTENSIONS,
38
- ...MPEG4_FILE_EXTENSIONS,
39
- ...OPUS_FILE_EXTENSIONS,
40
- ...OGG_FILE_EXTENSIONS,
41
- ...WAVE_FILE_EXTENSIONS,
42
- ...AIFF_FILE_EXTENSIONS,
43
- ...FLAC_FILE_EXTENSIONS,
44
- ...ALAC_FILE_EXTENSIONS,
45
- ...WEBM_FILE_EXTENSIONS
46
- ];
47
- function isAudioFile(filenameOrExt) {
48
- return AUDIO_FILE_EXTENSIONS.some((ext) => filenameOrExt.endsWith(ext));
49
- }
16
+ import { MemoryAdapter } from "./adapters/memory.js";
17
+ import { TmpFsAdapter as TmpFsAdapter2 } from "./adapters/tmpfs.js";
50
18
  class EpubVersionError extends Error {
51
19
  }
20
+ class EpubReadOnlyError extends Error {
21
+ }
52
22
  class Epub {
53
- constructor(extractPath, inputPath) {
54
- this.extractPath = extractPath;
23
+ /**
24
+ * Prefer the static factories ({@link Epub.using}, {@link Epub.from},
25
+ * {@link Epub.create}, {@link Epub.upgrade}) over calling this constructor
26
+ * directly. It's public so {@link EpubFactory} can construct instances; nothing
27
+ * else should need to.
28
+ */
29
+ constructor(adapterClass, adapter, inputPath, readonlyOverride = false) {
30
+ this.adapterClass = adapterClass;
31
+ this.adapter = adapter;
55
32
  this.inputPath = inputPath;
33
+ this.readonlyOverride = readonlyOverride;
34
+ this.storage = adapterClass.kind;
56
35
  this.readXhtmlItemContents = memoize(
57
36
  this.readXhtmlItemContents.bind(this),
58
37
  // This isn't unnecessary, the generic here just isn't handling the
@@ -248,86 +227,53 @@ ${JSON.stringify(element, null, 2)}`
248
227
  spine = null;
249
228
  packageMutex = new Mutex();
250
229
  /**
251
- * Construct an Epub instance, optionally beginning
252
- * with the provided metadata.
230
+ * Storage backend kind in use for this instance
253
231
  *
254
- * @param dublinCore Core metadata terms
255
- * @param additionalMetadata An array of additional metadata entries
232
+ * Public so callers can declare type-level requirements via {@link InMemoryEpubReader}
233
+ * Orthogonal to the read-only / writable axis (controlled by `readonlyOverride`
234
+ * and the adapter's capability bag)
256
235
  */
257
- static async create(path, {
258
- title,
259
- language,
260
- identifier,
261
- date,
262
- subjects,
263
- type,
264
- creators,
265
- contributors
266
- }, additionalMetadata = []) {
267
- const extractPath = join(
268
- tmpdir(),
269
- `storyteller-platform-epub-${randomUUID()}`
270
- );
271
- const encoder = new TextEncoder();
272
- const container = encoder.encode(`<?xml version="1.0"?>
273
- <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
274
- <rootfiles>
275
- <rootfile media-type="application/oebps-package+xml" full-path="OEBPS/content.opf"/>
276
- </rootfiles>
277
- </container>
278
- `);
279
- await mkdir(join(extractPath, "META-INF"), { recursive: true });
280
- await writeFile(join(extractPath, "META-INF", "container.xml"), container);
281
- const packageDocument = encoder.encode(`<?xml version="1.0"?>
282
- <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/">
283
- <metadata>
284
- </metadata>
285
- <manifest>
286
- </manifest>
287
- <spine>
288
- </spine>
289
- </package>
290
- `);
291
- await mkdir(join(extractPath, "OEBPS"));
292
- await writeFile(join(extractPath, "OEBPS", "content.opf"), packageDocument);
293
- const epub = new this(extractPath, path);
294
- const metadata = [
295
- {
296
- id: "pub-id",
297
- type: "dc:identifier",
298
- properties: {},
299
- value: identifier
300
- },
301
- ...additionalMetadata
302
- ];
303
- await Promise.all(metadata.map((entry) => epub.addMetadata(entry)));
304
- await epub.setTitle(title);
305
- await epub.setLanguage(language);
306
- if (date) await epub.setPublicationDate(date);
307
- if (type) await epub.setType(type);
308
- if (subjects) {
309
- await Promise.all(subjects.map((subject) => epub.addSubject(subject)));
310
- }
311
- if (creators) {
312
- await Promise.all(creators.map((creator) => epub.addCreator(creator)));
313
- }
314
- if (contributors) {
315
- await Promise.all(
316
- contributors.map((contributor) => epub.addCreator(contributor))
236
+ storage;
237
+ /**
238
+ * Runtime guard for mutation methods
239
+ */
240
+ assertWritable() {
241
+ if (!this.adapterClass.capabilities.writable || this.readonlyOverride) {
242
+ throw new EpubReadOnlyError(
243
+ "cannot mutate a read-only Epub. open via Epub.using(TmpFsAdapter).from(path) (without { readonly: true }) to modify."
317
244
  );
318
245
  }
319
- return epub;
320
246
  }
321
247
  /**
322
- * Construct an Epub instance by reading an existing EPUB
323
- * publication.
248
+ * Construct a new EPUB on a writable backend, optionally seeded
249
+ * with the provided metadata. Equivalent to
250
+ * `Epub.using(TmpFsAdapter).create(...)`.
324
251
  *
325
- * @param pathOrData Must be either a string representing the
326
- * path to an EPUB file on disk, or a Uint8Array representing
327
- * the data of the EPUB publication.
252
+ * @param dublinCore Core metadata terms
253
+ * @param additionalMetadata An array of additional metadata entries
328
254
  */
329
- static async from(pathOrData) {
330
- const epub = await this.open(pathOrData);
255
+ static async create(path, dublinCore, additionalMetadata = []) {
256
+ return Epub.using(TmpFsAdapter).create(path, dublinCore, additionalMetadata);
257
+ }
258
+ /**
259
+ * Specify the storage backend to use for the EPUB
260
+ *
261
+ * The returned factory exposes `from`, `create`, and `upgrade`,
262
+ * which route through the supplied adapter.
263
+ *
264
+ * @example
265
+ * ```ts
266
+ * using epub = await Epub.using(TmpFsAdapter).from(path)
267
+ * using reader = await Epub.using(MemoryAdapter).from(buffer, { cache: false })
268
+ * ```
269
+ */
270
+ static using(adapterClass) {
271
+ return new EpubFactory(adapterClass);
272
+ }
273
+ static async from(pathOrData, options = {}) {
274
+ return Epub.using(TmpFsAdapter).from(pathOrData, options);
275
+ }
276
+ static async assertEpub3(epub) {
331
277
  const version = await epub.getVersion();
332
278
  if (!version.startsWith("3.")) {
333
279
  epub.discardAndClose();
@@ -335,85 +281,52 @@ ${JSON.stringify(element, null, 2)}`
335
281
  "This is not a valid EPUB 3 publication. This library only supports EPUB 3, not EPUB 2. Use Epub.upgrade(path) to convert."
336
282
  );
337
283
  }
338
- return epub;
339
284
  }
340
- /**
341
- * Open an EPUB publication and return an Epub instance.
342
- */
343
- static async open(pathOrData) {
344
- const extractPath = join(
345
- tmpdir(),
346
- `storyteller-platform-epub-${randomUUID()}.epub`
347
- );
348
- try {
349
- var _stack = [];
350
- try {
351
- const zipfile = typeof pathOrData === "string" ? await open(pathOrData) : await fromBuffer(Buffer.from(pathOrData));
352
- const stack = __using(_stack, new AsyncDisposableStack(), true);
353
- stack.defer(async () => {
354
- await zipfile.close();
355
- });
356
- for await (const entry of zipfile) {
357
- if (entry.filename.endsWith(sep)) {
358
- } else {
359
- const writePath = join(extractPath, entry.filename);
360
- const readStream = await entry.openReadStream();
361
- await mkdir(dirname(writePath), { recursive: true });
362
- const writeStream = createWriteStream(writePath);
363
- await pipeline(readStream, writeStream);
364
- }
365
- }
366
- } catch (_) {
367
- var _error = _, _hasError = true;
368
- } finally {
369
- var _promise = __callDispose(_stack, _error, _hasError);
370
- _promise && await _promise;
371
- }
372
- } catch (error) {
373
- rmSync(extractPath, { force: true, recursive: true });
374
- throw error;
375
- }
376
- const epub = new this(
377
- extractPath,
378
- typeof pathOrData === "string" ? pathOrData : void 0
379
- );
380
- try {
381
- await epub.getPackageElement();
382
- } catch (e) {
383
- epub.discardAndClose();
384
- console.error(e);
285
+ async copy(path) {
286
+ if (!this.adapter.duplicate) {
385
287
  throw new Error(
386
- "This is not a valid EPUB publication. Could not read the package document."
288
+ `cannot copy an Epub backed by ${this.adapterClass.kind}: adapter does not implement duplicate()`
387
289
  );
388
290
  }
389
- return epub;
390
- }
391
- async copy(path) {
392
- const extractPath = join(
393
- tmpdir(),
394
- `storyteller-platform-epub-${randomUUID()}.epub`
395
- );
396
- try {
397
- await cp(this.extractPath, extractPath, { recursive: true });
398
- } catch (error) {
399
- rmSync(extractPath, { force: true, recursive: true });
400
- throw error;
401
- }
402
- return new Epub(extractPath, path);
291
+ const newAdapter = await this.adapter.duplicate();
292
+ return new Epub(this.adapterClass, newAdapter, path);
403
293
  }
404
294
  async removeEntry(href) {
295
+ this.assertWritable();
296
+ if (!this.adapter.remove) {
297
+ throw new EpubReadOnlyError(
298
+ `adapter ${this.adapterClass.kind} does not support entry removal`
299
+ );
300
+ }
405
301
  const rootfile = await this.getRootfile();
406
302
  const filename = this.resolveInternalHref(rootfile, href);
407
- await rm(filename);
303
+ await this.adapter.remove(filename);
408
304
  }
409
305
  async getFileData(path, encoding) {
410
- return await readFile(path, encoding);
306
+ if (encoding) {
307
+ return this.adapter.read(path, encoding);
308
+ }
309
+ return this.adapter.read(path);
310
+ }
311
+ /**
312
+ * Length of the underlying archive entry for a manifest item, in bytes
313
+ * Necessary to compute the readium page count which is for COMPRESSED content
314
+ * @see {@link https://github.com/readium/architecture/issues/123}
315
+ */
316
+ async getItemArchiveLength(id) {
317
+ const rootfile = await this.getRootfile();
318
+ const manifest = await this.getManifest();
319
+ const manifestItem = manifest[id];
320
+ if (!manifestItem)
321
+ 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);
411
324
  }
412
325
  async getRootfile() {
413
326
  var _a;
414
327
  if (this.rootfile !== null) return this.rootfile;
415
328
  const containerString = await this.getFileData(
416
- join(this.extractPath, "META-INF", "container.xml"),
329
+ join(this.adapter.rootPath, "META-INF", "container.xml"),
417
330
  "utf-8"
418
331
  );
419
332
  if (!containerString)
@@ -445,7 +358,7 @@ ${JSON.stringify(element, null, 2)}`
445
358
  throw new Error(
446
359
  "Failed to parse EPUB container.xml: Found no rootfile element"
447
360
  );
448
- this.rootfile = resolve(this.extractPath, fullPath);
361
+ this.rootfile = resolve(this.adapter.rootPath, fullPath);
449
362
  return this.rootfile;
450
363
  }
451
364
  async getPackageDocument() {
@@ -483,6 +396,7 @@ ${JSON.stringify(element, null, 2)}`
483
396
  * it will be assumed that the package document was modified in place.
484
397
  */
485
398
  async withPackage(producer) {
399
+ this.assertWritable();
486
400
  await this.packageMutex.runExclusive(async () => {
487
401
  const packageDocument = await this.getPackageDocument();
488
402
  const packageElement = Epub.findXmlChildByName("package", packageDocument);
@@ -1717,7 +1631,7 @@ ${JSON.stringify(element, null, 2)}`
1717
1631
  resolveInternalHref(from, href) {
1718
1632
  const startPath = dirname(from);
1719
1633
  return resolve(
1720
- this.extractPath,
1634
+ this.adapter.rootPath,
1721
1635
  hrefToPlatformPath(startPath),
1722
1636
  hrefToPlatformPath(href)
1723
1637
  );
@@ -1734,7 +1648,7 @@ ${JSON.stringify(element, null, 2)}`
1734
1648
  const rootfile = await this.getRootfile();
1735
1649
  const from = relativeTo ? this.resolveInternalHref(rootfile, relativeTo) : rootfile;
1736
1650
  const path = this.resolveInternalHref(from, href);
1737
- return path.replace(toRoot ? this.extractPath : dirname(rootfile), "").slice(1);
1651
+ return path.replace(toRoot ? this.adapter.rootPath : dirname(rootfile), "").slice(1);
1738
1652
  }
1739
1653
  async readFileContents(href, relativeTo, encoding) {
1740
1654
  const rootfile = await this.getRootfile();
@@ -1789,8 +1703,17 @@ ${JSON.stringify(element, null, 2)}`
1789
1703
  return Epub.getXhtmlTextContent(body);
1790
1704
  }
1791
1705
  async writeEntryContents(path, contents, encoding) {
1792
- await mkdir(dirname(path), { recursive: true });
1793
- await writeFile(path, contents, encoding);
1706
+ this.assertWritable();
1707
+ if (!this.adapter.write) {
1708
+ throw new EpubReadOnlyError(
1709
+ `adapter ${this.adapterClass.kind} does not support writes`
1710
+ );
1711
+ }
1712
+ if (encoding === "utf-8") {
1713
+ await this.adapter.write(path, contents, encoding);
1714
+ } else {
1715
+ await this.adapter.write(path, contents);
1716
+ }
1794
1717
  }
1795
1718
  async writeItemContents(id, contents, encoding) {
1796
1719
  const rootfile = await this.getRootfile();
@@ -1880,8 +1803,7 @@ ${JSON.stringify(element, null, 2)}`
1880
1803
  contents
1881
1804
  )
1882
1805
  ) : contents;
1883
- await mkdir(dirname(filename), { recursive: true });
1884
- await writeFile(filename, data);
1806
+ await this.writeEntryContents(filename, data);
1885
1807
  }
1886
1808
  /**
1887
1809
  * Update the manifest entry for an existing item.
@@ -2134,7 +2056,7 @@ ${JSON.stringify(element, null, 2)}`
2134
2056
  this.rootfile = null;
2135
2057
  this.manifest = null;
2136
2058
  this.spine = null;
2137
- rmSync(this.extractPath, { recursive: true, force: true });
2059
+ void this.adapter.dispose();
2138
2060
  }
2139
2061
  /**
2140
2062
  * Write the current contents of the Epub to a new
@@ -2145,63 +2067,157 @@ ${JSON.stringify(element, null, 2)}`
2145
2067
  * timestamp.
2146
2068
  */
2147
2069
  async saveAndClose() {
2148
- var _stack = [];
2149
- try {
2150
- if (!this.inputPath) {
2151
- throw new Error("In-memory EPUB files cannot be saved to disk");
2070
+ this.assertWritable();
2071
+ if (!this.inputPath) {
2072
+ throw new Error("In-memory EPUB files cannot be saved to disk");
2073
+ }
2074
+ if (!this.adapter.serialize) {
2075
+ throw new Error(
2076
+ `adapter ${this.adapterClass.kind} does not support serialization`
2077
+ );
2078
+ }
2079
+ await this.replaceMetadata(
2080
+ (entry) => entry.properties["property"] === "dcterms:modified",
2081
+ {
2082
+ type: "meta",
2083
+ properties: { property: "dcterms:modified" },
2084
+ // We need UTC with integer seconds, but toISOString gives UTC with ms
2085
+ value: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+/, "")
2152
2086
  }
2153
- await this.replaceMetadata(
2154
- (entry) => entry.properties["property"] === "dcterms:modified",
2155
- {
2156
- type: "meta",
2157
- properties: { property: "dcterms:modified" },
2158
- // We need UTC with integer seconds, but toISOString gives UTC with ms
2159
- value: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+/, "")
2160
- }
2087
+ );
2088
+ await this.adapter.serialize(this.inputPath);
2089
+ }
2090
+ /**
2091
+ * Upgrade an EPUB 2 publication to EPUB 3 in place, returning a new,
2092
+ * valid Epub 3 instance. Equivalent to
2093
+ * `Epub.using(TmpFsAdapter).upgrade(...)`.
2094
+ */
2095
+ static async upgrade(path, options = {}) {
2096
+ return Epub.using(TmpFsAdapter).upgrade(path, options);
2097
+ }
2098
+ [Symbol.dispose]() {
2099
+ this.discardAndClose();
2100
+ }
2101
+ }
2102
+ class EpubFactory {
2103
+ constructor(adapterClass) {
2104
+ this.adapterClass = adapterClass;
2105
+ }
2106
+ async from(source, options = {}) {
2107
+ const adapter = await this.adapterClass.init(
2108
+ source,
2109
+ options
2110
+ );
2111
+ const inputPath = typeof source === "string" ? source : void 0;
2112
+ const readonlyOverride = options.readonly === true;
2113
+ const epub = new Epub(
2114
+ this.adapterClass,
2115
+ adapter,
2116
+ inputPath,
2117
+ readonlyOverride
2118
+ );
2119
+ try {
2120
+ await epub.getPackageElement();
2121
+ } catch (e) {
2122
+ epub.discardAndClose();
2123
+ console.error(e);
2124
+ throw new Error(
2125
+ "This is not a valid EPUB publication. Could not read the package document."
2126
+ );
2127
+ }
2128
+ await Epub.assertEpub3(epub);
2129
+ return epub;
2130
+ }
2131
+ /**
2132
+ * Construct a new EPUB on this factory's adapter, optionally seeded
2133
+ * with the provided metadata. Requires a writable adapter that
2134
+ * implements `initEmpty` (today: {@link TmpFsAdapter}).
2135
+ *
2136
+ * @throws when the adapter is read-only or does not implement initEmpty
2137
+ */
2138
+ async create(path, {
2139
+ title,
2140
+ language,
2141
+ identifier,
2142
+ date,
2143
+ subjects,
2144
+ type,
2145
+ creators,
2146
+ contributors
2147
+ }, additionalMetadata = []) {
2148
+ if (!this.adapterClass.capabilities.writable) {
2149
+ throw new EpubReadOnlyError(
2150
+ `adapter ${this.adapterClass.kind} is read-only; cannot create`
2161
2151
  );
2162
- const tmpArchivePath = join(
2163
- tmpdir(),
2164
- `storyteller-platform-epub-${randomUUID()}`
2152
+ }
2153
+ if (!this.adapterClass.initEmpty) {
2154
+ throw new Error(
2155
+ `adapter ${this.adapterClass.kind} does not support create() (missing initEmpty)`
2156
+ );
2157
+ }
2158
+ const adapter = await this.adapterClass.initEmpty();
2159
+ if (!adapter.write) {
2160
+ throw new Error(
2161
+ `adapter ${this.adapterClass.kind} declared writable but did not implement write()`
2165
2162
  );
2166
- const { promise, resolve: resolve2 } = Promise.withResolvers();
2167
- const zipfile = new ZipFile();
2168
- const writeStream = createWriteStream(tmpArchivePath);
2169
- writeStream.on("close", () => {
2170
- resolve2();
2171
- });
2172
- const stack = __using(_stack, new AsyncDisposableStack(), true);
2173
- stack.defer(async () => {
2174
- writeStream.close();
2175
- await rm(tmpArchivePath, { force: true });
2176
- });
2177
- zipfile.outputStream.pipe(writeStream);
2178
- zipfile.addBuffer(Buffer.from("application/epub+zip"), "mimetype", {
2179
- compress: false
2180
- });
2181
- const entries = await readdir(this.extractPath, {
2182
- recursive: true,
2183
- withFileTypes: true
2184
- });
2185
- for (const entry of entries) {
2186
- if (entry.name === "mimetype" || entry.isDirectory()) continue;
2187
- zipfile.addFile(
2188
- join(entry.parentPath, entry.name),
2189
- join(entry.parentPath, entry.name).replace(`${this.extractPath}/`, ""),
2190
- { compress: !isAudioFile(entry.name) }
2191
- );
2192
- }
2193
- zipfile.end();
2194
- await promise;
2195
- await cp(tmpArchivePath, this.inputPath);
2196
- } catch (_) {
2197
- var _error = _, _hasError = true;
2198
- } finally {
2199
- var _promise = __callDispose(_stack, _error, _hasError);
2200
- _promise && await _promise;
2201
2163
  }
2164
+ const encoder = new TextEncoder();
2165
+ const container = encoder.encode(`<?xml version="1.0"?>
2166
+ <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
2167
+ <rootfiles>
2168
+ <rootfile media-type="application/oebps-package+xml" full-path="OEBPS/content.opf"/>
2169
+ </rootfiles>
2170
+ </container>
2171
+ `);
2172
+ await adapter.write(
2173
+ join(adapter.rootPath, "META-INF", "container.xml"),
2174
+ container
2175
+ );
2176
+ const packageDocument = encoder.encode(`<?xml version="1.0"?>
2177
+ <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/">
2178
+ <metadata>
2179
+ </metadata>
2180
+ <manifest>
2181
+ </manifest>
2182
+ <spine>
2183
+ </spine>
2184
+ </package>
2185
+ `);
2186
+ await adapter.write(
2187
+ join(adapter.rootPath, "OEBPS", "content.opf"),
2188
+ packageDocument
2189
+ );
2190
+ const epub = new Epub(this.adapterClass, adapter, path);
2191
+ const metadata = [
2192
+ {
2193
+ id: "pub-id",
2194
+ type: "dc:identifier",
2195
+ properties: {},
2196
+ value: identifier
2197
+ },
2198
+ ...additionalMetadata
2199
+ ];
2200
+ await Promise.all(metadata.map((entry) => epub.addMetadata(entry)));
2201
+ await epub.setTitle(title);
2202
+ await epub.setLanguage(language);
2203
+ if (date) await epub.setPublicationDate(date);
2204
+ if (type) await epub.setType(type);
2205
+ if (subjects) {
2206
+ await Promise.all(subjects.map((subject) => epub.addSubject(subject)));
2207
+ }
2208
+ if (creators) {
2209
+ await Promise.all(creators.map((creator) => epub.addCreator(creator)));
2210
+ }
2211
+ if (contributors) {
2212
+ await Promise.all(
2213
+ contributors.map((contributor) => epub.addCreator(contributor))
2214
+ );
2215
+ }
2216
+ return epub;
2202
2217
  }
2203
2218
  /**
2204
- * Upgrade an EPUB 2 publication to EPUB 3 in place, returning a new, valid Epub 3 instance.
2219
+ * Upgrade an EPUB 2 publication to EPUB 3 in place using this
2220
+ * factory's adapter, returning a new, valid Epub 3 instance.
2205
2221
  *
2206
2222
  * Performs the following transformations:
2207
2223
  * - upgrades OPF metadata to EPUB 3 conventions
@@ -2211,15 +2227,40 @@ ${JSON.stringify(element, null, 2)}`
2211
2227
  * - fixes common font MIME types
2212
2228
  * - bumps the package version to 3.0
2213
2229
  * - goes over each xhtml item and rewrites it using XMLParser to make sure the output is valid XHTML
2230
+ *
2231
+ * Requires a writable adapter. When {@link Upgrade.Epub2UpgradeOptions.outputPath}
2232
+ * is set, the source file is copied to that path on disk first; this
2233
+ * only makes sense for adapters whose `source` is a real fs path.
2234
+ *
2235
+ * @throws when the adapter is read-only
2214
2236
  */
2215
- static async upgrade(path, options = {}) {
2237
+ async upgrade(path, options = {}) {
2216
2238
  var _a;
2239
+ if (!this.adapterClass.capabilities.writable) {
2240
+ throw new EpubReadOnlyError(
2241
+ `adapter ${this.adapterClass.kind} is read-only; cannot upgrade`
2242
+ );
2243
+ }
2217
2244
  const { removeNcx = false, outputPath } = options;
2218
2245
  if (outputPath) {
2219
2246
  await mkdir(dirname(outputPath), { recursive: true });
2220
2247
  await cp(path, outputPath, { force: true });
2221
2248
  }
2222
- const epub = await Epub.open(outputPath ?? path);
2249
+ const source = outputPath ?? path;
2250
+ const adapter = await this.adapterClass.init(
2251
+ source,
2252
+ options
2253
+ );
2254
+ const epub = new Epub(this.adapterClass, adapter, source);
2255
+ try {
2256
+ await epub.getPackageElement();
2257
+ } catch (e) {
2258
+ epub.discardAndClose();
2259
+ console.error(e);
2260
+ throw new Error(
2261
+ "This is not a valid EPUB publication. Could not read the package document."
2262
+ );
2263
+ }
2223
2264
  const version = await epub.getVersion();
2224
2265
  if (version.startsWith("3.")) {
2225
2266
  return epub;
@@ -2264,11 +2305,12 @@ ${JSON.stringify(element, null, 2)}`
2264
2305
  }
2265
2306
  return epub;
2266
2307
  }
2267
- [Symbol.dispose]() {
2268
- this.discardAndClose();
2269
- }
2270
2308
  }
2271
2309
  export {
2272
2310
  Epub,
2273
- EpubVersionError
2311
+ EpubFactory,
2312
+ EpubReadOnlyError,
2313
+ EpubVersionError,
2314
+ MemoryAdapter,
2315
+ TmpFsAdapter2 as TmpFsAdapter
2274
2316
  };