@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.d.cts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
import 'fast-xml-parser';
|
|
2
|
-
export {
|
|
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 {
|
|
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,52 +1,37 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
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 {
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
import {
|
|
9
|
+
dirname,
|
|
10
|
+
hrefToPlatformPath,
|
|
11
|
+
join,
|
|
12
|
+
resolve
|
|
13
|
+
} from "@storyteller-platform/path";
|
|
14
|
+
import { TmpFsAdapter } from "./adapters/tmpfs.js";
|
|
18
15
|
import * as Upgrade from "./upgrade.js";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const AAC_FILE_EXTENSIONS = [".aac"];
|
|
22
|
-
const OGG_FILE_EXTENSIONS = [".ogg", ".oga", ".mogg"];
|
|
23
|
-
const OPUS_FILE_EXTENSIONS = [".opus"];
|
|
24
|
-
const WAVE_FILE_EXTENSIONS = [".wav"];
|
|
25
|
-
const AIFF_FILE_EXTENSIONS = [".aiff"];
|
|
26
|
-
const FLAC_FILE_EXTENSIONS = [".flac"];
|
|
27
|
-
const ALAC_FILE_EXTENSIONS = [".alac"];
|
|
28
|
-
const WEBM_FILE_EXTENSIONS = [".weba"];
|
|
29
|
-
const AUDIO_FILE_EXTENSIONS = [
|
|
30
|
-
...MP3_FILE_EXTENSIONS,
|
|
31
|
-
...AAC_FILE_EXTENSIONS,
|
|
32
|
-
...MPEG4_FILE_EXTENSIONS,
|
|
33
|
-
...OPUS_FILE_EXTENSIONS,
|
|
34
|
-
...OGG_FILE_EXTENSIONS,
|
|
35
|
-
...WAVE_FILE_EXTENSIONS,
|
|
36
|
-
...AIFF_FILE_EXTENSIONS,
|
|
37
|
-
...FLAC_FILE_EXTENSIONS,
|
|
38
|
-
...ALAC_FILE_EXTENSIONS,
|
|
39
|
-
...WEBM_FILE_EXTENSIONS
|
|
40
|
-
];
|
|
41
|
-
function isAudioFile(filenameOrExt) {
|
|
42
|
-
return AUDIO_FILE_EXTENSIONS.some((ext) => filenameOrExt.endsWith(ext));
|
|
43
|
-
}
|
|
16
|
+
import { MemoryAdapter } from "./adapters/memory.js";
|
|
17
|
+
import { TmpFsAdapter as TmpFsAdapter2 } from "./adapters/tmpfs.js";
|
|
44
18
|
class EpubVersionError extends Error {
|
|
45
19
|
}
|
|
20
|
+
class EpubReadOnlyError extends Error {
|
|
21
|
+
}
|
|
46
22
|
class Epub {
|
|
47
|
-
|
|
48
|
-
|
|
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;
|
|
49
32
|
this.inputPath = inputPath;
|
|
33
|
+
this.readonlyOverride = readonlyOverride;
|
|
34
|
+
this.storage = adapterClass.kind;
|
|
50
35
|
this.readXhtmlItemContents = memoize(
|
|
51
36
|
this.readXhtmlItemContents.bind(this),
|
|
52
37
|
// This isn't unnecessary, the generic here just isn't handling the
|
|
@@ -242,86 +227,53 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
242
227
|
spine = null;
|
|
243
228
|
packageMutex = new Mutex();
|
|
244
229
|
/**
|
|
245
|
-
*
|
|
246
|
-
* with the provided metadata.
|
|
230
|
+
* Storage backend kind in use for this instance
|
|
247
231
|
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
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)
|
|
250
235
|
*/
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
contributors
|
|
260
|
-
}, additionalMetadata = []) {
|
|
261
|
-
const extractPath = join(
|
|
262
|
-
tmpdir(),
|
|
263
|
-
`storyteller-platform-epub-${randomUUID()}`
|
|
264
|
-
);
|
|
265
|
-
const encoder = new TextEncoder();
|
|
266
|
-
const container = encoder.encode(`<?xml version="1.0"?>
|
|
267
|
-
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
|
268
|
-
<rootfiles>
|
|
269
|
-
<rootfile media-type="application/oebps-package+xml" full-path="OEBPS/content.opf"/>
|
|
270
|
-
</rootfiles>
|
|
271
|
-
</container>
|
|
272
|
-
`);
|
|
273
|
-
await mkdir(join(extractPath, "META-INF"), { recursive: true });
|
|
274
|
-
await writeFile(join(extractPath, "META-INF", "container.xml"), container);
|
|
275
|
-
const packageDocument = encoder.encode(`<?xml version="1.0"?>
|
|
276
|
-
<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/">
|
|
277
|
-
<metadata>
|
|
278
|
-
</metadata>
|
|
279
|
-
<manifest>
|
|
280
|
-
</manifest>
|
|
281
|
-
<spine>
|
|
282
|
-
</spine>
|
|
283
|
-
</package>
|
|
284
|
-
`);
|
|
285
|
-
await mkdir(join(extractPath, "OEBPS"));
|
|
286
|
-
await writeFile(join(extractPath, "OEBPS", "content.opf"), packageDocument);
|
|
287
|
-
const epub = new this(extractPath, path);
|
|
288
|
-
const metadata = [
|
|
289
|
-
{
|
|
290
|
-
id: "pub-id",
|
|
291
|
-
type: "dc:identifier",
|
|
292
|
-
properties: {},
|
|
293
|
-
value: identifier
|
|
294
|
-
},
|
|
295
|
-
...additionalMetadata
|
|
296
|
-
];
|
|
297
|
-
await Promise.all(metadata.map((entry) => epub.addMetadata(entry)));
|
|
298
|
-
await epub.setTitle(title);
|
|
299
|
-
await epub.setLanguage(language);
|
|
300
|
-
if (date) await epub.setPublicationDate(date);
|
|
301
|
-
if (type) await epub.setType(type);
|
|
302
|
-
if (subjects) {
|
|
303
|
-
await Promise.all(subjects.map((subject) => epub.addSubject(subject)));
|
|
304
|
-
}
|
|
305
|
-
if (creators) {
|
|
306
|
-
await Promise.all(creators.map((creator) => epub.addCreator(creator)));
|
|
307
|
-
}
|
|
308
|
-
if (contributors) {
|
|
309
|
-
await Promise.all(
|
|
310
|
-
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."
|
|
311
244
|
);
|
|
312
245
|
}
|
|
313
|
-
return epub;
|
|
314
246
|
}
|
|
315
247
|
/**
|
|
316
|
-
* Construct
|
|
317
|
-
*
|
|
248
|
+
* Construct a new EPUB on a writable backend, optionally seeded
|
|
249
|
+
* with the provided metadata. Equivalent to
|
|
250
|
+
* `Epub.using(TmpFsAdapter).create(...)`.
|
|
318
251
|
*
|
|
319
|
-
* @param
|
|
320
|
-
*
|
|
321
|
-
* the data of the EPUB publication.
|
|
252
|
+
* @param dublinCore Core metadata terms
|
|
253
|
+
* @param additionalMetadata An array of additional metadata entries
|
|
322
254
|
*/
|
|
323
|
-
static async
|
|
324
|
-
|
|
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) {
|
|
325
277
|
const version = await epub.getVersion();
|
|
326
278
|
if (!version.startsWith("3.")) {
|
|
327
279
|
epub.discardAndClose();
|
|
@@ -329,85 +281,52 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
329
281
|
"This is not a valid EPUB 3 publication. This library only supports EPUB 3, not EPUB 2. Use Epub.upgrade(path) to convert."
|
|
330
282
|
);
|
|
331
283
|
}
|
|
332
|
-
return epub;
|
|
333
284
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
*/
|
|
337
|
-
static async open(pathOrData) {
|
|
338
|
-
const extractPath = join(
|
|
339
|
-
tmpdir(),
|
|
340
|
-
`storyteller-platform-epub-${randomUUID()}.epub`
|
|
341
|
-
);
|
|
342
|
-
try {
|
|
343
|
-
var _stack = [];
|
|
344
|
-
try {
|
|
345
|
-
const zipfile = typeof pathOrData === "string" ? await open(pathOrData) : await fromBuffer(Buffer.from(pathOrData));
|
|
346
|
-
const stack = __using(_stack, new AsyncDisposableStack(), true);
|
|
347
|
-
stack.defer(async () => {
|
|
348
|
-
await zipfile.close();
|
|
349
|
-
});
|
|
350
|
-
for await (const entry of zipfile) {
|
|
351
|
-
if (entry.filename.endsWith("/")) {
|
|
352
|
-
} else {
|
|
353
|
-
const writePath = join(extractPath, entry.filename);
|
|
354
|
-
const readStream = await entry.openReadStream();
|
|
355
|
-
await mkdir(dirname(writePath), { recursive: true });
|
|
356
|
-
const writeStream = createWriteStream(writePath);
|
|
357
|
-
await pipeline(readStream, writeStream);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
} catch (_) {
|
|
361
|
-
var _error = _, _hasError = true;
|
|
362
|
-
} finally {
|
|
363
|
-
var _promise = __callDispose(_stack, _error, _hasError);
|
|
364
|
-
_promise && await _promise;
|
|
365
|
-
}
|
|
366
|
-
} catch (error) {
|
|
367
|
-
rmSync(extractPath, { force: true, recursive: true });
|
|
368
|
-
throw error;
|
|
369
|
-
}
|
|
370
|
-
const epub = new this(
|
|
371
|
-
extractPath,
|
|
372
|
-
typeof pathOrData === "string" ? pathOrData : void 0
|
|
373
|
-
);
|
|
374
|
-
try {
|
|
375
|
-
await epub.getPackageElement();
|
|
376
|
-
} catch (e) {
|
|
377
|
-
epub.discardAndClose();
|
|
378
|
-
console.error(e);
|
|
285
|
+
async copy(path) {
|
|
286
|
+
if (!this.adapter.duplicate) {
|
|
379
287
|
throw new Error(
|
|
380
|
-
|
|
288
|
+
`cannot copy an Epub backed by ${this.adapterClass.kind}: adapter does not implement duplicate()`
|
|
381
289
|
);
|
|
382
290
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
async copy(path) {
|
|
386
|
-
const extractPath = join(
|
|
387
|
-
tmpdir(),
|
|
388
|
-
`storyteller-platform-epub-${randomUUID()}.epub`
|
|
389
|
-
);
|
|
390
|
-
try {
|
|
391
|
-
await cp(this.extractPath, extractPath, { recursive: true });
|
|
392
|
-
} catch (error) {
|
|
393
|
-
rmSync(extractPath, { force: true, recursive: true });
|
|
394
|
-
throw error;
|
|
395
|
-
}
|
|
396
|
-
return new Epub(extractPath, path);
|
|
291
|
+
const newAdapter = await this.adapter.duplicate();
|
|
292
|
+
return new Epub(this.adapterClass, newAdapter, path);
|
|
397
293
|
}
|
|
398
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
|
+
}
|
|
399
301
|
const rootfile = await this.getRootfile();
|
|
400
302
|
const filename = this.resolveInternalHref(rootfile, href);
|
|
401
|
-
await
|
|
303
|
+
await this.adapter.remove(filename);
|
|
402
304
|
}
|
|
403
305
|
async getFileData(path, encoding) {
|
|
404
|
-
|
|
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);
|
|
405
324
|
}
|
|
406
325
|
async getRootfile() {
|
|
407
326
|
var _a;
|
|
408
327
|
if (this.rootfile !== null) return this.rootfile;
|
|
409
328
|
const containerString = await this.getFileData(
|
|
410
|
-
join(this.
|
|
329
|
+
join(this.adapter.rootPath, "META-INF", "container.xml"),
|
|
411
330
|
"utf-8"
|
|
412
331
|
);
|
|
413
332
|
if (!containerString)
|
|
@@ -439,7 +358,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
439
358
|
throw new Error(
|
|
440
359
|
"Failed to parse EPUB container.xml: Found no rootfile element"
|
|
441
360
|
);
|
|
442
|
-
this.rootfile = resolve(this.
|
|
361
|
+
this.rootfile = resolve(this.adapter.rootPath, fullPath);
|
|
443
362
|
return this.rootfile;
|
|
444
363
|
}
|
|
445
364
|
async getPackageDocument() {
|
|
@@ -477,6 +396,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
477
396
|
* it will be assumed that the package document was modified in place.
|
|
478
397
|
*/
|
|
479
398
|
async withPackage(producer) {
|
|
399
|
+
this.assertWritable();
|
|
480
400
|
await this.packageMutex.runExclusive(async () => {
|
|
481
401
|
const packageDocument = await this.getPackageDocument();
|
|
482
402
|
const packageElement = Epub.findXmlChildByName("package", packageDocument);
|
|
@@ -1710,7 +1630,11 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1710
1630
|
*/
|
|
1711
1631
|
resolveInternalHref(from, href) {
|
|
1712
1632
|
const startPath = dirname(from);
|
|
1713
|
-
return resolve(
|
|
1633
|
+
return resolve(
|
|
1634
|
+
this.adapter.rootPath,
|
|
1635
|
+
hrefToPlatformPath(startPath),
|
|
1636
|
+
hrefToPlatformPath(href)
|
|
1637
|
+
);
|
|
1714
1638
|
}
|
|
1715
1639
|
/**
|
|
1716
1640
|
* Returns a path-relative-scheme-less URL, relative to the
|
|
@@ -1724,7 +1648,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1724
1648
|
const rootfile = await this.getRootfile();
|
|
1725
1649
|
const from = relativeTo ? this.resolveInternalHref(rootfile, relativeTo) : rootfile;
|
|
1726
1650
|
const path = this.resolveInternalHref(from, href);
|
|
1727
|
-
return path.replace(toRoot ? this.
|
|
1651
|
+
return path.replace(toRoot ? this.adapter.rootPath : dirname(rootfile), "").slice(1);
|
|
1728
1652
|
}
|
|
1729
1653
|
async readFileContents(href, relativeTo, encoding) {
|
|
1730
1654
|
const rootfile = await this.getRootfile();
|
|
@@ -1779,8 +1703,17 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1779
1703
|
return Epub.getXhtmlTextContent(body);
|
|
1780
1704
|
}
|
|
1781
1705
|
async writeEntryContents(path, contents, encoding) {
|
|
1782
|
-
|
|
1783
|
-
|
|
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
|
+
}
|
|
1784
1717
|
}
|
|
1785
1718
|
async writeItemContents(id, contents, encoding) {
|
|
1786
1719
|
const rootfile = await this.getRootfile();
|
|
@@ -1870,8 +1803,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1870
1803
|
contents
|
|
1871
1804
|
)
|
|
1872
1805
|
) : contents;
|
|
1873
|
-
await
|
|
1874
|
-
await writeFile(filename, data);
|
|
1806
|
+
await this.writeEntryContents(filename, data);
|
|
1875
1807
|
}
|
|
1876
1808
|
/**
|
|
1877
1809
|
* Update the manifest entry for an existing item.
|
|
@@ -2124,7 +2056,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
2124
2056
|
this.rootfile = null;
|
|
2125
2057
|
this.manifest = null;
|
|
2126
2058
|
this.spine = null;
|
|
2127
|
-
|
|
2059
|
+
void this.adapter.dispose();
|
|
2128
2060
|
}
|
|
2129
2061
|
/**
|
|
2130
2062
|
* Write the current contents of the Epub to a new
|
|
@@ -2135,63 +2067,157 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
2135
2067
|
* timestamp.
|
|
2136
2068
|
*/
|
|
2137
2069
|
async saveAndClose() {
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
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+/, "")
|
|
2142
2086
|
}
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
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`
|
|
2151
2151
|
);
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2152
|
+
}
|
|
2153
|
+
if (!this.adapterClass.initEmpty) {
|
|
2154
|
+
throw new Error(
|
|
2155
|
+
`adapter ${this.adapterClass.kind} does not support create() (missing initEmpty)`
|
|
2155
2156
|
);
|
|
2156
|
-
const { promise, resolve: resolve2 } = Promise.withResolvers();
|
|
2157
|
-
const zipfile = new ZipFile();
|
|
2158
|
-
const writeStream = createWriteStream(tmpArchivePath);
|
|
2159
|
-
writeStream.on("close", () => {
|
|
2160
|
-
resolve2();
|
|
2161
|
-
});
|
|
2162
|
-
const stack = __using(_stack, new AsyncDisposableStack(), true);
|
|
2163
|
-
stack.defer(async () => {
|
|
2164
|
-
writeStream.close();
|
|
2165
|
-
await rm(tmpArchivePath, { force: true });
|
|
2166
|
-
});
|
|
2167
|
-
zipfile.outputStream.pipe(writeStream);
|
|
2168
|
-
zipfile.addBuffer(Buffer.from("application/epub+zip"), "mimetype", {
|
|
2169
|
-
compress: false
|
|
2170
|
-
});
|
|
2171
|
-
const entries = await readdir(this.extractPath, {
|
|
2172
|
-
recursive: true,
|
|
2173
|
-
withFileTypes: true
|
|
2174
|
-
});
|
|
2175
|
-
for (const entry of entries) {
|
|
2176
|
-
if (entry.name === "mimetype" || entry.isDirectory()) continue;
|
|
2177
|
-
zipfile.addFile(
|
|
2178
|
-
join(entry.parentPath, entry.name),
|
|
2179
|
-
join(entry.parentPath, entry.name).replace(`${this.extractPath}/`, ""),
|
|
2180
|
-
{ compress: !isAudioFile(entry.name) }
|
|
2181
|
-
);
|
|
2182
|
-
}
|
|
2183
|
-
zipfile.end();
|
|
2184
|
-
await promise;
|
|
2185
|
-
await cp(tmpArchivePath, this.inputPath);
|
|
2186
|
-
} catch (_) {
|
|
2187
|
-
var _error = _, _hasError = true;
|
|
2188
|
-
} finally {
|
|
2189
|
-
var _promise = __callDispose(_stack, _error, _hasError);
|
|
2190
|
-
_promise && await _promise;
|
|
2191
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()`
|
|
2162
|
+
);
|
|
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;
|
|
2192
2217
|
}
|
|
2193
2218
|
/**
|
|
2194
|
-
* Upgrade an EPUB 2 publication to EPUB 3 in place
|
|
2219
|
+
* Upgrade an EPUB 2 publication to EPUB 3 in place using this
|
|
2220
|
+
* factory's adapter, returning a new, valid Epub 3 instance.
|
|
2195
2221
|
*
|
|
2196
2222
|
* Performs the following transformations:
|
|
2197
2223
|
* - upgrades OPF metadata to EPUB 3 conventions
|
|
@@ -2201,15 +2227,40 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
2201
2227
|
* - fixes common font MIME types
|
|
2202
2228
|
* - bumps the package version to 3.0
|
|
2203
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
|
|
2204
2236
|
*/
|
|
2205
|
-
|
|
2237
|
+
async upgrade(path, options = {}) {
|
|
2206
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
|
+
}
|
|
2207
2244
|
const { removeNcx = false, outputPath } = options;
|
|
2208
2245
|
if (outputPath) {
|
|
2209
2246
|
await mkdir(dirname(outputPath), { recursive: true });
|
|
2210
2247
|
await cp(path, outputPath, { force: true });
|
|
2211
2248
|
}
|
|
2212
|
-
const
|
|
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
|
+
}
|
|
2213
2264
|
const version = await epub.getVersion();
|
|
2214
2265
|
if (version.startsWith("3.")) {
|
|
2215
2266
|
return epub;
|
|
@@ -2254,11 +2305,12 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
2254
2305
|
}
|
|
2255
2306
|
return epub;
|
|
2256
2307
|
}
|
|
2257
|
-
[Symbol.dispose]() {
|
|
2258
|
-
this.discardAndClose();
|
|
2259
|
-
}
|
|
2260
2308
|
}
|
|
2261
2309
|
export {
|
|
2262
2310
|
Epub,
|
|
2263
|
-
|
|
2311
|
+
EpubFactory,
|
|
2312
|
+
EpubReadOnlyError,
|
|
2313
|
+
EpubVersionError,
|
|
2314
|
+
MemoryAdapter,
|
|
2315
|
+
TmpFsAdapter2 as TmpFsAdapter
|
|
2264
2316
|
};
|