@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
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
|
|
2
|
+
var __typeError = (msg) => {
|
|
3
|
+
throw TypeError(msg);
|
|
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
|
+
};
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
__using,
|
|
49
|
+
__callDispose
|
|
50
|
+
};
|
package/dist/index.cjs
CHANGED
|
@@ -73,7 +73,8 @@ var __callDispose = (stack, error, hasError) => {
|
|
|
73
73
|
};
|
|
74
74
|
var index_exports = {};
|
|
75
75
|
__export(index_exports, {
|
|
76
|
-
Epub: () => Epub
|
|
76
|
+
Epub: () => Epub,
|
|
77
|
+
EpubVersionError: () => EpubVersionError
|
|
77
78
|
});
|
|
78
79
|
module.exports = __toCommonJS(index_exports);
|
|
79
80
|
var import_node_crypto = require("node:crypto");
|
|
@@ -89,6 +90,34 @@ var import_nanoid = require("nanoid");
|
|
|
89
90
|
var import_yauzl_promise = require("yauzl-promise");
|
|
90
91
|
var import_yazl = require("yazl");
|
|
91
92
|
var import_path = require("@storyteller-platform/path");
|
|
93
|
+
var Upgrade = __toESM(require("./upgrade.ts"), 1);
|
|
94
|
+
const MP3_FILE_EXTENSIONS = [".mp3"];
|
|
95
|
+
const MPEG4_FILE_EXTENSIONS = [".mp4", ".m4a", ".m4b"];
|
|
96
|
+
const AAC_FILE_EXTENSIONS = [".aac"];
|
|
97
|
+
const OGG_FILE_EXTENSIONS = [".ogg", ".oga", ".mogg"];
|
|
98
|
+
const OPUS_FILE_EXTENSIONS = [".opus"];
|
|
99
|
+
const WAVE_FILE_EXTENSIONS = [".wav"];
|
|
100
|
+
const AIFF_FILE_EXTENSIONS = [".aiff"];
|
|
101
|
+
const FLAC_FILE_EXTENSIONS = [".flac"];
|
|
102
|
+
const ALAC_FILE_EXTENSIONS = [".alac"];
|
|
103
|
+
const WEBM_FILE_EXTENSIONS = [".weba"];
|
|
104
|
+
const AUDIO_FILE_EXTENSIONS = [
|
|
105
|
+
...MP3_FILE_EXTENSIONS,
|
|
106
|
+
...AAC_FILE_EXTENSIONS,
|
|
107
|
+
...MPEG4_FILE_EXTENSIONS,
|
|
108
|
+
...OPUS_FILE_EXTENSIONS,
|
|
109
|
+
...OGG_FILE_EXTENSIONS,
|
|
110
|
+
...WAVE_FILE_EXTENSIONS,
|
|
111
|
+
...AIFF_FILE_EXTENSIONS,
|
|
112
|
+
...FLAC_FILE_EXTENSIONS,
|
|
113
|
+
...ALAC_FILE_EXTENSIONS,
|
|
114
|
+
...WEBM_FILE_EXTENSIONS
|
|
115
|
+
];
|
|
116
|
+
function isAudioFile(filenameOrExt) {
|
|
117
|
+
return AUDIO_FILE_EXTENSIONS.some((ext) => filenameOrExt.endsWith(ext));
|
|
118
|
+
}
|
|
119
|
+
class EpubVersionError extends Error {
|
|
120
|
+
}
|
|
92
121
|
class Epub {
|
|
93
122
|
constructor(extractPath, inputPath) {
|
|
94
123
|
this.extractPath = extractPath;
|
|
@@ -253,9 +282,29 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
253
282
|
* the provided name and optional filter.
|
|
254
283
|
*/
|
|
255
284
|
static findXmlChildByName(name, xml, filter) {
|
|
256
|
-
const element = xml.find(
|
|
285
|
+
const element = xml.find(
|
|
286
|
+
(e) => name in e && (filter ? filter(e) : true)
|
|
287
|
+
);
|
|
257
288
|
return element;
|
|
258
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Given an XML structure, find the first descendant matching
|
|
292
|
+
* the provided name and optional filter.
|
|
293
|
+
*
|
|
294
|
+
* Will perform a breadth first search for the element, returning
|
|
295
|
+
* the highest element in the tree matching the name and filter.
|
|
296
|
+
*/
|
|
297
|
+
static findXmlDescendantByName(name, xml, filter) {
|
|
298
|
+
const found = Epub.findXmlChildByName(name, xml, filter);
|
|
299
|
+
if (found) return found;
|
|
300
|
+
for (const node of xml) {
|
|
301
|
+
if (Epub.isXmlTextNode(node)) continue;
|
|
302
|
+
const children = Epub.getXmlChildren(node);
|
|
303
|
+
const found2 = this.findXmlDescendantByName(name, children, filter);
|
|
304
|
+
if (found2) return found2;
|
|
305
|
+
}
|
|
306
|
+
return void 0;
|
|
307
|
+
}
|
|
259
308
|
/**
|
|
260
309
|
* Given an XMLNode, determine whether it represents
|
|
261
310
|
* a text node or an XML element.
|
|
@@ -346,8 +395,21 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
346
395
|
* path to an EPUB file on disk, or a Uint8Array representing
|
|
347
396
|
* the data of the EPUB publication.
|
|
348
397
|
*/
|
|
349
|
-
// eslint-disable-next-line @typescript-eslint/require-await
|
|
350
398
|
static async from(pathOrData) {
|
|
399
|
+
const epub = await this.open(pathOrData);
|
|
400
|
+
const version = await epub.getVersion();
|
|
401
|
+
if (!version.startsWith("3.")) {
|
|
402
|
+
epub.discardAndClose();
|
|
403
|
+
throw new EpubVersionError(
|
|
404
|
+
"This is not a valid EPUB 3 publication. This library only supports EPUB 3, not EPUB 2. Use Epub.upgrade(path) to convert."
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
return epub;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Open an EPUB publication and return an Epub instance.
|
|
411
|
+
*/
|
|
412
|
+
static async open(pathOrData) {
|
|
351
413
|
const extractPath = (0, import_path.join)(
|
|
352
414
|
(0, import_node_os.tmpdir)(),
|
|
353
415
|
`storyteller-platform-epub-${(0, import_node_crypto.randomUUID)()}.epub`
|
|
@@ -380,10 +442,20 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
380
442
|
(0, import_node_fs.rmSync)(extractPath, { force: true, recursive: true });
|
|
381
443
|
throw error;
|
|
382
444
|
}
|
|
383
|
-
|
|
445
|
+
const epub = new this(
|
|
384
446
|
extractPath,
|
|
385
447
|
typeof pathOrData === "string" ? pathOrData : void 0
|
|
386
448
|
);
|
|
449
|
+
try {
|
|
450
|
+
await epub.getPackageElement();
|
|
451
|
+
} catch (e) {
|
|
452
|
+
epub.discardAndClose();
|
|
453
|
+
console.error(e);
|
|
454
|
+
throw new Error(
|
|
455
|
+
"This is not a valid EPUB publication. Could not read the package document."
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
return epub;
|
|
387
459
|
}
|
|
388
460
|
async copy(path) {
|
|
389
461
|
const extractPath = (0, import_path.join)(
|
|
@@ -400,7 +472,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
400
472
|
}
|
|
401
473
|
async removeEntry(href) {
|
|
402
474
|
const rootfile = await this.getRootfile();
|
|
403
|
-
const filename = this.
|
|
475
|
+
const filename = this.resolveInternalHref(rootfile, href);
|
|
404
476
|
await (0, import_promises.rm)(filename);
|
|
405
477
|
}
|
|
406
478
|
async getFileData(path, encoding) {
|
|
@@ -445,17 +517,6 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
445
517
|
this.rootfile = (0, import_path.resolve)(this.extractPath, fullPath);
|
|
446
518
|
return this.rootfile;
|
|
447
519
|
}
|
|
448
|
-
migratePackageDocument(packageDocument) {
|
|
449
|
-
for (const element of packageDocument) {
|
|
450
|
-
if (Epub.isXmlTextNode(element)) continue;
|
|
451
|
-
const elementName = Epub.getXmlElementName(element);
|
|
452
|
-
if (elementName.startsWith("opf:")) {
|
|
453
|
-
element[elementName.replace("opf:", "")] = Epub.getXmlChildren(element);
|
|
454
|
-
delete element[elementName];
|
|
455
|
-
this.migratePackageDocument(Epub.getXmlChildren(element));
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
520
|
async getPackageDocument() {
|
|
460
521
|
const rootfile = await this.getRootfile();
|
|
461
522
|
const packageDocumentString = await this.getFileData(rootfile, "utf-8");
|
|
@@ -470,13 +531,12 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
470
531
|
}
|
|
471
532
|
async getPackageElement() {
|
|
472
533
|
const packageDocument = await this.getPackageDocument();
|
|
473
|
-
const packageElement = Epub.findXmlChildByName("package", packageDocument)
|
|
534
|
+
const packageElement = Epub.findXmlChildByName("package", packageDocument);
|
|
474
535
|
if (!packageElement) {
|
|
475
536
|
throw new Error(
|
|
476
537
|
"Failed to parse EPUB: Found no package element in package document"
|
|
477
538
|
);
|
|
478
539
|
}
|
|
479
|
-
this.migratePackageDocument(packageDocument);
|
|
480
540
|
return packageElement;
|
|
481
541
|
}
|
|
482
542
|
/**
|
|
@@ -494,7 +554,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
494
554
|
async withPackage(producer) {
|
|
495
555
|
await this.packageMutex.runExclusive(async () => {
|
|
496
556
|
const packageDocument = await this.getPackageDocument();
|
|
497
|
-
const packageElement = Epub.findXmlChildByName("package", packageDocument)
|
|
557
|
+
const packageElement = Epub.findXmlChildByName("package", packageDocument);
|
|
498
558
|
if (!packageElement) {
|
|
499
559
|
throw new Error(
|
|
500
560
|
"Failed to parse EPUB: Found no package element in package document"
|
|
@@ -785,6 +845,57 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
785
845
|
value: date.toISOString()
|
|
786
846
|
});
|
|
787
847
|
}
|
|
848
|
+
/**
|
|
849
|
+
* Retrieve the modified date from the dcterms:modified metadata
|
|
850
|
+
* in the EPUB metadata as a Date object.
|
|
851
|
+
*
|
|
852
|
+
* If there is no meta element with dcterms:modified, returns null.
|
|
853
|
+
*
|
|
854
|
+
* @link https://www.w3.org/TR/epub-33/#sec-metadata-last-modified
|
|
855
|
+
*/
|
|
856
|
+
async getModifiedDate() {
|
|
857
|
+
const metadata = await this.getMetadata();
|
|
858
|
+
const entry = metadata.find(
|
|
859
|
+
({ properties }) => properties["property"] === "dcterms:modified"
|
|
860
|
+
);
|
|
861
|
+
if (!(entry == null ? void 0 : entry.value)) return null;
|
|
862
|
+
return new Date(entry.value);
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Retrieve the layout from the rendition:layout meta element
|
|
866
|
+
* in the EPUB metadata.
|
|
867
|
+
*
|
|
868
|
+
* If there is no meta element, returns 'reflowable'.
|
|
869
|
+
*
|
|
870
|
+
* @link https://www.w3.org/TR/epub-33/#layout
|
|
871
|
+
*/
|
|
872
|
+
async getLayout() {
|
|
873
|
+
const metadata = await this.getMetadata();
|
|
874
|
+
const entry = metadata.find(
|
|
875
|
+
({ properties }) => properties["property"] === "rendition:layout"
|
|
876
|
+
);
|
|
877
|
+
if ((entry == null ? void 0 : entry.value) !== "reflowable" && (entry == null ? void 0 : entry.value) !== "pre-paginated") {
|
|
878
|
+
return "reflowable";
|
|
879
|
+
}
|
|
880
|
+
return entry.value;
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Retrieve the base direction from the package element.
|
|
884
|
+
*
|
|
885
|
+
* If there is no `dir` attribute on the package element,
|
|
886
|
+
* returns 'auto'.
|
|
887
|
+
*
|
|
888
|
+
* @link https://www.w3.org/TR/epub-33/#attrdef-dir
|
|
889
|
+
*/
|
|
890
|
+
async getBaseDirection() {
|
|
891
|
+
var _a;
|
|
892
|
+
const packageEl = await this.getPackageElement();
|
|
893
|
+
const dir = (_a = packageEl[":@"]) == null ? void 0 : _a["@_dir"];
|
|
894
|
+
if (dir !== "ltr" && dir !== "rtl" && dir !== "auto") {
|
|
895
|
+
return "auto";
|
|
896
|
+
}
|
|
897
|
+
return dir;
|
|
898
|
+
}
|
|
788
899
|
/**
|
|
789
900
|
* Set the dc:type metadata element.
|
|
790
901
|
*
|
|
@@ -1565,17 +1676,135 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1565
1676
|
});
|
|
1566
1677
|
this.spine = null;
|
|
1567
1678
|
}
|
|
1679
|
+
async getNavigationChildren(ol, navHref, { resolveToRoot } = {}) {
|
|
1680
|
+
var _a;
|
|
1681
|
+
const children = [];
|
|
1682
|
+
const childrenElements = Epub.getXmlChildren(ol).filter(
|
|
1683
|
+
(node) => !Epub.isXmlTextNode(node) && Epub.getXmlElementName(node) === "li"
|
|
1684
|
+
);
|
|
1685
|
+
for (const childEl of childrenElements) {
|
|
1686
|
+
const [firstChild, secondChild] = Epub.getXmlChildren(childEl).filter(
|
|
1687
|
+
(node) => !Epub.isXmlTextNode(node) && ["a", "span", "ol"].includes(Epub.getXmlElementName(node))
|
|
1688
|
+
);
|
|
1689
|
+
if (!firstChild) continue;
|
|
1690
|
+
if (!["a", "span"].includes(Epub.getXmlElementName(firstChild))) {
|
|
1691
|
+
continue;
|
|
1692
|
+
}
|
|
1693
|
+
if (Epub.getXmlElementName(firstChild) === "span" && (!secondChild || Epub.getXmlElementName(secondChild) !== "ol")) {
|
|
1694
|
+
continue;
|
|
1695
|
+
}
|
|
1696
|
+
children.push({
|
|
1697
|
+
title: Epub.getXhtmlTextContent(Epub.getXmlChildren(firstChild)),
|
|
1698
|
+
...Epub.getXmlElementName(firstChild) === "a" && ((_a = firstChild[":@"]) == null ? void 0 : _a["@_href"]) && {
|
|
1699
|
+
href: await this.resolveHref(
|
|
1700
|
+
firstChild[":@"]["@_href"],
|
|
1701
|
+
void 0,
|
|
1702
|
+
{ toRoot: resolveToRoot }
|
|
1703
|
+
)
|
|
1704
|
+
},
|
|
1705
|
+
...secondChild && Epub.getXmlElementName(secondChild) === "ol" && {
|
|
1706
|
+
children: await this.getNavigationChildren(secondChild, navHref, {
|
|
1707
|
+
resolveToRoot
|
|
1708
|
+
})
|
|
1709
|
+
}
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
return children;
|
|
1713
|
+
}
|
|
1714
|
+
async getNavigation(role, { resolveToRoot } = {}) {
|
|
1715
|
+
const manifest = await this.getManifest();
|
|
1716
|
+
const navItem = Object.values(manifest).find(
|
|
1717
|
+
(item) => {
|
|
1718
|
+
var _a;
|
|
1719
|
+
return (_a = item.properties) == null ? void 0 : _a.includes("nav");
|
|
1720
|
+
}
|
|
1721
|
+
);
|
|
1722
|
+
if (!navItem) return null;
|
|
1723
|
+
const navContents = await this.readXhtmlItemContents(navItem.id);
|
|
1724
|
+
const navEl = Epub.findXmlDescendantByName(
|
|
1725
|
+
"nav",
|
|
1726
|
+
navContents,
|
|
1727
|
+
(node) => Epub.getXmlAttributes(node)["epub:type"] === role
|
|
1728
|
+
);
|
|
1729
|
+
if (!navEl) return null;
|
|
1730
|
+
const [firstChild, secondChild] = Epub.getXmlChildren(navEl).filter(
|
|
1731
|
+
(node) => !!(!Epub.isXmlTextNode(node) && Epub.getXmlElementName(node).match(/(?:h[1-6]|ol)/))
|
|
1732
|
+
);
|
|
1733
|
+
if (!firstChild) return null;
|
|
1734
|
+
const title = Epub.getXmlElementName(firstChild).match(/h[1-6]/) ? Epub.getXhtmlTextContent(Epub.getXmlChildren(firstChild)) : null;
|
|
1735
|
+
const list = Epub.getXmlElementName(firstChild) === "ol" ? firstChild : secondChild && Epub.getXmlElementName(secondChild) === "ol" ? secondChild : null;
|
|
1736
|
+
if (!list) return null;
|
|
1737
|
+
const children = await this.getNavigationChildren(list, navItem.href, {
|
|
1738
|
+
resolveToRoot
|
|
1739
|
+
});
|
|
1740
|
+
return {
|
|
1741
|
+
...title && { title },
|
|
1742
|
+
children
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Returns the structured table of contents navigation document
|
|
1747
|
+
* as a Navigation object.
|
|
1748
|
+
*
|
|
1749
|
+
* @link https://www.w3.org/TR/epub-33/#sec-nav-toc
|
|
1750
|
+
*/
|
|
1751
|
+
async getTableOfContents({
|
|
1752
|
+
resolveToRoot
|
|
1753
|
+
} = {}) {
|
|
1754
|
+
const navigationToc = await this.getNavigation("toc", { resolveToRoot });
|
|
1755
|
+
if (navigationToc) return navigationToc;
|
|
1756
|
+
const ncxToc = await this.getNcxTableOfContents();
|
|
1757
|
+
return {
|
|
1758
|
+
children: ncxToc
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
/**
|
|
1762
|
+
* Returns the structured landmarks navigation document
|
|
1763
|
+
* as a Navigation object
|
|
1764
|
+
*
|
|
1765
|
+
* @link https://www.w3.org/TR/epub-33/#sec-nav-landmarks
|
|
1766
|
+
*/
|
|
1767
|
+
async getLandmarks({
|
|
1768
|
+
resolveToRoot
|
|
1769
|
+
} = {}) {
|
|
1770
|
+
return this.getNavigation("landmarks", { resolveToRoot });
|
|
1771
|
+
}
|
|
1772
|
+
/**
|
|
1773
|
+
* Returns the structured page list navigation document
|
|
1774
|
+
* as a Navigation object
|
|
1775
|
+
*
|
|
1776
|
+
* @link https://www.w3.org/TR/epub-33/#sec-nav-landmarks
|
|
1777
|
+
*/
|
|
1778
|
+
async getPageList({
|
|
1779
|
+
resolveToRoot
|
|
1780
|
+
} = {}) {
|
|
1781
|
+
return this.getNavigation("page-list", { resolveToRoot });
|
|
1782
|
+
}
|
|
1568
1783
|
/**
|
|
1569
1784
|
* Returns a Zip Entry path for an HREF
|
|
1570
1785
|
*/
|
|
1571
|
-
|
|
1786
|
+
resolveInternalHref(from, href) {
|
|
1572
1787
|
const startPath = (0, import_path.dirname)(from);
|
|
1573
1788
|
return (0, import_path.resolve)(this.extractPath, startPath, href);
|
|
1574
1789
|
}
|
|
1790
|
+
/**
|
|
1791
|
+
* Returns a path-relative-scheme-less URL, relative to the
|
|
1792
|
+
* container root.
|
|
1793
|
+
*
|
|
1794
|
+
* @param href The href to resolve
|
|
1795
|
+
* @param [relativeTo] Optional - The href to resolve this href relative to.
|
|
1796
|
+
Use if resolving a relative href from a file other than the package document.
|
|
1797
|
+
*/
|
|
1798
|
+
async resolveHref(href, relativeTo, { toRoot } = {}) {
|
|
1799
|
+
const rootfile = await this.getRootfile();
|
|
1800
|
+
const from = relativeTo ? this.resolveInternalHref(rootfile, relativeTo) : rootfile;
|
|
1801
|
+
const path = this.resolveInternalHref(from, href);
|
|
1802
|
+
return path.replace(toRoot ? this.extractPath : (0, import_path.dirname)(rootfile), "").slice(1);
|
|
1803
|
+
}
|
|
1575
1804
|
async readFileContents(href, relativeTo, encoding) {
|
|
1576
1805
|
const rootfile = await this.getRootfile();
|
|
1577
|
-
const from = relativeTo ? this.
|
|
1578
|
-
const path = this.
|
|
1806
|
+
const from = relativeTo ? this.resolveInternalHref(rootfile, relativeTo) : rootfile;
|
|
1807
|
+
const path = this.resolveInternalHref(from, href);
|
|
1579
1808
|
const itemEntry = encoding ? await this.getFileData(path, encoding) : await this.getFileData(path);
|
|
1580
1809
|
return itemEntry;
|
|
1581
1810
|
}
|
|
@@ -1585,7 +1814,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1585
1814
|
const manifestItem = manifest[id];
|
|
1586
1815
|
if (!manifestItem)
|
|
1587
1816
|
throw new Error(`Could not find item with id "${id}" in manifest`);
|
|
1588
|
-
const path = this.
|
|
1817
|
+
const path = this.resolveInternalHref(rootfile, manifestItem.href);
|
|
1589
1818
|
const itemEntry = encoding ? await this.getFileData(path, encoding) : await this.getFileData(path);
|
|
1590
1819
|
return itemEntry;
|
|
1591
1820
|
}
|
|
@@ -1635,7 +1864,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1635
1864
|
if (!manifestItem)
|
|
1636
1865
|
throw new Error(`Could not find item with id "${id}" in manifest`);
|
|
1637
1866
|
import_mem.default.clear(this.readXhtmlItemContents);
|
|
1638
|
-
const href = this.
|
|
1867
|
+
const href = this.resolveInternalHref(rootfile, manifestItem.href);
|
|
1639
1868
|
if (encoding === "utf-8") {
|
|
1640
1869
|
await this.writeEntryContents(href, contents, encoding);
|
|
1641
1870
|
} else {
|
|
@@ -1710,7 +1939,7 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1710
1939
|
});
|
|
1711
1940
|
this.manifest = null;
|
|
1712
1941
|
const rootfile = await this.getRootfile();
|
|
1713
|
-
const filename = this.
|
|
1942
|
+
const filename = this.resolveInternalHref(rootfile, item.href);
|
|
1714
1943
|
const data = encoding === "utf-8" || encoding === "xml" ? new TextEncoder().encode(
|
|
1715
1944
|
encoding === "utf-8" ? contents : await Epub.xmlBuilder.build(
|
|
1716
1945
|
contents
|
|
@@ -1867,6 +2096,105 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1867
2096
|
}
|
|
1868
2097
|
});
|
|
1869
2098
|
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Returns the EPUB version declared on the package element.
|
|
2101
|
+
*/
|
|
2102
|
+
async getVersion() {
|
|
2103
|
+
var _a;
|
|
2104
|
+
const packageElement = await this.getPackageElement();
|
|
2105
|
+
return ((_a = packageElement[":@"]) == null ? void 0 : _a["@_version"]) ?? "2.0";
|
|
2106
|
+
}
|
|
2107
|
+
/**
|
|
2108
|
+
* Parse the NCX table of contents, if one exists, and return
|
|
2109
|
+
* a tree of TocEntry nodes.
|
|
2110
|
+
*
|
|
2111
|
+
* Useful for both EPUB 2 publications (where the NCX is the
|
|
2112
|
+
* primary navigation) and EPUB 3 publications that retain an
|
|
2113
|
+
* NCX for backwards compatibility.
|
|
2114
|
+
*/
|
|
2115
|
+
async getNcxTableOfContents() {
|
|
2116
|
+
var _a;
|
|
2117
|
+
const [manifest, packageElement] = await Promise.all([
|
|
2118
|
+
this.getManifest(),
|
|
2119
|
+
this.getPackageElement()
|
|
2120
|
+
]);
|
|
2121
|
+
const spine = Epub.findXmlChildByName(
|
|
2122
|
+
"spine",
|
|
2123
|
+
Epub.getXmlChildren(packageElement)
|
|
2124
|
+
);
|
|
2125
|
+
const spineTocId = (_a = spine == null ? void 0 : spine[":@"]) == null ? void 0 : _a["@_toc"];
|
|
2126
|
+
const ncxItem = spineTocId ? manifest[spineTocId] : Object.values(manifest).find(
|
|
2127
|
+
(item) => {
|
|
2128
|
+
var _a2;
|
|
2129
|
+
return ((_a2 = item.mediaType) == null ? void 0 : _a2.toLowerCase()) === "application/x-dtbncx+xml";
|
|
2130
|
+
}
|
|
2131
|
+
);
|
|
2132
|
+
if (!ncxItem) return [];
|
|
2133
|
+
const ncxContent = await this.readItemContents(ncxItem.id, "utf-8");
|
|
2134
|
+
const ncxXml = Epub.xmlParser.parse(ncxContent);
|
|
2135
|
+
const ncxElement = Epub.findXmlChildByName("ncx", ncxXml);
|
|
2136
|
+
if (!ncxElement) return [];
|
|
2137
|
+
const ncxChildren = Epub.getXmlChildren(ncxElement);
|
|
2138
|
+
const navMap = Epub.findXmlChildByName("navMap", ncxChildren) ?? Epub.findXmlChildByName("navmap", ncxChildren);
|
|
2139
|
+
if (!navMap) return [];
|
|
2140
|
+
return this.parseNavPoints(Epub.getXmlChildren(navMap), ncxItem.href);
|
|
2141
|
+
}
|
|
2142
|
+
async parseNavPoints(nodes, ncxHref) {
|
|
2143
|
+
var _a;
|
|
2144
|
+
const entries = [];
|
|
2145
|
+
for (const node of nodes) {
|
|
2146
|
+
if (Epub.isXmlTextNode(node)) continue;
|
|
2147
|
+
const name = Epub.getXmlElementName(node);
|
|
2148
|
+
const isNavPoint = name === "navPoint" || name === "navpoint";
|
|
2149
|
+
if (!isNavPoint) continue;
|
|
2150
|
+
const children = Epub.getXmlChildren(node);
|
|
2151
|
+
const navLabel = Epub.findXmlChildByName("navLabel", children) ?? Epub.findXmlChildByName("navlabel", children);
|
|
2152
|
+
let title = null;
|
|
2153
|
+
if (navLabel) {
|
|
2154
|
+
const textEl = Epub.findXmlChildByName(
|
|
2155
|
+
"text",
|
|
2156
|
+
Epub.getXmlChildren(navLabel)
|
|
2157
|
+
);
|
|
2158
|
+
if (textEl) {
|
|
2159
|
+
title = Epub.getXhtmlTextContent(Epub.getXmlChildren(textEl)).trim() || null;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
const contentEl = Epub.findXmlChildByName("content", children);
|
|
2163
|
+
const src = (_a = contentEl == null ? void 0 : contentEl[":@"]) == null ? void 0 : _a["@_src"];
|
|
2164
|
+
const href = src ? await this.resolveHref(src, ncxHref) : null;
|
|
2165
|
+
const childEntries = await this.parseNavPoints(children, ncxHref);
|
|
2166
|
+
entries.push({
|
|
2167
|
+
title: title ?? `${entries.length}`,
|
|
2168
|
+
...href && { href },
|
|
2169
|
+
children: childEntries
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2172
|
+
return entries;
|
|
2173
|
+
}
|
|
2174
|
+
/**
|
|
2175
|
+
* Retrieve the guide entries from the package document.
|
|
2176
|
+
*
|
|
2177
|
+
* The guide element is deprecated in EPUB 3 in favor of
|
|
2178
|
+
* the landmarks nav, but many publications still include it.
|
|
2179
|
+
*/
|
|
2180
|
+
async getGuideEntries() {
|
|
2181
|
+
const packageElement = await this.getPackageElement();
|
|
2182
|
+
const guide = Epub.findXmlChildByName(
|
|
2183
|
+
"guide",
|
|
2184
|
+
Epub.getXmlChildren(packageElement)
|
|
2185
|
+
);
|
|
2186
|
+
if (!guide) return [];
|
|
2187
|
+
return Epub.getXmlChildren(guide).filter(
|
|
2188
|
+
(node) => !Epub.isXmlTextNode(node) && "reference" in node
|
|
2189
|
+
).map((ref) => {
|
|
2190
|
+
var _a, _b, _c;
|
|
2191
|
+
return {
|
|
2192
|
+
href: ((_a = ref[":@"]) == null ? void 0 : _a["@_href"]) ?? "",
|
|
2193
|
+
title: ((_b = ref[":@"]) == null ? void 0 : _b["@_title"]) ?? "",
|
|
2194
|
+
type: (((_c = ref[":@"]) == null ? void 0 : _c["@_type"]) ?? "").toLowerCase()
|
|
2195
|
+
};
|
|
2196
|
+
}).filter((entry) => entry.href);
|
|
2197
|
+
}
|
|
1870
2198
|
discardAndClose() {
|
|
1871
2199
|
this.rootfile = null;
|
|
1872
2200
|
this.manifest = null;
|
|
@@ -1923,7 +2251,8 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1923
2251
|
if (entry.name === "mimetype" || entry.isDirectory()) continue;
|
|
1924
2252
|
zipfile.addFile(
|
|
1925
2253
|
(0, import_path.join)(entry.parentPath, entry.name),
|
|
1926
|
-
(0, import_path.join)(entry.parentPath, entry.name).replace(`${this.extractPath}/`, "")
|
|
2254
|
+
(0, import_path.join)(entry.parentPath, entry.name).replace(`${this.extractPath}/`, ""),
|
|
2255
|
+
{ compress: !isAudioFile(entry.name) }
|
|
1927
2256
|
);
|
|
1928
2257
|
}
|
|
1929
2258
|
zipfile.end();
|
|
@@ -1936,11 +2265,76 @@ ${JSON.stringify(element, null, 2)}`
|
|
|
1936
2265
|
_promise && await _promise;
|
|
1937
2266
|
}
|
|
1938
2267
|
}
|
|
2268
|
+
/**
|
|
2269
|
+
* Upgrade an EPUB 2 publication to EPUB 3 in place, returning a new, valid Epub 3 instance.
|
|
2270
|
+
*
|
|
2271
|
+
* Performs the following transformations:
|
|
2272
|
+
* - upgrades OPF metadata to EPUB 3 conventions
|
|
2273
|
+
* - scans XHTML documents and adds manifest item properties
|
|
2274
|
+
* - parses the NCX into a TOC tree and generates a nav.xhtml
|
|
2275
|
+
* - removes the NCX file and the guide element (configurable)
|
|
2276
|
+
* - fixes common font MIME types
|
|
2277
|
+
* - bumps the package version to 3.0
|
|
2278
|
+
* - goes over each xhtml item and rewrites it using XMLParser to make sure the output is valid XHTML
|
|
2279
|
+
*/
|
|
2280
|
+
static async upgrade(path, options = {}) {
|
|
2281
|
+
var _a;
|
|
2282
|
+
const { removeNcx = false, outputPath } = options;
|
|
2283
|
+
if (outputPath) {
|
|
2284
|
+
await (0, import_promises.mkdir)((0, import_path.dirname)(outputPath), { recursive: true });
|
|
2285
|
+
await (0, import_promises.cp)(path, outputPath, { force: true });
|
|
2286
|
+
}
|
|
2287
|
+
const epub = await Epub.open(outputPath ?? path);
|
|
2288
|
+
const version = await epub.getVersion();
|
|
2289
|
+
if (version.startsWith("3.")) {
|
|
2290
|
+
return epub;
|
|
2291
|
+
}
|
|
2292
|
+
const tocEntries = await epub.getNcxTableOfContents();
|
|
2293
|
+
let landmarks = [];
|
|
2294
|
+
await epub.withPackage((pkg) => {
|
|
2295
|
+
landmarks = Upgrade.extractGuideLandmarks(pkg);
|
|
2296
|
+
Upgrade.upgradePackageMetadata(pkg);
|
|
2297
|
+
Upgrade.fixFontMimeTypes(pkg);
|
|
2298
|
+
Upgrade.removeGuide(pkg);
|
|
2299
|
+
if (removeNcx) {
|
|
2300
|
+
Upgrade.removeSpineTocRef(pkg);
|
|
2301
|
+
}
|
|
2302
|
+
Upgrade.setPackageVersion(pkg, "3.0");
|
|
2303
|
+
});
|
|
2304
|
+
await Upgrade.collectManifestProperties(epub);
|
|
2305
|
+
if (removeNcx) {
|
|
2306
|
+
await Upgrade.removeNcx(epub);
|
|
2307
|
+
}
|
|
2308
|
+
const navHref = await Upgrade.chooseNavHref(epub);
|
|
2309
|
+
const navContent = await Upgrade.buildNavDocument(
|
|
2310
|
+
epub,
|
|
2311
|
+
tocEntries,
|
|
2312
|
+
landmarks
|
|
2313
|
+
);
|
|
2314
|
+
await epub.addManifestItem(
|
|
2315
|
+
{
|
|
2316
|
+
id: "nav",
|
|
2317
|
+
href: navHref,
|
|
2318
|
+
mediaType: "application/xhtml+xml",
|
|
2319
|
+
properties: ["nav"]
|
|
2320
|
+
},
|
|
2321
|
+
navContent,
|
|
2322
|
+
"utf-8"
|
|
2323
|
+
);
|
|
2324
|
+
const manifest = await epub.getManifest();
|
|
2325
|
+
for (const item of Object.values(manifest)) {
|
|
2326
|
+
if (((_a = item.mediaType) == null ? void 0 : _a.toLowerCase()) !== "application/xhtml+xml") continue;
|
|
2327
|
+
const contents = await epub.readXhtmlItemContents(item.id);
|
|
2328
|
+
await epub.writeXhtmlItemContents(item.id, contents);
|
|
2329
|
+
}
|
|
2330
|
+
return epub;
|
|
2331
|
+
}
|
|
1939
2332
|
[Symbol.dispose]() {
|
|
1940
2333
|
this.discardAndClose();
|
|
1941
2334
|
}
|
|
1942
2335
|
}
|
|
1943
2336
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1944
2337
|
0 && (module.exports = {
|
|
1945
|
-
Epub
|
|
2338
|
+
Epub,
|
|
2339
|
+
EpubVersionError
|
|
1946
2340
|
});
|