@storyteller-platform/epub 0.4.10 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +752 -134
- package/dist/chunk-BIEQXUOY.js +50 -0
- package/dist/index.cjs +388 -21
- package/dist/index.d.cts +2 -743
- package/dist/index.d.ts +2 -743
- package/dist/index.js +397 -66
- 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 +5 -4
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";
|
|
@@ -55,7 +14,14 @@ import { lookup } from "mime-types";
|
|
|
55
14
|
import { nanoid } from "nanoid";
|
|
56
15
|
import { fromBuffer, open } from "yauzl-promise";
|
|
57
16
|
import { ZipFile } from "yazl";
|
|
58
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
dirname,
|
|
19
|
+
hrefToPlatformPath,
|
|
20
|
+
join,
|
|
21
|
+
resolve,
|
|
22
|
+
sep
|
|
23
|
+
} from "@storyteller-platform/path";
|
|
24
|
+
import * as Upgrade from "./upgrade.js";
|
|
59
25
|
const MP3_FILE_EXTENSIONS = [".mp3"];
|
|
60
26
|
const MPEG4_FILE_EXTENSIONS = [".mp4", ".m4a", ".m4b"];
|
|
61
27
|
const AAC_FILE_EXTENSIONS = [".aac"];
|
|
@@ -81,6 +47,8 @@ const AUDIO_FILE_EXTENSIONS = [
|
|
|
81
47
|
function isAudioFile(filenameOrExt) {
|
|
82
48
|
return AUDIO_FILE_EXTENSIONS.some((ext) => filenameOrExt.endsWith(ext));
|
|
83
49
|
}
|
|
50
|
+
class EpubVersionError extends Error {
|
|
51
|
+
}
|
|
84
52
|
class Epub {
|
|
85
53
|
constructor(extractPath, inputPath) {
|
|
86
54
|
this.extractPath = extractPath;
|
|
@@ -245,9 +213,29 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
245
213
|
* the provided name and optional filter.
|
|
246
214
|
*/
|
|
247
215
|
static findXmlChildByName(name, xml, filter) {
|
|
248
|
-
const element = xml.find(
|
|
216
|
+
const element = xml.find(
|
|
217
|
+
(e) => name in e && (filter ? filter(e) : true)
|
|
218
|
+
);
|
|
249
219
|
return element;
|
|
250
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Given an XML structure, find the first descendant matching
|
|
223
|
+
* the provided name and optional filter.
|
|
224
|
+
*
|
|
225
|
+
* Will perform a breadth first search for the element, returning
|
|
226
|
+
* the highest element in the tree matching the name and filter.
|
|
227
|
+
*/
|
|
228
|
+
static findXmlDescendantByName(name, xml, filter) {
|
|
229
|
+
const found = Epub.findXmlChildByName(name, xml, filter);
|
|
230
|
+
if (found) return found;
|
|
231
|
+
for (const node of xml) {
|
|
232
|
+
if (Epub.isXmlTextNode(node)) continue;
|
|
233
|
+
const children = Epub.getXmlChildren(node);
|
|
234
|
+
const found2 = this.findXmlDescendantByName(name, children, filter);
|
|
235
|
+
if (found2) return found2;
|
|
236
|
+
}
|
|
237
|
+
return void 0;
|
|
238
|
+
}
|
|
251
239
|
/**
|
|
252
240
|
* Given an XMLNode, determine whether it represents
|
|
253
241
|
* a text node or an XML element.
|
|
@@ -338,9 +326,21 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
338
326
|
* path to an EPUB file on disk, or a Uint8Array representing
|
|
339
327
|
* the data of the EPUB publication.
|
|
340
328
|
*/
|
|
341
|
-
// eslint-disable-next-line @typescript-eslint/require-await
|
|
342
329
|
static async from(pathOrData) {
|
|
343
|
-
|
|
330
|
+
const epub = await this.open(pathOrData);
|
|
331
|
+
const version = await epub.getVersion();
|
|
332
|
+
if (!version.startsWith("3.")) {
|
|
333
|
+
epub.discardAndClose();
|
|
334
|
+
throw new EpubVersionError(
|
|
335
|
+
"This is not a valid EPUB 3 publication. This library only supports EPUB 3, not EPUB 2. Use Epub.upgrade(path) to convert."
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
return epub;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Open an EPUB publication and return an Epub instance.
|
|
342
|
+
*/
|
|
343
|
+
static async open(pathOrData) {
|
|
344
344
|
const extractPath = join(
|
|
345
345
|
tmpdir(),
|
|
346
346
|
`storyteller-platform-epub-${randomUUID()}.epub`
|
|
@@ -354,7 +354,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
354
354
|
await zipfile.close();
|
|
355
355
|
});
|
|
356
356
|
for await (const entry of zipfile) {
|
|
357
|
-
if (entry.filename.endsWith(
|
|
357
|
+
if (entry.filename.endsWith(sep)) {
|
|
358
358
|
} else {
|
|
359
359
|
const writePath = join(extractPath, entry.filename);
|
|
360
360
|
const readStream = await entry.openReadStream();
|
|
@@ -383,13 +383,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
383
383
|
epub.discardAndClose();
|
|
384
384
|
console.error(e);
|
|
385
385
|
throw new Error(
|
|
386
|
-
"This is not a valid EPUB
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
const packageEl = await epub.getPackageElement();
|
|
390
|
-
if (!((_b = (_a = packageEl[":@"]) == null ? void 0 : _a["@_version"]) == null ? void 0 : _b.startsWith("3."))) {
|
|
391
|
-
throw new Error(
|
|
392
|
-
"This is not a valid EPUB 3 publication. This library only support EPUB 3, not EPUB 2. Try using an automatic conversion tool to convert this publication to EPUB 3."
|
|
386
|
+
"This is not a valid EPUB publication. Could not read the package document."
|
|
393
387
|
);
|
|
394
388
|
}
|
|
395
389
|
return epub;
|
|
@@ -409,7 +403,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
409
403
|
}
|
|
410
404
|
async removeEntry(href) {
|
|
411
405
|
const rootfile = await this.getRootfile();
|
|
412
|
-
const filename = this.
|
|
406
|
+
const filename = this.resolveInternalHref(rootfile, href);
|
|
413
407
|
await rm(filename);
|
|
414
408
|
}
|
|
415
409
|
async getFileData(path, encoding) {
|
|
@@ -782,6 +776,57 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
782
776
|
value: date.toISOString()
|
|
783
777
|
});
|
|
784
778
|
}
|
|
779
|
+
/**
|
|
780
|
+
* Retrieve the modified date from the dcterms:modified metadata
|
|
781
|
+
* in the EPUB metadata as a Date object.
|
|
782
|
+
*
|
|
783
|
+
* If there is no meta element with dcterms:modified, returns null.
|
|
784
|
+
*
|
|
785
|
+
* @link https://www.w3.org/TR/epub-33/#sec-metadata-last-modified
|
|
786
|
+
*/
|
|
787
|
+
async getModifiedDate() {
|
|
788
|
+
const metadata = await this.getMetadata();
|
|
789
|
+
const entry = metadata.find(
|
|
790
|
+
({ properties }) => properties["property"] === "dcterms:modified"
|
|
791
|
+
);
|
|
792
|
+
if (!(entry == null ? void 0 : entry.value)) return null;
|
|
793
|
+
return new Date(entry.value);
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Retrieve the layout from the rendition:layout meta element
|
|
797
|
+
* in the EPUB metadata.
|
|
798
|
+
*
|
|
799
|
+
* If there is no meta element, returns 'reflowable'.
|
|
800
|
+
*
|
|
801
|
+
* @link https://www.w3.org/TR/epub-33/#layout
|
|
802
|
+
*/
|
|
803
|
+
async getLayout() {
|
|
804
|
+
const metadata = await this.getMetadata();
|
|
805
|
+
const entry = metadata.find(
|
|
806
|
+
({ properties }) => properties["property"] === "rendition:layout"
|
|
807
|
+
);
|
|
808
|
+
if ((entry == null ? void 0 : entry.value) !== "reflowable" && (entry == null ? void 0 : entry.value) !== "pre-paginated") {
|
|
809
|
+
return "reflowable";
|
|
810
|
+
}
|
|
811
|
+
return entry.value;
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Retrieve the base direction from the package element.
|
|
815
|
+
*
|
|
816
|
+
* If there is no `dir` attribute on the package element,
|
|
817
|
+
* returns 'auto'.
|
|
818
|
+
*
|
|
819
|
+
* @link https://www.w3.org/TR/epub-33/#attrdef-dir
|
|
820
|
+
*/
|
|
821
|
+
async getBaseDirection() {
|
|
822
|
+
var _a;
|
|
823
|
+
const packageEl = await this.getPackageElement();
|
|
824
|
+
const dir = (_a = packageEl[":@"]) == null ? void 0 : _a["@_dir"];
|
|
825
|
+
if (dir !== "ltr" && dir !== "rtl" && dir !== "auto") {
|
|
826
|
+
return "auto";
|
|
827
|
+
}
|
|
828
|
+
return dir;
|
|
829
|
+
}
|
|
785
830
|
/**
|
|
786
831
|
* Set the dc:type metadata element.
|
|
787
832
|
*
|
|
@@ -1562,17 +1607,139 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1562
1607
|
});
|
|
1563
1608
|
this.spine = null;
|
|
1564
1609
|
}
|
|
1610
|
+
async getNavigationChildren(ol, navHref, { resolveToRoot } = {}) {
|
|
1611
|
+
var _a;
|
|
1612
|
+
const children = [];
|
|
1613
|
+
const childrenElements = Epub.getXmlChildren(ol).filter(
|
|
1614
|
+
(node) => !Epub.isXmlTextNode(node) && Epub.getXmlElementName(node) === "li"
|
|
1615
|
+
);
|
|
1616
|
+
for (const childEl of childrenElements) {
|
|
1617
|
+
const [firstChild, secondChild] = Epub.getXmlChildren(childEl).filter(
|
|
1618
|
+
(node) => !Epub.isXmlTextNode(node) && ["a", "span", "ol"].includes(Epub.getXmlElementName(node))
|
|
1619
|
+
);
|
|
1620
|
+
if (!firstChild) continue;
|
|
1621
|
+
if (!["a", "span"].includes(Epub.getXmlElementName(firstChild))) {
|
|
1622
|
+
continue;
|
|
1623
|
+
}
|
|
1624
|
+
if (Epub.getXmlElementName(firstChild) === "span" && (!secondChild || Epub.getXmlElementName(secondChild) !== "ol")) {
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
children.push({
|
|
1628
|
+
title: Epub.getXhtmlTextContent(Epub.getXmlChildren(firstChild)),
|
|
1629
|
+
...Epub.getXmlElementName(firstChild) === "a" && ((_a = firstChild[":@"]) == null ? void 0 : _a["@_href"]) && {
|
|
1630
|
+
href: await this.resolveHref(
|
|
1631
|
+
firstChild[":@"]["@_href"],
|
|
1632
|
+
void 0,
|
|
1633
|
+
{ toRoot: resolveToRoot }
|
|
1634
|
+
)
|
|
1635
|
+
},
|
|
1636
|
+
...secondChild && Epub.getXmlElementName(secondChild) === "ol" && {
|
|
1637
|
+
children: await this.getNavigationChildren(secondChild, navHref, {
|
|
1638
|
+
resolveToRoot
|
|
1639
|
+
})
|
|
1640
|
+
}
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
return children;
|
|
1644
|
+
}
|
|
1645
|
+
async getNavigation(role, { resolveToRoot } = {}) {
|
|
1646
|
+
const manifest = await this.getManifest();
|
|
1647
|
+
const navItem = Object.values(manifest).find(
|
|
1648
|
+
(item) => {
|
|
1649
|
+
var _a;
|
|
1650
|
+
return (_a = item.properties) == null ? void 0 : _a.includes("nav");
|
|
1651
|
+
}
|
|
1652
|
+
);
|
|
1653
|
+
if (!navItem) return null;
|
|
1654
|
+
const navContents = await this.readXhtmlItemContents(navItem.id);
|
|
1655
|
+
const navEl = Epub.findXmlDescendantByName(
|
|
1656
|
+
"nav",
|
|
1657
|
+
navContents,
|
|
1658
|
+
(node) => Epub.getXmlAttributes(node)["epub:type"] === role
|
|
1659
|
+
);
|
|
1660
|
+
if (!navEl) return null;
|
|
1661
|
+
const [firstChild, secondChild] = Epub.getXmlChildren(navEl).filter(
|
|
1662
|
+
(node) => !!(!Epub.isXmlTextNode(node) && Epub.getXmlElementName(node).match(/(?:h[1-6]|ol)/))
|
|
1663
|
+
);
|
|
1664
|
+
if (!firstChild) return null;
|
|
1665
|
+
const title = Epub.getXmlElementName(firstChild).match(/h[1-6]/) ? Epub.getXhtmlTextContent(Epub.getXmlChildren(firstChild)) : null;
|
|
1666
|
+
const list = Epub.getXmlElementName(firstChild) === "ol" ? firstChild : secondChild && Epub.getXmlElementName(secondChild) === "ol" ? secondChild : null;
|
|
1667
|
+
if (!list) return null;
|
|
1668
|
+
const children = await this.getNavigationChildren(list, navItem.href, {
|
|
1669
|
+
resolveToRoot
|
|
1670
|
+
});
|
|
1671
|
+
return {
|
|
1672
|
+
...title && { title },
|
|
1673
|
+
children
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Returns the structured table of contents navigation document
|
|
1678
|
+
* as a Navigation object.
|
|
1679
|
+
*
|
|
1680
|
+
* @link https://www.w3.org/TR/epub-33/#sec-nav-toc
|
|
1681
|
+
*/
|
|
1682
|
+
async getTableOfContents({
|
|
1683
|
+
resolveToRoot
|
|
1684
|
+
} = {}) {
|
|
1685
|
+
const navigationToc = await this.getNavigation("toc", { resolveToRoot });
|
|
1686
|
+
if (navigationToc) return navigationToc;
|
|
1687
|
+
const ncxToc = await this.getNcxTableOfContents();
|
|
1688
|
+
return {
|
|
1689
|
+
children: ncxToc
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
/**
|
|
1693
|
+
* Returns the structured landmarks navigation document
|
|
1694
|
+
* as a Navigation object
|
|
1695
|
+
*
|
|
1696
|
+
* @link https://www.w3.org/TR/epub-33/#sec-nav-landmarks
|
|
1697
|
+
*/
|
|
1698
|
+
async getLandmarks({
|
|
1699
|
+
resolveToRoot
|
|
1700
|
+
} = {}) {
|
|
1701
|
+
return this.getNavigation("landmarks", { resolveToRoot });
|
|
1702
|
+
}
|
|
1703
|
+
/**
|
|
1704
|
+
* Returns the structured page list navigation document
|
|
1705
|
+
* as a Navigation object
|
|
1706
|
+
*
|
|
1707
|
+
* @link https://www.w3.org/TR/epub-33/#sec-nav-landmarks
|
|
1708
|
+
*/
|
|
1709
|
+
async getPageList({
|
|
1710
|
+
resolveToRoot
|
|
1711
|
+
} = {}) {
|
|
1712
|
+
return this.getNavigation("page-list", { resolveToRoot });
|
|
1713
|
+
}
|
|
1565
1714
|
/**
|
|
1566
1715
|
* Returns a Zip Entry path for an HREF
|
|
1567
1716
|
*/
|
|
1568
|
-
|
|
1717
|
+
resolveInternalHref(from, href) {
|
|
1569
1718
|
const startPath = dirname(from);
|
|
1570
|
-
return resolve(
|
|
1719
|
+
return resolve(
|
|
1720
|
+
this.extractPath,
|
|
1721
|
+
hrefToPlatformPath(startPath),
|
|
1722
|
+
hrefToPlatformPath(href)
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
/**
|
|
1726
|
+
* Returns a path-relative-scheme-less URL, relative to the
|
|
1727
|
+
* container root.
|
|
1728
|
+
*
|
|
1729
|
+
* @param href The href to resolve
|
|
1730
|
+
* @param [relativeTo] Optional - The href to resolve this href relative to.
|
|
1731
|
+
Use if resolving a relative href from a file other than the package document.
|
|
1732
|
+
*/
|
|
1733
|
+
async resolveHref(href, relativeTo, { toRoot } = {}) {
|
|
1734
|
+
const rootfile = await this.getRootfile();
|
|
1735
|
+
const from = relativeTo ? this.resolveInternalHref(rootfile, relativeTo) : rootfile;
|
|
1736
|
+
const path = this.resolveInternalHref(from, href);
|
|
1737
|
+
return path.replace(toRoot ? this.extractPath : dirname(rootfile), "").slice(1);
|
|
1571
1738
|
}
|
|
1572
1739
|
async readFileContents(href, relativeTo, encoding) {
|
|
1573
1740
|
const rootfile = await this.getRootfile();
|
|
1574
|
-
const from = relativeTo ? this.
|
|
1575
|
-
const path = this.
|
|
1741
|
+
const from = relativeTo ? this.resolveInternalHref(rootfile, relativeTo) : rootfile;
|
|
1742
|
+
const path = this.resolveInternalHref(from, href);
|
|
1576
1743
|
const itemEntry = encoding ? await this.getFileData(path, encoding) : await this.getFileData(path);
|
|
1577
1744
|
return itemEntry;
|
|
1578
1745
|
}
|
|
@@ -1582,7 +1749,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1582
1749
|
const manifestItem = manifest[id];
|
|
1583
1750
|
if (!manifestItem)
|
|
1584
1751
|
throw new Error(`Could not find item with id "${id}" in manifest`);
|
|
1585
|
-
const path = this.
|
|
1752
|
+
const path = this.resolveInternalHref(rootfile, manifestItem.href);
|
|
1586
1753
|
const itemEntry = encoding ? await this.getFileData(path, encoding) : await this.getFileData(path);
|
|
1587
1754
|
return itemEntry;
|
|
1588
1755
|
}
|
|
@@ -1632,7 +1799,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1632
1799
|
if (!manifestItem)
|
|
1633
1800
|
throw new Error(`Could not find item with id "${id}" in manifest`);
|
|
1634
1801
|
memoize.clear(this.readXhtmlItemContents);
|
|
1635
|
-
const href = this.
|
|
1802
|
+
const href = this.resolveInternalHref(rootfile, manifestItem.href);
|
|
1636
1803
|
if (encoding === "utf-8") {
|
|
1637
1804
|
await this.writeEntryContents(href, contents, encoding);
|
|
1638
1805
|
} else {
|
|
@@ -1707,7 +1874,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1707
1874
|
});
|
|
1708
1875
|
this.manifest = null;
|
|
1709
1876
|
const rootfile = await this.getRootfile();
|
|
1710
|
-
const filename = this.
|
|
1877
|
+
const filename = this.resolveInternalHref(rootfile, item.href);
|
|
1711
1878
|
const data = encoding === "utf-8" || encoding === "xml" ? new TextEncoder().encode(
|
|
1712
1879
|
encoding === "utf-8" ? contents : await Epub.xmlBuilder.build(
|
|
1713
1880
|
contents
|
|
@@ -1864,6 +2031,105 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1864
2031
|
}
|
|
1865
2032
|
});
|
|
1866
2033
|
}
|
|
2034
|
+
/**
|
|
2035
|
+
* Returns the EPUB version declared on the package element.
|
|
2036
|
+
*/
|
|
2037
|
+
async getVersion() {
|
|
2038
|
+
var _a;
|
|
2039
|
+
const packageElement = await this.getPackageElement();
|
|
2040
|
+
return ((_a = packageElement[":@"]) == null ? void 0 : _a["@_version"]) ?? "2.0";
|
|
2041
|
+
}
|
|
2042
|
+
/**
|
|
2043
|
+
* Parse the NCX table of contents, if one exists, and return
|
|
2044
|
+
* a tree of TocEntry nodes.
|
|
2045
|
+
*
|
|
2046
|
+
* Useful for both EPUB 2 publications (where the NCX is the
|
|
2047
|
+
* primary navigation) and EPUB 3 publications that retain an
|
|
2048
|
+
* NCX for backwards compatibility.
|
|
2049
|
+
*/
|
|
2050
|
+
async getNcxTableOfContents() {
|
|
2051
|
+
var _a;
|
|
2052
|
+
const [manifest, packageElement] = await Promise.all([
|
|
2053
|
+
this.getManifest(),
|
|
2054
|
+
this.getPackageElement()
|
|
2055
|
+
]);
|
|
2056
|
+
const spine = Epub.findXmlChildByName(
|
|
2057
|
+
"spine",
|
|
2058
|
+
Epub.getXmlChildren(packageElement)
|
|
2059
|
+
);
|
|
2060
|
+
const spineTocId = (_a = spine == null ? void 0 : spine[":@"]) == null ? void 0 : _a["@_toc"];
|
|
2061
|
+
const ncxItem = spineTocId ? manifest[spineTocId] : Object.values(manifest).find(
|
|
2062
|
+
(item) => {
|
|
2063
|
+
var _a2;
|
|
2064
|
+
return ((_a2 = item.mediaType) == null ? void 0 : _a2.toLowerCase()) === "application/x-dtbncx+xml";
|
|
2065
|
+
}
|
|
2066
|
+
);
|
|
2067
|
+
if (!ncxItem) return [];
|
|
2068
|
+
const ncxContent = await this.readItemContents(ncxItem.id, "utf-8");
|
|
2069
|
+
const ncxXml = Epub.xmlParser.parse(ncxContent);
|
|
2070
|
+
const ncxElement = Epub.findXmlChildByName("ncx", ncxXml);
|
|
2071
|
+
if (!ncxElement) return [];
|
|
2072
|
+
const ncxChildren = Epub.getXmlChildren(ncxElement);
|
|
2073
|
+
const navMap = Epub.findXmlChildByName("navMap", ncxChildren) ?? Epub.findXmlChildByName("navmap", ncxChildren);
|
|
2074
|
+
if (!navMap) return [];
|
|
2075
|
+
return this.parseNavPoints(Epub.getXmlChildren(navMap), ncxItem.href);
|
|
2076
|
+
}
|
|
2077
|
+
async parseNavPoints(nodes, ncxHref) {
|
|
2078
|
+
var _a;
|
|
2079
|
+
const entries = [];
|
|
2080
|
+
for (const node of nodes) {
|
|
2081
|
+
if (Epub.isXmlTextNode(node)) continue;
|
|
2082
|
+
const name = Epub.getXmlElementName(node);
|
|
2083
|
+
const isNavPoint = name === "navPoint" || name === "navpoint";
|
|
2084
|
+
if (!isNavPoint) continue;
|
|
2085
|
+
const children = Epub.getXmlChildren(node);
|
|
2086
|
+
const navLabel = Epub.findXmlChildByName("navLabel", children) ?? Epub.findXmlChildByName("navlabel", children);
|
|
2087
|
+
let title = null;
|
|
2088
|
+
if (navLabel) {
|
|
2089
|
+
const textEl = Epub.findXmlChildByName(
|
|
2090
|
+
"text",
|
|
2091
|
+
Epub.getXmlChildren(navLabel)
|
|
2092
|
+
);
|
|
2093
|
+
if (textEl) {
|
|
2094
|
+
title = Epub.getXhtmlTextContent(Epub.getXmlChildren(textEl)).trim() || null;
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
const contentEl = Epub.findXmlChildByName("content", children);
|
|
2098
|
+
const src = (_a = contentEl == null ? void 0 : contentEl[":@"]) == null ? void 0 : _a["@_src"];
|
|
2099
|
+
const href = src ? await this.resolveHref(src, ncxHref) : null;
|
|
2100
|
+
const childEntries = await this.parseNavPoints(children, ncxHref);
|
|
2101
|
+
entries.push({
|
|
2102
|
+
title: title ?? `${entries.length}`,
|
|
2103
|
+
...href && { href },
|
|
2104
|
+
children: childEntries
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
return entries;
|
|
2108
|
+
}
|
|
2109
|
+
/**
|
|
2110
|
+
* Retrieve the guide entries from the package document.
|
|
2111
|
+
*
|
|
2112
|
+
* The guide element is deprecated in EPUB 3 in favor of
|
|
2113
|
+
* the landmarks nav, but many publications still include it.
|
|
2114
|
+
*/
|
|
2115
|
+
async getGuideEntries() {
|
|
2116
|
+
const packageElement = await this.getPackageElement();
|
|
2117
|
+
const guide = Epub.findXmlChildByName(
|
|
2118
|
+
"guide",
|
|
2119
|
+
Epub.getXmlChildren(packageElement)
|
|
2120
|
+
);
|
|
2121
|
+
if (!guide) return [];
|
|
2122
|
+
return Epub.getXmlChildren(guide).filter(
|
|
2123
|
+
(node) => !Epub.isXmlTextNode(node) && "reference" in node
|
|
2124
|
+
).map((ref) => {
|
|
2125
|
+
var _a, _b, _c;
|
|
2126
|
+
return {
|
|
2127
|
+
href: ((_a = ref[":@"]) == null ? void 0 : _a["@_href"]) ?? "",
|
|
2128
|
+
title: ((_b = ref[":@"]) == null ? void 0 : _b["@_title"]) ?? "",
|
|
2129
|
+
type: (((_c = ref[":@"]) == null ? void 0 : _c["@_type"]) ?? "").toLowerCase()
|
|
2130
|
+
};
|
|
2131
|
+
}).filter((entry) => entry.href);
|
|
2132
|
+
}
|
|
1867
2133
|
discardAndClose() {
|
|
1868
2134
|
this.rootfile = null;
|
|
1869
2135
|
this.manifest = null;
|
|
@@ -1934,10 +2200,75 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1934
2200
|
_promise && await _promise;
|
|
1935
2201
|
}
|
|
1936
2202
|
}
|
|
2203
|
+
/**
|
|
2204
|
+
* Upgrade an EPUB 2 publication to EPUB 3 in place, returning a new, valid Epub 3 instance.
|
|
2205
|
+
*
|
|
2206
|
+
* Performs the following transformations:
|
|
2207
|
+
* - upgrades OPF metadata to EPUB 3 conventions
|
|
2208
|
+
* - scans XHTML documents and adds manifest item properties
|
|
2209
|
+
* - parses the NCX into a TOC tree and generates a nav.xhtml
|
|
2210
|
+
* - removes the NCX file and the guide element (configurable)
|
|
2211
|
+
* - fixes common font MIME types
|
|
2212
|
+
* - bumps the package version to 3.0
|
|
2213
|
+
* - goes over each xhtml item and rewrites it using XMLParser to make sure the output is valid XHTML
|
|
2214
|
+
*/
|
|
2215
|
+
static async upgrade(path, options = {}) {
|
|
2216
|
+
var _a;
|
|
2217
|
+
const { removeNcx = false, outputPath } = options;
|
|
2218
|
+
if (outputPath) {
|
|
2219
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
2220
|
+
await cp(path, outputPath, { force: true });
|
|
2221
|
+
}
|
|
2222
|
+
const epub = await Epub.open(outputPath ?? path);
|
|
2223
|
+
const version = await epub.getVersion();
|
|
2224
|
+
if (version.startsWith("3.")) {
|
|
2225
|
+
return epub;
|
|
2226
|
+
}
|
|
2227
|
+
const tocEntries = await epub.getNcxTableOfContents();
|
|
2228
|
+
let landmarks = [];
|
|
2229
|
+
await epub.withPackage((pkg) => {
|
|
2230
|
+
landmarks = Upgrade.extractGuideLandmarks(pkg);
|
|
2231
|
+
Upgrade.upgradePackageMetadata(pkg);
|
|
2232
|
+
Upgrade.fixFontMimeTypes(pkg);
|
|
2233
|
+
Upgrade.removeGuide(pkg);
|
|
2234
|
+
if (removeNcx) {
|
|
2235
|
+
Upgrade.removeSpineTocRef(pkg);
|
|
2236
|
+
}
|
|
2237
|
+
Upgrade.setPackageVersion(pkg, "3.0");
|
|
2238
|
+
});
|
|
2239
|
+
await Upgrade.collectManifestProperties(epub);
|
|
2240
|
+
if (removeNcx) {
|
|
2241
|
+
await Upgrade.removeNcx(epub);
|
|
2242
|
+
}
|
|
2243
|
+
const navHref = await Upgrade.chooseNavHref(epub);
|
|
2244
|
+
const navContent = await Upgrade.buildNavDocument(
|
|
2245
|
+
epub,
|
|
2246
|
+
tocEntries,
|
|
2247
|
+
landmarks
|
|
2248
|
+
);
|
|
2249
|
+
await epub.addManifestItem(
|
|
2250
|
+
{
|
|
2251
|
+
id: "nav",
|
|
2252
|
+
href: navHref,
|
|
2253
|
+
mediaType: "application/xhtml+xml",
|
|
2254
|
+
properties: ["nav"]
|
|
2255
|
+
},
|
|
2256
|
+
navContent,
|
|
2257
|
+
"utf-8"
|
|
2258
|
+
);
|
|
2259
|
+
const manifest = await epub.getManifest();
|
|
2260
|
+
for (const item of Object.values(manifest)) {
|
|
2261
|
+
if (((_a = item.mediaType) == null ? void 0 : _a.toLowerCase()) !== "application/xhtml+xml") continue;
|
|
2262
|
+
const contents = await epub.readXhtmlItemContents(item.id);
|
|
2263
|
+
await epub.writeXhtmlItemContents(item.id, contents);
|
|
2264
|
+
}
|
|
2265
|
+
return epub;
|
|
2266
|
+
}
|
|
1937
2267
|
[Symbol.dispose]() {
|
|
1938
2268
|
this.discardAndClose();
|
|
1939
2269
|
}
|
|
1940
2270
|
}
|
|
1941
2271
|
export {
|
|
1942
|
-
Epub
|
|
2272
|
+
Epub,
|
|
2273
|
+
EpubVersionError
|
|
1943
2274
|
};
|