@storyteller-platform/epub 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
@@ -5,10 +5,6 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
9
- var __typeError = (msg) => {
10
- throw TypeError(msg);
11
- };
12
8
  var __export = (target, all) => {
13
9
  for (var name in all)
14
10
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -30,98 +26,44 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
26
  mod
31
27
  ));
32
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
33
- var __using = (stack, value, async) => {
34
- if (value != null) {
35
- if (typeof value !== "object" && typeof value !== "function") __typeError("Object expected");
36
- var dispose, inner;
37
- if (async) dispose = value[__knownSymbol("asyncDispose")];
38
- if (dispose === void 0) {
39
- dispose = value[__knownSymbol("dispose")];
40
- if (async) inner = dispose;
41
- }
42
- if (typeof dispose !== "function") __typeError("Object not disposable");
43
- if (inner) dispose = function() {
44
- try {
45
- inner.call(this);
46
- } catch (e) {
47
- return Promise.reject(e);
48
- }
49
- };
50
- stack.push([async, dispose, value]);
51
- } else if (async) {
52
- stack.push([async]);
53
- }
54
- return value;
55
- };
56
- var __callDispose = (stack, error, hasError) => {
57
- var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) {
58
- return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _;
59
- };
60
- var fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e);
61
- var next = (it) => {
62
- while (it = stack.pop()) {
63
- try {
64
- var result = it[1] && it[1].call(it[2]);
65
- if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next()));
66
- } catch (e) {
67
- fail(e);
68
- }
69
- }
70
- if (hasError) throw error;
71
- };
72
- return next();
73
- };
74
29
  var index_exports = {};
75
30
  __export(index_exports, {
76
31
  Epub: () => Epub,
77
- EpubVersionError: () => EpubVersionError
32
+ EpubFactory: () => EpubFactory,
33
+ EpubReadOnlyError: () => EpubReadOnlyError,
34
+ EpubVersionError: () => EpubVersionError,
35
+ MemoryAdapter: () => import_memory.MemoryAdapter,
36
+ TmpFsAdapter: () => import_tmpfs2.TmpFsAdapter
78
37
  });
79
38
  module.exports = __toCommonJS(index_exports);
80
- var import_node_crypto = require("node:crypto");
81
- var import_node_fs = require("node:fs");
82
39
  var import_promises = require("node:fs/promises");
83
- var import_node_os = require("node:os");
84
- var import_promises2 = require("node:stream/promises");
85
40
  var import_async_mutex = require("async-mutex");
86
41
  var import_fast_xml_parser = require("fast-xml-parser");
87
42
  var import_mem = __toESM(require("mem"), 1);
88
43
  var import_mime_types = require("mime-types");
89
44
  var import_nanoid = require("nanoid");
90
- var import_yauzl_promise = require("yauzl-promise");
91
- var import_yazl = require("yazl");
92
45
  var import_path = require("@storyteller-platform/path");
46
+ var import_tmpfs = require("./adapters/tmpfs.cjs");
93
47
  var Upgrade = __toESM(require("./upgrade.ts"), 1);
94
- const MP3_FILE_EXTENSIONS = [".mp3"];
95
- const MPEG4_FILE_EXTENSIONS = [".mp4", ".m4a", ".m4b"];
96
- const AAC_FILE_EXTENSIONS = [".aac"];
97
- const OGG_FILE_EXTENSIONS = [".ogg", ".oga", ".mogg"];
98
- const OPUS_FILE_EXTENSIONS = [".opus"];
99
- const WAVE_FILE_EXTENSIONS = [".wav"];
100
- const AIFF_FILE_EXTENSIONS = [".aiff"];
101
- const FLAC_FILE_EXTENSIONS = [".flac"];
102
- const ALAC_FILE_EXTENSIONS = [".alac"];
103
- const WEBM_FILE_EXTENSIONS = [".weba"];
104
- const AUDIO_FILE_EXTENSIONS = [
105
- ...MP3_FILE_EXTENSIONS,
106
- ...AAC_FILE_EXTENSIONS,
107
- ...MPEG4_FILE_EXTENSIONS,
108
- ...OPUS_FILE_EXTENSIONS,
109
- ...OGG_FILE_EXTENSIONS,
110
- ...WAVE_FILE_EXTENSIONS,
111
- ...AIFF_FILE_EXTENSIONS,
112
- ...FLAC_FILE_EXTENSIONS,
113
- ...ALAC_FILE_EXTENSIONS,
114
- ...WEBM_FILE_EXTENSIONS
115
- ];
116
- function isAudioFile(filenameOrExt) {
117
- return AUDIO_FILE_EXTENSIONS.some((ext) => filenameOrExt.endsWith(ext));
118
- }
48
+ var import_memory = require("./adapters/memory.cjs");
49
+ var import_tmpfs2 = require("./adapters/tmpfs.cjs");
119
50
  class EpubVersionError extends Error {
120
51
  }
52
+ class EpubReadOnlyError extends Error {
53
+ }
121
54
  class Epub {
122
- constructor(extractPath, inputPath) {
123
- this.extractPath = extractPath;
55
+ /**
56
+ * Prefer the static factories ({@link Epub.using}, {@link Epub.from},
57
+ * {@link Epub.create}, {@link Epub.upgrade}) over calling this constructor
58
+ * directly. It's public so {@link EpubFactory} can construct instances; nothing
59
+ * else should need to.
60
+ */
61
+ constructor(adapterClass, adapter, inputPath, readonlyOverride = false) {
62
+ this.adapterClass = adapterClass;
63
+ this.adapter = adapter;
124
64
  this.inputPath = inputPath;
65
+ this.readonlyOverride = readonlyOverride;
66
+ this.storage = adapterClass.kind;
125
67
  this.readXhtmlItemContents = (0, import_mem.default)(
126
68
  this.readXhtmlItemContents.bind(this),
127
69
  // This isn't unnecessary, the generic here just isn't handling the
@@ -317,86 +259,53 @@ ${JSON.stringify(element, null, 2)}`
317
259
  spine = null;
318
260
  packageMutex = new import_async_mutex.Mutex();
319
261
  /**
320
- * Construct an Epub instance, optionally beginning
321
- * with the provided metadata.
262
+ * Storage backend kind in use for this instance
322
263
  *
323
- * @param dublinCore Core metadata terms
324
- * @param additionalMetadata An array of additional metadata entries
264
+ * Public so callers can declare type-level requirements via {@link InMemoryEpubReader}
265
+ * Orthogonal to the read-only / writable axis (controlled by `readonlyOverride`
266
+ * and the adapter's capability bag)
325
267
  */
326
- static async create(path, {
327
- title,
328
- language,
329
- identifier,
330
- date,
331
- subjects,
332
- type,
333
- creators,
334
- contributors
335
- }, additionalMetadata = []) {
336
- const extractPath = (0, import_path.join)(
337
- (0, import_node_os.tmpdir)(),
338
- `storyteller-platform-epub-${(0, import_node_crypto.randomUUID)()}`
339
- );
340
- const encoder = new TextEncoder();
341
- const container = encoder.encode(`<?xml version="1.0"?>
342
- <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
343
- <rootfiles>
344
- <rootfile media-type="application/oebps-package+xml" full-path="OEBPS/content.opf"/>
345
- </rootfiles>
346
- </container>
347
- `);
348
- await (0, import_promises.mkdir)((0, import_path.join)(extractPath, "META-INF"), { recursive: true });
349
- await (0, import_promises.writeFile)((0, import_path.join)(extractPath, "META-INF", "container.xml"), container);
350
- const packageDocument = encoder.encode(`<?xml version="1.0"?>
351
- <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/">
352
- <metadata>
353
- </metadata>
354
- <manifest>
355
- </manifest>
356
- <spine>
357
- </spine>
358
- </package>
359
- `);
360
- await (0, import_promises.mkdir)((0, import_path.join)(extractPath, "OEBPS"));
361
- await (0, import_promises.writeFile)((0, import_path.join)(extractPath, "OEBPS", "content.opf"), packageDocument);
362
- const epub = new this(extractPath, path);
363
- const metadata = [
364
- {
365
- id: "pub-id",
366
- type: "dc:identifier",
367
- properties: {},
368
- value: identifier
369
- },
370
- ...additionalMetadata
371
- ];
372
- await Promise.all(metadata.map((entry) => epub.addMetadata(entry)));
373
- await epub.setTitle(title);
374
- await epub.setLanguage(language);
375
- if (date) await epub.setPublicationDate(date);
376
- if (type) await epub.setType(type);
377
- if (subjects) {
378
- await Promise.all(subjects.map((subject) => epub.addSubject(subject)));
379
- }
380
- if (creators) {
381
- await Promise.all(creators.map((creator) => epub.addCreator(creator)));
382
- }
383
- if (contributors) {
384
- await Promise.all(
385
- contributors.map((contributor) => epub.addCreator(contributor))
268
+ storage;
269
+ /**
270
+ * Runtime guard for mutation methods
271
+ */
272
+ assertWritable() {
273
+ if (!this.adapterClass.capabilities.writable || this.readonlyOverride) {
274
+ throw new EpubReadOnlyError(
275
+ "cannot mutate a read-only Epub. open via Epub.using(TmpFsAdapter).from(path) (without { readonly: true }) to modify."
386
276
  );
387
277
  }
388
- return epub;
389
278
  }
390
279
  /**
391
- * Construct an Epub instance by reading an existing EPUB
392
- * publication.
280
+ * Construct a new EPUB on a writable backend, optionally seeded
281
+ * with the provided metadata. Equivalent to
282
+ * `Epub.using(TmpFsAdapter).create(...)`.
283
+ *
284
+ * @param dublinCore Core metadata terms
285
+ * @param additionalMetadata An array of additional metadata entries
286
+ */
287
+ static async create(path, dublinCore, additionalMetadata = []) {
288
+ return Epub.using(import_tmpfs.TmpFsAdapter).create(path, dublinCore, additionalMetadata);
289
+ }
290
+ /**
291
+ * Specify the storage backend to use for the EPUB
393
292
  *
394
- * @param pathOrData Must be either a string representing the
395
- * path to an EPUB file on disk, or a Uint8Array representing
396
- * the data of the EPUB publication.
293
+ * The returned factory exposes `from`, `create`, and `upgrade`,
294
+ * which route through the supplied adapter.
295
+ *
296
+ * @example
297
+ * ```ts
298
+ * using epub = await Epub.using(TmpFsAdapter).from(path)
299
+ * using reader = await Epub.using(MemoryAdapter).from(buffer, { cache: false })
300
+ * ```
397
301
  */
398
- static async from(pathOrData) {
399
- const epub = await this.open(pathOrData);
302
+ static using(adapterClass) {
303
+ return new EpubFactory(adapterClass);
304
+ }
305
+ static async from(pathOrData, options = {}) {
306
+ return Epub.using(import_tmpfs.TmpFsAdapter).from(pathOrData, options);
307
+ }
308
+ static async assertEpub3(epub) {
400
309
  const version = await epub.getVersion();
401
310
  if (!version.startsWith("3.")) {
402
311
  epub.discardAndClose();
@@ -404,85 +313,52 @@ ${JSON.stringify(element, null, 2)}`
404
313
  "This is not a valid EPUB 3 publication. This library only supports EPUB 3, not EPUB 2. Use Epub.upgrade(path) to convert."
405
314
  );
406
315
  }
407
- return epub;
408
316
  }
409
- /**
410
- * Open an EPUB publication and return an Epub instance.
411
- */
412
- static async open(pathOrData) {
413
- const extractPath = (0, import_path.join)(
414
- (0, import_node_os.tmpdir)(),
415
- `storyteller-platform-epub-${(0, import_node_crypto.randomUUID)()}.epub`
416
- );
417
- try {
418
- var _stack = [];
419
- try {
420
- const zipfile = typeof pathOrData === "string" ? await (0, import_yauzl_promise.open)(pathOrData) : await (0, import_yauzl_promise.fromBuffer)(Buffer.from(pathOrData));
421
- const stack = __using(_stack, new AsyncDisposableStack(), true);
422
- stack.defer(async () => {
423
- await zipfile.close();
424
- });
425
- for await (const entry of zipfile) {
426
- if (entry.filename.endsWith(import_path.sep)) {
427
- } else {
428
- const writePath = (0, import_path.join)(extractPath, entry.filename);
429
- const readStream = await entry.openReadStream();
430
- await (0, import_promises.mkdir)((0, import_path.dirname)(writePath), { recursive: true });
431
- const writeStream = (0, import_node_fs.createWriteStream)(writePath);
432
- await (0, import_promises2.pipeline)(readStream, writeStream);
433
- }
434
- }
435
- } catch (_) {
436
- var _error = _, _hasError = true;
437
- } finally {
438
- var _promise = __callDispose(_stack, _error, _hasError);
439
- _promise && await _promise;
440
- }
441
- } catch (error) {
442
- (0, import_node_fs.rmSync)(extractPath, { force: true, recursive: true });
443
- throw error;
444
- }
445
- const epub = new this(
446
- extractPath,
447
- typeof pathOrData === "string" ? pathOrData : void 0
448
- );
449
- try {
450
- await epub.getPackageElement();
451
- } catch (e) {
452
- epub.discardAndClose();
453
- console.error(e);
317
+ async copy(path) {
318
+ if (!this.adapter.duplicate) {
454
319
  throw new Error(
455
- "This is not a valid EPUB publication. Could not read the package document."
320
+ `cannot copy an Epub backed by ${this.adapterClass.kind}: adapter does not implement duplicate()`
456
321
  );
457
322
  }
458
- return epub;
459
- }
460
- async copy(path) {
461
- const extractPath = (0, import_path.join)(
462
- (0, import_node_os.tmpdir)(),
463
- `storyteller-platform-epub-${(0, import_node_crypto.randomUUID)()}.epub`
464
- );
465
- try {
466
- await (0, import_promises.cp)(this.extractPath, extractPath, { recursive: true });
467
- } catch (error) {
468
- (0, import_node_fs.rmSync)(extractPath, { force: true, recursive: true });
469
- throw error;
470
- }
471
- return new Epub(extractPath, path);
323
+ const newAdapter = await this.adapter.duplicate();
324
+ return new Epub(this.adapterClass, newAdapter, path);
472
325
  }
473
326
  async removeEntry(href) {
327
+ this.assertWritable();
328
+ if (!this.adapter.remove) {
329
+ throw new EpubReadOnlyError(
330
+ `adapter ${this.adapterClass.kind} does not support entry removal`
331
+ );
332
+ }
474
333
  const rootfile = await this.getRootfile();
475
334
  const filename = this.resolveInternalHref(rootfile, href);
476
- await (0, import_promises.rm)(filename);
335
+ await this.adapter.remove(filename);
477
336
  }
478
337
  async getFileData(path, encoding) {
479
- return await (0, import_promises.readFile)(path, encoding);
338
+ if (encoding) {
339
+ return this.adapter.read(path, encoding);
340
+ }
341
+ return this.adapter.read(path);
342
+ }
343
+ /**
344
+ * Length of the underlying archive entry for a manifest item, in bytes
345
+ * Necessary to compute the readium page count which is for COMPRESSED content
346
+ * @see {@link https://github.com/readium/architecture/issues/123}
347
+ */
348
+ async getItemArchiveLength(id) {
349
+ const rootfile = await this.getRootfile();
350
+ const manifest = await this.getManifest();
351
+ const manifestItem = manifest[id];
352
+ if (!manifestItem)
353
+ 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);
480
356
  }
481
357
  async getRootfile() {
482
358
  var _a;
483
359
  if (this.rootfile !== null) return this.rootfile;
484
360
  const containerString = await this.getFileData(
485
- (0, import_path.join)(this.extractPath, "META-INF", "container.xml"),
361
+ (0, import_path.join)(this.adapter.rootPath, "META-INF", "container.xml"),
486
362
  "utf-8"
487
363
  );
488
364
  if (!containerString)
@@ -514,7 +390,7 @@ ${JSON.stringify(element, null, 2)}`
514
390
  throw new Error(
515
391
  "Failed to parse EPUB container.xml: Found no rootfile element"
516
392
  );
517
- this.rootfile = (0, import_path.resolve)(this.extractPath, fullPath);
393
+ this.rootfile = (0, import_path.resolve)(this.adapter.rootPath, fullPath);
518
394
  return this.rootfile;
519
395
  }
520
396
  async getPackageDocument() {
@@ -552,6 +428,7 @@ ${JSON.stringify(element, null, 2)}`
552
428
  * it will be assumed that the package document was modified in place.
553
429
  */
554
430
  async withPackage(producer) {
431
+ this.assertWritable();
555
432
  await this.packageMutex.runExclusive(async () => {
556
433
  const packageDocument = await this.getPackageDocument();
557
434
  const packageElement = Epub.findXmlChildByName("package", packageDocument);
@@ -1786,7 +1663,7 @@ ${JSON.stringify(element, null, 2)}`
1786
1663
  resolveInternalHref(from, href) {
1787
1664
  const startPath = (0, import_path.dirname)(from);
1788
1665
  return (0, import_path.resolve)(
1789
- this.extractPath,
1666
+ this.adapter.rootPath,
1790
1667
  (0, import_path.hrefToPlatformPath)(startPath),
1791
1668
  (0, import_path.hrefToPlatformPath)(href)
1792
1669
  );
@@ -1803,7 +1680,7 @@ ${JSON.stringify(element, null, 2)}`
1803
1680
  const rootfile = await this.getRootfile();
1804
1681
  const from = relativeTo ? this.resolveInternalHref(rootfile, relativeTo) : rootfile;
1805
1682
  const path = this.resolveInternalHref(from, href);
1806
- return path.replace(toRoot ? this.extractPath : (0, import_path.dirname)(rootfile), "").slice(1);
1683
+ return path.replace(toRoot ? this.adapter.rootPath : (0, import_path.dirname)(rootfile), "").slice(1);
1807
1684
  }
1808
1685
  async readFileContents(href, relativeTo, encoding) {
1809
1686
  const rootfile = await this.getRootfile();
@@ -1858,8 +1735,17 @@ ${JSON.stringify(element, null, 2)}`
1858
1735
  return Epub.getXhtmlTextContent(body);
1859
1736
  }
1860
1737
  async writeEntryContents(path, contents, encoding) {
1861
- await (0, import_promises.mkdir)((0, import_path.dirname)(path), { recursive: true });
1862
- await (0, import_promises.writeFile)(path, contents, encoding);
1738
+ this.assertWritable();
1739
+ if (!this.adapter.write) {
1740
+ throw new EpubReadOnlyError(
1741
+ `adapter ${this.adapterClass.kind} does not support writes`
1742
+ );
1743
+ }
1744
+ if (encoding === "utf-8") {
1745
+ await this.adapter.write(path, contents, encoding);
1746
+ } else {
1747
+ await this.adapter.write(path, contents);
1748
+ }
1863
1749
  }
1864
1750
  async writeItemContents(id, contents, encoding) {
1865
1751
  const rootfile = await this.getRootfile();
@@ -1949,8 +1835,7 @@ ${JSON.stringify(element, null, 2)}`
1949
1835
  contents
1950
1836
  )
1951
1837
  ) : contents;
1952
- await (0, import_promises.mkdir)((0, import_path.dirname)(filename), { recursive: true });
1953
- await (0, import_promises.writeFile)(filename, data);
1838
+ await this.writeEntryContents(filename, data);
1954
1839
  }
1955
1840
  /**
1956
1841
  * Update the manifest entry for an existing item.
@@ -2203,7 +2088,7 @@ ${JSON.stringify(element, null, 2)}`
2203
2088
  this.rootfile = null;
2204
2089
  this.manifest = null;
2205
2090
  this.spine = null;
2206
- (0, import_node_fs.rmSync)(this.extractPath, { recursive: true, force: true });
2091
+ void this.adapter.dispose();
2207
2092
  }
2208
2093
  /**
2209
2094
  * Write the current contents of the Epub to a new
@@ -2214,63 +2099,157 @@ ${JSON.stringify(element, null, 2)}`
2214
2099
  * timestamp.
2215
2100
  */
2216
2101
  async saveAndClose() {
2217
- var _stack = [];
2218
- try {
2219
- if (!this.inputPath) {
2220
- throw new Error("In-memory EPUB files cannot be saved to disk");
2102
+ this.assertWritable();
2103
+ if (!this.inputPath) {
2104
+ throw new Error("In-memory EPUB files cannot be saved to disk");
2105
+ }
2106
+ if (!this.adapter.serialize) {
2107
+ throw new Error(
2108
+ `adapter ${this.adapterClass.kind} does not support serialization`
2109
+ );
2110
+ }
2111
+ await this.replaceMetadata(
2112
+ (entry) => entry.properties["property"] === "dcterms:modified",
2113
+ {
2114
+ type: "meta",
2115
+ properties: { property: "dcterms:modified" },
2116
+ // We need UTC with integer seconds, but toISOString gives UTC with ms
2117
+ value: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+/, "")
2221
2118
  }
2222
- await this.replaceMetadata(
2223
- (entry) => entry.properties["property"] === "dcterms:modified",
2224
- {
2225
- type: "meta",
2226
- properties: { property: "dcterms:modified" },
2227
- // We need UTC with integer seconds, but toISOString gives UTC with ms
2228
- value: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+/, "")
2229
- }
2119
+ );
2120
+ await this.adapter.serialize(this.inputPath);
2121
+ }
2122
+ /**
2123
+ * Upgrade an EPUB 2 publication to EPUB 3 in place, returning a new,
2124
+ * valid Epub 3 instance. Equivalent to
2125
+ * `Epub.using(TmpFsAdapter).upgrade(...)`.
2126
+ */
2127
+ static async upgrade(path, options = {}) {
2128
+ return Epub.using(import_tmpfs.TmpFsAdapter).upgrade(path, options);
2129
+ }
2130
+ [Symbol.dispose]() {
2131
+ this.discardAndClose();
2132
+ }
2133
+ }
2134
+ class EpubFactory {
2135
+ constructor(adapterClass) {
2136
+ this.adapterClass = adapterClass;
2137
+ }
2138
+ async from(source, options = {}) {
2139
+ const adapter = await this.adapterClass.init(
2140
+ source,
2141
+ options
2142
+ );
2143
+ const inputPath = typeof source === "string" ? source : void 0;
2144
+ const readonlyOverride = options.readonly === true;
2145
+ const epub = new Epub(
2146
+ this.adapterClass,
2147
+ adapter,
2148
+ inputPath,
2149
+ readonlyOverride
2150
+ );
2151
+ try {
2152
+ await epub.getPackageElement();
2153
+ } catch (e) {
2154
+ epub.discardAndClose();
2155
+ console.error(e);
2156
+ throw new Error(
2157
+ "This is not a valid EPUB publication. Could not read the package document."
2158
+ );
2159
+ }
2160
+ await Epub.assertEpub3(epub);
2161
+ return epub;
2162
+ }
2163
+ /**
2164
+ * Construct a new EPUB on this factory's adapter, optionally seeded
2165
+ * with the provided metadata. Requires a writable adapter that
2166
+ * implements `initEmpty` (today: {@link TmpFsAdapter}).
2167
+ *
2168
+ * @throws when the adapter is read-only or does not implement initEmpty
2169
+ */
2170
+ async create(path, {
2171
+ title,
2172
+ language,
2173
+ identifier,
2174
+ date,
2175
+ subjects,
2176
+ type,
2177
+ creators,
2178
+ contributors
2179
+ }, additionalMetadata = []) {
2180
+ if (!this.adapterClass.capabilities.writable) {
2181
+ throw new EpubReadOnlyError(
2182
+ `adapter ${this.adapterClass.kind} is read-only; cannot create`
2183
+ );
2184
+ }
2185
+ if (!this.adapterClass.initEmpty) {
2186
+ throw new Error(
2187
+ `adapter ${this.adapterClass.kind} does not support create() (missing initEmpty)`
2230
2188
  );
2231
- const tmpArchivePath = (0, import_path.join)(
2232
- (0, import_node_os.tmpdir)(),
2233
- `storyteller-platform-epub-${(0, import_node_crypto.randomUUID)()}`
2189
+ }
2190
+ const adapter = await this.adapterClass.initEmpty();
2191
+ if (!adapter.write) {
2192
+ throw new Error(
2193
+ `adapter ${this.adapterClass.kind} declared writable but did not implement write()`
2234
2194
  );
2235
- const { promise, resolve: resolve2 } = Promise.withResolvers();
2236
- const zipfile = new import_yazl.ZipFile();
2237
- const writeStream = (0, import_node_fs.createWriteStream)(tmpArchivePath);
2238
- writeStream.on("close", () => {
2239
- resolve2();
2240
- });
2241
- const stack = __using(_stack, new AsyncDisposableStack(), true);
2242
- stack.defer(async () => {
2243
- writeStream.close();
2244
- await (0, import_promises.rm)(tmpArchivePath, { force: true });
2245
- });
2246
- zipfile.outputStream.pipe(writeStream);
2247
- zipfile.addBuffer(Buffer.from("application/epub+zip"), "mimetype", {
2248
- compress: false
2249
- });
2250
- const entries = await (0, import_promises.readdir)(this.extractPath, {
2251
- recursive: true,
2252
- withFileTypes: true
2253
- });
2254
- for (const entry of entries) {
2255
- if (entry.name === "mimetype" || entry.isDirectory()) continue;
2256
- zipfile.addFile(
2257
- (0, import_path.join)(entry.parentPath, entry.name),
2258
- (0, import_path.join)(entry.parentPath, entry.name).replace(`${this.extractPath}/`, ""),
2259
- { compress: !isAudioFile(entry.name) }
2260
- );
2261
- }
2262
- zipfile.end();
2263
- await promise;
2264
- await (0, import_promises.cp)(tmpArchivePath, this.inputPath);
2265
- } catch (_) {
2266
- var _error = _, _hasError = true;
2267
- } finally {
2268
- var _promise = __callDispose(_stack, _error, _hasError);
2269
- _promise && await _promise;
2270
2195
  }
2196
+ const encoder = new TextEncoder();
2197
+ const container = encoder.encode(`<?xml version="1.0"?>
2198
+ <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
2199
+ <rootfiles>
2200
+ <rootfile media-type="application/oebps-package+xml" full-path="OEBPS/content.opf"/>
2201
+ </rootfiles>
2202
+ </container>
2203
+ `);
2204
+ await adapter.write(
2205
+ (0, import_path.join)(adapter.rootPath, "META-INF", "container.xml"),
2206
+ container
2207
+ );
2208
+ const packageDocument = encoder.encode(`<?xml version="1.0"?>
2209
+ <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/">
2210
+ <metadata>
2211
+ </metadata>
2212
+ <manifest>
2213
+ </manifest>
2214
+ <spine>
2215
+ </spine>
2216
+ </package>
2217
+ `);
2218
+ await adapter.write(
2219
+ (0, import_path.join)(adapter.rootPath, "OEBPS", "content.opf"),
2220
+ packageDocument
2221
+ );
2222
+ const epub = new Epub(this.adapterClass, adapter, path);
2223
+ const metadata = [
2224
+ {
2225
+ id: "pub-id",
2226
+ type: "dc:identifier",
2227
+ properties: {},
2228
+ value: identifier
2229
+ },
2230
+ ...additionalMetadata
2231
+ ];
2232
+ await Promise.all(metadata.map((entry) => epub.addMetadata(entry)));
2233
+ await epub.setTitle(title);
2234
+ await epub.setLanguage(language);
2235
+ if (date) await epub.setPublicationDate(date);
2236
+ if (type) await epub.setType(type);
2237
+ if (subjects) {
2238
+ await Promise.all(subjects.map((subject) => epub.addSubject(subject)));
2239
+ }
2240
+ if (creators) {
2241
+ await Promise.all(creators.map((creator) => epub.addCreator(creator)));
2242
+ }
2243
+ if (contributors) {
2244
+ await Promise.all(
2245
+ contributors.map((contributor) => epub.addCreator(contributor))
2246
+ );
2247
+ }
2248
+ return epub;
2271
2249
  }
2272
2250
  /**
2273
- * Upgrade an EPUB 2 publication to EPUB 3 in place, returning a new, valid Epub 3 instance.
2251
+ * Upgrade an EPUB 2 publication to EPUB 3 in place using this
2252
+ * factory's adapter, returning a new, valid Epub 3 instance.
2274
2253
  *
2275
2254
  * Performs the following transformations:
2276
2255
  * - upgrades OPF metadata to EPUB 3 conventions
@@ -2280,15 +2259,40 @@ ${JSON.stringify(element, null, 2)}`
2280
2259
  * - fixes common font MIME types
2281
2260
  * - bumps the package version to 3.0
2282
2261
  * - goes over each xhtml item and rewrites it using XMLParser to make sure the output is valid XHTML
2262
+ *
2263
+ * Requires a writable adapter. When {@link Upgrade.Epub2UpgradeOptions.outputPath}
2264
+ * is set, the source file is copied to that path on disk first; this
2265
+ * only makes sense for adapters whose `source` is a real fs path.
2266
+ *
2267
+ * @throws when the adapter is read-only
2283
2268
  */
2284
- static async upgrade(path, options = {}) {
2269
+ async upgrade(path, options = {}) {
2285
2270
  var _a;
2271
+ if (!this.adapterClass.capabilities.writable) {
2272
+ throw new EpubReadOnlyError(
2273
+ `adapter ${this.adapterClass.kind} is read-only; cannot upgrade`
2274
+ );
2275
+ }
2286
2276
  const { removeNcx = false, outputPath } = options;
2287
2277
  if (outputPath) {
2288
2278
  await (0, import_promises.mkdir)((0, import_path.dirname)(outputPath), { recursive: true });
2289
2279
  await (0, import_promises.cp)(path, outputPath, { force: true });
2290
2280
  }
2291
- const epub = await Epub.open(outputPath ?? path);
2281
+ const source = outputPath ?? path;
2282
+ const adapter = await this.adapterClass.init(
2283
+ source,
2284
+ options
2285
+ );
2286
+ const epub = new Epub(this.adapterClass, adapter, source);
2287
+ try {
2288
+ await epub.getPackageElement();
2289
+ } catch (e) {
2290
+ epub.discardAndClose();
2291
+ console.error(e);
2292
+ throw new Error(
2293
+ "This is not a valid EPUB publication. Could not read the package document."
2294
+ );
2295
+ }
2292
2296
  const version = await epub.getVersion();
2293
2297
  if (version.startsWith("3.")) {
2294
2298
  return epub;
@@ -2333,12 +2337,13 @@ ${JSON.stringify(element, null, 2)}`
2333
2337
  }
2334
2338
  return epub;
2335
2339
  }
2336
- [Symbol.dispose]() {
2337
- this.discardAndClose();
2338
- }
2339
2340
  }
2340
2341
  // Annotate the CommonJS export names for ESM import in node:
2341
2342
  0 && (module.exports = {
2342
2343
  Epub,
2343
- EpubVersionError
2344
+ EpubFactory,
2345
+ EpubReadOnlyError,
2346
+ EpubVersionError,
2347
+ MemoryAdapter,
2348
+ TmpFsAdapter
2344
2349
  });