@storyteller-platform/epub 0.5.0 → 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/README.md +1491 -284
- package/dist/adapters/interface.cjs +16 -0
- package/dist/adapters/interface.d.cts +69 -0
- package/dist/adapters/interface.d.ts +69 -0
- package/dist/adapters/interface.js +0 -0
- package/dist/adapters/memory.cjs +103 -0
- package/dist/adapters/memory.d.cts +37 -0
- package/dist/adapters/memory.d.ts +37 -0
- package/dist/adapters/memory.js +83 -0
- package/dist/adapters/tmpfs.cjs +235 -0
- package/dist/adapters/tmpfs.d.cts +29 -0
- package/dist/adapters/tmpfs.d.ts +29 -0
- package/dist/adapters/tmpfs.js +178 -0
- package/dist/index.cjs +289 -280
- package/dist/index.d.cts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +292 -240
- package/dist/upgrade.d.cts +142 -19
- package/dist/upgrade.d.ts +142 -19
- package/package.json +3 -2
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
*
|
|
321
|
-
* with the provided metadata.
|
|
262
|
+
* Storage backend kind in use for this instance
|
|
322
263
|
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
|
392
|
-
*
|
|
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
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
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
|
|
399
|
-
|
|
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
|
-
|
|
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("/")) {
|
|
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
|
-
|
|
320
|
+
`cannot copy an Epub backed by ${this.adapterClass.kind}: adapter does not implement duplicate()`
|
|
456
321
|
);
|
|
457
322
|
}
|
|
458
|
-
|
|
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
|
|
335
|
+
await this.adapter.remove(filename);
|
|
477
336
|
}
|
|
478
337
|
async getFileData(path, encoding) {
|
|
479
|
-
|
|
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.
|
|
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.
|
|
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);
|
|
@@ -1785,7 +1662,11 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1785
1662
|
*/
|
|
1786
1663
|
resolveInternalHref(from, href) {
|
|
1787
1664
|
const startPath = (0, import_path.dirname)(from);
|
|
1788
|
-
return (0, import_path.resolve)(
|
|
1665
|
+
return (0, import_path.resolve)(
|
|
1666
|
+
this.adapter.rootPath,
|
|
1667
|
+
(0, import_path.hrefToPlatformPath)(startPath),
|
|
1668
|
+
(0, import_path.hrefToPlatformPath)(href)
|
|
1669
|
+
);
|
|
1789
1670
|
}
|
|
1790
1671
|
/**
|
|
1791
1672
|
* Returns a path-relative-scheme-less URL, relative to the
|
|
@@ -1799,7 +1680,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1799
1680
|
const rootfile = await this.getRootfile();
|
|
1800
1681
|
const from = relativeTo ? this.resolveInternalHref(rootfile, relativeTo) : rootfile;
|
|
1801
1682
|
const path = this.resolveInternalHref(from, href);
|
|
1802
|
-
return path.replace(toRoot ? this.
|
|
1683
|
+
return path.replace(toRoot ? this.adapter.rootPath : (0, import_path.dirname)(rootfile), "").slice(1);
|
|
1803
1684
|
}
|
|
1804
1685
|
async readFileContents(href, relativeTo, encoding) {
|
|
1805
1686
|
const rootfile = await this.getRootfile();
|
|
@@ -1854,8 +1735,17 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1854
1735
|
return Epub.getXhtmlTextContent(body);
|
|
1855
1736
|
}
|
|
1856
1737
|
async writeEntryContents(path, contents, encoding) {
|
|
1857
|
-
|
|
1858
|
-
|
|
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
|
+
}
|
|
1859
1749
|
}
|
|
1860
1750
|
async writeItemContents(id, contents, encoding) {
|
|
1861
1751
|
const rootfile = await this.getRootfile();
|
|
@@ -1945,8 +1835,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1945
1835
|
contents
|
|
1946
1836
|
)
|
|
1947
1837
|
) : contents;
|
|
1948
|
-
await
|
|
1949
|
-
await (0, import_promises.writeFile)(filename, data);
|
|
1838
|
+
await this.writeEntryContents(filename, data);
|
|
1950
1839
|
}
|
|
1951
1840
|
/**
|
|
1952
1841
|
* Update the manifest entry for an existing item.
|
|
@@ -2199,7 +2088,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
2199
2088
|
this.rootfile = null;
|
|
2200
2089
|
this.manifest = null;
|
|
2201
2090
|
this.spine = null;
|
|
2202
|
-
|
|
2091
|
+
void this.adapter.dispose();
|
|
2203
2092
|
}
|
|
2204
2093
|
/**
|
|
2205
2094
|
* Write the current contents of the Epub to a new
|
|
@@ -2210,63 +2099,157 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
2210
2099
|
* timestamp.
|
|
2211
2100
|
*/
|
|
2212
2101
|
async saveAndClose() {
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
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+/, "")
|
|
2217
2118
|
}
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
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`
|
|
2226
2183
|
);
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
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 { promise, resolve: resolve2 } = Promise.withResolvers();
|
|
2232
|
-
const zipfile = new import_yazl.ZipFile();
|
|
2233
|
-
const writeStream = (0, import_node_fs.createWriteStream)(tmpArchivePath);
|
|
2234
|
-
writeStream.on("close", () => {
|
|
2235
|
-
resolve2();
|
|
2236
|
-
});
|
|
2237
|
-
const stack = __using(_stack, new AsyncDisposableStack(), true);
|
|
2238
|
-
stack.defer(async () => {
|
|
2239
|
-
writeStream.close();
|
|
2240
|
-
await (0, import_promises.rm)(tmpArchivePath, { force: true });
|
|
2241
|
-
});
|
|
2242
|
-
zipfile.outputStream.pipe(writeStream);
|
|
2243
|
-
zipfile.addBuffer(Buffer.from("application/epub+zip"), "mimetype", {
|
|
2244
|
-
compress: false
|
|
2245
|
-
});
|
|
2246
|
-
const entries = await (0, import_promises.readdir)(this.extractPath, {
|
|
2247
|
-
recursive: true,
|
|
2248
|
-
withFileTypes: true
|
|
2249
|
-
});
|
|
2250
|
-
for (const entry of entries) {
|
|
2251
|
-
if (entry.name === "mimetype" || entry.isDirectory()) continue;
|
|
2252
|
-
zipfile.addFile(
|
|
2253
|
-
(0, import_path.join)(entry.parentPath, entry.name),
|
|
2254
|
-
(0, import_path.join)(entry.parentPath, entry.name).replace(`${this.extractPath}/`, ""),
|
|
2255
|
-
{ compress: !isAudioFile(entry.name) }
|
|
2256
|
-
);
|
|
2257
|
-
}
|
|
2258
|
-
zipfile.end();
|
|
2259
|
-
await promise;
|
|
2260
|
-
await (0, import_promises.cp)(tmpArchivePath, this.inputPath);
|
|
2261
|
-
} catch (_) {
|
|
2262
|
-
var _error = _, _hasError = true;
|
|
2263
|
-
} finally {
|
|
2264
|
-
var _promise = __callDispose(_stack, _error, _hasError);
|
|
2265
|
-
_promise && await _promise;
|
|
2266
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()`
|
|
2194
|
+
);
|
|
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;
|
|
2267
2249
|
}
|
|
2268
2250
|
/**
|
|
2269
|
-
* Upgrade an EPUB 2 publication to EPUB 3 in place
|
|
2251
|
+
* Upgrade an EPUB 2 publication to EPUB 3 in place using this
|
|
2252
|
+
* factory's adapter, returning a new, valid Epub 3 instance.
|
|
2270
2253
|
*
|
|
2271
2254
|
* Performs the following transformations:
|
|
2272
2255
|
* - upgrades OPF metadata to EPUB 3 conventions
|
|
@@ -2276,15 +2259,40 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
2276
2259
|
* - fixes common font MIME types
|
|
2277
2260
|
* - bumps the package version to 3.0
|
|
2278
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
|
|
2279
2268
|
*/
|
|
2280
|
-
|
|
2269
|
+
async upgrade(path, options = {}) {
|
|
2281
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
|
+
}
|
|
2282
2276
|
const { removeNcx = false, outputPath } = options;
|
|
2283
2277
|
if (outputPath) {
|
|
2284
2278
|
await (0, import_promises.mkdir)((0, import_path.dirname)(outputPath), { recursive: true });
|
|
2285
2279
|
await (0, import_promises.cp)(path, outputPath, { force: true });
|
|
2286
2280
|
}
|
|
2287
|
-
const
|
|
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
|
+
}
|
|
2288
2296
|
const version = await epub.getVersion();
|
|
2289
2297
|
if (version.startsWith("3.")) {
|
|
2290
2298
|
return epub;
|
|
@@ -2329,12 +2337,13 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
2329
2337
|
}
|
|
2330
2338
|
return epub;
|
|
2331
2339
|
}
|
|
2332
|
-
[Symbol.dispose]() {
|
|
2333
|
-
this.discardAndClose();
|
|
2334
|
-
}
|
|
2335
2340
|
}
|
|
2336
2341
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2337
2342
|
0 && (module.exports = {
|
|
2338
2343
|
Epub,
|
|
2339
|
-
|
|
2344
|
+
EpubFactory,
|
|
2345
|
+
EpubReadOnlyError,
|
|
2346
|
+
EpubVersionError,
|
|
2347
|
+
MemoryAdapter,
|
|
2348
|
+
TmpFsAdapter
|
|
2340
2349
|
});
|