@storyteller-platform/epub 0.4.9 → 0.5.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 +753 -136
- package/dist/chunk-BIEQXUOY.js +50 -0
- package/dist/index.cjs +421 -27
- package/dist/index.d.cts +2 -744
- package/dist/index.d.ts +2 -744
- package/dist/index.js +423 -71
- package/dist/upgrade.cjs +555 -0
- package/dist/upgrade.d.cts +909 -0
- package/dist/upgrade.d.ts +909 -0
- package/dist/upgrade.js +515 -0
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -1,48 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
var __using = (stack, value, async) => {
|
|
6
|
-
if (value != null) {
|
|
7
|
-
if (typeof value !== "object" && typeof value !== "function") __typeError("Object expected");
|
|
8
|
-
var dispose, inner;
|
|
9
|
-
if (async) dispose = value[__knownSymbol("asyncDispose")];
|
|
10
|
-
if (dispose === void 0) {
|
|
11
|
-
dispose = value[__knownSymbol("dispose")];
|
|
12
|
-
if (async) inner = dispose;
|
|
13
|
-
}
|
|
14
|
-
if (typeof dispose !== "function") __typeError("Object not disposable");
|
|
15
|
-
if (inner) dispose = function() {
|
|
16
|
-
try {
|
|
17
|
-
inner.call(this);
|
|
18
|
-
} catch (e) {
|
|
19
|
-
return Promise.reject(e);
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
stack.push([async, dispose, value]);
|
|
23
|
-
} else if (async) {
|
|
24
|
-
stack.push([async]);
|
|
25
|
-
}
|
|
26
|
-
return value;
|
|
27
|
-
};
|
|
28
|
-
var __callDispose = (stack, error, hasError) => {
|
|
29
|
-
var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) {
|
|
30
|
-
return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _;
|
|
31
|
-
};
|
|
32
|
-
var fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e);
|
|
33
|
-
var next = (it) => {
|
|
34
|
-
while (it = stack.pop()) {
|
|
35
|
-
try {
|
|
36
|
-
var result = it[1] && it[1].call(it[2]);
|
|
37
|
-
if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next()));
|
|
38
|
-
} catch (e) {
|
|
39
|
-
fail(e);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
if (hasError) throw error;
|
|
43
|
-
};
|
|
44
|
-
return next();
|
|
45
|
-
};
|
|
1
|
+
import {
|
|
2
|
+
__callDispose,
|
|
3
|
+
__using
|
|
4
|
+
} from "./chunk-BIEQXUOY.js";
|
|
46
5
|
import { randomUUID } from "node:crypto";
|
|
47
6
|
import { createWriteStream, rmSync } from "node:fs";
|
|
48
7
|
import { cp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
@@ -56,6 +15,34 @@ import { nanoid } from "nanoid";
|
|
|
56
15
|
import { fromBuffer, open } from "yauzl-promise";
|
|
57
16
|
import { ZipFile } from "yazl";
|
|
58
17
|
import { dirname, join, resolve } from "@storyteller-platform/path";
|
|
18
|
+
import * as Upgrade from "./upgrade.js";
|
|
19
|
+
const MP3_FILE_EXTENSIONS = [".mp3"];
|
|
20
|
+
const MPEG4_FILE_EXTENSIONS = [".mp4", ".m4a", ".m4b"];
|
|
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
|
+
}
|
|
44
|
+
class EpubVersionError extends Error {
|
|
45
|
+
}
|
|
59
46
|
class Epub {
|
|
60
47
|
constructor(extractPath, inputPath) {
|
|
61
48
|
this.extractPath = extractPath;
|
|
@@ -220,9 +207,29 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
220
207
|
* the provided name and optional filter.
|
|
221
208
|
*/
|
|
222
209
|
static findXmlChildByName(name, xml, filter) {
|
|
223
|
-
const element = xml.find(
|
|
210
|
+
const element = xml.find(
|
|
211
|
+
(e) => name in e && (filter ? filter(e) : true)
|
|
212
|
+
);
|
|
224
213
|
return element;
|
|
225
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Given an XML structure, find the first descendant matching
|
|
217
|
+
* the provided name and optional filter.
|
|
218
|
+
*
|
|
219
|
+
* Will perform a breadth first search for the element, returning
|
|
220
|
+
* the highest element in the tree matching the name and filter.
|
|
221
|
+
*/
|
|
222
|
+
static findXmlDescendantByName(name, xml, filter) {
|
|
223
|
+
const found = Epub.findXmlChildByName(name, xml, filter);
|
|
224
|
+
if (found) return found;
|
|
225
|
+
for (const node of xml) {
|
|
226
|
+
if (Epub.isXmlTextNode(node)) continue;
|
|
227
|
+
const children = Epub.getXmlChildren(node);
|
|
228
|
+
const found2 = this.findXmlDescendantByName(name, children, filter);
|
|
229
|
+
if (found2) return found2;
|
|
230
|
+
}
|
|
231
|
+
return void 0;
|
|
232
|
+
}
|
|
226
233
|
/**
|
|
227
234
|
* Given an XMLNode, determine whether it represents
|
|
228
235
|
* a text node or an XML element.
|
|
@@ -313,8 +320,21 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
313
320
|
* path to an EPUB file on disk, or a Uint8Array representing
|
|
314
321
|
* the data of the EPUB publication.
|
|
315
322
|
*/
|
|
316
|
-
// eslint-disable-next-line @typescript-eslint/require-await
|
|
317
323
|
static async from(pathOrData) {
|
|
324
|
+
const epub = await this.open(pathOrData);
|
|
325
|
+
const version = await epub.getVersion();
|
|
326
|
+
if (!version.startsWith("3.")) {
|
|
327
|
+
epub.discardAndClose();
|
|
328
|
+
throw new EpubVersionError(
|
|
329
|
+
"This is not a valid EPUB 3 publication. This library only supports EPUB 3, not EPUB 2. Use Epub.upgrade(path) to convert."
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
return epub;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Open an EPUB publication and return an Epub instance.
|
|
336
|
+
*/
|
|
337
|
+
static async open(pathOrData) {
|
|
318
338
|
const extractPath = join(
|
|
319
339
|
tmpdir(),
|
|
320
340
|
`storyteller-platform-epub-${randomUUID()}.epub`
|
|
@@ -347,10 +367,20 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
347
367
|
rmSync(extractPath, { force: true, recursive: true });
|
|
348
368
|
throw error;
|
|
349
369
|
}
|
|
350
|
-
|
|
370
|
+
const epub = new this(
|
|
351
371
|
extractPath,
|
|
352
372
|
typeof pathOrData === "string" ? pathOrData : void 0
|
|
353
373
|
);
|
|
374
|
+
try {
|
|
375
|
+
await epub.getPackageElement();
|
|
376
|
+
} catch (e) {
|
|
377
|
+
epub.discardAndClose();
|
|
378
|
+
console.error(e);
|
|
379
|
+
throw new Error(
|
|
380
|
+
"This is not a valid EPUB publication. Could not read the package document."
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
return epub;
|
|
354
384
|
}
|
|
355
385
|
async copy(path) {
|
|
356
386
|
const extractPath = join(
|
|
@@ -367,7 +397,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
367
397
|
}
|
|
368
398
|
async removeEntry(href) {
|
|
369
399
|
const rootfile = await this.getRootfile();
|
|
370
|
-
const filename = this.
|
|
400
|
+
const filename = this.resolveInternalHref(rootfile, href);
|
|
371
401
|
await rm(filename);
|
|
372
402
|
}
|
|
373
403
|
async getFileData(path, encoding) {
|
|
@@ -412,17 +442,6 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
412
442
|
this.rootfile = resolve(this.extractPath, fullPath);
|
|
413
443
|
return this.rootfile;
|
|
414
444
|
}
|
|
415
|
-
migratePackageDocument(packageDocument) {
|
|
416
|
-
for (const element of packageDocument) {
|
|
417
|
-
if (Epub.isXmlTextNode(element)) continue;
|
|
418
|
-
const elementName = Epub.getXmlElementName(element);
|
|
419
|
-
if (elementName.startsWith("opf:")) {
|
|
420
|
-
element[elementName.replace("opf:", "")] = Epub.getXmlChildren(element);
|
|
421
|
-
delete element[elementName];
|
|
422
|
-
this.migratePackageDocument(Epub.getXmlChildren(element));
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
445
|
async getPackageDocument() {
|
|
427
446
|
const rootfile = await this.getRootfile();
|
|
428
447
|
const packageDocumentString = await this.getFileData(rootfile, "utf-8");
|
|
@@ -437,13 +456,12 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
437
456
|
}
|
|
438
457
|
async getPackageElement() {
|
|
439
458
|
const packageDocument = await this.getPackageDocument();
|
|
440
|
-
const packageElement = Epub.findXmlChildByName("package", packageDocument)
|
|
459
|
+
const packageElement = Epub.findXmlChildByName("package", packageDocument);
|
|
441
460
|
if (!packageElement) {
|
|
442
461
|
throw new Error(
|
|
443
462
|
"Failed to parse EPUB: Found no package element in package document"
|
|
444
463
|
);
|
|
445
464
|
}
|
|
446
|
-
this.migratePackageDocument(packageDocument);
|
|
447
465
|
return packageElement;
|
|
448
466
|
}
|
|
449
467
|
/**
|
|
@@ -461,7 +479,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
461
479
|
async withPackage(producer) {
|
|
462
480
|
await this.packageMutex.runExclusive(async () => {
|
|
463
481
|
const packageDocument = await this.getPackageDocument();
|
|
464
|
-
const packageElement = Epub.findXmlChildByName("package", packageDocument)
|
|
482
|
+
const packageElement = Epub.findXmlChildByName("package", packageDocument);
|
|
465
483
|
if (!packageElement) {
|
|
466
484
|
throw new Error(
|
|
467
485
|
"Failed to parse EPUB: Found no package element in package document"
|
|
@@ -752,6 +770,57 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
752
770
|
value: date.toISOString()
|
|
753
771
|
});
|
|
754
772
|
}
|
|
773
|
+
/**
|
|
774
|
+
* Retrieve the modified date from the dcterms:modified metadata
|
|
775
|
+
* in the EPUB metadata as a Date object.
|
|
776
|
+
*
|
|
777
|
+
* If there is no meta element with dcterms:modified, returns null.
|
|
778
|
+
*
|
|
779
|
+
* @link https://www.w3.org/TR/epub-33/#sec-metadata-last-modified
|
|
780
|
+
*/
|
|
781
|
+
async getModifiedDate() {
|
|
782
|
+
const metadata = await this.getMetadata();
|
|
783
|
+
const entry = metadata.find(
|
|
784
|
+
({ properties }) => properties["property"] === "dcterms:modified"
|
|
785
|
+
);
|
|
786
|
+
if (!(entry == null ? void 0 : entry.value)) return null;
|
|
787
|
+
return new Date(entry.value);
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Retrieve the layout from the rendition:layout meta element
|
|
791
|
+
* in the EPUB metadata.
|
|
792
|
+
*
|
|
793
|
+
* If there is no meta element, returns 'reflowable'.
|
|
794
|
+
*
|
|
795
|
+
* @link https://www.w3.org/TR/epub-33/#layout
|
|
796
|
+
*/
|
|
797
|
+
async getLayout() {
|
|
798
|
+
const metadata = await this.getMetadata();
|
|
799
|
+
const entry = metadata.find(
|
|
800
|
+
({ properties }) => properties["property"] === "rendition:layout"
|
|
801
|
+
);
|
|
802
|
+
if ((entry == null ? void 0 : entry.value) !== "reflowable" && (entry == null ? void 0 : entry.value) !== "pre-paginated") {
|
|
803
|
+
return "reflowable";
|
|
804
|
+
}
|
|
805
|
+
return entry.value;
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Retrieve the base direction from the package element.
|
|
809
|
+
*
|
|
810
|
+
* If there is no `dir` attribute on the package element,
|
|
811
|
+
* returns 'auto'.
|
|
812
|
+
*
|
|
813
|
+
* @link https://www.w3.org/TR/epub-33/#attrdef-dir
|
|
814
|
+
*/
|
|
815
|
+
async getBaseDirection() {
|
|
816
|
+
var _a;
|
|
817
|
+
const packageEl = await this.getPackageElement();
|
|
818
|
+
const dir = (_a = packageEl[":@"]) == null ? void 0 : _a["@_dir"];
|
|
819
|
+
if (dir !== "ltr" && dir !== "rtl" && dir !== "auto") {
|
|
820
|
+
return "auto";
|
|
821
|
+
}
|
|
822
|
+
return dir;
|
|
823
|
+
}
|
|
755
824
|
/**
|
|
756
825
|
* Set the dc:type metadata element.
|
|
757
826
|
*
|
|
@@ -1532,17 +1601,135 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1532
1601
|
});
|
|
1533
1602
|
this.spine = null;
|
|
1534
1603
|
}
|
|
1604
|
+
async getNavigationChildren(ol, navHref, { resolveToRoot } = {}) {
|
|
1605
|
+
var _a;
|
|
1606
|
+
const children = [];
|
|
1607
|
+
const childrenElements = Epub.getXmlChildren(ol).filter(
|
|
1608
|
+
(node) => !Epub.isXmlTextNode(node) && Epub.getXmlElementName(node) === "li"
|
|
1609
|
+
);
|
|
1610
|
+
for (const childEl of childrenElements) {
|
|
1611
|
+
const [firstChild, secondChild] = Epub.getXmlChildren(childEl).filter(
|
|
1612
|
+
(node) => !Epub.isXmlTextNode(node) && ["a", "span", "ol"].includes(Epub.getXmlElementName(node))
|
|
1613
|
+
);
|
|
1614
|
+
if (!firstChild) continue;
|
|
1615
|
+
if (!["a", "span"].includes(Epub.getXmlElementName(firstChild))) {
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1618
|
+
if (Epub.getXmlElementName(firstChild) === "span" && (!secondChild || Epub.getXmlElementName(secondChild) !== "ol")) {
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1621
|
+
children.push({
|
|
1622
|
+
title: Epub.getXhtmlTextContent(Epub.getXmlChildren(firstChild)),
|
|
1623
|
+
...Epub.getXmlElementName(firstChild) === "a" && ((_a = firstChild[":@"]) == null ? void 0 : _a["@_href"]) && {
|
|
1624
|
+
href: await this.resolveHref(
|
|
1625
|
+
firstChild[":@"]["@_href"],
|
|
1626
|
+
void 0,
|
|
1627
|
+
{ toRoot: resolveToRoot }
|
|
1628
|
+
)
|
|
1629
|
+
},
|
|
1630
|
+
...secondChild && Epub.getXmlElementName(secondChild) === "ol" && {
|
|
1631
|
+
children: await this.getNavigationChildren(secondChild, navHref, {
|
|
1632
|
+
resolveToRoot
|
|
1633
|
+
})
|
|
1634
|
+
}
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
return children;
|
|
1638
|
+
}
|
|
1639
|
+
async getNavigation(role, { resolveToRoot } = {}) {
|
|
1640
|
+
const manifest = await this.getManifest();
|
|
1641
|
+
const navItem = Object.values(manifest).find(
|
|
1642
|
+
(item) => {
|
|
1643
|
+
var _a;
|
|
1644
|
+
return (_a = item.properties) == null ? void 0 : _a.includes("nav");
|
|
1645
|
+
}
|
|
1646
|
+
);
|
|
1647
|
+
if (!navItem) return null;
|
|
1648
|
+
const navContents = await this.readXhtmlItemContents(navItem.id);
|
|
1649
|
+
const navEl = Epub.findXmlDescendantByName(
|
|
1650
|
+
"nav",
|
|
1651
|
+
navContents,
|
|
1652
|
+
(node) => Epub.getXmlAttributes(node)["epub:type"] === role
|
|
1653
|
+
);
|
|
1654
|
+
if (!navEl) return null;
|
|
1655
|
+
const [firstChild, secondChild] = Epub.getXmlChildren(navEl).filter(
|
|
1656
|
+
(node) => !!(!Epub.isXmlTextNode(node) && Epub.getXmlElementName(node).match(/(?:h[1-6]|ol)/))
|
|
1657
|
+
);
|
|
1658
|
+
if (!firstChild) return null;
|
|
1659
|
+
const title = Epub.getXmlElementName(firstChild).match(/h[1-6]/) ? Epub.getXhtmlTextContent(Epub.getXmlChildren(firstChild)) : null;
|
|
1660
|
+
const list = Epub.getXmlElementName(firstChild) === "ol" ? firstChild : secondChild && Epub.getXmlElementName(secondChild) === "ol" ? secondChild : null;
|
|
1661
|
+
if (!list) return null;
|
|
1662
|
+
const children = await this.getNavigationChildren(list, navItem.href, {
|
|
1663
|
+
resolveToRoot
|
|
1664
|
+
});
|
|
1665
|
+
return {
|
|
1666
|
+
...title && { title },
|
|
1667
|
+
children
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Returns the structured table of contents navigation document
|
|
1672
|
+
* as a Navigation object.
|
|
1673
|
+
*
|
|
1674
|
+
* @link https://www.w3.org/TR/epub-33/#sec-nav-toc
|
|
1675
|
+
*/
|
|
1676
|
+
async getTableOfContents({
|
|
1677
|
+
resolveToRoot
|
|
1678
|
+
} = {}) {
|
|
1679
|
+
const navigationToc = await this.getNavigation("toc", { resolveToRoot });
|
|
1680
|
+
if (navigationToc) return navigationToc;
|
|
1681
|
+
const ncxToc = await this.getNcxTableOfContents();
|
|
1682
|
+
return {
|
|
1683
|
+
children: ncxToc
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* Returns the structured landmarks navigation document
|
|
1688
|
+
* as a Navigation object
|
|
1689
|
+
*
|
|
1690
|
+
* @link https://www.w3.org/TR/epub-33/#sec-nav-landmarks
|
|
1691
|
+
*/
|
|
1692
|
+
async getLandmarks({
|
|
1693
|
+
resolveToRoot
|
|
1694
|
+
} = {}) {
|
|
1695
|
+
return this.getNavigation("landmarks", { resolveToRoot });
|
|
1696
|
+
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Returns the structured page list navigation document
|
|
1699
|
+
* as a Navigation object
|
|
1700
|
+
*
|
|
1701
|
+
* @link https://www.w3.org/TR/epub-33/#sec-nav-landmarks
|
|
1702
|
+
*/
|
|
1703
|
+
async getPageList({
|
|
1704
|
+
resolveToRoot
|
|
1705
|
+
} = {}) {
|
|
1706
|
+
return this.getNavigation("page-list", { resolveToRoot });
|
|
1707
|
+
}
|
|
1535
1708
|
/**
|
|
1536
1709
|
* Returns a Zip Entry path for an HREF
|
|
1537
1710
|
*/
|
|
1538
|
-
|
|
1711
|
+
resolveInternalHref(from, href) {
|
|
1539
1712
|
const startPath = dirname(from);
|
|
1540
1713
|
return resolve(this.extractPath, startPath, href);
|
|
1541
1714
|
}
|
|
1715
|
+
/**
|
|
1716
|
+
* Returns a path-relative-scheme-less URL, relative to the
|
|
1717
|
+
* container root.
|
|
1718
|
+
*
|
|
1719
|
+
* @param href The href to resolve
|
|
1720
|
+
* @param [relativeTo] Optional - The href to resolve this href relative to.
|
|
1721
|
+
Use if resolving a relative href from a file other than the package document.
|
|
1722
|
+
*/
|
|
1723
|
+
async resolveHref(href, relativeTo, { toRoot } = {}) {
|
|
1724
|
+
const rootfile = await this.getRootfile();
|
|
1725
|
+
const from = relativeTo ? this.resolveInternalHref(rootfile, relativeTo) : rootfile;
|
|
1726
|
+
const path = this.resolveInternalHref(from, href);
|
|
1727
|
+
return path.replace(toRoot ? this.extractPath : dirname(rootfile), "").slice(1);
|
|
1728
|
+
}
|
|
1542
1729
|
async readFileContents(href, relativeTo, encoding) {
|
|
1543
1730
|
const rootfile = await this.getRootfile();
|
|
1544
|
-
const from = relativeTo ? this.
|
|
1545
|
-
const path = this.
|
|
1731
|
+
const from = relativeTo ? this.resolveInternalHref(rootfile, relativeTo) : rootfile;
|
|
1732
|
+
const path = this.resolveInternalHref(from, href);
|
|
1546
1733
|
const itemEntry = encoding ? await this.getFileData(path, encoding) : await this.getFileData(path);
|
|
1547
1734
|
return itemEntry;
|
|
1548
1735
|
}
|
|
@@ -1552,7 +1739,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1552
1739
|
const manifestItem = manifest[id];
|
|
1553
1740
|
if (!manifestItem)
|
|
1554
1741
|
throw new Error(`Could not find item with id "${id}" in manifest`);
|
|
1555
|
-
const path = this.
|
|
1742
|
+
const path = this.resolveInternalHref(rootfile, manifestItem.href);
|
|
1556
1743
|
const itemEntry = encoding ? await this.getFileData(path, encoding) : await this.getFileData(path);
|
|
1557
1744
|
return itemEntry;
|
|
1558
1745
|
}
|
|
@@ -1602,7 +1789,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1602
1789
|
if (!manifestItem)
|
|
1603
1790
|
throw new Error(`Could not find item with id "${id}" in manifest`);
|
|
1604
1791
|
memoize.clear(this.readXhtmlItemContents);
|
|
1605
|
-
const href = this.
|
|
1792
|
+
const href = this.resolveInternalHref(rootfile, manifestItem.href);
|
|
1606
1793
|
if (encoding === "utf-8") {
|
|
1607
1794
|
await this.writeEntryContents(href, contents, encoding);
|
|
1608
1795
|
} else {
|
|
@@ -1677,7 +1864,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1677
1864
|
});
|
|
1678
1865
|
this.manifest = null;
|
|
1679
1866
|
const rootfile = await this.getRootfile();
|
|
1680
|
-
const filename = this.
|
|
1867
|
+
const filename = this.resolveInternalHref(rootfile, item.href);
|
|
1681
1868
|
const data = encoding === "utf-8" || encoding === "xml" ? new TextEncoder().encode(
|
|
1682
1869
|
encoding === "utf-8" ? contents : await Epub.xmlBuilder.build(
|
|
1683
1870
|
contents
|
|
@@ -1834,6 +2021,105 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1834
2021
|
}
|
|
1835
2022
|
});
|
|
1836
2023
|
}
|
|
2024
|
+
/**
|
|
2025
|
+
* Returns the EPUB version declared on the package element.
|
|
2026
|
+
*/
|
|
2027
|
+
async getVersion() {
|
|
2028
|
+
var _a;
|
|
2029
|
+
const packageElement = await this.getPackageElement();
|
|
2030
|
+
return ((_a = packageElement[":@"]) == null ? void 0 : _a["@_version"]) ?? "2.0";
|
|
2031
|
+
}
|
|
2032
|
+
/**
|
|
2033
|
+
* Parse the NCX table of contents, if one exists, and return
|
|
2034
|
+
* a tree of TocEntry nodes.
|
|
2035
|
+
*
|
|
2036
|
+
* Useful for both EPUB 2 publications (where the NCX is the
|
|
2037
|
+
* primary navigation) and EPUB 3 publications that retain an
|
|
2038
|
+
* NCX for backwards compatibility.
|
|
2039
|
+
*/
|
|
2040
|
+
async getNcxTableOfContents() {
|
|
2041
|
+
var _a;
|
|
2042
|
+
const [manifest, packageElement] = await Promise.all([
|
|
2043
|
+
this.getManifest(),
|
|
2044
|
+
this.getPackageElement()
|
|
2045
|
+
]);
|
|
2046
|
+
const spine = Epub.findXmlChildByName(
|
|
2047
|
+
"spine",
|
|
2048
|
+
Epub.getXmlChildren(packageElement)
|
|
2049
|
+
);
|
|
2050
|
+
const spineTocId = (_a = spine == null ? void 0 : spine[":@"]) == null ? void 0 : _a["@_toc"];
|
|
2051
|
+
const ncxItem = spineTocId ? manifest[spineTocId] : Object.values(manifest).find(
|
|
2052
|
+
(item) => {
|
|
2053
|
+
var _a2;
|
|
2054
|
+
return ((_a2 = item.mediaType) == null ? void 0 : _a2.toLowerCase()) === "application/x-dtbncx+xml";
|
|
2055
|
+
}
|
|
2056
|
+
);
|
|
2057
|
+
if (!ncxItem) return [];
|
|
2058
|
+
const ncxContent = await this.readItemContents(ncxItem.id, "utf-8");
|
|
2059
|
+
const ncxXml = Epub.xmlParser.parse(ncxContent);
|
|
2060
|
+
const ncxElement = Epub.findXmlChildByName("ncx", ncxXml);
|
|
2061
|
+
if (!ncxElement) return [];
|
|
2062
|
+
const ncxChildren = Epub.getXmlChildren(ncxElement);
|
|
2063
|
+
const navMap = Epub.findXmlChildByName("navMap", ncxChildren) ?? Epub.findXmlChildByName("navmap", ncxChildren);
|
|
2064
|
+
if (!navMap) return [];
|
|
2065
|
+
return this.parseNavPoints(Epub.getXmlChildren(navMap), ncxItem.href);
|
|
2066
|
+
}
|
|
2067
|
+
async parseNavPoints(nodes, ncxHref) {
|
|
2068
|
+
var _a;
|
|
2069
|
+
const entries = [];
|
|
2070
|
+
for (const node of nodes) {
|
|
2071
|
+
if (Epub.isXmlTextNode(node)) continue;
|
|
2072
|
+
const name = Epub.getXmlElementName(node);
|
|
2073
|
+
const isNavPoint = name === "navPoint" || name === "navpoint";
|
|
2074
|
+
if (!isNavPoint) continue;
|
|
2075
|
+
const children = Epub.getXmlChildren(node);
|
|
2076
|
+
const navLabel = Epub.findXmlChildByName("navLabel", children) ?? Epub.findXmlChildByName("navlabel", children);
|
|
2077
|
+
let title = null;
|
|
2078
|
+
if (navLabel) {
|
|
2079
|
+
const textEl = Epub.findXmlChildByName(
|
|
2080
|
+
"text",
|
|
2081
|
+
Epub.getXmlChildren(navLabel)
|
|
2082
|
+
);
|
|
2083
|
+
if (textEl) {
|
|
2084
|
+
title = Epub.getXhtmlTextContent(Epub.getXmlChildren(textEl)).trim() || null;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
const contentEl = Epub.findXmlChildByName("content", children);
|
|
2088
|
+
const src = (_a = contentEl == null ? void 0 : contentEl[":@"]) == null ? void 0 : _a["@_src"];
|
|
2089
|
+
const href = src ? await this.resolveHref(src, ncxHref) : null;
|
|
2090
|
+
const childEntries = await this.parseNavPoints(children, ncxHref);
|
|
2091
|
+
entries.push({
|
|
2092
|
+
title: title ?? `${entries.length}`,
|
|
2093
|
+
...href && { href },
|
|
2094
|
+
children: childEntries
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
return entries;
|
|
2098
|
+
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Retrieve the guide entries from the package document.
|
|
2101
|
+
*
|
|
2102
|
+
* The guide element is deprecated in EPUB 3 in favor of
|
|
2103
|
+
* the landmarks nav, but many publications still include it.
|
|
2104
|
+
*/
|
|
2105
|
+
async getGuideEntries() {
|
|
2106
|
+
const packageElement = await this.getPackageElement();
|
|
2107
|
+
const guide = Epub.findXmlChildByName(
|
|
2108
|
+
"guide",
|
|
2109
|
+
Epub.getXmlChildren(packageElement)
|
|
2110
|
+
);
|
|
2111
|
+
if (!guide) return [];
|
|
2112
|
+
return Epub.getXmlChildren(guide).filter(
|
|
2113
|
+
(node) => !Epub.isXmlTextNode(node) && "reference" in node
|
|
2114
|
+
).map((ref) => {
|
|
2115
|
+
var _a, _b, _c;
|
|
2116
|
+
return {
|
|
2117
|
+
href: ((_a = ref[":@"]) == null ? void 0 : _a["@_href"]) ?? "",
|
|
2118
|
+
title: ((_b = ref[":@"]) == null ? void 0 : _b["@_title"]) ?? "",
|
|
2119
|
+
type: (((_c = ref[":@"]) == null ? void 0 : _c["@_type"]) ?? "").toLowerCase()
|
|
2120
|
+
};
|
|
2121
|
+
}).filter((entry) => entry.href);
|
|
2122
|
+
}
|
|
1837
2123
|
discardAndClose() {
|
|
1838
2124
|
this.rootfile = null;
|
|
1839
2125
|
this.manifest = null;
|
|
@@ -1890,7 +2176,8 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1890
2176
|
if (entry.name === "mimetype" || entry.isDirectory()) continue;
|
|
1891
2177
|
zipfile.addFile(
|
|
1892
2178
|
join(entry.parentPath, entry.name),
|
|
1893
|
-
join(entry.parentPath, entry.name).replace(`${this.extractPath}/`, "")
|
|
2179
|
+
join(entry.parentPath, entry.name).replace(`${this.extractPath}/`, ""),
|
|
2180
|
+
{ compress: !isAudioFile(entry.name) }
|
|
1894
2181
|
);
|
|
1895
2182
|
}
|
|
1896
2183
|
zipfile.end();
|
|
@@ -1903,10 +2190,75 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1903
2190
|
_promise && await _promise;
|
|
1904
2191
|
}
|
|
1905
2192
|
}
|
|
2193
|
+
/**
|
|
2194
|
+
* Upgrade an EPUB 2 publication to EPUB 3 in place, returning a new, valid Epub 3 instance.
|
|
2195
|
+
*
|
|
2196
|
+
* Performs the following transformations:
|
|
2197
|
+
* - upgrades OPF metadata to EPUB 3 conventions
|
|
2198
|
+
* - scans XHTML documents and adds manifest item properties
|
|
2199
|
+
* - parses the NCX into a TOC tree and generates a nav.xhtml
|
|
2200
|
+
* - removes the NCX file and the guide element (configurable)
|
|
2201
|
+
* - fixes common font MIME types
|
|
2202
|
+
* - bumps the package version to 3.0
|
|
2203
|
+
* - goes over each xhtml item and rewrites it using XMLParser to make sure the output is valid XHTML
|
|
2204
|
+
*/
|
|
2205
|
+
static async upgrade(path, options = {}) {
|
|
2206
|
+
var _a;
|
|
2207
|
+
const { removeNcx = false, outputPath } = options;
|
|
2208
|
+
if (outputPath) {
|
|
2209
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
2210
|
+
await cp(path, outputPath, { force: true });
|
|
2211
|
+
}
|
|
2212
|
+
const epub = await Epub.open(outputPath ?? path);
|
|
2213
|
+
const version = await epub.getVersion();
|
|
2214
|
+
if (version.startsWith("3.")) {
|
|
2215
|
+
return epub;
|
|
2216
|
+
}
|
|
2217
|
+
const tocEntries = await epub.getNcxTableOfContents();
|
|
2218
|
+
let landmarks = [];
|
|
2219
|
+
await epub.withPackage((pkg) => {
|
|
2220
|
+
landmarks = Upgrade.extractGuideLandmarks(pkg);
|
|
2221
|
+
Upgrade.upgradePackageMetadata(pkg);
|
|
2222
|
+
Upgrade.fixFontMimeTypes(pkg);
|
|
2223
|
+
Upgrade.removeGuide(pkg);
|
|
2224
|
+
if (removeNcx) {
|
|
2225
|
+
Upgrade.removeSpineTocRef(pkg);
|
|
2226
|
+
}
|
|
2227
|
+
Upgrade.setPackageVersion(pkg, "3.0");
|
|
2228
|
+
});
|
|
2229
|
+
await Upgrade.collectManifestProperties(epub);
|
|
2230
|
+
if (removeNcx) {
|
|
2231
|
+
await Upgrade.removeNcx(epub);
|
|
2232
|
+
}
|
|
2233
|
+
const navHref = await Upgrade.chooseNavHref(epub);
|
|
2234
|
+
const navContent = await Upgrade.buildNavDocument(
|
|
2235
|
+
epub,
|
|
2236
|
+
tocEntries,
|
|
2237
|
+
landmarks
|
|
2238
|
+
);
|
|
2239
|
+
await epub.addManifestItem(
|
|
2240
|
+
{
|
|
2241
|
+
id: "nav",
|
|
2242
|
+
href: navHref,
|
|
2243
|
+
mediaType: "application/xhtml+xml",
|
|
2244
|
+
properties: ["nav"]
|
|
2245
|
+
},
|
|
2246
|
+
navContent,
|
|
2247
|
+
"utf-8"
|
|
2248
|
+
);
|
|
2249
|
+
const manifest = await epub.getManifest();
|
|
2250
|
+
for (const item of Object.values(manifest)) {
|
|
2251
|
+
if (((_a = item.mediaType) == null ? void 0 : _a.toLowerCase()) !== "application/xhtml+xml") continue;
|
|
2252
|
+
const contents = await epub.readXhtmlItemContents(item.id);
|
|
2253
|
+
await epub.writeXhtmlItemContents(item.id, contents);
|
|
2254
|
+
}
|
|
2255
|
+
return epub;
|
|
2256
|
+
}
|
|
1906
2257
|
[Symbol.dispose]() {
|
|
1907
2258
|
this.discardAndClose();
|
|
1908
2259
|
}
|
|
1909
2260
|
}
|
|
1910
2261
|
export {
|
|
1911
|
-
Epub
|
|
2262
|
+
Epub,
|
|
2263
|
+
EpubVersionError
|
|
1912
2264
|
};
|